1#!/usr/bin/env python3 2# 3# Copyright (C) 2018 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of 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, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16"""apexer is a command line tool for creating an APEX file, a package format for system components. 17 18Typical usage: apexer input_dir output.apex 19 20""" 21 22import sys 23 24if len(sys.path) >= 2 and "/execroot/__main__/" in sys.path[1] and "/execroot/__main__/" not in sys.path[0]: 25 # TODO(b/235287972): Remove this hack. Bazel currently has a bug where a path outside 26 # of the execroot is added to the beginning of sys.path, because the python interpreter 27 # will add the directory of the main file to the path, following symlinks as it does. 28 # This can be fixed with the -P option or the PYTHONSAFEPATH environment variable in 29 # python 3.11.0, which is not yet released. 30 del sys.path[0] 31 32import apex_build_info_pb2 33import argparse 34import hashlib 35import os 36import pkgutil 37import re 38import shlex 39import shutil 40import subprocess 41import tempfile 42import uuid 43import xml.etree.ElementTree as ET 44import zipfile 45import glob 46from apex_manifest import ValidateApexManifest 47from apex_manifest import ApexManifestError 48from apex_manifest import ParseApexManifest 49from manifest import android_ns 50from manifest import find_child_with_attribute 51from manifest import get_children_with_tag 52from manifest import get_indent 53from manifest import parse_manifest 54from manifest import write_xml 55from xml.dom import minidom 56 57tool_path_list = None 58BLOCK_SIZE = 4096 59 60 61def ParseArgs(argv): 62 parser = argparse.ArgumentParser(description='Create an APEX file') 63 parser.add_argument( 64 '-f', '--force', action='store_true', help='force overwriting output') 65 parser.add_argument( 66 '-v', '--verbose', action='store_true', help='verbose execution') 67 parser.add_argument( 68 '--manifest', 69 default='apex_manifest.pb', 70 help='path to the APEX manifest file (.pb)') 71 parser.add_argument( 72 '--manifest_json', 73 required=False, 74 help='path to the APEX manifest file (Q compatible .json)') 75 parser.add_argument( 76 '--android_manifest', 77 help='path to the AndroidManifest file. If omitted, a default one is created and used' 78 ) 79 parser.add_argument( 80 '--logging_parent', 81 help=('specify logging parent as an additional <meta-data> tag.' 82 'This value is ignored if the logging_parent meta-data tag is present.')) 83 parser.add_argument( 84 '--assets_dir', 85 help='an assets directory to be included in the APEX' 86 ) 87 parser.add_argument( 88 '--file_contexts', 89 help='selinux file contexts file. Required for "image" APEXs.') 90 parser.add_argument( 91 '--canned_fs_config', 92 help='canned_fs_config specifies uid/gid/mode of files. Required for ' + 93 '"image" APEXS.') 94 parser.add_argument( 95 '--key', help='path to the private key file. Required for "image" APEXs.') 96 parser.add_argument( 97 '--pubkey', 98 help='path to the public key file. Used to bundle the public key in APEX for testing.' 99 ) 100 parser.add_argument( 101 '--signing_args', 102 help='the extra signing arguments passed to avbtool. Used for "image" APEXs.' 103 ) 104 parser.add_argument( 105 'input_dir', 106 metavar='INPUT_DIR', 107 help='the directory having files to be packaged') 108 parser.add_argument('output', metavar='OUTPUT', help='name of the APEX file') 109 parser.add_argument( 110 '--payload_type', 111 metavar='TYPE', 112 required=False, 113 default='image', 114 choices=['zip', 'image'], 115 help='type of APEX payload being built "zip" or "image"') 116 parser.add_argument( 117 '--payload_fs_type', 118 metavar='FS_TYPE', 119 required=False, 120 default='ext4', 121 choices=['ext4', 'f2fs', 'erofs'], 122 help='type of filesystem being used for payload image "ext4", "f2fs" or "erofs"') 123 parser.add_argument( 124 '--override_apk_package_name', 125 required=False, 126 help='package name of the APK container. Default is the apex name in --manifest.' 127 ) 128 parser.add_argument( 129 '--no_hashtree', 130 required=False, 131 action='store_true', 132 help='hashtree is omitted from "image".' 133 ) 134 parser.add_argument( 135 '--android_jar_path', 136 required=False, 137 default='prebuilts/sdk/current/public/android.jar', 138 help='path to use as the source of the android API.') 139 apexer_path_in_environ = 'APEXER_TOOL_PATH' in os.environ 140 parser.add_argument( 141 '--apexer_tool_path', 142 required=not apexer_path_in_environ, 143 default=os.environ['APEXER_TOOL_PATH'].split(':') 144 if apexer_path_in_environ else None, 145 type=lambda s: s.split(':'), 146 help="""A list of directories containing all the tools used by apexer (e.g. 147 mke2fs, avbtool, etc.) separated by ':'. Can also be set using the 148 APEXER_TOOL_PATH environment variable""") 149 parser.add_argument( 150 '--target_sdk_version', 151 required=False, 152 help='Default target SDK version to use for AndroidManifest.xml') 153 parser.add_argument( 154 '--min_sdk_version', 155 required=False, 156 help='Default Min SDK version to use for AndroidManifest.xml') 157 parser.add_argument( 158 '--do_not_check_keyname', 159 required=False, 160 action='store_true', 161 help='Do not check key name. Use the name of apex instead of the basename of --key.') 162 parser.add_argument( 163 '--include_build_info', 164 required=False, 165 action='store_true', 166 help='Include build information file in the resulting apex.') 167 parser.add_argument( 168 '--include_cmd_line_in_build_info', 169 required=False, 170 action='store_true', 171 help='Include the command line in the build information file in the resulting apex. ' 172 'Note that this makes it harder to make deterministic builds.') 173 parser.add_argument( 174 '--build_info', 175 required=False, 176 help='Build information file to be used for default values.') 177 parser.add_argument( 178 '--payload_only', 179 action='store_true', 180 help='Outputs the payload image/zip only.' 181 ) 182 parser.add_argument( 183 '--unsigned_payload_only', 184 action='store_true', 185 help="""Outputs the unsigned payload image/zip only. Also, setting this flag implies 186 --payload_only is set too.""" 187 ) 188 parser.add_argument( 189 '--unsigned_payload', 190 action='store_true', 191 help="""Skip signing the apex payload. Used only for testing purposes.""" 192 ) 193 parser.add_argument( 194 '--test_only', 195 action='store_true', 196 help=( 197 'Add testOnly=true attribute to application element in ' 198 'AndroidManifest file.') 199 ) 200 201 return parser.parse_args(argv) 202 203 204def FindBinaryPath(binary): 205 for path in tool_path_list: 206 binary_path = os.path.join(path, binary) 207 if os.path.exists(binary_path): 208 return binary_path 209 raise Exception('Failed to find binary ' + binary + ' in path ' + 210 ':'.join(tool_path_list)) 211 212 213def RunCommand(cmd, verbose=False, env=None, expected_return_values={0}): 214 env = env or {} 215 env.update(os.environ.copy()) 216 217 cmd[0] = FindBinaryPath(cmd[0]) 218 219 if verbose: 220 print('Running: ' + ' '.join(cmd)) 221 p = subprocess.Popen( 222 cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) 223 output, _ = p.communicate() 224 output = output.decode() 225 226 if verbose or p.returncode not in expected_return_values: 227 print(output.rstrip()) 228 229 assert p.returncode in expected_return_values, 'Failed to execute: ' + ' '.join(cmd) 230 231 return (output, p.returncode) 232 233 234def GetDirSize(dir_name): 235 size = 0 236 for dirpath, _, filenames in os.walk(dir_name): 237 size += RoundUp(os.path.getsize(dirpath), BLOCK_SIZE) 238 for f in filenames: 239 path = os.path.join(dirpath, f) 240 if not os.path.isfile(path): 241 continue 242 size += RoundUp(os.path.getsize(path), BLOCK_SIZE) 243 return size 244 245 246def GetFilesAndDirsCount(dir_name): 247 count = 0 248 for root, dirs, files in os.walk(dir_name): 249 count += (len(dirs) + len(files)) 250 return count 251 252 253def RoundUp(size, unit): 254 assert unit & (unit - 1) == 0 255 return (size + unit - 1) & (~(unit - 1)) 256 257 258def PrepareAndroidManifest(package, version, test_only): 259 template = """\ 260<?xml version="1.0" encoding="utf-8"?> 261<manifest xmlns:android="http://schemas.android.com/apk/res/android" 262 package="{package}" android:versionCode="{version}"> 263 <!-- APEX does not have classes.dex --> 264 <application android:hasCode="false" {test_only_attribute}/> 265</manifest> 266""" 267 268 test_only_attribute = 'android:testOnly="true"' if test_only else '' 269 return template.format(package=package, version=version, 270 test_only_attribute=test_only_attribute) 271 272 273def ValidateAndroidManifest(package, android_manifest): 274 tree = ET.parse(android_manifest) 275 manifest_tag = tree.getroot() 276 package_in_xml = manifest_tag.attrib['package'] 277 if package_in_xml != package: 278 raise Exception("Package name '" + package_in_xml + "' in '" + 279 android_manifest + " differ from package name '" + package + 280 "' in the apex_manifest.pb") 281 282 283def ValidateGeneratedAndroidManifest(android_manifest, test_only): 284 tree = ET.parse(android_manifest) 285 manifest_tag = tree.getroot() 286 application_tag = manifest_tag.find('./application') 287 if test_only: 288 test_only_in_xml = application_tag.attrib[ 289 '{http://schemas.android.com/apk/res/android}testOnly'] 290 if test_only_in_xml != 'true': 291 raise Exception('testOnly attribute must be equal to true.') 292 293 294def ValidateArgs(args): 295 build_info = None 296 297 if args.build_info is not None: 298 if not os.path.exists(args.build_info): 299 print("Build info file '" + args.build_info + "' does not exist") 300 return False 301 with open(args.build_info, 'rb') as buildInfoFile: 302 build_info = apex_build_info_pb2.ApexBuildInfo() 303 build_info.ParseFromString(buildInfoFile.read()) 304 305 if not os.path.exists(args.manifest): 306 print("Manifest file '" + args.manifest + "' does not exist") 307 return False 308 309 if not os.path.isfile(args.manifest): 310 print("Manifest file '" + args.manifest + "' is not a file") 311 return False 312 313 if args.android_manifest is not None: 314 if not os.path.exists(args.android_manifest): 315 print("Android Manifest file '" + args.android_manifest + 316 "' does not exist") 317 return False 318 319 if not os.path.isfile(args.android_manifest): 320 print("Android Manifest file '" + args.android_manifest + 321 "' is not a file") 322 return False 323 elif build_info is not None: 324 with tempfile.NamedTemporaryFile(delete=False) as temp: 325 temp.write(build_info.android_manifest) 326 args.android_manifest = temp.name 327 328 if not os.path.exists(args.input_dir): 329 print("Input directory '" + args.input_dir + "' does not exist") 330 return False 331 332 if not os.path.isdir(args.input_dir): 333 print("Input directory '" + args.input_dir + "' is not a directory") 334 return False 335 336 if not args.force and os.path.exists(args.output): 337 print(args.output + ' already exists. Use --force to overwrite.') 338 return False 339 340 if args.unsigned_payload_only: 341 args.payload_only = True; 342 args.unsigned_payload = True; 343 344 if args.payload_type == 'image': 345 if not args.key and not args.unsigned_payload: 346 print('Missing --key {keyfile} argument!') 347 return False 348 349 if not args.file_contexts: 350 if build_info is not None: 351 with tempfile.NamedTemporaryFile(delete=False) as temp: 352 temp.write(build_info.file_contexts) 353 args.file_contexts = temp.name 354 else: 355 print('Missing --file_contexts {contexts} argument, or a --build_info argument!') 356 return False 357 358 if not args.canned_fs_config: 359 if not args.canned_fs_config: 360 if build_info is not None: 361 with tempfile.NamedTemporaryFile(delete=False) as temp: 362 temp.write(build_info.canned_fs_config) 363 args.canned_fs_config = temp.name 364 else: 365 print('Missing ----canned_fs_config {config} argument, or a --build_info argument!') 366 return False 367 368 if not args.target_sdk_version: 369 if build_info is not None: 370 if build_info.target_sdk_version: 371 args.target_sdk_version = build_info.target_sdk_version 372 373 if not args.no_hashtree: 374 if build_info is not None: 375 if build_info.no_hashtree: 376 args.no_hashtree = True 377 378 if not args.min_sdk_version: 379 if build_info is not None: 380 if build_info.min_sdk_version: 381 args.min_sdk_version = build_info.min_sdk_version 382 383 if not args.override_apk_package_name: 384 if build_info is not None: 385 if build_info.override_apk_package_name: 386 args.override_apk_package_name = build_info.override_apk_package_name 387 388 if not args.logging_parent: 389 if build_info is not None: 390 if build_info.logging_parent: 391 args.logging_parent = build_info.logging_parent 392 393 return True 394 395 396def GenerateBuildInfo(args): 397 build_info = apex_build_info_pb2.ApexBuildInfo() 398 if (args.include_cmd_line_in_build_info): 399 build_info.apexer_command_line = str(sys.argv) 400 401 with open(args.file_contexts, 'rb') as f: 402 build_info.file_contexts = f.read() 403 404 with open(args.canned_fs_config, 'rb') as f: 405 build_info.canned_fs_config = f.read() 406 407 with open(args.android_manifest, 'rb') as f: 408 build_info.android_manifest = f.read() 409 410 if args.target_sdk_version: 411 build_info.target_sdk_version = args.target_sdk_version 412 413 if args.min_sdk_version: 414 build_info.min_sdk_version = args.min_sdk_version 415 416 if args.no_hashtree: 417 build_info.no_hashtree = True 418 419 if args.override_apk_package_name: 420 build_info.override_apk_package_name = args.override_apk_package_name 421 422 if args.logging_parent: 423 build_info.logging_parent = args.logging_parent 424 425 if args.payload_type == 'image': 426 build_info.payload_fs_type = args.payload_fs_type 427 428 return build_info 429 430 431def AddLoggingParent(android_manifest, logging_parent_value): 432 """Add logging parent as an additional <meta-data> tag. 433 434 Args: 435 android_manifest: A string representing AndroidManifest.xml 436 logging_parent_value: A string representing the logging 437 parent value. 438 Raises: 439 RuntimeError: Invalid manifest 440 Returns: 441 A path to modified AndroidManifest.xml 442 """ 443 doc = minidom.parse(android_manifest) 444 manifest = parse_manifest(doc) 445 logging_parent_key = 'android.content.pm.LOGGING_PARENT' 446 elems = get_children_with_tag(manifest, 'application') 447 application = elems[0] if len(elems) == 1 else None 448 if len(elems) > 1: 449 raise RuntimeError('found multiple <application> tags') 450 elif not elems: 451 application = doc.createElement('application') 452 indent = get_indent(manifest.firstChild, 1) 453 first = manifest.firstChild 454 manifest.insertBefore(doc.createTextNode(indent), first) 455 manifest.insertBefore(application, first) 456 457 indent = get_indent(application.firstChild, 2) 458 last = application.lastChild 459 if last is not None and last.nodeType != minidom.Node.TEXT_NODE: 460 last = None 461 462 if not find_child_with_attribute(application, 'meta-data', android_ns, 463 'name', logging_parent_key): 464 ul = doc.createElement('meta-data') 465 ul.setAttributeNS(android_ns, 'android:name', logging_parent_key) 466 ul.setAttributeNS(android_ns, 'android:value', logging_parent_value) 467 application.insertBefore(doc.createTextNode(indent), last) 468 application.insertBefore(ul, last) 469 last = application.lastChild 470 471 if last and last.nodeType != minidom.Node.TEXT_NODE: 472 indent = get_indent(application.previousSibling, 1) 473 application.appendChild(doc.createTextNode(indent)) 474 475 with tempfile.NamedTemporaryFile(delete=False, mode='w') as temp: 476 write_xml(temp, doc) 477 return temp.name 478 479 480def ShaHashFiles(file_paths): 481 """get hash for a number of files.""" 482 h = hashlib.sha256() 483 for file_path in file_paths: 484 with open(file_path, 'rb') as file: 485 while True: 486 chunk = file.read(h.block_size) 487 if not chunk: 488 break 489 h.update(chunk) 490 return h.hexdigest() 491 492 493def CreateImageExt4(args, work_dir, manifests_dir, img_file): 494 """Create image for ext4 file system.""" 495 496 lost_found_location = os.path.join(args.input_dir, 'lost+found') 497 if os.path.exists(lost_found_location): 498 print('Warning: input_dir contains a lost+found/ root folder, which ' 499 'has been known to cause non-deterministic apex builds.') 500 501 # sufficiently big = size + 16MB margin 502 size_in_mb = (GetDirSize(args.input_dir) // (1024 * 1024)) 503 size_in_mb += 16 504 505 # Margin is for files that are not under args.input_dir. this consists of 506 # n inodes for apex_manifest files and 11 reserved inodes for ext4. 507 # TOBO(b/122991714) eliminate these details. Use build_image.py which 508 # determines the optimal inode count by first building an image and then 509 # count the inodes actually used. 510 inode_num_margin = GetFilesAndDirsCount(manifests_dir) + 11 511 inode_num = GetFilesAndDirsCount(args.input_dir) + inode_num_margin 512 513 cmd = ['mke2fs'] 514 cmd.extend(['-O', '^has_journal']) # because image is read-only 515 cmd.extend(['-b', str(BLOCK_SIZE)]) 516 cmd.extend(['-m', '0']) # reserved block percentage 517 cmd.extend(['-t', 'ext4']) 518 cmd.extend(['-I', '256']) # inode size 519 cmd.extend(['-N', str(inode_num)]) 520 uu = str(uuid.uuid5(uuid.NAMESPACE_URL, 'www.android.com')) 521 cmd.extend(['-U', uu]) 522 cmd.extend(['-E', 'hash_seed=' + uu]) 523 cmd.append(img_file) 524 cmd.append(str(size_in_mb) + 'M') 525 with tempfile.NamedTemporaryFile(dir=work_dir, 526 suffix='mke2fs.conf') as conf_file: 527 conf_data = pkgutil.get_data('apexer', 'mke2fs.conf') 528 conf_file.write(conf_data) 529 conf_file.flush() 530 RunCommand(cmd, args.verbose, 531 {'MKE2FS_CONFIG': conf_file.name, 'E2FSPROGS_FAKE_TIME': '1'}) 532 533 # Compile the file context into the binary form 534 compiled_file_contexts = os.path.join(work_dir, 'file_contexts.bin') 535 cmd = ['sefcontext_compile'] 536 cmd.extend(['-o', compiled_file_contexts]) 537 cmd.append(args.file_contexts) 538 RunCommand(cmd, args.verbose) 539 540 # Add files to the image file 541 cmd = ['e2fsdroid'] 542 cmd.append('-e') # input is not android_sparse_file 543 cmd.extend(['-f', args.input_dir]) 544 cmd.extend(['-T', '0']) # time is set to epoch 545 cmd.extend(['-S', compiled_file_contexts]) 546 cmd.extend(['-C', args.canned_fs_config]) 547 cmd.extend(['-a', '/']) 548 cmd.append('-s') # share dup blocks 549 cmd.append(img_file) 550 RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'}) 551 552 cmd = ['e2fsdroid'] 553 cmd.append('-e') # input is not android_sparse_file 554 cmd.extend(['-f', manifests_dir]) 555 cmd.extend(['-T', '0']) # time is set to epoch 556 cmd.extend(['-S', compiled_file_contexts]) 557 cmd.extend(['-C', args.canned_fs_config]) 558 cmd.extend(['-a', '/']) 559 cmd.append('-s') # share dup blocks 560 cmd.append(img_file) 561 RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'}) 562 563 # Resize the image file to save space 564 cmd = ['resize2fs'] 565 cmd.append('-M') # shrink as small as possible 566 cmd.append(img_file) 567 RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'}) 568 569 570def CreateImageF2fs(args, manifests_dir, img_file): 571 """Create image for f2fs file system.""" 572 # F2FS requires a ~100M minimum size (necessary for ART, could be reduced 573 # a bit for other) 574 # TODO(b/158453869): relax these requirements for readonly devices 575 size_in_mb = (GetDirSize(args.input_dir) // (1024 * 1024)) 576 size_in_mb += 100 577 578 # Create an empty image 579 cmd = ['/usr/bin/fallocate'] 580 cmd.extend(['-l', str(size_in_mb) + 'M']) 581 cmd.append(img_file) 582 RunCommand(cmd, args.verbose) 583 584 # Format the image to F2FS 585 cmd = ['make_f2fs'] 586 cmd.extend(['-g', 'android']) 587 uu = str(uuid.uuid5(uuid.NAMESPACE_URL, 'www.android.com')) 588 cmd.extend(['-U', uu]) 589 cmd.extend(['-T', '0']) 590 cmd.append('-r') # sets checkpointing seed to 0 to remove random bits 591 cmd.append(img_file) 592 RunCommand(cmd, args.verbose) 593 594 # Add files to the image 595 cmd = ['sload_f2fs'] 596 cmd.extend(['-C', args.canned_fs_config]) 597 cmd.extend(['-f', manifests_dir]) 598 cmd.extend(['-s', args.file_contexts]) 599 cmd.extend(['-T', '0']) 600 cmd.append(img_file) 601 RunCommand(cmd, args.verbose, expected_return_values={0, 1}) 602 603 cmd = ['sload_f2fs'] 604 cmd.extend(['-C', args.canned_fs_config]) 605 cmd.extend(['-f', args.input_dir]) 606 cmd.extend(['-s', args.file_contexts]) 607 cmd.extend(['-T', '0']) 608 cmd.append(img_file) 609 RunCommand(cmd, args.verbose, expected_return_values={0, 1}) 610 611 # TODO(b/158453869): resize the image file to save space 612 613 614def CreateImageErofs(args, work_dir, manifests_dir, img_file): 615 """Create image for erofs file system.""" 616 # mkfs.erofs doesn't support multiple input 617 618 tmp_input_dir = os.path.join(work_dir, 'tmp_input_dir') 619 os.mkdir(tmp_input_dir) 620 cmd = ['/bin/cp', '-ra'] 621 cmd.extend(glob.glob(manifests_dir + '/*')) 622 cmd.extend(glob.glob(args.input_dir + '/*')) 623 cmd.append(tmp_input_dir) 624 RunCommand(cmd, args.verbose) 625 626 cmd = ['make_erofs'] 627 cmd.extend(['-z', 'lz4hc']) 628 cmd.extend(['--fs-config-file', args.canned_fs_config]) 629 cmd.extend(['--file-contexts', args.file_contexts]) 630 uu = str(uuid.uuid5(uuid.NAMESPACE_URL, 'www.android.com')) 631 cmd.extend(['-U', uu]) 632 cmd.extend(['-T', '0']) 633 cmd.extend([img_file, tmp_input_dir]) 634 RunCommand(cmd, args.verbose) 635 shutil.rmtree(tmp_input_dir) 636 637 # The minimum image size of erofs is 4k, which will cause an error 638 # when execute generate_hash_tree in avbtool 639 cmd = ['/bin/ls', '-lgG', img_file] 640 output, _ = RunCommand(cmd, verbose=False) 641 image_size = int(output.split()[2]) 642 if image_size == 4096: 643 cmd = ['/usr/bin/fallocate', '-l', '8k', img_file] 644 RunCommand(cmd, verbose=False) 645 646 647def CreateImage(args, work_dir, manifests_dir, img_file): 648 """create payload image.""" 649 if args.payload_fs_type == 'ext4': 650 CreateImageExt4(args, work_dir, manifests_dir, img_file) 651 elif args.payload_fs_type == 'f2fs': 652 CreateImageF2fs(args, manifests_dir, img_file) 653 elif args.payload_fs_type == 'erofs': 654 CreateImageErofs(args, work_dir, manifests_dir, img_file) 655 656 657def SignImage(args, manifest_apex, img_file): 658 """sign payload image. 659 660 Args: 661 args: apexer options 662 manifest_apex: apex manifest proto 663 img_file: unsigned payload image file 664 """ 665 666 if args.do_not_check_keyname or args.unsigned_payload: 667 key_name = manifest_apex.name 668 else: 669 key_name = os.path.basename(os.path.splitext(args.key)[0]) 670 671 cmd = ['avbtool'] 672 cmd.append('add_hashtree_footer') 673 cmd.append('--do_not_generate_fec') 674 cmd.extend(['--algorithm', 'SHA256_RSA4096']) 675 cmd.extend(['--hash_algorithm', 'sha256']) 676 cmd.extend(['--key', args.key]) 677 cmd.extend(['--prop', 'apex.key:' + key_name]) 678 # Set up the salt based on manifest content which includes name 679 # and version 680 salt = hashlib.sha256(manifest_apex.SerializeToString()).hexdigest() 681 cmd.extend(['--salt', salt]) 682 cmd.extend(['--image', img_file]) 683 if args.no_hashtree: 684 cmd.append('--no_hashtree') 685 if args.signing_args: 686 cmd.extend(shlex.split(args.signing_args)) 687 RunCommand(cmd, args.verbose) 688 689 # Get the minimum size of the partition required. 690 # TODO(b/113320014) eliminate this step 691 info, _ = RunCommand(['avbtool', 'info_image', '--image', img_file], 692 args.verbose) 693 vbmeta_offset = int(re.search('VBMeta\ offset:\ *([0-9]+)', info).group(1)) 694 vbmeta_size = int(re.search('VBMeta\ size:\ *([0-9]+)', info).group(1)) 695 partition_size = RoundUp(vbmeta_offset + vbmeta_size, 696 BLOCK_SIZE) + BLOCK_SIZE 697 698 # Resize to the minimum size 699 # TODO(b/113320014) eliminate this step 700 cmd = ['avbtool'] 701 cmd.append('resize_image') 702 cmd.extend(['--image', img_file]) 703 cmd.extend(['--partition_size', str(partition_size)]) 704 RunCommand(cmd, args.verbose) 705 706 707def CreateApexPayload(args, work_dir, content_dir, manifests_dir, 708 manifest_apex): 709 """Create payload. 710 711 Args: 712 args: apexer options 713 work_dir: apex container working directory 714 content_dir: the working directory for payload contents 715 manifests_dir: manifests directory 716 manifest_apex: apex manifest proto 717 718 Returns: 719 payload file 720 """ 721 if args.payload_type == 'image': 722 img_file = os.path.join(content_dir, 'apex_payload.img') 723 CreateImage(args, work_dir, manifests_dir, img_file) 724 if not args.unsigned_payload: 725 SignImage(args, manifest_apex, img_file) 726 else: 727 img_file = os.path.join(content_dir, 'apex_payload.zip') 728 cmd = ['soong_zip'] 729 cmd.extend(['-o', img_file]) 730 cmd.extend(['-C', args.input_dir]) 731 cmd.extend(['-D', args.input_dir]) 732 cmd.extend(['-C', manifests_dir]) 733 cmd.extend(['-D', manifests_dir]) 734 RunCommand(cmd, args.verbose) 735 return img_file 736 737 738def CreateAndroidManifestXml(args, work_dir, manifest_apex): 739 """Create AndroidManifest.xml file. 740 741 Args: 742 args: apexer options 743 work_dir: apex container working directory 744 manifest_apex: apex manifest proto 745 746 Returns: 747 AndroidManifest.xml file inside the work dir 748 """ 749 android_manifest_file = os.path.join(work_dir, 'AndroidManifest.xml') 750 if not args.android_manifest: 751 if args.verbose: 752 print('Creating AndroidManifest ' + android_manifest_file) 753 with open(android_manifest_file, 'w') as f: 754 app_package_name = manifest_apex.name 755 f.write(PrepareAndroidManifest(app_package_name, manifest_apex.version, 756 args.test_only)) 757 args.android_manifest = android_manifest_file 758 ValidateGeneratedAndroidManifest(args.android_manifest, args.test_only) 759 else: 760 ValidateAndroidManifest(manifest_apex.name, args.android_manifest) 761 shutil.copyfile(args.android_manifest, android_manifest_file) 762 763 # If logging parent is specified, add it to the AndroidManifest. 764 if args.logging_parent: 765 android_manifest_file = AddLoggingParent(android_manifest_file, 766 args.logging_parent) 767 return android_manifest_file 768 769 770def CreateApex(args, work_dir): 771 if not ValidateArgs(args): 772 return False 773 774 if args.verbose: 775 print('Using tools from ' + str(tool_path_list)) 776 777 def CopyFile(src, dst): 778 if args.verbose: 779 print('Copying ' + src + ' to ' + dst) 780 shutil.copyfile(src, dst) 781 782 try: 783 manifest_apex = CreateApexManifest(args.manifest) 784 except ApexManifestError as err: 785 print("'" + args.manifest + "' is not a valid manifest file") 786 print(err.errmessage) 787 return False 788 789 # Create content dir and manifests dir, the manifests dir is used to 790 # create the payload image 791 content_dir = os.path.join(work_dir, 'content') 792 os.mkdir(content_dir) 793 manifests_dir = os.path.join(work_dir, 'manifests') 794 os.mkdir(manifests_dir) 795 796 # Create AndroidManifest.xml file first so that we can hash the file 797 # and store the hashed value in the manifest proto buf that goes into 798 # the payload image. So any change in this file will ensure changes 799 # in payload image file 800 android_manifest_file = CreateAndroidManifestXml( 801 args, work_dir, manifest_apex) 802 803 # APEX manifest is also included in the image. The manifest is included 804 # twice: once inside the image and once outside the image (but still 805 # within the zip container). 806 with open(os.path.join(manifests_dir, 'apex_manifest.pb'), 'wb') as f: 807 f.write(manifest_apex.SerializeToString()) 808 with open(os.path.join(content_dir, 'apex_manifest.pb'), 'wb') as f: 809 f.write(manifest_apex.SerializeToString()) 810 if args.manifest_json: 811 CopyFile(args.manifest_json, 812 os.path.join(manifests_dir, 'apex_manifest.json')) 813 CopyFile(args.manifest_json, 814 os.path.join(content_dir, 'apex_manifest.json')) 815 816 # Create payload 817 img_file = CreateApexPayload(args, work_dir, content_dir, manifests_dir, 818 manifest_apex) 819 820 if args.unsigned_payload_only or args.payload_only: 821 shutil.copyfile(img_file, args.output) 822 if args.verbose: 823 if args.unsigned_payload_only: 824 print('Created (unsigned payload only) ' + args.output) 825 else: 826 print('Created (payload only) ' + args.output) 827 return True 828 829 # copy the public key, if specified 830 if args.pubkey: 831 shutil.copyfile(args.pubkey, os.path.join(content_dir, 'apex_pubkey')) 832 833 if args.include_build_info: 834 build_info = GenerateBuildInfo(args) 835 with open(os.path.join(content_dir, 'apex_build_info.pb'), 'wb') as f: 836 f.write(build_info.SerializeToString()) 837 838 apk_file = os.path.join(work_dir, 'apex.apk') 839 cmd = ['aapt2'] 840 cmd.append('link') 841 cmd.extend(['--manifest', android_manifest_file]) 842 if args.override_apk_package_name: 843 cmd.extend(['--rename-manifest-package', args.override_apk_package_name]) 844 # This version from apex_manifest.json is used when versionCode isn't 845 # specified in AndroidManifest.xml 846 cmd.extend(['--version-code', str(manifest_apex.version)]) 847 if manifest_apex.versionName: 848 cmd.extend(['--version-name', manifest_apex.versionName]) 849 if args.target_sdk_version: 850 cmd.extend(['--target-sdk-version', args.target_sdk_version]) 851 if args.min_sdk_version: 852 cmd.extend(['--min-sdk-version', args.min_sdk_version]) 853 else: 854 # Default value for minSdkVersion. 855 cmd.extend(['--min-sdk-version', '29']) 856 if args.assets_dir: 857 cmd.extend(['-A', args.assets_dir]) 858 cmd.extend(['-o', apk_file]) 859 cmd.extend(['-I', args.android_jar_path]) 860 RunCommand(cmd, args.verbose) 861 862 zip_file = os.path.join(work_dir, 'apex.zip') 863 CreateZip(content_dir, zip_file) 864 MergeZips([apk_file, zip_file], args.output) 865 866 if args.verbose: 867 print('Created ' + args.output) 868 869 return True 870 871def CreateApexManifest(manifest_path): 872 try: 873 manifest_apex = ParseApexManifest(manifest_path) 874 ValidateApexManifest(manifest_apex) 875 return manifest_apex 876 except IOError: 877 raise ApexManifestError("Cannot read manifest file: '" + manifest_path + "'") 878 879class TempDirectory(object): 880 881 def __enter__(self): 882 self.name = tempfile.mkdtemp() 883 return self.name 884 885 def __exit__(self, *unused): 886 shutil.rmtree(self.name) 887 888 889def CreateZip(content_dir, apex_zip): 890 with zipfile.ZipFile(apex_zip, 'w', compression=zipfile.ZIP_DEFLATED) as out: 891 for root, _, files in os.walk(content_dir): 892 for file in files: 893 path = os.path.join(root, file) 894 rel_path = os.path.relpath(path, content_dir) 895 # "apex_payload.img" shouldn't be compressed 896 if rel_path == 'apex_payload.img': 897 out.write(path, rel_path, compress_type=zipfile.ZIP_STORED) 898 else: 899 out.write(path, rel_path) 900 901 902def MergeZips(zip_files, output_zip): 903 with zipfile.ZipFile(output_zip, 'w') as out: 904 for file in zip_files: 905 # copy to output_zip 906 with zipfile.ZipFile(file, 'r') as inzip: 907 for info in inzip.infolist(): 908 # reset timestamp for deterministic output 909 info.date_time = (1980, 1, 1, 0, 0, 0) 910 # reset filemode for deterministic output. The high 16 bits are for 911 # filemode. 0x81A4 corresponds to 0o100644(a regular file with 912 # '-rw-r--r--' permission). 913 info.external_attr = 0x81A40000 914 # "apex_payload.img" should be 4K aligned 915 if info.filename == 'apex_payload.img': 916 data_offset = out.fp.tell() + len(info.FileHeader()) 917 info.extra = b'\0' * (BLOCK_SIZE - data_offset % BLOCK_SIZE) 918 data = inzip.read(info) 919 out.writestr(info, data) 920 921 922def main(argv): 923 global tool_path_list 924 args = ParseArgs(argv) 925 tool_path_list = args.apexer_tool_path 926 with TempDirectory() as work_dir: 927 success = CreateApex(args, work_dir) 928 929 if not success: 930 sys.exit(1) 931 932 933if __name__ == '__main__': 934 main(sys.argv[1:]) 935