1# Copyright (C) 2020 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15import copy 16import itertools 17import logging 18import os 19import struct 20import zipfile 21 22import ota_metadata_pb2 23from common import (ZipDelete, ZipClose, OPTIONS, MakeTempFile, 24 ZipWriteStr, BuildInfo, LoadDictionaryFromFile, 25 SignFile, PARTITIONS_WITH_BUILD_PROP, PartitionBuildProps) 26 27logger = logging.getLogger(__name__) 28 29OPTIONS.no_signing = False 30OPTIONS.force_non_ab = False 31OPTIONS.wipe_user_data = False 32OPTIONS.downgrade = False 33OPTIONS.key_passwords = {} 34OPTIONS.package_key = None 35OPTIONS.incremental_source = None 36OPTIONS.retrofit_dynamic_partitions = False 37OPTIONS.output_metadata_path = None 38OPTIONS.boot_variable_file = None 39 40METADATA_NAME = 'META-INF/com/android/metadata' 41METADATA_PROTO_NAME = 'META-INF/com/android/metadata.pb' 42UNZIP_PATTERN = ['IMAGES/*', 'META/*', 'OTA/*', 'RADIO/*'] 43SECURITY_PATCH_LEVEL_PROP_NAME = "ro.build.version.security_patch" 44 45 46def FinalizeMetadata(metadata, input_file, output_file, needed_property_files): 47 """Finalizes the metadata and signs an A/B OTA package. 48 49 In order to stream an A/B OTA package, we need 'ota-streaming-property-files' 50 that contains the offsets and sizes for the ZIP entries. An example 51 property-files string is as follows. 52 53 "payload.bin:679:343,payload_properties.txt:378:45,metadata:69:379" 54 55 OTA server can pass down this string, in addition to the package URL, to the 56 system update client. System update client can then fetch individual ZIP 57 entries (ZIP_STORED) directly at the given offset of the URL. 58 59 Args: 60 metadata: The metadata dict for the package. 61 input_file: The input ZIP filename that doesn't contain the package METADATA 62 entry yet. 63 output_file: The final output ZIP filename. 64 needed_property_files: The list of PropertyFiles' to be generated. 65 """ 66 67 def ComputeAllPropertyFiles(input_file, needed_property_files): 68 # Write the current metadata entry with placeholders. 69 with zipfile.ZipFile(input_file, allowZip64=True) as input_zip: 70 for property_files in needed_property_files: 71 metadata.property_files[property_files.name] = property_files.Compute( 72 input_zip) 73 namelist = input_zip.namelist() 74 75 if METADATA_NAME in namelist or METADATA_PROTO_NAME in namelist: 76 ZipDelete(input_file, [METADATA_NAME, METADATA_PROTO_NAME]) 77 output_zip = zipfile.ZipFile(input_file, 'a', allowZip64=True) 78 WriteMetadata(metadata, output_zip) 79 ZipClose(output_zip) 80 81 if OPTIONS.no_signing: 82 return input_file 83 84 prelim_signing = MakeTempFile(suffix='.zip') 85 SignOutput(input_file, prelim_signing) 86 return prelim_signing 87 88 def FinalizeAllPropertyFiles(prelim_signing, needed_property_files): 89 with zipfile.ZipFile(prelim_signing, allowZip64=True) as prelim_signing_zip: 90 for property_files in needed_property_files: 91 metadata.property_files[property_files.name] = property_files.Finalize( 92 prelim_signing_zip, 93 len(metadata.property_files[property_files.name])) 94 95 # SignOutput(), which in turn calls signapk.jar, will possibly reorder the ZIP 96 # entries, as well as padding the entry headers. We do a preliminary signing 97 # (with an incomplete metadata entry) to allow that to happen. Then compute 98 # the ZIP entry offsets, write back the final metadata and do the final 99 # signing. 100 prelim_signing = ComputeAllPropertyFiles(input_file, needed_property_files) 101 try: 102 FinalizeAllPropertyFiles(prelim_signing, needed_property_files) 103 except PropertyFiles.InsufficientSpaceException: 104 # Even with the preliminary signing, the entry orders may change 105 # dramatically, which leads to insufficiently reserved space during the 106 # first call to ComputeAllPropertyFiles(). In that case, we redo all the 107 # preliminary signing works, based on the already ordered ZIP entries, to 108 # address the issue. 109 prelim_signing = ComputeAllPropertyFiles( 110 prelim_signing, needed_property_files) 111 FinalizeAllPropertyFiles(prelim_signing, needed_property_files) 112 113 # Replace the METADATA entry. 114 ZipDelete(prelim_signing, [METADATA_NAME, METADATA_PROTO_NAME]) 115 output_zip = zipfile.ZipFile(prelim_signing, 'a', allowZip64=True) 116 WriteMetadata(metadata, output_zip) 117 ZipClose(output_zip) 118 119 # Re-sign the package after updating the metadata entry. 120 if OPTIONS.no_signing: 121 output_file = prelim_signing 122 else: 123 SignOutput(prelim_signing, output_file) 124 125 # Reopen the final signed zip to double check the streaming metadata. 126 with zipfile.ZipFile(output_file, allowZip64=True) as output_zip: 127 for property_files in needed_property_files: 128 property_files.Verify( 129 output_zip, metadata.property_files[property_files.name].strip()) 130 131 # If requested, dump the metadata to a separate file. 132 output_metadata_path = OPTIONS.output_metadata_path 133 if output_metadata_path: 134 WriteMetadata(metadata, output_metadata_path) 135 136 137def WriteMetadata(metadata_proto, output): 138 """Writes the metadata to the zip archive or a file. 139 140 Args: 141 metadata_proto: The metadata protobuf for the package. 142 output: A ZipFile object or a string of the output file path. If a string 143 path is given, the metadata in the protobuf format will be written to 144 {output}.pb, e.g. ota_metadata.pb 145 """ 146 147 metadata_dict = BuildLegacyOtaMetadata(metadata_proto) 148 legacy_metadata = "".join(["%s=%s\n" % kv for kv in 149 sorted(metadata_dict.items())]) 150 if isinstance(output, zipfile.ZipFile): 151 ZipWriteStr(output, METADATA_PROTO_NAME, metadata_proto.SerializeToString(), 152 compress_type=zipfile.ZIP_STORED) 153 ZipWriteStr(output, METADATA_NAME, legacy_metadata, 154 compress_type=zipfile.ZIP_STORED) 155 return 156 157 with open('{}.pb'.format(output), 'wb') as f: 158 f.write(metadata_proto.SerializeToString()) 159 with open(output, 'w') as f: 160 f.write(legacy_metadata) 161 162 163def UpdateDeviceState(device_state, build_info, boot_variable_values, 164 is_post_build): 165 """Update the fields of the DeviceState proto with build info.""" 166 167 def UpdatePartitionStates(partition_states): 168 """Update the per-partition state according to its build.prop""" 169 if not build_info.is_ab: 170 return 171 build_info_set = ComputeRuntimeBuildInfos(build_info, 172 boot_variable_values) 173 assert "ab_partitions" in build_info.info_dict,\ 174 "ab_partitions property required for ab update." 175 ab_partitions = set(build_info.info_dict.get("ab_partitions")) 176 177 # delta_generator will error out on unused timestamps, 178 # so only generate timestamps for dynamic partitions 179 # used in OTA update. 180 for partition in sorted(set(PARTITIONS_WITH_BUILD_PROP) & ab_partitions): 181 partition_prop = build_info.info_dict.get( 182 '{}.build.prop'.format(partition)) 183 # Skip if the partition is missing, or it doesn't have a build.prop 184 if not partition_prop or not partition_prop.build_props: 185 continue 186 187 partition_state = partition_states.add() 188 partition_state.partition_name = partition 189 # Update the partition's runtime device names and fingerprints 190 partition_devices = set() 191 partition_fingerprints = set() 192 for runtime_build_info in build_info_set: 193 partition_devices.add( 194 runtime_build_info.GetPartitionBuildProp('ro.product.device', 195 partition)) 196 partition_fingerprints.add( 197 runtime_build_info.GetPartitionFingerprint(partition)) 198 199 partition_state.device.extend(sorted(partition_devices)) 200 partition_state.build.extend(sorted(partition_fingerprints)) 201 202 # TODO(xunchang) set the boot image's version with kmi. Note the boot 203 # image doesn't have a file map. 204 partition_state.version = build_info.GetPartitionBuildProp( 205 'ro.build.date.utc', partition) 206 207 # TODO(xunchang), we can save a call to ComputeRuntimeBuildInfos. 208 build_devices, build_fingerprints = \ 209 CalculateRuntimeDevicesAndFingerprints(build_info, boot_variable_values) 210 device_state.device.extend(sorted(build_devices)) 211 device_state.build.extend(sorted(build_fingerprints)) 212 device_state.build_incremental = build_info.GetBuildProp( 213 'ro.build.version.incremental') 214 215 UpdatePartitionStates(device_state.partition_state) 216 217 if is_post_build: 218 device_state.sdk_level = build_info.GetBuildProp( 219 'ro.build.version.sdk') 220 device_state.security_patch_level = build_info.GetBuildProp( 221 'ro.build.version.security_patch') 222 # Use the actual post-timestamp, even for a downgrade case. 223 device_state.timestamp = int(build_info.GetBuildProp('ro.build.date.utc')) 224 225 226def GetPackageMetadata(target_info, source_info=None): 227 """Generates and returns the metadata proto. 228 229 It generates a ota_metadata protobuf that contains the info to be written 230 into an OTA package (META-INF/com/android/metadata.pb). It also handles the 231 detection of downgrade / data wipe based on the global options. 232 233 Args: 234 target_info: The BuildInfo instance that holds the target build info. 235 source_info: The BuildInfo instance that holds the source build info, or 236 None if generating full OTA. 237 238 Returns: 239 A protobuf to be written into package metadata entry. 240 """ 241 assert isinstance(target_info, BuildInfo) 242 assert source_info is None or isinstance(source_info, BuildInfo) 243 244 boot_variable_values = {} 245 if OPTIONS.boot_variable_file: 246 d = LoadDictionaryFromFile(OPTIONS.boot_variable_file) 247 for key, values in d.items(): 248 boot_variable_values[key] = [val.strip() for val in values.split(',')] 249 250 metadata_proto = ota_metadata_pb2.OtaMetadata() 251 # TODO(xunchang) some fields, e.g. post-device isn't necessary. We can 252 # consider skipping them if they aren't used by clients. 253 UpdateDeviceState(metadata_proto.postcondition, target_info, 254 boot_variable_values, True) 255 256 if target_info.is_ab and not OPTIONS.force_non_ab: 257 metadata_proto.type = ota_metadata_pb2.OtaMetadata.AB 258 metadata_proto.required_cache = 0 259 else: 260 metadata_proto.type = ota_metadata_pb2.OtaMetadata.BLOCK 261 # cache requirement will be updated by the non-A/B codes. 262 263 if OPTIONS.wipe_user_data: 264 metadata_proto.wipe = True 265 266 if OPTIONS.retrofit_dynamic_partitions: 267 metadata_proto.retrofit_dynamic_partitions = True 268 269 is_incremental = source_info is not None 270 if is_incremental: 271 UpdateDeviceState(metadata_proto.precondition, source_info, 272 boot_variable_values, False) 273 else: 274 metadata_proto.precondition.device.extend( 275 metadata_proto.postcondition.device) 276 277 # Detect downgrades and set up downgrade flags accordingly. 278 if is_incremental: 279 HandleDowngradeMetadata(metadata_proto, target_info, source_info) 280 281 return metadata_proto 282 283 284def BuildLegacyOtaMetadata(metadata_proto): 285 """Converts the metadata proto to a legacy metadata dict. 286 287 This metadata dict is used to build the legacy metadata text file for 288 backward compatibility. We won't add new keys to the legacy metadata format. 289 If new information is needed, we should add it as a new field in OtaMetadata 290 proto definition. 291 """ 292 293 separator = '|' 294 295 metadata_dict = {} 296 if metadata_proto.type == ota_metadata_pb2.OtaMetadata.AB: 297 metadata_dict['ota-type'] = 'AB' 298 elif metadata_proto.type == ota_metadata_pb2.OtaMetadata.BLOCK: 299 metadata_dict['ota-type'] = 'BLOCK' 300 if metadata_proto.wipe: 301 metadata_dict['ota-wipe'] = 'yes' 302 if metadata_proto.retrofit_dynamic_partitions: 303 metadata_dict['ota-retrofit-dynamic-partitions'] = 'yes' 304 if metadata_proto.downgrade: 305 metadata_dict['ota-downgrade'] = 'yes' 306 307 metadata_dict['ota-required-cache'] = str(metadata_proto.required_cache) 308 309 post_build = metadata_proto.postcondition 310 metadata_dict['post-build'] = separator.join(post_build.build) 311 metadata_dict['post-build-incremental'] = post_build.build_incremental 312 metadata_dict['post-sdk-level'] = post_build.sdk_level 313 metadata_dict['post-security-patch-level'] = post_build.security_patch_level 314 metadata_dict['post-timestamp'] = str(post_build.timestamp) 315 316 pre_build = metadata_proto.precondition 317 metadata_dict['pre-device'] = separator.join(pre_build.device) 318 # incremental updates 319 if len(pre_build.build) != 0: 320 metadata_dict['pre-build'] = separator.join(pre_build.build) 321 metadata_dict['pre-build-incremental'] = pre_build.build_incremental 322 323 if metadata_proto.spl_downgrade: 324 metadata_dict['spl-downgrade'] = 'yes' 325 metadata_dict.update(metadata_proto.property_files) 326 327 return metadata_dict 328 329 330def HandleDowngradeMetadata(metadata_proto, target_info, source_info): 331 # Only incremental OTAs are allowed to reach here. 332 assert OPTIONS.incremental_source is not None 333 334 post_timestamp = target_info.GetBuildProp("ro.build.date.utc") 335 pre_timestamp = source_info.GetBuildProp("ro.build.date.utc") 336 is_downgrade = int(post_timestamp) < int(pre_timestamp) 337 338 if OPTIONS.spl_downgrade: 339 metadata_proto.spl_downgrade = True 340 341 if OPTIONS.downgrade: 342 if not is_downgrade: 343 raise RuntimeError( 344 "--downgrade or --override_timestamp specified but no downgrade " 345 "detected: pre: %s, post: %s" % (pre_timestamp, post_timestamp)) 346 metadata_proto.downgrade = True 347 else: 348 if is_downgrade: 349 raise RuntimeError( 350 "Downgrade detected based on timestamp check: pre: %s, post: %s. " 351 "Need to specify --override_timestamp OR --downgrade to allow " 352 "building the incremental." % (pre_timestamp, post_timestamp)) 353 354 355def ComputeRuntimeBuildInfos(default_build_info, boot_variable_values): 356 """Returns a set of build info objects that may exist during runtime.""" 357 358 build_info_set = {default_build_info} 359 if not boot_variable_values: 360 return build_info_set 361 362 # Calculate all possible combinations of the values for the boot variables. 363 keys = boot_variable_values.keys() 364 value_list = boot_variable_values.values() 365 combinations = [dict(zip(keys, values)) 366 for values in itertools.product(*value_list)] 367 for placeholder_values in combinations: 368 # Reload the info_dict as some build properties may change their values 369 # based on the value of ro.boot* properties. 370 info_dict = copy.deepcopy(default_build_info.info_dict) 371 for partition in PARTITIONS_WITH_BUILD_PROP: 372 partition_prop_key = "{}.build.prop".format(partition) 373 input_file = info_dict[partition_prop_key].input_file 374 if isinstance(input_file, zipfile.ZipFile): 375 with zipfile.ZipFile(input_file.filename, allowZip64=True) as input_zip: 376 info_dict[partition_prop_key] = \ 377 PartitionBuildProps.FromInputFile(input_zip, partition, 378 placeholder_values) 379 else: 380 info_dict[partition_prop_key] = \ 381 PartitionBuildProps.FromInputFile(input_file, partition, 382 placeholder_values) 383 info_dict["build.prop"] = info_dict["system.build.prop"] 384 build_info_set.add(BuildInfo(info_dict, default_build_info.oem_dicts)) 385 386 return build_info_set 387 388 389def CalculateRuntimeDevicesAndFingerprints(default_build_info, 390 boot_variable_values): 391 """Returns a tuple of sets for runtime devices and fingerprints""" 392 393 device_names = set() 394 fingerprints = set() 395 build_info_set = ComputeRuntimeBuildInfos(default_build_info, 396 boot_variable_values) 397 for runtime_build_info in build_info_set: 398 device_names.add(runtime_build_info.device) 399 fingerprints.add(runtime_build_info.fingerprint) 400 return device_names, fingerprints 401 402 403def GetZipEntryOffset(zfp, entry_info): 404 """Get offset to a beginning of a particular zip entry 405 Args: 406 fp: zipfile.ZipFile 407 entry_info: zipfile.ZipInfo 408 409 Returns: 410 (offset, size) tuple 411 """ 412 # Don't use len(entry_info.extra). Because that returns size of extra 413 # fields in central directory. We need to look at local file directory, 414 # as these two might have different sizes. 415 416 # We cannot work with zipfile.ZipFile instances, we need a |fp| for the underlying file. 417 zfp = zfp.fp 418 zfp.seek(entry_info.header_offset) 419 data = zfp.read(zipfile.sizeFileHeader) 420 fheader = struct.unpack(zipfile.structFileHeader, data) 421 # Last two fields of local file header are filename length and 422 # extra length 423 filename_len = fheader[-2] 424 extra_len = fheader[-1] 425 offset = entry_info.header_offset 426 offset += zipfile.sizeFileHeader 427 offset += filename_len + extra_len 428 size = entry_info.file_size 429 return (offset, size) 430 431 432class PropertyFiles(object): 433 """A class that computes the property-files string for an OTA package. 434 435 A property-files string is a comma-separated string that contains the 436 offset/size info for an OTA package. The entries, which must be ZIP_STORED, 437 can be fetched directly with the package URL along with the offset/size info. 438 These strings can be used for streaming A/B OTAs, or allowing an updater to 439 download package metadata entry directly, without paying the cost of 440 downloading entire package. 441 442 Computing the final property-files string requires two passes. Because doing 443 the whole package signing (with signapk.jar) will possibly reorder the ZIP 444 entries, which may in turn invalidate earlier computed ZIP entry offset/size 445 values. 446 447 This class provides functions to be called for each pass. The general flow is 448 as follows. 449 450 property_files = PropertyFiles() 451 # The first pass, which writes placeholders before doing initial signing. 452 property_files.Compute() 453 SignOutput() 454 455 # The second pass, by replacing the placeholders with actual data. 456 property_files.Finalize() 457 SignOutput() 458 459 And the caller can additionally verify the final result. 460 461 property_files.Verify() 462 """ 463 464 def __init__(self): 465 self.name = None 466 self.required = () 467 self.optional = () 468 469 def Compute(self, input_zip): 470 """Computes and returns a property-files string with placeholders. 471 472 We reserve extra space for the offset and size of the metadata entry itself, 473 although we don't know the final values until the package gets signed. 474 475 Args: 476 input_zip: The input ZIP file. 477 478 Returns: 479 A string with placeholders for the metadata offset/size info, e.g. 480 "payload.bin:679:343,payload_properties.txt:378:45,metadata: ". 481 """ 482 return self.GetPropertyFilesString(input_zip, reserve_space=True) 483 484 class InsufficientSpaceException(Exception): 485 pass 486 487 def Finalize(self, input_zip, reserved_length): 488 """Finalizes a property-files string with actual METADATA offset/size info. 489 490 The input ZIP file has been signed, with the ZIP entries in the desired 491 place (signapk.jar will possibly reorder the ZIP entries). Now we compute 492 the ZIP entry offsets and construct the property-files string with actual 493 data. Note that during this process, we must pad the property-files string 494 to the reserved length, so that the METADATA entry size remains the same. 495 Otherwise the entries' offsets and sizes may change again. 496 497 Args: 498 input_zip: The input ZIP file. 499 reserved_length: The reserved length of the property-files string during 500 the call to Compute(). The final string must be no more than this 501 size. 502 503 Returns: 504 A property-files string including the metadata offset/size info, e.g. 505 "payload.bin:679:343,payload_properties.txt:378:45,metadata:69:379 ". 506 507 Raises: 508 InsufficientSpaceException: If the reserved length is insufficient to hold 509 the final string. 510 """ 511 result = self.GetPropertyFilesString(input_zip, reserve_space=False) 512 if len(result) > reserved_length: 513 raise self.InsufficientSpaceException( 514 'Insufficient reserved space: reserved={}, actual={}'.format( 515 reserved_length, len(result))) 516 517 result += ' ' * (reserved_length - len(result)) 518 return result 519 520 def Verify(self, input_zip, expected): 521 """Verifies the input ZIP file contains the expected property-files string. 522 523 Args: 524 input_zip: The input ZIP file. 525 expected: The property-files string that's computed from Finalize(). 526 527 Raises: 528 AssertionError: On finding a mismatch. 529 """ 530 actual = self.GetPropertyFilesString(input_zip) 531 assert actual == expected, \ 532 "Mismatching streaming metadata: {} vs {}.".format(actual, expected) 533 534 def GetPropertyFilesString(self, zip_file, reserve_space=False): 535 """ 536 Constructs the property-files string per request. 537 538 Args: 539 zip_file: The input ZIP file. 540 reserved_length: The reserved length of the property-files string. 541 542 Returns: 543 A property-files string including the metadata offset/size info, e.g. 544 "payload.bin:679:343,payload_properties.txt:378:45,metadata: ". 545 """ 546 547 def ComputeEntryOffsetSize(name): 548 """Computes the zip entry offset and size.""" 549 info = zip_file.getinfo(name) 550 (offset, size) = GetZipEntryOffset(zip_file, info) 551 return '%s:%d:%d' % (os.path.basename(name), offset, size) 552 553 tokens = [] 554 tokens.extend(self._GetPrecomputed(zip_file)) 555 for entry in self.required: 556 tokens.append(ComputeEntryOffsetSize(entry)) 557 for entry in self.optional: 558 if entry in zip_file.namelist(): 559 tokens.append(ComputeEntryOffsetSize(entry)) 560 561 # 'META-INF/com/android/metadata' is required. We don't know its actual 562 # offset and length (as well as the values for other entries). So we reserve 563 # 15-byte as a placeholder ('offset:length'), which is sufficient to cover 564 # the space for metadata entry. Because 'offset' allows a max of 10-digit 565 # (i.e. ~9 GiB), with a max of 4-digit for the length. Note that all the 566 # reserved space serves the metadata entry only. 567 if reserve_space: 568 tokens.append('metadata:' + ' ' * 15) 569 tokens.append('metadata.pb:' + ' ' * 15) 570 else: 571 tokens.append(ComputeEntryOffsetSize(METADATA_NAME)) 572 if METADATA_PROTO_NAME in zip_file.namelist(): 573 tokens.append(ComputeEntryOffsetSize(METADATA_PROTO_NAME)) 574 575 return ','.join(tokens) 576 577 def _GetPrecomputed(self, input_zip): 578 """Computes the additional tokens to be included into the property-files. 579 580 This applies to tokens without actual ZIP entries, such as 581 payload_metadata.bin. We want to expose the offset/size to updaters, so 582 that they can download the payload metadata directly with the info. 583 584 Args: 585 input_zip: The input zip file. 586 587 Returns: 588 A list of strings (tokens) to be added to the property-files string. 589 """ 590 # pylint: disable=no-self-use 591 # pylint: disable=unused-argument 592 return [] 593 594 595def SignOutput(temp_zip_name, output_zip_name): 596 pw = OPTIONS.key_passwords[OPTIONS.package_key] 597 598 SignFile(temp_zip_name, output_zip_name, OPTIONS.package_key, pw, 599 whole_file=True) 600 601 602def ConstructOtaApexInfo(target_zip, source_file=None): 603 """If applicable, add the source version to the apex info.""" 604 605 def _ReadApexInfo(input_zip): 606 if "META/apex_info.pb" not in input_zip.namelist(): 607 logger.warning("target_file doesn't contain apex_info.pb %s", input_zip) 608 return None 609 610 with input_zip.open("META/apex_info.pb", "r") as zfp: 611 return zfp.read() 612 613 target_apex_string = _ReadApexInfo(target_zip) 614 # Return early if the target apex info doesn't exist or is empty. 615 if not target_apex_string: 616 return target_apex_string 617 618 # If the source apex info isn't available, just return the target info 619 if not source_file: 620 return target_apex_string 621 622 with zipfile.ZipFile(source_file, "r", allowZip64=True) as source_zip: 623 source_apex_string = _ReadApexInfo(source_zip) 624 if not source_apex_string: 625 return target_apex_string 626 627 source_apex_proto = ota_metadata_pb2.ApexMetadata() 628 source_apex_proto.ParseFromString(source_apex_string) 629 source_apex_versions = {apex.package_name: apex.version for apex in 630 source_apex_proto.apex_info} 631 632 # If the apex package is available in the source build, initialize the source 633 # apex version. 634 target_apex_proto = ota_metadata_pb2.ApexMetadata() 635 target_apex_proto.ParseFromString(target_apex_string) 636 for target_apex in target_apex_proto.apex_info: 637 name = target_apex.package_name 638 if name in source_apex_versions: 639 target_apex.source_version = source_apex_versions[name] 640 641 return target_apex_proto.SerializeToString() 642 643 644def IsLz4diffCompatible(source_file: str, target_file: str): 645 """Check whether lz4diff versions in two builds are compatible 646 647 Args: 648 source_file: Path to source build's target_file.zip 649 target_file: Path to target build's target_file.zip 650 651 Returns: 652 bool true if and only if lz4diff versions are compatible 653 """ 654 if source_file is None or target_file is None: 655 return False 656 # Right now we enable lz4diff as long as source build has liblz4.so. 657 # In the future we might introduce version system to lz4diff as well. 658 if zipfile.is_zipfile(source_file): 659 with zipfile.ZipFile(source_file, "r") as zfp: 660 return "META/liblz4.so" in zfp.namelist() 661 else: 662 assert os.path.isdir(source_file) 663 return os.path.exists(os.path.join(source_file, "META", "liblz4.so")) 664 665 666def IsZucchiniCompatible(source_file: str, target_file: str): 667 """Check whether zucchini versions in two builds are compatible 668 669 Args: 670 source_file: Path to source build's target_file.zip 671 target_file: Path to target build's target_file.zip 672 673 Returns: 674 bool true if and only if zucchini versions are compatible 675 """ 676 if source_file is None or target_file is None: 677 return False 678 assert os.path.exists(source_file) 679 assert os.path.exists(target_file) 680 681 assert zipfile.is_zipfile(source_file) or os.path.isdir(source_file) 682 assert zipfile.is_zipfile(target_file) or os.path.isdir(target_file) 683 _ZUCCHINI_CONFIG_ENTRY_NAME = "META/zucchini_config.txt" 684 685 def ReadEntry(path, entry): 686 # Read an entry inside a .zip file or extracted dir of .zip file 687 if zipfile.is_zipfile(path): 688 with zipfile.ZipFile(path, "r", allowZip64=True) as zfp: 689 if entry in zfp.namelist(): 690 return zfp.read(entry).decode() 691 else: 692 entry_path = os.path.join(entry, path) 693 if os.path.exists(entry_path): 694 with open(entry_path, "r") as fp: 695 return fp.read() 696 else: 697 return "" 698 return ReadEntry(source_file, _ZUCCHINI_CONFIG_ENTRY_NAME) == ReadEntry(target_file, _ZUCCHINI_CONFIG_ENTRY_NAME) 699