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