"""Processes an Android AAR file."""
import argparse
import os
import posixpath
import re
import shutil
import sys
from xml.etree import ElementTree
import zipfile
from util import build_utils
import action_helpers
import gn_helpers
_PROGUARD_TXT = 'proguard.txt'
def _GetManifestPackage(doc):
"""Returns the package specified in the manifest.
Args:
doc: an XML tree parsed by ElementTree
Returns:
String representing the package name.
"""
return doc.attrib['package']
def _IsManifestEmpty(doc):
"""Decides whether the given manifest has merge-worthy elements.
E.g.: <activity>, <service>, etc.
Args:
doc: an XML tree parsed by ElementTree
Returns:
Whether the manifest has merge-worthy elements.
"""
for node in doc:
if node.tag == 'application':
if list(node):
return False
elif node.tag != 'uses-sdk':
return False
return True
def _CreateInfo(aar_file, resource_exclusion_globs):
"""Extracts and return .info data from an .aar file.
Args:
aar_file: Path to an input .aar file.
resource_exclusion_globs: List of globs that exclude res/ files.
Returns:
A dict containing .info data.
"""
data = {}
data['aidl'] = []
data['assets'] = []
data['resources'] = []
data['subjars'] = []
data['subjar_tuples'] = []
data['has_classes_jar'] = False
data['has_proguard_flags'] = False
data['has_native_libraries'] = False
data['has_r_text_file'] = False
with zipfile.ZipFile(aar_file) as z:
manifest_xml = ElementTree.fromstring(z.read('AndroidManifest.xml'))
data['is_manifest_empty'] = _IsManifestEmpty(manifest_xml)
manifest_package = _GetManifestPackage(manifest_xml)
if manifest_package:
data['manifest_package'] = manifest_package
for name in z.namelist():
if name.endswith('/'):
continue
if name.startswith('aidl/'):
data['aidl'].append(name)
elif name.startswith('res/'):
if not build_utils.MatchesGlob(name, resource_exclusion_globs):
data['resources'].append(name)
elif name.startswith('libs/') and name.endswith('.jar'):
label = posixpath.basename(name)[:-4]
label = re.sub(r'[^a-zA-Z0-9._]', '_', label)
data['subjars'].append(name)
data['subjar_tuples'].append([label, name])
elif name.startswith('assets/'):
data['assets'].append(name)
elif name.startswith('jni/'):
data['has_native_libraries'] = True
if 'native_libraries' in data:
data['native_libraries'].append(name)
else:
data['native_libraries'] = [name]
elif name == 'classes.jar':
data['has_classes_jar'] = True
elif name == _PROGUARD_TXT:
data['has_proguard_flags'] = True
elif name == 'R.txt':
data['has_r_text_file'] = bool(z.read('R.txt').strip())
return data
def _PerformExtract(aar_file, output_dir, name_allowlist):
with build_utils.TempDir() as tmp_dir:
tmp_dir = os.path.join(tmp_dir, 'staging')
os.mkdir(tmp_dir)
build_utils.ExtractAll(
aar_file, path=tmp_dir, predicate=name_allowlist.__contains__)
with open(os.path.join(tmp_dir, 'source.info'), 'w') as f:
f.write('source={}\n'.format(aar_file))
shutil.rmtree(output_dir, ignore_errors=True)
shutil.move(tmp_dir, output_dir)
def _AddCommonArgs(parser):
parser.add_argument(
'aar_file', help='Path to the AAR file.', type=os.path.normpath)
parser.add_argument('--ignore-resources',
action='store_true',
help='Whether to skip extraction of res/')
parser.add_argument('--resource-exclusion-globs',
help='GN list of globs for res/ files to ignore')
def main():
parser = argparse.ArgumentParser(description=__doc__)
command_parsers = parser.add_subparsers(dest='command')
subp = command_parsers.add_parser(
'list', help='Output a GN scope describing the contents of the .aar.')
_AddCommonArgs(subp)
subp.add_argument('--output', help='Output file.', default='-')
subp = command_parsers.add_parser('extract', help='Extracts the .aar')
_AddCommonArgs(subp)
subp.add_argument(
'--output-dir',
help='Output directory for the extracted files.',
required=True,
type=os.path.normpath)
subp.add_argument(
'--assert-info-file',
help='Path to .info file. Asserts that it matches what '
'"list" would output.',
type=argparse.FileType('r'))
args = parser.parse_args()
args.resource_exclusion_globs = action_helpers.parse_gn_list(
args.resource_exclusion_globs)
if args.ignore_resources:
args.resource_exclusion_globs.append('res/*')
aar_info = _CreateInfo(args.aar_file, args.resource_exclusion_globs)
formatted_info = """\
# Generated by //build/android/gyp/aar.py
# To regenerate, use "update_android_aar_prebuilts = true" and run "gn gen".
""" + gn_helpers.ToGNString(aar_info, pretty=True)
if args.command == 'extract':
if args.assert_info_file:
cached_info = args.assert_info_file.read()
if formatted_info != cached_info:
raise Exception('android_aar_prebuilt() cached .info file is '
'out-of-date. Run gn gen with '
'update_android_aar_prebuilts=true to update it.')
with zipfile.ZipFile(args.aar_file) as zf:
names = {n for n in zf.namelist() if not n.startswith('res/')}
names.update(aar_info['resources'])
_PerformExtract(args.aar_file, args.output_dir, names)
elif args.command == 'list':
aar_output_present = args.output != '-' and os.path.isfile(args.output)
if aar_output_present:
file_info = open(args.output, 'r').read()
if file_info == formatted_info:
return
try:
with open(args.output, 'w') as f:
f.write(formatted_info)
except IOError as e:
if not aar_output_present:
raise e
raise Exception('Could not update output file: %s\n' % args.output) from e
if __name__ == '__main__':
sys.exit(main())