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