Ben Murdoch | 097c5b2 | 2016-05-18 11:27:45 +0100 | [diff] [blame^] | 1 | #!/usr/bin/env python |
| 2 | # |
| 3 | # Copyright 2014 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 | # pylint: disable=C0301 |
| 8 | """Package resources into an apk. |
| 9 | |
| 10 | See https://android.googlesource.com/platform/tools/base/+/master/legacy/ant-tasks/src/main/java/com/android/ant/AaptExecTask.java |
| 11 | and |
| 12 | https://android.googlesource.com/platform/sdk/+/master/files/ant/build.xml |
| 13 | """ |
| 14 | # pylint: enable=C0301 |
| 15 | |
| 16 | import optparse |
| 17 | import os |
| 18 | import re |
| 19 | import shutil |
| 20 | import sys |
| 21 | import zipfile |
| 22 | |
| 23 | from util import build_utils |
| 24 | |
| 25 | |
| 26 | # List is generated from the chrome_apk.apk_intermediates.ap_ via: |
| 27 | # unzip -l $FILE_AP_ | cut -c31- | grep res/draw | cut -d'/' -f 2 | sort \ |
| 28 | # | uniq | grep -- -tvdpi- | cut -c10- |
| 29 | # and then manually sorted. |
| 30 | # Note that we can't just do a cross-product of dimentions because the filenames |
| 31 | # become too big and aapt fails to create the files. |
| 32 | # This leaves all default drawables (mdpi) in the main apk. Android gets upset |
| 33 | # though if any drawables are missing from the default drawables/ directory. |
| 34 | DENSITY_SPLITS = { |
| 35 | 'hdpi': ( |
| 36 | 'hdpi-v4', # Order matters for output file names. |
| 37 | 'ldrtl-hdpi-v4', |
| 38 | 'sw600dp-hdpi-v13', |
| 39 | 'ldrtl-hdpi-v17', |
| 40 | 'ldrtl-sw600dp-hdpi-v17', |
| 41 | 'hdpi-v21', |
| 42 | ), |
| 43 | 'xhdpi': ( |
| 44 | 'xhdpi-v4', |
| 45 | 'ldrtl-xhdpi-v4', |
| 46 | 'sw600dp-xhdpi-v13', |
| 47 | 'ldrtl-xhdpi-v17', |
| 48 | 'ldrtl-sw600dp-xhdpi-v17', |
| 49 | 'xhdpi-v21', |
| 50 | ), |
| 51 | 'xxhdpi': ( |
| 52 | 'xxhdpi-v4', |
| 53 | 'ldrtl-xxhdpi-v4', |
| 54 | 'sw600dp-xxhdpi-v13', |
| 55 | 'ldrtl-xxhdpi-v17', |
| 56 | 'ldrtl-sw600dp-xxhdpi-v17', |
| 57 | 'xxhdpi-v21', |
| 58 | ), |
| 59 | 'xxxhdpi': ( |
| 60 | 'xxxhdpi-v4', |
| 61 | 'ldrtl-xxxhdpi-v4', |
| 62 | 'sw600dp-xxxhdpi-v13', |
| 63 | 'ldrtl-xxxhdpi-v17', |
| 64 | 'ldrtl-sw600dp-xxxhdpi-v17', |
| 65 | 'xxxhdpi-v21', |
| 66 | ), |
| 67 | 'tvdpi': ( |
| 68 | 'tvdpi-v4', |
| 69 | 'sw600dp-tvdpi-v13', |
| 70 | 'ldrtl-sw600dp-tvdpi-v17', |
| 71 | ), |
| 72 | } |
| 73 | |
| 74 | |
| 75 | def _ParseArgs(args): |
| 76 | """Parses command line options. |
| 77 | |
| 78 | Returns: |
| 79 | An options object as from optparse.OptionsParser.parse_args() |
| 80 | """ |
| 81 | parser = optparse.OptionParser() |
| 82 | build_utils.AddDepfileOption(parser) |
| 83 | parser.add_option('--android-sdk-jar', |
| 84 | help='path to the Android SDK jar.') |
| 85 | parser.add_option('--aapt-path', |
| 86 | help='path to the Android aapt tool') |
| 87 | |
| 88 | parser.add_option('--configuration-name', |
| 89 | help='Gyp\'s configuration name (Debug or Release).') |
| 90 | |
| 91 | parser.add_option('--android-manifest', help='AndroidManifest.xml path') |
| 92 | parser.add_option('--version-code', help='Version code for apk.') |
| 93 | parser.add_option('--version-name', help='Version name for apk.') |
| 94 | parser.add_option( |
| 95 | '--shared-resources', |
| 96 | action='store_true', |
| 97 | help='Make a resource package that can be loaded by a different' |
| 98 | 'application at runtime to access the package\'s resources.') |
| 99 | parser.add_option( |
| 100 | '--app-as-shared-lib', |
| 101 | action='store_true', |
| 102 | help='Make a resource package that can be loaded as shared library') |
| 103 | parser.add_option('--resource-zips', |
| 104 | default='[]', |
| 105 | help='zip files containing resources to be packaged') |
| 106 | parser.add_option('--asset-dir', |
| 107 | help='directories containing assets to be packaged') |
| 108 | parser.add_option('--no-compress', help='disables compression for the ' |
| 109 | 'given comma separated list of extensions') |
| 110 | parser.add_option( |
| 111 | '--create-density-splits', |
| 112 | action='store_true', |
| 113 | help='Enables density splits') |
| 114 | parser.add_option('--language-splits', |
| 115 | default='[]', |
| 116 | help='GYP list of languages to create splits for') |
| 117 | |
| 118 | parser.add_option('--apk-path', |
| 119 | help='Path to output (partial) apk.') |
| 120 | |
| 121 | options, positional_args = parser.parse_args(args) |
| 122 | |
| 123 | if positional_args: |
| 124 | parser.error('No positional arguments should be given.') |
| 125 | |
| 126 | # Check that required options have been provided. |
| 127 | required_options = ('android_sdk_jar', 'aapt_path', 'configuration_name', |
| 128 | 'android_manifest', 'version_code', 'version_name', |
| 129 | 'apk_path') |
| 130 | |
| 131 | build_utils.CheckOptions(options, parser, required=required_options) |
| 132 | |
| 133 | options.resource_zips = build_utils.ParseGypList(options.resource_zips) |
| 134 | options.language_splits = build_utils.ParseGypList(options.language_splits) |
| 135 | return options |
| 136 | |
| 137 | |
| 138 | def MoveImagesToNonMdpiFolders(res_root): |
| 139 | """Move images from drawable-*-mdpi-* folders to drawable-* folders. |
| 140 | |
| 141 | Why? http://crbug.com/289843 |
| 142 | """ |
| 143 | for src_dir_name in os.listdir(res_root): |
| 144 | src_components = src_dir_name.split('-') |
| 145 | if src_components[0] != 'drawable' or 'mdpi' not in src_components: |
| 146 | continue |
| 147 | src_dir = os.path.join(res_root, src_dir_name) |
| 148 | if not os.path.isdir(src_dir): |
| 149 | continue |
| 150 | dst_components = [c for c in src_components if c != 'mdpi'] |
| 151 | assert dst_components != src_components |
| 152 | dst_dir_name = '-'.join(dst_components) |
| 153 | dst_dir = os.path.join(res_root, dst_dir_name) |
| 154 | build_utils.MakeDirectory(dst_dir) |
| 155 | for src_file_name in os.listdir(src_dir): |
| 156 | if not src_file_name.endswith('.png'): |
| 157 | continue |
| 158 | src_file = os.path.join(src_dir, src_file_name) |
| 159 | dst_file = os.path.join(dst_dir, src_file_name) |
| 160 | assert not os.path.lexists(dst_file) |
| 161 | shutil.move(src_file, dst_file) |
| 162 | |
| 163 | |
| 164 | def PackageArgsForExtractedZip(d): |
| 165 | """Returns the aapt args for an extracted resources zip. |
| 166 | |
| 167 | A resources zip either contains the resources for a single target or for |
| 168 | multiple targets. If it is multiple targets merged into one, the actual |
| 169 | resource directories will be contained in the subdirectories 0, 1, 2, ... |
| 170 | """ |
| 171 | subdirs = [os.path.join(d, s) for s in os.listdir(d)] |
| 172 | subdirs = [s for s in subdirs if os.path.isdir(s)] |
| 173 | is_multi = '0' in [os.path.basename(s) for s in subdirs] |
| 174 | if is_multi: |
| 175 | res_dirs = sorted(subdirs, key=lambda p : int(os.path.basename(p))) |
| 176 | else: |
| 177 | res_dirs = [d] |
| 178 | package_command = [] |
| 179 | for d in res_dirs: |
| 180 | MoveImagesToNonMdpiFolders(d) |
| 181 | package_command += ['-S', d] |
| 182 | return package_command |
| 183 | |
| 184 | |
| 185 | def _GenerateDensitySplitPaths(apk_path): |
| 186 | for density, config in DENSITY_SPLITS.iteritems(): |
| 187 | src_path = '%s_%s' % (apk_path, '_'.join(config)) |
| 188 | dst_path = '%s_%s' % (apk_path, density) |
| 189 | yield src_path, dst_path |
| 190 | |
| 191 | |
| 192 | def _GenerateLanguageSplitOutputPaths(apk_path, languages): |
| 193 | for lang in languages: |
| 194 | yield '%s_%s' % (apk_path, lang) |
| 195 | |
| 196 | |
| 197 | def RenameDensitySplits(apk_path): |
| 198 | """Renames all density splits to have shorter / predictable names.""" |
| 199 | for src_path, dst_path in _GenerateDensitySplitPaths(apk_path): |
| 200 | shutil.move(src_path, dst_path) |
| 201 | |
| 202 | |
| 203 | def CheckForMissedConfigs(apk_path, check_density, languages): |
| 204 | """Raises an exception if apk_path contains any unexpected configs.""" |
| 205 | triggers = [] |
| 206 | if check_density: |
| 207 | triggers.extend(re.compile('-%s' % density) for density in DENSITY_SPLITS) |
| 208 | if languages: |
| 209 | triggers.extend(re.compile(r'-%s\b' % lang) for lang in languages) |
| 210 | with zipfile.ZipFile(apk_path) as main_apk_zip: |
| 211 | for name in main_apk_zip.namelist(): |
| 212 | for trigger in triggers: |
| 213 | if trigger.search(name) and not 'mipmap-' in name: |
| 214 | raise Exception(('Found config in main apk that should have been ' + |
| 215 | 'put into a split: %s\nYou need to update ' + |
| 216 | 'package_resources.py to include this new ' + |
| 217 | 'config (trigger=%s)') % (name, trigger.pattern)) |
| 218 | |
| 219 | |
| 220 | def _ConstructMostAaptArgs(options): |
| 221 | package_command = [ |
| 222 | options.aapt_path, |
| 223 | 'package', |
| 224 | '--version-code', options.version_code, |
| 225 | '--version-name', options.version_name, |
| 226 | '-M', options.android_manifest, |
| 227 | '--no-crunch', |
| 228 | '-f', |
| 229 | '--auto-add-overlay', |
| 230 | '--no-version-vectors', |
| 231 | '-I', options.android_sdk_jar, |
| 232 | '-F', options.apk_path, |
| 233 | '--ignore-assets', build_utils.AAPT_IGNORE_PATTERN, |
| 234 | ] |
| 235 | |
| 236 | if options.no_compress: |
| 237 | for ext in options.no_compress.split(','): |
| 238 | package_command += ['-0', ext] |
| 239 | |
| 240 | if options.shared_resources: |
| 241 | package_command.append('--shared-lib') |
| 242 | |
| 243 | if options.app_as_shared_lib: |
| 244 | package_command.append('--app-as-shared-lib') |
| 245 | |
| 246 | if options.asset_dir and os.path.exists(options.asset_dir): |
| 247 | package_command += ['-A', options.asset_dir] |
| 248 | |
| 249 | if options.create_density_splits: |
| 250 | for config in DENSITY_SPLITS.itervalues(): |
| 251 | package_command.extend(('--split', ','.join(config))) |
| 252 | |
| 253 | if options.language_splits: |
| 254 | for lang in options.language_splits: |
| 255 | package_command.extend(('--split', lang)) |
| 256 | |
| 257 | if 'Debug' in options.configuration_name: |
| 258 | package_command += ['--debug-mode'] |
| 259 | |
| 260 | return package_command |
| 261 | |
| 262 | |
| 263 | def _OnStaleMd5(package_command, options): |
| 264 | with build_utils.TempDir() as temp_dir: |
| 265 | if options.resource_zips: |
| 266 | dep_zips = options.resource_zips |
| 267 | for z in dep_zips: |
| 268 | subdir = os.path.join(temp_dir, os.path.basename(z)) |
| 269 | if os.path.exists(subdir): |
| 270 | raise Exception('Resource zip name conflict: ' + os.path.basename(z)) |
| 271 | build_utils.ExtractAll(z, path=subdir) |
| 272 | package_command += PackageArgsForExtractedZip(subdir) |
| 273 | |
| 274 | build_utils.CheckOutput( |
| 275 | package_command, print_stdout=False, print_stderr=False) |
| 276 | |
| 277 | if options.create_density_splits or options.language_splits: |
| 278 | CheckForMissedConfigs(options.apk_path, options.create_density_splits, |
| 279 | options.language_splits) |
| 280 | |
| 281 | if options.create_density_splits: |
| 282 | RenameDensitySplits(options.apk_path) |
| 283 | |
| 284 | |
| 285 | def main(args): |
| 286 | args = build_utils.ExpandFileArgs(args) |
| 287 | options = _ParseArgs(args) |
| 288 | |
| 289 | package_command = _ConstructMostAaptArgs(options) |
| 290 | |
| 291 | output_paths = [ options.apk_path ] |
| 292 | |
| 293 | if options.create_density_splits: |
| 294 | for _, dst_path in _GenerateDensitySplitPaths(options.apk_path): |
| 295 | output_paths.append(dst_path) |
| 296 | output_paths.extend( |
| 297 | _GenerateLanguageSplitOutputPaths(options.apk_path, |
| 298 | options.language_splits)) |
| 299 | |
| 300 | input_paths = [ options.android_manifest ] + options.resource_zips |
| 301 | |
| 302 | input_strings = [] |
| 303 | input_strings.extend(package_command) |
| 304 | |
| 305 | # The md5_check.py doesn't count file path in md5 intentionally, |
| 306 | # in order to repackage resources when assets' name changed, we need |
| 307 | # to put assets into input_strings, as we know the assets path isn't |
| 308 | # changed among each build if there is no asset change. |
| 309 | if options.asset_dir and os.path.exists(options.asset_dir): |
| 310 | asset_paths = [] |
| 311 | for root, _, filenames in os.walk(options.asset_dir): |
| 312 | asset_paths.extend(os.path.join(root, f) for f in filenames) |
| 313 | input_paths.extend(asset_paths) |
| 314 | input_strings.extend(sorted(asset_paths)) |
| 315 | |
| 316 | build_utils.CallAndWriteDepfileIfStale( |
| 317 | lambda: _OnStaleMd5(package_command, options), |
| 318 | options, |
| 319 | input_paths=input_paths, |
| 320 | input_strings=input_strings, |
| 321 | output_paths=output_paths) |
| 322 | |
| 323 | |
| 324 | if __name__ == '__main__': |
| 325 | main(sys.argv[1:]) |