xref: /aosp_15_r20/external/cronet/build/android/gyp/util/resource_utils.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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