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