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