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