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