| #!/usr/bin/env python3 | 
 | # Copyright 2020 The Chromium Authors | 
 | # Use of this source code is governed by a BSD-style license that can be | 
 | # found in the LICENSE file. | 
 |  | 
 | # Lint as: python3 | 
 | """Prints out available java targets. | 
 |  | 
 | Examples: | 
 | # List GN target for bundles: | 
 | build/android/list_java_targets.py -C out/Default --type android_app_bundle \ | 
 | --gn-labels | 
 |  | 
 | # List all android targets with types: | 
 | build/android/list_java_targets.py -C out/Default --print-types | 
 |  | 
 | # Build all apk targets: | 
 | build/android/list_java_targets.py -C out/Default --type android_apk | xargs \ | 
 | autoninja -C out/Default | 
 |  | 
 | # Show how many of each target type exist: | 
 | build/android/list_java_targets.py -C out/Default --stats | 
 |  | 
 | """ | 
 |  | 
 | import argparse | 
 | import collections | 
 | import glob | 
 | import json | 
 | import logging | 
 | import os | 
 | import shlex | 
 | import subprocess | 
 | import sys | 
 |  | 
 | _SRC_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', | 
 |                                           '..')) | 
 | sys.path.append(os.path.join(_SRC_ROOT, 'build')) | 
 | import gn_helpers | 
 |  | 
 | sys.path.append(os.path.join(_SRC_ROOT, 'build', 'android')) | 
 | from pylib import constants | 
 |  | 
 | _VALID_TYPES = ( | 
 |     'android_apk', | 
 |     'android_app_bundle', | 
 |     'android_app_bundle_module', | 
 |     'android_assets', | 
 |     'android_resources', | 
 |     'dist_aar', | 
 |     'dist_jar', | 
 |     'group', | 
 |     'java_annotation_processor', | 
 |     'java_binary', | 
 |     'java_library', | 
 |     'robolectric_binary', | 
 |     'system_java_library', | 
 | ) | 
 |  | 
 |  | 
 | def _compile(output_dir, args, quiet=False): | 
 |   cmd = gn_helpers.CreateBuildCommand(output_dir) + args | 
 |   logging.info('Running: %s', shlex.join(cmd)) | 
 |   if quiet: | 
 |     subprocess.run(cmd, check=True, capture_output=True) | 
 |   else: | 
 |     subprocess.run(cmd, check=True, stdout=sys.stderr) | 
 |  | 
 |  | 
 | def _query_for_targets(output_dir): | 
 |   # Query ninja rather than GN since it's faster. | 
 |   cmd = [ | 
 |       os.path.join(_SRC_ROOT, 'third_party', 'siso', 'cipd', 'siso'), 'query', | 
 |       'targets', '-C', output_dir | 
 |   ] | 
 |   logging.info('Running: %r', cmd) | 
 |   try: | 
 |     query_output = subprocess.run(cmd, | 
 |                                   check=True, | 
 |                                   capture_output=True, | 
 |                                   encoding='ascii').stdout | 
 |   except subprocess.CalledProcessError as e: | 
 |     sys.stderr.write('Command output:\n' + e.stdout + e.stderr) | 
 |     raise | 
 |  | 
 |   # Dict of target name -> has_build_config | 
 |   ret = {} | 
 |   # java_prebuilt() targets do not write build_config files, so look for | 
 |   # __assetres as well. Targets like android_assets() will not appear at all | 
 |   # (oh well). | 
 |   SUFFIX1 = '__build_config_crbug_908819' | 
 |   SUFFIX_LEN1 = len(SUFFIX1) | 
 |   SUFFIX2 = '__assetres' | 
 |   SUFFIX_LEN2 = len(SUFFIX2) | 
 |   for line in query_output.splitlines(): | 
 |     ninja_target = line.rsplit(':', 1)[0] | 
 |     # Ignore root aliases by ensuring a : exists. | 
 |     if ':' in ninja_target: | 
 |       if ninja_target.endswith(SUFFIX1): | 
 |         ret[f'//{ninja_target[:-SUFFIX_LEN1]}'] = True | 
 |       elif ninja_target.endswith(SUFFIX2): | 
 |         ret.setdefault(f'//{ninja_target[:-SUFFIX_LEN2]}', False) | 
 |   return ret | 
 |  | 
 |  | 
 | def _query_json(*, json_dict: dict, query: str, target: str): | 
 |   """Traverses through the json dictionary according to the query. | 
 |  | 
 |   If at any point a key does not exist, return the empty string, but raise an | 
 |   error if a key exists but is the wrong type. | 
 |  | 
 |   This is roughly equivalent to returning | 
 |   json_dict[queries[0]]?[queries[1]]?...[queries[N]]? where the ? means that if | 
 |   the key doesn't exist, the empty string is returned. | 
 |  | 
 |   Example: | 
 |   Given json_dict = {'a': {'b': 'c'}} | 
 |   - If queries = ['a', 'b'] | 
 |     Return: 'c' | 
 |   - If queries = ['a', 'd'] | 
 |     Return '' | 
 |   - If queries = ['x'] | 
 |     Return '' | 
 |   - If queries = ['a', 'b', 'x'] | 
 |     Raise an error since json_dict['a']['b'] is the string 'c' instead of an | 
 |     expected dict that can be indexed into. | 
 |  | 
 |   Returns the final result after exhausting all the queries. | 
 |   """ | 
 |   queries = query.split('.') | 
 |   value = json_dict | 
 |   try: | 
 |     for key in queries: | 
 |       value = value.get(key) | 
 |       if value is None: | 
 |         return '' | 
 |   except AttributeError as e: | 
 |     raise Exception( | 
 |         f'Failed when attempting to get {queries} from {target}') from e | 
 |   return value | 
 |  | 
 |  | 
 | class _TargetEntry: | 
 |  | 
 |   def __init__(self, gn_target, has_build_config): | 
 |     assert gn_target.startswith('//'), f'{gn_target} does not start with //' | 
 |     assert ':' in gn_target, f'Non-root {gn_target} required' | 
 |     self.gn_target = gn_target | 
 |     self.has_build_config = has_build_config | 
 |     self._combined_config = None | 
 |     self._params_json = None | 
 |  | 
 |   @property | 
 |   def ninja_target(self): | 
 |     return self.gn_target[2:] | 
 |  | 
 |   @property | 
 |   def ninja_build_config_target(self): | 
 |     assert self.has_build_config, 'No build config for ' + self.gn_target | 
 |     return self.ninja_target + '__build_config_crbug_908819' | 
 |  | 
 |   @property | 
 |   def build_config_path(self): | 
 |     """Returns the filepath of the project's .build_config.json.""" | 
 |     assert self.has_build_config, 'No build config for ' + self.gn_target | 
 |     return self.params_path.replace('.params.json', '.build_config.json') | 
 |  | 
 |   @property | 
 |   def params_path(self): | 
 |     """Returns the filepath of the project's .params.json.""" | 
 |     ninja_target = self.ninja_target | 
 |     # Support targets at the root level. e.g. //:foo | 
 |     if ninja_target[0] == ':': | 
 |       ninja_target = ninja_target[1:] | 
 |     subpath = ninja_target.replace(':', os.path.sep) + '.params.json' | 
 |     return os.path.relpath( | 
 |         os.path.join(constants.GetOutDirectory(), 'gen', subpath)) | 
 |  | 
 |   def params_values(self): | 
 |     if not self._params_json: | 
 |       with open(self.params_path) as f: | 
 |         self._params_json = json.load(f) | 
 |     return self._params_json | 
 |  | 
 |   def combined_config_values(self): | 
 |     """Union of .params.json and *.build_config.json""" | 
 |     if not self._combined_config: | 
 |       config = dict(self.params_values()) | 
 |       if self.has_build_config: | 
 |         pattern = self.build_config_path.replace('.build_config.json', | 
 |                                                  '*.build_config.json') | 
 |         for p in glob.glob(pattern): | 
 |           with open(p) as f: | 
 |             config.update(json.load(f)) | 
 |       self._combined_config = config | 
 |     return self._combined_config | 
 |  | 
 |   def get_type(self): | 
 |     """Returns the target type from its .build_config.json.""" | 
 |     return self.params_values()['type'] | 
 |  | 
 |   def proguard_enabled(self): | 
 |     """Returns whether proguard runs for this target.""" | 
 |     # Modules set proguard_enabled, but the proguarding happens only once at the | 
 |     # bundle level. | 
 |     if self.get_type() == 'android_app_bundle_module': | 
 |       return False | 
 |     return self.params_values().get('proguard_enabled', False) | 
 |  | 
 |  | 
 | def main(): | 
 |   parser = argparse.ArgumentParser( | 
 |       description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | 
 |   parser.add_argument('-C', | 
 |                       '--output-directory', | 
 |                       help='If outdir is not provided, will attempt to guess.') | 
 |   parser.add_argument('--gn-labels', | 
 |                       action='store_true', | 
 |                       help='Print GN labels rather than ninja targets') | 
 |   parser.add_argument('--omit-targets', | 
 |                       action='store_true', | 
 |                       help='Do not print the target / gn label') | 
 |   parser.add_argument( | 
 |       '--nested', | 
 |       action='store_true', | 
 |       help='Do not convert nested targets to their top-level equivalents. ' | 
 |       'E.g. Without this, foo_test__apk -> foo_test') | 
 |   parser.add_argument('--print-types', | 
 |                       action='store_true', | 
 |                       help='Print type of each target') | 
 |   parser.add_argument( | 
 |       '--print-build-config-paths', | 
 |       action='store_true', | 
 |       help='Print path to the .build_config.json of each target') | 
 |   parser.add_argument('--print-params-paths', | 
 |                       action='store_true', | 
 |                       help='Print path to the .params.json of each target') | 
 |   parser.add_argument('--build', | 
 |                       action='store_true', | 
 |                       help='Build all .build_config.json files.') | 
 |   parser.add_argument('--type', | 
 |                       action='append', | 
 |                       help='Restrict to targets of given type', | 
 |                       choices=_VALID_TYPES) | 
 |   parser.add_argument('--stats', | 
 |                       action='store_true', | 
 |                       help='Print counts of each target type.') | 
 |   parser.add_argument('--proguard-enabled', | 
 |                       action='store_true', | 
 |                       help='Restrict to targets that have proguard enabled.') | 
 |   parser.add_argument('--query', | 
 |                       help='A dot separated string specifying a query for a ' | 
 |                       'build config json value of each target. Example: Use ' | 
 |                       '--query unprocessed_jar_path to show a list ' | 
 |                       'of all targets that have a non-empty ' | 
 |                       '"unprocessed_jar_path" value in that dict.') | 
 |   parser.add_argument('-v', '--verbose', default=0, action='count') | 
 |   parser.add_argument('-q', '--quiet', default=0, action='count') | 
 |   args = parser.parse_args() | 
 |  | 
 |   args.build |= bool(args.type or args.proguard_enabled or args.print_types | 
 |                      or args.stats or args.query) | 
 |  | 
 |   logging.basicConfig(level=logging.WARNING + 10 * (args.quiet - args.verbose), | 
 |                       format='%(levelname).1s %(relativeCreated)6d %(message)s') | 
 |  | 
 |   if args.output_directory: | 
 |     constants.SetOutputDirectory(args.output_directory) | 
 |   constants.CheckOutputDirectory() | 
 |   output_dir = constants.GetOutDirectory() | 
 |  | 
 |   if args.build: | 
 |     _compile(output_dir, ['build.ninja']) | 
 |  | 
 |   # Query ninja for all __build_config_crbug_908819 targets. | 
 |   # TODO(agrieve): java_group, android_assets, and android_resources do not | 
 |   # write .build_config.json files, and so will not show up by this query. | 
 |   # If we ever need them to, use "gn gen" into a temp dir, and set an extra | 
 |   # gn arg that causes all write_build_config() template to print all targets. | 
 |   result = _query_for_targets(output_dir) | 
 |   entries = [_TargetEntry(t, v) for t, v in sorted(result.items())] | 
 |   entries = [e for e in entries if os.path.exists(e.params_path)] | 
 |  | 
 |   if not entries: | 
 |     logging.warning('No targets found. Run with --build') | 
 |     sys.exit(1) | 
 |  | 
 |   if args.build: | 
 |     targets = [ | 
 |         e.ninja_build_config_target for e in entries if e.has_build_config | 
 |     ] | 
 |     logging.warning('Building %d .build_config.json files...', len(targets)) | 
 |     _compile(output_dir, targets, quiet=args.quiet) | 
 |  | 
 |   if args.type: | 
 |     if set(args.type) & {'android_resources', 'android_assets', 'group'}: | 
 |       logging.warning('Cannot filter by this type. See TODO.') | 
 |       sys.exit(1) | 
 |     entries = [e for e in entries if e.get_type() in args.type] | 
 |  | 
 |   if args.proguard_enabled: | 
 |     entries = [e for e in entries if e.proguard_enabled()] | 
 |  | 
 |   if args.stats: | 
 |     counts = collections.Counter(e.get_type() for e in entries) | 
 |     for entry_type, count in sorted(counts.items()): | 
 |       print(f'{entry_type}: {count}') | 
 |   else: | 
 |     for e in entries: | 
 |       if args.omit_targets: | 
 |         target_part = '' | 
 |       else: | 
 |         if args.gn_labels: | 
 |           target_part = e.gn_target | 
 |         else: | 
 |           target_part = e.ninja_target | 
 |  | 
 |         # Convert to top-level target | 
 |         if not args.nested: | 
 |           target_part = target_part.replace('__test_apk', | 
 |                                             '').replace('__apk', '') | 
 |  | 
 |       type_part = '' | 
 |       if args.print_types: | 
 |         type_part = e.get_type() | 
 |       elif args.print_build_config_paths: | 
 |         type_part = e.build_config_path if e.has_build_config else 'N/A' | 
 |       elif args.print_params_paths: | 
 |         type_part = e.params_path | 
 |       elif args.query: | 
 |         type_part = _query_json(json_dict=e.combined_config_values(), | 
 |                                 query=args.query, | 
 |                                 target=e.gn_target) | 
 |         if not type_part: | 
 |           continue | 
 |  | 
 |       if target_part and type_part: | 
 |         print(f'{target_part}: {type_part}') | 
 |       elif target_part or type_part: | 
 |         print(target_part or type_part) | 
 |  | 
 |  | 
 | if __name__ == '__main__': | 
 |   main() |