diff options
Diffstat (limited to 'third_party/libwebrtc/build/android/gyp/compile_resources.py')
-rwxr-xr-x | third_party/libwebrtc/build/android/gyp/compile_resources.py | 1032 |
1 files changed, 1032 insertions, 0 deletions
diff --git a/third_party/libwebrtc/build/android/gyp/compile_resources.py b/third_party/libwebrtc/build/android/gyp/compile_resources.py new file mode 100755 index 0000000000..9add95aed8 --- /dev/null +++ b/third_party/libwebrtc/build/android/gyp/compile_resources.py @@ -0,0 +1,1032 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2012 The Chromium Authors. All rights reserved. +# 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 re +import shutil +import subprocess +import sys +import tempfile +import textwrap +import zipfile +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 + + +# 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, input_opts, output_opts = resource_utils.ResourceArgsParser() + + 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( + '--extra-main-r-text-files', + help='Additional R.txt files that will be added to the root R.java file, ' + 'but not packaged in the generated resources.arsc. If these resources ' + 'entries contain duplicate resources with the generated R.txt file, they ' + 'must be identical.') + + 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( + '--no-xml-namespaces', + action='store_true', + help='Whether to strip xml namespaces from processed xml resources.') + + output_opts.add_argument('--arsc-path', help='Apk output for arsc format.') + output_opts.add_argument('--proto-path', help='Apk output for proto format.') + group = input_opts.add_mutually_exclusive_group() + + output_opts.add_argument( + '--info-path', help='Path to output info file for the partial apk.') + + output_opts.add_argument( + '--srcjar-out', + required=True, + 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.') + + 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( + '--extra-verification-manifest', + help='Path to AndroidManifest.xml which should be merged into base ' + 'manifest when performing verification.') + + diff_utils.AddCommandLineFlags(parser) + options = parser.parse_args(args) + + resource_utils.HandleCommonOptions(options) + + options.locale_allowlist = build_utils.ParseGnList(options.locale_allowlist) + options.shared_resources_allowlist_locales = build_utils.ParseGnList( + options.shared_resources_allowlist_locales) + options.resource_exclusion_exceptions = build_utils.ParseGnList( + options.resource_exclusion_exceptions) + options.dependencies_res_zip_overlays = build_utils.ParseGnList( + options.dependencies_res_zip_overlays) + options.values_filter_rules = build_utils.ParseGnList( + options.values_filter_rules) + options.extra_main_r_text_files = build_utils.ParseGnList( + options.extra_main_r_text_files) + + 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 _FixManifest(options, temp_dir, extra_manifest=None): + """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. + extra_manifest: Path to an AndroidManifest.xml file which will get merged + into the application node of the base manifest. + Returns: + Tuple of: + * Manifest path within |temp_dir|. + * Original package_name. + * Manifest package name. + """ + def maybe_extract_version(j): + try: + return resource_utils.ExtractBinaryManifestValues(options.aapt2_path, j) + except build_utils.CalledProcessError: + return None + + android_sdk_jars = [j for j in options.include_resources + if os.path.basename(j) in ('android.jar', + 'android_system.jar')] + extract_all = [maybe_extract_version(j) for j in android_sdk_jars] + successful_extractions = [x for x in extract_all if x] + if len(successful_extractions) == 0: + raise Exception( + 'Unable to find android SDK jar among candidates: %s' + % ', '.join(android_sdk_jars)) + elif len(successful_extractions) > 1: + raise Exception( + 'Found multiple android SDK jars among candidates: %s' + % ', '.join(android_sdk_jars)) + version_code, version_name = successful_extractions.pop()[:2] + + debug_manifest_path = os.path.join(temp_dir, 'AndroidManifest.xml') + doc, manifest_node, app_node = manifest_utils.ParseManifest( + options.android_manifest) + + if extra_manifest: + _, extra_manifest_node, extra_app_node = manifest_utils.ParseManifest( + extra_manifest) + for node in extra_app_node: + app_node.append(node) + for node in extra_manifest_node: + # DFM manifests have a bunch of tags we don't care about inside + # <manifest>, so only take <queries>. + if node.tag == 'queries': + manifest_node.append(node) + + manifest_utils.AssertUsesSdk(manifest_node, options.min_sdk_version, + options.target_sdk_version) + # We explicitly check that maxSdkVersion is set in the manifest since we don't + # add it later like minSdkVersion and targetSdkVersion. + manifest_utils.AssertUsesSdk( + manifest_node, + max_sdk_version=options.max_sdk_version, + fail_if_not_exist=True) + manifest_utils.AssertPackage(manifest_node, options.manifest_package) + + manifest_node.set('platformBuildVersionCode', version_code) + manifest_node.set('platformBuildVersionName', version_name) + + orig_package = manifest_node.get('package') + fixed_package = orig_package + if options.arsc_package_name: + manifest_node.set('package', options.arsc_package_name) + fixed_package = options.arsc_package_name + + if options.debuggable: + app_node.set('{%s}%s' % (manifest_utils.ANDROID_NAMESPACE, 'debuggable'), + 'true') + + if options.uses_split: + uses_split = ElementTree.SubElement(manifest_node, 'uses-split') + uses_split.set('{%s}name' % manifest_utils.ANDROID_NAMESPACE, + 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) + + 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', + # Set SDK versions in case they are not set in the Android manifest. + '--min-sdk-version', + options.min_sdk_version, + '--target-sdk-version', + options.target_sdk_version, + '--output-text-symbols', + build.r_txt_path, + ] + + for j in options.include_resources: + link_command += ['-I', j] + if options.version_code: + link_command += ['--version-code', options.version_code] + if options.version_name: + link_command += ['--version-name', options.version_name] + 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 options.no_xml_namespaces: + link_command.append('--no-xml-namespaces') + + if options.package_id: + link_command += [ + '--package-id', + hex(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 + ] + + # Creates a .zip with AndroidManifest.xml, resources.arsc, res/* + # Also creates R.txt + if options.use_resource_ids_path: + _CreateStableIdsFile(options.use_resource_ids_path, build.stable_ids_path, + fixed_manifest_package) + 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. + _CreateResourceInfoFile(path_info, build.info_path, + options.dependencies_res_zips) + logging.debug('Created .res.info file') + + exit_code = link_proc.wait() + logging.debug('Finished: aapt2 link') + 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 + ]) + + return desired_manifest_package_name + + +@contextlib.contextmanager +def _CreateStableIdsFile(in_path, out_path, package_name): + """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. + + Note: This will fail if the package ID of the resources in + |options.use_resource_ids_path| does not match the package ID of the + resources being linked. + """ + with open(in_path) as stable_ids_file: + with open(out_path, 'w') as output_ids_file: + output_stable_ids = re.sub( + r'^.*?:', + package_name + ':', + stable_ids_file.read(), + flags=re.MULTILINE) + output_ids_file.write(output_stable_ids) + + +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, extra_manifest=options.extra_verification_manifest) + with open(fixed_manifest) as f: + return manifest_utils.NormalizeManifest(f.read()) + + +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 + + 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, + options.extra_main_r_text_files) + build_utils.ZipDir(build.srcjar_path, build.srcjar_dir) + + # Sanity check that the created resources have the expected package ID. + logging.debug('Performing sanity check') + if options.package_id: + expected_id = options.package_id + elif options.shared_resources: + expected_id = 0 + else: + expected_id = 127 # == '0x7f'. + _, 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 package_id is not None and package_id != expected_id: + raise Exception( + 'Invalid package ID 0x%x (expected 0x%x)' % (package_id, expected_id)) + + logging.debug('Copying outputs') + _WriteOutputs(options, build) + + if options.depfile: + depfile_deps = (options.dependencies_res_zips + + options.dependencies_res_zip_overlays + + options.extra_main_r_text_files + options.include_resources) + build_utils.WriteDepfile(options.depfile, options.srcjar_out, depfile_deps) + + +if __name__ == '__main__': + main(sys.argv[1:]) |