blob: 2bfbb3d0b7ee2f90da49f9690815d08bbabdb5c3 [file] [log] [blame]
#!/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())