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:]) 326