1#!/usr/bin/env python3 2# 3# Copyright 2016 The Chromium Authors 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Processes an Android AAR file.""" 8 9import argparse 10import os 11import posixpath 12import re 13import shutil 14import sys 15from xml.etree import ElementTree 16import zipfile 17 18from util import build_utils 19import action_helpers # build_utils adds //build to sys.path. 20import gn_helpers 21 22 23_PROGUARD_TXT = 'proguard.txt' 24 25 26def _GetManifestPackage(doc): 27 """Returns the package specified in the manifest. 28 29 Args: 30 doc: an XML tree parsed by ElementTree 31 32 Returns: 33 String representing the package name. 34 """ 35 return doc.attrib['package'] 36 37 38def _IsManifestEmpty(doc): 39 """Decides whether the given manifest has merge-worthy elements. 40 41 E.g.: <activity>, <service>, etc. 42 43 Args: 44 doc: an XML tree parsed by ElementTree 45 46 Returns: 47 Whether the manifest has merge-worthy elements. 48 """ 49 for node in doc: 50 if node.tag == 'application': 51 if list(node): 52 return False 53 elif node.tag != 'uses-sdk': 54 return False 55 56 return True 57 58 59def _CreateInfo(aar_file, resource_exclusion_globs): 60 """Extracts and return .info data from an .aar file. 61 62 Args: 63 aar_file: Path to an input .aar file. 64 resource_exclusion_globs: List of globs that exclude res/ files. 65 66 Returns: 67 A dict containing .info data. 68 """ 69 data = {} 70 data['aidl'] = [] 71 data['assets'] = [] 72 data['resources'] = [] 73 data['subjars'] = [] 74 data['subjar_tuples'] = [] 75 data['has_classes_jar'] = False 76 data['has_proguard_flags'] = False 77 data['has_native_libraries'] = False 78 data['has_r_text_file'] = False 79 with zipfile.ZipFile(aar_file) as z: 80 manifest_xml = ElementTree.fromstring(z.read('AndroidManifest.xml')) 81 data['is_manifest_empty'] = _IsManifestEmpty(manifest_xml) 82 manifest_package = _GetManifestPackage(manifest_xml) 83 if manifest_package: 84 data['manifest_package'] = manifest_package 85 86 for name in z.namelist(): 87 if name.endswith('/'): 88 continue 89 if name.startswith('aidl/'): 90 data['aidl'].append(name) 91 elif name.startswith('res/'): 92 if not build_utils.MatchesGlob(name, resource_exclusion_globs): 93 data['resources'].append(name) 94 elif name.startswith('libs/') and name.endswith('.jar'): 95 label = posixpath.basename(name)[:-4] 96 label = re.sub(r'[^a-zA-Z0-9._]', '_', label) 97 data['subjars'].append(name) 98 data['subjar_tuples'].append([label, name]) 99 elif name.startswith('assets/'): 100 data['assets'].append(name) 101 elif name.startswith('jni/'): 102 data['has_native_libraries'] = True 103 if 'native_libraries' in data: 104 data['native_libraries'].append(name) 105 else: 106 data['native_libraries'] = [name] 107 elif name == 'classes.jar': 108 data['has_classes_jar'] = True 109 elif name == _PROGUARD_TXT: 110 data['has_proguard_flags'] = True 111 elif name == 'R.txt': 112 # Some AARs, e.g. gvr_controller_java, have empty R.txt. Such AARs 113 # have no resources as well. We treat empty R.txt as having no R.txt. 114 data['has_r_text_file'] = bool(z.read('R.txt').strip()) 115 116 return data 117 118 119def _PerformExtract(aar_file, output_dir, name_allowlist): 120 with build_utils.TempDir() as tmp_dir: 121 tmp_dir = os.path.join(tmp_dir, 'staging') 122 os.mkdir(tmp_dir) 123 build_utils.ExtractAll( 124 aar_file, path=tmp_dir, predicate=name_allowlist.__contains__) 125 # Write a breadcrumb so that SuperSize can attribute files back to the .aar. 126 with open(os.path.join(tmp_dir, 'source.info'), 'w') as f: 127 f.write('source={}\n'.format(aar_file)) 128 129 shutil.rmtree(output_dir, ignore_errors=True) 130 shutil.move(tmp_dir, output_dir) 131 132 133def _AddCommonArgs(parser): 134 parser.add_argument( 135 'aar_file', help='Path to the AAR file.', type=os.path.normpath) 136 parser.add_argument('--ignore-resources', 137 action='store_true', 138 help='Whether to skip extraction of res/') 139 parser.add_argument('--resource-exclusion-globs', 140 help='GN list of globs for res/ files to ignore') 141 142 143def main(): 144 parser = argparse.ArgumentParser(description=__doc__) 145 command_parsers = parser.add_subparsers(dest='command') 146 subp = command_parsers.add_parser( 147 'list', help='Output a GN scope describing the contents of the .aar.') 148 _AddCommonArgs(subp) 149 subp.add_argument('--output', help='Output file.', default='-') 150 151 subp = command_parsers.add_parser('extract', help='Extracts the .aar') 152 _AddCommonArgs(subp) 153 subp.add_argument( 154 '--output-dir', 155 help='Output directory for the extracted files.', 156 required=True, 157 type=os.path.normpath) 158 subp.add_argument( 159 '--assert-info-file', 160 help='Path to .info file. Asserts that it matches what ' 161 '"list" would output.', 162 type=argparse.FileType('r')) 163 164 args = parser.parse_args() 165 166 args.resource_exclusion_globs = action_helpers.parse_gn_list( 167 args.resource_exclusion_globs) 168 if args.ignore_resources: 169 args.resource_exclusion_globs.append('res/*') 170 171 aar_info = _CreateInfo(args.aar_file, args.resource_exclusion_globs) 172 formatted_info = """\ 173# Generated by //build/android/gyp/aar.py 174# To regenerate, use "update_android_aar_prebuilts = true" and run "gn gen". 175 176""" + gn_helpers.ToGNString(aar_info, pretty=True) 177 178 if args.command == 'extract': 179 if args.assert_info_file: 180 cached_info = args.assert_info_file.read() 181 if formatted_info != cached_info: 182 raise Exception('android_aar_prebuilt() cached .info file is ' 183 'out-of-date. Run gn gen with ' 184 'update_android_aar_prebuilts=true to update it.') 185 186 # Extract all files except for filtered res/ files. 187 with zipfile.ZipFile(args.aar_file) as zf: 188 names = {n for n in zf.namelist() if not n.startswith('res/')} 189 names.update(aar_info['resources']) 190 191 _PerformExtract(args.aar_file, args.output_dir, names) 192 193 elif args.command == 'list': 194 aar_output_present = args.output != '-' and os.path.isfile(args.output) 195 if aar_output_present: 196 # Some .info files are read-only, for examples the cipd-controlled ones 197 # under third_party/android_deps/repository. To deal with these, first 198 # that its content is correct, and if it is, exit without touching 199 # the file system. 200 file_info = open(args.output, 'r').read() 201 if file_info == formatted_info: 202 return 203 204 # Try to write the file. This may fail for read-only ones that were 205 # not updated. 206 try: 207 with open(args.output, 'w') as f: 208 f.write(formatted_info) 209 except IOError as e: 210 if not aar_output_present: 211 raise e 212 raise Exception('Could not update output file: %s\n' % args.output) from e 213 214 215if __name__ == '__main__': 216 sys.exit(main()) 217