blob: af3d3486a1191d128953fe1ac22d6efdd09a4b68 [file] [log] [blame] [edit]
#!/usr/bin/env python3
# Copyright (C) 2019 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This tool uses a collection of BUILD.gn files and build targets to generate
# an "amalgamated" C++ header and source file pair which compiles to an
# equivalent program. The tool also outputs the necessary compiler and linker
# flags needed to compile the resulting source code.
from __future__ import print_function
import argparse
import os
import re
import shutil
import subprocess
import sys
import tempfile
import gn_utils
# Default targets to include in the result.
# TODO(primiano): change this script to recurse into target deps when generating
# headers, but only for proto targets. .pbzero.h files don't include each other
# and we need to list targets here individually, which is unmaintainable.
default_targets = [
'//:libperfetto_client_experimental',
'//include/perfetto/protozero:protozero',
'//protos/perfetto/config:zero',
'//protos/perfetto/trace:zero',
]
c_sdk_targets = [
'//src/shared_lib:shared_lib',
]
# Preamble to add to the C SDK header file to disable shared library exports.
c_sdk_header_preamble = [
'// Enable GNU extensions for syscall() on Linux when compiling as C.',
'#if defined(__linux__) && !defined(_GNU_SOURCE)',
'#define _GNU_SOURCE',
'#endif',
'',
'// Disable shared library export annotations for static linking.',
'#define PERFETTO_SDK_DISABLE_SHLIB_EXPORT',
'',
]
# Filter for C SDK headers: only include headers from include/perfetto/public/
# All other headers (C++ internal dependencies) go in the source file.
c_sdk_header_filter = r'^include/perfetto/public/'
# Arguments for the GN output directory (unless overridden from the command
# line).
gn_args = ' '.join([
'enable_perfetto_ipc=true',
'enable_perfetto_zlib=false',
'is_debug=false',
'is_perfetto_build_generator=true',
'is_perfetto_embedder=true',
'perfetto_enable_git_rev_version_header=true',
'use_custom_libcxx=false',
])
# By default, the amalgamated .h only recurses in #includes but not in the
# target deps. In the case of protos we want to follow deps even in lieu of
# direct #includes. This is because, by design, protozero headers don't
# include each other but rely on forward declarations. The alternative would
# be adding each proto sub-target individually (e.g. //proto/trace/gpu:zero),
# but doing that is unmaintainable. We also do this for cpp bindings since some
# tracing SDK functions depend on them (and the system tracing IPC mechanism
# does so too).
recurse_in_header_deps = r'^//protos/.*(cpp|zero)$|^//include/perfetto/public.*$'
# Compiler flags which aren't filtered out.
cflag_allowlist = r'^-(W.*|fno-exceptions|fPIC|std.*|fvisibility.*)$'
# Linker flags which aren't filtered out.
ldflag_allowlist = r'^-()$'
# Libraries which are filtered out.
lib_denylist = r'^(c|gcc_eh)$'
# Macros which aren't filtered out.
define_allowlist = r'^(PERFETTO.*|GOOGLE_PROTOBUF.*)$'
# Includes which will be removed from the generated source.
includes_to_remove = r'^(gtest).*$'
# From //gn:default_config (since "gn desc" doesn't describe configs).
default_includes = [
'include',
]
default_cflags = [
# Since we're expanding header files into the generated source file, some
# constant may remain unused.
'-Wno-unused-const-variable'
]
# Build flags to satisfy a protobuf (lite or full) dependency.
protobuf_cflags = [
# Note that these point to the local copy of protobuf in buildtools. In
# reality the user of the amalgamated result will have to provide a path to
# an installed copy of the exact same version of protobuf which was used to
# generate the amalgamated build.
'-isystembuildtools/protobuf/src',
'-Lbuildtools/protobuf/src/.libs',
# We also need to disable some warnings for protobuf.
'-Wno-missing-prototypes',
'-Wno-missing-variable-declarations',
'-Wno-sign-conversion',
'-Wno-unknown-pragmas',
'-Wno-unused-macros',
]
# A mapping of dependencies to system libraries. Libraries in this map will not
# be built statically but instead added as dependencies of the amalgamated
# project.
system_library_map = {
'//buildtools:protobuf_full': {
'libs': ['protobuf'],
'cflags': protobuf_cflags,
},
'//buildtools:protobuf_lite': {
'libs': ['protobuf-lite'],
'cflags': protobuf_cflags,
},
'//buildtools:protoc_lib': {
'libs': ['protoc']
},
}
# ----------------------------------------------------------------------------
# End of configuration.
# ----------------------------------------------------------------------------
tool_name = os.path.basename(__file__)
project_root = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
preamble = """// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// This file is automatically generated by %s. Do not edit.
""" % tool_name
def apply_denylist(denylist, items):
return [item for item in items if not re.match(denylist, item)]
def apply_allowlist(allowlist, items):
return [item for item in items if re.match(allowlist, item)]
def normalize_path(path):
path = os.path.relpath(path, project_root)
path = re.sub(r'^out/[^/]+/', '', path)
return path
class Error(Exception):
pass
class DependencyNode(object):
"""A target in a GN build description along with its dependencies."""
def __init__(self, target_name):
self.target_name = target_name
self.dependencies = set()
def add_dependency(self, target_node):
if target_node in self.dependencies:
return
self.dependencies.add(target_node)
def iterate_depth_first(self):
for node in sorted(self.dependencies, key=lambda n: n.target_name):
for node in node.iterate_depth_first():
yield node
if self.target_name:
yield self
class DependencyTree(object):
"""A tree of GN build target dependencies."""
def __init__(self):
self.target_to_node_map = {}
self.root = self._get_or_create_node(None)
def _get_or_create_node(self, target_name):
if target_name in self.target_to_node_map:
return self.target_to_node_map[target_name]
node = DependencyNode(target_name)
self.target_to_node_map[target_name] = node
return node
def add_dependency(self, from_target, to_target):
from_node = self._get_or_create_node(from_target)
to_node = self._get_or_create_node(to_target)
assert from_node is not to_node
from_node.add_dependency(to_node)
def iterate_depth_first(self):
for node in self.root.iterate_depth_first():
yield node
class AmalgamatedProject(object):
"""In-memory representation of an amalgamated source/header pair."""
def __init__(self,
desc,
source_deps,
compute_deps_only=False,
header_preamble=None,
header_filter=None):
"""Constructor.
Args:
desc: JSON build description.
source_deps: A map of (source file, [dependency header]) which is
to detect which header files are included by each source file.
compute_deps_only: If True, the project will only be used to compute
dependency information. Use |get_source_files()| to retrieve
the result.
header_preamble: Optional list of lines to prepend to the header file.
header_filter: Optional regex pattern. Only headers matching this pattern
will be included in the header file; others go in the source file.
"""
self.desc = desc
self.source_deps = source_deps
self.header = []
self.source = []
self.source_defines = []
self.header_preamble = header_preamble or []
self.header_filter = header_filter
# Note that we don't support multi-arg flags.
self.cflags = set(default_cflags)
self.ldflags = set()
self.defines = set()
self.libs = set()
self._dependency_tree = DependencyTree()
self._processed_sources = set()
self._processed_headers = set()
self._processed_header_deps = set()
self._processed_source_headers = set() # Header files included from .cc
self._include_re = re.compile(r'#include "(.*)"')
self._compute_deps_only = compute_deps_only
def add_target(self, target_name):
"""Include |target_name| in the amalgamated result."""
self._dependency_tree.add_dependency(None, target_name)
self._add_target_dependencies(target_name)
self._add_target_flags(target_name)
self._add_target_headers(target_name)
# Recurse into target deps, but only for protos. This generates headers
# for all the .{pbzero,gen}.h files, even if they don't #include each other.
for _, dep in self._iterate_dep_edges(target_name):
if (dep not in self._processed_header_deps and
re.match(recurse_in_header_deps, dep)):
self._processed_header_deps.add(dep)
self.add_target(dep)
def _iterate_dep_edges(self, target_name):
target = self.desc[target_name]
for dep in target.get('deps', []):
# Ignore system libraries since they will be added as build-time
# dependencies.
if dep in system_library_map:
continue
# Don't descend into build action dependencies.
if self.desc[dep]['type'] == 'action':
continue
for sub_target, sub_dep in self._iterate_dep_edges(dep):
yield sub_target, sub_dep
yield target_name, dep
def _iterate_target_and_deps(self, target_name):
yield target_name
for _, dep in self._iterate_dep_edges(target_name):
yield dep
def _add_target_dependencies(self, target_name):
for target, dep in self._iterate_dep_edges(target_name):
self._dependency_tree.add_dependency(target, dep)
def process_dep(dep):
if dep in system_library_map:
self.libs.update(system_library_map[dep].get('libs', []))
self.cflags.update(system_library_map[dep].get('cflags', []))
self.defines.update(system_library_map[dep].get('defines', []))
return True
def walk_all_deps(target_name):
target = self.desc[target_name]
for dep in target.get('deps', []):
if process_dep(dep):
return
walk_all_deps(dep)
walk_all_deps(target_name)
def _filter_cflags(self, cflags):
# Since we want to deduplicate flags, combine two-part switches (e.g.,
# "-foo bar") into one value ("-foobar") so we can store the result as
# a set.
result = []
for flag in cflags:
if flag.startswith('-'):
result.append(flag)
else:
result[-1] += flag
return apply_allowlist(cflag_allowlist, result)
def _add_target_flags(self, target_name):
for target_name in self._iterate_target_and_deps(target_name):
target = self.desc[target_name]
self.cflags.update(self._filter_cflags(target.get('cflags', [])))
self.cflags.update(self._filter_cflags(target.get('cflags_cc', [])))
self.ldflags.update(
apply_allowlist(ldflag_allowlist, target.get('ldflags', [])))
self.libs.update(apply_denylist(lib_denylist, target.get('libs', [])))
self.defines.update(
apply_allowlist(define_allowlist, target.get('defines', [])))
def _add_target_headers(self, target_name):
target = self.desc[target_name]
if not 'sources' in target:
return
headers = [
gn_utils.label_to_path(s) for s in target['sources'] if s.endswith('.h')
]
for header in headers:
self._add_header(target_name, header)
def _get_include_dirs(self, target_name):
include_dirs = set(default_includes)
for target_name in self._iterate_target_and_deps(target_name):
target = self.desc[target_name]
if 'include_dirs' in target:
include_dirs.update(
[gn_utils.label_to_path(d) for d in target['include_dirs']])
return include_dirs
def _add_source_included_header(self, include_dirs, allowed_files,
header_name):
for include_dir in include_dirs:
rel_path = os.path.join(include_dir, header_name)
full_path = os.path.join(gn_utils.repo_root(), rel_path)
if os.path.exists(full_path):
if not rel_path in allowed_files:
return
if full_path in self._processed_headers:
return
if full_path in self._processed_source_headers:
return
self._processed_source_headers.add(full_path)
with open(full_path) as f:
self.source.append('// %s begin header: %s' %
(tool_name, normalize_path(full_path)))
self.source.extend(
self._process_source_includes(include_dirs, allowed_files, f))
return
if self._compute_deps_only:
return
msg = 'Looked in %s' % ', '.join('"%s"' % d for d in include_dirs)
raise Error('Header file %s not found. %s' % (header_name, msg))
def _add_source(self, target_name, source_name):
if source_name in self._processed_sources:
return
self._processed_sources.add(source_name)
include_dirs = self._get_include_dirs(target_name)
deps = self.source_deps[source_name]
full_path = os.path.join(gn_utils.repo_root(), source_name)
if not os.path.exists(full_path):
raise Error('Source file %s not found' % source_name)
with open(full_path) as f:
self.source.append('// %s begin source: %s' %
(tool_name, normalize_path(full_path)))
try:
self.source.extend(
self._patch_source(
source_name,
self._process_source_includes(include_dirs, deps, f)))
except Error as e:
raise Error('Failed adding source %s: %s' % (source_name, e))
def _should_header_go_in_header_file(self, header_path):
"""Returns True if header should go in .h file, False if it should go in .cc"""
if not self.header_filter:
return True
return re.match(self.header_filter, normalize_path(header_path)) is not None
def _add_header_included_header(self, include_dirs, header_name):
for include_dir in include_dirs:
full_path = os.path.join(gn_utils.repo_root(), include_dir, header_name)
if os.path.exists(full_path):
if full_path in self._processed_headers:
return
self._processed_headers.add(full_path)
# Check if this header should go in the header file or source file
if self._should_header_go_in_header_file(full_path):
output = self.header
process_func = self._process_header_includes
else:
# Headers that don't match the filter go in the source file
if full_path in self._processed_source_headers:
return
self._processed_source_headers.add(full_path)
output = self.source
process_func = lambda dirs, f: self._process_header_includes(dirs, f)
with open(full_path) as f:
output.append('// %s begin header: %s' %
(tool_name, normalize_path(full_path)))
output.extend(process_func(include_dirs, f))
return
if self._compute_deps_only:
return
msg = 'Looked in %s' % ', '.join('"%s"' % d for d in include_dirs)
raise Error('Header file %s not found. %s' % (header_name, msg))
def _add_header(self, target_name, header_name):
include_dirs = self._get_include_dirs(target_name)
full_path = os.path.join(gn_utils.repo_root(), header_name)
if full_path in self._processed_headers:
return
self._processed_headers.add(full_path)
if not os.path.exists(full_path):
if self._compute_deps_only:
return
raise Error('Header file %s not found' % header_name)
# Check if this header should go in the header file or source file
if self._should_header_go_in_header_file(full_path):
output = self.header
else:
# Headers that don't match the filter go in the source file
if full_path in self._processed_source_headers:
return
self._processed_source_headers.add(full_path)
output = self.source
with open(full_path) as f:
output.append('// %s begin header: %s' %
(tool_name, normalize_path(full_path)))
try:
output.extend(self._process_header_includes(include_dirs, f))
except Error as e:
raise Error('Failed adding header %s: %s' % (header_name, e))
def _patch_source(self, source_name, lines):
result = []
namespace = re.sub(r'[^a-z]', '_',
os.path.splitext(os.path.basename(source_name))[0])
for line in lines:
# Protobuf generates an identical anonymous function into each
# message description. Rename all but the first occurrence to avoid
# duplicate symbol definitions.
line = line.replace('MergeFromFail', '%s_MergeFromFail' % namespace)
result.append(line)
return result
def _process_source_includes(self, include_dirs, allowed_files, file):
result = []
for line in file:
line = line.rstrip('\n')
m = self._include_re.match(line)
if not m:
result.append(line)
continue
elif re.match(includes_to_remove, m.group(1)):
result.append('// %s removed: %s' % (tool_name, line))
else:
result.append('// %s expanded: %s' % (tool_name, line))
self._add_source_included_header(include_dirs, allowed_files,
m.group(1))
return result
def _process_header_includes(self, include_dirs, file):
result = []
for line in file:
line = line.rstrip('\n')
m = self._include_re.match(line)
if not m:
result.append(line)
continue
elif re.match(includes_to_remove, m.group(1)):
result.append('// %s removed: %s' % (tool_name, line))
else:
result.append('// %s expanded: %s' % (tool_name, line))
self._add_header_included_header(include_dirs, m.group(1))
return result
def generate(self):
"""Prepares the output for this amalgamated project.
Call save() to persist the result.
"""
assert not self._compute_deps_only
self.source_defines.append('// %s: predefined macros' % tool_name)
def add_define(name):
# Valued macros aren't supported for now.
assert '=' not in name
self.source_defines.append('#if !defined(%s)' % name)
self.source_defines.append('#define %s' % name)
self.source_defines.append('#endif')
for name in self.defines:
add_define(name)
for target_name, source_name in self.get_source_files():
self._add_source(target_name, source_name)
def get_source_files(self):
"""Return a list of (target, [source file]) that describes the source
files pulled in by each target which is a dependency of this project.
"""
source_files = []
for node in self._dependency_tree.iterate_depth_first():
target = self.desc[node.target_name]
if not 'sources' in target:
continue
sources = [(node.target_name, gn_utils.label_to_path(s))
for s in target['sources']
if s.endswith('.cc')]
source_files.extend(sources)
return source_files
def _get_nice_path(self, prefix, format):
basename = os.path.basename(prefix)
return os.path.join(
os.path.relpath(os.path.dirname(prefix)), format % basename)
def _make_directories(self, directory):
if not os.path.isdir(directory):
os.makedirs(directory)
def save(self, output_prefix, system_buildtools=False):
"""Save the generated header and source file pair.
Returns a message describing the output with build instructions.
"""
header_file = self._get_nice_path(output_prefix, '%s.h')
source_file = self._get_nice_path(output_prefix, '%s.cc')
self._make_directories(os.path.dirname(header_file))
self._make_directories(os.path.dirname(source_file))
with open(header_file, 'w') as f:
f.write('\n'.join([preamble] + self.header_preamble + self.header +
['\n']))
with open(source_file, 'w') as f:
include_stmt = '#include "%s"' % os.path.basename(header_file)
f.write('\n'.join([preamble] + self.source_defines + [include_stmt] +
self.source + ['\n']))
build_cmd = self.get_build_command(output_prefix, system_buildtools)
return """Amalgamated project written to %s and %s.
Build settings:
- cflags: %s
- ldflags: %s
- libs: %s
Example build command:
%s
""" % (header_file, source_file, ' '.join(self.cflags), ' '.join(
self.ldflags), ' '.join(self.libs), ' '.join(build_cmd))
def get_build_command(self, output_prefix, system_buildtools=False):
"""Returns an example command line for building the output source."""
source = self._get_nice_path(output_prefix, '%s.cc')
library = self._get_nice_path(output_prefix, 'lib%s.so')
if sys.platform.startswith('linux') and not system_buildtools:
llvm_script = os.path.join(gn_utils.repo_root(), 'gn', 'standalone',
'toolchain', 'linux_find_llvm.py')
cxx = subprocess.check_output([llvm_script]).splitlines()[2].decode()
else:
cxx = 'clang++'
build_cmd = [cxx, source, '-o', library, '-shared'] + \
sorted(self.cflags) + sorted(self.ldflags)
for lib in sorted(self.libs):
build_cmd.append('-l%s' % lib)
return build_cmd
def generate_sdk(targets,
output_prefix,
args,
out,
header_preamble=None,
header_filter=None):
source_deps = gn_utils.compute_source_dependencies(out,
args.system_buildtools)
project = AmalgamatedProject(
gn_utils.load_build_description(out, args.system_buildtools),
source_deps,
compute_deps_only=args.dump_deps,
header_preamble=header_preamble,
header_filter=header_filter)
for target in targets:
project.add_target(target)
if args.dump_deps:
source_files = [
source_file for _, source_file in project.get_source_files()
]
print('\n'.join(sorted(set(source_files))))
return
project.generate()
result = project.save(output_prefix, args.system_buildtools)
if not args.quiet:
print(result)
if args.build:
if not args.quiet:
sys.stdout.write('Building amalgamated project...')
sys.stdout.flush()
subprocess.check_call(
project.get_build_command(output_prefix, args.system_buildtools))
if not args.quiet:
print('done')
def main():
parser = argparse.ArgumentParser(
description='Generate an amalgamated header/source pair from a GN '
'build description.')
parser.add_argument(
'--out',
help='The name of the temporary build folder in \'out\'',
default='tmp.gen_amalgamated.%u' % os.getpid())
parser.add_argument(
'--output',
help='Base name of files to create. A .cc/.h extension will be added',
default=None)
parser.add_argument(
'--gn_args',
help='GN arguments used to prepare the output directory',
default=gn_args)
parser.add_argument(
'--keep',
help='Don\'t delete the GN output directory at exit',
action='store_true')
parser.add_argument(
'--build', help='Also compile the generated files', action='store_true')
parser.add_argument(
'--check', help='Don\'t keep the generated files', action='store_true')
parser.add_argument('--quiet', help='Only report errors', action='store_true')
parser.add_argument(
'--dump-deps',
help='List all source files that the amalgamated output depends on',
action='store_true')
parser.add_argument(
'--system_buildtools',
help='Use the buildtools (e.g. gn) preinstalled in the system instead '
'of the hermetic ones',
action='store_true')
parser.add_argument(
'--sdk',
choices=['c', 'cpp', 'all'],
default='all',
help='Which SDK to generate. Default: all')
parser.add_argument(
'targets',
nargs=argparse.REMAINDER,
help='Targets to include in the output (e.g., "//:libperfetto")')
args = parser.parse_args()
# The CHANGELOG mtime triggers the perfetto_version.gen.h genrule. This is
# to avoid emitting a stale version information in the remote case of somebody
# running gen_amalgamated incrementally after having moved to another commit.
changelog_path = os.path.join(project_root, 'CHANGELOG')
assert (os.path.exists(changelog_path))
subprocess.check_call(['touch', '-c', changelog_path])
# Determine base output path
if args.check:
base_output = tempfile.mkdtemp()
elif args.output is not None:
base_output = os.path.dirname(args.output)
base_name = os.path.basename(args.output)
else:
base_output = os.path.join(gn_utils.repo_root(), 'out/amalgamated')
base_name = 'perfetto'
sdks = []
if args.sdk in ['cpp', 'all']:
tgt = args.targets or default_targets
if args.check:
out_path = os.path.join(base_output, 'perfetto_amalgamated')
elif args.output is not None:
out_path = args.output
else:
out_path = os.path.join(base_output, 'perfetto')
sdks.append({
'targets': tgt,
'output': out_path,
'header_preamble': None,
'header_filter': None
})
if args.sdk in ['c', 'all']:
tgt = args.targets or c_sdk_targets
if args.check:
out_path = os.path.join(base_output, 'perfetto_c_amalgamated')
elif args.output is not None:
# When user specifies output path, append _c to the basename
out_path = os.path.join(
os.path.dirname(args.output),
os.path.basename(args.output) + '_c')
else:
out_path = os.path.join(base_output, 'perfetto_c')
sdks.append({
'targets': tgt,
'output': out_path,
'header_preamble': c_sdk_header_preamble,
'header_filter': c_sdk_header_filter
})
out = gn_utils.prepare_out_directory(
args.gn_args, args.out, system_buildtools=args.system_buildtools)
if not args.quiet:
print('Building project...')
try:
desc = gn_utils.load_build_description(out, args.system_buildtools)
# Build all targets first
all_targets = []
for sdk in sdks:
all_targets.extend(sdk['targets'])
if not args.dump_deps:
gn_utils.build_targets(
out, all_targets, system_buildtools=args.system_buildtools)
for sdk in sdks:
generate_sdk(sdk['targets'], sdk['output'], args, out,
sdk['header_preamble'], sdk['header_filter'])
finally:
if not args.keep:
shutil.rmtree(out)
if args.check:
for sdk in sdks:
shutil.rmtree(os.path.dirname(sdk['output']))
if __name__ == '__main__':
sys.exit(main())