• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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