aar.py (7341B)
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 9 import argparse 10 import os 11 import posixpath 12 import re 13 import shutil 14 import sys 15 from xml.etree import ElementTree 16 import zipfile 17 18 from util import build_utils 19 import action_helpers # build_utils adds //build to sys.path. 20 import gn_helpers 21 22 23 _PROGUARD_TXT = 'proguard.txt' 24 25 26 def _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 38 def _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 59 def _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 prefab_headers = [] 80 prefab_include_dirs = [] 81 with zipfile.ZipFile(aar_file) as z: 82 manifest_xml = ElementTree.fromstring(z.read('AndroidManifest.xml')) 83 data['is_manifest_empty'] = _IsManifestEmpty(manifest_xml) 84 manifest_package = _GetManifestPackage(manifest_xml) 85 if manifest_package: 86 data['manifest_package'] = manifest_package 87 88 for name in z.namelist(): 89 if name.endswith('/'): 90 continue 91 if name.startswith('aidl/'): 92 data['aidl'].append(name) 93 elif name.startswith('res/'): 94 if not build_utils.MatchesGlob(name, resource_exclusion_globs): 95 data['resources'].append(name) 96 elif name.startswith('libs/') and name.endswith('.jar'): 97 label = posixpath.basename(name)[:-4] 98 label = re.sub(r'[^a-zA-Z0-9._]', '_', label) 99 data['subjars'].append(name) 100 data['subjar_tuples'].append([label, name]) 101 elif name.startswith('assets/'): 102 data['assets'].append(name) 103 elif name.startswith('jni/'): 104 data['has_native_libraries'] = True 105 if 'native_libraries' in data: 106 data['native_libraries'].append(name) 107 else: 108 data['native_libraries'] = [name] 109 elif name == 'classes.jar': 110 data['has_classes_jar'] = True 111 elif name == _PROGUARD_TXT: 112 data['has_proguard_flags'] = True 113 elif name == 'R.txt': 114 # Some AARs, e.g. gvr_controller_java, have empty R.txt. Such AARs 115 # have no resources as well. We treat empty R.txt as having no R.txt. 116 data['has_r_text_file'] = bool(z.read('R.txt').strip()) 117 elif name.startswith('prefab/modules') and '/include/' in name: 118 prefab_headers.append(name) 119 subdir = name[:name.index('/include/')] + '/include' 120 if subdir not in prefab_include_dirs: 121 prefab_include_dirs.append(subdir) 122 123 if prefab_include_dirs: 124 data['prefab_headers'] = prefab_headers 125 data['prefab_include_dirs'] = prefab_include_dirs 126 return data 127 128 129 def _PerformExtract(aar_file, output_dir, name_allowlist): 130 with build_utils.TempDir() as tmp_dir: 131 tmp_dir = os.path.join(tmp_dir, 'staging') 132 os.mkdir(tmp_dir) 133 build_utils.ExtractAll( 134 aar_file, path=tmp_dir, predicate=name_allowlist.__contains__) 135 # Write a breadcrumb so that SuperSize can attribute files back to the .aar. 136 with open(os.path.join(tmp_dir, 'source.info'), 'w') as f: 137 f.write('source={}\n'.format(aar_file)) 138 139 shutil.rmtree(output_dir, ignore_errors=True) 140 shutil.move(tmp_dir, output_dir) 141 142 143 def _AddCommonArgs(parser): 144 parser.add_argument( 145 'aar_file', help='Path to the AAR file.', type=os.path.normpath) 146 parser.add_argument('--ignore-resources', 147 action='store_true', 148 help='Whether to skip extraction of res/') 149 parser.add_argument('--resource-exclusion-globs', 150 help='GN list of globs for res/ files to ignore') 151 152 153 def main(): 154 parser = argparse.ArgumentParser(description=__doc__) 155 command_parsers = parser.add_subparsers(dest='command') 156 subp = command_parsers.add_parser( 157 'list', help='Output a GN scope describing the contents of the .aar.') 158 _AddCommonArgs(subp) 159 subp.add_argument('--output', help='Output file.', default='-') 160 161 subp = command_parsers.add_parser('extract', help='Extracts the .aar') 162 _AddCommonArgs(subp) 163 subp.add_argument( 164 '--output-dir', 165 help='Output directory for the extracted files.', 166 required=True, 167 type=os.path.normpath) 168 subp.add_argument( 169 '--assert-info-file', 170 help='Path to .info file. Asserts that it matches what ' 171 '"list" would output.', 172 type=argparse.FileType('r')) 173 174 args = parser.parse_args() 175 176 args.resource_exclusion_globs = action_helpers.parse_gn_list( 177 args.resource_exclusion_globs) 178 if args.ignore_resources: 179 args.resource_exclusion_globs.append('res/*') 180 181 aar_info = _CreateInfo(args.aar_file, args.resource_exclusion_globs) 182 formatted_info = """\ 183 # Generated by //build/android/gyp/aar.py 184 # To regenerate, use "update_android_aar_prebuilts = true" and run "gn gen". 185 186 """ + gn_helpers.ToGNString(aar_info, pretty=True) 187 188 if args.command == 'extract': 189 if args.assert_info_file: 190 cached_info = args.assert_info_file.read() 191 if formatted_info != cached_info: 192 raise Exception('android_aar_prebuilt() cached .info file is ' 193 'out-of-date. Run gn gen with ' 194 'update_android_aar_prebuilts=true to update it.') 195 196 # Extract all files except for filtered res/ files. 197 with zipfile.ZipFile(args.aar_file) as zf: 198 names = {n for n in zf.namelist() if not n.startswith('res/')} 199 names.update(aar_info['resources']) 200 201 _PerformExtract(args.aar_file, args.output_dir, names) 202 203 elif args.command == 'list': 204 aar_output_present = args.output != '-' and os.path.isfile(args.output) 205 if aar_output_present: 206 # Some .info files are read-only, for examples the cipd-controlled ones 207 # under third_party/android_deps/repository. To deal with these, first 208 # that its content is correct, and if it is, exit without touching 209 # the file system. 210 file_info = open(args.output, 'r').read() 211 if file_info == formatted_info: 212 return 213 214 # Try to write the file. This may fail for read-only ones that were 215 # not updated. 216 try: 217 with open(args.output, 'w') as f: 218 f.write(formatted_info) 219 except IOError as e: 220 if not aar_output_present: 221 raise e 222 raise Exception('Could not update output file: %s\n' % args.output) from e 223 224 225 if __name__ == '__main__': 226 sys.exit(main())