Ben Murdoch | 097c5b2 | 2016-05-18 11:27:45 +0100 | [diff] [blame^] | 1 | #!/usr/bin/env python |
| 2 | # |
| 3 | # Copyright (c) 2015 The Chromium Authors. All rights reserved. |
| 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 | |
| 9 | import argparse |
| 10 | import itertools |
| 11 | import os |
| 12 | import shutil |
| 13 | import sys |
| 14 | import zipfile |
| 15 | |
| 16 | from util import build_utils |
| 17 | |
| 18 | |
| 19 | # Taken from aapt's Package.cpp: |
| 20 | _NO_COMPRESS_EXTENSIONS = ('.jpg', '.jpeg', '.png', '.gif', '.wav', '.mp2', |
| 21 | '.mp3', '.ogg', '.aac', '.mpg', '.mpeg', '.mid', |
| 22 | '.midi', '.smf', '.jet', '.rtttl', '.imy', '.xmf', |
| 23 | '.mp4', '.m4a', '.m4v', '.3gp', '.3gpp', '.3g2', |
| 24 | '.3gpp2', '.amr', '.awb', '.wma', '.wmv', '.webm') |
| 25 | |
| 26 | |
| 27 | def _ParseArgs(args): |
| 28 | parser = argparse.ArgumentParser() |
| 29 | build_utils.AddDepfileOption(parser) |
| 30 | parser.add_argument('--assets', |
| 31 | help='GYP-list of files to add as assets in the form ' |
| 32 | '"srcPath:zipPath", where ":zipPath" is optional.', |
| 33 | default='[]') |
| 34 | parser.add_argument('--write-asset-list', |
| 35 | action='store_true', |
| 36 | help='Whether to create an assets/assets_list file.') |
| 37 | parser.add_argument('--uncompressed-assets', |
| 38 | help='Same as --assets, except disables compression.', |
| 39 | default='[]') |
| 40 | parser.add_argument('--resource-apk', |
| 41 | help='An .ap_ file built using aapt', |
| 42 | required=True) |
| 43 | parser.add_argument('--output-apk', |
| 44 | help='Path to the output file', |
| 45 | required=True) |
| 46 | parser.add_argument('--dex-file', |
| 47 | help='Path to the classes.dex to use') |
| 48 | parser.add_argument('--native-libs', |
| 49 | action='append', |
| 50 | help='GYP-list of native libraries to include. ' |
| 51 | 'Can be specified multiple times.', |
| 52 | default=[]) |
| 53 | parser.add_argument('--secondary-native-libs', |
| 54 | action='append', |
| 55 | help='GYP-list of native libraries for secondary ' |
| 56 | 'android-abi. Can be specified multiple times.', |
| 57 | default=[]) |
| 58 | parser.add_argument('--android-abi', |
| 59 | help='Android architecture to use for native libraries') |
| 60 | parser.add_argument('--secondary-android-abi', |
| 61 | help='The secondary Android architecture to use for' |
| 62 | 'secondary native libraries') |
| 63 | parser.add_argument('--native-lib-placeholders', |
| 64 | help='GYP-list of native library placeholders to add.', |
| 65 | default='[]') |
| 66 | parser.add_argument('--emma-device-jar', |
| 67 | help='Path to emma_device.jar to include.') |
| 68 | parser.add_argument('--uncompress-shared-libraries', |
| 69 | action='store_true', |
| 70 | help='Uncompress shared libraries') |
| 71 | options = parser.parse_args(args) |
| 72 | options.assets = build_utils.ParseGypList(options.assets) |
| 73 | options.uncompressed_assets = build_utils.ParseGypList( |
| 74 | options.uncompressed_assets) |
| 75 | options.native_lib_placeholders = build_utils.ParseGypList( |
| 76 | options.native_lib_placeholders) |
| 77 | all_libs = [] |
| 78 | for gyp_list in options.native_libs: |
| 79 | all_libs.extend(build_utils.ParseGypList(gyp_list)) |
| 80 | options.native_libs = all_libs |
| 81 | secondary_libs = [] |
| 82 | for gyp_list in options.secondary_native_libs: |
| 83 | secondary_libs.extend(build_utils.ParseGypList(gyp_list)) |
| 84 | options.secondary_native_libs = secondary_libs |
| 85 | |
| 86 | |
| 87 | if not options.android_abi and (options.native_libs or |
| 88 | options.native_lib_placeholders): |
| 89 | raise Exception('Must specify --android-abi with --native-libs') |
| 90 | if not options.secondary_android_abi and options.secondary_native_libs: |
| 91 | raise Exception('Must specify --secondary-android-abi with' |
| 92 | ' --secondary-native-libs') |
| 93 | return options |
| 94 | |
| 95 | |
| 96 | def _SplitAssetPath(path): |
| 97 | """Returns (src, dest) given an asset path in the form src[:dest].""" |
| 98 | path_parts = path.split(':') |
| 99 | src_path = path_parts[0] |
| 100 | if len(path_parts) > 1: |
| 101 | dest_path = path_parts[1] |
| 102 | else: |
| 103 | dest_path = os.path.basename(src_path) |
| 104 | return src_path, dest_path |
| 105 | |
| 106 | |
| 107 | def _ExpandPaths(paths): |
| 108 | """Converts src:dst into tuples and enumerates files within directories. |
| 109 | |
| 110 | Args: |
| 111 | paths: Paths in the form "src_path:dest_path" |
| 112 | |
| 113 | Returns: |
| 114 | A list of (src_path, dest_path) tuples sorted by dest_path (for stable |
| 115 | ordering within output .apk). |
| 116 | """ |
| 117 | ret = [] |
| 118 | for path in paths: |
| 119 | src_path, dest_path = _SplitAssetPath(path) |
| 120 | if os.path.isdir(src_path): |
| 121 | for f in build_utils.FindInDirectory(src_path, '*'): |
| 122 | ret.append((f, os.path.join(dest_path, f[len(src_path) + 1:]))) |
| 123 | else: |
| 124 | ret.append((src_path, dest_path)) |
| 125 | ret.sort(key=lambda t:t[1]) |
| 126 | return ret |
| 127 | |
| 128 | |
| 129 | def _AddAssets(apk, path_tuples, disable_compression=False): |
| 130 | """Adds the given paths to the apk. |
| 131 | |
| 132 | Args: |
| 133 | apk: ZipFile to write to. |
| 134 | paths: List of paths (with optional :zipPath suffix) to add. |
| 135 | disable_compression: Whether to disable compression. |
| 136 | """ |
| 137 | # Group all uncompressed assets together in the hope that it will increase |
| 138 | # locality of mmap'ed files. |
| 139 | for target_compress in (False, True): |
| 140 | for src_path, dest_path in path_tuples: |
| 141 | |
| 142 | compress = not disable_compression and ( |
| 143 | os.path.splitext(src_path)[1] not in _NO_COMPRESS_EXTENSIONS) |
| 144 | if target_compress == compress: |
| 145 | apk_path = 'assets/' + dest_path |
| 146 | try: |
| 147 | apk.getinfo(apk_path) |
| 148 | # Should never happen since write_build_config.py handles merging. |
| 149 | raise Exception('Multiple targets specified the asset path: %s' % |
| 150 | apk_path) |
| 151 | except KeyError: |
| 152 | build_utils.AddToZipHermetic(apk, apk_path, src_path=src_path, |
| 153 | compress=compress) |
| 154 | |
| 155 | |
| 156 | def _CreateAssetsList(path_tuples): |
| 157 | """Returns a newline-separated list of asset paths for the given paths.""" |
| 158 | dests = sorted(t[1] for t in path_tuples) |
| 159 | return '\n'.join(dests) + '\n' |
| 160 | |
| 161 | |
| 162 | def _AddNativeLibraries(out_apk, native_libs, android_abi, uncompress): |
| 163 | """Add native libraries to APK.""" |
| 164 | for path in native_libs: |
| 165 | basename = os.path.basename(path) |
| 166 | apk_path = 'lib/%s/%s' % (android_abi, basename) |
| 167 | |
| 168 | compress = None |
| 169 | if (uncompress and os.path.splitext(basename)[1] == '.so'): |
| 170 | compress = False |
| 171 | |
| 172 | build_utils.AddToZipHermetic(out_apk, |
| 173 | apk_path, |
| 174 | src_path=path, |
| 175 | compress=compress) |
| 176 | |
| 177 | |
| 178 | def main(args): |
| 179 | args = build_utils.ExpandFileArgs(args) |
| 180 | options = _ParseArgs(args) |
| 181 | |
| 182 | native_libs = sorted(options.native_libs) |
| 183 | |
| 184 | input_paths = [options.resource_apk, __file__] + native_libs |
| 185 | |
| 186 | secondary_native_libs = [] |
| 187 | if options.secondary_native_libs: |
| 188 | secondary_native_libs = sorted(options.secondary_native_libs) |
| 189 | input_paths += secondary_native_libs |
| 190 | |
| 191 | if options.dex_file: |
| 192 | input_paths.append(options.dex_file) |
| 193 | |
| 194 | if options.emma_device_jar: |
| 195 | input_paths.append(options.emma_device_jar) |
| 196 | |
| 197 | input_strings = [options.android_abi, |
| 198 | options.native_lib_placeholders, |
| 199 | options.uncompress_shared_libraries] |
| 200 | |
| 201 | if options.secondary_android_abi: |
| 202 | input_strings.append(options.secondary_android_abi) |
| 203 | |
| 204 | _assets = _ExpandPaths(options.assets) |
| 205 | _uncompressed_assets = _ExpandPaths(options.uncompressed_assets) |
| 206 | |
| 207 | for src_path, dest_path in itertools.chain(_assets, _uncompressed_assets): |
| 208 | input_paths.append(src_path) |
| 209 | input_strings.append(dest_path) |
| 210 | |
| 211 | def on_stale_md5(): |
| 212 | tmp_apk = options.output_apk + '.tmp' |
| 213 | try: |
| 214 | # TODO(agrieve): It would be more efficient to combine this step |
| 215 | # with finalize_apk(), which sometimes aligns and uncompresses the |
| 216 | # native libraries. |
| 217 | with zipfile.ZipFile(options.resource_apk) as resource_apk, \ |
| 218 | zipfile.ZipFile(tmp_apk, 'w', zipfile.ZIP_DEFLATED) as out_apk: |
| 219 | def copy_resource(zipinfo): |
| 220 | compress = zipinfo.compress_type != zipfile.ZIP_STORED |
| 221 | build_utils.AddToZipHermetic(out_apk, zipinfo.filename, |
| 222 | data=resource_apk.read(zipinfo.filename), |
| 223 | compress=compress) |
| 224 | |
| 225 | # Make assets come before resources in order to maintain the same file |
| 226 | # ordering as GYP / aapt. http://crbug.com/561862 |
| 227 | resource_infos = resource_apk.infolist() |
| 228 | |
| 229 | # 1. AndroidManifest.xml |
| 230 | assert resource_infos[0].filename == 'AndroidManifest.xml' |
| 231 | copy_resource(resource_infos[0]) |
| 232 | |
| 233 | # 2. Assets |
| 234 | if options.write_asset_list: |
| 235 | data = _CreateAssetsList( |
| 236 | itertools.chain(_assets, _uncompressed_assets)) |
| 237 | build_utils.AddToZipHermetic(out_apk, 'assets/assets_list', data=data) |
| 238 | |
| 239 | _AddAssets(out_apk, _assets, disable_compression=False) |
| 240 | _AddAssets(out_apk, _uncompressed_assets, disable_compression=True) |
| 241 | |
| 242 | # 3. Dex files |
| 243 | if options.dex_file and options.dex_file.endswith('.zip'): |
| 244 | with zipfile.ZipFile(options.dex_file, 'r') as dex_zip: |
| 245 | for dex in (d for d in dex_zip.namelist() if d.endswith('.dex')): |
| 246 | build_utils.AddToZipHermetic(out_apk, dex, data=dex_zip.read(dex)) |
| 247 | elif options.dex_file: |
| 248 | build_utils.AddToZipHermetic(out_apk, 'classes.dex', |
| 249 | src_path=options.dex_file) |
| 250 | |
| 251 | # 4. Native libraries. |
| 252 | _AddNativeLibraries(out_apk, |
| 253 | native_libs, |
| 254 | options.android_abi, |
| 255 | options.uncompress_shared_libraries) |
| 256 | |
| 257 | if options.secondary_android_abi: |
| 258 | _AddNativeLibraries(out_apk, |
| 259 | secondary_native_libs, |
| 260 | options.secondary_android_abi, |
| 261 | options.uncompress_shared_libraries) |
| 262 | |
| 263 | for name in sorted(options.native_lib_placeholders): |
| 264 | # Empty libs files are ignored by md5check, but rezip requires them |
| 265 | # to be empty in order to identify them as placeholders. |
| 266 | apk_path = 'lib/%s/%s' % (options.android_abi, name) |
| 267 | build_utils.AddToZipHermetic(out_apk, apk_path, data='') |
| 268 | |
| 269 | # 5. Resources |
| 270 | for info in resource_infos[1:]: |
| 271 | copy_resource(info) |
| 272 | |
| 273 | # 6. Java resources. Used only when coverage is enabled, so order |
| 274 | # doesn't matter). |
| 275 | if options.emma_device_jar: |
| 276 | # Add EMMA Java resources to APK. |
| 277 | with zipfile.ZipFile(options.emma_device_jar, 'r') as emma_device_jar: |
| 278 | for apk_path in emma_device_jar.namelist(): |
| 279 | apk_path_lower = apk_path.lower() |
| 280 | if apk_path_lower.startswith('meta-inf/'): |
| 281 | continue |
| 282 | |
| 283 | if apk_path_lower.endswith('/'): |
| 284 | continue |
| 285 | |
| 286 | if apk_path_lower.endswith('.class'): |
| 287 | continue |
| 288 | |
| 289 | build_utils.AddToZipHermetic(out_apk, apk_path, |
| 290 | data=emma_device_jar.read(apk_path)) |
| 291 | |
| 292 | shutil.move(tmp_apk, options.output_apk) |
| 293 | finally: |
| 294 | if os.path.exists(tmp_apk): |
| 295 | os.unlink(tmp_apk) |
| 296 | |
| 297 | build_utils.CallAndWriteDepfileIfStale( |
| 298 | on_stale_md5, |
| 299 | options, |
| 300 | input_paths=input_paths, |
| 301 | input_strings=input_strings, |
| 302 | output_paths=[options.output_apk]) |
| 303 | |
| 304 | |
| 305 | if __name__ == '__main__': |
| 306 | main(sys.argv[1:]) |