Add an Android.bp generator
This patch adds a tool to automatically generate the Android.bp
blueprint file from the GN build files. The tool isn't used yet; it will
later be hooked up to a presubmit check which ensures Android.bp is kept
in sync with BUILD.gn changes.
TEST=gn desc out --format=json --all-toolchains "//*" > desc.json
tools/gen_android_bp desc.json //:perfetto_tests > .../external/perfetto/Android.bp
Change-Id: I81bc646f37a512bb278d135e1a571b8bfafce9a2
diff --git a/tools/gen_android_bp b/tools/gen_android_bp
new file mode 100755
index 0000000..28af300
--- /dev/null
+++ b/tools/gen_android_bp
@@ -0,0 +1,492 @@
+#!/usr/bin/env python
+# Copyright (C) 2017 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
+# Android.bp file for the Android Soong 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 Android.bp
+# build rules. The dependencies for the GN labels are squashed to the generated
+# Android.bp target, except for actions which get their own genrule. Some
+# libraries are also mapped to their Android equivalents -- see |builtin_deps|.
+
+import argparse
+import json
+import os
+import re
+import sys
+
+# All module names are prefixed with this string to avoid collisions.
+module_prefix = 'perfetto_'
+
+# Shared libraries which are directly translated to Android system equivalents.
+library_whitelist = [
+ 'android',
+ 'log',
+]
+
+# Name of the module which settings such as compiler flags for all other
+# modules.
+defaults_module = module_prefix + 'defaults'
+
+# Location of the project in the Android source tree.
+tree_path = 'external/perfetto'
+
+
+def enable_gmock(module):
+ module.static_libs.append('libgmock')
+
+
+def enable_gtest(module):
+ assert module.type == 'cc_test'
+
+
+def enable_protobuf_full(module):
+ module.shared_libs.append('libprotobuf-cpp-full')
+
+
+def enable_protobuf_lite(module):
+ module.shared_libs.append('libprotobuf-cpp-lite')
+
+
+def enable_protoc_lib(module):
+ module.shared_libs.append('libprotoc')
+
+
+def enable_libunwind(module):
+ module.shared_libs.append('libunwind')
+
+
+# Android equivalents for third-party libraries that the upstream project
+# depends on.
+builtin_deps = {
+ '//buildtools:gmock': enable_gmock,
+ '//buildtools:gtest': enable_gtest,
+ '//buildtools:gtest_main': enable_gtest,
+ '//buildtools:libunwind': enable_libunwind,
+ '//buildtools:protobuf_full': enable_protobuf_full,
+ '//buildtools:protobuf_lite': enable_protobuf_lite,
+ '//buildtools:protoc_lib': enable_protoc_lib,
+}
+
+# ----------------------------------------------------------------------------
+# End of configuration.
+# ----------------------------------------------------------------------------
+
+
+class Error(Exception):
+ pass
+
+
+class ThrowingArgumentParser(argparse.ArgumentParser):
+ def __init__(self, context):
+ super(ThrowingArgumentParser, self).__init__()
+ self.context = context
+
+ def error(self, message):
+ raise Error('%s: %s' % (self.context, message))
+
+
+class Module(object):
+ """A single module (e.g., cc_binary, cc_test) in a blueprint."""
+
+ def __init__(self, mod_type, name):
+ self.type = mod_type
+ self.name = name
+ self.srcs = []
+ self.comment = None
+ self.shared_libs = []
+ self.static_libs = []
+ self.tools = []
+ self.cmd = None
+ self.out = []
+ self.export_include_dirs = []
+ self.generated_headers = []
+ self.defaults = []
+ self.cflags = []
+ self.local_include_dirs = []
+
+ def to_string(self, output):
+ if self.comment:
+ output.append('// %s' % self.comment)
+ output.append('%s {' % self.type)
+ self._output_field(output, 'name')
+ self._output_field(output, 'srcs')
+ self._output_field(output, 'shared_libs')
+ self._output_field(output, 'static_libs')
+ self._output_field(output, 'tools')
+ self._output_field(output, 'cmd', sort=False)
+ self._output_field(output, 'out')
+ self._output_field(output, 'export_include_dirs')
+ self._output_field(output, 'generated_headers')
+ self._output_field(output, 'defaults')
+ self._output_field(output, 'cflags')
+ self._output_field(output, 'local_include_dirs')
+ output.append('}')
+ output.append('')
+
+ def _output_field(self, output, name, sort=True):
+ value = getattr(self, name)
+ if not value:
+ return
+ if isinstance(value, list):
+ output.append(' %s: [' % name)
+ for item in sorted(value) if sort else value:
+ output.append(' "%s",' % item)
+ output.append(' ],')
+ else:
+ output.append(' %s: "%s",' % (name, value))
+
+
+class Blueprint(object):
+ """In-memory representation of an Android.bp file."""
+
+ def __init__(self):
+ self.modules = {}
+
+ def add_module(self, module):
+ """Adds a new module to the blueprint, replacing any existing module
+ with the same name.
+
+ Args:
+ module: Module instance.
+ """
+ self.modules[module.name] = module
+
+ def to_string(self, output):
+ for m in self.modules.itervalues():
+ m.to_string(output)
+
+
+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_module_name(label):
+ """Turn a GN label (e.g., //:perfetto_tests) into a module name."""
+ label = re.sub(r'^//:?', '', label)
+ module = re.sub(r'[^a-zA-Z0-9_]', '_', label)
+ if not module.startswith(module_prefix):
+ return module_prefix + module
+ return module
+
+
+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_supported_source_file(name):
+ """Returns True if |name| can appear in a 'srcs' list."""
+ return os.path.splitext(name)[1] in ['.c', '.cc', '.proto']
+
+
+def is_generated_by_action(desc, label):
+ """Checks if a label is generated by an action.
+
+ Returns True if a GN output label |label| is an output for any action,
+ i.e., the file is generated dynamically.
+ """
+ for target in desc.itervalues():
+ if target['type'] == 'action' and label in target['outputs']:
+ return True
+ return False
+
+
+def apply_module_dependency(blueprint, desc, module, dep_name):
+ """Recursively collect dependencies for a given module.
+
+ Walk the transitive dependencies for a GN target and apply them to a given
+ module. This effectively flattens the dependency tree so that |module|
+ directly contains all the sources, libraries, etc. in the corresponding GN
+ dependency tree.
+
+ Args:
+ blueprint: Blueprint instance which is being generated.
+ desc: JSON GN description.
+ module: Module to which dependencies should be added.
+ dep_name: GN target of the dependency.
+ """
+ # Don't try to inject library/source dependencies into genrules because they
+ # are not compiled in the traditional sense.
+ if module.type == 'cc_genrule':
+ return
+
+ # If the dependency refers to a library which we can replace with an Android
+ # equivalent, stop recursing and patch the dependency in.
+ if label_without_toolchain(dep_name) in builtin_deps:
+ builtin_deps[label_without_toolchain(dep_name)](module)
+ return
+
+ # Similarly some shared libraries are directly mapped to Android
+ # equivalents.
+ target = desc[dep_name]
+ for lib in target.get('libs', []):
+ android_lib = 'lib' + lib
+ if lib in library_whitelist and not android_lib in module.shared_libs:
+ module.shared_libs.append(android_lib)
+
+ type = target['type']
+ if type == 'action':
+ create_modules_from_target(blueprint, desc, dep_name)
+ # Depend both on the generated sources and headers -- see
+ # make_genrules_for_action.
+ module.srcs.append(':' + label_to_module_name(dep_name))
+ module.generated_headers.append(
+ label_to_module_name(dep_name) + '_headers')
+ elif type in ['group', 'source_set', 'executable'] and 'sources' in target:
+ # Ignore source files that are generated by actions since they will be
+ # implicitly added by the genrule dependencies.
+ module.srcs.extend(
+ label_to_path(src) for src in target['sources']
+ if is_supported_source_file(src)
+ and not is_generated_by_action(desc, src))
+
+
+def make_genrules_for_action(blueprint, desc, target_name):
+ """Generate genrules for a GN action.
+
+ GN actions are used to dynamically generate files during the build. The
+ Soong equivalent is a genrule. This function turns a specific kind of
+ genrule which turns .proto files into source and header files into a pair
+ equivalent cc_genrules.
+
+ Args:
+ blueprint: Blueprint instance which is being generated.
+ desc: JSON GN description.
+ target_name: GN target for genrule generation.
+
+ Returns:
+ A (source_genrule, header_genrule) module tuple.
+ """
+ target = desc[target_name]
+
+ # We only support genrules which call protoc (with or without a plugin) to
+ # turn .proto files into header and source files.
+ args = target['args']
+ if not args[0].endswith('/protoc'):
+ raise Error('Unsupported action in target %s: %s' % (target_name,
+ target['args']))
+
+ # We create two genrules for each action: one for the protobuf headers and
+ # another for the sources. This is because the module that depends on the
+ # generated files needs to declare two different types of dependencies --
+ # source files in 'srcs' and headers in 'generated_headers' -- and it's not
+ # valid to generate .h files from a source dependency and vice versa.
+ source_module = Module('cc_genrule', label_to_module_name(target_name))
+ source_module.srcs.extend(label_to_path(src) for src in target['sources'])
+ source_module.tools = ['aprotoc']
+
+ header_module = Module('cc_genrule',
+ label_to_module_name(target_name) + '_headers')
+ header_module.srcs = source_module.srcs[:]
+ header_module.tools = source_module.tools[:]
+ header_module.export_include_dirs = ['.']
+
+ # TODO(skyostil): Is there a way to avoid hardcoding the tree path here?
+ # TODO(skyostil): Find a way to avoid creating the directory.
+ cmd = [
+ 'mkdir -p $(genDir)/%s &&' % tree_path, '$(location aprotoc)',
+ '--cpp_out=$(genDir)/%s' % tree_path,
+ '--proto_path=%s' % tree_path
+ ]
+ namespaces = ['pb']
+
+ parser = ThrowingArgumentParser('Action in target %s (%s)' %
+ (target_name, ' '.join(target['args'])))
+ parser.add_argument('--proto_path')
+ parser.add_argument('--cpp_out')
+ parser.add_argument('--plugin')
+ parser.add_argument('--plugin_out')
+ parser.add_argument('protos', nargs=argparse.REMAINDER)
+
+ args = parser.parse_args(args[1:])
+ if args.plugin:
+ _, plugin = os.path.split(args.plugin)
+ # TODO(skyostil): Can we detect this some other way?
+ if plugin == 'ipc_plugin':
+ namespaces.append('ipc')
+ elif plugin == 'protoc_plugin':
+ namespaces = ['pbzero']
+ for dep in target['deps']:
+ if desc[dep]['type'] != 'executable':
+ continue
+ _, executable = os.path.split(desc[dep]['outputs'][0])
+ if executable == plugin:
+ cmd += [
+ '--plugin=protoc-gen-plugin=$(location %s)' %
+ label_to_module_name(dep)
+ ]
+ source_module.tools.append(label_to_module_name(dep))
+ # Also make sure the module for the tool is generated.
+ create_modules_from_target(blueprint, desc, dep)
+ break
+ else:
+ raise Error('Unrecognized protoc plugin in target %s: %s' %
+ (target_name, args[i]))
+ if args.plugin_out:
+ plugin_args = args.plugin_out.split(':')[0]
+ cmd += ['--plugin_out=%s:$(genDir)/%s' % (plugin_args, tree_path)]
+
+ cmd += ['$(in)']
+ source_module.cmd = ' '.join(cmd)
+ header_module.cmd = source_module.cmd
+ header_module.tools = source_module.tools[:]
+
+ for ns in namespaces:
+ source_module.out += [
+ '%s/%s' % (tree_path, src.replace('.proto', '.%s.cc' % ns))
+ for src in source_module.srcs
+ ]
+ header_module.out += [
+ '%s/%s' % (tree_path, src.replace('.proto', '.%s.h' % ns))
+ for src in header_module.srcs
+ ]
+ return source_module, header_module
+
+
+def create_modules_from_target(blueprint, desc, target_name):
+ """Generate module(s) for a given GN target.
+
+ Given a GN target name, generate one or more corresponding modules into a
+ blueprint.
+
+ Args:
+ blueprint: Blueprint instance which is being generated.
+ desc: JSON GN description.
+ target_name: GN target for module generation.
+ """
+ target = desc[target_name]
+ if target['type'] == 'executable':
+ if 'host' in target['toolchain']:
+ module_type = 'cc_binary_host'
+ elif target.get('testonly'):
+ module_type = 'cc_test'
+ else:
+ module_type = 'cc_binary'
+ modules = [Module(module_type, label_to_module_name(target_name))]
+ elif target['type'] == 'action':
+ modules = make_genrules_for_action(blueprint, desc, target_name)
+ else:
+ raise Error('Unknown target type: %s' % target['type'])
+
+ for module in modules:
+ module.comment = 'GN target: %s' % target_name
+ if module.type != 'cc_genrule':
+ module.defaults = [defaults_module]
+
+ apply_module_dependency(blueprint, desc, module, target_name)
+ for dep in resolve_dependencies(desc, target_name):
+ apply_module_dependency(blueprint, desc, module, dep)
+
+ blueprint.add_module(module)
+
+
+def resolve_dependencies(desc, target_name):
+ """Return the transitive set of dependent-on targets for a GN target.
+
+ Args:
+ blueprint: Blueprint instance which is being generated.
+ desc: JSON GN description.
+
+ Returns:
+ A set of transitive dependencies in the form of GN targets.
+ """
+
+ if label_without_toolchain(target_name) in builtin_deps:
+ return set()
+ target = desc[target_name]
+ resolved_deps = set()
+ for dep in target.get('deps', []):
+ resolved_deps.add(dep)
+ # Ignore the transitive dependencies of actions because they are
+ # explicitly converted to genrules.
+ if desc[dep]['type'] == 'action':
+ continue
+ resolved_deps.update(resolve_dependencies(desc, dep))
+ return resolved_deps
+
+
+def create_blueprint_for_targets(desc, targets):
+ """Generate a blueprint for a list of GN targets."""
+ blueprint = Blueprint()
+
+ # Default settings used by all modules.
+ defaults = Module('cc_defaults', defaults_module)
+ defaults.local_include_dirs = ['include']
+ defaults.cflags = [
+ '-Wno-error=return-type',
+ '-Wno-sign-compare',
+ '-Wno-sign-promo',
+ '-Wno-unused-parameter',
+ ]
+
+ blueprint.add_module(defaults)
+ for target in targets:
+ create_modules_from_target(blueprint, desc, target)
+ return blueprint
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Generate Android.bp from a GN description.')
+ parser.add_argument(
+ 'desc',
+ help=
+ 'GN description (e.g., gn desc out --format=json --all-toolchains "//*"'
+ )
+ parser.add_argument(
+ 'targets',
+ nargs=argparse.REMAINDER,
+ help='Targets to include in the blueprint (e.g., "//:perfetto_tests")')
+ args = parser.parse_args()
+
+ with open(args.desc) as f:
+ desc = json.load(f)
+
+ blueprint = create_blueprint_for_targets(desc, args.targets)
+ output = [
+ """// Copyright (C) 2017 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.
+""" % (__file__)
+ ]
+ blueprint.to_string(output)
+ print '\n'.join(output)
+
+
+if __name__ == '__main__':
+ sys.exit(main())