xref: /aosp_15_r20/external/cronet/build/android/pylib/utils/app_bundle_utils.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1# Copyright 2018 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import json
6import logging
7import os
8import pathlib
9import re
10import shutil
11import sys
12import zipfile
13
14sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'gyp'))
15
16from util import build_utils
17from util import md5_check
18from util import resource_utils
19import bundletool
20
21# "system_apks" is "default", but with locale list and compressed dex.
22_SYSTEM_MODES = ('system', 'system_apks')
23BUILD_APKS_MODES = _SYSTEM_MODES + ('default', 'universal')
24OPTIMIZE_FOR_OPTIONS = ('ABI', 'SCREEN_DENSITY', 'LANGUAGE',
25                        'TEXTURE_COMPRESSION_FORMAT')
26
27_ALL_ABIS = ['armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64']
28
29
30def _BundleMinSdkVersion(bundle_path):
31  manifest_data = bundletool.RunBundleTool(
32      ['dump', 'manifest', '--bundle', bundle_path])
33  return int(re.search(r'minSdkVersion.*?(\d+)', manifest_data).group(1))
34
35
36def _CreateDeviceSpec(bundle_path, sdk_version, locales):
37  if not sdk_version:
38    sdk_version = _BundleMinSdkVersion(bundle_path)
39
40  # Setting sdkVersion=minSdkVersion prevents multiple per-minSdkVersion .apk
41  # files from being created within the .apks file.
42  return {
43      'screenDensity': 1000,  # Ignored since we don't split on density.
44      'sdkVersion': sdk_version,
45      'supportedAbis': _ALL_ABIS,  # Our .aab files are already split on abi.
46      'supportedLocales': locales,
47  }
48
49
50def _FixBundleDexCompressionGlob(src_bundle, dst_bundle):
51  # Modifies the BundleConfig.pb of the given .aab to add "classes*.dex" to the
52  # "uncompressedGlob" list.
53  with zipfile.ZipFile(src_bundle) as src, \
54      zipfile.ZipFile(dst_bundle, 'w') as dst:
55    for info in src.infolist():
56      data = src.read(info)
57      if info.filename == 'BundleConfig.pb':
58        # A classesX.dex entry is added by create_app_bundle.py so that we can
59        # modify it here in order to have it take effect. b/176198991
60        data = data.replace(b'classesX.dex', b'classes*.dex')
61      dst.writestr(info, data)
62
63
64def GenerateBundleApks(bundle_path,
65                       bundle_apks_path,
66                       aapt2_path,
67                       keystore_path,
68                       keystore_password,
69                       keystore_alias,
70                       mode=None,
71                       local_testing=False,
72                       minimal=False,
73                       minimal_sdk_version=None,
74                       check_for_noop=True,
75                       system_image_locales=None,
76                       optimize_for=None):
77  """Generate an .apks archive from a an app bundle if needed.
78
79  Args:
80    bundle_path: Input bundle file path.
81    bundle_apks_path: Output bundle .apks archive path. Name must end with
82      '.apks' or this operation will fail.
83    aapt2_path: Path to aapt2 build tool.
84    keystore_path: Path to keystore.
85    keystore_password: Keystore password, as a string.
86    keystore_alias: Keystore signing key alias.
87    mode: Build mode, which must be either None or one of BUILD_APKS_MODES.
88    minimal: Create the minimal set of apks possible (english-only).
89    minimal_sdk_version: Use this sdkVersion when |minimal| or
90      |system_image_locales| args are present.
91    check_for_noop: Use md5_check to short-circuit when inputs have not changed.
92    system_image_locales: Locales to package in the APK when mode is "system"
93      or "system_compressed".
94    optimize_for: Overrides split configuration, which must be None or
95      one of OPTIMIZE_FOR_OPTIONS.
96  """
97  device_spec = None
98  if minimal_sdk_version:
99    assert minimal or system_image_locales, (
100        'minimal_sdk_version is only used when minimal or system_image_locales '
101        'is specified')
102  if minimal:
103    # Measure with one language split installed. Use Hindi because it is
104    # popular. resource_size.py looks for splits/base-hi.apk.
105    # Note: English is always included since it's in base-master.apk.
106    device_spec = _CreateDeviceSpec(bundle_path, minimal_sdk_version, ['hi'])
107  elif mode in _SYSTEM_MODES:
108    if not system_image_locales:
109      raise Exception('system modes require system_image_locales')
110    # Bundletool doesn't seem to understand device specs with locales in the
111    # form of "<lang>-r<region>", so just provide the language code instead.
112    locales = [
113        resource_utils.ToAndroidLocaleName(l).split('-')[0]
114        for l in system_image_locales
115    ]
116    device_spec = _CreateDeviceSpec(bundle_path, minimal_sdk_version, locales)
117
118  def rebuild():
119    logging.info('Building %s', bundle_apks_path)
120    with build_utils.TempDir() as tmp_dir:
121      tmp_apks_file = os.path.join(tmp_dir, 'output.apks')
122      cmd_args = [
123          'build-apks',
124          '--aapt2=%s' % aapt2_path,
125          '--output=%s' % tmp_apks_file,
126          '--ks=%s' % keystore_path,
127          '--ks-pass=pass:%s' % keystore_password,
128          '--ks-key-alias=%s' % keystore_alias,
129          '--overwrite',
130      ]
131      input_bundle_path = bundle_path
132      # Work around bundletool not respecting uncompressDexFiles setting.
133      # b/176198991
134      if mode not in _SYSTEM_MODES and _BundleMinSdkVersion(bundle_path) >= 27:
135        input_bundle_path = os.path.join(tmp_dir, 'system.aab')
136        _FixBundleDexCompressionGlob(bundle_path, input_bundle_path)
137
138      cmd_args += ['--bundle=%s' % input_bundle_path]
139
140      if local_testing:
141        cmd_args += ['--local-testing']
142
143      if mode is not None:
144        if mode not in BUILD_APKS_MODES:
145          raise Exception('Invalid mode parameter %s (should be in %s)' %
146                          (mode, BUILD_APKS_MODES))
147        if mode != 'system_apks':
148          cmd_args += ['--mode=' + mode]
149        else:
150          # Specify --optimize-for to prevent language splits being created.
151          cmd_args += ['--optimize-for=device_tier']
152
153      if optimize_for:
154        if optimize_for not in OPTIMIZE_FOR_OPTIONS:
155          raise Exception('Invalid optimize_for parameter %s '
156                          '(should be in %s)' %
157                          (mode, OPTIMIZE_FOR_OPTIONS))
158        cmd_args += ['--optimize-for=' + optimize_for]
159
160      if device_spec:
161        data = json.dumps(device_spec)
162        logging.debug('Device Spec: %s', data)
163        spec_file = pathlib.Path(tmp_dir) / 'device.json'
164        spec_file.write_text(data)
165        cmd_args += ['--device-spec=' + str(spec_file)]
166
167      bundletool.RunBundleTool(cmd_args)
168
169      shutil.move(tmp_apks_file, bundle_apks_path)
170
171  if check_for_noop:
172    input_paths = [
173        bundle_path,
174        bundletool.BUNDLETOOL_JAR_PATH,
175        aapt2_path,
176        keystore_path,
177    ]
178    input_strings = [
179        keystore_password,
180        keystore_alias,
181        device_spec,
182    ]
183    if mode is not None:
184      input_strings.append(mode)
185
186    # Avoid rebuilding (saves ~20s) when the input files have not changed. This
187    # is essential when calling the apk_operations.py script multiple times with
188    # the same bundle (e.g. out/Debug/bin/monochrome_public_bundle run).
189    md5_check.CallAndRecordIfStale(
190        rebuild,
191        input_paths=input_paths,
192        input_strings=input_strings,
193        output_paths=[bundle_apks_path])
194  else:
195    rebuild()
196