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