| #!/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() |