1# Copyright 2018 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import collections 6import contextlib 7import itertools 8import os 9import re 10import shutil 11import subprocess 12import sys 13import tempfile 14import zipfile 15from xml.etree import ElementTree 16 17import util.build_utils as build_utils 18 19_SOURCE_ROOT = os.path.abspath( 20 os.path.join(os.path.dirname(__file__), '..', '..', '..', '..')) 21# Import jinja2 from third_party/jinja2 22sys.path.insert(1, os.path.join(_SOURCE_ROOT, 'third_party')) 23from jinja2 import Template # pylint: disable=F0401 24 25 26# A variation of these maps also exists in: 27# //base/android/java/src/org/chromium/base/LocaleUtils.java 28# //ui/android/java/src/org/chromium/base/LocalizationUtils.java 29_CHROME_TO_ANDROID_LOCALE_MAP = { 30 'es-419': 'es-rUS', 31 'sr-Latn': 'b+sr+Latn', 32 'fil': 'tl', 33 'he': 'iw', 34 'id': 'in', 35 'yi': 'ji', 36} 37_ANDROID_TO_CHROMIUM_LANGUAGE_MAP = { 38 'tl': 'fil', 39 'iw': 'he', 40 'in': 'id', 41 'ji': 'yi', 42 'no': 'nb', # 'no' is not a real language. http://crbug.com/920960 43} 44 45ALL_RESOURCE_TYPES = { 46 'anim', 'animator', 'array', 'attr', 'bool', 'color', 'dimen', 'drawable', 47 'font', 'fraction', 'id', 'integer', 'interpolator', 'layout', 'macro', 48 'menu', 'mipmap', 'overlayable', 'plurals', 'raw', 'string', 'style', 49 'styleable', 'transition', 'xml' 50} 51 52AAPT_IGNORE_PATTERN = ':'.join([ 53 '*OWNERS', # Allow OWNERS files within res/ 54 'DIR_METADATA', # Allow DIR_METADATA files within res/ 55 '*.py', # PRESUBMIT.py sometimes exist. 56 '*.pyc', 57 '*~', # Some editors create these as temp files. 58 '.*', # Never makes sense to include dot(files/dirs). 59 '*.d.stamp', # Ignore stamp files 60 '*.backup', # Some tools create temporary backup files. 61]) 62 63MULTIPLE_RES_MAGIC_STRING = b'magic' 64 65 66def ToAndroidLocaleName(chromium_locale): 67 """Convert a Chromium locale name into a corresponding Android one.""" 68 # Should be in sync with build/config/locales.gni. 69 # First handle the special cases, these are needed to deal with Android 70 # releases *before* 5.0/Lollipop. 71 android_locale = _CHROME_TO_ANDROID_LOCALE_MAP.get(chromium_locale) 72 if android_locale: 73 return android_locale 74 75 # Format of Chromium locale name is '<lang>' or '<lang>-<region>' 76 # where <lang> is a 2 or 3 letter language code (ISO 639-1 or 639-2) 77 # and region is a capitalized locale region name. 78 lang, _, region = chromium_locale.partition('-') 79 if not region: 80 return lang 81 82 # Translate newer language tags into obsolete ones. Only necessary if 83 # region is not None (e.g. 'he-IL' -> 'iw-rIL') 84 lang = _CHROME_TO_ANDROID_LOCALE_MAP.get(lang, lang) 85 86 # Using '<lang>-r<region>' is now acceptable as a locale name for all 87 # versions of Android. 88 return '%s-r%s' % (lang, region) 89 90 91# ISO 639 language code + optional ("-r" + capitalized region code). 92# Note that before Android 5.0/Lollipop, only 2-letter ISO 639-1 codes 93# are supported. 94_RE_ANDROID_LOCALE_QUALIFIER_1 = re.compile(r'^([a-z]{2,3})(\-r([A-Z]+))?$') 95 96# Starting with Android 7.0/Nougat, BCP 47 codes are supported but must 97# be prefixed with 'b+', and may include optional tags. 98# e.g. 'b+en+US', 'b+ja+Latn', 'b+ja+Latn+JP' 99_RE_ANDROID_LOCALE_QUALIFIER_2 = re.compile(r'^b\+([a-z]{2,3})(\+.+)?$') 100 101 102def ToChromiumLocaleName(android_locale): 103 """Convert an Android locale name into a Chromium one.""" 104 lang = None 105 region = None 106 script = None 107 m = _RE_ANDROID_LOCALE_QUALIFIER_1.match(android_locale) 108 if m: 109 lang = m.group(1) 110 if m.group(2): 111 region = m.group(3) 112 elif _RE_ANDROID_LOCALE_QUALIFIER_2.match(android_locale): 113 # Split an Android BCP-47 locale (e.g. b+sr+Latn+RS) 114 tags = android_locale.split('+') 115 116 # The Lang tag is always the first tag. 117 lang = tags[1] 118 119 # The optional region tag is 2ALPHA or 3DIGIT tag in pos 1 or 2. 120 # The optional script tag is 4ALPHA and always in pos 1. 121 optional_tags = iter(tags[2:]) 122 123 next_tag = next(optional_tags, None) 124 if next_tag and len(next_tag) == 4: 125 script = next_tag 126 next_tag = next(optional_tags, None) 127 if next_tag and len(next_tag) < 4: 128 region = next_tag 129 130 if not lang: 131 return None 132 133 # Special case for es-rUS -> es-419 134 if lang == 'es' and region == 'US': 135 return 'es-419' 136 137 lang = _ANDROID_TO_CHROMIUM_LANGUAGE_MAP.get(lang, lang) 138 139 if script: 140 lang = '%s-%s' % (lang, script) 141 142 if not region: 143 return lang 144 145 return '%s-%s' % (lang, region) 146 147 148def IsAndroidLocaleQualifier(string): 149 """Returns true if |string| is a valid Android resource locale qualifier.""" 150 return (_RE_ANDROID_LOCALE_QUALIFIER_1.match(string) 151 or _RE_ANDROID_LOCALE_QUALIFIER_2.match(string)) 152 153 154def FindLocaleInStringResourceFilePath(file_path): 155 """Return Android locale name of a string resource file path. 156 157 Args: 158 file_path: A file path. 159 Returns: 160 If |file_path| is of the format '.../values-<locale>/<name>.xml', return 161 the value of <locale> (and Android locale qualifier). Otherwise return None. 162 """ 163 if not file_path.endswith('.xml'): 164 return None 165 prefix = 'values-' 166 dir_name = os.path.basename(os.path.dirname(file_path)) 167 if not dir_name.startswith(prefix): 168 return None 169 qualifier = dir_name[len(prefix):] 170 return qualifier if IsAndroidLocaleQualifier(qualifier) else None 171 172 173def ToAndroidLocaleList(locale_list): 174 """Convert a list of Chromium locales into the corresponding Android list.""" 175 return sorted(ToAndroidLocaleName(locale) for locale in locale_list) 176 177# Represents a line from a R.txt file. 178_TextSymbolEntry = collections.namedtuple('RTextEntry', 179 ('java_type', 'resource_type', 'name', 'value')) 180 181 182def _GenerateGlobs(pattern): 183 # This function processes the aapt ignore assets pattern into a list of globs 184 # to be used to exclude files using build_utils.MatchesGlob. It removes the 185 # '!', which is used by aapt to mean 'not chatty' so it does not output if the 186 # file is ignored (we dont output anyways, so it is not required). This 187 # function does not handle the <dir> and <file> prefixes used by aapt and are 188 # assumed not to be included in the pattern string. 189 return pattern.replace('!', '').split(':') 190 191 192def DeduceResourceDirsFromFileList(resource_files): 193 """Return a list of resource directories from a list of resource files.""" 194 # Directory list order is important, cannot use set or other data structures 195 # that change order. This is because resource files of the same name in 196 # multiple res/ directories ellide one another (the last one passed is used). 197 # Thus the order must be maintained to prevent non-deterministic and possibly 198 # flakey builds. 199 resource_dirs = [] 200 for resource_path in resource_files: 201 # Resources are always 1 directory deep under res/. 202 res_dir = os.path.dirname(os.path.dirname(resource_path)) 203 if res_dir not in resource_dirs: 204 resource_dirs.append(res_dir) 205 206 # Check if any resource_dirs are children of other ones. This indicates that a 207 # file was listed that is not exactly 1 directory deep under res/. 208 # E.g.: 209 # sources = ["java/res/values/foo.xml", "java/res/README.md"] 210 # ^^ This will cause "java" to be detected as resource directory. 211 for a, b in itertools.permutations(resource_dirs, 2): 212 if not os.path.relpath(a, b).startswith('..'): 213 bad_sources = (s for s in resource_files 214 if os.path.dirname(os.path.dirname(s)) == b) 215 msg = """\ 216Resource(s) found that are not in a proper directory structure: 217 {} 218All resource files must follow a structure of "$ROOT/$SUBDIR/$FILE".""" 219 raise Exception(msg.format('\n '.join(bad_sources))) 220 221 return resource_dirs 222 223 224def IterResourceFilesInDirectories(directories, 225 ignore_pattern=AAPT_IGNORE_PATTERN): 226 globs = _GenerateGlobs(ignore_pattern) 227 for d in directories: 228 for root, _, files in os.walk(d): 229 for f in files: 230 archive_path = f 231 parent_dir = os.path.relpath(root, d) 232 if parent_dir != '.': 233 archive_path = os.path.join(parent_dir, f) 234 path = os.path.join(root, f) 235 if build_utils.MatchesGlob(archive_path, globs): 236 continue 237 yield path, archive_path 238 239 240class ResourceInfoFile: 241 """Helper for building up .res.info files.""" 242 243 def __init__(self): 244 # Dict of archive_path -> source_path for the current target. 245 self._entries = {} 246 # List of (old_archive_path, new_archive_path) tuples. 247 self._renames = [] 248 # We don't currently support using both AddMapping and MergeInfoFile. 249 self._add_mapping_was_called = False 250 251 def AddMapping(self, archive_path, source_path): 252 """Adds a single |archive_path| -> |source_path| entry.""" 253 self._add_mapping_was_called = True 254 # "values/" files do not end up in the apk except through resources.arsc. 255 if archive_path.startswith('values'): 256 return 257 source_path = os.path.normpath(source_path) 258 new_value = self._entries.setdefault(archive_path, source_path) 259 if new_value != source_path: 260 raise Exception('Duplicate AddMapping for "{}". old={} new={}'.format( 261 archive_path, new_value, source_path)) 262 263 def RegisterRename(self, old_archive_path, new_archive_path): 264 """Records an archive_path rename. 265 266 |old_archive_path| does not need to currently exist in the mappings. Renames 267 are buffered and replayed only when Write() is called. 268 """ 269 if not old_archive_path.startswith('values'): 270 self._renames.append((old_archive_path, new_archive_path)) 271 272 def MergeInfoFile(self, info_file_path): 273 """Merges the mappings from |info_file_path| into this object. 274 275 Any existing entries are overridden. 276 """ 277 assert not self._add_mapping_was_called 278 # Allows clobbering, which is used when overriding resources. 279 with open(info_file_path) as f: 280 self._entries.update(l.rstrip().split('\t') for l in f) 281 282 def _ApplyRenames(self): 283 applied_renames = set() 284 ret = self._entries 285 for rename_tup in self._renames: 286 # Duplicate entries happen for resource overrides. 287 # Use a "seen" set to ensure we still error out if multiple renames 288 # happen for the same old_archive_path with different new_archive_paths. 289 if rename_tup in applied_renames: 290 continue 291 applied_renames.add(rename_tup) 292 old_archive_path, new_archive_path = rename_tup 293 ret[new_archive_path] = ret[old_archive_path] 294 del ret[old_archive_path] 295 296 self._entries = None 297 self._renames = None 298 return ret 299 300 def Write(self, info_file_path): 301 """Applies renames and writes out the file. 302 303 No other methods may be called after this. 304 """ 305 entries = self._ApplyRenames() 306 lines = [] 307 for archive_path, source_path in entries.items(): 308 lines.append('{}\t{}\n'.format(archive_path, source_path)) 309 with open(info_file_path, 'w') as info_file: 310 info_file.writelines(sorted(lines)) 311 312 313def _ParseTextSymbolsFile(path, fix_package_ids=False): 314 """Given an R.txt file, returns a list of _TextSymbolEntry. 315 316 Args: 317 path: Input file path. 318 fix_package_ids: if True, 0x00 and 0x02 package IDs read from the file 319 will be fixed to 0x7f. 320 Returns: 321 A list of _TextSymbolEntry instances. 322 Raises: 323 Exception: An unexpected line was detected in the input. 324 """ 325 ret = [] 326 with open(path) as f: 327 for line in f: 328 m = re.match(r'(int(?:\[\])?) (\w+) (\w+) (.+)$', line) 329 if not m: 330 raise Exception('Unexpected line in R.txt: %s' % line) 331 java_type, resource_type, name, value = m.groups() 332 if fix_package_ids: 333 value = _FixPackageIds(value) 334 ret.append(_TextSymbolEntry(java_type, resource_type, name, value)) 335 return ret 336 337 338def _FixPackageIds(resource_value): 339 # Resource IDs for resources belonging to regular APKs have their first byte 340 # as 0x7f (package id). However with webview, since it is not a regular apk 341 # but used as a shared library, aapt is passed the --shared-resources flag 342 # which changes some of the package ids to 0x00. This function normalises 343 # these (0x00) package ids to 0x7f, which the generated code in R.java changes 344 # to the correct package id at runtime. resource_value is a string with 345 # either, a single value '0x12345678', or an array of values like '{ 346 # 0xfedcba98, 0x01234567, 0x56789abc }' 347 return resource_value.replace('0x00', '0x7f') 348 349 350def ResolveStyleableReferences(r_txt_path): 351 # Convert lines like: 352 # int[] styleable ViewBack { 0x010100d4, com.android.webview.R.attr.backTint } 353 # to: 354 # int[] styleable ViewBack { 0x010100d4, 0xREALVALUE } 355 entries = _ParseTextSymbolsFile(r_txt_path) 356 lookup_table = {(e.resource_type, e.name): e.value for e in entries} 357 358 sb = [] 359 with open(r_txt_path, encoding='utf8') as f: 360 for l in f: 361 if l.startswith('int[] styleable'): 362 brace_start = l.index('{') + 2 363 brace_end = l.index('}') - 1 364 values = [x for x in l[brace_start:brace_end].split(', ') if x] 365 new_values = [] 366 for v in values: 367 try: 368 if not v.startswith('0x'): 369 resource_type, name = v.split('.')[-2:] 370 new_values.append(lookup_table[(resource_type, name)]) 371 else: 372 new_values.append(v) 373 except: 374 logging.warning('Failed line: %r %r', l, v) 375 raise 376 l = l[:brace_start] + ', '.join(new_values) + l[brace_end:] 377 sb.append(l) 378 379 with open(r_txt_path, 'w', encoding='utf8') as f: 380 f.writelines(sb) 381 382 383def _GetRTxtResourceNames(r_txt_path): 384 """Parse an R.txt file and extract the set of resource names from it.""" 385 return {entry.name for entry in _ParseTextSymbolsFile(r_txt_path)} 386 387 388def GetRTxtStringResourceNames(r_txt_path): 389 """Parse an R.txt file and the list of its string resource names.""" 390 return sorted({ 391 entry.name 392 for entry in _ParseTextSymbolsFile(r_txt_path) 393 if entry.resource_type == 'string' 394 }) 395 396 397def GenerateStringResourcesAllowList(module_r_txt_path, allowlist_r_txt_path): 398 """Generate a allowlist of string resource IDs. 399 400 Args: 401 module_r_txt_path: Input base module R.txt path. 402 allowlist_r_txt_path: Input allowlist R.txt path. 403 Returns: 404 A dictionary mapping numerical resource IDs to the corresponding 405 string resource names. The ID values are taken from string resources in 406 |module_r_txt_path| that are also listed by name in |allowlist_r_txt_path|. 407 """ 408 allowlisted_names = { 409 entry.name 410 for entry in _ParseTextSymbolsFile(allowlist_r_txt_path) 411 if entry.resource_type == 'string' 412 } 413 return { 414 int(entry.value, 0): entry.name 415 for entry in _ParseTextSymbolsFile(module_r_txt_path) 416 if entry.resource_type == 'string' and entry.name in allowlisted_names 417 } 418 419 420class RJavaBuildOptions: 421 """A class used to model the various ways to build an R.java file. 422 423 This is used to control which resource ID variables will be final or 424 non-final, and whether an onResourcesLoaded() method will be generated 425 to adjust the non-final ones, when the corresponding library is loaded 426 at runtime. 427 428 Note that by default, all resources are final, and there is no 429 method generated, which corresponds to calling ExportNoResources(). 430 """ 431 def __init__(self): 432 self.has_constant_ids = True 433 self.resources_allowlist = None 434 self.has_on_resources_loaded = False 435 self.export_const_styleable = False 436 self.final_package_id = None 437 self.fake_on_resources_loaded = False 438 439 def ExportNoResources(self): 440 """Make all resource IDs final, and don't generate a method.""" 441 self.has_constant_ids = True 442 self.resources_allowlist = None 443 self.has_on_resources_loaded = False 444 self.export_const_styleable = False 445 446 def ExportAllResources(self): 447 """Make all resource IDs non-final in the R.java file.""" 448 self.has_constant_ids = False 449 self.resources_allowlist = None 450 451 def ExportSomeResources(self, r_txt_file_path): 452 """Only select specific resource IDs to be non-final. 453 454 Args: 455 r_txt_file_path: The path to an R.txt file. All resources named 456 int it will be non-final in the generated R.java file, all others 457 will be final. 458 """ 459 self.has_constant_ids = True 460 self.resources_allowlist = _GetRTxtResourceNames(r_txt_file_path) 461 462 def ExportAllStyleables(self): 463 """Make all styleable constants non-final, even non-resources ones. 464 465 Resources that are styleable but not of int[] type are not actually 466 resource IDs but constants. By default they are always final. Call this 467 method to make them non-final anyway in the final R.java file. 468 """ 469 self.export_const_styleable = True 470 471 def GenerateOnResourcesLoaded(self, fake=False): 472 """Generate an onResourcesLoaded() method. 473 474 This Java method will be called at runtime by the framework when 475 the corresponding library (which includes the R.java source file) 476 will be loaded at runtime. This corresponds to the --shared-resources 477 or --app-as-shared-lib flags of 'aapt package'. 478 479 if |fake|, then the method will be empty bodied to compile faster. This 480 useful for dummy R.java files that will eventually be replaced by real 481 ones. 482 """ 483 self.has_on_resources_loaded = True 484 self.fake_on_resources_loaded = fake 485 486 def SetFinalPackageId(self, package_id): 487 """Sets a package ID to be used for resources marked final.""" 488 self.final_package_id = package_id 489 490 def _MaybeRewriteRTxtPackageIds(self, r_txt_path): 491 """Rewrites package IDs in the R.txt file if necessary. 492 493 If SetFinalPackageId() was called, some of the resource IDs may have had 494 their package ID changed. This function rewrites the R.txt file to match 495 those changes. 496 """ 497 if self.final_package_id is None: 498 return 499 500 entries = _ParseTextSymbolsFile(r_txt_path) 501 with open(r_txt_path, 'w') as f: 502 for entry in entries: 503 value = entry.value 504 if self._IsResourceFinal(entry): 505 value = re.sub(r'0x(?:00|7f)', 506 '0x{:02x}'.format(self.final_package_id), value) 507 f.write('{} {} {} {}\n'.format(entry.java_type, entry.resource_type, 508 entry.name, value)) 509 510 def _IsResourceFinal(self, entry): 511 """Determines whether a resource should be final or not. 512 513 Args: 514 entry: A _TextSymbolEntry instance. 515 Returns: 516 True iff the corresponding entry should be final. 517 """ 518 if entry.resource_type == 'styleable' and entry.java_type != 'int[]': 519 # A styleable constant may be exported as non-final after all. 520 return not self.export_const_styleable 521 if not self.has_constant_ids: 522 # Every resource is non-final 523 return False 524 if not self.resources_allowlist: 525 # No allowlist means all IDs are non-final. 526 return True 527 # Otherwise, only those in the 528 return entry.name not in self.resources_allowlist 529 530 531def CreateRJavaFiles(srcjar_dir, 532 package, 533 main_r_txt_file, 534 extra_res_packages, 535 rjava_build_options, 536 srcjar_out, 537 custom_root_package_name=None, 538 grandparent_custom_package_name=None, 539 ignore_mismatched_values=False): 540 """Create all R.java files for a set of packages and R.txt files. 541 542 Args: 543 srcjar_dir: The top-level output directory for the generated files. 544 package: Package name for R java source files which will inherit 545 from the root R java file. 546 main_r_txt_file: The main R.txt file containing the valid values 547 of _all_ resource IDs. 548 extra_res_packages: A list of extra package names. 549 rjava_build_options: An RJavaBuildOptions instance that controls how 550 exactly the R.java file is generated. 551 srcjar_out: Path of desired output srcjar. 552 custom_root_package_name: Custom package name for module root R.java file, 553 (eg. vr for gen.vr package). 554 grandparent_custom_package_name: Custom root package name for the root 555 R.java file to inherit from. DFM root R.java files will have "base" 556 as the grandparent_custom_package_name. The format of this package name 557 is identical to custom_root_package_name. 558 (eg. for vr grandparent_custom_package_name would be "base") 559 ignore_mismatched_values: If True, ignores if a resource appears multiple 560 times with different entry values (useful when all the values are 561 dummy anyways). 562 Raises: 563 Exception if a package name appears several times in |extra_res_packages| 564 """ 565 rjava_build_options._MaybeRewriteRTxtPackageIds(main_r_txt_file) 566 567 packages = list(extra_res_packages) 568 569 if package and package not in packages: 570 # Sometimes, an apk target and a resources target share the same 571 # AndroidManifest.xml and thus |package| will already be in |packages|. 572 packages.append(package) 573 574 # Map of (resource_type, name) -> Entry. 575 # Contains the correct values for resources. 576 all_resources = {} 577 all_resources_by_type = collections.defaultdict(list) 578 579 main_r_text_files = [main_r_txt_file] 580 for r_txt_file in main_r_text_files: 581 for entry in _ParseTextSymbolsFile(r_txt_file, fix_package_ids=True): 582 entry_key = (entry.resource_type, entry.name) 583 if entry_key in all_resources: 584 if not ignore_mismatched_values: 585 assert entry == all_resources[entry_key], ( 586 'Input R.txt %s provided a duplicate resource with a different ' 587 'entry value. Got %s, expected %s.' % 588 (r_txt_file, entry, all_resources[entry_key])) 589 else: 590 all_resources[entry_key] = entry 591 all_resources_by_type[entry.resource_type].append(entry) 592 assert entry.resource_type in ALL_RESOURCE_TYPES, ( 593 'Unknown resource type: %s, add to ALL_RESOURCE_TYPES!' % 594 entry.resource_type) 595 596 if custom_root_package_name: 597 # Custom package name is available, thus use it for root_r_java_package. 598 root_r_java_package = GetCustomPackagePath(custom_root_package_name) 599 else: 600 # Create a unique name using srcjar_out. Underscores are added to ensure 601 # no reserved keywords are used for directory names. 602 root_r_java_package = re.sub('[^\w\.]', '', srcjar_out.replace('/', '._')) 603 604 root_r_java_dir = os.path.join(srcjar_dir, *root_r_java_package.split('.')) 605 build_utils.MakeDirectory(root_r_java_dir) 606 root_r_java_path = os.path.join(root_r_java_dir, 'R.java') 607 root_java_file_contents = _RenderRootRJavaSource( 608 root_r_java_package, all_resources_by_type, rjava_build_options, 609 grandparent_custom_package_name) 610 with open(root_r_java_path, 'w') as f: 611 f.write(root_java_file_contents) 612 613 for p in packages: 614 _CreateRJavaSourceFile(srcjar_dir, p, root_r_java_package, 615 rjava_build_options) 616 617 618def _CreateRJavaSourceFile(srcjar_dir, package, root_r_java_package, 619 rjava_build_options): 620 """Generates an R.java source file.""" 621 package_r_java_dir = os.path.join(srcjar_dir, *package.split('.')) 622 build_utils.MakeDirectory(package_r_java_dir) 623 package_r_java_path = os.path.join(package_r_java_dir, 'R.java') 624 java_file_contents = _RenderRJavaSource(package, root_r_java_package, 625 rjava_build_options) 626 with open(package_r_java_path, 'w') as f: 627 f.write(java_file_contents) 628 629 630# Resource IDs inside resource arrays are sorted. Application resource IDs start 631# with 0x7f but system resource IDs start with 0x01 thus system resource ids are 632# always at the start of the array. This function finds the index of the first 633# non system resource id to be used for package ID rewriting (we should not 634# rewrite system resource ids). 635def _GetNonSystemIndex(entry): 636 """Get the index of the first application resource ID within a resource 637 array.""" 638 res_ids = re.findall(r'0x[0-9a-f]{8}', entry.value) 639 for i, res_id in enumerate(res_ids): 640 if res_id.startswith('0x7f'): 641 return i 642 return len(res_ids) 643 644 645def _RenderRJavaSource(package, root_r_java_package, rjava_build_options): 646 """Generates the contents of a R.java file.""" 647 template = Template( 648 """/* AUTO-GENERATED FILE. DO NOT MODIFY. */ 649 650package {{ package }}; 651 652public final class R { 653 {% for resource_type in resource_types %} 654 public static final class {{ resource_type }} extends 655 {{ root_package }}.R.{{ resource_type }} {} 656 {% endfor %} 657 {% if has_on_resources_loaded %} 658 public static void onResourcesLoaded(int packageId) { 659 {{ root_package }}.R.onResourcesLoaded(packageId); 660 } 661 {% endif %} 662} 663""", 664 trim_blocks=True, 665 lstrip_blocks=True) 666 667 return template.render( 668 package=package, 669 resource_types=sorted(ALL_RESOURCE_TYPES), 670 root_package=root_r_java_package, 671 has_on_resources_loaded=rjava_build_options.has_on_resources_loaded) 672 673 674def GetCustomPackagePath(package_name): 675 return 'gen.' + package_name + '_module' 676 677 678def _RenderRootRJavaSource(package, all_resources_by_type, rjava_build_options, 679 grandparent_custom_package_name): 680 """Render an R.java source file. See _CreateRJaveSourceFile for args info.""" 681 final_resources_by_type = collections.defaultdict(list) 682 non_final_resources_by_type = collections.defaultdict(list) 683 for res_type, resources in all_resources_by_type.items(): 684 for entry in resources: 685 # Entries in stylable that are not int[] are not actually resource ids 686 # but constants. 687 if rjava_build_options._IsResourceFinal(entry): 688 final_resources_by_type[res_type].append(entry) 689 else: 690 non_final_resources_by_type[res_type].append(entry) 691 692 # Here we diverge from what aapt does. Because we have so many 693 # resources, the onResourcesLoaded method was exceeding the 64KB limit that 694 # Java imposes. For this reason we split onResourcesLoaded into different 695 # methods for each resource type. 696 extends_string = '' 697 dep_path = '' 698 if grandparent_custom_package_name: 699 extends_string = 'extends {{ parent_path }}.R.{{ resource_type }} ' 700 dep_path = GetCustomPackagePath(grandparent_custom_package_name) 701 702 # Don't actually mark fields as "final" or else R8 complain when aapt2 uses 703 # --proguard-conditional-keep-rules. E.g.: 704 # Rule precondition matches static final fields javac has inlined. 705 # Such rules are unsound as the shrinker cannot infer the inlining precisely. 706 template = Template("""/* AUTO-GENERATED FILE. DO NOT MODIFY. */ 707 708package {{ package }}; 709 710public final class R { 711 {% for resource_type in resource_types %} 712 public static class {{ resource_type }} """ + extends_string + """ { 713 {% for e in final_resources[resource_type] %} 714 public static {{ e.java_type }} {{ e.name }} = {{ e.value }}; 715 {% endfor %} 716 {% for e in non_final_resources[resource_type] %} 717 {% if e.value != '0' %} 718 public static {{ e.java_type }} {{ e.name }} = {{ e.value }}; 719 {% else %} 720 public static {{ e.java_type }} {{ e.name }}; 721 {% endif %} 722 {% endfor %} 723 } 724 {% endfor %} 725 {% if has_on_resources_loaded %} 726 {% if fake_on_resources_loaded %} 727 public static void onResourcesLoaded(int packageId) { 728 } 729 {% else %} 730 private static boolean sResourcesDidLoad; 731 732 private static void patchArray( 733 int[] arr, int startIndex, int packageIdTransform) { 734 for (int i = startIndex; i < arr.length; ++i) { 735 arr[i] ^= packageIdTransform; 736 } 737 } 738 739 public static void onResourcesLoaded(int packageId) { 740 if (sResourcesDidLoad) { 741 return; 742 } 743 sResourcesDidLoad = true; 744 int packageIdTransform = (packageId ^ 0x7f) << 24; 745 {# aapt2 makes int[] resources refer to other resources by reference 746 rather than by value. Thus, need to transform the int[] resources 747 first, before the referenced resources are transformed in order to 748 ensure the transform applies exactly once. 749 See https://crbug.com/1237059 for context. 750 #} 751 {% for resource_type in resource_types %} 752 {% for e in non_final_resources[resource_type] %} 753 {% if e.java_type == 'int[]' %} 754 patchArray({{ e.resource_type }}.{{ e.name }}, {{ startIndex(e) }}, \ 755packageIdTransform); 756 {% endif %} 757 {% endfor %} 758 {% endfor %} 759 {% for resource_type in resource_types %} 760 onResourcesLoaded{{ resource_type|title }}(packageIdTransform); 761 {% endfor %} 762 } 763 {% for res_type in resource_types %} 764 private static void onResourcesLoaded{{ res_type|title }} ( 765 int packageIdTransform) { 766 {% for e in non_final_resources[res_type] %} 767 {% if res_type != 'styleable' and e.java_type != 'int[]' %} 768 {{ e.resource_type }}.{{ e.name }} ^= packageIdTransform; 769 {% endif %} 770 {% endfor %} 771 } 772 {% endfor %} 773 {% endif %} 774 {% endif %} 775} 776""", 777 trim_blocks=True, 778 lstrip_blocks=True) 779 return template.render( 780 package=package, 781 resource_types=sorted(ALL_RESOURCE_TYPES), 782 has_on_resources_loaded=rjava_build_options.has_on_resources_loaded, 783 fake_on_resources_loaded=rjava_build_options.fake_on_resources_loaded, 784 final_resources=final_resources_by_type, 785 non_final_resources=non_final_resources_by_type, 786 startIndex=_GetNonSystemIndex, 787 parent_path=dep_path) 788 789 790def ExtractBinaryManifestValues(aapt2_path, apk_path): 791 """Returns (version_code, version_name, package_name) for the given apk.""" 792 output = subprocess.check_output([ 793 aapt2_path, 'dump', 'xmltree', apk_path, '--file', 'AndroidManifest.xml' 794 ]).decode('utf-8') 795 version_code = re.search(r'versionCode.*?=(\d*)', output).group(1) 796 version_name = re.search(r'versionName.*?="(.*?)"', output).group(1) 797 package_name = re.search(r'package.*?="(.*?)"', output).group(1) 798 return version_code, version_name, package_name 799 800 801def ExtractArscPackage(aapt2_path, apk_path): 802 """Returns (package_name, package_id) of resources.arsc from apk_path. 803 804 When the apk does not have any entries in its resources file, in recent aapt2 805 versions it will not contain a "Package" line. The package is not even in the 806 actual resources.arsc/resources.pb file (which itself is mostly empty). Thus 807 return (None, None) when dump succeeds and there are no errors to indicate 808 that the package name does not exist in the resources file. 809 """ 810 proc = subprocess.Popen([aapt2_path, 'dump', 'resources', apk_path], 811 stdout=subprocess.PIPE, 812 stderr=subprocess.PIPE) 813 for line in proc.stdout: 814 line = line.decode('utf-8') 815 # Package name=org.chromium.webview_shell id=7f 816 if line.startswith('Package'): 817 proc.kill() 818 parts = line.split() 819 package_name = parts[1].split('=')[1] 820 package_id = parts[2][3:] 821 return package_name, int(package_id, 16) 822 823 # aapt2 currently crashes when dumping webview resources, but not until after 824 # it prints the "Package" line (b/130553900). 825 stderr_output = proc.stderr.read().decode('utf-8') 826 if stderr_output: 827 sys.stderr.write(stderr_output) 828 raise Exception('Failed to find arsc package name') 829 return None, None 830 831 832def _RenameSubdirsWithPrefix(dir_path, prefix): 833 subdirs = [ 834 d for d in os.listdir(dir_path) 835 if os.path.isdir(os.path.join(dir_path, d)) 836 ] 837 renamed_subdirs = [] 838 for d in subdirs: 839 old_path = os.path.join(dir_path, d) 840 new_path = os.path.join(dir_path, '{}_{}'.format(prefix, d)) 841 renamed_subdirs.append(new_path) 842 os.rename(old_path, new_path) 843 return renamed_subdirs 844 845 846def _HasMultipleResDirs(zip_path): 847 """Checks for magic comment set by prepare_resources.py 848 849 Returns: True iff the zipfile has the magic comment that means it contains 850 multiple res/ dirs inside instead of just contents of a single res/ dir 851 (without a wrapping res/). 852 """ 853 with zipfile.ZipFile(zip_path) as z: 854 return z.comment == MULTIPLE_RES_MAGIC_STRING 855 856 857def ExtractDeps(dep_zips, deps_dir): 858 """Extract a list of resource dependency zip files. 859 860 Args: 861 dep_zips: A list of zip file paths, each one will be extracted to 862 a subdirectory of |deps_dir|, named after the zip file's path (e.g. 863 '/some/path/foo.zip' -> '{deps_dir}/some_path_foo/'). 864 deps_dir: Top-level extraction directory. 865 Returns: 866 The list of all sub-directory paths, relative to |deps_dir|. 867 Raises: 868 Exception: If a sub-directory already exists with the same name before 869 extraction. 870 """ 871 dep_subdirs = [] 872 for z in dep_zips: 873 subdirname = z.replace(os.path.sep, '_') 874 subdir = os.path.join(deps_dir, subdirname) 875 if os.path.exists(subdir): 876 raise Exception('Resource zip name conflict: ' + subdirname) 877 build_utils.ExtractAll(z, path=subdir) 878 if _HasMultipleResDirs(z): 879 # basename of the directory is used to create a zip during resource 880 # compilation, include the path in the basename to help blame errors on 881 # the correct target. For example directory 0_res may be renamed 882 # chrome_android_chrome_app_java_resources_0_res pointing to the name and 883 # path of the android_resources target from whence it came. 884 subdir_subdirs = _RenameSubdirsWithPrefix(subdir, subdirname) 885 dep_subdirs.extend(subdir_subdirs) 886 else: 887 dep_subdirs.append(subdir) 888 return dep_subdirs 889 890 891class _ResourceBuildContext: 892 """A temporary directory for packaging and compiling Android resources. 893 894 Args: 895 temp_dir: Optional root build directory path. If None, a temporary 896 directory will be created, and removed in Close(). 897 """ 898 899 def __init__(self, temp_dir=None, keep_files=False): 900 """Initialized the context.""" 901 # The top-level temporary directory. 902 if temp_dir: 903 self.temp_dir = temp_dir 904 os.makedirs(temp_dir) 905 else: 906 self.temp_dir = tempfile.mkdtemp() 907 self.remove_on_exit = not keep_files 908 909 # A location to store resources extracted form dependency zip files. 910 self.deps_dir = os.path.join(self.temp_dir, 'deps') 911 os.mkdir(self.deps_dir) 912 # A location to place aapt-generated files. 913 self.gen_dir = os.path.join(self.temp_dir, 'gen') 914 os.mkdir(self.gen_dir) 915 # A location to place generated R.java files. 916 self.srcjar_dir = os.path.join(self.temp_dir, 'java') 917 os.mkdir(self.srcjar_dir) 918 # Temporary file locacations. 919 self.r_txt_path = os.path.join(self.gen_dir, 'R.txt') 920 self.srcjar_path = os.path.join(self.temp_dir, 'R.srcjar') 921 self.info_path = os.path.join(self.temp_dir, 'size.info') 922 self.stable_ids_path = os.path.join(self.temp_dir, 'in_ids.txt') 923 self.emit_ids_path = os.path.join(self.temp_dir, 'out_ids.txt') 924 self.proguard_path = os.path.join(self.temp_dir, 'keeps.flags') 925 self.proguard_main_dex_path = os.path.join(self.temp_dir, 'maindex.flags') 926 self.arsc_path = os.path.join(self.temp_dir, 'out.ap_') 927 self.proto_path = os.path.join(self.temp_dir, 'out.proto.ap_') 928 self.optimized_arsc_path = os.path.join(self.temp_dir, 'out.opt.ap_') 929 self.optimized_proto_path = os.path.join(self.temp_dir, 'out.opt.proto.ap_') 930 931 def Close(self): 932 """Close the context and destroy all temporary files.""" 933 if self.remove_on_exit: 934 shutil.rmtree(self.temp_dir) 935 936 937@contextlib.contextmanager 938def BuildContext(temp_dir=None, keep_files=False): 939 """Generator for a _ResourceBuildContext instance.""" 940 context = None 941 try: 942 context = _ResourceBuildContext(temp_dir, keep_files) 943 yield context 944 finally: 945 if context: 946 context.Close() 947 948 949def ParseAndroidResourceStringsFromXml(xml_data): 950 """Parse and Android xml resource file and extract strings from it. 951 952 Args: 953 xml_data: XML file data. 954 Returns: 955 A (dict, namespaces) tuple, where |dict| maps string names to their UTF-8 956 encoded value, and |namespaces| is a dictionary mapping prefixes to URLs 957 corresponding to namespaces declared in the <resources> element. 958 """ 959 # NOTE: This uses regular expression matching because parsing with something 960 # like ElementTree makes it tedious to properly parse some of the structured 961 # text found in string resources, e.g.: 962 # <string msgid="3300176832234831527" \ 963 # name="abc_shareactionprovider_share_with_application">\ 964 # "Condividi tramite <ns1:g id="APPLICATION_NAME">%s</ns1:g>"\ 965 # </string> 966 result = {} 967 968 # Find <resources> start tag and extract namespaces from it. 969 m = re.search('<resources([^>]*)>', xml_data, re.MULTILINE) 970 if not m: 971 raise Exception('<resources> start tag expected: ' + xml_data) 972 input_data = xml_data[m.end():] 973 resource_attrs = m.group(1) 974 re_namespace = re.compile('\s*(xmlns:(\w+)="([^"]+)")') 975 namespaces = {} 976 while resource_attrs: 977 m = re_namespace.match(resource_attrs) 978 if not m: 979 break 980 namespaces[m.group(2)] = m.group(3) 981 resource_attrs = resource_attrs[m.end(1):] 982 983 # Find each string element now. 984 re_string_element_start = re.compile('<string ([^>]* )?name="([^">]+)"[^>]*>') 985 re_string_element_end = re.compile('</string>') 986 while input_data: 987 m = re_string_element_start.search(input_data) 988 if not m: 989 break 990 name = m.group(2) 991 input_data = input_data[m.end():] 992 m2 = re_string_element_end.search(input_data) 993 if not m2: 994 raise Exception('Expected closing string tag: ' + input_data) 995 text = input_data[:m2.start()] 996 input_data = input_data[m2.end():] 997 if len(text) != 0 and text[0] == '"' and text[-1] == '"': 998 text = text[1:-1] 999 result[name] = text 1000 1001 return result, namespaces 1002 1003 1004def GenerateAndroidResourceStringsXml(names_to_utf8_text, namespaces=None): 1005 """Generate an XML text corresponding to an Android resource strings map. 1006 1007 Args: 1008 names_to_text: A dictionary mapping resource names to localized 1009 text (encoded as UTF-8). 1010 namespaces: A map of namespace prefix to URL. 1011 Returns: 1012 New non-Unicode string containing an XML data structure describing the 1013 input as an Android resource .xml file. 1014 """ 1015 result = '<?xml version="1.0" encoding="utf-8"?>\n' 1016 result += '<resources' 1017 if namespaces: 1018 for prefix, url in sorted(namespaces.items()): 1019 result += ' xmlns:%s="%s"' % (prefix, url) 1020 result += '>\n' 1021 if not names_to_utf8_text: 1022 result += '<!-- this file intentionally empty -->\n' 1023 else: 1024 for name, utf8_text in sorted(names_to_utf8_text.items()): 1025 result += '<string name="%s">"%s"</string>\n' % (name, utf8_text) 1026 result += '</resources>\n' 1027 return result.encode('utf8') 1028 1029 1030def FilterAndroidResourceStringsXml(xml_file_path, string_predicate): 1031 """Remove unwanted localized strings from an Android resource .xml file. 1032 1033 This function takes a |string_predicate| callable object that will 1034 receive a resource string name, and should return True iff the 1035 corresponding <string> element should be kept in the file. 1036 1037 Args: 1038 xml_file_path: Android resource strings xml file path. 1039 string_predicate: A predicate function which will receive the string name 1040 and shal 1041 """ 1042 with open(xml_file_path) as f: 1043 xml_data = f.read() 1044 strings_map, namespaces = ParseAndroidResourceStringsFromXml(xml_data) 1045 1046 string_deletion = False 1047 for name in list(strings_map.keys()): 1048 if not string_predicate(name): 1049 del strings_map[name] 1050 string_deletion = True 1051 1052 if string_deletion: 1053 new_xml_data = GenerateAndroidResourceStringsXml(strings_map, namespaces) 1054 with open(xml_file_path, 'wb') as f: 1055 f.write(new_xml_data) 1056