blob: bfc9f97490cccfd27395ec36e88a9040ecf9ecd3 [file] [log] [blame]
Sami Kyostila0a34b032019-05-16 18:28:48 +01001#!/usr/bin/env python
2# Copyright (C) 2019 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16# This tool uses a collection of BUILD.gn files and build targets to generate
17# an "amalgamated" C++ header and source file pair which compiles to an
18# equivalent program. The tool also outputs the necessary compiler and linker
19# flags needed to compile the resulting source code.
20
Sami Kyostila3c88a1d2019-05-22 18:29:42 +010021from __future__ import print_function
Sami Kyostila0a34b032019-05-16 18:28:48 +010022import argparse
Sami Kyostila0a34b032019-05-16 18:28:48 +010023import os
24import re
25import shutil
26import subprocess
27import sys
Sami Kyostila468e61d2019-05-23 15:54:01 +010028import tempfile
Sami Kyostila0a34b032019-05-16 18:28:48 +010029
Sami Kyostila3c88a1d2019-05-22 18:29:42 +010030import gn_utils
31
Sami Kyostila0a34b032019-05-16 18:28:48 +010032# Default targets to include in the result.
33default_targets = [
Primiano Tucci658e2d62019-06-14 10:03:32 +010034 '//:libperfetto_client_experimental',
35 "//protos/perfetto/trace:zero",
36 "//protos/perfetto/config:zero",
37 "//protos/perfetto/common:zero",
Sami Kyostila0a34b032019-05-16 18:28:48 +010038]
39
40# Arguments for the GN output directory (unless overridden from the command
41# line).
42gn_args = 'is_debug=false'
43
44# Compiler flags which aren't filtered out.
45cflag_whitelist = r'^-(W.*|fno-exceptions|fPIC|std.*|fvisibility.*)$'
46
47# Linker flags which aren't filtered out.
48ldflag_whitelist = r'^-()$'
49
50# Libraries which are filtered out.
51lib_blacklist = r'^(c|gcc_eh)$'
52
53# Macros which aren't filtered out.
54define_whitelist = r'^(PERFETTO.*|GOOGLE_PROTOBUF.*)$'
55
Sami Kyostila0a34b032019-05-16 18:28:48 +010056# Includes which will be removed from the generated source.
57includes_to_remove = r'^(gtest).*$'
58
Sami Kyostila7e8509f2019-05-29 12:36:24 +010059default_cflags = [
60 # Since we're expanding header files into the generated source file, some
61 # constant may remain unused.
62 '-Wno-unused-const-variable'
63]
64
Sami Kyostila0a34b032019-05-16 18:28:48 +010065# Build flags to satisfy a protobuf (lite or full) dependency.
66protobuf_cflags = [
67 # Note that these point to the local copy of protobuf in buildtools. In
68 # reality the user of the amalgamated result will have to provide a path to
69 # an installed copy of the exact same version of protobuf which was used to
70 # generate the amalgamated build.
71 '-isystembuildtools/protobuf/src',
72 '-Lbuildtools/protobuf/src/.libs',
73 # We also need to disable some warnings for protobuf.
74 '-Wno-missing-prototypes',
75 '-Wno-missing-variable-declarations',
76 '-Wno-sign-conversion',
77 '-Wno-unknown-pragmas',
78 '-Wno-unused-macros',
79]
80
81# A mapping of dependencies to system libraries. Libraries in this map will not
82# be built statically but instead added as dependencies of the amalgamated
83# project.
84system_library_map = {
85 '//buildtools:protobuf_full': {
86 'libs': ['protobuf'],
87 'cflags': protobuf_cflags,
88 },
89 '//buildtools:protobuf_lite': {
90 'libs': ['protobuf-lite'],
91 'cflags': protobuf_cflags,
92 },
93 '//buildtools:protoc_lib': {'libs': ['protoc']},
Sami Kyostila0a34b032019-05-16 18:28:48 +010094}
95
96# ----------------------------------------------------------------------------
97# End of configuration.
98# ----------------------------------------------------------------------------
99
100tool_name = os.path.basename(__file__)
101preamble = """// Copyright (C) 2019 The Android Open Source Project
102//
103// Licensed under the Apache License, Version 2.0 (the "License");
104// you may not use this file except in compliance with the License.
105// You may obtain a copy of the License at
106//
107// http://www.apache.org/licenses/LICENSE-2.0
108//
109// Unless required by applicable law or agreed to in writing, software
110// distributed under the License is distributed on an "AS IS" BASIS,
111// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
112// See the License for the specific language governing permissions and
113// limitations under the License.
114//
115// This file is automatically generated by %s. Do not edit.
116""" % tool_name
117
118
119def apply_blacklist(blacklist, items):
120 return [item for item in items if not re.match(blacklist, item)]
121
122
123def apply_whitelist(whitelist, items):
124 return [item for item in items if re.match(whitelist, item)]
125
126
127class Error(Exception):
128 pass
129
130
131class DependencyNode(object):
132 """A target in a GN build description along with its dependencies."""
133
134 def __init__(self, target_name):
135 self.target_name = target_name
136 self.dependencies = set()
137
138 def add_dependency(self, target_node):
139 if target_node in self.dependencies:
140 return
141 self.dependencies.add(target_node)
142
143 def iterate_depth_first(self):
144 for node in sorted(self.dependencies, key=lambda n: n.target_name):
145 for node in node.iterate_depth_first():
146 yield node
147 if self.target_name:
148 yield self
149
150
151class DependencyTree(object):
152 """A tree of GN build target dependencies."""
153
154 def __init__(self):
155 self.target_to_node_map = {}
156 self.root = self._get_or_create_node(None)
157
158 def _get_or_create_node(self, target_name):
159 if target_name in self.target_to_node_map:
160 return self.target_to_node_map[target_name]
161 node = DependencyNode(target_name)
162 self.target_to_node_map[target_name] = node
163 return node
164
165 def add_dependency(self, from_target, to_target):
166 from_node = self._get_or_create_node(from_target)
167 to_node = self._get_or_create_node(to_target)
168 assert from_node is not to_node
169 from_node.add_dependency(to_node)
170
171 def iterate_depth_first(self):
172 for node in self.root.iterate_depth_first():
173 yield node
174
175
176class AmalgamatedProject(object):
177 """In-memory representation of an amalgamated source/header pair."""
178
179 def __init__(self, desc, source_deps):
180 """Constructor.
181
182 Args:
183 desc: JSON build description.
184 source_deps: A map of (source file, [dependency header]) which is
185 to detect which header files are included by each source file.
186 """
187 self.desc = desc
188 self.source_deps = source_deps
189 self.header = []
190 self.source = []
Sami Kyostila7e8509f2019-05-29 12:36:24 +0100191 # Note that we don't support multi-arg flags.
192 self.cflags = set(default_cflags)
Sami Kyostila0a34b032019-05-16 18:28:48 +0100193 self.ldflags = set()
194 self.defines = set()
195 self.libs = set()
196 self._dependency_tree = DependencyTree()
Sami Kyostila7e8509f2019-05-29 12:36:24 +0100197 self._processed_sources = set()
198 self._processed_headers = set()
199 self._processed_source_headers = set() # Header files included from .cc
Sami Kyostila0a34b032019-05-16 18:28:48 +0100200 self._include_re = re.compile(r'#include "(.*)"')
201
202 def add_target(self, target_name):
203 """Include |target_name| in the amalgamated result."""
204 self._dependency_tree.add_dependency(None, target_name)
205 self._add_target_dependencies(target_name)
206 self._add_target_flags(target_name)
Sami Kyostila7e8509f2019-05-29 12:36:24 +0100207 self._add_target_headers(target_name)
Sami Kyostila0a34b032019-05-16 18:28:48 +0100208
209 def _iterate_dep_edges(self, target_name):
210 target = self.desc[target_name]
211 for dep in target.get('deps', []):
212 # Ignore system libraries since they will be added as build-time
213 # dependencies.
214 if dep in system_library_map:
215 continue
216 # Don't descend into build action dependencies.
217 if self.desc[dep]['type'] == 'action':
218 continue
219 for sub_target, sub_dep in self._iterate_dep_edges(dep):
220 yield sub_target, sub_dep
221 yield target_name, dep
222
223 def _iterate_target_and_deps(self, target_name):
224 yield target_name
225 for _, dep in self._iterate_dep_edges(target_name):
226 yield dep
227
228 def _add_target_dependencies(self, target_name):
229 for target, dep in self._iterate_dep_edges(target_name):
230 self._dependency_tree.add_dependency(target, dep)
231
232 def process_dep(dep):
233 if dep in system_library_map:
234 self.libs.update(system_library_map[dep].get('libs', []))
235 self.cflags.update(system_library_map[dep].get('cflags', []))
236 self.defines.update(system_library_map[dep].get('defines', []))
237 return True
238
239 def walk_all_deps(target_name):
240 target = self.desc[target_name]
241 for dep in target.get('deps', []):
242 if process_dep(dep):
243 return
244 walk_all_deps(dep)
245 walk_all_deps(target_name)
246
247 def _filter_cflags(self, cflags):
248 # Since we want to deduplicate flags, combine two-part switches (e.g.,
249 # "-foo bar") into one value ("-foobar") so we can store the result as
250 # a set.
251 result = []
252 for flag in cflags:
253 if flag.startswith('-'):
254 result.append(flag)
255 else:
256 result[-1] += flag
257 return apply_whitelist(cflag_whitelist, result)
258
259 def _add_target_flags(self, target_name):
260 for target_name in self._iterate_target_and_deps(target_name):
261 target = self.desc[target_name]
262 self.cflags.update(self._filter_cflags(target.get('cflags', [])))
263 self.cflags.update(self._filter_cflags(target.get('cflags_cc', [])))
264 self.ldflags.update(
265 apply_whitelist(ldflag_whitelist, target.get('ldflags', [])))
266 self.libs.update(
267 apply_blacklist(lib_blacklist, target.get('libs', [])))
268 self.defines.update(
269 apply_whitelist(define_whitelist, target.get('defines', [])))
270
Sami Kyostila7e8509f2019-05-29 12:36:24 +0100271 def _add_target_headers(self, target_name):
272 target = self.desc[target_name]
273 if not 'sources' in target:
274 return
275 headers = [gn_utils.label_to_path(s)
276 for s in target['sources'] if s.endswith('.h')]
277 for header in headers:
278 self._add_header(target_name, header)
279
Sami Kyostila0a34b032019-05-16 18:28:48 +0100280 def _get_include_dirs(self, target_name):
281 include_dirs = set()
282 for target_name in self._iterate_target_and_deps(target_name):
283 target = self.desc[target_name]
284 if 'include_dirs' in target:
285 include_dirs.update(
Sami Kyostila3c88a1d2019-05-22 18:29:42 +0100286 [gn_utils.label_to_path(d) for d in target['include_dirs']])
Sami Kyostila0a34b032019-05-16 18:28:48 +0100287 return include_dirs
288
Sami Kyostila7e8509f2019-05-29 12:36:24 +0100289 def _add_source_included_header(
290 self, include_dirs, allowed_files, header_name):
291 if header_name in self._processed_source_headers:
Sami Kyostila0a34b032019-05-16 18:28:48 +0100292 return
Sami Kyostila7e8509f2019-05-29 12:36:24 +0100293 self._processed_source_headers.add(header_name)
Sami Kyostila0a34b032019-05-16 18:28:48 +0100294 for include_dir in include_dirs:
295 full_path = os.path.join(include_dir, header_name)
296 if os.path.exists(full_path):
297 if not full_path in allowed_files:
298 return
299 with open(full_path) as f:
Sami Kyostila7e8509f2019-05-29 12:36:24 +0100300 self.source.append(
Sami Kyostila0a34b032019-05-16 18:28:48 +0100301 '// %s begin header: %s' % (tool_name, full_path))
Primiano Tucci2e1ae922019-05-30 12:13:47 +0100302 self.source.extend(
Sami Kyostila7e8509f2019-05-29 12:36:24 +0100303 self._process_source_includes(
Primiano Tucci2e1ae922019-05-30 12:13:47 +0100304 include_dirs, allowed_files, f))
Sami Kyostila0a34b032019-05-16 18:28:48 +0100305 return
306 msg = 'Looked in %s' % ', '.join('"%s"' % d for d in include_dirs)
307 raise Error('Header file %s not found. %s' % (header_name, msg))
308
309 def _add_source(self, target_name, source_name):
Sami Kyostila7e8509f2019-05-29 12:36:24 +0100310 if source_name in self._processed_sources:
Sami Kyostila0a34b032019-05-16 18:28:48 +0100311 return
Sami Kyostila7e8509f2019-05-29 12:36:24 +0100312 self._processed_sources.add(source_name)
Sami Kyostila0a34b032019-05-16 18:28:48 +0100313 include_dirs = self._get_include_dirs(target_name)
314 deps = self.source_deps[source_name]
315 if not os.path.exists(source_name):
316 raise Error('Source file %s not found' % source_name)
317 with open(source_name) as f:
318 self.source.append(
319 '// %s begin source: %s' % (tool_name, source_name))
320 try:
321 self.source.extend(self._patch_source(source_name,
Sami Kyostila7e8509f2019-05-29 12:36:24 +0100322 self._process_source_includes(include_dirs, deps, f)))
Sami Kyostila0a34b032019-05-16 18:28:48 +0100323 except Error as e:
324 raise Error(
325 'Failed adding source %s: %s' % (source_name, e.message))
326
Sami Kyostila7e8509f2019-05-29 12:36:24 +0100327 def _add_header_included_header(self, include_dirs, header_name):
328 if header_name in self._processed_headers:
329 return
330 self._processed_headers.add(header_name)
331 for include_dir in include_dirs:
332 full_path = os.path.join(include_dir, header_name)
333 if os.path.exists(full_path):
334 with open(full_path) as f:
335 self.header.append(
336 '// %s begin header: %s' % (tool_name, full_path))
Primiano Tucci2e1ae922019-05-30 12:13:47 +0100337 self.header.extend(
338 self._process_header_includes(include_dirs, f))
Sami Kyostila7e8509f2019-05-29 12:36:24 +0100339 return
340 msg = 'Looked in %s' % ', '.join('"%s"' % d for d in include_dirs)
341 raise Error('Header file %s not found. %s' % (header_name, msg))
342
343 def _add_header(self, target_name, header_name):
344 if header_name in self._processed_headers:
345 return
346 self._processed_headers.add(header_name)
347 include_dirs = self._get_include_dirs(target_name)
348 if not os.path.exists(header_name):
349 raise Error('Header file %s not found' % source_name)
350 with open(header_name) as f:
351 self.header.append(
352 '// %s begin header: %s' % (tool_name, header_name))
353 try:
Primiano Tucci2e1ae922019-05-30 12:13:47 +0100354 self.header.extend(
355 self._process_header_includes(include_dirs, f))
Sami Kyostila7e8509f2019-05-29 12:36:24 +0100356 except Error as e:
357 raise Error(
358 'Failed adding header %s: %s' % (header_name, e.message))
359
Sami Kyostila0a34b032019-05-16 18:28:48 +0100360 def _patch_source(self, source_name, lines):
361 result = []
362 namespace = re.sub(r'[^a-z]', '_',
363 os.path.splitext(os.path.basename(source_name))[0])
364 for line in lines:
365 # Protobuf generates an identical anonymous function into each
366 # message description. Rename all but the first occurrence to avoid
367 # duplicate symbol definitions.
368 line = line.replace('MergeFromFail', '%s_MergeFromFail' % namespace)
369 result.append(line)
370 return result
371
Sami Kyostila7e8509f2019-05-29 12:36:24 +0100372 def _process_source_includes(self, include_dirs, allowed_files, file):
Sami Kyostila0a34b032019-05-16 18:28:48 +0100373 result = []
374 for line in file:
375 line = line.rstrip('\n')
376 m = self._include_re.match(line)
377 if not m:
378 result.append(line)
379 continue
380 elif re.match(includes_to_remove, m.group(1)):
381 result.append('// %s removed: %s' % (tool_name, line))
Sami Kyostilafd367762019-05-22 17:25:50 +0100382 else:
Sami Kyostila0a34b032019-05-16 18:28:48 +0100383 result.append('// %s expanded: %s' % (tool_name, line))
Sami Kyostila7e8509f2019-05-29 12:36:24 +0100384 self._add_source_included_header(
385 include_dirs, allowed_files, m.group(1))
386 return result
387
388 def _process_header_includes(self, include_dirs, file):
389 result = []
390 for line in file:
391 line = line.rstrip('\n')
392 m = self._include_re.match(line)
393 if not m:
394 result.append(line)
395 continue
396 elif re.match(includes_to_remove, m.group(1)):
397 result.append('// %s removed: %s' % (tool_name, line))
398 else:
399 result.append('// %s expanded: %s' % (tool_name, line))
400 self._add_header_included_header(include_dirs, m.group(1))
Sami Kyostila0a34b032019-05-16 18:28:48 +0100401 return result
402
403 def generate(self):
404 """Prepares the output for this amalgamated project.
405
406 Call save() to persist the result.
407 """
408
409 source_files = []
410 for node in self._dependency_tree.iterate_depth_first():
411 target = self.desc[node.target_name]
412 if not 'sources' in target:
413 continue
Sami Kyostila3c88a1d2019-05-22 18:29:42 +0100414 sources = [(node.target_name, gn_utils.label_to_path(s))
Sami Kyostila0a34b032019-05-16 18:28:48 +0100415 for s in target['sources'] if s.endswith('.cc')]
416 source_files.extend(sources)
417 for target_name, source_name in source_files:
418 self._add_source(target_name, source_name)
419
420 def _get_nice_path(self, prefix, format):
421 basename = os.path.basename(prefix)
422 return os.path.join(
423 os.path.relpath(os.path.dirname(prefix)), format % basename)
424
425 def save(self, output_prefix):
426 """Save the generated header and source file pair.
427
428 Returns a message describing the output with build instructions.
429 """
430 header_file = self._get_nice_path(output_prefix, '%s.h')
431 source_file = self._get_nice_path(output_prefix, '%s.cc')
432 with open(header_file, 'w') as f:
433 f.write('\n'.join([preamble] + self.header + ['\n']))
434 with open(source_file, 'w') as f:
435 include_stmt = '#include "%s"' % os.path.basename(header_file)
436 f.write('\n'.join([preamble, include_stmt] + self.source + ['\n']))
437 build_cmd = self.get_build_command(output_prefix)
438
439 return """Amalgamated project written to %s and %s.
440
441Build settings:
442 - cflags: %s
443 - ldflags: %s
444 - libs: %s
445 - defines: %s
446
447Example build command:
448
449%s
450""" % (header_file, source_file, ' '.join(self.cflags), ' '.join(self.ldflags),
451 ' '.join(self.libs), ' '.join(self.defines), ' '.join(build_cmd))
452
453 def get_build_command(self, output_prefix):
454 """Returns an example command line for building the output source."""
455 source = self._get_nice_path(output_prefix, '%s.cc')
456 library = self._get_nice_path(output_prefix, 'lib%s.so')
457 build_cmd = ['clang++', source, '-o', library, '-shared'] + \
458 sorted(self.cflags) + sorted(self.ldflags)
459 for lib in sorted(self.libs):
460 build_cmd.append('-l%s' % lib)
461 for define in sorted(self.defines):
462 build_cmd.append('-D%s' % define)
463 return build_cmd
464
465
466
Sami Kyostila0a34b032019-05-16 18:28:48 +0100467def create_amalgamated_project_for_targets(desc, targets, source_deps):
468 """Generate an amalgamated project for a list of GN targets."""
469 project = AmalgamatedProject(desc, source_deps)
470 for target in targets:
471 project.add_target(target)
472 project.generate()
473 return project
474
475
Sami Kyostila0a34b032019-05-16 18:28:48 +0100476def main():
477 parser = argparse.ArgumentParser(
478 description='Generate an amalgamated header/source pair from a GN '
479 'build description.')
480 parser.add_argument(
481 '--output',
482 help='Base name of files to create. A .cc/.h extension will be added',
Sami Kyostila3c88a1d2019-05-22 18:29:42 +0100483 default=os.path.join(gn_utils.repo_root(), 'perfetto'))
Sami Kyostila0a34b032019-05-16 18:28:48 +0100484 parser.add_argument(
485 '--gn_args', help='GN arguments used to prepare the output directory',
486 default=gn_args)
487 parser.add_argument(
Sami Kyostila468e61d2019-05-23 15:54:01 +0100488 '--keep', help='Don\'t delete the GN output directory at exit',
Sami Kyostila0a34b032019-05-16 18:28:48 +0100489 action='store_true')
490 parser.add_argument(
491 '--build', help='Also compile the generated files',
492 action='store_true')
493 parser.add_argument(
Sami Kyostila468e61d2019-05-23 15:54:01 +0100494 '--check', help='Don\'t keep the generated files',
495 action='store_true')
496 parser.add_argument('--quiet', help='Only report errors',
497 action='store_true')
498 parser.add_argument(
Sami Kyostila0a34b032019-05-16 18:28:48 +0100499 'targets',
500 nargs=argparse.REMAINDER,
501 help='Targets to include in the output (e.g., "//:libperfetto")')
502 args = parser.parse_args()
503 targets = args.targets or default_targets
504
Sami Kyostila468e61d2019-05-23 15:54:01 +0100505 output = args.output
506 if args.check:
507 output = os.path.join(tempfile.mkdtemp(), 'perfetto_amalgamated')
508
Sami Kyostila0a34b032019-05-16 18:28:48 +0100509 try:
Sami Kyostila468e61d2019-05-23 15:54:01 +0100510 if not args.quiet:
511 print('Building project...')
Sami Kyostila3c88a1d2019-05-22 18:29:42 +0100512 out = gn_utils.prepare_out_directory(
513 args.gn_args, 'tmp.gen_amalgamated')
514 desc = gn_utils.load_build_description(out)
Sami Kyostila0a34b032019-05-16 18:28:48 +0100515 # We need to build everything first so that the necessary header
516 # dependencies get generated.
Sami Kyostila3c88a1d2019-05-22 18:29:42 +0100517 gn_utils.build_targets(out, targets)
518 source_deps = gn_utils.compute_source_dependencies(out)
Sami Kyostila0a34b032019-05-16 18:28:48 +0100519 project = create_amalgamated_project_for_targets(
520 desc, targets, source_deps)
Sami Kyostila468e61d2019-05-23 15:54:01 +0100521 result = project.save(output)
522 if not args.quiet:
523 print(result)
Sami Kyostila0a34b032019-05-16 18:28:48 +0100524 if args.build:
Sami Kyostila468e61d2019-05-23 15:54:01 +0100525 if not args.quiet:
526 sys.stdout.write('Building amalgamated project...')
527 sys.stdout.flush()
528 subprocess.check_call(project.get_build_command(output))
529 if not args.quiet:
530 print('done')
Sami Kyostila0a34b032019-05-16 18:28:48 +0100531 finally:
532 if not args.keep:
533 shutil.rmtree(out)
Sami Kyostila468e61d2019-05-23 15:54:01 +0100534 if args.check:
535 shutil.rmtree(os.path.dirname(output))
Sami Kyostila0a34b032019-05-16 18:28:48 +0100536
537if __name__ == '__main__':
538 sys.exit(main())