| #!/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', | 
 | ] | 
 |  | 
 | # 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 = '^//protos/.*(cpp|zero)$' | 
 |  | 
 | # 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).*$' | 
 |  | 
 | 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): | 
 |     """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. | 
 |         """ | 
 |     self.desc = desc | 
 |     self.source_deps = source_deps | 
 |     self.header = [] | 
 |     self.source = [] | 
 |     self.source_defines = [] | 
 |     # 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() | 
 |     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): | 
 |     if header_name in self._processed_headers: | 
 |       return | 
 |     if header_name in self._processed_source_headers: | 
 |       return | 
 |     self._processed_source_headers.add(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 | 
 |         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.message)) | 
 |  | 
 |   def _add_header_included_header(self, include_dirs, header_name): | 
 |     if header_name in self._processed_headers: | 
 |       return | 
 |     self._processed_headers.add(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): | 
 |         with open(full_path) as f: | 
 |           self.header.append('// %s begin header: %s' % | 
 |                              (tool_name, normalize_path(full_path))) | 
 |           self.header.extend(self._process_header_includes(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): | 
 |     if header_name in self._processed_headers: | 
 |       return | 
 |     self._processed_headers.add(header_name) | 
 |     include_dirs = self._get_include_dirs(target_name) | 
 |     full_path = os.path.join(gn_utils.repo_root(), header_name) | 
 |     if not os.path.exists(full_path): | 
 |       if self._compute_deps_only: | 
 |         return | 
 |       raise Error('Header file %s not found' % header_name) | 
 |     with open(full_path) as f: | 
 |       self.header.append('// %s begin header: %s' % | 
 |                          (tool_name, normalize_path(full_path))) | 
 |       try: | 
 |         self.header.extend(self._process_header_includes(include_dirs, f)) | 
 |       except Error as e: | 
 |         raise Error('Failed adding header %s: %s' % (header_name, e.message)) | 
 |  | 
 |   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 + ['\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 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=os.path.join(gn_utils.repo_root(), 'out/amalgamated/perfetto')) | 
 |   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( | 
 |       'targets', | 
 |       nargs=argparse.REMAINDER, | 
 |       help='Targets to include in the output (e.g., "//:libperfetto")') | 
 |   args = parser.parse_args() | 
 |   targets = args.targets or default_targets | 
 |  | 
 |   # 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]) | 
 |  | 
 |   output = args.output | 
 |   if args.check: | 
 |     output = os.path.join(tempfile.mkdtemp(), 'perfetto_amalgamated') | 
 |  | 
 |   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) | 
 |  | 
 |     # We need to build everything first so that the necessary header | 
 |     # dependencies get generated. However if we are just dumping dependency | 
 |     # information this can be skipped, allowing cross-platform operation. | 
 |     if not args.dump_deps: | 
 |       gn_utils.build_targets(out, targets, | 
 |                              system_buildtools=args.system_buildtools) | 
 |     source_deps = gn_utils.compute_source_dependencies(out, | 
 |                                                        args.system_buildtools) | 
 |     project = AmalgamatedProject( | 
 |         desc, source_deps, compute_deps_only=args.dump_deps) | 
 |  | 
 |     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, 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, | 
 |                                                       args.system_buildtools)) | 
 |       if not args.quiet: | 
 |         print('done') | 
 |   finally: | 
 |     if not args.keep: | 
 |       shutil.rmtree(out) | 
 |     if args.check: | 
 |       shutil.rmtree(os.path.dirname(output)) | 
 |  | 
 |  | 
 | if __name__ == '__main__': | 
 |   sys.exit(main()) |