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