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 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 129def _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 143def _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 153def 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 225if __name__ == '__main__': 226 sys.exit(main()) 227