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