1#!/usr/bin/env python 2# 3# Copyright (C) 2022 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); you may not 6# use this file except in compliance with the License. You may obtain a copy of 7# the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14# License for the specific language governing permissions and limitations under 15# the License. 16# 17"""This script merges two partial target files packages. 18 19One input package contains framework files, and the other contains vendor files. 20 21This script produces a complete, merged target files package: 22 - This package can be used to generate a flashable IMG package. 23 See --output-img. 24 - This package can be used to generate an OTA package. See --output-ota. 25 - The merged package is checked for compatibility between the two inputs. 26 27Usage: merge_target_files [args] 28 29 --framework-target-files framework-target-files-zip-archive 30 The input target files package containing framework bits. This is a zip 31 archive. 32 33 --framework-item-list framework-item-list-file 34 The optional path to a newline-separated config file of items that 35 are extracted as-is from the framework target files package. 36 37 --framework-misc-info-keys framework-misc-info-keys-file 38 The optional path to a newline-separated config file of keys to 39 extract from the framework META/misc_info.txt file. 40 41 --vendor-target-files vendor-target-files-zip-archive 42 The input target files package containing vendor bits. This is a zip 43 archive. 44 45 --vendor-item-list vendor-item-list-file 46 The optional path to a newline-separated config file of items that 47 are extracted as-is from the vendor target files package. 48 49 --output-target-files output-target-files-package 50 If provided, the output merged target files package. Also a zip archive. 51 52 --output-dir output-directory 53 If provided, the destination directory for saving merged files. Requires 54 the --output-item-list flag. 55 Can be provided alongside --output-target-files, or by itself. 56 57 --output-item-list output-item-list-file. 58 The optional path to a newline-separated config file that specifies the 59 file patterns to copy into the --output-dir. Required if providing 60 the --output-dir flag. 61 62 --output-ota output-ota-package 63 The output ota package. This is a zip archive. Use of this flag may 64 require passing the --path common flag; see common.py. 65 66 --output-img output-img-package 67 The output img package, suitable for use with 'fastboot update'. Use of 68 this flag may require passing the --path common flag; see common.py. 69 70 --output-super-empty output-super-empty-image 71 If provided, creates a super_empty.img file from the merged target 72 files package and saves it at this path. 73 74 --rebuild_recovery 75 Copy the recovery image used by non-A/B devices, used when 76 regenerating vendor images with --rebuild-sepolicy. 77 78 --allow-duplicate-apkapex-keys 79 If provided, duplicate APK/APEX keys are ignored and the value from the 80 framework is used. 81 82 --rebuild-sepolicy 83 If provided, rebuilds odm.img or vendor.img to include merged sepolicy 84 files. If odm is present then odm is preferred. 85 86 --vendor-otatools otatools.zip 87 If provided, use this otatools.zip when recompiling the odm or vendor 88 image to include sepolicy. 89 90 --keep-tmp 91 Keep tempoary files for debugging purposes. 92 93 The following only apply when using the VSDK to perform dexopt on vendor apps: 94 95 --framework-dexpreopt-config 96 If provided, the location of framwework's dexpreopt_config.zip. 97 98 --framework-dexpreopt-tools 99 if provided, the location of framework's dexpreopt_tools.zip. 100 101 --vendor-dexpreopt-config 102 If provided, the location of vendor's dexpreopt_config.zip. 103""" 104 105import logging 106import os 107import shutil 108import subprocess 109import sys 110import zipfile 111 112import add_img_to_target_files 113import build_image 114import build_super_image 115import common 116import img_from_target_files 117import merge_compatibility_checks 118import merge_dexopt 119import merge_meta 120import merge_utils 121import ota_from_target_files 122 123from common import ExternalError 124 125logger = logging.getLogger(__name__) 126 127OPTIONS = common.OPTIONS 128# Always turn on verbose logging. 129OPTIONS.verbose = True 130OPTIONS.framework_target_files = None 131OPTIONS.framework_item_list = [] 132OPTIONS.framework_misc_info_keys = [] 133OPTIONS.vendor_target_files = None 134OPTIONS.vendor_item_list = [] 135OPTIONS.output_target_files = None 136OPTIONS.output_dir = None 137OPTIONS.output_item_list = [] 138OPTIONS.output_ota = None 139OPTIONS.output_img = None 140OPTIONS.output_super_empty = None 141OPTIONS.rebuild_recovery = False 142# TODO(b/150582573): Remove this option. 143OPTIONS.allow_duplicate_apkapex_keys = False 144OPTIONS.vendor_otatools = None 145OPTIONS.rebuild_sepolicy = False 146OPTIONS.keep_tmp = False 147OPTIONS.framework_dexpreopt_config = None 148OPTIONS.framework_dexpreopt_tools = None 149OPTIONS.vendor_dexpreopt_config = None 150 151 152def create_merged_package(temp_dir): 153 """Merges two target files packages into one target files structure. 154 155 Returns: 156 Path to merged package under temp directory. 157 """ 158 # Extract "as is" items from the input framework and vendor partial target 159 # files packages directly into the output temporary directory, since these items 160 # do not need special case processing. 161 162 output_target_files_temp_dir = os.path.join(temp_dir, 'output') 163 merge_utils.ExtractItems( 164 input_zip=OPTIONS.framework_target_files, 165 output_dir=output_target_files_temp_dir, 166 extract_item_list=OPTIONS.framework_item_list) 167 merge_utils.ExtractItems( 168 input_zip=OPTIONS.vendor_target_files, 169 output_dir=output_target_files_temp_dir, 170 extract_item_list=OPTIONS.vendor_item_list) 171 172 # Perform special case processing on META/* items. 173 # After this function completes successfully, all the files we need to create 174 # the output target files package are in place. 175 merge_meta.MergeMetaFiles( 176 temp_dir=temp_dir, merged_dir=output_target_files_temp_dir) 177 178 merge_dexopt.MergeDexopt( 179 temp_dir=temp_dir, output_target_files_dir=output_target_files_temp_dir) 180 181 return output_target_files_temp_dir 182 183 184def generate_missing_images(target_files_dir): 185 """Generate any missing images from target files.""" 186 187 # Regenerate IMAGES in the target directory. 188 189 add_img_args = [ 190 '--verbose', 191 '--add_missing', 192 ] 193 if OPTIONS.rebuild_recovery: 194 add_img_args.append('--rebuild_recovery') 195 add_img_args.append(target_files_dir) 196 197 add_img_to_target_files.main(add_img_args) 198 199 200def rebuild_image_with_sepolicy(target_files_dir): 201 """Rebuilds odm.img or vendor.img to include merged sepolicy files. 202 203 If odm is present then odm is preferred -- otherwise vendor is used. 204 """ 205 partition = 'vendor' 206 if os.path.exists(os.path.join(target_files_dir, 'ODM')) or os.path.exists( 207 os.path.join(target_files_dir, 'IMAGES/odm.img')): 208 partition = 'odm' 209 partition_img = '{}.img'.format(partition) 210 partition_map = '{}.map'.format(partition) 211 212 logger.info('Recompiling %s using the merged sepolicy files.', partition_img) 213 214 # Copy the combined SEPolicy file and framework hashes to the image that is 215 # being rebuilt. 216 def copy_selinux_file(input_path, output_filename): 217 input_filename = os.path.join(target_files_dir, input_path) 218 if not os.path.exists(input_filename): 219 input_filename = input_filename.replace('SYSTEM_EXT/', 'SYSTEM/system_ext/') \ 220 .replace('PRODUCT/', 'SYSTEM/product/') 221 if not os.path.exists(input_filename): 222 logger.info('Skipping copy_selinux_file for %s', input_filename) 223 return 224 shutil.copy( 225 input_filename, 226 os.path.join(target_files_dir, partition.upper(), 'etc/selinux', 227 output_filename)) 228 229 copy_selinux_file('META/combined_sepolicy', 'precompiled_sepolicy') 230 copy_selinux_file('SYSTEM/etc/selinux/plat_sepolicy_and_mapping.sha256', 231 'precompiled_sepolicy.plat_sepolicy_and_mapping.sha256') 232 copy_selinux_file( 233 'SYSTEM_EXT/etc/selinux/system_ext_sepolicy_and_mapping.sha256', 234 'precompiled_sepolicy.system_ext_sepolicy_and_mapping.sha256') 235 copy_selinux_file('PRODUCT/etc/selinux/product_sepolicy_and_mapping.sha256', 236 'precompiled_sepolicy.product_sepolicy_and_mapping.sha256') 237 238 if not OPTIONS.vendor_otatools: 239 # Remove the partition from the merged target-files archive. It will be 240 # rebuilt later automatically by generate_missing_images(). 241 os.remove(os.path.join(target_files_dir, 'IMAGES', partition_img)) 242 return 243 244 # TODO(b/192253131): Remove the need for vendor_otatools by fixing 245 # backwards-compatibility issues when compiling images across releases. 246 if not OPTIONS.vendor_target_files: 247 raise ValueError( 248 'Expected vendor_target_files if vendor_otatools is not None.') 249 logger.info( 250 '%s recompilation will be performed using the vendor otatools.zip', 251 partition_img) 252 253 # Unzip the vendor build's otatools.zip and target-files archive. 254 vendor_otatools_dir = common.MakeTempDir( 255 prefix='merge_target_files_vendor_otatools_') 256 vendor_target_files_dir = common.MakeTempDir( 257 prefix='merge_target_files_vendor_target_files_') 258 common.UnzipToDir(OPTIONS.vendor_otatools, vendor_otatools_dir) 259 common.UnzipToDir(OPTIONS.vendor_target_files, vendor_target_files_dir) 260 261 # Copy the partition contents from the merged target-files archive to the 262 # vendor target-files archive. 263 shutil.rmtree(os.path.join(vendor_target_files_dir, partition.upper())) 264 shutil.copytree( 265 os.path.join(target_files_dir, partition.upper()), 266 os.path.join(vendor_target_files_dir, partition.upper()), 267 symlinks=True) 268 269 # Delete then rebuild the partition. 270 os.remove(os.path.join(vendor_target_files_dir, 'IMAGES', partition_img)) 271 rebuild_partition_command = [ 272 os.path.join(vendor_otatools_dir, 'bin', 'add_img_to_target_files'), 273 '--verbose', 274 '--add_missing', 275 ] 276 if OPTIONS.rebuild_recovery: 277 rebuild_partition_command.append('--rebuild_recovery') 278 rebuild_partition_command.append(vendor_target_files_dir) 279 logger.info('Recompiling %s: %s', partition_img, 280 ' '.join(rebuild_partition_command)) 281 common.RunAndCheckOutput(rebuild_partition_command, verbose=True) 282 283 # Move the newly-created image to the merged target files dir. 284 if not os.path.exists(os.path.join(target_files_dir, 'IMAGES')): 285 os.makedirs(os.path.join(target_files_dir, 'IMAGES')) 286 shutil.move( 287 os.path.join(vendor_target_files_dir, 'IMAGES', partition_img), 288 os.path.join(target_files_dir, 'IMAGES', partition_img)) 289 shutil.move( 290 os.path.join(vendor_target_files_dir, 'IMAGES', partition_map), 291 os.path.join(target_files_dir, 'IMAGES', partition_map)) 292 293 def copy_recovery_file(filename): 294 for subdir in ('VENDOR', 'SYSTEM/vendor'): 295 source = os.path.join(vendor_target_files_dir, subdir, filename) 296 if os.path.exists(source): 297 dest = os.path.join(target_files_dir, subdir, filename) 298 shutil.copy(source, dest) 299 return 300 logger.info('Skipping copy_recovery_file for %s, file not found', filename) 301 302 if OPTIONS.rebuild_recovery: 303 copy_recovery_file('etc/recovery.img') 304 copy_recovery_file('bin/install-recovery.sh') 305 copy_recovery_file('recovery-from-boot.p') 306 307 308def generate_super_empty_image(target_dir, output_super_empty): 309 """Generates super_empty image from target package. 310 311 Args: 312 target_dir: Path to the target file package which contains misc_info.txt for 313 detailed information for super image. 314 output_super_empty: If provided, copies a super_empty.img file from the 315 target files package to this path. 316 """ 317 # Create super_empty.img using the merged misc_info.txt. 318 319 misc_info_txt = os.path.join(target_dir, 'META', 'misc_info.txt') 320 321 use_dynamic_partitions = common.LoadDictionaryFromFile(misc_info_txt).get( 322 'use_dynamic_partitions') 323 324 if use_dynamic_partitions != 'true' and output_super_empty: 325 raise ValueError( 326 'Building super_empty.img requires use_dynamic_partitions=true.') 327 elif use_dynamic_partitions == 'true': 328 super_empty_img = os.path.join(target_dir, 'IMAGES', 'super_empty.img') 329 build_super_image_args = [ 330 misc_info_txt, 331 super_empty_img, 332 ] 333 build_super_image.main(build_super_image_args) 334 335 # Copy super_empty.img to the user-provided output_super_empty location. 336 if output_super_empty: 337 shutil.copyfile(super_empty_img, output_super_empty) 338 339 340def create_target_files_archive(output_zip, source_dir, temp_dir): 341 """Creates a target_files zip archive from the input source dir. 342 343 Args: 344 output_zip: The name of the zip archive target files package. 345 source_dir: The target directory contains package to be archived. 346 temp_dir: Path to temporary directory for any intermediate files. 347 """ 348 output_target_files_list = os.path.join(temp_dir, 'output.list') 349 output_target_files_meta_dir = os.path.join(source_dir, 'META') 350 351 def files_from_path(target_path, extra_args=None): 352 """Gets files under the given path and return a sorted list.""" 353 find_command = ['find', target_path] + (extra_args or []) 354 find_process = common.Run( 355 find_command, stdout=subprocess.PIPE, verbose=False) 356 return common.RunAndCheckOutput(['sort'], 357 stdin=find_process.stdout, 358 verbose=False) 359 360 # META content appears first in the zip. This is done by the 361 # standard build system for optimized extraction of those files, 362 # so we do the same step for merged target_files.zips here too. 363 meta_content = files_from_path(output_target_files_meta_dir) 364 other_content = files_from_path( 365 source_dir, 366 ['-path', output_target_files_meta_dir, '-prune', '-o', '-print']) 367 368 with open(output_target_files_list, 'w') as f: 369 f.write(meta_content) 370 f.write(other_content) 371 372 command = [ 373 'soong_zip', 374 '-d', 375 '-o', 376 os.path.abspath(output_zip), 377 '-C', 378 source_dir, 379 '-r', 380 output_target_files_list, 381 ] 382 383 logger.info('creating %s', output_zip) 384 common.RunAndCheckOutput(command, verbose=True) 385 logger.info('finished creating %s', output_zip) 386 387 388def merge_target_files(temp_dir): 389 """Merges two target files packages together. 390 391 This function uses framework and vendor target files packages as input, 392 performs various file extractions, special case processing, and finally 393 creates a merged zip archive as output. 394 395 Args: 396 temp_dir: The name of a directory we use when we extract items from the 397 input target files packages, and also a scratch directory that we use for 398 temporary files. 399 """ 400 401 logger.info('starting: merge framework %s and vendor %s into output %s', 402 OPTIONS.framework_target_files, OPTIONS.vendor_target_files, 403 OPTIONS.output_target_files) 404 405 output_target_files_temp_dir = create_merged_package(temp_dir) 406 407 partition_map = common.PartitionMapFromTargetFiles( 408 output_target_files_temp_dir) 409 410 compatibility_errors = merge_compatibility_checks.CheckCompatibility( 411 target_files_dir=output_target_files_temp_dir, 412 partition_map=partition_map) 413 if compatibility_errors: 414 for error in compatibility_errors: 415 logger.error(error) 416 raise ExternalError( 417 'Found incompatibilities in the merged target files package.') 418 419 # Include the compiled policy in an image if requested. 420 if OPTIONS.rebuild_sepolicy: 421 rebuild_image_with_sepolicy(output_target_files_temp_dir) 422 423 generate_missing_images(output_target_files_temp_dir) 424 425 generate_super_empty_image(output_target_files_temp_dir, 426 OPTIONS.output_super_empty) 427 428 # Finally, create the output target files zip archive and/or copy the 429 # output items to the output target files directory. 430 431 if OPTIONS.output_dir: 432 merge_utils.CopyItems(output_target_files_temp_dir, OPTIONS.output_dir, 433 OPTIONS.output_item_list) 434 435 if not OPTIONS.output_target_files: 436 return 437 438 create_target_files_archive(OPTIONS.output_target_files, 439 output_target_files_temp_dir, temp_dir) 440 441 # Create the IMG package from the merged target files package. 442 if OPTIONS.output_img: 443 img_from_target_files.main( 444 [OPTIONS.output_target_files, OPTIONS.output_img]) 445 446 # Create the OTA package from the merged target files package. 447 448 if OPTIONS.output_ota: 449 ota_from_target_files.main( 450 [OPTIONS.output_target_files, OPTIONS.output_ota]) 451 452 453def main(): 454 """The main function. 455 456 Process command line arguments, then call merge_target_files to 457 perform the heavy lifting. 458 """ 459 460 common.InitLogging() 461 462 def option_handler(o, a): 463 if o == '--system-target-files': 464 logger.warning( 465 '--system-target-files has been renamed to --framework-target-files') 466 OPTIONS.framework_target_files = a 467 elif o == '--framework-target-files': 468 OPTIONS.framework_target_files = a 469 elif o == '--system-item-list': 470 logger.warning( 471 '--system-item-list has been renamed to --framework-item-list') 472 OPTIONS.framework_item_list = a 473 elif o == '--framework-item-list': 474 OPTIONS.framework_item_list = a 475 elif o == '--system-misc-info-keys': 476 logger.warning('--system-misc-info-keys has been renamed to ' 477 '--framework-misc-info-keys') 478 OPTIONS.framework_misc_info_keys = a 479 elif o == '--framework-misc-info-keys': 480 OPTIONS.framework_misc_info_keys = a 481 elif o == '--other-target-files': 482 logger.warning( 483 '--other-target-files has been renamed to --vendor-target-files') 484 OPTIONS.vendor_target_files = a 485 elif o == '--vendor-target-files': 486 OPTIONS.vendor_target_files = a 487 elif o == '--other-item-list': 488 logger.warning('--other-item-list has been renamed to --vendor-item-list') 489 OPTIONS.vendor_item_list = a 490 elif o == '--vendor-item-list': 491 OPTIONS.vendor_item_list = a 492 elif o == '--output-target-files': 493 OPTIONS.output_target_files = a 494 elif o == '--output-dir': 495 OPTIONS.output_dir = a 496 elif o == '--output-item-list': 497 OPTIONS.output_item_list = a 498 elif o == '--output-ota': 499 OPTIONS.output_ota = a 500 elif o == '--output-img': 501 OPTIONS.output_img = a 502 elif o == '--output-super-empty': 503 OPTIONS.output_super_empty = a 504 elif o == '--rebuild_recovery' or o == '--rebuild-recovery': 505 OPTIONS.rebuild_recovery = True 506 elif o == '--allow-duplicate-apkapex-keys': 507 OPTIONS.allow_duplicate_apkapex_keys = True 508 elif o == '--vendor-otatools': 509 OPTIONS.vendor_otatools = a 510 elif o == '--rebuild-sepolicy': 511 OPTIONS.rebuild_sepolicy = True 512 elif o == '--keep-tmp': 513 OPTIONS.keep_tmp = True 514 elif o == '--framework-dexpreopt-config': 515 OPTIONS.framework_dexpreopt_config = a 516 elif o == '--framework-dexpreopt-tools': 517 OPTIONS.framework_dexpreopt_tools = a 518 elif o == '--vendor-dexpreopt-config': 519 OPTIONS.vendor_dexpreopt_config = a 520 else: 521 return False 522 return True 523 524 args = common.ParseOptions( 525 sys.argv[1:], 526 __doc__, 527 extra_long_opts=[ 528 'system-target-files=', 529 'framework-target-files=', 530 'system-item-list=', 531 'framework-item-list=', 532 'system-misc-info-keys=', 533 'framework-misc-info-keys=', 534 'other-target-files=', 535 'vendor-target-files=', 536 'other-item-list=', 537 'vendor-item-list=', 538 'output-target-files=', 539 'output-dir=', 540 'output-item-list=', 541 'output-ota=', 542 'output-img=', 543 'output-super-empty=', 544 'framework-dexpreopt-config=', 545 'framework-dexpreopt-tools=', 546 'vendor-dexpreopt-config=', 547 'rebuild_recovery', 548 'rebuild-recovery', 549 'allow-duplicate-apkapex-keys', 550 'vendor-otatools=', 551 'rebuild-sepolicy', 552 'keep-tmp', 553 ], 554 extra_option_handler=option_handler) 555 556 # pylint: disable=too-many-boolean-expressions 557 if (args or OPTIONS.framework_target_files is None or 558 OPTIONS.vendor_target_files is None or 559 (OPTIONS.output_target_files is None and OPTIONS.output_dir is None) or 560 (OPTIONS.output_dir is not None and not OPTIONS.output_item_list) or 561 (OPTIONS.rebuild_recovery and not OPTIONS.rebuild_sepolicy)): 562 common.Usage(__doc__) 563 sys.exit(1) 564 565 with zipfile.ZipFile(OPTIONS.framework_target_files, allowZip64=True) as fz: 566 framework_namelist = fz.namelist() 567 with zipfile.ZipFile(OPTIONS.vendor_target_files, allowZip64=True) as vz: 568 vendor_namelist = vz.namelist() 569 570 if OPTIONS.framework_item_list: 571 OPTIONS.framework_item_list = common.LoadListFromFile( 572 OPTIONS.framework_item_list) 573 else: 574 OPTIONS.framework_item_list = merge_utils.InferItemList( 575 input_namelist=framework_namelist, framework=True) 576 OPTIONS.framework_partition_set = merge_utils.ItemListToPartitionSet( 577 OPTIONS.framework_item_list) 578 579 if OPTIONS.framework_misc_info_keys: 580 OPTIONS.framework_misc_info_keys = common.LoadListFromFile( 581 OPTIONS.framework_misc_info_keys) 582 else: 583 OPTIONS.framework_misc_info_keys = merge_utils.InferFrameworkMiscInfoKeys( 584 input_namelist=framework_namelist) 585 586 if OPTIONS.vendor_item_list: 587 OPTIONS.vendor_item_list = common.LoadListFromFile(OPTIONS.vendor_item_list) 588 else: 589 OPTIONS.vendor_item_list = merge_utils.InferItemList( 590 input_namelist=vendor_namelist, framework=False) 591 OPTIONS.vendor_partition_set = merge_utils.ItemListToPartitionSet( 592 OPTIONS.vendor_item_list) 593 594 if OPTIONS.output_item_list: 595 OPTIONS.output_item_list = common.LoadListFromFile(OPTIONS.output_item_list) 596 597 if not merge_utils.ValidateConfigLists(): 598 sys.exit(1) 599 600 temp_dir = common.MakeTempDir(prefix='merge_target_files_') 601 try: 602 merge_target_files(temp_dir) 603 finally: 604 if OPTIONS.keep_tmp: 605 logger.info('Keeping temp_dir %s', temp_dir) 606 else: 607 common.Cleanup() 608 609 610if __name__ == '__main__': 611 main() 612