1#!/usr/bin/env python 2# 3# Copyright (c) 2012 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"""Process Android resources to generate R.java, and prepare for packaging. 8 9This will crunch images and generate v14 compatible resources 10(see generate_v14_compatible_resources.py). 11""" 12 13import codecs 14import collections 15import optparse 16import os 17import re 18import shutil 19import sys 20 21import generate_v14_compatible_resources 22 23from util import build_utils 24 25# Import jinja2 from third_party/jinja2 26sys.path.insert(1, 27 os.path.join(os.path.dirname(__file__), '../../../third_party')) 28from jinja2 import Template # pylint: disable=F0401 29 30 31# Represents a line from a R.txt file. 32TextSymbolsEntry = collections.namedtuple('RTextEntry', 33 ('java_type', 'resource_type', 'name', 'value')) 34 35 36def _ParseArgs(args): 37 """Parses command line options. 38 39 Returns: 40 An options object as from optparse.OptionsParser.parse_args() 41 """ 42 parser = optparse.OptionParser() 43 build_utils.AddDepfileOption(parser) 44 45 parser.add_option('--android-sdk-jar', 46 help='the path to android jar file.') 47 parser.add_option('--aapt-path', 48 help='path to the Android aapt tool') 49 parser.add_option('--non-constant-id', action='store_true') 50 51 parser.add_option('--android-manifest', help='AndroidManifest.xml path') 52 parser.add_option('--custom-package', help='Java package for R.java') 53 parser.add_option( 54 '--shared-resources', 55 action='store_true', 56 help='Make a resource package that can be loaded by a different' 57 'application at runtime to access the package\'s resources.') 58 parser.add_option( 59 '--app-as-shared-lib', 60 action='store_true', 61 help='Make a resource package that can be loaded as shared library.') 62 63 parser.add_option('--resource-dirs', 64 help='Directories containing resources of this target.') 65 parser.add_option('--dependencies-res-zips', 66 help='Resources from dependents.') 67 68 parser.add_option('--resource-zip-out', 69 help='Path for output zipped resources.') 70 71 parser.add_option('--R-dir', 72 help='directory to hold generated R.java.') 73 parser.add_option('--srcjar-out', 74 help='Path to srcjar to contain generated R.java.') 75 parser.add_option('--r-text-out', 76 help='Path to store the R.txt file generated by appt.') 77 78 parser.add_option('--proguard-file', 79 help='Path to proguard.txt generated file') 80 81 parser.add_option( 82 '--v14-skip', 83 action="store_true", 84 help='Do not generate nor verify v14 resources') 85 86 parser.add_option( 87 '--extra-res-packages', 88 help='Additional package names to generate R.java files for') 89 parser.add_option( 90 '--extra-r-text-files', 91 help='For each additional package, the R.txt file should contain a ' 92 'list of resources to be included in the R.java file in the format ' 93 'generated by aapt') 94 parser.add_option( 95 '--include-all-resources', 96 action='store_true', 97 help='Include every resource ID in every generated R.java file ' 98 '(ignoring R.txt).') 99 100 parser.add_option( 101 '--all-resources-zip-out', 102 help='Path for output of all resources. This includes resources in ' 103 'dependencies.') 104 105 parser.add_option('--stamp', help='File to touch on success') 106 107 options, positional_args = parser.parse_args(args) 108 109 if positional_args: 110 parser.error('No positional arguments should be given.') 111 112 # Check that required options have been provided. 113 required_options = ( 114 'android_sdk_jar', 115 'aapt_path', 116 'android_manifest', 117 'dependencies_res_zips', 118 'resource_dirs', 119 'resource_zip_out', 120 ) 121 build_utils.CheckOptions(options, parser, required=required_options) 122 123 if (options.R_dir is None) == (options.srcjar_out is None): 124 raise Exception('Exactly one of --R-dir or --srcjar-out must be specified.') 125 126 options.resource_dirs = build_utils.ParseGypList(options.resource_dirs) 127 options.dependencies_res_zips = ( 128 build_utils.ParseGypList(options.dependencies_res_zips)) 129 130 # Don't use [] as default value since some script explicitly pass "". 131 if options.extra_res_packages: 132 options.extra_res_packages = ( 133 build_utils.ParseGypList(options.extra_res_packages)) 134 else: 135 options.extra_res_packages = [] 136 137 if options.extra_r_text_files: 138 options.extra_r_text_files = ( 139 build_utils.ParseGypList(options.extra_r_text_files)) 140 else: 141 options.extra_r_text_files = [] 142 143 return options 144 145 146def CreateExtraRJavaFiles( 147 r_dir, extra_packages, extra_r_text_files, shared_resources, include_all): 148 if include_all: 149 java_files = build_utils.FindInDirectory(r_dir, "R.java") 150 if len(java_files) != 1: 151 return 152 r_java_file = java_files[0] 153 r_java_contents = codecs.open(r_java_file, encoding='utf-8').read() 154 155 for package in extra_packages: 156 package_r_java_dir = os.path.join(r_dir, *package.split('.')) 157 build_utils.MakeDirectory(package_r_java_dir) 158 package_r_java_path = os.path.join(package_r_java_dir, 'R.java') 159 new_r_java = re.sub(r'package [.\w]*;', u'package %s;' % package, 160 r_java_contents) 161 codecs.open(package_r_java_path, 'w', encoding='utf-8').write(new_r_java) 162 else: 163 if len(extra_packages) != len(extra_r_text_files): 164 raise Exception('Need one R.txt file per extra package') 165 166 r_txt_file = os.path.join(r_dir, 'R.txt') 167 if not os.path.exists(r_txt_file): 168 return 169 170 # Map of (resource_type, name) -> Entry. 171 # Contains the correct values for resources. 172 all_resources = {} 173 for entry in _ParseTextSymbolsFile(r_txt_file): 174 all_resources[(entry.resource_type, entry.name)] = entry 175 176 # Map of package_name->resource_type->entry 177 resources_by_package = ( 178 collections.defaultdict(lambda: collections.defaultdict(list))) 179 # Build the R.java files using each package's R.txt file, but replacing 180 # each entry's placeholder value with correct values from all_resources. 181 for package, r_text_file in zip(extra_packages, extra_r_text_files): 182 if not os.path.exists(r_text_file): 183 continue 184 if package in resources_by_package: 185 raise Exception(('Package name "%s" appeared twice. All ' 186 'android_resources() targets must use unique package ' 187 'names, or no package name at all.') % package) 188 resources_by_type = resources_by_package[package] 189 # The sub-R.txt files have the wrong values at this point. Read them to 190 # figure out which entries belong to them, but use the values from the 191 # main R.txt file. 192 for entry in _ParseTextSymbolsFile(r_text_file): 193 entry = all_resources[(entry.resource_type, entry.name)] 194 resources_by_type[entry.resource_type].append(entry) 195 196 for package, resources_by_type in resources_by_package.iteritems(): 197 package_r_java_dir = os.path.join(r_dir, *package.split('.')) 198 build_utils.MakeDirectory(package_r_java_dir) 199 package_r_java_path = os.path.join(package_r_java_dir, 'R.java') 200 java_file_contents = _CreateExtraRJavaFile( 201 package, resources_by_type, shared_resources) 202 with open(package_r_java_path, 'w') as f: 203 f.write(java_file_contents) 204 205 206def _ParseTextSymbolsFile(path): 207 """Given an R.txt file, returns a list of TextSymbolsEntry.""" 208 ret = [] 209 with open(path) as f: 210 for line in f: 211 m = re.match(r'(int(?:\[\])?) (\w+) (\w+) (.+)$', line) 212 if not m: 213 raise Exception('Unexpected line in R.txt: %s' % line) 214 java_type, resource_type, name, value = m.groups() 215 ret.append(TextSymbolsEntry(java_type, resource_type, name, value)) 216 return ret 217 218 219def _CreateExtraRJavaFile(package, resources_by_type, shared_resources): 220 """Generates the contents of a R.java file.""" 221 template = Template("""/* AUTO-GENERATED FILE. DO NOT MODIFY. */ 222 223package {{ package }}; 224 225public final class R { 226 {% for resource_type in resources %} 227 public static final class {{ resource_type }} { 228 {% for e in resources[resource_type] %} 229 {% if shared_resources %} 230 public static {{ e.java_type }} {{ e.name }} = {{ e.value }}; 231 {% else %} 232 public static final {{ e.java_type }} {{ e.name }} = {{ e.value }}; 233 {% endif %} 234 {% endfor %} 235 } 236 {% endfor %} 237 {% if shared_resources %} 238 public static void onResourcesLoaded(int packageId) { 239 {% for resource_type in resources %} 240 {% for e in resources[resource_type] %} 241 {% if e.java_type == 'int[]' %} 242 for(int i = 0; i < {{ e.resource_type }}.{{ e.name }}.length; ++i) { 243 {{ e.resource_type }}.{{ e.name }}[i] = 244 ({{ e.resource_type }}.{{ e.name }}[i] & 0x00ffffff) 245 | (packageId << 24); 246 } 247 {% else %} 248 {{ e.resource_type }}.{{ e.name }} = 249 ({{ e.resource_type }}.{{ e.name }} & 0x00ffffff) 250 | (packageId << 24); 251 {% endif %} 252 {% endfor %} 253 {% endfor %} 254 } 255 {% endif %} 256} 257""", trim_blocks=True, lstrip_blocks=True) 258 259 return template.render(package=package, resources=resources_by_type, 260 shared_resources=shared_resources) 261 262 263def CrunchDirectory(aapt, input_dir, output_dir): 264 """Crunches the images in input_dir and its subdirectories into output_dir. 265 266 If an image is already optimized, crunching often increases image size. In 267 this case, the crunched image is overwritten with the original image. 268 """ 269 aapt_cmd = [aapt, 270 'crunch', 271 '-C', output_dir, 272 '-S', input_dir, 273 '--ignore-assets', build_utils.AAPT_IGNORE_PATTERN] 274 build_utils.CheckOutput(aapt_cmd, stderr_filter=FilterCrunchStderr, 275 fail_func=DidCrunchFail) 276 277 # Check for images whose size increased during crunching and replace them 278 # with their originals (except for 9-patches, which must be crunched). 279 for dir_, _, files in os.walk(output_dir): 280 for crunched in files: 281 if crunched.endswith('.9.png'): 282 continue 283 if not crunched.endswith('.png'): 284 raise Exception('Unexpected file in crunched dir: ' + crunched) 285 crunched = os.path.join(dir_, crunched) 286 original = os.path.join(input_dir, os.path.relpath(crunched, output_dir)) 287 original_size = os.path.getsize(original) 288 crunched_size = os.path.getsize(crunched) 289 if original_size < crunched_size: 290 shutil.copyfile(original, crunched) 291 292 293def FilterCrunchStderr(stderr): 294 """Filters out lines from aapt crunch's stderr that can safely be ignored.""" 295 filtered_lines = [] 296 for line in stderr.splitlines(True): 297 # Ignore this libpng warning, which is a known non-error condition. 298 # http://crbug.com/364355 299 if ('libpng warning: iCCP: Not recognizing known sRGB profile that has ' 300 + 'been edited' in line): 301 continue 302 filtered_lines.append(line) 303 return ''.join(filtered_lines) 304 305 306def DidCrunchFail(returncode, stderr): 307 """Determines whether aapt crunch failed from its return code and output. 308 309 Because aapt's return code cannot be trusted, any output to stderr is 310 an indication that aapt has failed (http://crbug.com/314885). 311 """ 312 return returncode != 0 or stderr 313 314 315def ZipResources(resource_dirs, zip_path): 316 # Python zipfile does not provide a way to replace a file (it just writes 317 # another file with the same name). So, first collect all the files to put 318 # in the zip (with proper overriding), and then zip them. 319 files_to_zip = dict() 320 for d in resource_dirs: 321 for root, _, files in os.walk(d): 322 for f in files: 323 archive_path = f 324 parent_dir = os.path.relpath(root, d) 325 if parent_dir != '.': 326 archive_path = os.path.join(parent_dir, f) 327 path = os.path.join(root, f) 328 files_to_zip[archive_path] = path 329 build_utils.DoZip(files_to_zip.iteritems(), zip_path) 330 331 332def CombineZips(zip_files, output_path): 333 # When packaging resources, if the top-level directories in the zip file are 334 # of the form 0, 1, ..., then each subdirectory will be passed to aapt as a 335 # resources directory. While some resources just clobber others (image files, 336 # etc), other resources (particularly .xml files) need to be more 337 # intelligently merged. That merging is left up to aapt. 338 def path_transform(name, src_zip): 339 return '%d/%s' % (zip_files.index(src_zip), name) 340 341 build_utils.MergeZips(output_path, zip_files, path_transform=path_transform) 342 343 344def _OnStaleMd5(options): 345 aapt = options.aapt_path 346 with build_utils.TempDir() as temp_dir: 347 deps_dir = os.path.join(temp_dir, 'deps') 348 build_utils.MakeDirectory(deps_dir) 349 v14_dir = os.path.join(temp_dir, 'v14') 350 build_utils.MakeDirectory(v14_dir) 351 352 gen_dir = os.path.join(temp_dir, 'gen') 353 build_utils.MakeDirectory(gen_dir) 354 355 input_resource_dirs = options.resource_dirs 356 357 if not options.v14_skip: 358 for resource_dir in input_resource_dirs: 359 generate_v14_compatible_resources.GenerateV14Resources( 360 resource_dir, 361 v14_dir) 362 363 dep_zips = options.dependencies_res_zips 364 dep_subdirs = [] 365 for z in dep_zips: 366 subdir = os.path.join(deps_dir, os.path.basename(z)) 367 if os.path.exists(subdir): 368 raise Exception('Resource zip name conflict: ' + os.path.basename(z)) 369 build_utils.ExtractAll(z, path=subdir) 370 dep_subdirs.append(subdir) 371 372 # Generate R.java. This R.java contains non-final constants and is used only 373 # while compiling the library jar (e.g. chromium_content.jar). When building 374 # an apk, a new R.java file with the correct resource -> ID mappings will be 375 # generated by merging the resources from all libraries and the main apk 376 # project. 377 package_command = [aapt, 378 'package', 379 '-m', 380 '-M', options.android_manifest, 381 '--auto-add-overlay', 382 '--no-version-vectors', 383 '-I', options.android_sdk_jar, 384 '--output-text-symbols', gen_dir, 385 '-J', gen_dir, 386 '--ignore-assets', build_utils.AAPT_IGNORE_PATTERN] 387 388 for d in input_resource_dirs: 389 package_command += ['-S', d] 390 391 for d in dep_subdirs: 392 package_command += ['-S', d] 393 394 if options.non_constant_id: 395 package_command.append('--non-constant-id') 396 if options.custom_package: 397 package_command += ['--custom-package', options.custom_package] 398 if options.proguard_file: 399 package_command += ['-G', options.proguard_file] 400 if options.shared_resources: 401 package_command.append('--shared-lib') 402 if options.app_as_shared_lib: 403 package_command.append('--app-as-shared-lib') 404 build_utils.CheckOutput(package_command, print_stderr=False) 405 406 if options.extra_res_packages: 407 CreateExtraRJavaFiles( 408 gen_dir, 409 options.extra_res_packages, 410 options.extra_r_text_files, 411 options.shared_resources or options.app_as_shared_lib, 412 options.include_all_resources) 413 414 # This is the list of directories with resources to put in the final .zip 415 # file. The order of these is important so that crunched/v14 resources 416 # override the normal ones. 417 zip_resource_dirs = input_resource_dirs + [v14_dir] 418 419 base_crunch_dir = os.path.join(temp_dir, 'crunch') 420 421 # Crunch image resources. This shrinks png files and is necessary for 422 # 9-patch images to display correctly. 'aapt crunch' accepts only a single 423 # directory at a time and deletes everything in the output directory. 424 for idx, input_dir in enumerate(input_resource_dirs): 425 crunch_dir = os.path.join(base_crunch_dir, str(idx)) 426 build_utils.MakeDirectory(crunch_dir) 427 zip_resource_dirs.append(crunch_dir) 428 CrunchDirectory(aapt, input_dir, crunch_dir) 429 430 ZipResources(zip_resource_dirs, options.resource_zip_out) 431 432 if options.all_resources_zip_out: 433 CombineZips([options.resource_zip_out] + dep_zips, 434 options.all_resources_zip_out) 435 436 if options.R_dir: 437 build_utils.DeleteDirectory(options.R_dir) 438 shutil.copytree(gen_dir, options.R_dir) 439 else: 440 build_utils.ZipDir(options.srcjar_out, gen_dir) 441 442 if options.r_text_out: 443 r_text_path = os.path.join(gen_dir, 'R.txt') 444 if os.path.exists(r_text_path): 445 shutil.copyfile(r_text_path, options.r_text_out) 446 else: 447 open(options.r_text_out, 'w').close() 448 449 450def main(args): 451 args = build_utils.ExpandFileArgs(args) 452 options = _ParseArgs(args) 453 454 possible_output_paths = [ 455 options.resource_zip_out, 456 options.all_resources_zip_out, 457 options.proguard_file, 458 options.r_text_out, 459 options.srcjar_out, 460 ] 461 output_paths = [x for x in possible_output_paths if x] 462 463 # List python deps in input_strings rather than input_paths since the contents 464 # of them does not change what gets written to the depsfile. 465 input_strings = options.extra_res_packages + [ 466 options.app_as_shared_lib, 467 options.custom_package, 468 options.include_all_resources, 469 options.non_constant_id, 470 options.shared_resources, 471 options.v14_skip, 472 ] 473 474 input_paths = [ 475 options.aapt_path, 476 options.android_manifest, 477 options.android_sdk_jar, 478 ] 479 input_paths.extend(options.dependencies_res_zips) 480 input_paths.extend(p for p in options.extra_r_text_files if os.path.exists(p)) 481 482 resource_names = [] 483 for resource_dir in options.resource_dirs: 484 for resource_file in build_utils.FindInDirectory(resource_dir, '*'): 485 input_paths.append(resource_file) 486 resource_names.append(os.path.relpath(resource_file, resource_dir)) 487 488 # Resource filenames matter to the output, so add them to strings as well. 489 # This matters if a file is renamed but not changed (http://crbug.com/597126). 490 input_strings.extend(sorted(resource_names)) 491 492 build_utils.CallAndWriteDepfileIfStale( 493 lambda: _OnStaleMd5(options), 494 options, 495 input_paths=input_paths, 496 input_strings=input_strings, 497 output_paths=output_paths, 498 # TODO(agrieve): Remove R_dir when it's no longer used (used only by GYP). 499 force=options.R_dir) 500 501 502if __name__ == '__main__': 503 main(sys.argv[1:]) 504