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