import collections
import contextlib
import itertools
import os
import re
import shutil
import subprocess
import sys
import tempfile
import zipfile
from xml.etree import ElementTree
import util.build_utils as build_utils
_SOURCE_ROOT = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', '..', '..', '..'))
sys.path.insert(1, os.path.join(_SOURCE_ROOT, 'third_party'))
from jinja2 import Template
_CHROME_TO_ANDROID_LOCALE_MAP = {
'es-419': 'es-rUS',
'sr-Latn': 'b+sr+Latn',
'fil': 'tl',
'he': 'iw',
'id': 'in',
'yi': 'ji',
}
_ANDROID_TO_CHROMIUM_LANGUAGE_MAP = {
'tl': 'fil',
'iw': 'he',
'in': 'id',
'ji': 'yi',
'no': 'nb',
}
ALL_RESOURCE_TYPES = {
'anim', 'animator', 'array', 'attr', 'bool', 'color', 'dimen', 'drawable',
'font', 'fraction', 'id', 'integer', 'interpolator', 'layout', 'macro',
'menu', 'mipmap', 'plurals', 'raw', 'string', 'style', 'styleable',
'transition', 'xml'
}
AAPT_IGNORE_PATTERN = ':'.join([
'*OWNERS',
'DIR_METADATA',
'*.py',
'*.pyc',
'*~',
'.*',
'*.d.stamp',
'*.backup',
])
MULTIPLE_RES_MAGIC_STRING = b'magic'
def ToAndroidLocaleName(chromium_locale):
"""Convert a Chromium locale name into a corresponding Android one."""
android_locale = _CHROME_TO_ANDROID_LOCALE_MAP.get(chromium_locale)
if android_locale:
return android_locale
lang, _, region = chromium_locale.partition('-')
if not region:
return lang
lang = _CHROME_TO_ANDROID_LOCALE_MAP.get(lang, lang)
return '%s-r%s' % (lang, region)
_RE_ANDROID_LOCALE_QUALIFIER_1 = re.compile(r'^([a-z]{2,3})(\-r([A-Z]+))?$')
_RE_ANDROID_LOCALE_QUALIFIER_2 = re.compile(r'^b\+([a-z]{2,3})(\+.+)?$')
def ToChromiumLocaleName(android_locale):
"""Convert an Android locale name into a Chromium one."""
lang = None
region = None
script = None
m = _RE_ANDROID_LOCALE_QUALIFIER_1.match(android_locale)
if m:
lang = m.group(1)
if m.group(2):
region = m.group(3)
elif _RE_ANDROID_LOCALE_QUALIFIER_2.match(android_locale):
tags = android_locale.split('+')
lang = tags[1]
optional_tags = iter(tags[2:])
next_tag = next(optional_tags, None)
if next_tag and len(next_tag) == 4:
script = next_tag
next_tag = next(optional_tags, None)
if next_tag and len(next_tag) < 4:
region = next_tag
if not lang:
return None
if lang == 'es' and region == 'US':
return 'es-419'
lang = _ANDROID_TO_CHROMIUM_LANGUAGE_MAP.get(lang, lang)
if script:
lang = '%s-%s' % (lang, script)
if not region:
return lang
return '%s-%s' % (lang, region)
def IsAndroidLocaleQualifier(string):
"""Returns true if |string| is a valid Android resource locale qualifier."""
return (_RE_ANDROID_LOCALE_QUALIFIER_1.match(string)
or _RE_ANDROID_LOCALE_QUALIFIER_2.match(string))
def FindLocaleInStringResourceFilePath(file_path):
"""Return Android locale name of a string resource file path.
Args:
file_path: A file path.
Returns:
If |file_path| is of the format '.../values-<locale>/<name>.xml', return
the value of <locale> (and Android locale qualifier). Otherwise return None.
"""
if not file_path.endswith('.xml'):
return None
prefix = 'values-'
dir_name = os.path.basename(os.path.dirname(file_path))
if not dir_name.startswith(prefix):
return None
qualifier = dir_name[len(prefix):]
return qualifier if IsAndroidLocaleQualifier(qualifier) else None
def ToAndroidLocaleList(locale_list):
"""Convert a list of Chromium locales into the corresponding Android list."""
return sorted(ToAndroidLocaleName(locale) for locale in locale_list)
_TextSymbolEntry = collections.namedtuple('RTextEntry',
('java_type', 'resource_type', 'name', 'value'))
def _GenerateGlobs(pattern):
return pattern.replace('!', '').split(':')
def DeduceResourceDirsFromFileList(resource_files):
"""Return a list of resource directories from a list of resource files."""
resource_dirs = []
for resource_path in resource_files:
res_dir = os.path.dirname(os.path.dirname(resource_path))
if res_dir not in resource_dirs:
resource_dirs.append(res_dir)
for a, b in itertools.permutations(resource_dirs, 2):
if not os.path.relpath(a, b).startswith('..'):
bad_sources = (s for s in resource_files
if os.path.dirname(os.path.dirname(s)) == b)
msg = """\
Resource(s) found that are not in a proper directory structure:
{}
All resource files must follow a structure of "$ROOT/$SUBDIR/$FILE"."""
raise Exception(msg.format('\n '.join(bad_sources)))
return resource_dirs
def IterResourceFilesInDirectories(directories,
ignore_pattern=AAPT_IGNORE_PATTERN):
globs = _GenerateGlobs(ignore_pattern)
for d in directories:
for root, _, files in os.walk(d):
for f in files:
archive_path = f
parent_dir = os.path.relpath(root, d)
if parent_dir != '.':
archive_path = os.path.join(parent_dir, f)
path = os.path.join(root, f)
if build_utils.MatchesGlob(archive_path, globs):
continue
yield path, archive_path
class ResourceInfoFile:
"""Helper for building up .res.info files."""
def __init__(self):
self._entries = {}
self._renames = []
self._add_mapping_was_called = False
def AddMapping(self, archive_path, source_path):
"""Adds a single |archive_path| -> |source_path| entry."""
self._add_mapping_was_called = True
if archive_path.startswith('values'):
return
source_path = os.path.normpath(source_path)
new_value = self._entries.setdefault(archive_path, source_path)
if new_value != source_path:
raise Exception('Duplicate AddMapping for "{}". old={} new={}'.format(
archive_path, new_value, source_path))
def RegisterRename(self, old_archive_path, new_archive_path):
"""Records an archive_path rename.
|old_archive_path| does not need to currently exist in the mappings. Renames
are buffered and replayed only when Write() is called.
"""
if not old_archive_path.startswith('values'):
self._renames.append((old_archive_path, new_archive_path))
def MergeInfoFile(self, info_file_path):
"""Merges the mappings from |info_file_path| into this object.
Any existing entries are overridden.
"""
assert not self._add_mapping_was_called
with open(info_file_path) as f:
self._entries.update(l.rstrip().split('\t') for l in f)
def _ApplyRenames(self):
applied_renames = set()
ret = self._entries
for rename_tup in self._renames:
if rename_tup in applied_renames:
continue
applied_renames.add(rename_tup)
old_archive_path, new_archive_path = rename_tup
ret[new_archive_path] = ret[old_archive_path]
del ret[old_archive_path]
self._entries = None
self._renames = None
return ret
def Write(self, info_file_path):
"""Applies renames and writes out the file.
No other methods may be called after this.
"""
entries = self._ApplyRenames()
lines = []
for archive_path, source_path in entries.items():
lines.append('{}\t{}\n'.format(archive_path, source_path))
with open(info_file_path, 'w') as info_file:
info_file.writelines(sorted(lines))
def _ParseTextSymbolsFile(path, fix_package_ids=False):
"""Given an R.txt file, returns a list of _TextSymbolEntry.
Args:
path: Input file path.
fix_package_ids: if True, 0x00 and 0x02 package IDs read from the file
will be fixed to 0x7f.
Returns:
A list of _TextSymbolEntry instances.
Raises:
Exception: An unexpected line was detected in the input.
"""
ret = []
with open(path) as f:
for line in f:
m = re.match(r'(int(?:\[\])?) (\w+) (\w+) (.+)$', line)
if not m:
raise Exception('Unexpected line in R.txt: %s' % line)
java_type, resource_type, name, value = m.groups()
if fix_package_ids:
value = _FixPackageIds(value)
ret.append(_TextSymbolEntry(java_type, resource_type, name, value))
return ret
def _FixPackageIds(resource_value):
return resource_value.replace('0x00', '0x7f')
def ResolveStyleableReferences(r_txt_path):
entries = _ParseTextSymbolsFile(r_txt_path)
lookup_table = {(e.resource_type, e.name): e.value for e in entries}
sb = []
with open(r_txt_path, encoding='utf8') as f:
for l in f:
if l.startswith('int[] styleable'):
brace_start = l.index('{') + 2
brace_end = l.index('}') - 1
values = [x for x in l[brace_start:brace_end].split(', ') if x]
new_values = []
for v in values:
try:
if not v.startswith('0x'):
resource_type, name = v.split('.')[-2:]
new_values.append(lookup_table[(resource_type, name)])
else:
new_values.append(v)
except:
logging.warning('Failed line: %r %r', l, v)
raise
l = l[:brace_start] + ', '.join(new_values) + l[brace_end:]
sb.append(l)
with open(r_txt_path, 'w', encoding='utf8') as f:
f.writelines(sb)
def _GetRTxtResourceNames(r_txt_path):
"""Parse an R.txt file and extract the set of resource names from it."""
return {entry.name for entry in _ParseTextSymbolsFile(r_txt_path)}
def GetRTxtStringResourceNames(r_txt_path):
"""Parse an R.txt file and the list of its string resource names."""
return sorted({
entry.name
for entry in _ParseTextSymbolsFile(r_txt_path)
if entry.resource_type == 'string'
})
def GenerateStringResourcesAllowList(module_r_txt_path, allowlist_r_txt_path):
"""Generate a allowlist of string resource IDs.
Args:
module_r_txt_path: Input base module R.txt path.
allowlist_r_txt_path: Input allowlist R.txt path.
Returns:
A dictionary mapping numerical resource IDs to the corresponding
string resource names. The ID values are taken from string resources in
|module_r_txt_path| that are also listed by name in |allowlist_r_txt_path|.
"""
allowlisted_names = {
entry.name
for entry in _ParseTextSymbolsFile(allowlist_r_txt_path)
if entry.resource_type == 'string'
}
return {
int(entry.value, 0): entry.name
for entry in _ParseTextSymbolsFile(module_r_txt_path)
if entry.resource_type == 'string' and entry.name in allowlisted_names
}
class RJavaBuildOptions:
"""A class used to model the various ways to build an R.java file.
This is used to control which resource ID variables will be final or
non-final, and whether an onResourcesLoaded() method will be generated
to adjust the non-final ones, when the corresponding library is loaded
at runtime.
Note that by default, all resources are final, and there is no
method generated, which corresponds to calling ExportNoResources().
"""
def __init__(self):
self.has_constant_ids = True
self.resources_allowlist = None
self.has_on_resources_loaded = False
self.export_const_styleable = False
self.final_package_id = None
self.fake_on_resources_loaded = False
def ExportNoResources(self):
"""Make all resource IDs final, and don't generate a method."""
self.has_constant_ids = True
self.resources_allowlist = None
self.has_on_resources_loaded = False
self.export_const_styleable = False
def ExportAllResources(self):
"""Make all resource IDs non-final in the R.java file."""
self.has_constant_ids = False
self.resources_allowlist = None
def ExportSomeResources(self, r_txt_file_path):
"""Only select specific resource IDs to be non-final.
Args:
r_txt_file_path: The path to an R.txt file. All resources named
int it will be non-final in the generated R.java file, all others
will be final.
"""
self.has_constant_ids = True
self.resources_allowlist = _GetRTxtResourceNames(r_txt_file_path)
def ExportAllStyleables(self):
"""Make all styleable constants non-final, even non-resources ones.
Resources that are styleable but not of int[] type are not actually
resource IDs but constants. By default they are always final. Call this
method to make them non-final anyway in the final R.java file.
"""
self.export_const_styleable = True
def GenerateOnResourcesLoaded(self, fake=False):
"""Generate an onResourcesLoaded() method.
This Java method will be called at runtime by the framework when
the corresponding library (which includes the R.java source file)
will be loaded at runtime. This corresponds to the --shared-resources
or --app-as-shared-lib flags of 'aapt package'.
if |fake|, then the method will be empty bodied to compile faster. This
useful for dummy R.java files that will eventually be replaced by real
ones.
"""
self.has_on_resources_loaded = True
self.fake_on_resources_loaded = fake
def SetFinalPackageId(self, package_id):
"""Sets a package ID to be used for resources marked final."""
self.final_package_id = package_id
def _MaybeRewriteRTxtPackageIds(self, r_txt_path):
"""Rewrites package IDs in the R.txt file if necessary.
If SetFinalPackageId() was called, some of the resource IDs may have had
their package ID changed. This function rewrites the R.txt file to match
those changes.
"""
if self.final_package_id is None:
return
entries = _ParseTextSymbolsFile(r_txt_path)
with open(r_txt_path, 'w') as f:
for entry in entries:
value = entry.value
if self._IsResourceFinal(entry):
value = re.sub(r'0x(?:00|7f)',
'0x{:02x}'.format(self.final_package_id), value)
f.write('{} {} {} {}\n'.format(entry.java_type, entry.resource_type,
entry.name, value))
def _IsResourceFinal(self, entry):
"""Determines whether a resource should be final or not.
Args:
entry: A _TextSymbolEntry instance.
Returns:
True iff the corresponding entry should be final.
"""
if entry.resource_type == 'styleable' and entry.java_type != 'int[]':
return not self.export_const_styleable
if not self.has_constant_ids:
return False
if not self.resources_allowlist:
return True
return entry.name not in self.resources_allowlist
def CreateRJavaFiles(srcjar_dir,
package,
main_r_txt_file,
extra_res_packages,
rjava_build_options,
srcjar_out,
custom_root_package_name=None,
grandparent_custom_package_name=None,
ignore_mismatched_values=False):
"""Create all R.java files for a set of packages and R.txt files.
Args:
srcjar_dir: The top-level output directory for the generated files.
package: Package name for R java source files which will inherit
from the root R java file.
main_r_txt_file: The main R.txt file containing the valid values
of _all_ resource IDs.
extra_res_packages: A list of extra package names.
rjava_build_options: An RJavaBuildOptions instance that controls how
exactly the R.java file is generated.
srcjar_out: Path of desired output srcjar.
custom_root_package_name: Custom package name for module root R.java file,
(eg. vr for gen.vr package).
grandparent_custom_package_name: Custom root package name for the root
R.java file to inherit from. DFM root R.java files will have "base"
as the grandparent_custom_package_name. The format of this package name
is identical to custom_root_package_name.
(eg. for vr grandparent_custom_package_name would be "base")
ignore_mismatched_values: If True, ignores if a resource appears multiple
times with different entry values (useful when all the values are
dummy anyways).
Raises:
Exception if a package name appears several times in |extra_res_packages|
"""
rjava_build_options._MaybeRewriteRTxtPackageIds(main_r_txt_file)
packages = list(extra_res_packages)
if package and package not in packages:
packages.append(package)
all_resources = {}
all_resources_by_type = collections.defaultdict(list)
main_r_text_files = [main_r_txt_file]
for r_txt_file in main_r_text_files:
for entry in _ParseTextSymbolsFile(r_txt_file, fix_package_ids=True):
entry_key = (entry.resource_type, entry.name)
if entry_key in all_resources:
if not ignore_mismatched_values:
assert entry == all_resources[entry_key], (
'Input R.txt %s provided a duplicate resource with a different '
'entry value. Got %s, expected %s.' %
(r_txt_file, entry, all_resources[entry_key]))
else:
all_resources[entry_key] = entry
all_resources_by_type[entry.resource_type].append(entry)
assert entry.resource_type in ALL_RESOURCE_TYPES, (
'Unknown resource type: %s, add to ALL_RESOURCE_TYPES!' %
entry.resource_type)
if custom_root_package_name:
root_r_java_package = GetCustomPackagePath(custom_root_package_name)
else:
root_r_java_package = re.sub('[^\w\.]', '', srcjar_out.replace('/', '._'))
root_r_java_dir = os.path.join(srcjar_dir, *root_r_java_package.split('.'))
build_utils.MakeDirectory(root_r_java_dir)
root_r_java_path = os.path.join(root_r_java_dir, 'R.java')
root_java_file_contents = _RenderRootRJavaSource(
root_r_java_package, all_resources_by_type, rjava_build_options,
grandparent_custom_package_name)
with open(root_r_java_path, 'w') as f:
f.write(root_java_file_contents)
for p in packages:
_CreateRJavaSourceFile(srcjar_dir, p, root_r_java_package,
rjava_build_options)
def _CreateRJavaSourceFile(srcjar_dir, package, root_r_java_package,
rjava_build_options):
"""Generates an R.java source file."""
package_r_java_dir = os.path.join(srcjar_dir, *package.split('.'))
build_utils.MakeDirectory(package_r_java_dir)
package_r_java_path = os.path.join(package_r_java_dir, 'R.java')
java_file_contents = _RenderRJavaSource(package, root_r_java_package,
rjava_build_options)
with open(package_r_java_path, 'w') as f:
f.write(java_file_contents)
def _GetNonSystemIndex(entry):
"""Get the index of the first application resource ID within a resource
array."""
res_ids = re.findall(r'0x[0-9a-f]{8}', entry.value)
for i, res_id in enumerate(res_ids):
if res_id.startswith('0x7f'):
return i
return len(res_ids)
def _RenderRJavaSource(package, root_r_java_package, rjava_build_options):
"""Generates the contents of a R.java file."""
template = Template(
"""/* AUTO-GENERATED FILE. DO NOT MODIFY. */
package {{ package }};
public final class R {
{% for resource_type in resource_types %}
public static final class {{ resource_type }} extends
{{ root_package }}.R.{{ resource_type }} {}
{% endfor %}
{% if has_on_resources_loaded %}
public static void onResourcesLoaded(int packageId) {
{{ root_package }}.R.onResourcesLoaded(packageId);
}
{% endif %}
}
""",
trim_blocks=True,
lstrip_blocks=True)
return template.render(
package=package,
resource_types=sorted(ALL_RESOURCE_TYPES),
root_package=root_r_java_package,
has_on_resources_loaded=rjava_build_options.has_on_resources_loaded)
def GetCustomPackagePath(package_name):
return 'gen.' + package_name + '_module'
def _RenderRootRJavaSource(package, all_resources_by_type, rjava_build_options,
grandparent_custom_package_name):
"""Render an R.java source file. See _CreateRJaveSourceFile for args info."""
final_resources_by_type = collections.defaultdict(list)
non_final_resources_by_type = collections.defaultdict(list)
for res_type, resources in all_resources_by_type.items():
for entry in resources:
if rjava_build_options._IsResourceFinal(entry):
final_resources_by_type[res_type].append(entry)
else:
non_final_resources_by_type[res_type].append(entry)
extends_string = ''
dep_path = ''
if grandparent_custom_package_name:
extends_string = 'extends {{ parent_path }}.R.{{ resource_type }} '
dep_path = GetCustomPackagePath(grandparent_custom_package_name)
template = Template("""/* AUTO-GENERATED FILE. DO NOT MODIFY. */
package {{ package }};
public final class R {
{% for resource_type in resource_types %}
public static class {{ resource_type }} """ + extends_string + """ {
{% for e in final_resources[resource_type] %}
public static {{ e.java_type }} {{ e.name }} = {{ e.value }};
{% endfor %}
{% for e in non_final_resources[resource_type] %}
{% if e.value != '0' %}
public static {{ e.java_type }} {{ e.name }} = {{ e.value }};
{% else %}
public static {{ e.java_type }} {{ e.name }};
{% endif %}
{% endfor %}
}
{% endfor %}
{% if has_on_resources_loaded %}
{% if fake_on_resources_loaded %}
public static void onResourcesLoaded(int packageId) {
}
{% else %}
private static boolean sResourcesDidLoad;
private static void patchArray(
int[] arr, int startIndex, int packageIdTransform) {
for (int i = startIndex; i < arr.length; ++i) {
arr[i] ^= packageIdTransform;
}
}
public static void onResourcesLoaded(int packageId) {
if (sResourcesDidLoad) {
return;
}
sResourcesDidLoad = true;
int packageIdTransform = (packageId ^ 0x7f) << 24;
{# aapt2 makes int[] resources refer to other resources by reference
rather than by value. Thus, need to transform the int[] resources
first, before the referenced resources are transformed in order to
ensure the transform applies exactly once.
See https://crbug.com/1237059 for context.
#}
{% for resource_type in resource_types %}
{% for e in non_final_resources[resource_type] %}
{% if e.java_type == 'int[]' %}
patchArray({{ e.resource_type }}.{{ e.name }}, {{ startIndex(e) }}, \
packageIdTransform);
{% endif %}
{% endfor %}
{% endfor %}
{% for resource_type in resource_types %}
onResourcesLoaded{{ resource_type|title }}(packageIdTransform);
{% endfor %}
}
{% for res_type in resource_types %}
private static void onResourcesLoaded{{ res_type|title }} (
int packageIdTransform) {
{% for e in non_final_resources[res_type] %}
{% if res_type != 'styleable' and e.java_type != 'int[]' %}
{{ e.resource_type }}.{{ e.name }} ^= packageIdTransform;
{% endif %}
{% endfor %}
}
{% endfor %}
{% endif %}
{% endif %}
}
""",
trim_blocks=True,
lstrip_blocks=True)
return template.render(
package=package,
resource_types=sorted(ALL_RESOURCE_TYPES),
has_on_resources_loaded=rjava_build_options.has_on_resources_loaded,
fake_on_resources_loaded=rjava_build_options.fake_on_resources_loaded,
final_resources=final_resources_by_type,
non_final_resources=non_final_resources_by_type,
startIndex=_GetNonSystemIndex,
parent_path=dep_path)
def ExtractBinaryManifestValues(aapt2_path, apk_path):
"""Returns (version_code, version_name, package_name) for the given apk."""
output = subprocess.check_output([
aapt2_path, 'dump', 'xmltree', apk_path, '--file', 'AndroidManifest.xml'
]).decode('utf-8')
version_code = re.search(r'versionCode.*?=(\d*)', output).group(1)
version_name = re.search(r'versionName.*?="(.*?)"', output).group(1)
package_name = re.search(r'package.*?="(.*?)"', output).group(1)
return version_code, version_name, package_name
def ExtractArscPackage(aapt2_path, apk_path):
"""Returns (package_name, package_id) of resources.arsc from apk_path.
When the apk does not have any entries in its resources file, in recent aapt2
versions it will not contain a "Package" line. The package is not even in the
actual resources.arsc/resources.pb file (which itself is mostly empty). Thus
return (None, None) when dump succeeds and there are no errors to indicate
that the package name does not exist in the resources file.
"""
proc = subprocess.Popen([aapt2_path, 'dump', 'resources', apk_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
for line in proc.stdout:
line = line.decode('utf-8')
if line.startswith('Package'):
proc.kill()
parts = line.split()
package_name = parts[1].split('=')[1]
package_id = parts[2][3:]
return package_name, int(package_id, 16)
stderr_output = proc.stderr.read().decode('utf-8')
if stderr_output:
sys.stderr.write(stderr_output)
raise Exception('Failed to find arsc package name')
return None, None
def _RenameSubdirsWithPrefix(dir_path, prefix):
subdirs = [
d for d in os.listdir(dir_path)
if os.path.isdir(os.path.join(dir_path, d))
]
renamed_subdirs = []
for d in subdirs:
old_path = os.path.join(dir_path, d)
new_path = os.path.join(dir_path, '{}_{}'.format(prefix, d))
renamed_subdirs.append(new_path)
os.rename(old_path, new_path)
return renamed_subdirs
def _HasMultipleResDirs(zip_path):
"""Checks for magic comment set by prepare_resources.py
Returns: True iff the zipfile has the magic comment that means it contains
multiple res/ dirs inside instead of just contents of a single res/ dir
(without a wrapping res/).
"""
with zipfile.ZipFile(zip_path) as z:
return z.comment == MULTIPLE_RES_MAGIC_STRING
def ExtractDeps(dep_zips, deps_dir):
"""Extract a list of resource dependency zip files.
Args:
dep_zips: A list of zip file paths, each one will be extracted to
a subdirectory of |deps_dir|, named after the zip file's path (e.g.
'/some/path/foo.zip' -> '{deps_dir}/some_path_foo/').
deps_dir: Top-level extraction directory.
Returns:
The list of all sub-directory paths, relative to |deps_dir|.
Raises:
Exception: If a sub-directory already exists with the same name before
extraction.
"""
dep_subdirs = []
for z in dep_zips:
subdirname = z.replace(os.path.sep, '_')
subdir = os.path.join(deps_dir, subdirname)
if os.path.exists(subdir):
raise Exception('Resource zip name conflict: ' + subdirname)
build_utils.ExtractAll(z, path=subdir)
if _HasMultipleResDirs(z):
subdir_subdirs = _RenameSubdirsWithPrefix(subdir, subdirname)
dep_subdirs.extend(subdir_subdirs)
else:
dep_subdirs.append(subdir)
return dep_subdirs
class _ResourceBuildContext:
"""A temporary directory for packaging and compiling Android resources.
Args:
temp_dir: Optional root build directory path. If None, a temporary
directory will be created, and removed in Close().
"""
def __init__(self, temp_dir=None, keep_files=False):
"""Initialized the context."""
if temp_dir:
self.temp_dir = temp_dir
os.makedirs(temp_dir)
else:
self.temp_dir = tempfile.mkdtemp()
self.remove_on_exit = not keep_files
self.deps_dir = os.path.join(self.temp_dir, 'deps')
os.mkdir(self.deps_dir)
self.gen_dir = os.path.join(self.temp_dir, 'gen')
os.mkdir(self.gen_dir)
self.srcjar_dir = os.path.join(self.temp_dir, 'java')
os.mkdir(self.srcjar_dir)
self.r_txt_path = os.path.join(self.gen_dir, 'R.txt')
self.srcjar_path = os.path.join(self.temp_dir, 'R.srcjar')
self.info_path = os.path.join(self.temp_dir, 'size.info')
self.stable_ids_path = os.path.join(self.temp_dir, 'in_ids.txt')
self.emit_ids_path = os.path.join(self.temp_dir, 'out_ids.txt')
self.proguard_path = os.path.join(self.temp_dir, 'keeps.flags')
self.proguard_main_dex_path = os.path.join(self.temp_dir, 'maindex.flags')
self.arsc_path = os.path.join(self.temp_dir, 'out.ap_')
self.proto_path = os.path.join(self.temp_dir, 'out.proto.ap_')
self.optimized_arsc_path = os.path.join(self.temp_dir, 'out.opt.ap_')
self.optimized_proto_path = os.path.join(self.temp_dir, 'out.opt.proto.ap_')
def Close(self):
"""Close the context and destroy all temporary files."""
if self.remove_on_exit:
shutil.rmtree(self.temp_dir)
@contextlib.contextmanager
def BuildContext(temp_dir=None, keep_files=False):
"""Generator for a _ResourceBuildContext instance."""
context = None
try:
context = _ResourceBuildContext(temp_dir, keep_files)
yield context
finally:
if context:
context.Close()
def ParseAndroidResourceStringsFromXml(xml_data):
"""Parse and Android xml resource file and extract strings from it.
Args:
xml_data: XML file data.
Returns:
A (dict, namespaces) tuple, where |dict| maps string names to their UTF-8
encoded value, and |namespaces| is a dictionary mapping prefixes to URLs
corresponding to namespaces declared in the <resources> element.
"""
result = {}
m = re.search('<resources([^>]*)>', xml_data, re.MULTILINE)
if not m:
raise Exception('<resources> start tag expected: ' + xml_data)
input_data = xml_data[m.end():]
resource_attrs = m.group(1)
re_namespace = re.compile('\s*(xmlns:(\w+)="([^"]+)")')
namespaces = {}
while resource_attrs:
m = re_namespace.match(resource_attrs)
if not m:
break
namespaces[m.group(2)] = m.group(3)
resource_attrs = resource_attrs[m.end(1):]
re_string_element_start = re.compile('<string ([^>]* )?name="([^">]+)"[^>]*>')
re_string_element_end = re.compile('</string>')
while input_data:
m = re_string_element_start.search(input_data)
if not m:
break
name = m.group(2)
input_data = input_data[m.end():]
m2 = re_string_element_end.search(input_data)
if not m2:
raise Exception('Expected closing string tag: ' + input_data)
text = input_data[:m2.start()]
input_data = input_data[m2.end():]
if len(text) != 0 and text[0] == '"' and text[-1] == '"':
text = text[1:-1]
result[name] = text
return result, namespaces
def GenerateAndroidResourceStringsXml(names_to_utf8_text, namespaces=None):
"""Generate an XML text corresponding to an Android resource strings map.
Args:
names_to_text: A dictionary mapping resource names to localized
text (encoded as UTF-8).
namespaces: A map of namespace prefix to URL.
Returns:
New non-Unicode string containing an XML data structure describing the
input as an Android resource .xml file.
"""
result = '<?xml version="1.0" encoding="utf-8"?>\n'
result += '<resources'
if namespaces:
for prefix, url in sorted(namespaces.items()):
result += ' xmlns:%s="%s"' % (prefix, url)
result += '>\n'
if not names_to_utf8_text:
result += '<!-- this file intentionally empty -->\n'
else:
for name, utf8_text in sorted(names_to_utf8_text.items()):
result += '<string name="%s">"%s"</string>\n' % (name, utf8_text)
result += '</resources>\n'
return result.encode('utf8')
def FilterAndroidResourceStringsXml(xml_file_path, string_predicate):
"""Remove unwanted localized strings from an Android resource .xml file.
This function takes a |string_predicate| callable object that will
receive a resource string name, and should return True iff the
corresponding <string> element should be kept in the file.
Args:
xml_file_path: Android resource strings xml file path.
string_predicate: A predicate function which will receive the string name
and shal
"""
with open(xml_file_path) as f:
xml_data = f.read()
strings_map, namespaces = ParseAndroidResourceStringsFromXml(xml_data)
string_deletion = False
for name in list(strings_map.keys()):
if not string_predicate(name):
del strings_map[name]
string_deletion = True
if string_deletion:
new_xml_data = GenerateAndroidResourceStringsXml(strings_map, namespaces)
with open(xml_file_path, 'wb') as f:
f.write(new_xml_data)