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