• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3# Copyright 2012 The Chromium Authors
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Compile Android resources into an intermediate APK.
8
9This can also generate an R.txt, and an .srcjar file containing the proper
10final R.java class for all resource packages the APK depends on.
11
12This will crunch images with aapt2.
13"""
14
15import argparse
16import collections
17import contextlib
18import filecmp
19import hashlib
20import logging
21import os
22import pathlib
23import re
24import shutil
25import subprocess
26import sys
27import textwrap
28from xml.etree import ElementTree
29
30from util import build_utils
31from util import diff_utils
32from util import manifest_utils
33from util import parallel
34from util import protoresources
35from util import resource_utils
36import action_helpers  # build_utils adds //build to sys.path.
37import zip_helpers
38
39
40# Pngs that we shouldn't convert to webp. Please add rationale when updating.
41_PNG_WEBP_EXCLUSION_PATTERN = re.compile('|'.join([
42    # Android requires pngs for 9-patch images.
43    r'.*\.9\.png',
44    # Daydream requires pngs for icon files.
45    r'.*daydream_icon_.*\.png'
46]))
47
48
49def _ParseArgs(args):
50  """Parses command line options.
51
52  Returns:
53    An options object as from argparse.ArgumentParser.parse_args()
54  """
55  parser = argparse.ArgumentParser(description=__doc__)
56
57  input_opts = parser.add_argument_group('Input options')
58  output_opts = parser.add_argument_group('Output options')
59
60  input_opts.add_argument('--include-resources',
61                          action='append',
62                          required=True,
63                          help='Paths to arsc resource files used to link '
64                          'against. Can be specified multiple times.')
65  input_opts.add_argument(
66      '--dependencies-res-zips',
67      default=[],
68      help='Resources zip archives from dependents. Required to '
69      'resolve @type/foo references into dependent libraries.')
70  input_opts.add_argument(
71      '--extra-res-packages',
72      help='Additional package names to generate R.java files for.')
73  input_opts.add_argument(
74      '--aapt2-path', required=True, help='Path to the Android aapt2 tool.')
75  input_opts.add_argument(
76      '--android-manifest', required=True, help='AndroidManifest.xml path.')
77  input_opts.add_argument(
78      '--r-java-root-package-name',
79      default='base',
80      help='Short package name for this target\'s root R java file (ex. '
81      'input of "base" would become gen.base_module). Defaults to "base".')
82  group = input_opts.add_mutually_exclusive_group()
83  group.add_argument(
84      '--shared-resources',
85      action='store_true',
86      help='Make all resources in R.java non-final and allow the resource IDs '
87      'to be reset to a different package index when the apk is loaded by '
88      'another application at runtime.')
89  group.add_argument(
90      '--app-as-shared-lib',
91      action='store_true',
92      help='Same as --shared-resources, but also ensures all resource IDs are '
93      'directly usable from the APK loaded as an application.')
94  input_opts.add_argument(
95      '--package-id',
96      type=int,
97      help='Decimal integer representing custom package ID for resources '
98      '(instead of 127==0x7f). Cannot be used with --shared-resources.')
99  input_opts.add_argument(
100      '--package-name',
101      help='Package name that will be used to create R class.')
102  input_opts.add_argument(
103      '--rename-manifest-package', help='Package name to force AAPT to use.')
104  input_opts.add_argument(
105      '--arsc-package-name',
106      help='Package name to set in manifest of resources.arsc file. This is '
107      'only used for apks under test.')
108  input_opts.add_argument(
109      '--shared-resources-allowlist',
110      help='An R.txt file acting as a allowlist for resources that should be '
111      'non-final and have their package ID changed at runtime in R.java. '
112      'Implies and overrides --shared-resources.')
113  input_opts.add_argument(
114      '--shared-resources-allowlist-locales',
115      default='[]',
116      help='Optional GN-list of locales. If provided, all strings corresponding'
117      ' to this locale list will be kept in the final output for the '
118      'resources identified through --shared-resources-allowlist, even '
119      'if --locale-allowlist is being used.')
120  input_opts.add_argument(
121      '--use-resource-ids-path',
122      help='Use resource IDs generated by aapt --emit-ids.')
123  input_opts.add_argument(
124      '--debuggable',
125      action='store_true',
126      help='Whether to add android:debuggable="true".')
127  input_opts.add_argument('--static-library-version',
128                          help='Version code for static library.')
129  input_opts.add_argument('--version-code', help='Version code for apk.')
130  input_opts.add_argument('--version-name', help='Version name for apk.')
131  input_opts.add_argument(
132      '--min-sdk-version', required=True, help='android:minSdkVersion for APK.')
133  input_opts.add_argument(
134      '--target-sdk-version',
135      required=True,
136      help="android:targetSdkVersion for APK.")
137  input_opts.add_argument(
138      '--max-sdk-version',
139      help="android:maxSdkVersion expected in AndroidManifest.xml.")
140  input_opts.add_argument(
141      '--manifest-package', help='Package name of the AndroidManifest.xml.')
142  input_opts.add_argument(
143      '--locale-allowlist',
144      default='[]',
145      help='GN list of languages to include. All other language configs will '
146      'be stripped out. List may include a combination of Android locales '
147      'or Chrome locales.')
148  input_opts.add_argument(
149      '--resource-exclusion-regex',
150      default='',
151      help='File-based filter for resources (applied before compiling)')
152  input_opts.add_argument(
153      '--resource-exclusion-exceptions',
154      default='[]',
155      help='GN list of globs that say which files to include even '
156      'when --resource-exclusion-regex is set.')
157  input_opts.add_argument(
158      '--dependencies-res-zip-overlays',
159      help='GN list with subset of --dependencies-res-zips to use overlay '
160      'semantics for.')
161  input_opts.add_argument(
162      '--values-filter-rules',
163      help='GN list of source_glob:regex for filtering resources after they '
164      'are compiled. Use this to filter out entries within values/ files.')
165  input_opts.add_argument('--png-to-webp', action='store_true',
166                          help='Convert png files to webp format.')
167
168  input_opts.add_argument('--webp-binary', default='',
169                          help='Path to the cwebp binary.')
170  input_opts.add_argument(
171      '--webp-cache-dir', help='The directory to store webp image cache.')
172  input_opts.add_argument(
173      '--is-bundle-module',
174      action='store_true',
175      help='Whether resources are being generated for a bundle module.')
176  input_opts.add_argument(
177      '--uses-split',
178      help='Value to set uses-split to in the AndroidManifest.xml.')
179  input_opts.add_argument(
180      '--verification-version-code-offset',
181      help='Subtract this from versionCode for expectation files')
182  input_opts.add_argument(
183      '--verification-library-version-offset',
184      help='Subtract this from static-library version for expectation files')
185
186  action_helpers.add_depfile_arg(output_opts)
187  output_opts.add_argument('--arsc-path', help='Apk output for arsc format.')
188  output_opts.add_argument('--proto-path', help='Apk output for proto format.')
189  output_opts.add_argument(
190      '--info-path', help='Path to output info file for the partial apk.')
191  output_opts.add_argument(
192      '--srcjar-out',
193      help='Path to srcjar to contain generated R.java.')
194  output_opts.add_argument('--r-text-out',
195                           help='Path to store the generated R.txt file.')
196  output_opts.add_argument(
197      '--proguard-file', help='Path to proguard.txt generated file.')
198  output_opts.add_argument(
199      '--emit-ids-out', help='Path to file produced by aapt2 --emit-ids.')
200
201  diff_utils.AddCommandLineFlags(parser)
202  options = parser.parse_args(args)
203
204  options.include_resources = action_helpers.parse_gn_list(
205      options.include_resources)
206  options.dependencies_res_zips = action_helpers.parse_gn_list(
207      options.dependencies_res_zips)
208  options.extra_res_packages = action_helpers.parse_gn_list(
209      options.extra_res_packages)
210  options.locale_allowlist = action_helpers.parse_gn_list(
211      options.locale_allowlist)
212  options.shared_resources_allowlist_locales = action_helpers.parse_gn_list(
213      options.shared_resources_allowlist_locales)
214  options.resource_exclusion_exceptions = action_helpers.parse_gn_list(
215      options.resource_exclusion_exceptions)
216  options.dependencies_res_zip_overlays = action_helpers.parse_gn_list(
217      options.dependencies_res_zip_overlays)
218  options.values_filter_rules = action_helpers.parse_gn_list(
219      options.values_filter_rules)
220
221  if not options.arsc_path and not options.proto_path:
222    parser.error('One of --arsc-path or --proto-path is required.')
223
224  if options.package_id and options.shared_resources:
225    parser.error('--package-id and --shared-resources are mutually exclusive')
226
227  if options.static_library_version and (options.static_library_version !=
228                                         options.version_code):
229    assert options.static_library_version == options.version_code, (
230        f'static_library_version={options.static_library_version} must equal '
231        f'version_code={options.version_code}. Please verify the version code '
232        'map for this target is defined correctly.')
233
234  return options
235
236
237def _IterFiles(root_dir):
238  for root, _, files in os.walk(root_dir):
239    for f in files:
240      yield os.path.join(root, f)
241
242
243def _RenameLocaleResourceDirs(resource_dirs, path_info):
244  """Rename locale resource directories into standard names when necessary.
245
246  This is necessary to deal with the fact that older Android releases only
247  support ISO 639-1 two-letter codes, and sometimes even obsolete versions
248  of them.
249
250  In practice it means:
251    * 3-letter ISO 639-2 qualifiers are renamed under a corresponding
252      2-letter one. E.g. for Filipino, strings under values-fil/ will be moved
253      to a new corresponding values-tl/ sub-directory.
254
255    * Modern ISO 639-1 codes will be renamed to their obsolete variant
256      for Indonesian, Hebrew and Yiddish (e.g. 'values-in/ -> values-id/).
257
258    * Norwegian macrolanguage strings will be renamed to Bokmal (main
259      Norway language). See http://crbug.com/920960. In practice this
260      means that 'values-no/ -> values-nb/' unless 'values-nb/' already
261      exists.
262
263    * BCP 47 langauge tags will be renamed to an equivalent ISO 639-1
264      locale qualifier if possible (e.g. 'values-b+en+US/ -> values-en-rUS').
265
266  Args:
267    resource_dirs: list of top-level resource directories.
268  """
269  for resource_dir in resource_dirs:
270    ignore_dirs = {}
271    for path in _IterFiles(resource_dir):
272      locale = resource_utils.FindLocaleInStringResourceFilePath(path)
273      if not locale:
274        continue
275      cr_locale = resource_utils.ToChromiumLocaleName(locale)
276      if not cr_locale:
277        continue  # Unsupported Android locale qualifier!?
278      locale2 = resource_utils.ToAndroidLocaleName(cr_locale)
279      if locale != locale2:
280        path2 = path.replace('/values-%s/' % locale, '/values-%s/' % locale2)
281        if path == path2:
282          raise Exception('Could not substitute locale %s for %s in %s' %
283                          (locale, locale2, path))
284
285        # Ignore rather than rename when the destination resources config
286        # already exists.
287        # e.g. some libraries provide both values-nb/ and values-no/.
288        # e.g. material design provides:
289        # * res/values-rUS/values-rUS.xml
290        # * res/values-b+es+419/values-b+es+419.xml
291        config_dir = os.path.dirname(path2)
292        already_has_renamed_config = ignore_dirs.get(config_dir)
293        if already_has_renamed_config is None:
294          # Cache the result of the first time the directory is encountered
295          # since subsequent encounters will find the directory already exists
296          # (due to the rename).
297          already_has_renamed_config = os.path.exists(config_dir)
298          ignore_dirs[config_dir] = already_has_renamed_config
299        if already_has_renamed_config:
300          continue
301
302        build_utils.MakeDirectory(os.path.dirname(path2))
303        shutil.move(path, path2)
304        path_info.RegisterRename(
305            os.path.relpath(path, resource_dir),
306            os.path.relpath(path2, resource_dir))
307
308
309def _ToAndroidLocales(locale_allowlist):
310  """Converts the list of Chrome locales to Android config locale qualifiers.
311
312  Args:
313    locale_allowlist: A list of Chromium locale names.
314  Returns:
315    A set of matching Android config locale qualifier names.
316  """
317  ret = set()
318  for locale in locale_allowlist:
319    locale = resource_utils.ToAndroidLocaleName(locale)
320    if locale is None or ('-' in locale and '-r' not in locale):
321      raise Exception('Unsupported Chromium locale name: %s' % locale)
322    ret.add(locale)
323    # Always keep non-regional fall-backs.
324    language = locale.split('-')[0]
325    ret.add(language)
326
327  return ret
328
329
330def _MoveImagesToNonMdpiFolders(res_root, path_info):
331  """Move images from drawable-*-mdpi-* folders to drawable-* folders.
332
333  Why? http://crbug.com/289843
334  """
335  for src_dir_name in os.listdir(res_root):
336    src_components = src_dir_name.split('-')
337    if src_components[0] != 'drawable' or 'mdpi' not in src_components:
338      continue
339    src_dir = os.path.join(res_root, src_dir_name)
340    if not os.path.isdir(src_dir):
341      continue
342    dst_components = [c for c in src_components if c != 'mdpi']
343    assert dst_components != src_components
344    dst_dir_name = '-'.join(dst_components)
345    dst_dir = os.path.join(res_root, dst_dir_name)
346    build_utils.MakeDirectory(dst_dir)
347    for src_file_name in os.listdir(src_dir):
348      src_file = os.path.join(src_dir, src_file_name)
349      dst_file = os.path.join(dst_dir, src_file_name)
350      assert not os.path.lexists(dst_file)
351      shutil.move(src_file, dst_file)
352      path_info.RegisterRename(
353          os.path.relpath(src_file, res_root),
354          os.path.relpath(dst_file, res_root))
355
356
357def _DeterminePlatformVersion(aapt2_path, jar_candidates):
358  def maybe_extract_version(j):
359    try:
360      return resource_utils.ExtractBinaryManifestValues(aapt2_path, j)
361    except build_utils.CalledProcessError:
362      return None
363
364  def is_sdk_jar(jar_name):
365    if jar_name in ('android.jar', 'android_system.jar'):
366      return True
367    # Robolectric jar looks a bit different.
368    return 'android-all' in jar_name and 'robolectric' in jar_name
369
370  android_sdk_jars = [
371      j for j in jar_candidates if is_sdk_jar(os.path.basename(j))
372  ]
373  extract_all = [maybe_extract_version(j) for j in android_sdk_jars]
374  extract_all = [x for x in extract_all if x]
375  if len(extract_all) == 0:
376    raise Exception(
377        'Unable to find android SDK jar among candidates: %s'
378            % ', '.join(android_sdk_jars))
379  if len(extract_all) > 1:
380    raise Exception(
381        'Found multiple android SDK jars among candidates: %s'
382            % ', '.join(android_sdk_jars))
383  platform_version_code, platform_version_name = extract_all.pop()[:2]
384  return platform_version_code, platform_version_name
385
386
387def _FixManifest(options, temp_dir):
388  """Fix the APK's AndroidManifest.xml.
389
390  This adds any missing namespaces for 'android' and 'tools', and
391  sets certains elements like 'platformBuildVersionCode' or
392  'android:debuggable' depending on the content of |options|.
393
394  Args:
395    options: The command-line arguments tuple.
396    temp_dir: A temporary directory where the fixed manifest will be written to.
397  Returns:
398    Tuple of:
399     * Manifest path within |temp_dir|.
400     * Original package_name.
401     * Manifest package name.
402  """
403  doc, manifest_node, app_node = manifest_utils.ParseManifest(
404      options.android_manifest)
405
406  # merge_manifest.py also sets package & <uses-sdk>. We may want to ensure
407  # manifest merger is always enabled and remove these command-line arguments.
408  manifest_utils.SetUsesSdk(manifest_node, options.target_sdk_version,
409                            options.min_sdk_version, options.max_sdk_version)
410  orig_package = manifest_node.get('package') or options.manifest_package
411  fixed_package = (options.arsc_package_name or options.manifest_package
412                   or orig_package)
413  manifest_node.set('package', fixed_package)
414
415  platform_version_code, platform_version_name = _DeterminePlatformVersion(
416      options.aapt2_path, options.include_resources)
417  manifest_node.set('platformBuildVersionCode', platform_version_code)
418  manifest_node.set('platformBuildVersionName', platform_version_name)
419  if options.version_code:
420    manifest_utils.NamespacedSet(manifest_node, 'versionCode',
421                                 options.version_code)
422  if options.version_name:
423    manifest_utils.NamespacedSet(manifest_node, 'versionName',
424                                 options.version_name)
425  if options.debuggable:
426    manifest_utils.NamespacedSet(app_node, 'debuggable', 'true')
427
428  if options.uses_split:
429    uses_split = ElementTree.SubElement(manifest_node, 'uses-split')
430    manifest_utils.NamespacedSet(uses_split, 'name', options.uses_split)
431
432  # Make sure the min-sdk condition is not less than the min-sdk of the bundle.
433  for min_sdk_node in manifest_node.iter('{%s}min-sdk' %
434                                         manifest_utils.DIST_NAMESPACE):
435    dist_value = '{%s}value' % manifest_utils.DIST_NAMESPACE
436    if int(min_sdk_node.get(dist_value)) < int(options.min_sdk_version):
437      min_sdk_node.set(dist_value, options.min_sdk_version)
438
439  debug_manifest_path = os.path.join(temp_dir, 'AndroidManifest.xml')
440  manifest_utils.SaveManifest(doc, debug_manifest_path)
441  return debug_manifest_path, orig_package, fixed_package
442
443
444def _CreateKeepPredicate(resource_exclusion_regex,
445                         resource_exclusion_exceptions):
446  """Return a predicate lambda to determine which resource files to keep.
447
448  Args:
449    resource_exclusion_regex: A regular expression describing all resources
450      to exclude, except if they are mip-maps, or if they are listed
451      in |resource_exclusion_exceptions|.
452    resource_exclusion_exceptions: A list of glob patterns corresponding
453      to exceptions to the |resource_exclusion_regex|.
454  Returns:
455    A lambda that takes a path, and returns true if the corresponding file
456    must be kept.
457  """
458  predicate = lambda path: os.path.basename(path)[0] != '.'
459  if resource_exclusion_regex == '':
460    # Do not extract dotfiles (e.g. ".gitkeep"). aapt ignores them anyways.
461    return predicate
462
463  # A simple predicate that only removes (returns False for) paths covered by
464  # the exclusion regex or listed as exceptions.
465  return lambda path: (
466      not re.search(resource_exclusion_regex, path) or
467      build_utils.MatchesGlob(path, resource_exclusion_exceptions))
468
469
470def _ComputeSha1(path):
471  with open(path, 'rb') as f:
472    data = f.read()
473  return hashlib.sha1(data).hexdigest()
474
475
476def _ConvertToWebPSingle(png_path, cwebp_binary, cwebp_version, webp_cache_dir):
477  sha1_hash = _ComputeSha1(png_path)
478
479  # The set of arguments that will appear in the cache key.
480  quality_args = ['-m', '6', '-q', '100', '-lossless']
481
482  webp_cache_path = os.path.join(
483      webp_cache_dir, '{}-{}-{}'.format(sha1_hash, cwebp_version,
484                                        ''.join(quality_args)))
485  # No need to add .webp. Android can load images fine without them.
486  webp_path = os.path.splitext(png_path)[0]
487
488  cache_hit = os.path.exists(webp_cache_path)
489  if cache_hit:
490    os.link(webp_cache_path, webp_path)
491  else:
492    # We place the generated webp image to webp_path, instead of in the
493    # webp_cache_dir to avoid concurrency issues.
494    args = [cwebp_binary, png_path, '-o', webp_path, '-quiet'] + quality_args
495    subprocess.check_call(args)
496
497    try:
498      os.link(webp_path, webp_cache_path)
499    except OSError:
500      # Because of concurrent run, a webp image may already exists in
501      # webp_cache_path.
502      pass
503
504  os.remove(png_path)
505  original_dir = os.path.dirname(os.path.dirname(png_path))
506  rename_tuple = (os.path.relpath(png_path, original_dir),
507                  os.path.relpath(webp_path, original_dir))
508  return rename_tuple, cache_hit
509
510
511def _ConvertToWebP(cwebp_binary, png_paths, path_info, webp_cache_dir):
512  cwebp_version = subprocess.check_output([cwebp_binary, '-version']).rstrip()
513  shard_args = [(f, ) for f in png_paths
514                if not _PNG_WEBP_EXCLUSION_PATTERN.match(f)]
515
516  build_utils.MakeDirectory(webp_cache_dir)
517  results = parallel.BulkForkAndCall(_ConvertToWebPSingle,
518                                     shard_args,
519                                     cwebp_binary=cwebp_binary,
520                                     cwebp_version=cwebp_version,
521                                     webp_cache_dir=webp_cache_dir)
522  total_cache_hits = 0
523  for rename_tuple, cache_hit in results:
524    path_info.RegisterRename(*rename_tuple)
525    total_cache_hits += int(cache_hit)
526
527  logging.debug('png->webp cache: %d/%d', total_cache_hits, len(shard_args))
528
529
530def _RemoveImageExtensions(directory, path_info):
531  """Remove extensions from image files in the passed directory.
532
533  This reduces binary size but does not affect android's ability to load the
534  images.
535  """
536  for f in _IterFiles(directory):
537    if (f.endswith('.png') or f.endswith('.webp')) and not f.endswith('.9.png'):
538      path_with_extension = f
539      path_no_extension = os.path.splitext(path_with_extension)[0]
540      if path_no_extension != path_with_extension:
541        shutil.move(path_with_extension, path_no_extension)
542        path_info.RegisterRename(
543            os.path.relpath(path_with_extension, directory),
544            os.path.relpath(path_no_extension, directory))
545
546
547def _CompileSingleDep(index, dep_subdir, keep_predicate, aapt2_path,
548                      partials_dir):
549  unique_name = '{}_{}'.format(index, os.path.basename(dep_subdir))
550  partial_path = os.path.join(partials_dir, '{}.zip'.format(unique_name))
551
552  compile_command = [
553      aapt2_path,
554      'compile',
555      # TODO(wnwen): Turn this on once aapt2 forces 9-patch to be crunched.
556      # '--no-crunch',
557      '--dir',
558      dep_subdir,
559      '-o',
560      partial_path
561  ]
562
563  # There are resources targeting API-versions lower than our minapi. For
564  # various reasons it's easier to let aapt2 ignore these than for us to
565  # remove them from our build (e.g. it's from a 3rd party library).
566  build_utils.CheckOutput(
567      compile_command,
568      stderr_filter=lambda output: build_utils.FilterLines(
569          output, r'ignoring configuration .* for (styleable|attribute)'))
570
571  # Filtering these files is expensive, so only apply filters to the partials
572  # that have been explicitly targeted.
573  if keep_predicate:
574    logging.debug('Applying .arsc filtering to %s', dep_subdir)
575    protoresources.StripUnwantedResources(partial_path, keep_predicate)
576  return partial_path
577
578
579def _CreateValuesKeepPredicate(exclusion_rules, dep_subdir):
580  patterns = [
581      x[1] for x in exclusion_rules
582      if build_utils.MatchesGlob(dep_subdir, [x[0]])
583  ]
584  if not patterns:
585    return None
586
587  regexes = [re.compile(p) for p in patterns]
588  return lambda x: not any(r.search(x) for r in regexes)
589
590
591def _CompileDeps(aapt2_path, dep_subdirs, dep_subdir_overlay_set, temp_dir,
592                 exclusion_rules):
593  partials_dir = os.path.join(temp_dir, 'partials')
594  build_utils.MakeDirectory(partials_dir)
595
596  job_params = [(i, dep_subdir,
597                 _CreateValuesKeepPredicate(exclusion_rules, dep_subdir))
598                for i, dep_subdir in enumerate(dep_subdirs)]
599
600  # Filtering is slow, so ensure jobs with keep_predicate are started first.
601  job_params.sort(key=lambda x: not x[2])
602  partials = list(
603      parallel.BulkForkAndCall(_CompileSingleDep,
604                               job_params,
605                               aapt2_path=aapt2_path,
606                               partials_dir=partials_dir))
607
608  partials_cmd = list()
609  for i, partial in enumerate(partials):
610    dep_subdir = job_params[i][1]
611    if dep_subdir in dep_subdir_overlay_set:
612      partials_cmd += ['-R']
613    partials_cmd += [partial]
614  return partials_cmd
615
616
617def _CreateResourceInfoFile(path_info, info_path, dependencies_res_zips):
618  for zip_file in dependencies_res_zips:
619    zip_info_file_path = zip_file + '.info'
620    if os.path.exists(zip_info_file_path):
621      path_info.MergeInfoFile(zip_info_file_path)
622  path_info.Write(info_path)
623
624
625def _RemoveUnwantedLocalizedStrings(dep_subdirs, options):
626  """Remove localized strings that should not go into the final output.
627
628  Args:
629    dep_subdirs: List of resource dependency directories.
630    options: Command-line options namespace.
631  """
632  # Collect locale and file paths from the existing subdirs.
633  # The following variable maps Android locale names to
634  # sets of corresponding xml file paths.
635  locale_to_files_map = collections.defaultdict(set)
636  for directory in dep_subdirs:
637    for f in _IterFiles(directory):
638      locale = resource_utils.FindLocaleInStringResourceFilePath(f)
639      if locale:
640        locale_to_files_map[locale].add(f)
641
642  all_locales = set(locale_to_files_map)
643
644  # Set A: wanted locales, either all of them or the
645  # list provided by --locale-allowlist.
646  wanted_locales = all_locales
647  if options.locale_allowlist:
648    wanted_locales = _ToAndroidLocales(options.locale_allowlist)
649
650  # Set B: shared resources locales, which is either set A
651  # or the list provided by --shared-resources-allowlist-locales
652  shared_resources_locales = wanted_locales
653  shared_names_allowlist = set()
654  if options.shared_resources_allowlist_locales:
655    shared_names_allowlist = set(
656        resource_utils.GetRTxtStringResourceNames(
657            options.shared_resources_allowlist))
658
659    shared_resources_locales = _ToAndroidLocales(
660        options.shared_resources_allowlist_locales)
661
662  # Remove any file that belongs to a locale not covered by
663  # either A or B.
664  removable_locales = (all_locales - wanted_locales - shared_resources_locales)
665  for locale in removable_locales:
666    for path in locale_to_files_map[locale]:
667      os.remove(path)
668
669  # For any locale in B but not in A, only keep the shared
670  # resource strings in each file.
671  for locale in shared_resources_locales - wanted_locales:
672    for path in locale_to_files_map[locale]:
673      resource_utils.FilterAndroidResourceStringsXml(
674          path, lambda x: x in shared_names_allowlist)
675
676  # For any locale in A but not in B, only keep the strings
677  # that are _not_ from shared resources in the file.
678  for locale in wanted_locales - shared_resources_locales:
679    for path in locale_to_files_map[locale]:
680      resource_utils.FilterAndroidResourceStringsXml(
681          path, lambda x: x not in shared_names_allowlist)
682
683
684def _FilterResourceFiles(dep_subdirs, keep_predicate):
685  # Create a function that selects which resource files should be packaged
686  # into the final output. Any file that does not pass the predicate will
687  # be removed below.
688  png_paths = []
689  for directory in dep_subdirs:
690    for f in _IterFiles(directory):
691      if not keep_predicate(f):
692        os.remove(f)
693      elif f.endswith('.png'):
694        png_paths.append(f)
695
696  return png_paths
697
698
699def _PackageApk(options, build):
700  """Compile and link resources with aapt2.
701
702  Args:
703    options: The command-line options.
704    build: BuildContext object.
705  Returns:
706    The manifest package name for the APK.
707  """
708  logging.debug('Extracting resource .zips')
709  dep_subdirs = []
710  dep_subdir_overlay_set = set()
711  for dependency_res_zip in options.dependencies_res_zips:
712    extracted_dep_subdirs = resource_utils.ExtractDeps([dependency_res_zip],
713                                                       build.deps_dir)
714    dep_subdirs += extracted_dep_subdirs
715    if dependency_res_zip in options.dependencies_res_zip_overlays:
716      dep_subdir_overlay_set.update(extracted_dep_subdirs)
717
718  logging.debug('Applying locale transformations')
719  path_info = resource_utils.ResourceInfoFile()
720  _RenameLocaleResourceDirs(dep_subdirs, path_info)
721
722  logging.debug('Applying file-based exclusions')
723  keep_predicate = _CreateKeepPredicate(options.resource_exclusion_regex,
724                                        options.resource_exclusion_exceptions)
725  png_paths = _FilterResourceFiles(dep_subdirs, keep_predicate)
726
727  if options.locale_allowlist or options.shared_resources_allowlist_locales:
728    logging.debug('Applying locale-based string exclusions')
729    _RemoveUnwantedLocalizedStrings(dep_subdirs, options)
730
731  if png_paths and options.png_to_webp:
732    logging.debug('Converting png->webp')
733    _ConvertToWebP(options.webp_binary, png_paths, path_info,
734                   options.webp_cache_dir)
735  logging.debug('Applying drawable transformations')
736  for directory in dep_subdirs:
737    _MoveImagesToNonMdpiFolders(directory, path_info)
738    _RemoveImageExtensions(directory, path_info)
739
740  logging.debug('Running aapt2 compile')
741  exclusion_rules = [x.split(':', 1) for x in options.values_filter_rules]
742  partials = _CompileDeps(options.aapt2_path, dep_subdirs,
743                          dep_subdir_overlay_set, build.temp_dir,
744                          exclusion_rules)
745
746  link_command = [
747      options.aapt2_path,
748      'link',
749      '--auto-add-overlay',
750      '--no-version-vectors',
751      '--no-xml-namespaces',
752      '--output-text-symbols',
753      build.r_txt_path,
754  ]
755
756  for j in options.include_resources:
757    link_command += ['-I', j]
758  if options.proguard_file:
759    link_command += ['--proguard', build.proguard_path]
760    link_command += ['--proguard-minimal-keep-rules']
761  if options.emit_ids_out:
762    link_command += ['--emit-ids', build.emit_ids_path]
763
764  # Note: only one of --proto-format, --shared-lib or --app-as-shared-lib
765  #       can be used with recent versions of aapt2.
766  if options.shared_resources:
767    link_command.append('--shared-lib')
768
769  if options.package_id:
770    link_command += [
771        '--package-id',
772        '0x%02x' % options.package_id,
773        '--allow-reserved-package-id',
774    ]
775
776  fixed_manifest, desired_manifest_package_name, fixed_manifest_package = (
777      _FixManifest(options, build.temp_dir))
778  if options.rename_manifest_package:
779    desired_manifest_package_name = options.rename_manifest_package
780
781  link_command += [
782      '--manifest', fixed_manifest, '--rename-manifest-package',
783      desired_manifest_package_name
784  ]
785
786  if options.package_id is not None:
787    package_id = options.package_id
788  elif options.shared_resources:
789    package_id = 0
790  else:
791    package_id = 0x7f
792  _CreateStableIdsFile(options.use_resource_ids_path, build.stable_ids_path,
793                       fixed_manifest_package, package_id)
794  link_command += ['--stable-ids', build.stable_ids_path]
795
796  link_command += partials
797
798  # We always create a binary arsc file first, then convert to proto, so flags
799  # such as --shared-lib can be supported.
800  link_command += ['-o', build.arsc_path]
801
802  logging.debug('Starting: aapt2 link')
803  link_proc = subprocess.Popen(link_command)
804
805  # Create .res.info file in parallel.
806  if options.info_path:
807    logging.debug('Creating .res.info file')
808    _CreateResourceInfoFile(path_info, build.info_path,
809                            options.dependencies_res_zips)
810
811  exit_code = link_proc.wait()
812  assert exit_code == 0, f'aapt2 link cmd failed with {exit_code=}'
813  logging.debug('Finished: aapt2 link')
814
815  if options.shared_resources:
816    logging.debug('Resolving styleables in R.txt')
817    # Need to resolve references because unused resource removal tool does not
818    # support references in R.txt files.
819    resource_utils.ResolveStyleableReferences(build.r_txt_path)
820
821  if exit_code:
822    raise subprocess.CalledProcessError(exit_code, link_command)
823
824  if options.proguard_file and (options.shared_resources
825                                or options.app_as_shared_lib):
826    # Make sure the R class associated with the manifest package does not have
827    # its onResourcesLoaded method obfuscated or removed, so that the framework
828    # can call it in the case where the APK is being loaded as a library.
829    with open(build.proguard_path, 'a') as proguard_file:
830      keep_rule = '''
831                  -keep,allowoptimization class {package}.R {{
832                    public static void onResourcesLoaded(int);
833                  }}
834                  '''.format(package=desired_manifest_package_name)
835      proguard_file.write(textwrap.dedent(keep_rule))
836
837  logging.debug('Running aapt2 convert')
838  build_utils.CheckOutput([
839      options.aapt2_path, 'convert', '--output-format', 'proto', '-o',
840      build.proto_path, build.arsc_path
841  ])
842
843  # Workaround for b/147674078. This is only needed for WebLayer and does not
844  # affect WebView usage, since WebView does not used dynamic attributes.
845  if options.shared_resources:
846    logging.debug('Hardcoding dynamic attributes')
847    protoresources.HardcodeSharedLibraryDynamicAttributes(
848        build.proto_path, options.is_bundle_module,
849        options.shared_resources_allowlist)
850
851    build_utils.CheckOutput([
852        options.aapt2_path, 'convert', '--output-format', 'binary', '-o',
853        build.arsc_path, build.proto_path
854    ])
855
856  # Sanity check that the created resources have the expected package ID.
857  logging.debug('Performing sanity check')
858  _, actual_package_id = resource_utils.ExtractArscPackage(
859      options.aapt2_path,
860      build.arsc_path if options.arsc_path else build.proto_path)
861  # When there are no resources, ExtractArscPackage returns (None, None), in
862  # this case there is no need to check for matching package ID.
863  if actual_package_id is not None and actual_package_id != package_id:
864    raise Exception('Invalid package ID 0x%x (expected 0x%x)' %
865                    (actual_package_id, package_id))
866
867  return desired_manifest_package_name
868
869
870def _CreateStableIdsFile(in_path, out_path, package_name, package_id):
871  """Transforms a file generated by --emit-ids from another package.
872
873  --stable-ids is generally meant to be used by different versions of the same
874  package. To make it work for other packages, we need to transform the package
875  name references to match the package that resources are being generated for.
876  """
877  if in_path:
878    data = pathlib.Path(in_path).read_text()
879  else:
880    # Force IDs to use 0x01 for the type byte in order to ensure they are
881    # different from IDs generated by other apps. https://crbug.com/1293336
882    data = 'pkg:id/fake_resource_id = 0x7f010000\n'
883  # Replace "pkg:" with correct package name.
884  data = re.sub(r'^.*?:', package_name + ':', data, flags=re.MULTILINE)
885  # Replace "0x7f" with correct package id.
886  data = re.sub(r'0x..', '0x%02x' % package_id, data)
887  pathlib.Path(out_path).write_text(data)
888
889
890def _WriteOutputs(options, build):
891  possible_outputs = [
892      (options.srcjar_out, build.srcjar_path),
893      (options.r_text_out, build.r_txt_path),
894      (options.arsc_path, build.arsc_path),
895      (options.proto_path, build.proto_path),
896      (options.proguard_file, build.proguard_path),
897      (options.emit_ids_out, build.emit_ids_path),
898      (options.info_path, build.info_path),
899  ]
900
901  for final, temp in possible_outputs:
902    # Write file only if it's changed.
903    if final and not (os.path.exists(final) and filecmp.cmp(final, temp)):
904      shutil.move(temp, final)
905
906
907def _CreateNormalizedManifestForVerification(options):
908  with build_utils.TempDir() as tempdir:
909    fixed_manifest, _, _ = _FixManifest(options, tempdir)
910    with open(fixed_manifest) as f:
911      return manifest_utils.NormalizeManifest(
912          f.read(), options.verification_version_code_offset,
913          options.verification_library_version_offset)
914
915
916def main(args):
917  build_utils.InitLogging('RESOURCE_DEBUG')
918  args = build_utils.ExpandFileArgs(args)
919  options = _ParseArgs(args)
920
921  if options.expected_file:
922    actual_data = _CreateNormalizedManifestForVerification(options)
923    diff_utils.CheckExpectations(actual_data, options)
924    if options.only_verify_expectations:
925      return
926
927  path = options.arsc_path or options.proto_path
928  debug_temp_resources_dir = os.environ.get('TEMP_RESOURCES_DIR')
929  if debug_temp_resources_dir:
930    path = os.path.join(debug_temp_resources_dir, os.path.basename(path))
931  else:
932    # Use a deterministic temp directory since .pb files embed the absolute
933    # path of resources: crbug.com/939984
934    path = path + '.tmpdir'
935  build_utils.DeleteDirectory(path)
936
937  with resource_utils.BuildContext(
938      temp_dir=path, keep_files=bool(debug_temp_resources_dir)) as build:
939
940    manifest_package_name = _PackageApk(options, build)
941
942    # If --shared-resources-allowlist is used, all the resources listed in the
943    # corresponding R.txt file will be non-final, and an onResourcesLoaded()
944    # will be generated to adjust them at runtime.
945    #
946    # Otherwise, if --shared-resources is used, the all resources will be
947    # non-final, and an onResourcesLoaded() method will be generated too.
948    #
949    # Otherwise, all resources will be final, and no method will be generated.
950    #
951    rjava_build_options = resource_utils.RJavaBuildOptions()
952    if options.shared_resources_allowlist:
953      rjava_build_options.ExportSomeResources(
954          options.shared_resources_allowlist)
955      rjava_build_options.GenerateOnResourcesLoaded()
956      if options.shared_resources:
957        # The final resources will only be used in WebLayer, so hardcode the
958        # package ID to be what WebLayer expects.
959        rjava_build_options.SetFinalPackageId(
960            protoresources.SHARED_LIBRARY_HARDCODED_ID)
961    elif options.shared_resources or options.app_as_shared_lib:
962      rjava_build_options.ExportAllResources()
963      rjava_build_options.GenerateOnResourcesLoaded()
964
965    custom_root_package_name = options.r_java_root_package_name
966    grandparent_custom_package_name = None
967
968    # Always generate an R.java file for the package listed in
969    # AndroidManifest.xml because this is where Android framework looks to find
970    # onResourcesLoaded() for shared library apks. While not actually necessary
971    # for application apks, it also doesn't hurt.
972    apk_package_name = manifest_package_name
973
974    if options.package_name and not options.arsc_package_name:
975      # Feature modules have their own custom root package name and should
976      # inherit from the appropriate base module package. This behaviour should
977      # not be present for test apks with an apk under test. Thus,
978      # arsc_package_name is used as it is only defined for test apks with an
979      # apk under test.
980      custom_root_package_name = options.package_name
981      grandparent_custom_package_name = options.r_java_root_package_name
982      # Feature modules have the same manifest package as the base module but
983      # they should not create an R.java for said manifest package because it
984      # will be created in the base module.
985      apk_package_name = None
986
987    if options.srcjar_out:
988      logging.debug('Creating R.srcjar')
989      resource_utils.CreateRJavaFiles(build.srcjar_dir, apk_package_name,
990                                      build.r_txt_path,
991                                      options.extra_res_packages,
992                                      rjava_build_options, options.srcjar_out,
993                                      custom_root_package_name,
994                                      grandparent_custom_package_name)
995      with action_helpers.atomic_output(build.srcjar_path) as f:
996        zip_helpers.zip_directory(f, build.srcjar_dir)
997
998    logging.debug('Copying outputs')
999    _WriteOutputs(options, build)
1000
1001  if options.depfile:
1002    assert options.srcjar_out, 'Update first output below and remove assert.'
1003    depfile_deps = (options.dependencies_res_zips +
1004                    options.dependencies_res_zip_overlays +
1005                    options.include_resources)
1006    action_helpers.write_depfile(options.depfile, options.srcjar_out,
1007                                 depfile_deps)
1008
1009
1010if __name__ == '__main__':
1011  main(sys.argv[1:])
1012