xref: /aosp_15_r20/external/cronet/build/android/gyp/aar.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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