1#!/usr/bin/env python 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 17""" 18apexer is a command line tool for creating an APEX file, a package format 19for system components. 20 21Typical usage: apexer input_dir output.apex 22 23""" 24 25import argparse 26import hashlib 27import os 28import re 29import shutil 30import subprocess 31import sys 32import tempfile 33import uuid 34import xml.etree.ElementTree as ET 35from apex_manifest import ValidateApexManifest 36from apex_manifest import ApexManifestError 37 38tool_path_list = None 39BLOCK_SIZE = 4096 40 41def ParseArgs(argv): 42 parser = argparse.ArgumentParser(description='Create an APEX file') 43 parser.add_argument('-f', '--force', action='store_true', 44 help='force overwriting output') 45 parser.add_argument('-v', '--verbose', action='store_true', 46 help='verbose execution') 47 parser.add_argument('--manifest', default='apex_manifest.json', 48 help='path to the APEX manifest file') 49 parser.add_argument('--android_manifest', 50 help='path to the AndroidManifest file. If omitted, a default one is created and used') 51 parser.add_argument('--assets_dir', 52 help='an assets directory to be included in the APEX') 53 parser.add_argument('--file_contexts', 54 help='selinux file contexts file. Required for "image" APEXs.') 55 parser.add_argument('--canned_fs_config', 56 help='canned_fs_config specifies uid/gid/mode of files. Required for ' + 57 '"image" APEXS.') 58 parser.add_argument('--key', 59 help='path to the private key file. Required for "image" APEXs.') 60 parser.add_argument('--pubkey', 61 help='path to the public key file. Used to bundle the public key in APEX for testing.') 62 parser.add_argument('input_dir', metavar='INPUT_DIR', 63 help='the directory having files to be packaged') 64 parser.add_argument('output', metavar='OUTPUT', 65 help='name of the APEX file') 66 parser.add_argument('--payload_type', metavar='TYPE', required=False, default="image", 67 choices=["zip", "image"], 68 help='type of APEX payload being built "zip" or "image"') 69 parser.add_argument('--override_apk_package_name', required=False, 70 help='package name of the APK container. Default is the apex name in --manifest.') 71 parser.add_argument('--android_jar_path', required=False, 72 default="prebuilts/sdk/current/public/android.jar", 73 help='path to use as the source of the android API.') 74 apexer_path_in_environ = "APEXER_TOOL_PATH" in os.environ 75 parser.add_argument('--apexer_tool_path', required=not apexer_path_in_environ, 76 default=os.environ['APEXER_TOOL_PATH'].split(":") if apexer_path_in_environ else None, 77 type=lambda s: s.split(":"), 78 help="""A list of directories containing all the tools used by apexer (e.g. 79 mke2fs, avbtool, etc.) separated by ':'. Can also be set using the 80 APEXER_TOOL_PATH environment variable""") 81 parser.add_argument('--target_sdk_version', required=False, 82 help='Default target SDK version to use for AndroidManifest.xml') 83 return parser.parse_args(argv) 84 85def FindBinaryPath(binary): 86 for path in tool_path_list: 87 binary_path = os.path.join(path, binary) 88 if os.path.exists(binary_path): 89 return binary_path 90 raise Exception("Failed to find binary " + binary + " in path " + ":".join(tool_path_list)) 91 92def RunCommand(cmd, verbose=False, env=None): 93 env = env or {} 94 env.update(os.environ.copy()) 95 96 cmd[0] = FindBinaryPath(cmd[0]) 97 98 if verbose: 99 print("Running: " + " ".join(cmd)) 100 p = subprocess.Popen( 101 cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) 102 output, _ = p.communicate() 103 104 if verbose or p.returncode is not 0: 105 print(output.rstrip()) 106 107 assert p.returncode is 0, "Failed to execute: " + " ".join(cmd) 108 109 return (output, p.returncode) 110 111def GetDirSize(dir_name): 112 size = 0 113 for dirpath, _, filenames in os.walk(dir_name): 114 size += RoundUp(os.path.getsize(dirpath), BLOCK_SIZE) 115 for f in filenames: 116 size += RoundUp(os.path.getsize(os.path.join(dirpath, f)), BLOCK_SIZE) 117 return size 118 119def GetFilesAndDirsCount(dir_name): 120 count = 0; 121 for root, dirs, files in os.walk(dir_name): 122 count += (len(dirs) + len(files)) 123 return count 124 125def RoundUp(size, unit): 126 assert unit & (unit - 1) == 0 127 return (size + unit - 1) & (~(unit - 1)) 128 129def PrepareAndroidManifest(package, version): 130 template = """\ 131<?xml version="1.0" encoding="utf-8"?> 132<manifest xmlns:android="http://schemas.android.com/apk/res/android" 133 package="{package}" android:versionCode="{version}"> 134 <!-- APEX does not have classes.dex --> 135 <application android:hasCode="false" /> 136</manifest> 137""" 138 return template.format(package=package, version=version) 139 140def ValidateAndroidManifest(package, android_manifest): 141 tree = ET.parse(android_manifest) 142 manifest_tag = tree.getroot() 143 package_in_xml = manifest_tag.attrib['package'] 144 if package_in_xml != package: 145 raise Exception("Package name '" + package_in_xml + "' in '" + android_manifest + 146 " differ from package name '" + package + "' in the apex_manifest.json") 147 148def ValidateArgs(args): 149 if not os.path.exists(args.manifest): 150 print("Manifest file '" + args.manifest + "' does not exist") 151 return False 152 153 if not os.path.isfile(args.manifest): 154 print("Manifest file '" + args.manifest + "' is not a file") 155 return False 156 157 if args.android_manifest is not None: 158 if not os.path.exists(args.android_manifest): 159 print("Android Manifest file '" + args.android_manifest + "' does not exist") 160 return False 161 162 if not os.path.isfile(args.android_manifest): 163 print("Android Manifest file '" + args.android_manifest + "' is not a file") 164 return False 165 166 if not os.path.exists(args.input_dir): 167 print("Input directory '" + args.input_dir + "' does not exist") 168 return False 169 170 if not os.path.isdir(args.input_dir): 171 print("Input directory '" + args.input_dir + "' is not a directory") 172 return False 173 174 if not args.force and os.path.exists(args.output): 175 print(args.output + ' already exists. Use --force to overwrite.') 176 return False 177 178 if args.payload_type == "image": 179 if not args.key: 180 print("Missing --key {keyfile} argument!") 181 return False 182 183 if not args.file_contexts: 184 print("Missing --file_contexts {contexts} argument!") 185 return False 186 187 if not args.canned_fs_config: 188 print("Missing --canned_fs_config {config} argument!") 189 return False 190 191 return True 192 193def CreateApex(args, work_dir): 194 if not ValidateArgs(args): 195 return False 196 197 if args.verbose: 198 print "Using tools from " + str(tool_path_list) 199 200 try: 201 with open(args.manifest, "r") as f: 202 manifest_raw = f.read() 203 manifest_apex = ValidateApexManifest(manifest_raw) 204 except ApexManifestError as err: 205 print("'" + args.manifest + "' is not a valid manifest file") 206 print err.errmessage 207 return False 208 except IOError: 209 print("Cannot read manifest file: '" + args.manifest + "'") 210 return False 211 212 # create an empty ext4 image that is sufficiently big 213 # sufficiently big = size + 16MB margin 214 size_in_mb = (GetDirSize(args.input_dir) / (1024*1024)) + 16 215 216 content_dir = os.path.join(work_dir, 'content') 217 os.mkdir(content_dir) 218 219 # APEX manifest is also included in the image. The manifest is included 220 # twice: once inside the image and once outside the image (but still 221 # within the zip container). 222 manifests_dir = os.path.join(work_dir, 'manifests') 223 os.mkdir(manifests_dir) 224 manifest_file = os.path.join(manifests_dir, 'apex_manifest.json') 225 if args.verbose: 226 print('Copying ' + args.manifest + ' to ' + manifest_file) 227 shutil.copyfile(args.manifest, manifest_file) 228 229 if args.payload_type == 'image': 230 key_name = os.path.basename(os.path.splitext(args.key)[0]) 231 232 if manifest_apex.name != key_name: 233 print("package name '" + manifest_apex.name + "' does not match with key name '" + key_name + "'") 234 return False 235 img_file = os.path.join(content_dir, 'apex_payload.img') 236 237 # margin is for files that are not under args.input_dir. this consists of 238 # one inode for apex_manifest.json and 11 reserved inodes for ext4. 239 # TOBO(b/122991714) eliminate these details. use build_image.py which 240 # determines the optimal inode count by first building an image and then 241 # count the inodes actually used. 242 inode_num_margin = 12 243 inode_num = GetFilesAndDirsCount(args.input_dir) + inode_num_margin 244 245 cmd = ['mke2fs'] 246 cmd.extend(['-O', '^has_journal']) # because image is read-only 247 cmd.extend(['-b', str(BLOCK_SIZE)]) 248 cmd.extend(['-m', '0']) # reserved block percentage 249 cmd.extend(['-t', 'ext4']) 250 cmd.extend(['-I', '256']) # inode size 251 cmd.extend(['-N', str(inode_num)]) 252 uu = str(uuid.uuid5(uuid.NAMESPACE_URL, "www.android.com")) 253 cmd.extend(['-U', uu]) 254 cmd.extend(['-E', 'hash_seed=' + uu]) 255 cmd.append(img_file) 256 cmd.append(str(size_in_mb) + 'M') 257 RunCommand(cmd, args.verbose, {"E2FSPROGS_FAKE_TIME": "1"}) 258 259 # Compile the file context into the binary form 260 compiled_file_contexts = os.path.join(work_dir, 'file_contexts.bin') 261 cmd = ['sefcontext_compile'] 262 cmd.extend(['-o', compiled_file_contexts]) 263 cmd.append(args.file_contexts) 264 RunCommand(cmd, args.verbose) 265 266 # Add files to the image file 267 cmd = ['e2fsdroid'] 268 cmd.append('-e') # input is not android_sparse_file 269 cmd.extend(['-f', args.input_dir]) 270 cmd.extend(['-T', '0']) # time is set to epoch 271 cmd.extend(['-S', compiled_file_contexts]) 272 cmd.extend(['-C', args.canned_fs_config]) 273 cmd.append('-s') # share dup blocks 274 cmd.append(img_file) 275 RunCommand(cmd, args.verbose, {"E2FSPROGS_FAKE_TIME": "1"}) 276 277 cmd = ['e2fsdroid'] 278 cmd.append('-e') # input is not android_sparse_file 279 cmd.extend(['-f', manifests_dir]) 280 cmd.extend(['-T', '0']) # time is set to epoch 281 cmd.extend(['-S', compiled_file_contexts]) 282 cmd.extend(['-C', args.canned_fs_config]) 283 cmd.append('-s') # share dup blocks 284 cmd.append(img_file) 285 RunCommand(cmd, args.verbose, {"E2FSPROGS_FAKE_TIME": "1"}) 286 287 # Resize the image file to save space 288 cmd = ['resize2fs'] 289 cmd.append('-M') # shrink as small as possible 290 cmd.append(img_file) 291 RunCommand(cmd, args.verbose, {"E2FSPROGS_FAKE_TIME": "1"}) 292 293 294 cmd = ['avbtool'] 295 cmd.append('add_hashtree_footer') 296 cmd.append('--do_not_generate_fec') 297 cmd.extend(['--algorithm', 'SHA256_RSA4096']) 298 cmd.extend(['--key', args.key]) 299 cmd.extend(['--prop', "apex.key:" + key_name]) 300 # Set up the salt based on manifest content which includes name 301 # and version 302 salt = hashlib.sha256(manifest_raw).hexdigest() 303 cmd.extend(['--salt', salt]) 304 cmd.extend(['--image', img_file]) 305 RunCommand(cmd, args.verbose) 306 307 # Get the minimum size of the partition required. 308 # TODO(b/113320014) eliminate this step 309 info, _ = RunCommand(['avbtool', 'info_image', '--image', img_file], args.verbose) 310 vbmeta_offset = int(re.search('VBMeta\ offset:\ *([0-9]+)', info).group(1)) 311 vbmeta_size = int(re.search('VBMeta\ size:\ *([0-9]+)', info).group(1)) 312 partition_size = RoundUp(vbmeta_offset + vbmeta_size, BLOCK_SIZE) + BLOCK_SIZE 313 314 # Resize to the minimum size 315 # TODO(b/113320014) eliminate this step 316 cmd = ['avbtool'] 317 cmd.append('resize_image') 318 cmd.extend(['--image', img_file]) 319 cmd.extend(['--partition_size', str(partition_size)]) 320 RunCommand(cmd, args.verbose) 321 else: 322 img_file = os.path.join(content_dir, 'apex_payload.zip') 323 cmd = ['soong_zip'] 324 cmd.extend(['-o', img_file]) 325 cmd.extend(['-C', args.input_dir]) 326 cmd.extend(['-D', args.input_dir]) 327 cmd.extend(['-C', manifests_dir]) 328 cmd.extend(['-D', manifests_dir]) 329 RunCommand(cmd, args.verbose) 330 331 # package the image file and APEX manifest as an APK. 332 # The AndroidManifest file is automatically generated if not given. 333 android_manifest_file = os.path.join(work_dir, 'AndroidManifest.xml') 334 if not args.android_manifest: 335 if args.verbose: 336 print('Creating AndroidManifest ' + android_manifest_file) 337 with open(android_manifest_file, 'w+') as f: 338 app_package_name = manifest_apex.name 339 f.write(PrepareAndroidManifest(app_package_name, manifest_apex.version)) 340 else: 341 ValidateAndroidManifest(manifest_apex.name, args.android_manifest) 342 shutil.copyfile(args.android_manifest, android_manifest_file) 343 344 # copy manifest to the content dir so that it is also accessible 345 # without mounting the image 346 shutil.copyfile(args.manifest, os.path.join(content_dir, 'apex_manifest.json')) 347 348 # copy the public key, if specified 349 if args.pubkey: 350 shutil.copyfile(args.pubkey, os.path.join(content_dir, "apex_pubkey")) 351 352 apk_file = os.path.join(work_dir, 'apex.apk') 353 cmd = ['aapt2'] 354 cmd.append('link') 355 cmd.extend(['--manifest', android_manifest_file]) 356 if args.override_apk_package_name: 357 cmd.extend(['--rename-manifest-package', args.override_apk_package_name]) 358 # This version from apex_manifest.json is used when versionCode isn't 359 # specified in AndroidManifest.xml 360 cmd.extend(['--version-code', str(manifest_apex.version)]) 361 if manifest_apex.versionName: 362 cmd.extend(['--version-name', manifest_apex.versionName]) 363 if args.target_sdk_version: 364 cmd.extend(['--target-sdk-version', args.target_sdk_version]) 365 if args.assets_dir: 366 cmd.extend(['-A', args.assets_dir]) 367 # Default value for minSdkVersion. 368 cmd.extend(['--min-sdk-version', '29']) 369 cmd.extend(['-o', apk_file]) 370 cmd.extend(['-I', args.android_jar_path]) 371 RunCommand(cmd, args.verbose) 372 373 zip_file = os.path.join(work_dir, 'apex.zip') 374 cmd = ['soong_zip'] 375 cmd.append('-d') # include directories 376 cmd.extend(['-C', content_dir]) # relative root 377 cmd.extend(['-D', content_dir]) # input dir 378 for file_ in os.listdir(content_dir): 379 if os.path.isfile(os.path.join(content_dir, file_)): 380 cmd.extend(['-s', file_]) # don't compress any files 381 cmd.extend(['-o', zip_file]) 382 RunCommand(cmd, args.verbose) 383 384 unaligned_apex_file = os.path.join(work_dir, 'unaligned.apex') 385 cmd = ['merge_zips'] 386 cmd.append('-j') # sort 387 cmd.append(unaligned_apex_file) # output 388 cmd.append(apk_file) # input 389 cmd.append(zip_file) # input 390 RunCommand(cmd, args.verbose) 391 392 # Align the files at page boundary for efficient access 393 cmd = ['zipalign'] 394 cmd.append('-f') 395 cmd.append(str(BLOCK_SIZE)) 396 cmd.append(unaligned_apex_file) 397 cmd.append(args.output) 398 RunCommand(cmd, args.verbose) 399 400 if (args.verbose): 401 print('Created ' + args.output) 402 403 return True 404 405 406class TempDirectory(object): 407 def __enter__(self): 408 self.name = tempfile.mkdtemp() 409 return self.name 410 411 def __exit__(self, *unused): 412 shutil.rmtree(self.name) 413 414 415def main(argv): 416 global tool_path_list 417 args = ParseArgs(argv) 418 tool_path_list = args.apexer_tool_path 419 with TempDirectory() as work_dir: 420 success = CreateApex(args, work_dir) 421 422 if not success: 423 sys.exit(1) 424 425 426if __name__ == '__main__': 427 main(sys.argv[1:]) 428