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