1# Copyright (C) 2019 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15# A collection of utilities for extracting build rule information from GN 16# projects. 17 18from __future__ import print_function 19import collections 20from compat import iteritems 21import errno 22import filecmp 23import json 24import os 25import re 26import shutil 27import subprocess 28import sys 29from typing import Dict 30from typing import List 31from typing import Optional 32from typing import Set 33from typing import Tuple 34 35BUILDFLAGS_TARGET = '//gn:gen_buildflags' 36GEN_VERSION_TARGET = '//src/base:version_gen_h' 37TARGET_TOOLCHAIN = '//gn/standalone/toolchain:gcc_like_host' 38HOST_TOOLCHAIN = '//gn/standalone/toolchain:gcc_like_host' 39LINKER_UNIT_TYPES = ('executable', 'shared_library', 'static_library') 40 41# TODO(primiano): investigate these, they require further componentization. 42ODR_VIOLATION_IGNORE_TARGETS = { 43 '//test/cts:perfetto_cts_deps', 44 '//:perfetto_integrationtests', 45} 46 47 48def _check_command_output(cmd, cwd): 49 try: 50 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, cwd=cwd) 51 except subprocess.CalledProcessError as e: 52 print( 53 'Command "{}" failed in {}:'.format(' '.join(cmd), cwd), 54 file=sys.stderr) 55 print(e.output.decode(), file=sys.stderr) 56 sys.exit(1) 57 else: 58 return output.decode() 59 60 61def repo_root(): 62 """Returns an absolute path to the repository root.""" 63 return os.path.join( 64 os.path.realpath(os.path.dirname(__file__)), os.path.pardir) 65 66 67def _tool_path(name, system_buildtools=False): 68 # Pass-through to use name if the caller requests to use the system 69 # toolchain. 70 if system_buildtools: 71 return [name] 72 wrapper = os.path.abspath( 73 os.path.join(repo_root(), 'tools', 'run_buildtools_binary.py')) 74 return ['python3', wrapper, name] 75 76 77def prepare_out_directory(gn_args, 78 name, 79 root=repo_root(), 80 system_buildtools=False): 81 """Creates the JSON build description by running GN. 82 83 Returns (path, desc) where |path| is the location of the output directory 84 and |desc| is the JSON build description. 85 """ 86 out = os.path.join(root, 'out', name) 87 try: 88 os.makedirs(out) 89 except OSError as e: 90 if e.errno != errno.EEXIST: 91 raise 92 _check_command_output( 93 _tool_path('gn', system_buildtools) + 94 ['gen', out, '--args=%s' % gn_args], 95 cwd=repo_root()) 96 return out 97 98 99def load_build_description(out, system_buildtools=False): 100 """Creates the JSON build description by running GN.""" 101 desc = _check_command_output( 102 _tool_path('gn', system_buildtools) + 103 ['desc', out, '--format=json', '--all-toolchains', '//*'], 104 cwd=repo_root()) 105 return json.loads(desc) 106 107 108def create_build_description(gn_args, root=repo_root()): 109 """Prepares a GN out directory and loads the build description from it. 110 111 The temporary out directory is automatically deleted. 112 """ 113 out = prepare_out_directory(gn_args, 'tmp.gn_utils', root=root) 114 try: 115 return load_build_description(out) 116 finally: 117 shutil.rmtree(out) 118 119 120def build_targets(out, targets, quiet=False, system_buildtools=False): 121 """Runs ninja to build a list of GN targets in the given out directory. 122 123 Compiling these targets is required so that we can include any generated 124 source files in the amalgamated result. 125 """ 126 targets = [t.replace('//', '') for t in targets] 127 with open(os.devnull, 'w', newline='\n') as devnull: 128 stdout = devnull if quiet else None 129 cmd = _tool_path('ninja', system_buildtools) + targets 130 subprocess.check_call(cmd, cwd=os.path.abspath(out), stdout=stdout) 131 132 133def compute_source_dependencies(out, system_buildtools=False): 134 """For each source file, computes a set of headers it depends on.""" 135 ninja_deps = _check_command_output( 136 _tool_path('ninja', system_buildtools) + ['-t', 'deps'], cwd=out) 137 deps = {} 138 current_source = None 139 for line in ninja_deps.split('\n'): 140 filename = os.path.relpath(os.path.join(out, line.strip()), repo_root()) 141 # Sanitizer builds may have a dependency of ignorelist.txt. Just skip it. 142 if filename.endswith('gn/standalone/sanitizers/ignorelist.txt'): 143 continue 144 if not line or line[0] != ' ': 145 current_source = None 146 continue 147 elif not current_source: 148 # We're assuming the source file is always listed before the 149 # headers. 150 assert os.path.splitext(line)[1] in ['.c', '.cc', '.cpp', '.S'] 151 current_source = filename 152 deps[current_source] = [] 153 else: 154 assert current_source 155 deps[current_source].append(filename) 156 return deps 157 158 159def label_to_path(label): 160 """Turn a GN output label (e.g., //some_dir/file.cc) into a path.""" 161 assert label.startswith('//') 162 return label[2:] 163 164 165def label_without_toolchain(label): 166 """Strips the toolchain from a GN label. 167 168 Return a GN label (e.g //buildtools:protobuf(//gn/standalone/toolchain: 169 gcc_like_host) without the parenthesised toolchain part. 170 """ 171 return label.split('(')[0] 172 173 174def label_to_target_name_with_path(label): 175 """ 176 Turn a GN label into a target name involving the full path. 177 e.g., //src/perfetto:tests -> src_perfetto_tests 178 """ 179 name = re.sub(r'^//:?', '', label) 180 name = re.sub(r'[^a-zA-Z0-9_]', '_', name) 181 return name 182 183 184def gen_buildflags(gn_args, target_file): 185 """Generates the perfetto_build_flags.h for the given config. 186 187 target_file: the path, relative to the repo root, where the generated 188 buildflag header will be copied into. 189 """ 190 tmp_out = prepare_out_directory(gn_args, 'tmp.gen_buildflags') 191 build_targets(tmp_out, [BUILDFLAGS_TARGET], quiet=True) 192 src = os.path.join(tmp_out, 'gen', 'build_config', 'perfetto_build_flags.h') 193 shutil.copy(src, os.path.join(repo_root(), target_file)) 194 shutil.rmtree(tmp_out) 195 196 197def check_or_commit_generated_files(tmp_files, check): 198 """Checks that gen files are unchanged or renames them to the final location 199 200 Takes in input a list of 'xxx.swp' files that have been written. 201 If check == False, it renames xxx.swp -> xxx. 202 If check == True, it just checks that the contents of 'xxx.swp' == 'xxx'. 203 Returns 0 if no diff was detected, 1 otherwise (to be used as exit code). 204 """ 205 res = 0 206 for tmp_file in tmp_files: 207 assert (tmp_file.endswith('.swp')) 208 target_file = os.path.relpath(tmp_file[:-4]) 209 if check: 210 if not filecmp.cmp(tmp_file, target_file): 211 sys.stderr.write('%s needs to be regenerated\n' % target_file) 212 res = 1 213 os.unlink(tmp_file) 214 else: 215 os.replace(tmp_file, target_file) 216 return res 217 218 219class ODRChecker(object): 220 """Detects ODR violations in linker units 221 222 When we turn GN source sets into Soong & Bazel file groups, there is the risk 223 to create ODR violations by including the same file group into different 224 linker unit (this is because other build systems don't have a concept 225 equivalent to GN's source_set). This class navigates the transitive 226 dependencies (mostly static libraries) of a target and detects if multiple 227 paths end up including the same file group. This is to avoid situations like: 228 229 traced.exe -> base(file group) 230 traced.exe -> libperfetto(static lib) -> base(file group) 231 """ 232 233 def __init__(self, gn: 'GnParser', target_name: str): 234 self.gn = gn 235 self.root = gn.get_target(target_name) 236 self.source_sets: Dict[str, Set[str]] = collections.defaultdict(set) 237 self.deps_visited = set() 238 self.source_set_hdr_only = {} 239 240 self._visit(target_name) 241 num_violations = 0 242 if target_name in ODR_VIOLATION_IGNORE_TARGETS: 243 return 244 for sset, paths in self.source_sets.items(): 245 if self.is_header_only(sset): 246 continue 247 if len(paths) != 1: 248 num_violations += 1 249 print( 250 'ODR violation in target %s, multiple paths include %s:\n %s' % 251 (target_name, sset, '\n '.join(paths)), 252 file=sys.stderr) 253 if num_violations > 0: 254 raise Exception('%d ODR violations detected. Build generation aborted' % 255 num_violations) 256 257 def _visit(self, target_name: str, parent_path=''): 258 target = self.gn.get_target(target_name) 259 path = ((parent_path + ' > ') if parent_path else '') + target_name 260 if not target: 261 raise Exception('Cannot find target %s' % target_name) 262 for ssdep in target.transitive_source_set_deps(): 263 name_and_path = '%s (via %s)' % (target_name, path) 264 self.source_sets[ssdep.name].add(name_and_path) 265 deps = set(target.non_proto_or_source_set_deps()).union( 266 target.transitive_proto_deps()) - self.deps_visited 267 for dep in deps: 268 if dep.type == 'executable': 269 continue # Execs are strong boundaries and don't cause ODR violations. 270 # static_library dependencies should reset the path. It doesn't matter if 271 # we get to a source file via: 272 # source_set1 > static_lib > source.cc OR 273 # source_set1 > source_set2 > static_lib > source.cc 274 # This is NOT an ODR violation because source.cc is linked from the same 275 # static library 276 next_parent_path = path if dep.type != 'static_library' else '' 277 self.deps_visited.add(dep.name) 278 self._visit(dep.name, next_parent_path) 279 280 def is_header_only(self, source_set_name: str): 281 cached = self.source_set_hdr_only.get(source_set_name) 282 if cached is not None: 283 return cached 284 target = self.gn.get_target(source_set_name) 285 if target.type != 'source_set': 286 raise TypeError('%s is not a source_set' % source_set_name) 287 res = all(src.endswith('.h') for src in target.sources) 288 self.source_set_hdr_only[source_set_name] = res 289 return res 290 291 292class GnParser(object): 293 """A parser with some cleverness for GN json desc files 294 295 The main goals of this parser are: 296 1) Deal with the fact that other build systems don't have an equivalent 297 notion to GN's source_set. Conversely to Bazel's and Soong's filegroups, 298 GN source_sets expect that dependencies, cflags and other source_set 299 properties propagate up to the linker unit (static_library, executable or 300 shared_library). This parser simulates the same behavior: when a 301 source_set is encountered, some of its variables (cflags and such) are 302 copied up to the dependent targets. This is to allow gen_xxx to create 303 one filegroup for each source_set and then squash all the other flags 304 onto the linker unit. 305 2) Detect and special-case protobuf targets, figuring out the protoc-plugin 306 being used. 307 """ 308 309 class Target(object): 310 """Reperesents A GN target. 311 312 Maked properties are propagated up the dependency chain when a 313 source_set dependency is encountered. 314 """ 315 316 def __init__(self, name, type): 317 self.name = name # e.g. //src/ipc:ipc 318 319 VALID_TYPES = ('static_library', 'shared_library', 'executable', 'group', 320 'action', 'source_set', 'proto_library', 'generated_file') 321 assert (type in VALID_TYPES) 322 self.type = type 323 self.testonly = False 324 self.toolchain = None 325 326 # These are valid only for type == proto_library. 327 # This is typically: 'proto', 'protozero', 'ipc'. 328 self.proto_plugin: Optional[str] = None 329 self.proto_paths = set() 330 self.proto_exports = set() 331 332 self.sources = set() 333 # TODO(primiano): consider whether the public section should be part of 334 # bubbled-up sources. 335 self.public_headers = set() # 'public' 336 337 self.metadata: Dict[str, List[str]] = dict() 338 339 # These are valid only for type == 'action' 340 self.data = set() 341 self.inputs = set() 342 self.outputs = set() 343 self.script = None 344 self.args = [] 345 self.custom_action_type = None 346 self.python_main = None 347 # Used only when custom_action_type 348 # in ['perfetto_android_library', 'perfetto_android_app'] 349 self.manifest: Optional[str] = None 350 # Used only when custom_action_type == 'perfetto_android_app' 351 self.resource_files: Optional[str] = None 352 # Used only when custom_action_type == 'perfetto_android_app' 353 self.instruments: Optional[str] = None 354 # Used only when 355 # custom_action_type == 'perfetto_android_instrumentation_test' 356 self.a_i_t_app: Optional[str] = None 357 self.a_i_t_test_app: Optional[str] = None 358 self.a_i_t_android_bp_test_manifest: Optional[str] = None 359 self.a_i_t_android_bp_test_config: Optional[str] = None 360 361 # These variables are propagated up when encountering a dependency 362 # on a source_set target. 363 self.cflags = set() 364 self.defines = set() 365 self.deps: Set[GnParser.Target] = set() 366 self.transitive_deps: Set[GnParser.Target] = set() 367 self.libs = set() 368 self.include_dirs = set() 369 self.ldflags = set() 370 371 # Deps on //gn:xxx have this flag set to True. These dependencies 372 # are special because they pull third_party code from buildtools/. 373 # We don't want to keep recursing into //buildtools in generators, 374 # this flag is used to stop the recursion and create an empty 375 # placeholder target once we hit //gn:protoc or similar. 376 self.is_third_party_dep_ = False 377 378 def non_proto_or_source_set_deps(self): 379 return set(d for d in self.deps 380 if d.type != 'proto_library' and d.type != 'source_set') 381 382 def proto_deps(self): 383 return set(d for d in self.deps if d.type == 'proto_library') 384 385 def transitive_proto_deps(self): 386 return set(d for d in self.transitive_deps if d.type == 'proto_library') 387 388 def transitive_cpp_proto_deps(self): 389 return set( 390 d for d in self.transitive_deps if d.type == 'proto_library' and 391 d.proto_plugin != 'descriptor' and d.proto_plugin != 'source_set') 392 393 def transitive_source_set_deps(self): 394 return set(d for d in self.transitive_deps if d.type == 'source_set') 395 396 def custom_target_type(self) -> Optional[str]: 397 custom_bazel_type = self.metadata.get('perfetto_custom_target_type') 398 return custom_bazel_type[0] if custom_bazel_type else None 399 400 def linkopts(self) -> List[str]: 401 return self.metadata.get('perfetto_bazel_argument_linkopts', []) 402 403 def binary_name(self) -> Optional[str]: 404 binary_name = self.metadata.get('perfetto_argument_binary_name') 405 return binary_name[0] if binary_name else None 406 407 def __lt__(self, other): 408 if isinstance(other, self.__class__): 409 return self.name < other.name 410 raise TypeError( 411 '\'<\' not supported between instances of \'%s\' and \'%s\'' % 412 (type(self).__name__, type(other).__name__)) 413 414 def __repr__(self): 415 serializable_dict = dict() 416 # 'set' is not serializable type, so we convert sets to the sorted lists 417 # 'deps' and 'transitive_deps' fields are 'Set[Target]', we don't want to 418 # recursively dump all Targets, so we convert them to the list of names. 419 for (k, v) in iteritems(self.__dict__): 420 vv = v 421 if k == "deps" or k == "transitive_deps": 422 vv = sorted([target.name for target in v]) 423 if isinstance(vv, set): 424 vv = sorted(vv) 425 serializable_dict[k] = vv 426 return json.dumps(serializable_dict, indent=4, sort_keys=True) 427 428 def update(self, other): 429 for key in ('cflags', 'data', 'defines', 'deps', 'include_dirs', 430 'ldflags', 'transitive_deps', 'libs', 'proto_paths'): 431 self.__dict__[key].update(other.__dict__.get(key, [])) 432 433 def __init__(self, gn_desc): 434 self.gn_desc_ = gn_desc 435 self.all_targets = {} 436 self.linker_units = {} # Executables, shared or static libraries. 437 self.source_sets = {} 438 self.actions = {} 439 self.proto_libs = {} 440 441 def get_target(self, gn_target_name: str) -> Target: 442 """Returns a Target object from the fully qualified GN target name. 443 444 It bubbles up variables from source_set dependencies as described in the 445 class-level comments. 446 """ 447 target = self.all_targets.get(gn_target_name) 448 if target is not None: 449 return target # Target already processed. 450 451 desc = self.gn_desc_.get(gn_target_name) 452 if not desc: 453 return None 454 455 target = GnParser.Target(gn_target_name, desc['type']) 456 target.testonly = desc.get('testonly', False) 457 target.toolchain = desc.get('toolchain', None) 458 self.all_targets[gn_target_name] = target 459 460 # We should never have GN targets directly depend on buidtools. They 461 # should hop via //gn:xxx, so we can give generators an opportunity to 462 # override them. 463 assert (not gn_target_name.startswith('//buildtools')) 464 465 # Don't descend further into third_party targets. Genrators are supposed 466 # to either ignore them or route to other externally-provided targets. 467 if gn_target_name.startswith('//gn'): 468 target.is_third_party_dep_ = True 469 return target 470 471 target.metadata = desc.get('metadata', {}) 472 473 proto_target_type, proto_desc = self.get_proto_target_type(target) 474 if proto_target_type: 475 assert proto_desc 476 self.proto_libs[target.name] = target 477 target.type = 'proto_library' 478 target.proto_plugin = proto_target_type 479 target.proto_paths.update(self.get_proto_paths(proto_desc)) 480 target.proto_exports.update(self.get_proto_exports(proto_desc)) 481 target.sources.update( 482 self.get_proto_sources(proto_target_type, proto_desc)) 483 assert (all(x.endswith('.proto') for x in target.sources)) 484 elif target.type == 'source_set': 485 self.source_sets[gn_target_name] = target 486 target.sources.update(desc.get('sources', [])) 487 target.inputs.update(desc.get('inputs', [])) 488 elif target.type in LINKER_UNIT_TYPES: 489 self.linker_units[gn_target_name] = target 490 target.sources.update(desc.get('sources', [])) 491 elif target.type == 'action': 492 self.actions[gn_target_name] = target 493 target.data.update(target.metadata.get('perfetto_data', [])) 494 target.inputs.update(desc.get('inputs', [])) 495 target.sources.update(desc.get('sources', [])) 496 outs = [re.sub('^//out/.+?/gen/', '', x) for x in desc['outputs']] 497 target.outputs.update(outs) 498 target.script = desc['script'] 499 # Args are typically relative to the root build dir (../../xxx) 500 # because root build dir is typically out/xxx/). 501 target.args = [re.sub('^../../', '//', x) for x in desc['args']] 502 action_types = target.metadata.get('perfetto_action_type_for_generator') 503 target.custom_action_type = action_types[0] if action_types else None 504 python_main = target.metadata.get('perfetto_python_main') 505 target.python_main = python_main[0] if python_main else None 506 manifest = target.metadata.get('perfetto_android_manifest') 507 if manifest: 508 target.manifest = manifest[0] 509 resource_files = target.metadata.get( 510 'perfetto_android_resource_files_glob') 511 if resource_files: 512 target.resource_files = resource_files[0] 513 assert (target.resource_files.endswith('/**/*')) 514 a_i_t_app = target.metadata.get('perfetto_android_a_i_t_app') 515 target.a_i_t_app = a_i_t_app[0] if a_i_t_app else None 516 a_i_t_test_app = target.metadata.get('perfetto_android_a_i_t_test_app') 517 target.a_i_t_test_app = a_i_t_test_app[0] if a_i_t_test_app else None 518 a_i_t_android_bp_test_manifest = target.metadata.get( 519 'perfetto_android_a_i_t_android_bp_test_manifest') 520 target.a_i_t_android_bp_test_manifest = a_i_t_android_bp_test_manifest[ 521 0] if a_i_t_android_bp_test_manifest else None 522 a_i_t_android_bp_test_config = target.metadata.get( 523 'perfetto_android_a_i_t_android_bp_test_config') 524 target.a_i_t_android_bp_test_config = a_i_t_android_bp_test_config[ 525 0] if a_i_t_android_bp_test_config else None 526 527 # Default for 'public' is //* - all headers in 'sources' are public. 528 # TODO(primiano): if a 'public' section is specified (even if empty), then 529 # the rest of 'sources' is considered inaccessible by gn. Consider 530 # emulating that, so that generated build files don't end up with overly 531 # accessible headers. 532 public_headers = [x for x in desc.get('public', []) if x != '*'] 533 target.public_headers.update(public_headers) 534 535 target.cflags.update(desc.get('cflags', []) + desc.get('cflags_cc', [])) 536 target.libs.update(desc.get('libs', [])) 537 target.ldflags.update(desc.get('ldflags', [])) 538 target.defines.update(desc.get('defines', [])) 539 target.include_dirs.update(desc.get('include_dirs', [])) 540 541 # Recurse in dependencies. 542 for dep_name in desc.get('deps', []): 543 dep = self.get_target(dep_name) 544 545 # generated_file targets only exist for GN builds: we can safely ignore 546 # them. 547 if dep.type == 'generated_file': 548 continue 549 550 # When a proto_library depends on an action, that is always the "_gen" 551 # rule of the action which is "private" to the proto_library rule. 552 # therefore, just ignore it for dep tracking purposes. 553 if dep.type == 'action' and proto_target_type is not None: 554 target_no_toolchain = label_without_toolchain(target.name) 555 dep_no_toolchain = label_without_toolchain(dep.name) 556 assert (dep_no_toolchain == f'{target_no_toolchain}_gen') 557 continue 558 559 # Non-third party groups are only used for bubbling cflags etc so don't 560 # add a dep. 561 if dep.type == 'group' and not dep.is_third_party_dep_: 562 target.update(dep) # Bubble up groups's cflags/ldflags etc. 563 continue 564 565 # Linker units act as a hard boundary making all their internal deps 566 # opaque to the outside world. For this reason, do not propogate deps 567 # transitively across them. 568 if dep.type in LINKER_UNIT_TYPES: 569 target.deps.add(dep) 570 continue 571 572 if dep.type == 'source_set': 573 target.update(dep) # Bubble up source set's cflags/ldflags etc. 574 elif dep.type == 'proto_library': 575 target.proto_paths.update(dep.proto_paths) 576 577 if (target.custom_action_type == 'perfetto_android_library' or 578 target.custom_action_type == 'perfetto_android_app'): 579 jni_library = dep.type == 'shared_library' and dep.custom_target_type( 580 ) == 'perfetto_android_jni_library' 581 android_lib = dep.custom_action_type == 'perfetto_android_library' 582 assert (jni_library or android_lib or dep.is_third_party_dep_) 583 584 if target.custom_action_type == 'perfetto_android_instrumentation_test': 585 assert (dep.custom_action_type == 'perfetto_android_app') 586 assert (dep.name == target.a_i_t_app or 587 dep.name == target.a_i_t_test_app) 588 if dep.name == target.a_i_t_test_app: 589 dep.instruments = target.a_i_t_app 590 591 592 target.deps.add(dep) 593 target.transitive_deps.add(dep) 594 target.transitive_deps.update(dep.transitive_deps) 595 596 return target 597 598 def get_proto_exports(self, proto_desc): 599 # exports in metadata will be available for source_set targets. 600 metadata = proto_desc.get('metadata', {}) 601 return metadata.get('exports', []) 602 603 def get_proto_paths(self, proto_desc): 604 metadata = proto_desc.get('metadata', {}) 605 return metadata.get('proto_import_dirs', []) 606 607 def get_proto_sources(self, proto_target_type, proto_desc): 608 if proto_target_type == 'source_set': 609 metadata = proto_desc.get('metadata', {}) 610 return metadata.get('proto_library_sources', []) 611 return proto_desc.get('sources', []) 612 613 def get_proto_target_type( 614 self, target: Target) -> Tuple[Optional[str], Optional[Dict]]: 615 """ Checks if the target is a proto library and return the plugin. 616 617 Returns: 618 (None, None): if the target is not a proto library. 619 (plugin, proto_desc) where |plugin| is 'proto' in the default (lite) 620 case or 'protozero' or 'ipc' or 'descriptor'; |proto_desc| is the GN 621 json desc of the target with the .proto sources (_gen target for 622 non-descriptor types or the target itself for descriptor type). 623 """ 624 parts = target.name.split('(', 1) 625 name = parts[0] 626 toolchain = '(' + parts[1] if len(parts) > 1 else '' 627 628 # Descriptor targets don't have a _gen target; instead we look for the 629 # characteristic flag in the args of the target itself. 630 desc = self.gn_desc_.get(target.name) 631 if '--descriptor_set_out' in desc.get('args', []): 632 return 'descriptor', desc 633 634 # Source set proto targets have a non-empty proto_library_sources in the 635 # metadata of the description. 636 metadata = desc.get('metadata', {}) 637 if 'proto_library_sources' in metadata: 638 return 'source_set', desc 639 640 # In all other cases, we want to look at the _gen target as that has the 641 # important information. 642 gen_desc = self.gn_desc_.get('%s_gen%s' % (name, toolchain)) 643 if gen_desc is None or gen_desc['type'] != 'action': 644 return None, None 645 args = gen_desc.get('args', []) 646 if '/protoc' not in args[0]: 647 return None, None 648 plugin = 'proto' 649 for arg in (arg for arg in args if arg.startswith('--plugin=')): 650 # |arg| at this point looks like: 651 # --plugin=protoc-gen-plugin=gcc_like_host/protozero_plugin 652 # or 653 # --plugin=protoc-gen-plugin=protozero_plugin 654 plugin = arg.split('=')[-1].split('/')[-1].replace('_plugin', '') 655 return plugin, gen_desc 656