list_java_targets.py (9730B)
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 9 Examples: 10 # List GN target for bundles: 11 build/android/list_java_targets.py -C out/Default --type android_app_bundle \ 12 --gn-labels 13 14 # List all android targets with types: 15 build/android/list_java_targets.py -C out/Default --print-types 16 17 # Build all apk targets: 18 build/android/list_java_targets.py -C out/Default --type android_apk | xargs \ 19 autoninja -C out/Default 20 21 # Show how many of each target type exist: 22 build/android/list_java_targets.py -C out/Default --stats 23 24 """ 25 26 import argparse 27 import collections 28 import json 29 import logging 30 import os 31 import shlex 32 import shutil 33 import subprocess 34 import sys 35 36 _SRC_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', 37 '..')) 38 sys.path.append(os.path.join(_SRC_ROOT, 'build')) 39 import gn_helpers 40 41 sys.path.append(os.path.join(_SRC_ROOT, 'build', 'android')) 42 from 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 61 def _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 69 def _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 78 def _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 99 def _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 136 class _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 182 def 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 if args.build: 238 _compile(output_dir, ['build.ninja']) 239 240 # Query ninja for all __build_config_crbug_908819 targets. 241 targets = _query_for_build_config_targets(output_dir) 242 entries = [_TargetEntry(t) for t in targets] 243 244 if not entries: 245 logging.warning('No targets found. Run with --build') 246 sys.exit(1) 247 248 if args.build: 249 logging.warning('Building %d .build_config.json files...', len(entries)) 250 _compile(output_dir, [e.ninja_build_config_target for e in entries], 251 quiet=args.quiet) 252 253 if args.type: 254 entries = [e for e in entries if e.get_type() in args.type] 255 256 if args.proguard_enabled: 257 entries = [e for e in entries if e.proguard_enabled()] 258 259 if args.stats: 260 counts = collections.Counter(e.get_type() for e in entries) 261 for entry_type, count in sorted(counts.items()): 262 print(f'{entry_type}: {count}') 263 else: 264 for e in entries: 265 if args.gn_labels: 266 to_print = e.gn_target 267 else: 268 to_print = e.ninja_target 269 270 # Convert to top-level target 271 if not args.nested: 272 to_print = to_print.replace('__test_apk', '').replace('__apk', '') 273 274 if args.print_types: 275 to_print = f'{to_print}: {e.get_type()}' 276 elif args.print_build_config_paths: 277 to_print = f'{to_print}: {e.build_config_path}' 278 elif args.query: 279 value = _query_json(json_dict=e.build_config(), 280 query=args.query, 281 path=e.build_config_path) 282 if not value: 283 continue 284 to_print = f'{to_print}: {value}' 285 286 print(to_print) 287 288 289 if __name__ == '__main__': 290 main()