xref: /aosp_15_r20/external/cronet/build/android/gyp/apkbuilder.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1#!/usr/bin/env python3
2#
3# Copyright 2015 The Chromium Authors
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Adds the code parts to a resource APK."""
8
9import argparse
10import logging
11import os
12import posixpath
13import shutil
14import sys
15import tempfile
16import zipfile
17import zlib
18
19import finalize_apk
20
21from util import build_utils
22from util import diff_utils
23import action_helpers  # build_utils adds //build to sys.path.
24import zip_helpers
25
26
27# Taken from aapt's Package.cpp:
28_NO_COMPRESS_EXTENSIONS = ('.jpg', '.jpeg', '.png', '.gif', '.wav', '.mp2',
29                           '.mp3', '.ogg', '.aac', '.mpg', '.mpeg', '.mid',
30                           '.midi', '.smf', '.jet', '.rtttl', '.imy', '.xmf',
31                           '.mp4', '.m4a', '.m4v', '.3gp', '.3gpp', '.3g2',
32                           '.3gpp2', '.amr', '.awb', '.wma', '.wmv', '.webm')
33
34
35def _ParseArgs(args):
36  parser = argparse.ArgumentParser()
37  action_helpers.add_depfile_arg(parser)
38  parser.add_argument('--assets',
39                      action='append',
40                      help='GYP-list of files to add as assets in the form '
41                      '"srcPath:zipPath", where ":zipPath" is optional.')
42  parser.add_argument(
43      '--java-resources', help='GYP-list of java_resources JARs to include.')
44  parser.add_argument('--write-asset-list',
45                      action='store_true',
46                      help='Whether to create an assets/assets_list file.')
47  parser.add_argument(
48      '--uncompressed-assets',
49      help='Same as --assets, except disables compression.')
50  parser.add_argument('--resource-apk',
51                      help='An .ap_ file built using aapt',
52                      required=True)
53  parser.add_argument('--output-apk',
54                      help='Path to the output file',
55                      required=True)
56  parser.add_argument('--format', choices=['apk', 'bundle-module'],
57                      default='apk', help='Specify output format.')
58  parser.add_argument('--dex-file',
59                      help='Path to the classes.dex to use')
60  parser.add_argument('--uncompress-dex', action='store_true',
61                      help='Store .dex files uncompressed in the APK')
62  parser.add_argument('--native-libs',
63                      action='append',
64                      help='GYP-list of native libraries to include. '
65                           'Can be specified multiple times.',
66                      default=[])
67  parser.add_argument('--secondary-native-libs',
68                      action='append',
69                      help='GYP-list of native libraries for secondary '
70                           'android-abi. Can be specified multiple times.',
71                      default=[])
72  parser.add_argument('--android-abi',
73                      help='Android architecture to use for native libraries')
74  parser.add_argument('--secondary-android-abi',
75                      help='The secondary Android architecture to use for'
76                           'secondary native libraries')
77  parser.add_argument(
78      '--is-multi-abi',
79      action='store_true',
80      help='Will add a placeholder for the missing ABI if no native libs or '
81      'placeholders are set for either the primary or secondary ABI. Can only '
82      'be set if both --android-abi and --secondary-android-abi are set.')
83  parser.add_argument(
84      '--native-lib-placeholders',
85      help='GYP-list of native library placeholders to add.')
86  parser.add_argument(
87      '--secondary-native-lib-placeholders',
88      help='GYP-list of native library placeholders to add '
89      'for the secondary ABI')
90  parser.add_argument('--uncompress-shared-libraries', default='False',
91      choices=['true', 'True', 'false', 'False'],
92      help='Whether to uncompress native shared libraries. Argument must be '
93           'a boolean value.')
94  parser.add_argument(
95      '--apksigner-jar', help='Path to the apksigner executable.')
96  parser.add_argument('--zipalign-path',
97                      help='Path to the zipalign executable.')
98  parser.add_argument('--key-path',
99                      help='Path to keystore for signing.')
100  parser.add_argument('--key-passwd',
101                      help='Keystore password')
102  parser.add_argument('--key-name',
103                      help='Keystore name')
104  parser.add_argument(
105      '--min-sdk-version', required=True, help='Value of APK\'s minSdkVersion')
106  parser.add_argument(
107      '--best-compression',
108      action='store_true',
109      help='Use zip -9 rather than zip -1')
110  parser.add_argument(
111      '--library-always-compress',
112      action='append',
113      help='The list of library files that we always compress.')
114  parser.add_argument('--warnings-as-errors',
115                      action='store_true',
116                      help='Treat all warnings as errors.')
117  diff_utils.AddCommandLineFlags(parser)
118  options = parser.parse_args(args)
119  options.assets = action_helpers.parse_gn_list(options.assets)
120  options.uncompressed_assets = action_helpers.parse_gn_list(
121      options.uncompressed_assets)
122  options.native_lib_placeholders = action_helpers.parse_gn_list(
123      options.native_lib_placeholders)
124  options.secondary_native_lib_placeholders = action_helpers.parse_gn_list(
125      options.secondary_native_lib_placeholders)
126  options.java_resources = action_helpers.parse_gn_list(options.java_resources)
127  options.native_libs = action_helpers.parse_gn_list(options.native_libs)
128  options.secondary_native_libs = action_helpers.parse_gn_list(
129      options.secondary_native_libs)
130  options.library_always_compress = action_helpers.parse_gn_list(
131      options.library_always_compress)
132
133  if not options.android_abi and (options.native_libs or
134                                  options.native_lib_placeholders):
135    raise Exception('Must specify --android-abi with --native-libs')
136  if not options.secondary_android_abi and (options.secondary_native_libs or
137      options.secondary_native_lib_placeholders):
138    raise Exception('Must specify --secondary-android-abi with'
139                    ' --secondary-native-libs')
140  if options.is_multi_abi and not (options.android_abi
141                                   and options.secondary_android_abi):
142    raise Exception('Must specify --is-multi-abi with both --android-abi '
143                    'and --secondary-android-abi.')
144  return options
145
146
147def _SplitAssetPath(path):
148  """Returns (src, dest) given an asset path in the form src[:dest]."""
149  path_parts = path.split(':')
150  src_path = path_parts[0]
151  if len(path_parts) > 1:
152    dest_path = path_parts[1]
153  else:
154    dest_path = os.path.basename(src_path)
155  return src_path, dest_path
156
157
158def _ExpandPaths(paths):
159  """Converts src:dst into tuples and enumerates files within directories.
160
161  Args:
162    paths: Paths in the form "src_path:dest_path"
163
164  Returns:
165    A list of (src_path, dest_path) tuples sorted by dest_path (for stable
166    ordering within output .apk).
167  """
168  ret = []
169  for path in paths:
170    src_path, dest_path = _SplitAssetPath(path)
171    if os.path.isdir(src_path):
172      for f in build_utils.FindInDirectory(src_path, '*'):
173        ret.append((f, os.path.join(dest_path, f[len(src_path) + 1:])))
174    else:
175      ret.append((src_path, dest_path))
176  ret.sort(key=lambda t:t[1])
177  return ret
178
179
180def _GetAssetsToAdd(path_tuples,
181                    fast_align,
182                    disable_compression=False,
183                    allow_reads=True,
184                    apk_root_dir=''):
185  """Returns the list of file_detail tuples for assets in the apk.
186
187  Args:
188    path_tuples: List of src_path, dest_path tuples to add.
189    fast_align: Whether to perform alignment in python zipfile (alternatively
190                alignment can be done using the zipalign utility out of band).
191    disable_compression: Whether to disable compression.
192    allow_reads: If false, we do not try to read the files from disk (to find
193                 their size for example).
194
195  Returns: A list of (src_path, apk_path, compress, alignment) tuple
196  representing what and how assets are added.
197  """
198  assets_to_add = []
199
200  # Group all uncompressed assets together in the hope that it will increase
201  # locality of mmap'ed files.
202  for target_compress in (False, True):
203    for src_path, dest_path in path_tuples:
204      compress = not disable_compression and (
205          os.path.splitext(src_path)[1] not in _NO_COMPRESS_EXTENSIONS)
206
207      if target_compress == compress:
208        # add_to_zip_hermetic() uses this logic to avoid growing small files.
209        # We need it here in order to set alignment correctly.
210        if allow_reads and compress and os.path.getsize(src_path) < 16:
211          compress = False
212
213        if dest_path.startswith('../'):
214          # posixpath.join('', 'foo') == 'foo'
215          apk_path = posixpath.join(apk_root_dir, dest_path[3:])
216        else:
217          apk_path = 'assets/' + dest_path
218        alignment = 0 if compress and not fast_align else 4
219        assets_to_add.append((apk_path, src_path, compress, alignment))
220  return assets_to_add
221
222
223def _AddFiles(apk, details):
224  """Adds files to the apk.
225
226  Args:
227    apk: path to APK to add to.
228    details: A list of file detail tuples (src_path, apk_path, compress,
229    alignment) representing what and how files are added to the APK.
230  """
231  for apk_path, src_path, compress, alignment in details:
232    # This check is only relevant for assets, but it should not matter if it is
233    # checked for the whole list of files.
234    try:
235      apk.getinfo(apk_path)
236      # Should never happen since write_build_config.py handles merging.
237      raise Exception(
238          'Multiple targets specified the asset path: %s' % apk_path)
239    except KeyError:
240      zip_helpers.add_to_zip_hermetic(apk,
241                                      apk_path,
242                                      src_path=src_path,
243                                      compress=compress,
244                                      alignment=alignment)
245
246
247def _GetAbiAlignment(android_abi):
248  if '64' in android_abi:
249    return 0x4000  # 16k alignment
250  return 0x1000  # 4k alignment
251
252
253def _GetNativeLibrariesToAdd(native_libs, android_abi, fast_align,
254                             lib_always_compress):
255  """Returns the list of file_detail tuples for native libraries in the apk.
256
257  Returns: A list of (src_path, apk_path, compress, alignment) tuple
258  representing what and how native libraries are added.
259  """
260  libraries_to_add = []
261
262
263  for path in native_libs:
264    basename = os.path.basename(path)
265    compress = any(lib_name in basename for lib_name in lib_always_compress)
266    lib_android_abi = android_abi
267    if path.startswith('android_clang_arm64_hwasan/'):
268      lib_android_abi = 'arm64-v8a-hwasan'
269
270    apk_path = 'lib/%s/%s' % (lib_android_abi, basename)
271    if compress and not fast_align:
272      alignment = 0
273    else:
274      alignment = _GetAbiAlignment(android_abi)
275    libraries_to_add.append((apk_path, path, compress, alignment))
276
277  return libraries_to_add
278
279
280def _CreateExpectationsData(native_libs, assets):
281  """Creates list of native libraries and assets."""
282  native_libs = sorted(native_libs)
283  assets = sorted(assets)
284
285  ret = []
286  for apk_path, _, compress, alignment in native_libs + assets:
287    ret.append('apk_path=%s, compress=%s, alignment=%s\n' %
288               (apk_path, compress, alignment))
289  return ''.join(ret)
290
291
292def main(args):
293  build_utils.InitLogging('APKBUILDER_DEBUG')
294  args = build_utils.ExpandFileArgs(args)
295  options = _ParseArgs(args)
296
297  # Until Python 3.7, there's no better way to set compression level.
298  # The default is 6.
299  if options.best_compression:
300    # Compresses about twice as slow as the default.
301    zlib.Z_DEFAULT_COMPRESSION = 9
302  else:
303    # Compresses about twice as fast as the default.
304    zlib.Z_DEFAULT_COMPRESSION = 1
305
306  # Python's zip implementation duplicates file comments in the central
307  # directory, whereas zipalign does not, so use zipalign for official builds.
308  requires_alignment = options.format == 'apk'
309  # TODO(crbug.com/1495851): Re-enable zipalign once we are using Android V SDK.
310  run_zipalign = requires_alignment and options.best_compression and False
311  fast_align = bool(requires_alignment and not run_zipalign)
312
313  native_libs = sorted(options.native_libs)
314
315  # Include native libs in the depfile_deps since GN doesn't know about the
316  # dependencies when is_component_build=true.
317  depfile_deps = list(native_libs)
318
319  # For targets that depend on static library APKs, dex paths are created by
320  # the static library's dexsplitter target and GN doesn't know about these
321  # paths.
322  if options.dex_file:
323    depfile_deps.append(options.dex_file)
324
325  secondary_native_libs = []
326  if options.secondary_native_libs:
327    secondary_native_libs = sorted(options.secondary_native_libs)
328    depfile_deps += secondary_native_libs
329
330  if options.java_resources:
331    # Included via .build_config.json, so need to write it to depfile.
332    depfile_deps.extend(options.java_resources)
333
334  assets = _ExpandPaths(options.assets)
335  uncompressed_assets = _ExpandPaths(options.uncompressed_assets)
336
337  # Included via .build_config.json, so need to write it to depfile.
338  depfile_deps.extend(x[0] for x in assets)
339  depfile_deps.extend(x[0] for x in uncompressed_assets)
340  depfile_deps.append(options.resource_apk)
341
342  # Bundle modules have a structure similar to APKs, except that resources
343  # are compiled in protobuf format (instead of binary xml), and that some
344  # files are located into different top-level directories, e.g.:
345  #  AndroidManifest.xml -> manifest/AndroidManifest.xml
346  #  classes.dex -> dex/classes.dex
347  #  res/ -> res/  (unchanged)
348  #  assets/ -> assets/  (unchanged)
349  #  <other-file> -> root/<other-file>
350  #
351  # Hence, the following variables are used to control the location of files in
352  # the final archive.
353  if options.format == 'bundle-module':
354    apk_manifest_dir = 'manifest/'
355    apk_root_dir = 'root/'
356    apk_dex_dir = 'dex/'
357  else:
358    apk_manifest_dir = ''
359    apk_root_dir = ''
360    apk_dex_dir = ''
361
362  def _GetAssetDetails(assets, uncompressed_assets, fast_align, allow_reads):
363    ret = _GetAssetsToAdd(assets,
364                          fast_align,
365                          disable_compression=False,
366                          allow_reads=allow_reads,
367                          apk_root_dir=apk_root_dir)
368    ret.extend(
369        _GetAssetsToAdd(uncompressed_assets,
370                        fast_align,
371                        disable_compression=True,
372                        allow_reads=allow_reads,
373                        apk_root_dir=apk_root_dir))
374    return ret
375
376  libs_to_add = _GetNativeLibrariesToAdd(native_libs, options.android_abi,
377                                         fast_align,
378                                         options.library_always_compress)
379  if options.secondary_android_abi:
380    libs_to_add.extend(
381        _GetNativeLibrariesToAdd(secondary_native_libs,
382                                 options.secondary_android_abi,
383                                 fast_align, options.library_always_compress))
384
385  if options.expected_file:
386    # We compute expectations without reading the files. This allows us to check
387    # expectations for different targets by just generating their build_configs
388    # and not have to first generate all the actual files and all their
389    # dependencies (for example by just passing --only-verify-expectations).
390    asset_details = _GetAssetDetails(assets,
391                                     uncompressed_assets,
392                                     fast_align,
393                                     allow_reads=False)
394
395    actual_data = _CreateExpectationsData(libs_to_add, asset_details)
396    diff_utils.CheckExpectations(actual_data, options)
397
398    if options.only_verify_expectations:
399      if options.depfile:
400        action_helpers.write_depfile(options.depfile,
401                                     options.actual_file,
402                                     inputs=depfile_deps)
403      return
404
405  # If we are past this point, we are going to actually create the final apk so
406  # we should recompute asset details again but maybe perform some optimizations
407  # based on the size of the files on disk.
408  assets_to_add = _GetAssetDetails(
409      assets, uncompressed_assets, fast_align, allow_reads=True)
410
411  # Targets generally do not depend on apks, so no need for only_if_changed.
412  with action_helpers.atomic_output(options.output_apk,
413                                    only_if_changed=False) as f:
414    with zipfile.ZipFile(options.resource_apk) as resource_apk, \
415         zipfile.ZipFile(f, 'w') as out_apk:
416
417      def add_to_zip(zip_path, data, compress=True, alignment=4):
418        zip_helpers.add_to_zip_hermetic(
419            out_apk,
420            zip_path,
421            data=data,
422            compress=compress,
423            alignment=0 if compress and not fast_align else alignment)
424
425      def copy_resource(zipinfo, out_dir=''):
426        add_to_zip(
427            out_dir + zipinfo.filename,
428            resource_apk.read(zipinfo.filename),
429            compress=zipinfo.compress_type != zipfile.ZIP_STORED)
430
431      # Make assets come before resources in order to maintain the same file
432      # ordering as GYP / aapt. http://crbug.com/561862
433      resource_infos = resource_apk.infolist()
434
435      # 1. AndroidManifest.xml
436      logging.debug('Adding AndroidManifest.xml')
437      copy_resource(
438          resource_apk.getinfo('AndroidManifest.xml'), out_dir=apk_manifest_dir)
439
440      # 2. Assets
441      logging.debug('Adding assets/')
442      _AddFiles(out_apk, assets_to_add)
443
444      # 3. Dex files
445      logging.debug('Adding classes.dex')
446      if options.dex_file:
447        with open(options.dex_file, 'rb') as dex_file_obj:
448          if options.dex_file.endswith('.dex'):
449            max_dex_number = 1
450            # This is the case for incremental_install=true.
451            add_to_zip(
452                apk_dex_dir + 'classes.dex',
453                dex_file_obj.read(),
454                compress=not options.uncompress_dex)
455          else:
456            max_dex_number = 0
457            with zipfile.ZipFile(dex_file_obj) as dex_zip:
458              for dex in (d for d in dex_zip.namelist() if d.endswith('.dex')):
459                max_dex_number += 1
460                add_to_zip(
461                    apk_dex_dir + dex,
462                    dex_zip.read(dex),
463                    compress=not options.uncompress_dex)
464
465      # 4. Native libraries.
466      logging.debug('Adding lib/')
467      _AddFiles(out_apk, libs_to_add)
468
469      # Add a placeholder lib if the APK should be multi ABI but is missing libs
470      # for one of the ABIs.
471      native_lib_placeholders = options.native_lib_placeholders
472      secondary_native_lib_placeholders = (
473          options.secondary_native_lib_placeholders)
474      if options.is_multi_abi:
475        if ((secondary_native_libs or secondary_native_lib_placeholders)
476            and not native_libs and not native_lib_placeholders):
477          native_lib_placeholders += ['libplaceholder.so']
478        if ((native_libs or native_lib_placeholders)
479            and not secondary_native_libs
480            and not secondary_native_lib_placeholders):
481          secondary_native_lib_placeholders += ['libplaceholder.so']
482
483      # Add placeholder libs.
484      for name in sorted(native_lib_placeholders):
485        # Note: Empty libs files are ignored by md5check (can cause issues
486        # with stale builds when the only change is adding/removing
487        # placeholders).
488        apk_path = 'lib/%s/%s' % (options.android_abi, name)
489        alignment = _GetAbiAlignment(options.android_abi)
490        add_to_zip(apk_path, '', alignment=alignment)
491
492      for name in sorted(secondary_native_lib_placeholders):
493        # Note: Empty libs files are ignored by md5check (can cause issues
494        # with stale builds when the only change is adding/removing
495        # placeholders).
496        apk_path = 'lib/%s/%s' % (options.secondary_android_abi, name)
497        alignment = _GetAbiAlignment(options.secondary_android_abi)
498        add_to_zip(apk_path, '', alignment=alignment)
499
500      # 5. Resources
501      logging.debug('Adding res/')
502      for info in sorted(resource_infos, key=lambda i: i.filename):
503        if info.filename != 'AndroidManifest.xml':
504          copy_resource(info)
505
506      # 6. Java resources that should be accessible via
507      # Class.getResourceAsStream(), in particular parts of Emma jar.
508      # Prebuilt jars may contain class files which we shouldn't include.
509      logging.debug('Adding Java resources')
510      for java_resource in options.java_resources:
511        with zipfile.ZipFile(java_resource, 'r') as java_resource_jar:
512          for apk_path in sorted(java_resource_jar.namelist()):
513            apk_path_lower = apk_path.lower()
514
515            if apk_path_lower.startswith('meta-inf/'):
516              continue
517            if apk_path_lower.endswith('/'):
518              continue
519            if apk_path_lower.endswith('.class'):
520              continue
521
522            add_to_zip(apk_root_dir + apk_path,
523                       java_resource_jar.read(apk_path))
524
525    if options.format == 'apk' and options.key_path:
526      zipalign_path = None if fast_align else options.zipalign_path
527      finalize_apk.FinalizeApk(options.apksigner_jar,
528                               zipalign_path,
529                               f.name,
530                               f.name,
531                               options.key_path,
532                               options.key_passwd,
533                               options.key_name,
534                               int(options.min_sdk_version),
535                               warnings_as_errors=options.warnings_as_errors)
536    logging.debug('Moving file into place')
537
538    if options.depfile:
539      action_helpers.write_depfile(options.depfile,
540                                   options.output_apk,
541                                   inputs=depfile_deps)
542
543
544if __name__ == '__main__':
545  main(sys.argv[1:])
546