#!/usr/bin/env python
|
# Copyright (C) 2018 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 translates a collection of BUILD.gn files into a mostly equivalent
|
# BUILD file for the Bazel build system. The input to the tool is a
|
# JSON description of the GN build definition generated with the following
|
# command:
|
#
|
# gn desc out --format=json --all-toolchains "//*" > desc.json
|
#
|
# The tool is then given a list of GN labels for which to generate Bazel
|
# build rules.
|
|
from __future__ import print_function
|
import argparse
|
import errno
|
import functools
|
import json
|
import os
|
import re
|
import shutil
|
import subprocess
|
import sys
|
import textwrap
|
|
# Copyright header for generated code.
|
header = """# 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 {}. Do not edit.
|
""".format(__file__)
|
|
# Arguments for the GN output directory.
|
# host_os="linux" is to generate the right build files from Mac OS.
|
gn_args = 'target_os="linux" is_debug=false host_os="linux"'
|
|
# Default targets to translate to the blueprint file.
|
default_targets = [
|
'//src/protozero:libprotozero',
|
'//src/trace_processor:trace_processor',
|
'//src/trace_processor:trace_processor_shell_host(//gn/standalone/toolchain:gcc_like_host)',
|
'//tools/trace_to_text:trace_to_text_host(//gn/standalone/toolchain:gcc_like_host)',
|
'//protos/perfetto/config:merged_config_gen',
|
'//protos/perfetto/trace:merged_trace_gen',
|
]
|
|
# Aliases to add to the BUILD file
|
alias_targets = {
|
'//src/protozero:libprotozero': 'libprotozero',
|
'//src/trace_processor:trace_processor': 'trace_processor',
|
'//src/trace_processor:trace_processor_shell_host': 'trace_processor_shell',
|
'//tools/trace_to_text:trace_to_text_host': 'trace_to_text',
|
}
|
|
|
def enable_sqlite(module):
|
module.deps.add(Label('//third_party/sqlite'))
|
module.deps.add(Label('//third_party/sqlite:sqlite_ext_percentile'))
|
|
|
def enable_jsoncpp(module):
|
module.deps.add(Label('//third_party/perfetto/google:jsoncpp'))
|
|
|
def enable_linenoise(module):
|
module.deps.add(Label('//third_party/perfetto/google:linenoise'))
|
|
|
def enable_gtest_prod(module):
|
module.deps.add(Label('//third_party/perfetto/google:gtest_prod'))
|
|
|
def enable_protobuf_full(module):
|
module.deps.add(Label('//third_party/protobuf:libprotoc'))
|
module.deps.add(Label('//third_party/protobuf'))
|
|
|
def enable_perfetto_version(module):
|
module.deps.add(Label('//third_party/perfetto/google:perfetto_version'))
|
|
|
def disable_module(module):
|
pass
|
|
|
# Internal equivalents for third-party libraries that the upstream project
|
# depends on.
|
builtin_deps = {
|
'//gn:jsoncpp_deps': enable_jsoncpp,
|
'//buildtools:linenoise': enable_linenoise,
|
'//buildtools:protobuf_lite': disable_module,
|
'//buildtools:protobuf_full': enable_protobuf_full,
|
'//buildtools:protoc': disable_module,
|
'//buildtools:sqlite': enable_sqlite,
|
'//gn:default_deps': disable_module,
|
'//gn:gtest_prod_config': enable_gtest_prod,
|
'//gn:protoc_lib_deps': enable_protobuf_full,
|
'//gn/standalone:gen_git_revision': enable_perfetto_version,
|
}
|
|
# ----------------------------------------------------------------------------
|
# End of configuration.
|
# ----------------------------------------------------------------------------
|
|
|
def check_output(cmd, cwd):
|
try:
|
output = subprocess.check_output(
|
cmd, stderr=subprocess.STDOUT, cwd=cwd)
|
except subprocess.CalledProcessError as e:
|
print('Cmd "{}" failed in {}:'.format(
|
' '.join(cmd), cwd), file=sys.stderr)
|
print(e.output)
|
exit(1)
|
else:
|
return output
|
|
|
class Error(Exception):
|
pass
|
|
|
def repo_root():
|
"""Returns an absolute path to the repository root."""
|
return os.path.join(
|
os.path.realpath(os.path.dirname(__file__)), os.path.pardir)
|
|
|
def create_build_description(repo_root):
|
"""Creates the JSON build description by running GN."""
|
|
out = os.path.join(repo_root, 'out', 'tmp.gen_build')
|
try:
|
try:
|
os.makedirs(out)
|
except OSError as e:
|
if e.errno != errno.EEXIST:
|
raise
|
check_output(
|
['gn', 'gen', out, '--args=%s' % gn_args], repo_root)
|
desc = check_output(
|
['gn', 'desc', out, '--format=json', '--all-toolchains', '//*'],
|
repo_root)
|
return json.loads(desc)
|
finally:
|
shutil.rmtree(out)
|
|
|
def label_to_path(label):
|
"""Turn a GN output label (e.g., //some_dir/file.cc) into a path."""
|
assert label.startswith('//')
|
return label[2:]
|
|
|
def label_to_target_name_with_path(label):
|
"""
|
Turn a GN label into a target name involving the full path.
|
e.g., //src/perfetto:tests -> src_perfetto_tests
|
"""
|
name = re.sub(r'^//:?', '', label)
|
name = re.sub(r'[^a-zA-Z0-9_]', '_', name)
|
return name
|
|
|
def label_without_toolchain(label):
|
"""Strips the toolchain from a GN label.
|
|
Return a GN label (e.g //buildtools:protobuf(//gn/standalone/toolchain:
|
gcc_like_host) without the parenthesised toolchain part.
|
"""
|
return label.split('(')[0]
|
|
|
def is_public_header(label):
|
"""
|
Returns if this is a c++ header file that is part of the API.
|
Args:
|
label: Label to evaluate
|
"""
|
return label.endswith('.h') and label.startswith('//include/perfetto/')
|
|
|
@functools.total_ordering
|
class Label(object):
|
"""Represents a label in BUILD file terminology. This class wraps a string
|
label to allow for correct comparision of labels for sorting.
|
|
Args:
|
label: The string rerepsentation of the label.
|
"""
|
|
def __init__(self, label):
|
self.label = label
|
|
def is_absolute(self):
|
return self.label.startswith('//')
|
|
def dirname(self):
|
return self.label.split(':')[0] if ':' in self.label else self.label
|
|
def basename(self):
|
return self.label.split(':')[1] if ':' in self.label else ''
|
|
def __eq__(self, other):
|
return self.label == other.label
|
|
def __lt__(self, other):
|
return (
|
self.is_absolute(),
|
self.dirname(),
|
self.basename()
|
) < (
|
other.is_absolute(),
|
other.dirname(),
|
other.basename()
|
)
|
|
def __str__(self):
|
return self.label
|
|
def __hash__(self):
|
return hash(self.label)
|
|
|
class Writer(object):
|
def __init__(self, output, width=79):
|
self.output = output
|
self.width = width
|
|
def comment(self, text):
|
for line in textwrap.wrap(text,
|
self.width - 2,
|
break_long_words=False,
|
break_on_hyphens=False):
|
self.output.write('# {}\n'.format(line))
|
|
def newline(self):
|
self.output.write('\n')
|
|
def line(self, s, indent=0):
|
self.output.write(' ' * indent + s + '\n')
|
|
def variable(self, key, value, sort=True):
|
if value is None:
|
return
|
if isinstance(value, set) or isinstance(value, list):
|
if len(value) == 0:
|
return
|
self.line('{} = ['.format(key), indent=1)
|
for v in sorted(list(value)) if sort else value:
|
self.line('"{}",'.format(v), indent=2)
|
self.line('],', indent=1)
|
elif isinstance(value, basestring):
|
self.line('{} = "{}",'.format(key, value), indent=1)
|
else:
|
self.line('{} = {},'.format(key, value), indent=1)
|
|
def header(self):
|
self.output.write(header)
|
|
|
class Target(object):
|
"""In-memory representation of a BUILD target."""
|
|
def __init__(self, type, name, gn_name=None):
|
assert type in ('cc_binary', 'cc_library', 'cc_proto_library',
|
'proto_library', 'filegroup', 'alias',
|
'pbzero_cc_proto_library', 'genrule', )
|
self.type = type
|
self.name = name
|
self.srcs = set()
|
self.hdrs = set()
|
self.deps = set()
|
self.visibility = set()
|
self.gn_name = gn_name
|
self.is_pbzero = False
|
self.src_proto_library = None
|
self.outs = set()
|
self.cmd = None
|
self.tools = set()
|
|
def write(self, writer):
|
if self.gn_name:
|
writer.comment('GN target: {}'.format(self.gn_name))
|
|
writer.line('{}('.format(self.type))
|
writer.variable('name', self.name)
|
writer.variable('srcs', self.srcs)
|
writer.variable('hdrs', self.hdrs)
|
|
if self.type == 'proto_library' and not self.is_pbzero:
|
if self.srcs:
|
writer.variable('has_services', 1)
|
writer.variable('cc_api_version', 2)
|
if self.srcs:
|
writer.variable('cc_generic_services', 1)
|
|
writer.variable('src_proto_library', self.src_proto_library)
|
|
writer.variable('outs', self.outs)
|
writer.variable('cmd', self.cmd)
|
writer.variable('tools', self.tools)
|
|
# Keep visibility and deps last.
|
writer.variable('visibility', self.visibility)
|
|
if type != 'filegroup':
|
writer.variable('deps', self.deps)
|
|
writer.line(')')
|
|
|
class Build(object):
|
"""In-memory representation of a BUILD file."""
|
|
def __init__(self, public, header_lines=[]):
|
self.targets = {}
|
self.public = public
|
self.header_lines = header_lines
|
|
def add_target(self, target):
|
self.targets[target.name] = target
|
|
def write(self, writer):
|
writer.header()
|
writer.newline()
|
for line in self.header_lines:
|
writer.line(line)
|
if self.header_lines:
|
writer.newline()
|
if self.public:
|
writer.line(
|
'package(default_visibility = ["//visibility:public"])')
|
else:
|
writer.line(
|
'package(default_visibility = ["//third_party/perfetto:__subpackages__"])')
|
writer.newline()
|
writer.line('licenses(["notice"]) # Apache 2.0')
|
writer.newline()
|
writer.line('exports_files(["LICENSE"])')
|
writer.newline()
|
|
sorted_targets = sorted(
|
self.targets.itervalues(), key=lambda m: m.name)
|
for target in sorted_targets[:-1]:
|
target.write(writer)
|
writer.newline()
|
|
# BUILD files shouldn't have a trailing new line.
|
sorted_targets[-1].write(writer)
|
|
|
class BuildGenerator(object):
|
def __init__(self, desc):
|
self.desc = desc
|
self.action_generated_files = set()
|
|
for target in self.desc.itervalues():
|
if target['type'] == 'action':
|
self.action_generated_files.update(target['outputs'])
|
|
|
def create_build_for_targets(self, targets):
|
"""Generate a BUILD for a list of GN targets and aliases."""
|
self.build = Build(public=True)
|
|
proto_cc_import = 'load("//tools/build_defs/proto/cpp:cc_proto_library.bzl", "cc_proto_library")'
|
pbzero_cc_import = 'load("//third_party/perfetto/google:build_defs.bzl", "pbzero_cc_proto_library")'
|
self.proto_build = Build(public=False, header_lines=[
|
proto_cc_import, pbzero_cc_import])
|
|
for target in targets:
|
self.create_target(target)
|
|
return (self.build, self.proto_build)
|
|
|
def resolve_dependencies(self, target_name):
|
"""Return the set of direct dependent-on targets for a GN target.
|
|
Args:
|
desc: JSON GN description.
|
target_name: Name of target
|
|
Returns:
|
A set of transitive dependencies in the form of GN targets.
|
"""
|
|
if label_without_toolchain(target_name) in builtin_deps:
|
return set()
|
target = self.desc[target_name]
|
resolved_deps = set()
|
for dep in target.get('deps', []):
|
resolved_deps.add(dep)
|
return resolved_deps
|
|
|
def apply_module_sources_to_target(self, target, module_desc):
|
"""
|
Args:
|
target: Module to which dependencies should be added.
|
module_desc: JSON GN description of the module.
|
visibility: Whether the module is visible with respect to the target.
|
"""
|
for src in module_desc['sources']:
|
label = Label(label_to_path(src))
|
if target.type == 'cc_library' and is_public_header(src):
|
target.hdrs.add(label)
|
else:
|
target.srcs.add(label)
|
|
|
def apply_module_dependency(self, target, dep_name):
|
"""
|
Args:
|
build: BUILD instance which is being generated.
|
proto_build: BUILD instance which is being generated to hold protos.
|
desc: JSON GN description.
|
target: Module to which dependencies should be added.
|
dep_name: GN target of the dependency.
|
"""
|
# If the dependency refers to a library which we can replace with an internal
|
# equivalent, stop recursing and patch the dependency in.
|
dep_name_no_toolchain = label_without_toolchain(dep_name)
|
if dep_name_no_toolchain in builtin_deps:
|
builtin_deps[dep_name_no_toolchain](target)
|
return
|
|
dep_desc = self.desc[dep_name]
|
if dep_desc['type'] == 'source_set':
|
for inner_name in self.resolve_dependencies(dep_name):
|
self.apply_module_dependency(target, inner_name)
|
|
# Any source set which has a source generated by an action doesn't need
|
# to be depended on as we will depend on the action directly.
|
if any(src in self.action_generated_files for src in dep_desc['sources']):
|
return
|
|
self.apply_module_sources_to_target(target, dep_desc)
|
elif dep_desc['type'] == 'action':
|
args = dep_desc['args']
|
if "gen_merged_sql_metrics" in dep_name:
|
dep_target = self.create_merged_sql_metrics_target(dep_name)
|
target.deps.add(Label("//third_party/perfetto:" + dep_target.name))
|
|
if target.type == 'cc_library' or target.type == 'cc_binary':
|
target.srcs.update(dep_target.outs)
|
elif args[0].endswith('/protoc'):
|
(proto_target, cc_target) = self.create_proto_target(dep_name)
|
if target.type == 'proto_library':
|
dep_target_name = proto_target.name
|
else:
|
dep_target_name = cc_target.name
|
target.deps.add(
|
Label("//third_party/perfetto/protos:" + dep_target_name))
|
else:
|
raise Error('Unsupported action in target %s: %s' % (dep_target_name,
|
args))
|
elif dep_desc['type'] == 'static_library':
|
dep_target = self.create_target(dep_name)
|
target.deps.add(Label("//third_party/perfetto:" + dep_target.name))
|
elif dep_desc['type'] == 'group':
|
for inner_name in self.resolve_dependencies(dep_name):
|
self.apply_module_dependency(target, inner_name)
|
elif dep_desc['type'] == 'executable':
|
# Just create the dep target but don't add it as a dep because it's an
|
# executable.
|
self.create_target(dep_name)
|
else:
|
raise Error('Unknown target name %s with type: %s' %
|
(dep_name, dep_desc['type']))
|
|
def create_merged_sql_metrics_target(self, gn_target_name):
|
target_desc = self.desc[gn_target_name]
|
gn_target_name_no_toolchain = label_without_toolchain(gn_target_name)
|
target = Target(
|
'genrule',
|
'gen_merged_sql_metrics',
|
gn_name=gn_target_name_no_toolchain,
|
)
|
target.outs.update(
|
Label(src[src.index('gen/') + len('gen/'):])
|
for src in target_desc.get('outputs', [])
|
)
|
target.cmd = '$(location gen_merged_sql_metrics_py) --cpp_out=$@ $(SRCS)'
|
target.tools.update([
|
'gen_merged_sql_metrics_py',
|
])
|
target.srcs.update(
|
Label(label_to_path(src))
|
for src in target_desc.get('inputs', [])
|
if src not in self.action_generated_files
|
)
|
self.build.add_target(target)
|
return target
|
|
def create_proto_target(self, gn_target_name):
|
target_desc = self.desc[gn_target_name]
|
args = target_desc['args']
|
|
gn_target_name_no_toolchain = label_without_toolchain(gn_target_name)
|
stripped_path = gn_target_name_no_toolchain.replace("protos/perfetto/", "")
|
pretty_target_name = label_to_target_name_with_path(stripped_path)
|
pretty_target_name = pretty_target_name.replace("_lite_gen", "")
|
pretty_target_name = pretty_target_name.replace("_zero_gen", "_zero")
|
|
proto_target = Target(
|
'proto_library',
|
pretty_target_name,
|
gn_name=gn_target_name_no_toolchain
|
)
|
proto_target.is_pbzero = any("pbzero" in arg for arg in args)
|
proto_target.srcs.update([
|
Label(label_to_path(src).replace('protos/', ''))
|
for src in target_desc.get('sources', [])
|
])
|
if not proto_target.is_pbzero:
|
proto_target.visibility.add("//visibility:public")
|
self.proto_build.add_target(proto_target)
|
|
for dep_name in self.resolve_dependencies(gn_target_name):
|
self.apply_module_dependency(proto_target, dep_name)
|
|
if proto_target.is_pbzero:
|
# Remove all the protozero srcs from the proto_library.
|
proto_target.srcs.difference_update(
|
[src for src in proto_target.srcs if not src.label.endswith('.proto')])
|
|
# Remove all the non-proto deps from the proto_library and add to the cc
|
# library.
|
cc_deps = [
|
dep for dep in proto_target.deps
|
if not dep.label.startswith('//third_party/perfetto/protos')
|
]
|
proto_target.deps.difference_update(cc_deps)
|
|
cc_target_name = proto_target.name + "_cc_proto"
|
cc_target = Target('pbzero_cc_proto_library', cc_target_name,
|
gn_name=gn_target_name_no_toolchain)
|
|
cc_target.deps.add(Label('//third_party/perfetto:libprotozero'))
|
cc_target.deps.update(cc_deps)
|
|
# Add the proto_library to the cc_target.
|
cc_target.src_proto_library = \
|
"//third_party/perfetto/protos:" + proto_target.name
|
|
self.proto_build.add_target(cc_target)
|
else:
|
cc_target_name = proto_target.name + "_cc_proto"
|
cc_target = Target('cc_proto_library',
|
cc_target_name, gn_name=gn_target_name_no_toolchain)
|
cc_target.visibility.add("//visibility:public")
|
cc_target.deps.add(
|
Label("//third_party/perfetto/protos:" + proto_target.name))
|
self.proto_build.add_target(cc_target)
|
|
return (proto_target, cc_target)
|
|
|
def create_target(self, gn_target_name):
|
"""Generate module(s) for a given GN target.
|
|
Given a GN target name, generate one or more corresponding modules into a
|
build file.
|
|
Args:
|
build: Build instance which is being generated.
|
desc: JSON GN description.
|
gn_target_name: GN target name for module generation.
|
"""
|
|
target_desc = self.desc[gn_target_name]
|
if target_desc['type'] == 'action':
|
args = target_desc['args']
|
if args[0].endswith('/protoc'):
|
return self.create_proto_target(gn_target_name)
|
else:
|
raise Error('Unsupported action in target %s: %s' % (gn_target_name,
|
args))
|
elif target_desc['type'] == 'executable':
|
target_type = 'cc_binary'
|
elif target_desc['type'] == 'static_library':
|
target_type = 'cc_library'
|
elif target_desc['type'] == 'source_set':
|
target_type = 'filegroup'
|
else:
|
raise Error('Unknown target type: %s' % target_desc['type'])
|
|
label_no_toolchain = label_without_toolchain(gn_target_name)
|
target_name_path = label_to_target_name_with_path(label_no_toolchain)
|
target_name = alias_targets.get(label_no_toolchain, target_name_path)
|
target = Target(target_type, target_name, gn_name=label_no_toolchain)
|
target.srcs.update(
|
Label(label_to_path(src))
|
for src in target_desc.get('sources', [])
|
if src not in self.action_generated_files
|
)
|
|
for dep_name in self.resolve_dependencies(gn_target_name):
|
self.apply_module_dependency(target, dep_name)
|
|
self.build.add_target(target)
|
return target
|
|
def main():
|
parser = argparse.ArgumentParser(
|
description='Generate BUILD from a GN description.')
|
parser.add_argument(
|
'--desc',
|
help='GN description (e.g., gn desc out --format=json --all-toolchains "//*"'
|
)
|
parser.add_argument(
|
'--repo-root',
|
help='Standalone Perfetto repository to generate a GN description',
|
default=repo_root(),
|
)
|
parser.add_argument(
|
'--extras',
|
help='Extra targets to include at the end of the BUILD file',
|
default=os.path.join(repo_root(), 'BUILD.extras'),
|
)
|
parser.add_argument(
|
'--output',
|
help='BUILD file to create',
|
default=os.path.join(repo_root(), 'BUILD'),
|
)
|
parser.add_argument(
|
'--output-proto',
|
help='Proto BUILD file to create',
|
default=os.path.join(repo_root(), 'protos', 'BUILD'),
|
)
|
parser.add_argument(
|
'targets',
|
nargs=argparse.REMAINDER,
|
help='Targets to include in the BUILD file (e.g., "//:perfetto_tests")')
|
args = parser.parse_args()
|
|
if args.desc:
|
with open(args.desc) as f:
|
desc = json.load(f)
|
else:
|
desc = create_build_description(args.repo_root)
|
|
build_generator = BuildGenerator(desc)
|
build, proto_build = build_generator.create_build_for_targets(
|
args.targets or default_targets)
|
with open(args.output, 'w') as f:
|
writer = Writer(f)
|
build.write(writer)
|
writer.newline()
|
|
with open(args.extras, 'r') as r:
|
for line in r:
|
writer.line(line.rstrip("\n\r"))
|
|
with open(args.output_proto, 'w') as f:
|
proto_build.write(Writer(f))
|
|
return 0
|
|
|
if __name__ == '__main__':
|
sys.exit(main())
|