• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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