| #!/usr/bin/env python3 |
| # |
| # Copyright 2012 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Compile Android resources into an intermediate APK. |
| |
| This can also generate an R.txt, and an .srcjar file containing the proper |
| final R.java class for all resource packages the APK depends on. |
| |
| This will crunch images with aapt2. |
| """ |
| |
| import argparse |
| import collections |
| import contextlib |
| import filecmp |
| import hashlib |
| import logging |
| import os |
| import pathlib |
| import re |
| import shutil |
| import subprocess |
| import sys |
| import textwrap |
| from xml.etree import ElementTree |
| |
| from util import build_utils |
| from util import diff_utils |
| from util import manifest_utils |
| from util import parallel |
| from util import protoresources |
| from util import resource_utils |
| import action_helpers # build_utils adds //build to sys.path. |
| import zip_helpers |
| |
| |
| # Pngs that we shouldn't convert to webp. Please add rationale when updating. |
| _PNG_WEBP_EXCLUSION_PATTERN = re.compile('|'.join([ |
| # Crashes on Galaxy S5 running L (https://crbug.com/807059). |
| r'.*star_gray\.png', |
| # Android requires pngs for 9-patch images. |
| r'.*\.9\.png', |
| # Daydream requires pngs for icon files. |
| r'.*daydream_icon_.*\.png' |
| ])) |
| |
| |
| def _ParseArgs(args): |
| """Parses command line options. |
| |
| Returns: |
| An options object as from argparse.ArgumentParser.parse_args() |
| """ |
| parser = argparse.ArgumentParser(description=__doc__) |
| |
| input_opts = parser.add_argument_group('Input options') |
| output_opts = parser.add_argument_group('Output options') |
| |
| input_opts.add_argument('--include-resources', |
| action='append', |
| required=True, |
| help='Paths to arsc resource files used to link ' |
| 'against. Can be specified multiple times.') |
| input_opts.add_argument( |
| '--dependencies-res-zips', |
| default=[], |
| help='Resources zip archives from dependents. Required to ' |
| 'resolve @type/foo references into dependent libraries.') |
| input_opts.add_argument( |
| '--extra-res-packages', |
| help='Additional package names to generate R.java files for.') |
| input_opts.add_argument( |
| '--aapt2-path', required=True, help='Path to the Android aapt2 tool.') |
| input_opts.add_argument( |
| '--android-manifest', required=True, help='AndroidManifest.xml path.') |
| input_opts.add_argument( |
| '--r-java-root-package-name', |
| default='base', |
| help='Short package name for this target\'s root R java file (ex. ' |
| 'input of "base" would become gen.base_module). Defaults to "base".') |
| group = input_opts.add_mutually_exclusive_group() |
| group.add_argument( |
| '--shared-resources', |
| action='store_true', |
| help='Make all resources in R.java non-final and allow the resource IDs ' |
| 'to be reset to a different package index when the apk is loaded by ' |
| 'another application at runtime.') |
| group.add_argument( |
| '--app-as-shared-lib', |
| action='store_true', |
| help='Same as --shared-resources, but also ensures all resource IDs are ' |
| 'directly usable from the APK loaded as an application.') |
| input_opts.add_argument( |
| '--package-id', |
| type=int, |
| help='Decimal integer representing custom package ID for resources ' |
| '(instead of 127==0x7f). Cannot be used with --shared-resources.') |
| input_opts.add_argument( |
| '--package-name', |
| help='Package name that will be used to create R class.') |
| input_opts.add_argument( |
| '--rename-manifest-package', help='Package name to force AAPT to use.') |
| input_opts.add_argument( |
| '--arsc-package-name', |
| help='Package name to set in manifest of resources.arsc file. This is ' |
| 'only used for apks under test.') |
| input_opts.add_argument( |
| '--shared-resources-allowlist', |
| help='An R.txt file acting as a allowlist for resources that should be ' |
| 'non-final and have their package ID changed at runtime in R.java. ' |
| 'Implies and overrides --shared-resources.') |
| input_opts.add_argument( |
| '--shared-resources-allowlist-locales', |
| default='[]', |
| help='Optional GN-list of locales. If provided, all strings corresponding' |
| ' to this locale list will be kept in the final output for the ' |
| 'resources identified through --shared-resources-allowlist, even ' |
| 'if --locale-allowlist is being used.') |
| input_opts.add_argument( |
| '--use-resource-ids-path', |
| help='Use resource IDs generated by aapt --emit-ids.') |
| input_opts.add_argument( |
| '--debuggable', |
| action='store_true', |
| help='Whether to add android:debuggable="true".') |
| input_opts.add_argument('--version-code', help='Version code for apk.') |
| input_opts.add_argument('--version-name', help='Version name for apk.') |
| input_opts.add_argument( |
| '--min-sdk-version', required=True, help='android:minSdkVersion for APK.') |
| input_opts.add_argument( |
| '--target-sdk-version', |
| required=True, |
| help="android:targetSdkVersion for APK.") |
| input_opts.add_argument( |
| '--max-sdk-version', |
| help="android:maxSdkVersion expected in AndroidManifest.xml.") |
| input_opts.add_argument( |
| '--manifest-package', help='Package name of the AndroidManifest.xml.') |
| input_opts.add_argument( |
| '--locale-allowlist', |
| default='[]', |
| help='GN list of languages to include. All other language configs will ' |
| 'be stripped out. List may include a combination of Android locales ' |
| 'or Chrome locales.') |
| input_opts.add_argument( |
| '--resource-exclusion-regex', |
| default='', |
| help='File-based filter for resources (applied before compiling)') |
| input_opts.add_argument( |
| '--resource-exclusion-exceptions', |
| default='[]', |
| help='GN list of globs that say which files to include even ' |
| 'when --resource-exclusion-regex is set.') |
| input_opts.add_argument( |
| '--dependencies-res-zip-overlays', |
| help='GN list with subset of --dependencies-res-zips to use overlay ' |
| 'semantics for.') |
| input_opts.add_argument( |
| '--values-filter-rules', |
| help='GN list of source_glob:regex for filtering resources after they ' |
| 'are compiled. Use this to filter out entries within values/ files.') |
| input_opts.add_argument('--png-to-webp', action='store_true', |
| help='Convert png files to webp format.') |
| |
| input_opts.add_argument('--webp-binary', default='', |
| help='Path to the cwebp binary.') |
| input_opts.add_argument( |
| '--webp-cache-dir', help='The directory to store webp image cache.') |
| input_opts.add_argument( |
| '--is-bundle-module', |
| action='store_true', |
| help='Whether resources are being generated for a bundle module.') |
| input_opts.add_argument( |
| '--uses-split', |
| help='Value to set uses-split to in the AndroidManifest.xml.') |
| input_opts.add_argument( |
| '--verification-version-code-offset', |
| help='Subtract this from versionCode for expectation files') |
| input_opts.add_argument( |
| '--verification-library-version-offset', |
| help='Subtract this from static-library version for expectation files') |
| |
| action_helpers.add_depfile_arg(output_opts) |
| output_opts.add_argument('--arsc-path', help='Apk output for arsc format.') |
| output_opts.add_argument('--proto-path', help='Apk output for proto format.') |
| output_opts.add_argument( |
| '--info-path', help='Path to output info file for the partial apk.') |
| output_opts.add_argument( |
| '--srcjar-out', |
| help='Path to srcjar to contain generated R.java.') |
| output_opts.add_argument('--r-text-out', |
| help='Path to store the generated R.txt file.') |
| output_opts.add_argument( |
| '--proguard-file', help='Path to proguard.txt generated file.') |
| output_opts.add_argument( |
| '--proguard-file-main-dex', |
| help='Path to proguard.txt generated file for main dex.') |
| output_opts.add_argument( |
| '--emit-ids-out', help='Path to file produced by aapt2 --emit-ids.') |
| |
| diff_utils.AddCommandLineFlags(parser) |
| options = parser.parse_args(args) |
| |
| options.include_resources = action_helpers.parse_gn_list( |
| options.include_resources) |
| options.dependencies_res_zips = action_helpers.parse_gn_list( |
| options.dependencies_res_zips) |
| options.extra_res_packages = action_helpers.parse_gn_list( |
| options.extra_res_packages) |
| options.locale_allowlist = action_helpers.parse_gn_list( |
| options.locale_allowlist) |
| options.shared_resources_allowlist_locales = action_helpers.parse_gn_list( |
| options.shared_resources_allowlist_locales) |
| options.resource_exclusion_exceptions = action_helpers.parse_gn_list( |
| options.resource_exclusion_exceptions) |
| options.dependencies_res_zip_overlays = action_helpers.parse_gn_list( |
| options.dependencies_res_zip_overlays) |
| options.values_filter_rules = action_helpers.parse_gn_list( |
| options.values_filter_rules) |
| |
| if not options.arsc_path and not options.proto_path: |
| parser.error('One of --arsc-path or --proto-path is required.') |
| |
| if options.package_id and options.shared_resources: |
| parser.error('--package-id and --shared-resources are mutually exclusive') |
| |
| return options |
| |
| |
| def _IterFiles(root_dir): |
| for root, _, files in os.walk(root_dir): |
| for f in files: |
| yield os.path.join(root, f) |
| |
| |
| def _RenameLocaleResourceDirs(resource_dirs, path_info): |
| """Rename locale resource directories into standard names when necessary. |
| |
| This is necessary to deal with the fact that older Android releases only |
| support ISO 639-1 two-letter codes, and sometimes even obsolete versions |
| of them. |
| |
| In practice it means: |
| * 3-letter ISO 639-2 qualifiers are renamed under a corresponding |
| 2-letter one. E.g. for Filipino, strings under values-fil/ will be moved |
| to a new corresponding values-tl/ sub-directory. |
| |
| * Modern ISO 639-1 codes will be renamed to their obsolete variant |
| for Indonesian, Hebrew and Yiddish (e.g. 'values-in/ -> values-id/). |
| |
| * Norwegian macrolanguage strings will be renamed to Bokmal (main |
| Norway language). See http://crbug.com/920960. In practice this |
| means that 'values-no/ -> values-nb/' unless 'values-nb/' already |
| exists. |
| |
| * BCP 47 langauge tags will be renamed to an equivalent ISO 639-1 |
| locale qualifier if possible (e.g. 'values-b+en+US/ -> values-en-rUS'). |
| |
| Args: |
| resource_dirs: list of top-level resource directories. |
| """ |
| for resource_dir in resource_dirs: |
| ignore_dirs = {} |
| for path in _IterFiles(resource_dir): |
| locale = resource_utils.FindLocaleInStringResourceFilePath(path) |
| if not locale: |
| continue |
| cr_locale = resource_utils.ToChromiumLocaleName(locale) |
| if not cr_locale: |
| continue # Unsupported Android locale qualifier!? |
| locale2 = resource_utils.ToAndroidLocaleName(cr_locale) |
| if locale != locale2: |
| path2 = path.replace('/values-%s/' % locale, '/values-%s/' % locale2) |
| if path == path2: |
| raise Exception('Could not substitute locale %s for %s in %s' % |
| (locale, locale2, path)) |
| |
| # Ignore rather than rename when the destination resources config |
| # already exists. |
| # e.g. some libraries provide both values-nb/ and values-no/. |
| # e.g. material design provides: |
| # * res/values-rUS/values-rUS.xml |
| # * res/values-b+es+419/values-b+es+419.xml |
| config_dir = os.path.dirname(path2) |
| already_has_renamed_config = ignore_dirs.get(config_dir) |
| if already_has_renamed_config is None: |
| # Cache the result of the first time the directory is encountered |
| # since subsequent encounters will find the directory already exists |
| # (due to the rename). |
| already_has_renamed_config = os.path.exists(config_dir) |
| ignore_dirs[config_dir] = already_has_renamed_config |
| if already_has_renamed_config: |
| continue |
| |
| build_utils.MakeDirectory(os.path.dirname(path2)) |
| shutil.move(path, path2) |
| path_info.RegisterRename( |
| os.path.relpath(path, resource_dir), |
| os.path.relpath(path2, resource_dir)) |
| |
| |
| def _ToAndroidLocales(locale_allowlist): |
| """Converts the list of Chrome locales to Android config locale qualifiers. |
| |
| Args: |
| locale_allowlist: A list of Chromium locale names. |
| Returns: |
| A set of matching Android config locale qualifier names. |
| """ |
| ret = set() |
| for locale in locale_allowlist: |
| locale = resource_utils.ToAndroidLocaleName(locale) |
| if locale is None or ('-' in locale and '-r' not in locale): |
| raise Exception('Unsupported Chromium locale name: %s' % locale) |
| ret.add(locale) |
| # Always keep non-regional fall-backs. |
| language = locale.split('-')[0] |
| ret.add(language) |
| |
| return ret |
| |
| |
| def _MoveImagesToNonMdpiFolders(res_root, path_info): |
| """Move images from drawable-*-mdpi-* folders to drawable-* folders. |
| |
| Why? http://crbug.com/289843 |
| """ |
| for src_dir_name in os.listdir(res_root): |
| src_components = src_dir_name.split('-') |
| if src_components[0] != 'drawable' or 'mdpi' not in src_components: |
| continue |
| src_dir = os.path.join(res_root, src_dir_name) |
| if not os.path.isdir(src_dir): |
| continue |
| dst_components = [c for c in src_components if c != 'mdpi'] |
| assert dst_components != src_components |
| dst_dir_name = '-'.join(dst_components) |
| dst_dir = os.path.join(res_root, dst_dir_name) |
| build_utils.MakeDirectory(dst_dir) |
| for src_file_name in os.listdir(src_dir): |
| if not os.path.splitext(src_file_name)[1] in ('.png', '.webp', ''): |
| continue |
| src_file = os.path.join(src_dir, src_file_name) |
| dst_file = os.path.join(dst_dir, src_file_name) |
| assert not os.path.lexists(dst_file) |
| shutil.move(src_file, dst_file) |
| path_info.RegisterRename( |
| os.path.relpath(src_file, res_root), |
| os.path.relpath(dst_file, res_root)) |
| |
| |
| def _DeterminePlatformVersion(aapt2_path, jar_candidates): |
| def maybe_extract_version(j): |
| try: |
| return resource_utils.ExtractBinaryManifestValues(aapt2_path, j) |
| except build_utils.CalledProcessError: |
| return None |
| |
| def is_sdk_jar(jar_name): |
| if jar_name in ('android.jar', 'android_system.jar'): |
| return True |
| # Robolectric jar looks a bit different. |
| return 'android-all' in jar_name and 'robolectric' in jar_name |
| |
| android_sdk_jars = [ |
| j for j in jar_candidates if is_sdk_jar(os.path.basename(j)) |
| ] |
| extract_all = [maybe_extract_version(j) for j in android_sdk_jars] |
| extract_all = [x for x in extract_all if x] |
| if len(extract_all) == 0: |
| raise Exception( |
| 'Unable to find android SDK jar among candidates: %s' |
| % ', '.join(android_sdk_jars)) |
| if len(extract_all) > 1: |
| raise Exception( |
| 'Found multiple android SDK jars among candidates: %s' |
| % ', '.join(android_sdk_jars)) |
| platform_version_code, platform_version_name = extract_all.pop()[:2] |
| return platform_version_code, platform_version_name |
| |
| |
| def _FixManifest(options, temp_dir): |
| """Fix the APK's AndroidManifest.xml. |
| |
| This adds any missing namespaces for 'android' and 'tools', and |
| sets certains elements like 'platformBuildVersionCode' or |
| 'android:debuggable' depending on the content of |options|. |
| |
| Args: |
| options: The command-line arguments tuple. |
| temp_dir: A temporary directory where the fixed manifest will be written to. |
| Returns: |
| Tuple of: |
| * Manifest path within |temp_dir|. |
| * Original package_name. |
| * Manifest package name. |
| """ |
| doc, manifest_node, app_node = manifest_utils.ParseManifest( |
| options.android_manifest) |
| |
| # merge_manifest.py also sets package & <uses-sdk>. We may want to ensure |
| # manifest merger is always enabled and remove these command-line arguments. |
| manifest_utils.SetUsesSdk(manifest_node, options.target_sdk_version, |
| options.min_sdk_version, options.max_sdk_version) |
| orig_package = manifest_node.get('package') or options.manifest_package |
| fixed_package = (options.arsc_package_name or options.manifest_package |
| or orig_package) |
| manifest_node.set('package', fixed_package) |
| |
| platform_version_code, platform_version_name = _DeterminePlatformVersion( |
| options.aapt2_path, options.include_resources) |
| manifest_node.set('platformBuildVersionCode', platform_version_code) |
| manifest_node.set('platformBuildVersionName', platform_version_name) |
| if options.version_code: |
| manifest_utils.NamespacedSet(manifest_node, 'versionCode', |
| options.version_code) |
| if options.version_name: |
| manifest_utils.NamespacedSet(manifest_node, 'versionName', |
| options.version_name) |
| if options.debuggable: |
| manifest_utils.NamespacedSet(app_node, 'debuggable', 'true') |
| |
| if options.uses_split: |
| uses_split = ElementTree.SubElement(manifest_node, 'uses-split') |
| manifest_utils.NamespacedSet(uses_split, 'name', options.uses_split) |
| |
| # Make sure the min-sdk condition is not less than the min-sdk of the bundle. |
| for min_sdk_node in manifest_node.iter('{%s}min-sdk' % |
| manifest_utils.DIST_NAMESPACE): |
| dist_value = '{%s}value' % manifest_utils.DIST_NAMESPACE |
| if int(min_sdk_node.get(dist_value)) < int(options.min_sdk_version): |
| min_sdk_node.set(dist_value, options.min_sdk_version) |
| |
| debug_manifest_path = os.path.join(temp_dir, 'AndroidManifest.xml') |
| manifest_utils.SaveManifest(doc, debug_manifest_path) |
| return debug_manifest_path, orig_package, fixed_package |
| |
| |
| def _CreateKeepPredicate(resource_exclusion_regex, |
| resource_exclusion_exceptions): |
| """Return a predicate lambda to determine which resource files to keep. |
| |
| Args: |
| resource_exclusion_regex: A regular expression describing all resources |
| to exclude, except if they are mip-maps, or if they are listed |
| in |resource_exclusion_exceptions|. |
| resource_exclusion_exceptions: A list of glob patterns corresponding |
| to exceptions to the |resource_exclusion_regex|. |
| Returns: |
| A lambda that takes a path, and returns true if the corresponding file |
| must be kept. |
| """ |
| predicate = lambda path: os.path.basename(path)[0] != '.' |
| if resource_exclusion_regex == '': |
| # Do not extract dotfiles (e.g. ".gitkeep"). aapt ignores them anyways. |
| return predicate |
| |
| # A simple predicate that only removes (returns False for) paths covered by |
| # the exclusion regex or listed as exceptions. |
| return lambda path: ( |
| not re.search(resource_exclusion_regex, path) or |
| build_utils.MatchesGlob(path, resource_exclusion_exceptions)) |
| |
| |
| def _ComputeSha1(path): |
| with open(path, 'rb') as f: |
| data = f.read() |
| return hashlib.sha1(data).hexdigest() |
| |
| |
| def _ConvertToWebPSingle(png_path, cwebp_binary, cwebp_version, webp_cache_dir): |
| sha1_hash = _ComputeSha1(png_path) |
| |
| # The set of arguments that will appear in the cache key. |
| quality_args = ['-m', '6', '-q', '100', '-lossless'] |
| |
| webp_cache_path = os.path.join( |
| webp_cache_dir, '{}-{}-{}'.format(sha1_hash, cwebp_version, |
| ''.join(quality_args))) |
| # No need to add .webp. Android can load images fine without them. |
| webp_path = os.path.splitext(png_path)[0] |
| |
| cache_hit = os.path.exists(webp_cache_path) |
| if cache_hit: |
| os.link(webp_cache_path, webp_path) |
| else: |
| # We place the generated webp image to webp_path, instead of in the |
| # webp_cache_dir to avoid concurrency issues. |
| args = [cwebp_binary, png_path, '-o', webp_path, '-quiet'] + quality_args |
| subprocess.check_call(args) |
| |
| try: |
| os.link(webp_path, webp_cache_path) |
| except OSError: |
| # Because of concurrent run, a webp image may already exists in |
| # webp_cache_path. |
| pass |
| |
| os.remove(png_path) |
| original_dir = os.path.dirname(os.path.dirname(png_path)) |
| rename_tuple = (os.path.relpath(png_path, original_dir), |
| os.path.relpath(webp_path, original_dir)) |
| return rename_tuple, cache_hit |
| |
| |
| def _ConvertToWebP(cwebp_binary, png_paths, path_info, webp_cache_dir): |
| cwebp_version = subprocess.check_output([cwebp_binary, '-version']).rstrip() |
| shard_args = [(f, ) for f in png_paths |
| if not _PNG_WEBP_EXCLUSION_PATTERN.match(f)] |
| |
| build_utils.MakeDirectory(webp_cache_dir) |
| results = parallel.BulkForkAndCall(_ConvertToWebPSingle, |
| shard_args, |
| cwebp_binary=cwebp_binary, |
| cwebp_version=cwebp_version, |
| webp_cache_dir=webp_cache_dir) |
| total_cache_hits = 0 |
| for rename_tuple, cache_hit in results: |
| path_info.RegisterRename(*rename_tuple) |
| total_cache_hits += int(cache_hit) |
| |
| logging.debug('png->webp cache: %d/%d', total_cache_hits, len(shard_args)) |
| |
| |
| def _RemoveImageExtensions(directory, path_info): |
| """Remove extensions from image files in the passed directory. |
| |
| This reduces binary size but does not affect android's ability to load the |
| images. |
| """ |
| for f in _IterFiles(directory): |
| if (f.endswith('.png') or f.endswith('.webp')) and not f.endswith('.9.png'): |
| path_with_extension = f |
| path_no_extension = os.path.splitext(path_with_extension)[0] |
| if path_no_extension != path_with_extension: |
| shutil.move(path_with_extension, path_no_extension) |
| path_info.RegisterRename( |
| os.path.relpath(path_with_extension, directory), |
| os.path.relpath(path_no_extension, directory)) |
| |
| |
| def _CompileSingleDep(index, dep_subdir, keep_predicate, aapt2_path, |
| partials_dir): |
| unique_name = '{}_{}'.format(index, os.path.basename(dep_subdir)) |
| partial_path = os.path.join(partials_dir, '{}.zip'.format(unique_name)) |
| |
| compile_command = [ |
| aapt2_path, |
| 'compile', |
| # TODO(wnwen): Turn this on once aapt2 forces 9-patch to be crunched. |
| # '--no-crunch', |
| '--dir', |
| dep_subdir, |
| '-o', |
| partial_path |
| ] |
| |
| # There are resources targeting API-versions lower than our minapi. For |
| # various reasons it's easier to let aapt2 ignore these than for us to |
| # remove them from our build (e.g. it's from a 3rd party library). |
| build_utils.CheckOutput( |
| compile_command, |
| stderr_filter=lambda output: build_utils.FilterLines( |
| output, r'ignoring configuration .* for (styleable|attribute)')) |
| |
| # Filtering these files is expensive, so only apply filters to the partials |
| # that have been explicitly targeted. |
| if keep_predicate: |
| logging.debug('Applying .arsc filtering to %s', dep_subdir) |
| protoresources.StripUnwantedResources(partial_path, keep_predicate) |
| return partial_path |
| |
| |
| def _CreateValuesKeepPredicate(exclusion_rules, dep_subdir): |
| patterns = [ |
| x[1] for x in exclusion_rules |
| if build_utils.MatchesGlob(dep_subdir, [x[0]]) |
| ] |
| if not patterns: |
| return None |
| |
| regexes = [re.compile(p) for p in patterns] |
| return lambda x: not any(r.search(x) for r in regexes) |
| |
| |
| def _CompileDeps(aapt2_path, dep_subdirs, dep_subdir_overlay_set, temp_dir, |
| exclusion_rules): |
| partials_dir = os.path.join(temp_dir, 'partials') |
| build_utils.MakeDirectory(partials_dir) |
| |
| job_params = [(i, dep_subdir, |
| _CreateValuesKeepPredicate(exclusion_rules, dep_subdir)) |
| for i, dep_subdir in enumerate(dep_subdirs)] |
| |
| # Filtering is slow, so ensure jobs with keep_predicate are started first. |
| job_params.sort(key=lambda x: not x[2]) |
| partials = list( |
| parallel.BulkForkAndCall(_CompileSingleDep, |
| job_params, |
| aapt2_path=aapt2_path, |
| partials_dir=partials_dir)) |
| |
| partials_cmd = list() |
| for i, partial in enumerate(partials): |
| dep_subdir = job_params[i][1] |
| if dep_subdir in dep_subdir_overlay_set: |
| partials_cmd += ['-R'] |
| partials_cmd += [partial] |
| return partials_cmd |
| |
| |
| def _CreateResourceInfoFile(path_info, info_path, dependencies_res_zips): |
| for zip_file in dependencies_res_zips: |
| zip_info_file_path = zip_file + '.info' |
| if os.path.exists(zip_info_file_path): |
| path_info.MergeInfoFile(zip_info_file_path) |
| path_info.Write(info_path) |
| |
| |
| def _RemoveUnwantedLocalizedStrings(dep_subdirs, options): |
| """Remove localized strings that should not go into the final output. |
| |
| Args: |
| dep_subdirs: List of resource dependency directories. |
| options: Command-line options namespace. |
| """ |
| # Collect locale and file paths from the existing subdirs. |
| # The following variable maps Android locale names to |
| # sets of corresponding xml file paths. |
| locale_to_files_map = collections.defaultdict(set) |
| for directory in dep_subdirs: |
| for f in _IterFiles(directory): |
| locale = resource_utils.FindLocaleInStringResourceFilePath(f) |
| if locale: |
| locale_to_files_map[locale].add(f) |
| |
| all_locales = set(locale_to_files_map) |
| |
| # Set A: wanted locales, either all of them or the |
| # list provided by --locale-allowlist. |
| wanted_locales = all_locales |
| if options.locale_allowlist: |
| wanted_locales = _ToAndroidLocales(options.locale_allowlist) |
| |
| # Set B: shared resources locales, which is either set A |
| # or the list provided by --shared-resources-allowlist-locales |
| shared_resources_locales = wanted_locales |
| shared_names_allowlist = set() |
| if options.shared_resources_allowlist_locales: |
| shared_names_allowlist = set( |
| resource_utils.GetRTxtStringResourceNames( |
| options.shared_resources_allowlist)) |
| |
| shared_resources_locales = _ToAndroidLocales( |
| options.shared_resources_allowlist_locales) |
| |
| # Remove any file that belongs to a locale not covered by |
| # either A or B. |
| removable_locales = (all_locales - wanted_locales - shared_resources_locales) |
| for locale in removable_locales: |
| for path in locale_to_files_map[locale]: |
| os.remove(path) |
| |
| # For any locale in B but not in A, only keep the shared |
| # resource strings in each file. |
| for locale in shared_resources_locales - wanted_locales: |
| for path in locale_to_files_map[locale]: |
| resource_utils.FilterAndroidResourceStringsXml( |
| path, lambda x: x in shared_names_allowlist) |
| |
| # For any locale in A but not in B, only keep the strings |
| # that are _not_ from shared resources in the file. |
| for locale in wanted_locales - shared_resources_locales: |
| for path in locale_to_files_map[locale]: |
| resource_utils.FilterAndroidResourceStringsXml( |
| path, lambda x: x not in shared_names_allowlist) |
| |
| |
| def _FilterResourceFiles(dep_subdirs, keep_predicate): |
| # Create a function that selects which resource files should be packaged |
| # into the final output. Any file that does not pass the predicate will |
| # be removed below. |
| png_paths = [] |
| for directory in dep_subdirs: |
| for f in _IterFiles(directory): |
| if not keep_predicate(f): |
| os.remove(f) |
| elif f.endswith('.png'): |
| png_paths.append(f) |
| |
| return png_paths |
| |
| |
| def _PackageApk(options, build): |
| """Compile and link resources with aapt2. |
| |
| Args: |
| options: The command-line options. |
| build: BuildContext object. |
| Returns: |
| The manifest package name for the APK. |
| """ |
| logging.debug('Extracting resource .zips') |
| dep_subdirs = [] |
| dep_subdir_overlay_set = set() |
| for dependency_res_zip in options.dependencies_res_zips: |
| extracted_dep_subdirs = resource_utils.ExtractDeps([dependency_res_zip], |
| build.deps_dir) |
| dep_subdirs += extracted_dep_subdirs |
| if dependency_res_zip in options.dependencies_res_zip_overlays: |
| dep_subdir_overlay_set.update(extracted_dep_subdirs) |
| |
| logging.debug('Applying locale transformations') |
| path_info = resource_utils.ResourceInfoFile() |
| _RenameLocaleResourceDirs(dep_subdirs, path_info) |
| |
| logging.debug('Applying file-based exclusions') |
| keep_predicate = _CreateKeepPredicate(options.resource_exclusion_regex, |
| options.resource_exclusion_exceptions) |
| png_paths = _FilterResourceFiles(dep_subdirs, keep_predicate) |
| |
| if options.locale_allowlist or options.shared_resources_allowlist_locales: |
| logging.debug('Applying locale-based string exclusions') |
| _RemoveUnwantedLocalizedStrings(dep_subdirs, options) |
| |
| if png_paths and options.png_to_webp: |
| logging.debug('Converting png->webp') |
| _ConvertToWebP(options.webp_binary, png_paths, path_info, |
| options.webp_cache_dir) |
| logging.debug('Applying drawable transformations') |
| for directory in dep_subdirs: |
| _MoveImagesToNonMdpiFolders(directory, path_info) |
| _RemoveImageExtensions(directory, path_info) |
| |
| logging.debug('Running aapt2 compile') |
| exclusion_rules = [x.split(':', 1) for x in options.values_filter_rules] |
| partials = _CompileDeps(options.aapt2_path, dep_subdirs, |
| dep_subdir_overlay_set, build.temp_dir, |
| exclusion_rules) |
| |
| link_command = [ |
| options.aapt2_path, |
| 'link', |
| '--auto-add-overlay', |
| '--no-version-vectors', |
| '--output-text-symbols', |
| build.r_txt_path, |
| ] |
| |
| for j in options.include_resources: |
| link_command += ['-I', j] |
| if options.proguard_file: |
| link_command += ['--proguard', build.proguard_path] |
| link_command += ['--proguard-minimal-keep-rules'] |
| if options.proguard_file_main_dex: |
| link_command += ['--proguard-main-dex', build.proguard_main_dex_path] |
| if options.emit_ids_out: |
| link_command += ['--emit-ids', build.emit_ids_path] |
| |
| # Note: only one of --proto-format, --shared-lib or --app-as-shared-lib |
| # can be used with recent versions of aapt2. |
| if options.shared_resources: |
| link_command.append('--shared-lib') |
| |
| if int(options.min_sdk_version) > 21: |
| link_command.append('--no-xml-namespaces') |
| |
| if options.package_id: |
| link_command += [ |
| '--package-id', |
| '0x%02x' % options.package_id, |
| '--allow-reserved-package-id', |
| ] |
| |
| fixed_manifest, desired_manifest_package_name, fixed_manifest_package = ( |
| _FixManifest(options, build.temp_dir)) |
| if options.rename_manifest_package: |
| desired_manifest_package_name = options.rename_manifest_package |
| |
| link_command += [ |
| '--manifest', fixed_manifest, '--rename-manifest-package', |
| desired_manifest_package_name |
| ] |
| |
| if options.package_id is not None: |
| package_id = options.package_id |
| elif options.shared_resources: |
| package_id = 0 |
| else: |
| package_id = 0x7f |
| _CreateStableIdsFile(options.use_resource_ids_path, build.stable_ids_path, |
| fixed_manifest_package, package_id) |
| link_command += ['--stable-ids', build.stable_ids_path] |
| |
| link_command += partials |
| |
| # We always create a binary arsc file first, then convert to proto, so flags |
| # such as --shared-lib can be supported. |
| link_command += ['-o', build.arsc_path] |
| |
| logging.debug('Starting: aapt2 link') |
| link_proc = subprocess.Popen(link_command) |
| |
| # Create .res.info file in parallel. |
| if options.info_path: |
| logging.debug('Creating .res.info file') |
| _CreateResourceInfoFile(path_info, build.info_path, |
| options.dependencies_res_zips) |
| |
| exit_code = link_proc.wait() |
| assert exit_code == 0, f'aapt2 link cmd failed with {exit_code=}' |
| logging.debug('Finished: aapt2 link') |
| |
| if options.shared_resources: |
| logging.debug('Resolving styleables in R.txt') |
| # Need to resolve references because unused resource removal tool does not |
| # support references in R.txt files. |
| resource_utils.ResolveStyleableReferences(build.r_txt_path) |
| |
| if exit_code: |
| raise subprocess.CalledProcessError(exit_code, link_command) |
| |
| if options.proguard_file and (options.shared_resources |
| or options.app_as_shared_lib): |
| # Make sure the R class associated with the manifest package does not have |
| # its onResourcesLoaded method obfuscated or removed, so that the framework |
| # can call it in the case where the APK is being loaded as a library. |
| with open(build.proguard_path, 'a') as proguard_file: |
| keep_rule = ''' |
| -keep,allowoptimization class {package}.R {{ |
| public static void onResourcesLoaded(int); |
| }} |
| '''.format(package=desired_manifest_package_name) |
| proguard_file.write(textwrap.dedent(keep_rule)) |
| |
| logging.debug('Running aapt2 convert') |
| build_utils.CheckOutput([ |
| options.aapt2_path, 'convert', '--output-format', 'proto', '-o', |
| build.proto_path, build.arsc_path |
| ]) |
| |
| # Workaround for b/147674078. This is only needed for WebLayer and does not |
| # affect WebView usage, since WebView does not used dynamic attributes. |
| if options.shared_resources: |
| logging.debug('Hardcoding dynamic attributes') |
| protoresources.HardcodeSharedLibraryDynamicAttributes( |
| build.proto_path, options.is_bundle_module, |
| options.shared_resources_allowlist) |
| |
| build_utils.CheckOutput([ |
| options.aapt2_path, 'convert', '--output-format', 'binary', '-o', |
| build.arsc_path, build.proto_path |
| ]) |
| |
| # Sanity check that the created resources have the expected package ID. |
| logging.debug('Performing sanity check') |
| _, actual_package_id = resource_utils.ExtractArscPackage( |
| options.aapt2_path, |
| build.arsc_path if options.arsc_path else build.proto_path) |
| # When there are no resources, ExtractArscPackage returns (None, None), in |
| # this case there is no need to check for matching package ID. |
| if actual_package_id is not None and actual_package_id != package_id: |
| raise Exception('Invalid package ID 0x%x (expected 0x%x)' % |
| (actual_package_id, package_id)) |
| |
| return desired_manifest_package_name |
| |
| |
| def _CreateStableIdsFile(in_path, out_path, package_name, package_id): |
| """Transforms a file generated by --emit-ids from another package. |
| |
| --stable-ids is generally meant to be used by different versions of the same |
| package. To make it work for other packages, we need to transform the package |
| name references to match the package that resources are being generated for. |
| """ |
| if in_path: |
| data = pathlib.Path(in_path).read_text() |
| else: |
| # Force IDs to use 0x01 for the type byte in order to ensure they are |
| # different from IDs generated by other apps. https://crbug.com/1293336 |
| data = 'pkg:id/fake_resource_id = 0x7f010000\n' |
| # Replace "pkg:" with correct package name. |
| data = re.sub(r'^.*?:', package_name + ':', data, flags=re.MULTILINE) |
| # Replace "0x7f" with correct package id. |
| data = re.sub(r'0x..', '0x%02x' % package_id, data) |
| pathlib.Path(out_path).write_text(data) |
| |
| |
| def _WriteOutputs(options, build): |
| possible_outputs = [ |
| (options.srcjar_out, build.srcjar_path), |
| (options.r_text_out, build.r_txt_path), |
| (options.arsc_path, build.arsc_path), |
| (options.proto_path, build.proto_path), |
| (options.proguard_file, build.proguard_path), |
| (options.proguard_file_main_dex, build.proguard_main_dex_path), |
| (options.emit_ids_out, build.emit_ids_path), |
| (options.info_path, build.info_path), |
| ] |
| |
| for final, temp in possible_outputs: |
| # Write file only if it's changed. |
| if final and not (os.path.exists(final) and filecmp.cmp(final, temp)): |
| shutil.move(temp, final) |
| |
| |
| def _CreateNormalizedManifestForVerification(options): |
| with build_utils.TempDir() as tempdir: |
| fixed_manifest, _, _ = _FixManifest(options, tempdir) |
| with open(fixed_manifest) as f: |
| return manifest_utils.NormalizeManifest( |
| f.read(), options.verification_version_code_offset, |
| options.verification_library_version_offset) |
| |
| |
| def main(args): |
| build_utils.InitLogging('RESOURCE_DEBUG') |
| args = build_utils.ExpandFileArgs(args) |
| options = _ParseArgs(args) |
| |
| if options.expected_file: |
| actual_data = _CreateNormalizedManifestForVerification(options) |
| diff_utils.CheckExpectations(actual_data, options) |
| if options.only_verify_expectations: |
| return |
| |
| path = options.arsc_path or options.proto_path |
| debug_temp_resources_dir = os.environ.get('TEMP_RESOURCES_DIR') |
| if debug_temp_resources_dir: |
| path = os.path.join(debug_temp_resources_dir, os.path.basename(path)) |
| else: |
| # Use a deterministic temp directory since .pb files embed the absolute |
| # path of resources: crbug.com/939984 |
| path = path + '.tmpdir' |
| build_utils.DeleteDirectory(path) |
| |
| with resource_utils.BuildContext( |
| temp_dir=path, keep_files=bool(debug_temp_resources_dir)) as build: |
| |
| manifest_package_name = _PackageApk(options, build) |
| |
| # If --shared-resources-allowlist is used, all the resources listed in the |
| # corresponding R.txt file will be non-final, and an onResourcesLoaded() |
| # will be generated to adjust them at runtime. |
| # |
| # Otherwise, if --shared-resources is used, the all resources will be |
| # non-final, and an onResourcesLoaded() method will be generated too. |
| # |
| # Otherwise, all resources will be final, and no method will be generated. |
| # |
| rjava_build_options = resource_utils.RJavaBuildOptions() |
| if options.shared_resources_allowlist: |
| rjava_build_options.ExportSomeResources( |
| options.shared_resources_allowlist) |
| rjava_build_options.GenerateOnResourcesLoaded() |
| if options.shared_resources: |
| # The final resources will only be used in WebLayer, so hardcode the |
| # package ID to be what WebLayer expects. |
| rjava_build_options.SetFinalPackageId( |
| protoresources.SHARED_LIBRARY_HARDCODED_ID) |
| elif options.shared_resources or options.app_as_shared_lib: |
| rjava_build_options.ExportAllResources() |
| rjava_build_options.GenerateOnResourcesLoaded() |
| |
| custom_root_package_name = options.r_java_root_package_name |
| grandparent_custom_package_name = None |
| |
| # Always generate an R.java file for the package listed in |
| # AndroidManifest.xml because this is where Android framework looks to find |
| # onResourcesLoaded() for shared library apks. While not actually necessary |
| # for application apks, it also doesn't hurt. |
| apk_package_name = manifest_package_name |
| |
| if options.package_name and not options.arsc_package_name: |
| # Feature modules have their own custom root package name and should |
| # inherit from the appropriate base module package. This behaviour should |
| # not be present for test apks with an apk under test. Thus, |
| # arsc_package_name is used as it is only defined for test apks with an |
| # apk under test. |
| custom_root_package_name = options.package_name |
| grandparent_custom_package_name = options.r_java_root_package_name |
| # Feature modules have the same manifest package as the base module but |
| # they should not create an R.java for said manifest package because it |
| # will be created in the base module. |
| apk_package_name = None |
| |
| if options.srcjar_out: |
| logging.debug('Creating R.srcjar') |
| resource_utils.CreateRJavaFiles(build.srcjar_dir, apk_package_name, |
| build.r_txt_path, |
| options.extra_res_packages, |
| rjava_build_options, options.srcjar_out, |
| custom_root_package_name, |
| grandparent_custom_package_name) |
| with action_helpers.atomic_output(build.srcjar_path) as f: |
| zip_helpers.zip_directory(f, build.srcjar_dir) |
| |
| logging.debug('Copying outputs') |
| _WriteOutputs(options, build) |
| |
| if options.depfile: |
| assert options.srcjar_out, 'Update first output below and remove assert.' |
| depfile_deps = (options.dependencies_res_zips + |
| options.dependencies_res_zip_overlays + |
| options.include_resources) |
| action_helpers.write_depfile(options.depfile, options.srcjar_out, |
| depfile_deps) |
| |
| |
| if __name__ == '__main__': |
| main(sys.argv[1:]) |