From 481bab8d48a8a40544bdd8e77d6d92b746bdbed8 Mon Sep 17 00:00:00 2001 From: Tao Bao Date: Thu, 21 Dec 2017 11:23:09 -0800 Subject: [PATCH] releasetools: Add a BuildInfo class. Prior to this CL, we have similar codes in different paths (WriteFullOTAPackage / WriteBlockIncrementalOTAPackage / WriteABOTAPackageWithBrilloScript). This CL factors out the common codes that deal with OEM-specific properties into BuildInfo class, and adds tests for the new class. Test: python -m unittest test_ota_from_target_files Test: Create an incremental package that uses and doesn't use OEM properties respectively. Test: Create a full package that uses and doesn't use OEM properties respectively. Change-Id: Ida914cace12803d55396fa503bafcac2db2a520e --- tools/releasetools/edify_generator.py | 6 +- tools/releasetools/ota_from_target_files.py | 488 +++++++++++------- .../test_ota_from_target_files.py | 302 +++++++++++ 3 files changed, 593 insertions(+), 203 deletions(-) create mode 100644 tools/releasetools/test_ota_from_target_files.py diff --git a/tools/releasetools/edify_generator.py b/tools/releasetools/edify_generator.py index 0c44faf22..7a819288e 100644 --- a/tools/releasetools/edify_generator.py +++ b/tools/releasetools/edify_generator.py @@ -77,14 +77,14 @@ class EdifyGenerator(object): with temporary=True) to this one.""" self.script.extend(other.script) - def AssertOemProperty(self, name, values): + def AssertOemProperty(self, name, values, oem_no_mount): """Assert that a property on the OEM paritition matches allowed values.""" if not name: raise ValueError("must specify an OEM property") if not values: raise ValueError("must specify the OEM value") - get_prop_command = None - if common.OPTIONS.oem_no_mount: + + if oem_no_mount: get_prop_command = 'getprop("%s")' % name else: get_prop_command = 'file_getprop("/oem/oem.prop", "%s")' % name diff --git a/tools/releasetools/ota_from_target_files.py b/tools/releasetools/ota_from_target_files.py index d5ac9224a..1a4383c2b 100755 --- a/tools/releasetools/ota_from_target_files.py +++ b/tools/releasetools/ota_from_target_files.py @@ -49,8 +49,10 @@ Usage: ota_from_target_files [flags] input_target_files output_ota_package -o (--oem_settings) Comma seperated list of files used to specify the expected OEM-specific - properties on the OEM partition of the intended device. - Multiple expected values can be used by providing multiple files. + properties on the OEM partition of the intended device. Multiple expected + values can be used by providing multiple files. Only the first dict will + be used to compute fingerprint, while the rest will be used to assert + OEM-specific properties. --oem_no_mount For devices with OEM-specific properties but without an OEM partition, @@ -128,16 +130,11 @@ Usage: ota_from_target_files [flags] input_target_files output_ota_package from __future__ import print_function -import sys - -if sys.hexversion < 0x02070000: - print("Python 2.7 or newer is required.", file=sys.stderr) - sys.exit(1) - import multiprocessing import os.path -import subprocess import shlex +import subprocess +import sys import tempfile import zipfile @@ -145,6 +142,11 @@ import common import edify_generator import sparse_img +if sys.hexversion < 0x02070000: + print("Python 2.7 or newer is required.", file=sys.stderr) + sys.exit(1) + + OPTIONS = common.OPTIONS OPTIONS.package_key = None OPTIONS.incremental_source = None @@ -179,6 +181,136 @@ METADATA_NAME = 'META-INF/com/android/metadata' UNZIP_PATTERN = ['IMAGES/*', 'META/*'] +class BuildInfo(object): + """A class that holds the information for a given build. + + This class wraps up the property querying for a given source or target build. + It abstracts away the logic of handling OEM-specific properties, and caches + the commonly used properties such as fingerprint. + + There are two types of info dicts: a) build-time info dict, which is generated + at build time (i.e. included in a target_files zip); b) OEM info dict that is + specified at package generation time (via command line argument + '--oem_settings'). If a build doesn't use OEM-specific properties (i.e. not + having "oem_fingerprint_properties" in build-time info dict), all the queries + would be answered based on build-time info dict only. Otherwise if using + OEM-specific properties, some of them will be calculated from two info dicts. + + Users can query properties similarly as using a dict() (e.g. info['fstab']), + or to query build properties via GetBuildProp() or GetVendorBuildProp(). + + Attributes: + info_dict: The build-time info dict. + is_ab: Whether it's a build that uses A/B OTA. + oem_dicts: A list of OEM dicts. + oem_props: A list of OEM properties that should be read from OEM dicts; None + if the build doesn't use any OEM-specific property. + fingerprint: The fingerprint of the build, which would be calculated based + on OEM properties if applicable. + device: The device name, which could come from OEM dicts if applicable. + """ + + def __init__(self, info_dict, oem_dicts): + """Initializes a BuildInfo instance with the given dicts. + + Arguments: + info_dict: The build-time info dict. + oem_dicts: A list of OEM dicts (which is parsed from --oem_settings). Note + that it always uses the first dict to calculate the fingerprint or the + device name. The rest would be used for asserting OEM properties only + (e.g. one package can be installed on one of these devices). + """ + self.info_dict = info_dict + self.oem_dicts = oem_dicts + + self._is_ab = info_dict.get("ab_update") == "true" + self._oem_props = info_dict.get("oem_fingerprint_properties") + + if self._oem_props: + assert oem_dicts, "OEM source required for this build" + + # These two should be computed only after setting self._oem_props. + self._device = self.GetOemProperty("ro.product.device") + self._fingerprint = self.CalculateFingerprint() + + @property + def is_ab(self): + return self._is_ab + + @property + def device(self): + return self._device + + @property + def fingerprint(self): + return self._fingerprint + + @property + def oem_props(self): + return self._oem_props + + def __getitem__(self, key): + return self.info_dict[key] + + def get(self, key, default=None): + return self.info_dict.get(key, default) + + def GetBuildProp(self, prop): + """Returns the inquired build property.""" + try: + return self.info_dict.get("build.prop", {})[prop] + except KeyError: + raise common.ExternalError("couldn't find %s in build.prop" % (prop,)) + + def GetVendorBuildProp(self, prop): + """Returns the inquired vendor build property.""" + try: + return self.info_dict.get("vendor.build.prop", {})[prop] + except KeyError: + raise common.ExternalError( + "couldn't find %s in vendor.build.prop" % (prop,)) + + def GetOemProperty(self, key): + if self.oem_props is not None and key in self.oem_props: + return self.oem_dicts[0][key] + return self.GetBuildProp(key) + + def CalculateFingerprint(self): + if self.oem_props is None: + return self.GetBuildProp("ro.build.fingerprint") + return "%s/%s/%s:%s" % ( + self.GetOemProperty("ro.product.brand"), + self.GetOemProperty("ro.product.name"), + self.GetOemProperty("ro.product.device"), + self.GetBuildProp("ro.build.thumbprint")) + + def WriteMountOemScript(self, script): + assert self.oem_props is not None + recovery_mount_options = self.info_dict.get("recovery_mount_options") + script.Mount("/oem", recovery_mount_options) + + def WriteDeviceAssertions(self, script, oem_no_mount): + # Read the property directly if not using OEM properties. + if not self.oem_props: + script.AssertDevice(self.device) + return + + # Otherwise assert OEM properties. + if not self.oem_dicts: + raise common.ExternalError( + "No OEM file provided to answer expected assertions") + + for prop in self.oem_props.split(): + values = [] + for oem_dict in self.oem_dicts: + if prop in oem_dict: + values.append(oem_dict[prop]) + if not values: + raise common.ExternalError( + "The OEM file is missing the property %s" % (prop,)) + script.AssertOemProperty(prop, values, oem_no_mount) + + def SignOutput(temp_zip_name, output_zip_name): pw = OPTIONS.key_passwords[OPTIONS.package_key] @@ -186,37 +318,15 @@ def SignOutput(temp_zip_name, output_zip_name): whole_file=True) -def AppendAssertions(script, info_dict, oem_dicts=None): - oem_props = info_dict.get("oem_fingerprint_properties") - if not oem_props: - device = GetBuildProp("ro.product.device", info_dict) - script.AssertDevice(device) - else: - if not oem_dicts: - raise common.ExternalError( - "No OEM file provided to answer expected assertions") - for prop in oem_props.split(): - values = [] - for oem_dict in oem_dicts: - if oem_dict.get(prop): - values.append(oem_dict[prop]) - if not values: - raise common.ExternalError( - "The OEM file is missing the property %s" % prop) - script.AssertOemProperty(prop, values) - - -def _LoadOemDicts(script, recovery_mount_options=None): +def _LoadOemDicts(oem_source): """Returns the list of loaded OEM properties dict.""" - oem_dicts = None - if OPTIONS.oem_source is None: - raise common.ExternalError("OEM source required for this build") - if not OPTIONS.oem_no_mount and script: - script.Mount("/oem", recovery_mount_options) + if not oem_source: + return None + oem_dicts = [] - for oem_file in OPTIONS.oem_source: - oem_dicts.append(common.LoadDictionaryFromLines( - open(oem_file).readlines())) + for oem_file in oem_source: + with open(oem_file) as fp: + oem_dicts.append(common.LoadDictionaryFromLines(fp.readlines())) return oem_dicts @@ -267,25 +377,30 @@ def HasVendorPartition(target_files_zip): return False -def HasTrebleEnabled(target_files_zip, info_dict): +def HasTrebleEnabled(target_files_zip, target_info): return (HasVendorPartition(target_files_zip) and - GetBuildProp("ro.treble.enabled", info_dict) == "true") + target_info.GetBuildProp("ro.treble.enabled") == "true") -def GetOemProperty(name, oem_props, oem_dict, info_dict): - if oem_props is not None and name in oem_props: - return oem_dict[name] - return GetBuildProp(name, info_dict) +def WriteFingerprintAssertion(script, target_info, source_info): + source_oem_props = source_info.oem_props + target_oem_props = target_info.oem_props - -def CalculateFingerprint(oem_props, oem_dict, info_dict): - if oem_props is None: - return GetBuildProp("ro.build.fingerprint", info_dict) - return "%s/%s/%s:%s" % ( - GetOemProperty("ro.product.brand", oem_props, oem_dict, info_dict), - GetOemProperty("ro.product.name", oem_props, oem_dict, info_dict), - GetOemProperty("ro.product.device", oem_props, oem_dict, info_dict), - GetBuildProp("ro.build.thumbprint", info_dict)) + if source_oem_props is None and target_oem_props is None: + script.AssertSomeFingerprint( + source_info.fingerprint, target_info.fingerprint) + elif source_oem_props is not None and target_oem_props is not None: + script.AssertSomeThumbprint( + target_info.GetBuildProp("ro.build.thumbprint"), + source_info.GetBuildProp("ro.build.thumbprint")) + elif source_oem_props is None and target_oem_props is not None: + script.AssertFingerprintOrThumbprint( + source_info.fingerprint, + target_info.GetBuildProp("ro.build.thumbprint")) + else: + script.AssertFingerprintOrThumbprint( + target_info.fingerprint, + source_info.GetBuildProp("ro.build.thumbprint")) def GetImage(which, tmpdir): @@ -313,9 +428,8 @@ def GetImage(which, tmpdir): return sparse_img.SparseImage(path, mappath, clobbered_blocks) -def AddCompatibilityArchiveIfTrebleEnabled(target_zip, output_zip, - target_info_dict, - source_info_dict=None): +def AddCompatibilityArchiveIfTrebleEnabled(target_zip, output_zip, target_info, + source_info=None): """Adds compatibility info into the output zip if it's Treble-enabled target. Metadata used for on-device compatibility verification is retrieved from @@ -328,9 +442,9 @@ def AddCompatibilityArchiveIfTrebleEnabled(target_zip, output_zip, Args: target_zip: Zip file containing the source files to be included for OTA. output_zip: Zip file that will be sent for OTA. - target_info_dict: The dict that holds the target build info. - source_info_dict: The dict that holds the source build info, if generating - an incremental OTA; None otherwise. + target_info: The BuildInfo instance that holds the target build info. + source_info: The BuildInfo instance that holds the source build info, if + generating an incremental OTA; None otherwise. """ def AddCompatibilityArchive(system_updated, vendor_updated): @@ -353,8 +467,8 @@ def AddCompatibilityArchiveIfTrebleEnabled(target_zip, output_zip, # Create new archive. compatibility_archive = tempfile.NamedTemporaryFile() - compatibility_archive_zip = zipfile.ZipFile(compatibility_archive, "w", - compression=zipfile.ZIP_DEFLATED) + compatibility_archive_zip = zipfile.ZipFile( + compatibility_archive, "w", compression=zipfile.ZIP_DEFLATED) # Add metadata. for file_name in compatibility_files: @@ -375,59 +489,55 @@ def AddCompatibilityArchiveIfTrebleEnabled(target_zip, output_zip, # Will only proceed if the target has enabled the Treble support (as well as # having a /vendor partition). - if not HasTrebleEnabled(target_zip, target_info_dict): + if not HasTrebleEnabled(target_zip, target_info): return # We don't support OEM thumbprint in Treble world (which calculates # fingerprints in a different way as shown in CalculateFingerprint()). - assert not target_info_dict.get("oem_fingerprint_properties") + assert not target_info.oem_props # Full OTA carries the info for system/vendor both. - if source_info_dict is None: + if source_info is None: AddCompatibilityArchive(True, True) return - assert not source_info_dict.get("oem_fingerprint_properties") + assert not source_info.oem_props - source_fp = GetBuildProp("ro.build.fingerprint", source_info_dict) - target_fp = GetBuildProp("ro.build.fingerprint", target_info_dict) + source_fp = source_info.fingerprint + target_fp = target_info.fingerprint system_updated = source_fp != target_fp - source_fp_vendor = GetVendorBuildProp("ro.vendor.build.fingerprint", - source_info_dict) - target_fp_vendor = GetVendorBuildProp("ro.vendor.build.fingerprint", - target_info_dict) + source_fp_vendor = source_info.GetVendorBuildProp( + "ro.vendor.build.fingerprint") + target_fp_vendor = target_info.GetVendorBuildProp( + "ro.vendor.build.fingerprint") vendor_updated = source_fp_vendor != target_fp_vendor AddCompatibilityArchive(system_updated, vendor_updated) def WriteFullOTAPackage(input_zip, output_zip): - # TODO: how to determine this? We don't know what version it will - # be installed on top of. For now, we expect the API just won't - # change very often. Similarly for fstab, it might have changed - # in the target build. - script = edify_generator.EdifyGenerator(3, OPTIONS.info_dict) + target_info = BuildInfo(OPTIONS.info_dict, OPTIONS.oem_dicts) - oem_props = OPTIONS.info_dict.get("oem_fingerprint_properties") - oem_dicts = None - if oem_props: - recovery_mount_options = OPTIONS.info_dict.get("recovery_mount_options") - oem_dicts = _LoadOemDicts(script, recovery_mount_options) + # We don't know what version it will be installed on top of. We expect the API + # just won't change very often. Similarly for fstab, it might have changed in + # the target build. + target_api_version = target_info["recovery_api_version"] + script = edify_generator.EdifyGenerator(target_api_version, target_info) + + if target_info.oem_props and not OPTIONS.oem_no_mount: + target_info.WriteMountOemScript(script) - target_fp = CalculateFingerprint(oem_props, oem_dicts and oem_dicts[0], - OPTIONS.info_dict) metadata = { - "post-build": target_fp, - "pre-device": GetOemProperty("ro.product.device", oem_props, - oem_dicts and oem_dicts[0], - OPTIONS.info_dict), - "post-timestamp": GetBuildProp("ro.build.date.utc", OPTIONS.info_dict), + "post-build": target_info.fingerprint, + "pre-device": target_info.device, + "post-timestamp": target_info.GetBuildProp("ro.build.date.utc"), + "ota-type" : "BLOCK", } device_specific = common.DeviceSpecificParams( input_zip=input_zip, - input_version=OPTIONS.info_dict["recovery_api_version"], + input_version=target_api_version, output_zip=output_zip, script=script, input_tmp=OPTIONS.input_tmp, @@ -436,13 +546,12 @@ def WriteFullOTAPackage(input_zip, output_zip): assert HasRecoveryPatch(input_zip) - metadata["ota-type"] = "BLOCK" - - ts = GetBuildProp("ro.build.date.utc", OPTIONS.info_dict) - ts_text = GetBuildProp("ro.build.date", OPTIONS.info_dict) + # Assertions (e.g. downgrade check, device properties check). + ts = target_info.GetBuildProp("ro.build.date.utc") + ts_text = target_info.GetBuildProp("ro.build.date") script.AssertOlderBuild(ts, ts_text) - AppendAssertions(script, OPTIONS.info_dict, oem_dicts) + target_info.WriteDeviceAssertions(script, OPTIONS.oem_no_mount) device_specific.FullOTA_Assertions() # Two-step package strategy (in chronological order, which is *not* @@ -468,9 +577,9 @@ def WriteFullOTAPackage(input_zip, output_zip): recovery_img = common.GetBootableImage("recovery.img", "recovery.img", OPTIONS.input_tmp, "RECOVERY") if OPTIONS.two_step: - if not OPTIONS.info_dict.get("multistage_support", None): + if not target_info.get("multistage_support"): assert False, "two-step packages not supported by this build" - fs = OPTIONS.info_dict["fstab"]["/misc"] + fs = target_info["fstab"]["/misc"] assert fs.fs_type.upper() == "EMMC", \ "two-step packages only supported on devices with EMMC /misc partitions" bcb_dev = {"bcb_dev": fs.device} @@ -492,7 +601,7 @@ else if get_stage("%(bcb_dev)s") == "3/3" then script.Comment("Stage 3/3") # Dump fingerprints - script.Print("Target: %s" % target_fp) + script.Print("Target: {}".format(target_info.fingerprint)) device_specific.FullOTA_InstallBegin() @@ -525,10 +634,9 @@ else if get_stage("%(bcb_dev)s") == "3/3" then vendor_diff = common.BlockDifference("vendor", vendor_tgt) vendor_diff.WriteScript(script, output_zip) - AddCompatibilityArchiveIfTrebleEnabled(input_zip, output_zip, - OPTIONS.info_dict) + AddCompatibilityArchiveIfTrebleEnabled(input_zip, output_zip, target_info) - common.CheckSize(boot_img.data, "boot.img", OPTIONS.info_dict) + common.CheckSize(boot_img.data, "boot.img", target_info) common.ZipWriteStr(output_zip, "boot.img", boot_img.data) script.ShowProgress(0.05, 5) @@ -592,12 +700,12 @@ def GetVendorBuildProp(prop, info_dict): "couldn't find %s in vendor.build.prop" % (prop,)) -def HandleDowngradeMetadata(metadata): +def HandleDowngradeMetadata(metadata, target_info, source_info): # Only incremental OTAs are allowed to reach here. assert OPTIONS.incremental_source is not None - post_timestamp = GetBuildProp("ro.build.date.utc", OPTIONS.target_info_dict) - pre_timestamp = GetBuildProp("ro.build.date.utc", OPTIONS.source_info_dict) + post_timestamp = target_info.GetBuildProp("ro.build.date.utc") + pre_timestamp = source_info.GetBuildProp("ro.build.date.utc") is_downgrade = long(post_timestamp) < long(pre_timestamp) if OPTIONS.downgrade: @@ -607,72 +715,65 @@ def HandleDowngradeMetadata(metadata): metadata["ota-downgrade"] = "yes" elif OPTIONS.timestamp: if not is_downgrade: - raise RuntimeError("--timestamp specified but no timestamp hack needed: " - "pre: %s, post: %s" % (pre_timestamp, post_timestamp)) + raise RuntimeError("--override_timestamp specified but no timestamp hack " + "needed: pre: %s, post: %s" % (pre_timestamp, + post_timestamp)) metadata["post-timestamp"] = str(long(pre_timestamp) + 1) else: if is_downgrade: raise RuntimeError("Downgrade detected based on timestamp check: " - "pre: %s, post: %s. Need to specify --timestamp OR " - "--downgrade to allow building the incremental." % ( - pre_timestamp, post_timestamp)) + "pre: %s, post: %s. Need to specify " + "--override_timestamp OR --downgrade to allow " + "building the incremental." % (pre_timestamp, + post_timestamp)) metadata["post-timestamp"] = post_timestamp def WriteBlockIncrementalOTAPackage(target_zip, source_zip, output_zip): - source_version = OPTIONS.source_info_dict["recovery_api_version"] - target_version = OPTIONS.target_info_dict["recovery_api_version"] + target_info = BuildInfo(OPTIONS.target_info_dict, OPTIONS.oem_dicts) + source_info = BuildInfo(OPTIONS.source_info_dict, OPTIONS.oem_dicts) - if source_version == 0: + target_api_version = target_info["recovery_api_version"] + source_api_version = source_info["recovery_api_version"] + if source_api_version == 0: print("WARNING: generating edify script for a source that " "can't install it.") - script = edify_generator.EdifyGenerator( - source_version, OPTIONS.target_info_dict, - fstab=OPTIONS.source_info_dict["fstab"]) - source_oem_props = OPTIONS.source_info_dict.get("oem_fingerprint_properties") - target_oem_props = OPTIONS.target_info_dict.get("oem_fingerprint_properties") - oem_dicts = None - if source_oem_props or target_oem_props: - recovery_mount_options = OPTIONS.source_info_dict.get( - "recovery_mount_options") - oem_dicts = _LoadOemDicts(script, recovery_mount_options) + script = edify_generator.EdifyGenerator( + source_api_version, target_info, fstab=source_info["fstab"]) + + if target_info.oem_props or source_info.oem_props: + if not OPTIONS.oem_no_mount: + source_info.WriteMountOemScript(script) metadata = { - "pre-device": GetOemProperty("ro.product.device", source_oem_props, - oem_dicts and oem_dicts[0], - OPTIONS.source_info_dict), + "pre-device": source_info.device, "ota-type": "BLOCK", } - HandleDowngradeMetadata(metadata) + HandleDowngradeMetadata(metadata, target_info, source_info) device_specific = common.DeviceSpecificParams( source_zip=source_zip, - source_version=source_version, + source_version=source_api_version, target_zip=target_zip, - target_version=target_version, + target_version=target_api_version, output_zip=output_zip, script=script, metadata=metadata, - info_dict=OPTIONS.source_info_dict) + info_dict=source_info) - source_fp = CalculateFingerprint(source_oem_props, oem_dicts and oem_dicts[0], - OPTIONS.source_info_dict) - target_fp = CalculateFingerprint(target_oem_props, oem_dicts and oem_dicts[0], - OPTIONS.target_info_dict) - metadata["pre-build"] = source_fp - metadata["post-build"] = target_fp - metadata["pre-build-incremental"] = GetBuildProp( - "ro.build.version.incremental", OPTIONS.source_info_dict) - metadata["post-build-incremental"] = GetBuildProp( - "ro.build.version.incremental", OPTIONS.target_info_dict) + metadata["pre-build"] = source_info.fingerprint + metadata["post-build"] = target_info.fingerprint + metadata["pre-build-incremental"] = source_info.GetBuildProp( + "ro.build.version.incremental") + metadata["post-build-incremental"] = target_info.GetBuildProp( + "ro.build.version.incremental") source_boot = common.GetBootableImage( - "/tmp/boot.img", "boot.img", OPTIONS.source_tmp, "BOOT", - OPTIONS.source_info_dict) + "/tmp/boot.img", "boot.img", OPTIONS.source_tmp, "BOOT", source_info) target_boot = common.GetBootableImage( - "/tmp/boot.img", "boot.img", OPTIONS.target_tmp, "BOOT") + "/tmp/boot.img", "boot.img", OPTIONS.target_tmp, "BOOT", target_info) updating_boot = (not OPTIONS.two_step and (source_boot.data != target_boot.data)) @@ -683,19 +784,18 @@ def WriteBlockIncrementalOTAPackage(target_zip, source_zip, output_zip): system_tgt = GetImage("system", OPTIONS.target_tmp) blockimgdiff_version = max( - int(i) for i in - OPTIONS.info_dict.get("blockimgdiff_versions", "1").split(",")) + int(i) for i in target_info.get("blockimgdiff_versions", "1").split(",")) assert blockimgdiff_version >= 3 # Check the first block of the source system partition for remount R/W only # if the filesystem is ext4. - system_src_partition = OPTIONS.source_info_dict["fstab"]["/system"] + system_src_partition = source_info["fstab"]["/system"] check_first_block = system_src_partition.fs_type == "ext4" # Disable using imgdiff for squashfs. 'imgdiff -z' expects input files to be # in zip formats. However with squashfs, a) all files are compressed in LZ4; # b) the blocks listed in block map may not contain all the bytes for a given # file (because they're rounded to be 4K-aligned). - system_tgt_partition = OPTIONS.target_info_dict["fstab"]["/system"] + system_tgt_partition = target_info["fstab"]["/system"] disable_imgdiff = (system_src_partition.fs_type == "squashfs" or system_tgt_partition.fs_type == "squashfs") system_diff = common.BlockDifference("system", system_tgt, system_src, @@ -711,7 +811,7 @@ def WriteBlockIncrementalOTAPackage(target_zip, source_zip, output_zip): # Check first block of vendor partition for remount R/W only if # disk type is ext4 - vendor_partition = OPTIONS.source_info_dict["fstab"]["/vendor"] + vendor_partition = source_info["fstab"]["/vendor"] check_first_block = vendor_partition.fs_type == "ext4" disable_imgdiff = vendor_partition.fs_type == "squashfs" vendor_diff = common.BlockDifference("vendor", vendor_tgt, vendor_src, @@ -722,10 +822,10 @@ def WriteBlockIncrementalOTAPackage(target_zip, source_zip, output_zip): vendor_diff = None AddCompatibilityArchiveIfTrebleEnabled( - target_zip, output_zip, OPTIONS.target_info_dict, - OPTIONS.source_info_dict) + target_zip, output_zip, target_info, source_info) - AppendAssertions(script, OPTIONS.target_info_dict, oem_dicts) + # Assertions (e.g. device properties check). + target_info.WriteDeviceAssertions(script, OPTIONS.oem_no_mount) device_specific.IncrementalOTA_Assertions() # Two-step incremental package strategy (in chronological order, @@ -751,12 +851,12 @@ def WriteBlockIncrementalOTAPackage(target_zip, source_zip, output_zip): # (allow recovery to mark itself finished and reboot) if OPTIONS.two_step: - if not OPTIONS.source_info_dict.get("multistage_support", None): + if not source_info.get("multistage_support"): assert False, "two-step packages not supported by this build" fs = OPTIONS.source_info_dict["fstab"]["/misc"] assert fs.fs_type.upper() == "EMMC", \ "two-step packages only supported on devices with EMMC /misc partitions" - bcb_dev = {"bcb_dev": fs.device} + bcb_dev = {"bcb_dev" : fs.device} common.ZipWriteStr(output_zip, "recovery.img", target_recovery.data) script.AppendExtra(""" if get_stage("%(bcb_dev)s") == "2/3" then @@ -776,27 +876,14 @@ else if get_stage("%(bcb_dev)s") != "3/3" then script.Comment("Stage 1/3") # Dump fingerprints - script.Print("Source: %s" % (source_fp,)) - script.Print("Target: %s" % (target_fp,)) + script.Print("Source: {}".format(source_info.fingerprint)) + script.Print("Target: {}".format(target_info.fingerprint)) script.Print("Verifying current system...") device_specific.IncrementalOTA_VerifyBegin() - if source_oem_props is None and target_oem_props is None: - script.AssertSomeFingerprint(source_fp, target_fp) - elif source_oem_props is not None and target_oem_props is not None: - script.AssertSomeThumbprint( - GetBuildProp("ro.build.thumbprint", OPTIONS.target_info_dict), - GetBuildProp("ro.build.thumbprint", OPTIONS.source_info_dict)) - elif source_oem_props is None and target_oem_props is not None: - script.AssertFingerprintOrThumbprint( - source_fp, - GetBuildProp("ro.build.thumbprint", OPTIONS.target_info_dict)) - else: - script.AssertFingerprintOrThumbprint( - target_fp, - GetBuildProp("ro.build.thumbprint", OPTIONS.source_info_dict)) + WriteFingerprintAssertion(script, target_info, source_info) # Check the required cache size (i.e. stashed blocks). size = [] @@ -806,8 +893,7 @@ else if get_stage("%(bcb_dev)s") != "3/3" then size.append(vendor_diff.required_cache) if updating_boot: - boot_type, boot_device = common.GetTypeAndDevice( - "/boot", OPTIONS.source_info_dict) + boot_type, boot_device = common.GetTypeAndDevice("/boot", source_info) d = common.Difference(target_boot, source_boot) _, _, d = d.ComputePatch() if d is None: @@ -984,7 +1070,8 @@ def WriteABOTAPackageWithBrilloScript(target_file, output_file, cmd.extend(["-passin", "pass:" + pw] if pw else ["-nocrypt"]) rsa_key = common.MakeTempFile(prefix="key-", suffix=".key") cmd.extend(["-out", rsa_key]) - p1 = common.Run(cmd, verbose=False, stdout=log_file, stderr=subprocess.STDOUT) + p1 = common.Run(cmd, verbose=False, stdout=log_file, + stderr=subprocess.STDOUT) p1.communicate() assert p1.returncode == 0, "openssl pkcs8 failed" @@ -993,35 +1080,32 @@ def WriteABOTAPackageWithBrilloScript(target_file, output_file, output_zip = zipfile.ZipFile(temp_zip_file, "w", compression=zipfile.ZIP_DEFLATED) - # Metadata to comply with Android OTA package format. - oem_props = OPTIONS.info_dict.get("oem_fingerprint_properties", None) - oem_dicts = None - if oem_props: - oem_dicts = _LoadOemDicts(None) + if source_file is not None: + target_info = BuildInfo(OPTIONS.target_info_dict, OPTIONS.oem_dicts) + source_info = BuildInfo(OPTIONS.source_info_dict, OPTIONS.oem_dicts) + else: + target_info = BuildInfo(OPTIONS.info_dict, OPTIONS.oem_dicts) + source_info = None + # Metadata to comply with Android OTA package format. metadata = { - "post-build": CalculateFingerprint(oem_props, oem_dicts and oem_dicts[0], - OPTIONS.info_dict), - "post-build-incremental" : GetBuildProp("ro.build.version.incremental", - OPTIONS.info_dict), - "pre-device": GetOemProperty("ro.product.device", oem_props, - oem_dicts and oem_dicts[0], - OPTIONS.info_dict), - "ota-required-cache": "0", - "ota-type": "AB", + "post-build" : target_info.fingerprint, + "post-build-incremental" : target_info.GetBuildProp( + "ro.build.version.incremental"), + "ota-required-cache" : "0", + "ota-type" : "AB", } if source_file is not None: - metadata["pre-build"] = CalculateFingerprint(oem_props, - oem_dicts and oem_dicts[0], - OPTIONS.source_info_dict) - metadata["pre-build-incremental"] = GetBuildProp( - "ro.build.version.incremental", OPTIONS.source_info_dict) + metadata["pre-device"] = source_info.device + metadata["pre-build"] = source_info.fingerprint + metadata["pre-build-incremental"] = source_info.GetBuildProp( + "ro.build.version.incremental") - HandleDowngradeMetadata(metadata) + HandleDowngradeMetadata(metadata, target_info, source_info) else: - metadata["post-timestamp"] = GetBuildProp( - "ro.build.date.utc", OPTIONS.info_dict) + metadata["pre-device"] = target_info.device + metadata["post-timestamp"] = target_info.GetBuildProp("ro.build.date.utc") # 1. Generate payload. payload_file = common.MakeTempFile(prefix="payload-", suffix=".bin") @@ -1120,23 +1204,23 @@ def WriteABOTAPackageWithBrilloScript(target_file, output_file, # If dm-verity is supported for the device, copy contents of care_map # into A/B OTA package. target_zip = zipfile.ZipFile(target_file, "r") - if (OPTIONS.info_dict.get("verity") == "true" or - OPTIONS.info_dict.get("avb_enable") == "true"): + if (target_info.get("verity") == "true" or + target_info.get("avb_enable") == "true"): care_map_path = "META/care_map.txt" namelist = target_zip.namelist() if care_map_path in namelist: care_map_data = target_zip.read(care_map_path) common.ZipWriteStr(output_zip, "care_map.txt", care_map_data, - compress_type=zipfile.ZIP_STORED) + compress_type=zipfile.ZIP_STORED) else: print("Warning: cannot find care map file in target_file package") - # OPTIONS.source_info_dict must be None for incrementals. + # source_info must be None for full OTAs. if source_file is None: - assert OPTIONS.source_info_dict is None + assert source_info is None AddCompatibilityArchiveIfTrebleEnabled( - target_zip, output_zip, OPTIONS.info_dict, OPTIONS.source_info_dict) + target_zip, output_zip, target_info, source_info) common.ZipClose(target_zip) @@ -1289,12 +1373,15 @@ def main(argv): # Load the dict file from the zip directly to have a peek at the OTA type. # For packages using A/B update, unzipping is not needed. if OPTIONS.extracted_input is not None: - OPTIONS.info_dict = common.LoadInfoDict(OPTIONS.extracted_input, OPTIONS.extracted_input) + OPTIONS.info_dict = common.LoadInfoDict(OPTIONS.extracted_input, + OPTIONS.extracted_input) else: input_zip = zipfile.ZipFile(args[0], "r") OPTIONS.info_dict = common.LoadInfoDict(input_zip) common.ZipClose(input_zip) + OPTIONS.oem_dicts = _LoadOemDicts(OPTIONS.oem_source) + ab_update = OPTIONS.info_dict.get("ab_update") == "true" # Use the default key to sign the package if not specified with package_key. @@ -1337,7 +1424,8 @@ def main(argv): if OPTIONS.extracted_input is not None: OPTIONS.input_tmp = OPTIONS.extracted_input OPTIONS.target_tmp = OPTIONS.input_tmp - OPTIONS.info_dict = common.LoadInfoDict(OPTIONS.input_tmp, OPTIONS.input_tmp) + OPTIONS.info_dict = common.LoadInfoDict(OPTIONS.input_tmp, + OPTIONS.input_tmp) input_zip = zipfile.ZipFile(args[0], "r") else: print("unzipping target target-files...") diff --git a/tools/releasetools/test_ota_from_target_files.py b/tools/releasetools/test_ota_from_target_files.py new file mode 100644 index 000000000..0948c61b8 --- /dev/null +++ b/tools/releasetools/test_ota_from_target_files.py @@ -0,0 +1,302 @@ +# +# Copyright (C) 2018 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import copy +import unittest + +import common +from ota_from_target_files import ( + _LoadOemDicts, BuildInfo, WriteFingerprintAssertion) + + +class MockScriptWriter(object): + """A class that mocks edify_generator.EdifyGenerator. + + It simply pushes the incoming arguments onto script stack, which is to assert + the calls to EdifyGenerator functions. + """ + + def __init__(self): + self.script = [] + + def Mount(self, *args): + self.script.append(('Mount',) + args) + + def AssertDevice(self, *args): + self.script.append(('AssertDevice',) + args) + + def AssertOemProperty(self, *args): + self.script.append(('AssertOemProperty',) + args) + + def AssertFingerprintOrThumbprint(self, *args): + self.script.append(('AssertFingerprintOrThumbprint',) + args) + + def AssertSomeFingerprint(self, *args): + self.script.append(('AssertSomeFingerprint',) + args) + + def AssertSomeThumbprint(self, *args): + self.script.append(('AssertSomeThumbprint',) + args) + + +class BuildInfoTest(unittest.TestCase): + + TEST_INFO_DICT = { + 'build.prop' : { + 'ro.product.device' : 'product-device', + 'ro.product.name' : 'product-name', + 'ro.build.fingerprint' : 'build-fingerprint', + 'ro.build.foo' : 'build-foo', + }, + 'vendor.build.prop' : { + 'ro.vendor.build.fingerprint' : 'vendor-build-fingerprint', + }, + 'property1' : 'value1', + 'property2' : 4096, + } + + TEST_INFO_DICT_USES_OEM_PROPS = { + 'build.prop' : { + 'ro.product.name' : 'product-name', + 'ro.build.thumbprint' : 'build-thumbprint', + 'ro.build.bar' : 'build-bar', + }, + 'vendor.build.prop' : { + 'ro.vendor.build.fingerprint' : 'vendor-build-fingerprint', + }, + 'property1' : 'value1', + 'property2' : 4096, + 'oem_fingerprint_properties' : 'ro.product.device ro.product.brand', + } + + TEST_OEM_DICTS = [ + { + 'ro.product.brand' : 'brand1', + 'ro.product.device' : 'device1', + }, + { + 'ro.product.brand' : 'brand2', + 'ro.product.device' : 'device2', + }, + { + 'ro.product.brand' : 'brand3', + 'ro.product.device' : 'device3', + }, + ] + + def test_init(self): + target_info = BuildInfo(self.TEST_INFO_DICT, None) + self.assertEqual('product-device', target_info.device) + self.assertEqual('build-fingerprint', target_info.fingerprint) + self.assertFalse(target_info.is_ab) + self.assertIsNone(target_info.oem_props) + + def test_init_with_oem_props(self): + target_info = BuildInfo(self.TEST_INFO_DICT_USES_OEM_PROPS, + self.TEST_OEM_DICTS) + self.assertEqual('device1', target_info.device) + self.assertEqual('brand1/product-name/device1:build-thumbprint', + target_info.fingerprint) + + # Swap the order in oem_dicts, which would lead to different BuildInfo. + oem_dicts = copy.copy(self.TEST_OEM_DICTS) + oem_dicts[0], oem_dicts[2] = oem_dicts[2], oem_dicts[0] + target_info = BuildInfo(self.TEST_INFO_DICT_USES_OEM_PROPS, oem_dicts) + self.assertEqual('device3', target_info.device) + self.assertEqual('brand3/product-name/device3:build-thumbprint', + target_info.fingerprint) + + # Missing oem_dict should be rejected. + self.assertRaises(AssertionError, BuildInfo, + self.TEST_INFO_DICT_USES_OEM_PROPS, None) + + def test___getitem__(self): + target_info = BuildInfo(self.TEST_INFO_DICT, None) + self.assertEqual('value1', target_info['property1']) + self.assertEqual(4096, target_info['property2']) + self.assertEqual('build-foo', target_info['build.prop']['ro.build.foo']) + + def test___getitem__with_oem_props(self): + target_info = BuildInfo(self.TEST_INFO_DICT_USES_OEM_PROPS, + self.TEST_OEM_DICTS) + self.assertEqual('value1', target_info['property1']) + self.assertEqual(4096, target_info['property2']) + self.assertRaises(KeyError, + lambda: target_info['build.prop']['ro.build.foo']) + + def test_get(self): + target_info = BuildInfo(self.TEST_INFO_DICT, None) + self.assertEqual('value1', target_info.get('property1')) + self.assertEqual(4096, target_info.get('property2')) + self.assertEqual(4096, target_info.get('property2', 1024)) + self.assertEqual(1024, target_info.get('property-nonexistent', 1024)) + self.assertEqual('build-foo', target_info.get('build.prop')['ro.build.foo']) + + def test_get_with_oem_props(self): + target_info = BuildInfo(self.TEST_INFO_DICT_USES_OEM_PROPS, + self.TEST_OEM_DICTS) + self.assertEqual('value1', target_info.get('property1')) + self.assertEqual(4096, target_info.get('property2')) + self.assertEqual(4096, target_info.get('property2', 1024)) + self.assertEqual(1024, target_info.get('property-nonexistent', 1024)) + self.assertIsNone(target_info.get('build.prop').get('ro.build.foo')) + self.assertRaises(KeyError, + lambda: target_info.get('build.prop')['ro.build.foo']) + + def test_GetBuildProp(self): + target_info = BuildInfo(self.TEST_INFO_DICT, None) + self.assertEqual('build-foo', target_info.GetBuildProp('ro.build.foo')) + self.assertRaises(common.ExternalError, target_info.GetBuildProp, + 'ro.build.nonexistent') + + def test_GetBuildProp_with_oem_props(self): + target_info = BuildInfo(self.TEST_INFO_DICT_USES_OEM_PROPS, + self.TEST_OEM_DICTS) + self.assertEqual('build-bar', target_info.GetBuildProp('ro.build.bar')) + self.assertRaises(common.ExternalError, target_info.GetBuildProp, + 'ro.build.nonexistent') + + def test_GetVendorBuildProp(self): + target_info = BuildInfo(self.TEST_INFO_DICT, None) + self.assertEqual('vendor-build-fingerprint', + target_info.GetVendorBuildProp( + 'ro.vendor.build.fingerprint')) + self.assertRaises(common.ExternalError, target_info.GetVendorBuildProp, + 'ro.build.nonexistent') + + def test_GetVendorBuildProp_with_oem_props(self): + target_info = BuildInfo(self.TEST_INFO_DICT_USES_OEM_PROPS, + self.TEST_OEM_DICTS) + self.assertEqual('vendor-build-fingerprint', + target_info.GetVendorBuildProp( + 'ro.vendor.build.fingerprint')) + self.assertRaises(common.ExternalError, target_info.GetVendorBuildProp, + 'ro.build.nonexistent') + + def test_WriteMountOemScript(self): + target_info = BuildInfo(self.TEST_INFO_DICT_USES_OEM_PROPS, + self.TEST_OEM_DICTS) + script_writer = MockScriptWriter() + target_info.WriteMountOemScript(script_writer) + self.assertEqual([('Mount', '/oem', None)], script_writer.script) + + def test_WriteDeviceAssertions(self): + target_info = BuildInfo(self.TEST_INFO_DICT, None) + script_writer = MockScriptWriter() + target_info.WriteDeviceAssertions(script_writer, False) + self.assertEqual([('AssertDevice', 'product-device')], script_writer.script) + + def test_WriteDeviceAssertions_with_oem_props(self): + target_info = BuildInfo(self.TEST_INFO_DICT_USES_OEM_PROPS, + self.TEST_OEM_DICTS) + script_writer = MockScriptWriter() + target_info.WriteDeviceAssertions(script_writer, False) + self.assertEqual( + [ + ('AssertOemProperty', 'ro.product.device', + ['device1', 'device2', 'device3'], False), + ('AssertOemProperty', 'ro.product.brand', + ['brand1', 'brand2', 'brand3'], False), + ], + script_writer.script) + + def test_WriteFingerprintAssertion_without_oem_props(self): + target_info = BuildInfo(self.TEST_INFO_DICT, None) + source_info_dict = copy.deepcopy(self.TEST_INFO_DICT) + source_info_dict['build.prop']['ro.build.fingerprint'] = ( + 'source-build-fingerprint') + source_info = BuildInfo(source_info_dict, None) + + script_writer = MockScriptWriter() + WriteFingerprintAssertion(script_writer, target_info, source_info) + self.assertEqual( + [('AssertSomeFingerprint', 'source-build-fingerprint', + 'build-fingerprint')], + script_writer.script) + + def test_WriteFingerprintAssertion_with_source_oem_props(self): + target_info = BuildInfo(self.TEST_INFO_DICT, None) + source_info = BuildInfo(self.TEST_INFO_DICT_USES_OEM_PROPS, + self.TEST_OEM_DICTS) + + script_writer = MockScriptWriter() + WriteFingerprintAssertion(script_writer, target_info, source_info) + self.assertEqual( + [('AssertFingerprintOrThumbprint', 'build-fingerprint', + 'build-thumbprint')], + script_writer.script) + + def test_WriteFingerprintAssertion_with_target_oem_props(self): + target_info = BuildInfo(self.TEST_INFO_DICT_USES_OEM_PROPS, + self.TEST_OEM_DICTS) + source_info = BuildInfo(self.TEST_INFO_DICT, None) + + script_writer = MockScriptWriter() + WriteFingerprintAssertion(script_writer, target_info, source_info) + self.assertEqual( + [('AssertFingerprintOrThumbprint', 'build-fingerprint', + 'build-thumbprint')], + script_writer.script) + + def test_WriteFingerprintAssertion_with_both_oem_props(self): + target_info = BuildInfo(self.TEST_INFO_DICT_USES_OEM_PROPS, + self.TEST_OEM_DICTS) + source_info_dict = copy.deepcopy(self.TEST_INFO_DICT_USES_OEM_PROPS) + source_info_dict['build.prop']['ro.build.thumbprint'] = ( + 'source-build-thumbprint') + source_info = BuildInfo(source_info_dict, self.TEST_OEM_DICTS) + + script_writer = MockScriptWriter() + WriteFingerprintAssertion(script_writer, target_info, source_info) + self.assertEqual( + [('AssertSomeThumbprint', 'build-thumbprint', + 'source-build-thumbprint')], + script_writer.script) + + +class LoadOemDictsTest(unittest.TestCase): + + def tearDown(self): + common.Cleanup() + + def test_NoneDict(self): + self.assertIsNone(_LoadOemDicts(None)) + + def test_SingleDict(self): + dict_file = common.MakeTempFile() + with open(dict_file, 'w') as dict_fp: + dict_fp.write('abc=1\ndef=2\nxyz=foo\na.b.c=bar\n') + + oem_dicts = _LoadOemDicts([dict_file]) + self.assertEqual(1, len(oem_dicts)) + self.assertEqual('foo', oem_dicts[0]['xyz']) + self.assertEqual('bar', oem_dicts[0]['a.b.c']) + + def test_MultipleDicts(self): + oem_source = [] + for i in range(3): + dict_file = common.MakeTempFile() + with open(dict_file, 'w') as dict_fp: + dict_fp.write( + 'ro.build.index={}\ndef=2\nxyz=foo\na.b.c=bar\n'.format(i)) + oem_source.append(dict_file) + + oem_dicts = _LoadOemDicts(oem_source) + self.assertEqual(3, len(oem_dicts)) + for i, oem_dict in enumerate(oem_dicts): + self.assertEqual('2', oem_dict['def']) + self.assertEqual('foo', oem_dict['xyz']) + self.assertEqual('bar', oem_dict['a.b.c']) + self.assertEqual('{}'.format(i), oem_dict['ro.build.index'])