"""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
import zip_helpers
_PNG_WEBP_EXCLUSION_PATTERN = re.compile('|'.join([
r'.*star_gray\.png',
r'.*\.9\.png',
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('--static-library-version',
help='Version code for static library.')
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(
'--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')
if options.static_library_version and (options.static_library_version !=
options.version_code):
assert options.static_library_version == options.version_code, (
f'static_library_version={options.static_library_version} must equal '
f'version_code={options.version_code}. Please verify the version code '
'map for this target is defined correctly.')
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
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))
config_dir = os.path.dirname(path2)
already_has_renamed_config = ignore_dirs.get(config_dir)
if already_has_renamed_config is None:
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)
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):
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
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)
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)
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 == '':
return predicate
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)
quality_args = ['-m', '6', '-q', '100', '-lossless']
webp_cache_path = os.path.join(
webp_cache_dir, '{}-{}-{}'.format(sha1_hash, cwebp_version,
''.join(quality_args)))
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:
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:
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',
'--dir',
dep_subdir,
'-o',
partial_path
]
build_utils.CheckOutput(
compile_command,
stderr_filter=lambda output: build_utils.FilterLines(
output, r'ignoring configuration .* for (styleable|attribute)'))
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)]
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.
"""
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)
wanted_locales = all_locales
if options.locale_allowlist:
wanted_locales = _ToAndroidLocales(options.locale_allowlist)
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)
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 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 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):
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.emit_ids_out:
link_command += ['--emit-ids', build.emit_ids_path]
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
link_command += ['-o', build.arsc_path]
logging.debug('Starting: aapt2 link')
link_proc = subprocess.Popen(link_command)
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')
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):
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
])
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
])
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)
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:
data = 'pkg:id/fake_resource_id = 0x7f010000\n'
data = re.sub(r'^.*?:', package_name + ':', data, flags=re.MULTILINE)
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.emit_ids_out, build.emit_ids_path),
(options.info_path, build.info_path),
]
for final, temp in possible_outputs:
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:
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)
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:
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
apk_package_name = manifest_package_name
if options.package_name and not options.arsc_package_name:
custom_root_package_name = options.package_name
grandparent_custom_package_name = options.r_java_root_package_name
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:])