1# Copyright 2018 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import argparse 6import collections 7import contextlib 8import os 9import re 10import shutil 11import sys 12import tempfile 13from xml.etree import ElementTree 14 15import util.build_utils as build_utils 16 17_SOURCE_ROOT = os.path.abspath( 18 os.path.join(os.path.dirname(__file__), '..', '..', '..', '..')) 19# Import jinja2 from third_party/jinja2 20sys.path.insert(1, os.path.join(_SOURCE_ROOT, 'third_party')) 21from jinja2 import Template # pylint: disable=F0401 22 23 24EMPTY_ANDROID_MANIFEST_PATH = os.path.join( 25 _SOURCE_ROOT, 'build', 'android', 'AndroidManifest.xml') 26 27 28# A variation of this lists also exists in: 29# //base/android/java/src/org/chromium/base/LocaleUtils.java 30# //ui/android/java/src/org/chromium/base/LocalizationUtils.java 31CHROME_TO_ANDROID_LOCALE_MAP = { 32 'en-GB': 'en-rGB', 33 'en-US': 'en-rUS', 34 'es-419': 'es-rUS', 35 'fil': 'tl', 36 'he': 'iw', 37 'id': 'in', 38 'pt-PT': 'pt-rPT', 39 'pt-BR': 'pt-rBR', 40 'yi': 'ji', 41 'zh-CN': 'zh-rCN', 42 'zh-TW': 'zh-rTW', 43} 44 45# Represents a line from a R.txt file. 46_TextSymbolEntry = collections.namedtuple('RTextEntry', 47 ('java_type', 'resource_type', 'name', 'value')) 48 49 50def CreateResourceInfoFile(files_to_zip, zip_path): 51 """Given a mapping of archive paths to their source, write an info file. 52 53 The info file contains lines of '{archive_path},{source_path}' for ease of 54 parsing. Assumes that there is no comma in the file names. 55 56 Args: 57 files_to_zip: Dict mapping path in the zip archive to original source. 58 zip_path: Path where the zip file ends up, this is where the info file goes. 59 """ 60 info_file_path = zip_path + '.info' 61 with open(info_file_path, 'w') as info_file: 62 for archive_path, source_path in files_to_zip.iteritems(): 63 info_file.write('{},{}\n'.format(archive_path, source_path)) 64 65 66def _ParseTextSymbolsFile(path, fix_package_ids=False): 67 """Given an R.txt file, returns a list of _TextSymbolEntry. 68 69 Args: 70 path: Input file path. 71 fix_package_ids: if True, all packaged IDs read from the file 72 will be fixed to 0x7f. 73 Returns: 74 A list of _TextSymbolEntry instances. 75 Raises: 76 Exception: An unexpected line was detected in the input. 77 """ 78 ret = [] 79 with open(path) as f: 80 for line in f: 81 m = re.match(r'(int(?:\[\])?) (\w+) (\w+) (.+)$', line) 82 if not m: 83 raise Exception('Unexpected line in R.txt: %s' % line) 84 java_type, resource_type, name, value = m.groups() 85 if fix_package_ids: 86 value = _FixPackageIds(value) 87 ret.append(_TextSymbolEntry(java_type, resource_type, name, value)) 88 return ret 89 90 91def _FixPackageIds(resource_value): 92 # Resource IDs for resources belonging to regular APKs have their first byte 93 # as 0x7f (package id). However with webview, since it is not a regular apk 94 # but used as a shared library, aapt is passed the --shared-resources flag 95 # which changes some of the package ids to 0x02 and 0x00. This function just 96 # normalises all package ids to 0x7f, which the generated code in R.java 97 # changes to the correct package id at runtime. 98 # resource_value is a string with either, a single value '0x12345678', or an 99 # array of values like '{ 0xfedcba98, 0x01234567, 0x56789abc }' 100 return re.sub(r'0x(?!01)\d\d', r'0x7f', resource_value) 101 102 103def _GetRTxtResourceNames(r_txt_path): 104 """Parse an R.txt file and extract the set of resource names from it.""" 105 result = set() 106 for entry in _ParseTextSymbolsFile(r_txt_path): 107 result.add(entry.name) 108 return result 109 110 111class RJavaBuildOptions: 112 """A class used to model the various ways to build an R.java file. 113 114 This is used to control which resource ID variables will be final or 115 non-final, and whether an onResourcesLoaded() method will be generated 116 to adjust the non-final ones, when the corresponding library is loaded 117 at runtime. 118 119 Note that by default, all resources are final, and there is no 120 method generated, which corresponds to calling ExportNoResources(). 121 """ 122 def __init__(self): 123 self.has_constant_ids = True 124 self.resources_whitelist = None 125 self.has_on_resources_loaded = False 126 self.export_const_styleable = False 127 128 def ExportNoResources(self): 129 """Make all resource IDs final, and don't generate a method.""" 130 self.has_constant_ids = True 131 self.resources_whitelist = None 132 self.has_on_resources_loaded = False 133 self.export_const_styleable = False 134 135 def ExportAllResources(self): 136 """Make all resource IDs non-final in the R.java file.""" 137 self.has_constant_ids = False 138 self.resources_whitelist = None 139 140 def ExportSomeResources(self, r_txt_file_path): 141 """Only select specific resource IDs to be non-final. 142 143 Args: 144 r_txt_file_path: The path to an R.txt file. All resources named 145 int it will be non-final in the generated R.java file, all others 146 will be final. 147 """ 148 self.has_constant_ids = True 149 self.resources_whitelist = _GetRTxtResourceNames(r_txt_file_path) 150 151 def ExportAllStyleables(self): 152 """Make all styleable constants non-final, even non-resources ones. 153 154 Resources that are styleable but not of int[] type are not actually 155 resource IDs but constants. By default they are always final. Call this 156 method to make them non-final anyway in the final R.java file. 157 """ 158 self.export_const_styleable = True 159 160 def GenerateOnResourcesLoaded(self): 161 """Generate an onResourcesLoaded() method. 162 163 This Java method will be called at runtime by the framework when 164 the corresponding library (which includes the R.java source file) 165 will be loaded at runtime. This corresponds to the --shared-resources 166 or --app-as-shared-lib flags of 'aapt package'. 167 """ 168 self.has_on_resources_loaded = True 169 170 def _IsResourceFinal(self, entry): 171 """Determines whether a resource should be final or not. 172 173 Args: 174 entry: A _TextSymbolEntry instance. 175 Returns: 176 True iff the corresponding entry should be final. 177 """ 178 if entry.resource_type == 'styleable' and entry.java_type != 'int[]': 179 # A styleable constant may be exported as non-final after all. 180 return not self.export_const_styleable 181 elif not self.has_constant_ids: 182 # Every resource is non-final 183 return False 184 elif not self.resources_whitelist: 185 # No whitelist means all IDs are non-final. 186 return True 187 else: 188 # Otherwise, only those in the 189 return entry.name not in self.resources_whitelist 190 191 192def CreateRJavaFiles(srcjar_dir, package, main_r_txt_file, 193 extra_res_packages, extra_r_txt_files, 194 rjava_build_options): 195 """Create all R.java files for a set of packages and R.txt files. 196 197 Args: 198 srcjar_dir: The top-level output directory for the generated files. 199 package: Top-level package name. 200 main_r_txt_file: The main R.txt file containing the valid values 201 of _all_ resource IDs. 202 extra_res_packages: A list of extra package names. 203 extra_r_txt_files: A list of extra R.txt files. One per item in 204 |extra_res_packages|. Note that all resource IDs in them will be ignored, 205 |and replaced by the values extracted from |main_r_txt_file|. 206 rjava_build_options: An RJavaBuildOptions instance that controls how 207 exactly the R.java file is generated. 208 Raises: 209 Exception if a package name appears several times in |extra_res_packages| 210 """ 211 assert len(extra_res_packages) == len(extra_r_txt_files), \ 212 'Need one R.txt file per package' 213 214 packages = list(extra_res_packages) 215 r_txt_files = list(extra_r_txt_files) 216 217 if package and package not in packages: 218 # Sometimes, an apk target and a resources target share the same 219 # AndroidManifest.xml and thus |package| will already be in |packages|. 220 packages.append(package) 221 r_txt_files.append(main_r_txt_file) 222 223 # Map of (resource_type, name) -> Entry. 224 # Contains the correct values for resources. 225 all_resources = {} 226 for entry in _ParseTextSymbolsFile(main_r_txt_file, fix_package_ids=True): 227 all_resources[(entry.resource_type, entry.name)] = entry 228 229 # Map of package_name->resource_type->entry 230 resources_by_package = ( 231 collections.defaultdict(lambda: collections.defaultdict(list))) 232 # Build the R.java files using each package's R.txt file, but replacing 233 # each entry's placeholder value with correct values from all_resources. 234 for package, r_txt_file in zip(packages, r_txt_files): 235 if package in resources_by_package: 236 raise Exception(('Package name "%s" appeared twice. All ' 237 'android_resources() targets must use unique package ' 238 'names, or no package name at all.') % package) 239 resources_by_type = resources_by_package[package] 240 # The sub-R.txt files have the wrong values at this point. Read them to 241 # figure out which entries belong to them, but use the values from the 242 # main R.txt file. 243 for entry in _ParseTextSymbolsFile(r_txt_file): 244 entry = all_resources.get((entry.resource_type, entry.name)) 245 # For most cases missing entry here is an error. It means that some 246 # library claims to have or depend on a resource that isn't included into 247 # the APK. There is one notable exception: Google Play Services (GMS). 248 # GMS is shipped as a bunch of AARs. One of them - basement - contains 249 # R.txt with ids of all resources, but most of the resources are in the 250 # other AARs. However, all other AARs reference their resources via 251 # basement's R.java so the latter must contain all ids that are in its 252 # R.txt. Most targets depend on only a subset of GMS AARs so some 253 # resources are missing, which is okay because the code that references 254 # them is missing too. We can't get an id for a resource that isn't here 255 # so the only solution is to skip the resource entry entirely. 256 # 257 # We can verify that all entries referenced in the code were generated 258 # correctly by running Proguard on the APK: it will report missing 259 # fields. 260 if entry: 261 resources_by_type[entry.resource_type].append(entry) 262 263 for package, resources_by_type in resources_by_package.iteritems(): 264 _CreateRJavaSourceFile(srcjar_dir, package, resources_by_type, 265 rjava_build_options) 266 267 268def _CreateRJavaSourceFile(srcjar_dir, package, resources_by_type, 269 rjava_build_options): 270 """Generates an R.java source file.""" 271 package_r_java_dir = os.path.join(srcjar_dir, *package.split('.')) 272 build_utils.MakeDirectory(package_r_java_dir) 273 package_r_java_path = os.path.join(package_r_java_dir, 'R.java') 274 java_file_contents = _RenderRJavaSource(package, resources_by_type, 275 rjava_build_options) 276 with open(package_r_java_path, 'w') as f: 277 f.write(java_file_contents) 278 279 280# Resource IDs inside resource arrays are sorted. Application resource IDs start 281# with 0x7f but system resource IDs start with 0x01 thus system resource ids are 282# always at the start of the array. This function finds the index of the first 283# non system resource id to be used for package ID rewriting (we should not 284# rewrite system resource ids). 285def _GetNonSystemIndex(entry): 286 """Get the index of the first application resource ID within a resource 287 array.""" 288 res_ids = re.findall(r'0x[0-9a-f]{8}', entry.value) 289 for i, res_id in enumerate(res_ids): 290 if res_id.startswith('0x7f'): 291 return i 292 return len(res_ids) 293 294 295def _RenderRJavaSource(package, resources_by_type, rjava_build_options): 296 """Render an R.java source file. See _CreateRJaveSourceFile for args info.""" 297 final_resources_by_type = collections.defaultdict(list) 298 non_final_resources_by_type = collections.defaultdict(list) 299 for res_type, resources in resources_by_type.iteritems(): 300 for entry in resources: 301 # Entries in stylable that are not int[] are not actually resource ids 302 # but constants. 303 if rjava_build_options._IsResourceFinal(entry): 304 final_resources_by_type[res_type].append(entry) 305 else: 306 non_final_resources_by_type[res_type].append(entry) 307 308 # Keep these assignments all on one line to make diffing against regular 309 # aapt-generated files easier. 310 create_id = ('{{ e.resource_type }}.{{ e.name }} ^= packageIdTransform;') 311 create_id_arr = ('{{ e.resource_type }}.{{ e.name }}[i] ^=' 312 ' packageIdTransform;') 313 for_loop_condition = ('int i = {{ startIndex(e) }}; i < ' 314 '{{ e.resource_type }}.{{ e.name }}.length; ++i') 315 316 # Here we diverge from what aapt does. Because we have so many 317 # resources, the onResourcesLoaded method was exceeding the 64KB limit that 318 # Java imposes. For this reason we split onResourcesLoaded into different 319 # methods for each resource type. 320 template = Template("""/* AUTO-GENERATED FILE. DO NOT MODIFY. */ 321 322package {{ package }}; 323 324public final class R { 325 private static boolean sResourcesDidLoad; 326 {% for resource_type in resource_types %} 327 public static final class {{ resource_type }} { 328 {% for e in final_resources[resource_type] %} 329 public static final {{ e.java_type }} {{ e.name }} = {{ e.value }}; 330 {% endfor %} 331 {% for e in non_final_resources[resource_type] %} 332 public static {{ e.java_type }} {{ e.name }} = {{ e.value }}; 333 {% endfor %} 334 } 335 {% endfor %} 336 {% if has_on_resources_loaded %} 337 public static void onResourcesLoaded(int packageId) { 338 assert !sResourcesDidLoad; 339 sResourcesDidLoad = true; 340 int packageIdTransform = (packageId ^ 0x7f) << 24; 341 {% for resource_type in resource_types %} 342 onResourcesLoaded{{ resource_type|title }}(packageIdTransform); 343 {% for e in non_final_resources[resource_type] %} 344 {% if e.java_type == 'int[]' %} 345 for(""" + for_loop_condition + """) { 346 """ + create_id_arr + """ 347 } 348 {% endif %} 349 {% endfor %} 350 {% endfor %} 351 } 352 {% for res_type in resource_types %} 353 private static void onResourcesLoaded{{ res_type|title }} ( 354 int packageIdTransform) { 355 {% for e in non_final_resources[res_type] %} 356 {% if res_type != 'styleable' and e.java_type != 'int[]' %} 357 """ + create_id + """ 358 {% endif %} 359 {% endfor %} 360 } 361 {% endfor %} 362 {% endif %} 363} 364""", trim_blocks=True, lstrip_blocks=True) 365 366 return template.render( 367 package=package, 368 resource_types=sorted(resources_by_type), 369 has_on_resources_loaded=rjava_build_options.has_on_resources_loaded, 370 final_resources=final_resources_by_type, 371 non_final_resources=non_final_resources_by_type, 372 startIndex=_GetNonSystemIndex) 373 374 375def ExtractPackageFromManifest(manifest_path): 376 """Extract package name from Android manifest file.""" 377 doc = ElementTree.parse(manifest_path) 378 return doc.getroot().get('package') 379 380 381def ExtractDeps(dep_zips, deps_dir): 382 """Extract a list of resource dependency zip files. 383 384 Args: 385 dep_zips: A list of zip file paths, each one will be extracted to 386 a subdirectory of |deps_dir|, named after the zip file (e.g. 387 '/some/path/foo.zip' -> '{deps_dir}/foo/'). 388 deps_dir: Top-level extraction directory. 389 Returns: 390 The list of all sub-directory paths, relative to |deps_dir|. 391 Raises: 392 Exception: If a sub-directory already exists with the same name before 393 extraction. 394 """ 395 dep_subdirs = [] 396 for z in dep_zips: 397 subdir = os.path.join(deps_dir, os.path.basename(z)) 398 if os.path.exists(subdir): 399 raise Exception('Resource zip name conflict: ' + os.path.basename(z)) 400 build_utils.ExtractAll(z, path=subdir) 401 dep_subdirs.append(subdir) 402 return dep_subdirs 403 404 405class _ResourceBuildContext(object): 406 """A temporary directory for packaging and compiling Android resources.""" 407 def __init__(self): 408 """Initialized the context.""" 409 # The top-level temporary directory. 410 self.temp_dir = tempfile.mkdtemp() 411 # A location to store resources extracted form dependency zip files. 412 self.deps_dir = os.path.join(self.temp_dir, 'deps') 413 os.mkdir(self.deps_dir) 414 # A location to place aapt-generated files. 415 self.gen_dir = os.path.join(self.temp_dir, 'gen') 416 os.mkdir(self.gen_dir) 417 # Location of the generated R.txt file. 418 self.r_txt_path = os.path.join(self.gen_dir, 'R.txt') 419 # A location to place generated R.java files. 420 self.srcjar_dir = os.path.join(self.temp_dir, 'java') 421 os.mkdir(self.srcjar_dir) 422 423 def Close(self): 424 """Close the context and destroy all temporary files.""" 425 shutil.rmtree(self.temp_dir) 426 427 428@contextlib.contextmanager 429def BuildContext(): 430 """Generator for a _ResourceBuildContext instance.""" 431 try: 432 context = _ResourceBuildContext() 433 yield context 434 finally: 435 context.Close() 436 437 438def ResourceArgsParser(): 439 """Create an argparse.ArgumentParser instance with common argument groups. 440 441 Returns: 442 A tuple of (parser, in_group, out_group) corresponding to the parser 443 instance, and the input and output argument groups for it, respectively. 444 """ 445 parser = argparse.ArgumentParser(description=__doc__) 446 447 input_opts = parser.add_argument_group('Input options') 448 output_opts = parser.add_argument_group('Output options') 449 450 build_utils.AddDepfileOption(output_opts) 451 452 input_opts.add_argument('--android-sdk-jars', required=True, 453 help='Path to the android.jar file.') 454 455 input_opts.add_argument('--aapt-path', required=True, 456 help='Path to the Android aapt tool') 457 458 input_opts.add_argument('--aapt2-path', 459 help='Path to the Android aapt2 tool. If in different' 460 ' directory from --aapt-path.') 461 462 input_opts.add_argument('--dependencies-res-zips', required=True, 463 help='Resources zip archives from dependents. Required to ' 464 'resolve @type/foo references into dependent ' 465 'libraries.') 466 467 input_opts.add_argument( 468 '--r-text-in', 469 help='Path to pre-existing R.txt. Its resource IDs override those found ' 470 'in the aapt-generated R.txt when generating R.java.') 471 472 input_opts.add_argument( 473 '--extra-res-packages', 474 help='Additional package names to generate R.java files for.') 475 476 input_opts.add_argument( 477 '--extra-r-text-files', 478 help='For each additional package, the R.txt file should contain a ' 479 'list of resources to be included in the R.java file in the format ' 480 'generated by aapt.') 481 482 return (parser, input_opts, output_opts) 483 484 485def HandleCommonOptions(options): 486 """Handle common command-line options after parsing. 487 488 Args: 489 options: the result of parse_args() on the parser returned by 490 ResourceArgsParser(). This function updates a few common fields. 491 """ 492 options.android_sdk_jars = build_utils.ParseGnList(options.android_sdk_jars) 493 494 options.dependencies_res_zips = ( 495 build_utils.ParseGnList(options.dependencies_res_zips)) 496 497 # Don't use [] as default value since some script explicitly pass "". 498 if options.extra_res_packages: 499 options.extra_res_packages = ( 500 build_utils.ParseGnList(options.extra_res_packages)) 501 else: 502 options.extra_res_packages = [] 503 504 if options.extra_r_text_files: 505 options.extra_r_text_files = ( 506 build_utils.ParseGnList(options.extra_r_text_files)) 507 else: 508 options.extra_r_text_files = [] 509 510 if not options.aapt2_path: 511 options.aapt2_path = options.aapt_path + '2' 512