• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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