xref: /aosp_15_r20/external/cronet/build/android/list_java_targets.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1#!/usr/bin/env python3
2# Copyright 2020 The Chromium Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6# Lint as: python3
7"""Prints out available java targets.
8
9Examples:
10# List GN target for bundles:
11build/android/list_java_targets.py -C out/Default --type android_app_bundle \
12--gn-labels
13
14# List all android targets with types:
15build/android/list_java_targets.py -C out/Default --print-types
16
17# Build all apk targets:
18build/android/list_java_targets.py -C out/Default --type android_apk | xargs \
19autoninja -C out/Default
20
21# Show how many of each target type exist:
22build/android/list_java_targets.py -C out/Default --stats
23
24"""
25
26import argparse
27import collections
28import json
29import logging
30import os
31import shlex
32import shutil
33import subprocess
34import sys
35
36_SRC_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), '..',
37                                          '..'))
38sys.path.append(os.path.join(_SRC_ROOT, 'build'))
39import gn_helpers
40
41sys.path.append(os.path.join(_SRC_ROOT, 'build', 'android'))
42from pylib import constants
43
44_VALID_TYPES = (
45    'android_apk',
46    'android_app_bundle',
47    'android_app_bundle_module',
48    'android_assets',
49    'android_resources',
50    'dist_aar',
51    'dist_jar',
52    'group',
53    'java_annotation_processor',
54    'java_binary',
55    'java_library',
56    'robolectric_binary',
57    'system_java_library',
58)
59
60
61def _resolve_ninja():
62  # Prefer the version on PATH, but fallback to known version if PATH doesn't
63  # have one (e.g. on bots).
64  if shutil.which('ninja') is None:
65    return os.path.join(_SRC_ROOT, 'third_party', 'ninja', 'ninja')
66  return 'ninja'
67
68
69def _compile(output_dir, args, quiet=False):
70  cmd = gn_helpers.CreateBuildCommand(output_dir) + args
71  logging.info('Running: %s', shlex.join(cmd))
72  if quiet:
73    subprocess.run(cmd, check=True, capture_output=True)
74  else:
75    subprocess.run(cmd, check=True, stdout=sys.stderr)
76
77
78def _query_for_build_config_targets(output_dir):
79  # Query ninja rather than GN since it's faster.
80  # Use ninja rather than autoninja to avoid extra output if user has set the
81  # NINJA_SUMMARIZE_BUILD environment variable.
82  cmd = [_resolve_ninja(), '-C', output_dir, '-t', 'targets']
83  logging.info('Running: %r', cmd)
84  ninja_output = subprocess.run(cmd,
85                                check=True,
86                                capture_output=True,
87                                encoding='ascii').stdout
88  ret = []
89  SUFFIX = '__build_config_crbug_908819'
90  SUFFIX_LEN = len(SUFFIX)
91  for line in ninja_output.splitlines():
92    ninja_target = line.rsplit(':', 1)[0]
93    # Ignore root aliases by ensuring a : exists.
94    if ':' in ninja_target and ninja_target.endswith(SUFFIX):
95      ret.append(f'//{ninja_target[:-SUFFIX_LEN]}')
96  return ret
97
98
99def _query_json(*, json_dict: dict, query: str, path: str):
100  """Traverses through the json dictionary according to the query.
101
102  If at any point a key does not exist, return the empty string, but raise an
103  error if a key exists but is the wrong type.
104
105  This is roughly equivalent to returning
106  json_dict[queries[0]]?[queries[1]]?...[queries[N]]? where the ? means that if
107  the key doesn't exist, the empty string is returned.
108
109  Example:
110  Given json_dict = {'a': {'b': 'c'}}
111  - If queries = ['a', 'b']
112    Return: 'c'
113  - If queries = ['a', 'd']
114    Return ''
115  - If queries = ['x']
116    Return ''
117  - If queries = ['a', 'b', 'x']
118    Raise an error since json_dict['a']['b'] is the string 'c' instead of an
119    expected dict that can be indexed into.
120
121  Returns the final result after exhausting all the queries.
122  """
123  queries = query.split('.')
124  value = json_dict
125  try:
126    for key in queries:
127      value = value.get(key)
128      if value is None:
129        return ''
130  except AttributeError as e:
131    raise Exception(
132        f'Failed when attempting to get {queries} from {path}') from e
133  return value
134
135
136class _TargetEntry:
137
138  def __init__(self, gn_target):
139    assert gn_target.startswith('//'), f'{gn_target} does not start with //'
140    assert ':' in gn_target, f'Non-root {gn_target} required'
141    self.gn_target = gn_target
142    self._build_config = None
143
144  @property
145  def ninja_target(self):
146    return self.gn_target[2:]
147
148  @property
149  def ninja_build_config_target(self):
150    return self.ninja_target + '__build_config_crbug_908819'
151
152  @property
153  def build_config_path(self):
154    """Returns the filepath of the project's .build_config.json."""
155    ninja_target = self.ninja_target
156    # Support targets at the root level. e.g. //:foo
157    if ninja_target[0] == ':':
158      ninja_target = ninja_target[1:]
159    subpath = ninja_target.replace(':', os.path.sep) + '.build_config.json'
160    return os.path.join(constants.GetOutDirectory(), 'gen', subpath)
161
162  def build_config(self):
163    """Reads and returns the project's .build_config.json JSON."""
164    if not self._build_config:
165      with open(self.build_config_path) as jsonfile:
166        self._build_config = json.load(jsonfile)
167    return self._build_config
168
169  def get_type(self):
170    """Returns the target type from its .build_config.json."""
171    return self.build_config()['deps_info']['type']
172
173  def proguard_enabled(self):
174    """Returns whether proguard runs for this target."""
175    # Modules set proguard_enabled, but the proguarding happens only once at the
176    # bundle level.
177    if self.get_type() == 'android_app_bundle_module':
178      return False
179    return self.build_config()['deps_info'].get('proguard_enabled', False)
180
181
182def main():
183  parser = argparse.ArgumentParser(
184      description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
185  parser.add_argument('-C',
186                      '--output-directory',
187                      help='If outdir is not provided, will attempt to guess.')
188  parser.add_argument('--gn-labels',
189                      action='store_true',
190                      help='Print GN labels rather than ninja targets')
191  parser.add_argument(
192      '--nested',
193      action='store_true',
194      help='Do not convert nested targets to their top-level equivalents. '
195      'E.g. Without this, foo_test__apk -> foo_test')
196  parser.add_argument('--print-types',
197                      action='store_true',
198                      help='Print type of each target')
199  parser.add_argument(
200      '--print-build-config-paths',
201      action='store_true',
202      help='Print path to the .build_config.json of each target')
203  parser.add_argument('--build',
204                      action='store_true',
205                      help='Build all .build_config.json files.')
206  parser.add_argument('--type',
207                      action='append',
208                      help='Restrict to targets of given type',
209                      choices=_VALID_TYPES)
210  parser.add_argument('--stats',
211                      action='store_true',
212                      help='Print counts of each target type.')
213  parser.add_argument('--proguard-enabled',
214                      action='store_true',
215                      help='Restrict to targets that have proguard enabled.')
216  parser.add_argument('--query',
217                      help='A dot separated string specifying a query for a '
218                      'build config json value of each target. Example: Use '
219                      '--query deps_info.unprocessed_jar_path to show a list '
220                      'of all targets that have a non-empty deps_info dict and '
221                      'non-empty "unprocessed_jar_path" value in that dict.')
222  parser.add_argument('-v', '--verbose', default=0, action='count')
223  parser.add_argument('-q', '--quiet', default=0, action='count')
224  args = parser.parse_args()
225
226  args.build |= bool(args.type or args.proguard_enabled or args.print_types
227                     or args.stats or args.query)
228
229  logging.basicConfig(level=logging.WARNING + 10 * (args.quiet - args.verbose),
230                      format='%(levelname).1s %(relativeCreated)6d %(message)s')
231
232  if args.output_directory:
233    constants.SetOutputDirectory(args.output_directory)
234  constants.CheckOutputDirectory()
235  output_dir = constants.GetOutDirectory()
236
237  # Query ninja for all __build_config_crbug_908819 targets.
238  targets = _query_for_build_config_targets(output_dir)
239  entries = [_TargetEntry(t) for t in targets]
240
241  if args.build:
242    logging.warning('Building %d .build_config.json files...', len(entries))
243    _compile(output_dir, [e.ninja_build_config_target for e in entries],
244             quiet=args.quiet)
245
246  if args.type:
247    entries = [e for e in entries if e.get_type() in args.type]
248
249  if args.proguard_enabled:
250    entries = [e for e in entries if e.proguard_enabled()]
251
252  if args.stats:
253    counts = collections.Counter(e.get_type() for e in entries)
254    for entry_type, count in sorted(counts.items()):
255      print(f'{entry_type}: {count}')
256  else:
257    for e in entries:
258      if args.gn_labels:
259        to_print = e.gn_target
260      else:
261        to_print = e.ninja_target
262
263      # Convert to top-level target
264      if not args.nested:
265        to_print = to_print.replace('__test_apk', '').replace('__apk', '')
266
267      if args.print_types:
268        to_print = f'{to_print}: {e.get_type()}'
269      elif args.print_build_config_paths:
270        to_print = f'{to_print}: {e.build_config_path}'
271      elif args.query:
272        value = _query_json(json_dict=e.build_config(),
273                            query=args.query,
274                            path=e.build_config_path)
275        if not value:
276          continue
277        to_print = f'{to_print}: {value}'
278
279      print(to_print)
280
281
282if __name__ == '__main__':
283  main()
284