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