blob: 6163ea00d5b9fdc123a2a48fe056e81c751ed1fb [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('//')
Patrick Rohrc6331c82022-10-25 11:34:20 -0700153 return label[2:] or "./"
Patrick Rohr92d74122022-10-21 15:50:52 -0700154
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
Patrick Rohr92d74122022-10-21 15:50:52 -0700210class GnParser(object):
211 """A parser with some cleverness for GN json desc files
212
213 The main goals of this parser are:
214 1) Deal with the fact that other build systems don't have an equivalent
215 notion to GN's source_set. Conversely to Bazel's and Soong's filegroups,
216 GN source_sets expect that dependencies, cflags and other source_set
217 properties propagate up to the linker unit (static_library, executable or
218 shared_library). This parser simulates the same behavior: when a
219 source_set is encountered, some of its variables (cflags and such) are
220 copied up to the dependent targets. This is to allow gen_xxx to create
221 one filegroup for each source_set and then squash all the other flags
222 onto the linker unit.
223 2) Detect and special-case protobuf targets, figuring out the protoc-plugin
224 being used.
225 """
226
227 class Target(object):
228 """Reperesents A GN target.
229
230 Maked properties are propagated up the dependency chain when a
231 source_set dependency is encountered.
232 """
233
234 def __init__(self, name, type):
235 self.name = name # e.g. //src/ipc:ipc
236
237 VALID_TYPES = ('static_library', 'shared_library', 'executable', 'group',
238 'action', 'source_set', 'proto_library')
239 assert (type in VALID_TYPES)
240 self.type = type
241 self.testonly = False
242 self.toolchain = None
243
244 # These are valid only for type == proto_library.
245 # This is typically: 'proto', 'protozero', 'ipc'.
246 self.proto_plugin = None
247 self.proto_paths = set()
248 self.proto_exports = set()
249
250 self.sources = set()
251 # TODO(primiano): consider whether the public section should be part of
252 # bubbled-up sources.
253 self.public_headers = set() # 'public'
254
255 # These are valid only for type == 'action'
256 self.inputs = set()
257 self.outputs = set()
258 self.script = None
259 self.args = []
260
261 # These variables are propagated up when encountering a dependency
262 # on a source_set target.
263 self.cflags = set()
264 self.defines = set()
265 self.deps = set()
266 self.libs = set()
267 self.include_dirs = set()
268 self.ldflags = set()
269 self.source_set_deps = set() # Transitive set of source_set deps.
270 self.proto_deps = set()
271 self.transitive_proto_deps = set()
272
273 # Deps on //gn:xxx have this flag set to True. These dependencies
274 # are special because they pull third_party code from buildtools/.
275 # We don't want to keep recursing into //buildtools in generators,
276 # this flag is used to stop the recursion and create an empty
277 # placeholder target once we hit //gn:protoc or similar.
278 self.is_third_party_dep_ = False
279
280 def __lt__(self, other):
281 if isinstance(other, self.__class__):
282 return self.name < other.name
283 raise TypeError(
284 '\'<\' not supported between instances of \'%s\' and \'%s\'' %
285 (type(self).__name__, type(other).__name__))
286
287 def __repr__(self):
288 return json.dumps({
289 k: (list(sorted(v)) if isinstance(v, set) else v)
Patrick Rohr23f26192022-10-25 09:45:22 -0700290 for (k, v) in self.__dict__.items()
Patrick Rohr92d74122022-10-21 15:50:52 -0700291 },
292 indent=4,
293 sort_keys=True)
294
295 def update(self, other):
296 for key in ('cflags', 'defines', 'deps', 'include_dirs', 'ldflags',
297 'source_set_deps', 'proto_deps', 'transitive_proto_deps',
298 'libs', 'proto_paths'):
299 self.__dict__[key].update(other.__dict__.get(key, []))
300
301 def __init__(self, gn_desc):
302 self.gn_desc_ = gn_desc
303 self.all_targets = {}
304 self.linker_units = {} # Executables, shared or static libraries.
305 self.source_sets = {}
306 self.actions = {}
307 self.proto_libs = {}
308
309 def get_target(self, gn_target_name):
310 """Returns a Target object from the fully qualified GN target name.
311
312 It bubbles up variables from source_set dependencies as described in the
313 class-level comments.
314 """
315 target = self.all_targets.get(gn_target_name)
316 if target is not None:
317 return target # Target already processed.
318
319 desc = self.gn_desc_[gn_target_name]
320 target = GnParser.Target(gn_target_name, desc['type'])
321 target.testonly = desc.get('testonly', False)
322 target.toolchain = desc.get('toolchain', None)
323 self.all_targets[gn_target_name] = target
324
325 # We should never have GN targets directly depend on buidtools. They
326 # should hop via //gn:xxx, so we can give generators an opportunity to
327 # override them.
328 assert (not gn_target_name.startswith('//buildtools'))
329
330 # Don't descend further into third_party targets. Genrators are supposed
331 # to either ignore them or route to other externally-provided targets.
332 if gn_target_name.startswith('//gn'):
333 target.is_third_party_dep_ = True
334 return target
335
336 proto_target_type, proto_desc = self.get_proto_target_type(target)
337 if proto_target_type is not None:
338 self.proto_libs[target.name] = target
339 target.type = 'proto_library'
340 target.proto_plugin = proto_target_type
341 target.proto_paths.update(self.get_proto_paths(proto_desc))
342 target.proto_exports.update(self.get_proto_exports(proto_desc))
343 target.sources.update(proto_desc.get('sources', []))
344 assert (all(x.endswith('.proto') for x in target.sources))
345 elif target.type == 'source_set':
346 self.source_sets[gn_target_name] = target
347 target.sources.update(desc.get('sources', []))
348 elif target.type in LINKER_UNIT_TYPES:
349 self.linker_units[gn_target_name] = target
350 target.sources.update(desc.get('sources', []))
351 elif target.type == 'action':
352 self.actions[gn_target_name] = target
353 target.inputs.update(desc.get('inputs', []))
354 target.sources.update(desc.get('sources', []))
355 outs = [re.sub('^//out/.+?/gen/', '', x) for x in desc['outputs']]
356 target.outputs.update(outs)
357 target.script = desc['script']
358 # Args are typically relative to the root build dir (../../xxx)
359 # because root build dir is typically out/xxx/).
360 target.args = [re.sub('^../../', '//', x) for x in desc['args']]
361
362 # Default for 'public' is //* - all headers in 'sources' are public.
363 # TODO(primiano): if a 'public' section is specified (even if empty), then
364 # the rest of 'sources' is considered inaccessible by gn. Consider
365 # emulating that, so that generated build files don't end up with overly
366 # accessible headers.
367 public_headers = [x for x in desc.get('public', []) if x != '*']
368 target.public_headers.update(public_headers)
369
370 target.cflags.update(desc.get('cflags', []) + desc.get('cflags_cc', []))
371 target.libs.update(desc.get('libs', []))
372 target.ldflags.update(desc.get('ldflags', []))
373 target.defines.update(desc.get('defines', []))
374 target.include_dirs.update(desc.get('include_dirs', []))
375
376 # Recurse in dependencies.
377 for dep_name in desc.get('deps', []):
378 dep = self.get_target(dep_name)
379 if dep.is_third_party_dep_:
380 target.deps.add(dep_name)
381 elif dep.type == 'proto_library':
382 target.proto_deps.add(dep_name)
383 target.transitive_proto_deps.add(dep_name)
384 target.proto_paths.update(dep.proto_paths)
385 target.transitive_proto_deps.update(dep.transitive_proto_deps)
386 elif dep.type == 'source_set':
387 target.source_set_deps.add(dep_name)
388 target.update(dep) # Bubble up source set's cflags/ldflags etc.
389 elif dep.type == 'group':
390 target.update(dep) # Bubble up groups's cflags/ldflags etc.
391 elif dep.type == 'action':
392 if proto_target_type is None:
393 target.deps.add(dep_name)
394 elif dep.type in LINKER_UNIT_TYPES:
395 target.deps.add(dep_name)
396
397 return target
398
399 def get_proto_exports(self, proto_desc):
400 # exports in metadata will be available for source_set targets.
401 metadata = proto_desc.get('metadata', {})
402 return metadata.get('exports', [])
403
404 def get_proto_paths(self, proto_desc):
405 # import_dirs in metadata will be available for source_set targets.
406 metadata = proto_desc.get('metadata', {})
407 return metadata.get('import_dirs', [])
408
409 def get_proto_target_type(self, target):
410 """ Checks if the target is a proto library and return the plugin.
411
412 Returns:
413 (None, None): if the target is not a proto library.
414 (plugin, proto_desc) where |plugin| is 'proto' in the default (lite)
415 case or 'protozero' or 'ipc' or 'descriptor'; |proto_desc| is the GN
416 json desc of the target with the .proto sources (_gen target for
417 non-descriptor types or the target itself for descriptor type).
418 """
419 parts = target.name.split('(', 1)
420 name = parts[0]
421 toolchain = '(' + parts[1] if len(parts) > 1 else ''
422
423 # Descriptor targets don't have a _gen target; instead we look for the
424 # characteristic flag in the args of the target itself.
425 desc = self.gn_desc_.get(target.name)
426 if '--descriptor_set_out' in desc.get('args', []):
427 return 'descriptor', desc
428
429 # Source set proto targets have a non-empty proto_library_sources in the
430 # metadata of the description.
431 metadata = desc.get('metadata', {})
432 if 'proto_library_sources' in metadata:
433 return 'source_set', desc
434
435 # In all other cases, we want to look at the _gen target as that has the
436 # important information.
437 gen_desc = self.gn_desc_.get('%s_gen%s' % (name, toolchain))
438 if gen_desc is None or gen_desc['type'] != 'action':
439 return None, None
440 args = gen_desc.get('args', [])
441 if '/protoc' not in args[0]:
442 return None, None
443 plugin = 'proto'
444 for arg in (arg for arg in args if arg.startswith('--plugin=')):
445 # |arg| at this point looks like:
446 # --plugin=protoc-gen-plugin=gcc_like_host/protozero_plugin
447 # or
448 # --plugin=protoc-gen-plugin=protozero_plugin
449 plugin = arg.split('=')[-1].split('/')[-1].replace('_plugin', '')
450 return plugin, gen_desc