blob: 45edbb0f9e81b86938098a8507fee58c81d76762 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright (C) 2022 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 collections
import json
import logging as log
import operator
import os
import re
import sys
import copy
import gn_utils
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Default targets to translate to the blueprint file.
default_targets = [
'//components/cronet/android:cronet',
'//components/cronet:cronet_package',
]
# Defines a custom init_rc argument to be applied to the corresponding output
# blueprint target.
target_initrc = {
# TODO: this can probably be removed.
}
target_host_supported = [
# TODO: remove if this is not useful for the cronet build.
]
# Proto target groups which will be made public.
proto_groups = {
# TODO: remove if this is not used for the cronet build.
}
# All module names are prefixed with this string to avoid collisions.
module_prefix = 'cronet_aml_'
# Shared libraries which are directly translated to Android system equivalents.
shared_library_allowlist = [
'android',
'android.hardware.atrace@1.0',
'android.hardware.health@2.0',
'android.hardware.health-V1-ndk',
'android.hardware.power.stats@1.0',
"android.hardware.power.stats-V1-cpp",
'base',
'binder',
'binder_ndk',
'cutils',
'hidlbase',
'hidltransport',
'hwbinder',
'incident',
'log',
'services',
'statssocket',
"tracingproxy",
'utils',
]
# Static libraries which are directly translated to Android system equivalents.
static_library_allowlist = [
'statslog_perfetto',
]
# Include directories that will be removed from all targets.
local_include_dirs_denylist = [
]
# 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/chromium_org'
# Path for the protobuf sources in the standalone build.
buildtools_protobuf_src = '//buildtools/protobuf/src'
# Location of the protobuf src dir in the Android source tree.
android_protobuf_src = 'external/protobuf/src'
# put all args on a new line for better diffs.
NEWLINE = ' " +\n "'
# Compiler flags which are passed through to the blueprint.
cflag_allowlist = [
# needed for zlib:zlib
"-mpclmul",
# needed for zlib:zlib
"-mssse3",
# needed for zlib:zlib
"-msse3",
# needed for zlib:zlib
"-msse4.2",
]
# Additional arguments to apply to Android.bp rules.
additional_args = {
# TODO: remove if not needed.
'cronet_aml_components_cronet_android_cronet': [
('linker_scripts', {
'base/android/library_loader/anchor_functions.lds',
}),
],
'cronet_aml_net_net': [
('export_static_lib_headers', {
'cronet_aml_net_third_party_quiche_quiche',
'cronet_aml_crypto_crypto',
}),
],
}
# Android equivalents for third-party libraries that the upstream project
# depends on.
builtin_deps = {
'//net/tools/root_store_tool:root_store_tool':
lambda x: None,
}
# Name of tethering apex module
tethering_apex = "com.android.tethering"
# ----------------------------------------------------------------------------
# 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))
def write_blueprint_key_value(output, name, value, sort=True):
"""Writes a Blueprint key-value pair to the output"""
if isinstance(value, bool):
if value:
output.append(' %s: true,' % name)
else:
output.append(' %s: false,' % name)
return
if not value:
return
if isinstance(value, set):
value = sorted(value)
if isinstance(value, list):
output.append(' %s: [' % name)
for item in sorted(value) if sort else value:
output.append(' "%s",' % item)
output.append(' ],')
return
if isinstance(value, Target):
value.to_string(output)
return
if isinstance(value, dict):
kv_output = []
for k, v in value.items():
write_blueprint_key_value(kv_output, k, v)
output.append(' %s: {' % name)
for line in kv_output:
output.append(' %s' % line)
output.append(' },')
return
output.append(' %s: "%s",' % (name, value))
class Target(object):
"""A target-scoped part of a module"""
def __init__(self, name):
self.name = name
self.srcs = set()
self.shared_libs = set()
self.static_libs = set()
self.whole_static_libs = set()
self.header_libs = set()
self.cflags = set()
self.dist = dict()
self.strip = dict()
self.stl = None
self.cppflags = set()
self.local_include_dirs = set()
self.export_system_include_dirs = set()
self.generated_headers = set()
self.export_generated_headers = set()
def to_string(self, output):
nested_out = []
self._output_field(nested_out, 'srcs')
self._output_field(nested_out, 'shared_libs')
self._output_field(nested_out, 'static_libs')
self._output_field(nested_out, 'whole_static_libs')
self._output_field(nested_out, 'header_libs')
self._output_field(nested_out, 'cflags')
self._output_field(nested_out, 'stl')
self._output_field(nested_out, 'dist')
self._output_field(nested_out, 'strip')
self._output_field(nested_out, 'cppflags')
self._output_field(nested_out, 'local_include_dirs')
self._output_field(nested_out, 'export_system_include_dirs')
self._output_field(nested_out, 'generated_headers')
self._output_field(nested_out, 'export_generated_headers')
if nested_out:
output.append(' %s: {' % self.name)
for line in nested_out:
output.append(' %s' % line)
output.append(' },')
def _output_field(self, output, name, sort=True):
value = getattr(self, name)
return write_blueprint_key_value(output, name, value, sort)
class Module(object):
"""A single module (e.g., cc_binary, cc_test) in a blueprint."""
def __init__(self, mod_type, name, gn_target):
self.type = mod_type
self.gn_target = gn_target
self.name = name
self.srcs = set()
self.comment = 'GN: ' + gn_target
self.shared_libs = set()
self.static_libs = set()
self.whole_static_libs = set()
self.runtime_libs = set()
self.tools = set()
self.cmd = None
self.host_supported = False
self.device_supported = True
self.vendor_available = False
self.init_rc = set()
self.out = set()
self.export_include_dirs = set()
self.generated_headers = set()
self.export_generated_headers = set()
self.export_static_lib_headers = set()
self.defaults = set()
self.cflags = set()
self.include_dirs = set()
self.local_include_dirs = set()
self.header_libs = set()
self.required = set()
self.tool_files = set()
# target contains a dict of Targets indexed by os_arch.
# example: { 'android_x86': Target('android_x86')
self.target = dict()
self.target['android'] = Target('android')
self.target['android_x86'] = Target('android_x86')
self.target['android_x86_64'] = Target('android_x86_64')
self.target['android_arm'] = Target('android_arm')
self.target['android_arm64'] = Target('android_arm64')
self.target['host'] = Target('host')
self.stl = None
self.cpp_std = None
self.dist = dict()
self.strip = dict()
self.data = set()
self.apex_available = set()
self.min_sdk_version = None
self.proto = dict()
self.linker_scripts = set()
# The genrule_XXX below are properties that must to be propagated back
# on the module(s) that depend on the genrule.
self.genrule_headers = set()
self.genrule_srcs = set()
self.genrule_shared_libs = set()
self.genrule_header_libs = set()
self.version_script = None
self.test_suites = set()
self.test_config = None
self.stubs = {}
self.cppflags = set()
self.rtti = False
# Name of the output. Used for setting .so file name for libcronet
self.stem = None
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, 'whole_static_libs')
self._output_field(output, 'runtime_libs')
self._output_field(output, 'tools')
self._output_field(output, 'cmd', sort=False)
if self.host_supported:
self._output_field(output, 'host_supported')
if not self.device_supported:
self._output_field(output, 'device_supported')
if self.vendor_available:
self._output_field(output, 'vendor_available')
self._output_field(output, 'init_rc')
self._output_field(output, 'out')
self._output_field(output, 'export_include_dirs')
self._output_field(output, 'generated_headers')
self._output_field(output, 'export_generated_headers')
self._output_field(output, 'export_static_lib_headers')
self._output_field(output, 'defaults')
self._output_field(output, 'cflags')
self._output_field(output, 'include_dirs')
self._output_field(output, 'local_include_dirs')
self._output_field(output, 'header_libs')
self._output_field(output, 'required')
self._output_field(output, 'dist')
self._output_field(output, 'strip')
self._output_field(output, 'tool_files')
self._output_field(output, 'data')
self._output_field(output, 'stl')
self._output_field(output, 'cpp_std')
self._output_field(output, 'apex_available')
self._output_field(output, 'min_sdk_version')
self._output_field(output, 'version_script')
self._output_field(output, 'test_suites')
self._output_field(output, 'test_config')
self._output_field(output, 'stubs')
self._output_field(output, 'proto')
self._output_field(output, 'linker_scripts')
self._output_field(output, 'cppflags')
self._output_field(output, 'stem')
if self.rtti:
self._output_field(output, 'rtti')
target_out = []
for arch, target in sorted(self.target.items()):
# _output_field calls getattr(self, arch).
setattr(self, arch, target)
self._output_field(target_out, arch)
if target_out:
output.append(' target: {')
for line in target_out:
output.append(' %s' % line)
output.append(' },')
output.append('}')
output.append('')
def add_android_static_lib(self, lib):
if self.type == 'cc_binary_host':
raise Exception('Adding Android static lib for host tool is unsupported')
elif self.host_supported:
self.target['android'].static_libs.add(lib)
else:
self.static_libs.add(lib)
def add_android_shared_lib(self, lib):
if self.type == 'cc_binary_host':
raise Exception('Adding Android shared lib for host tool is unsupported')
elif self.host_supported:
self.target['android'].shared_libs.add(lib)
else:
self.shared_libs.add(lib)
def _output_field(self, output, name, sort=True):
value = getattr(self, name)
return write_blueprint_key_value(output, name, value, sort)
def is_compiled(self):
return self.type not in ('cc_genrule', 'filegroup', 'java_genrule')
def is_genrule(self):
return self.type == "cc_genrule"
def has_input_files(self):
return len(self.srcs) > 0 or any([len(target.srcs) > 0 for target in self.target.values()])
def merge_attribute(self, key, source_module, allowed_archs, source_key = None):
"""
Merges the value of the attribute `source_key` for the `dep_module` with
the value of the attribute `key` for this module. If the value of the
`source_key` is equal to None. Then `key` is used for both modules.
This merges the attribute for both non-arch and archs
specified in `allowed_archs`.
:param key: The attribute used for merging in the calling module. Also
used for `dep_module` if the `source_key` is None.
:param source_module: The module where data is propagated from.
:param allowed_archs: A list of archs to merge the attribute on.
:param source_key: if the attribute merged from the `dep_module`
is different from the `key`
"""
if not source_key:
source_key = key
self.__dict__[key].update(source_module.__dict__[source_key])
for arch_name in source_module.target.keys():
if arch_name in allowed_archs:
self.target[arch_name].__dict__[key].update(
source_module.target[arch_name].__dict__[source_key])
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 sorted(self.modules.values(), key=lambda m: m.name):
m.to_string(output)
def label_to_module_name(label):
"""Turn a GN label (e.g., //:perfetto_tests) into a module name."""
module = re.sub(r'^//:?', '', label)
module = re.sub(r'[^a-zA-Z0-9_]', '_', module)
if not module.startswith(module_prefix):
return module_prefix + module
return module
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', '.cpp', '.java', '.proto', '.S']
def create_proto_modules(blueprint, gn, target):
"""Generate genrules for a proto GN target.
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 genrules.
Args:
blueprint: Blueprint instance which is being generated.
target: gn_utils.Target object.
Returns:
The source_genrule module.
"""
assert (target.type == 'proto_library')
protoc_gn_target_name = gn.get_target('//third_party/protobuf:protoc').name
protoc_module_name = label_to_module_name(protoc_gn_target_name)
tools = {protoc_module_name}
cpp_out_dir = '$(genDir)/%s/%s/' % (tree_path, target.proto_in_dir)
target_module_name = label_to_module_name(target.name)
# In GN builds the proto path is always relative to the output directory
# (out/tmp.xxx).
cmd = ['$(location %s)' % protoc_module_name]
cmd += ['--proto_path=%s/%s' % (tree_path, target.proto_in_dir)]
if buildtools_protobuf_src in target.proto_paths:
cmd += ['--proto_path=%s' % android_protobuf_src]
# We don't generate any targets for source_set proto modules because
# they will be inlined into other modules if required.
if target.proto_plugin == 'source_set':
return None
# Descriptor targets only generate a single target.
if target.proto_plugin == 'descriptor':
out = '{}.bin'.format(target_module_name)
cmd += ['--descriptor_set_out=$(out)']
cmd += ['$(in)']
descriptor_module = Module('cc_genrule', target_module_name, target.name)
descriptor_module.cmd = ' '.join(cmd)
descriptor_module.out = [out]
descriptor_module.tools = tools
blueprint.add_module(descriptor_module)
# Recursively extract the .proto files of all the dependencies and
# add them to srcs.
descriptor_module.srcs.update(
gn_utils.label_to_path(src) for src in target.sources)
for dep in target.transitive_proto_deps:
current_target = gn.get_target(dep)
descriptor_module.srcs.update(
gn_utils.label_to_path(src) for src in current_target.sources)
return descriptor_module
# We create two genrules for each proto target: one for the 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_name = target_module_name + '_gen'
source_module = Module('cc_genrule', source_module_name, target.name)
blueprint.add_module(source_module)
source_module.srcs.update(
gn_utils.label_to_path(src) for src in target.sources)
header_module = Module('cc_genrule', source_module_name + '_headers',
target.name)
blueprint.add_module(header_module)
header_module.srcs = set(source_module.srcs)
# TODO(primiano): at some point we should remove this. This was introduced
# by aosp/1108421 when adding "protos/" to .proto include paths, in order to
# avoid doing multi-repo changes and allow old clients in the android tree
# to still do the old #include "perfetto/..." rather than
# #include "protos/perfetto/...".
header_module.export_include_dirs = {'.', 'protos'}
# Since the .cc file and .h get created by a different gerule target, they
# are not put in the same intermediate path, so local includes do not work
# without explictily exporting the include dir.
header_module.export_include_dirs.add(target.proto_in_dir)
# This function does not return header_module so setting apex_available attribute here.
header_module.apex_available.add(tethering_apex)
source_module.genrule_srcs.add(':' + source_module.name)
source_module.genrule_headers.add(header_module.name)
if target.proto_plugin == 'proto':
suffixes = ['pb']
source_module.genrule_shared_libs.add('libprotobuf-cpp-lite')
cmd += ['--cpp_out=lite=true:' + cpp_out_dir]
elif target.proto_plugin == 'protozero':
suffixes = ['pbzero']
plugin = create_modules_from_target(blueprint, gn, protozero_plugin)
tools.add(plugin.name)
cmd += ['--plugin=protoc-gen-plugin=$(location %s)' % plugin.name]
cmd += ['--plugin_out=wrapper_namespace=pbzero:' + cpp_out_dir]
elif target.proto_plugin == 'cppgen':
suffixes = ['gen']
plugin = create_modules_from_target(blueprint, gn, cppgen_plugin)
tools.add(plugin.name)
cmd += ['--plugin=protoc-gen-plugin=$(location %s)' % plugin.name]
cmd += ['--plugin_out=wrapper_namespace=gen:' + cpp_out_dir]
elif target.proto_plugin == 'ipc':
suffixes = ['ipc']
plugin = create_modules_from_target(blueprint, gn, ipc_plugin)
tools.add(plugin.name)
cmd += ['--plugin=protoc-gen-plugin=$(location %s)' % plugin.name]
cmd += ['--plugin_out=wrapper_namespace=gen:' + cpp_out_dir]
else:
raise Error('Unsupported proto plugin: %s' % target.proto_plugin)
cmd += ['$(in)']
source_module.cmd = ' '.join(cmd)
header_module.cmd = source_module.cmd
source_module.tools = tools
header_module.tools = tools
for sfx in suffixes:
source_module.out.update('%s/%s' %
(tree_path, src.replace('.proto', '.%s.cc' % sfx))
for src in source_module.srcs)
header_module.out.update('%s/%s' %
(tree_path, src.replace('.proto', '.%s.h' % sfx))
for src in header_module.srcs)
return source_module
def create_proto_group_modules(blueprint, gn, module_name, target_names):
# TODO(lalitm): today, we're only adding a Java lite module because that's
# the only one used in practice. In the future, if we need other target types
# (e.g. C++, Java full etc.) add them here.
bp_module_name = label_to_module_name(module_name) + '_java_protos'
module = Module('java_library', bp_module_name, bp_module_name)
module.comment = f'''GN: [{', '.join(target_names)}]'''
module.proto = {'type': 'lite', 'canonical_path_from_root': False}
for name in target_names:
target = gn.get_target(name)
module.srcs.update(gn_utils.label_to_path(src) for src in target.sources)
for dep_label in target.transitive_proto_deps:
dep = gn.get_target(dep_label)
module.srcs.update(gn_utils.label_to_path(src) for src in dep.sources)
blueprint.add_module(module)
class BaseActionSanitizer():
def __init__(self, target):
# Just to be on the safe side, create a deep-copy.
self.target = copy.deepcopy(target)
self.target.args = self._normalize_args()
def get_name(self):
return label_to_module_name(self.target.name)
def _normalize_args(self):
# Convert ['--param=value'] to ['--param', 'value'] for consistency.
# Escape quotations.
normalized_args = []
for arg in self.target.args:
arg = arg.replace('"', r'\"')
if arg.startswith('-'):
normalized_args.extend(arg.split('='))
else:
normalized_args.append(arg)
return normalized_args
# There are three types of args:
# - flags (--flag)
# - value args (--arg value)
# - list args (--arg value1 --arg value2)
# value args have exactly one arg value pair and list args have one or more arg value pairs.
# Note that the set of list args contains the set of value args.
# This is because list and value args are identical when the list args has only one arg value pair
# Some functions provide special implementations for each type, while others
# work on all of them.
def _has_arg(self, arg):
return arg in self.target.args
def _get_arg_indices(self, target_arg):
return [i for i, arg in enumerate(self.target.args) if arg == target_arg]
# Whether an arg value pair appears once or more times
def _is_list_arg(self, arg):
indices = self._get_arg_indices(arg)
return len(indices) > 0 and all([not self.target.args[i + 1].startswith('--') for i in indices])
def _update_list_arg(self, arg, func, throw_if_absent = True):
if self._should_fail_silently(arg, throw_if_absent):
return
assert(self._is_list_arg(arg))
indices = self._get_arg_indices(arg)
for i in indices:
self._set_arg_at(i + 1, func(self.target.args[i + 1]))
# Whether an arg value pair appears exactly once
def _is_value_arg(self, arg):
return operator.countOf(self.target.args, arg) == 1 and self._is_list_arg(arg)
def _get_value_arg(self, arg):
assert(self._is_value_arg(arg))
i = self.target.args.index(arg)
return self.target.args[i + 1]
# used to check whether a function call should cause an error when an arg is
# missing.
def _should_fail_silently(self, arg, throw_if_absent):
return not throw_if_absent and not self._has_arg(arg)
def _set_value_arg(self, arg, value, throw_if_absent = True):
if self._should_fail_silently(arg, throw_if_absent):
return
assert(self._is_value_arg(arg))
i = self.target.args.index(arg)
self.target.args[i + 1] = value
def _update_value_arg(self, arg, func, throw_if_absent = True):
if self._should_fail_silently(arg, throw_if_absent):
return
self._set_value_arg(arg, func(self._get_value_arg(arg)))
def _set_arg_at(self, position, value):
self.target.args[position] = value
def _delete_value_arg(self, arg, throw_if_absent = True):
if self._should_fail_silently(arg, throw_if_absent):
return
assert(self._is_value_arg(arg))
i = self.target.args.index(arg)
self.target.args.pop(i)
self.target.args.pop(i)
def _append_arg(self, arg, value):
self.target.args.append(arg)
self.target.args.append(value)
def _sanitize_filepath_with_location_tag(self, arg):
if arg.startswith('../../'):
arg = self._sanitize_filepath(arg)
arg = self._add_location_tag(arg)
return arg
# wrap filename in location tag.
def _add_location_tag(self, filename):
return '$(location %s)' % filename
# applies common directory transformation that *should* be universally applicable.
# TODO: verify if it actually *is* universally applicable.
def _sanitize_filepath(self, filepath):
# Careful, order matters!
# delete all leading ../
filepath = re.sub('^(\.\./)+', '', filepath)
filepath = re.sub('^gen/jni_headers', '$(genDir)', filepath)
filepath = re.sub('^gen', '$(genDir)', filepath)
return filepath
# Iterate through all the args and apply function
def _update_all_args(self, func):
self.target.args = [func(arg) for arg in self.target.args]
def get_cmd(self):
arg_string = NEWLINE.join(self.target.args)
cmd = '$(location %s) %s' % (
gn_utils.label_to_path(self.target.script), arg_string)
if self.use_response_file:
# Pipe response file contents into script
cmd = 'echo \'%s\' |%s%s' % (self.target.response_file_contents, NEWLINE, cmd)
return cmd
def get_outputs(self):
return self.target.outputs
def get_srcs(self):
# gn treats inputs and sources for actions equally.
# soong only supports source files inside srcs, non-source files are added as
# tool_files dependency.
files = self.target.sources.union(self.target.inputs)
return {gn_utils.label_to_path(file) for file in files if is_supported_source_file(file)}
def get_tool_files(self):
# gn treats inputs and sources for actions equally.
# soong only supports source files inside srcs, non-source files are added as
# tool_files dependency.
files = self.target.sources.union(self.target.inputs)
tool_files = {gn_utils.label_to_path(file)
for file in files if not is_supported_source_file(file)}
tool_files.add(gn_utils.label_to_path(self.target.script))
return tool_files
def _sanitize_args(self):
# Handle passing parameters via response file by piping them into the script
# and reading them from /dev/stdin.
self.use_response_file = gn_utils.RESPONSE_FILE in self.target.args
if self.use_response_file:
# Replace {{response_file_contents}} with /dev/stdin
self.target.args = ['/dev/stdin' if it == gn_utils.RESPONSE_FILE else it
for it in self.target.args]
def _sanitize_outputs(self):
pass
def _sanitize_inputs(self):
pass
def sanitize(self):
self._sanitize_args()
self._sanitize_outputs()
self._sanitize_inputs()
# Whether this target generates header files
def is_header_generated(self):
return any(os.path.splitext(it)[1] == '.h' for it in self.target.outputs)
class WriteBuildDateHeaderSanitizer(BaseActionSanitizer):
def _sanitize_args(self):
self._set_arg_at(0, '$(out)')
super()._sanitize_args()
class WriteBuildFlagHeaderSanitizer(BaseActionSanitizer):
def _sanitize_args(self):
self._set_value_arg('--gen-dir', '.')
self._set_value_arg('--output', '$(out)')
super()._sanitize_args()
class JniGeneratorSanitizer(BaseActionSanitizer):
def _add_location_tag_to_filepath(self, arg):
if not arg.endswith('.class'):
# --input_file supports both .class specifiers or source files as arguments.
# Only source files need to be wrapped inside a $(location <label>) tag.
arg = self._add_location_tag(arg)
return arg
def _sanitize_args(self):
self._update_value_arg('--jar_file', self._sanitize_filepath, False)
self._update_value_arg('--jar_file', self._add_location_tag, False)
if self._has_arg('--jar_file'):
self._append_arg('--javap', '$$(find out/.path -name javap)')
self._update_value_arg('--output_dir', self._sanitize_filepath)
self._update_value_arg('--includes', self._sanitize_filepath, False)
self._delete_value_arg('--prev_output_dir', False)
self._update_list_arg('--input_file', self._sanitize_filepath)
self._update_list_arg('--input_file', self._add_location_tag_to_filepath)
super()._sanitize_args()
def _sanitize_outputs(self):
# fix target.output directory to match #include statements.
self.target.outputs = {re.sub('^jni_headers/', '', out) for out in self.target.outputs}
super()._sanitize_outputs()
def get_tool_files(self):
tool_files = super().get_tool_files()
# android_jar.classes should be part of the tools as it list implicit classes
# for the script to generate JNI headers.
tool_files.add("base/android/jni_generator/android_jar.classes")
return tool_files
class JniRegistrationGeneratorSanitizer(BaseActionSanitizer):
def _sanitize_inputs(self):
self.target.inputs = [file for file in self.target.inputs if not file.startswith('//out/')]
def _sanitize_args(self):
self._update_value_arg('--depfile', self._sanitize_filepath)
self._update_value_arg('--srcjar-path', self._sanitize_filepath)
self._update_value_arg('--header-path', self._sanitize_filepath)
self._set_value_arg('--sources-files', '$(genDir)/java.sources')
# update_jni_registration_module removes them from the srcs of the module
# It might be better to remove sources by '--sources-exclusions'
self._delete_value_arg('--sources-exclusions')
super()._sanitize_args()
def get_cmd(self):
# jni_registration_generator.py doesn't work with python2
cmd = "python3 " + super().get_cmd()
# Path in the original sources file does not work in genrule.
# So creating sources file in cmd based on the srcs of this target.
# Adding ../$(current_dir)/ to the head because jni_registration_generator.py uses the files
# whose path startswith(..)
commands = ["current_dir=`basename \\\`pwd\\\``;",
"for f in $(in);",
"do",
"echo \\\"../$$current_dir/$$f\\\" >> $(genDir)/java.sources;",
"done;",
cmd]
# .h file jni_registration_generator.py generates has #define with directory name.
# With the genrule env that contains "." which is invalid. So replace that at the end of cmd.
commands.append(";sed -i -e 's/OUT_SOONG_.TEMP_SBOX_.*_OUT/GEN/g' ")
commands.append("$(genDir)/components/cronet/android/cronet_jni_registration.h")
return NEWLINE.join(commands)
class JavaJniRegistrationGeneratorSanitizer(JniRegistrationGeneratorSanitizer):
def get_name(self):
return label_to_module_name(self.target.name) + "__java"
def _sanitize_outputs(self):
self.target.outputs = [out for out in self.target.outputs if
out.endswith(".srcjar")]
super()._sanitize_outputs()
class VersionSanitizer(BaseActionSanitizer):
def _sanitize_args(self):
self._set_value_arg('-o', '$(out)')
# args for the version.py contain file path without leading --arg key. So apply sanitize
# function for all the args.
self._update_all_args(self._sanitize_filepath_with_location_tag)
self._set_value_arg('-e', "'%s'" % self._get_value_arg('-e'))
super()._sanitize_args()
def get_tool_files(self):
tool_files = super().get_tool_files()
# android_chrome_version.py is not specified in anywhere but version.py imports this file
tool_files.add('build/util/android_chrome_version.py')
return tool_files
class JavaCppEnumSanitizer(BaseActionSanitizer):
def _sanitize_args(self):
self._update_all_args(self._sanitize_filepath_with_location_tag)
self._set_value_arg('--srcjar', '$(out)')
super()._sanitize_args()
class MakeDafsaSanitizer(BaseActionSanitizer):
def is_header_generated(self):
# This script generates .cc files but they are #included by other sources
# (e.g. registry_controlled_domain.cc)
return True
class JavaCppFeatureSanitizer(BaseActionSanitizer):
def _sanitize_args(self):
self._update_all_args(self._sanitize_filepath_with_location_tag)
self._set_value_arg('--srcjar', '$(out)')
super()._sanitize_args()
class JavaCppStringSanitizer(BaseActionSanitizer):
def _sanitize_args(self):
self._update_all_args(self._sanitize_filepath_with_location_tag)
self._set_value_arg('--srcjar', '$(out)')
super()._sanitize_args()
def get_action_sanitizer(target, type):
if target.script == "//build/write_buildflag_header.py":
return WriteBuildFlagHeaderSanitizer(target)
elif target.script == "//build/write_build_date_header.py":
return WriteBuildDateHeaderSanitizer(target)
elif target.script == '//base/android/jni_generator/jni_generator.py':
return JniGeneratorSanitizer(target)
elif target.script == '//base/android/jni_generator/jni_registration_generator.py':
if type == 'java_genrule':
return JavaJniRegistrationGeneratorSanitizer(target)
else:
return JniRegistrationGeneratorSanitizer(target)
elif target.script == "//build/util/version.py":
return VersionSanitizer(target)
elif target.script == "//build/android/gyp/java_cpp_enum.py":
return JavaCppEnumSanitizer(target)
elif target.script == "//net/tools/dafsa/make_dafsa.py":
return MakeDafsaSanitizer(target)
elif target.script == '//build/android/gyp/java_cpp_features.py':
return JavaCppFeatureSanitizer(target)
elif target.script == '//build/android/gyp/java_cpp_strings.py':
return JavaCppStringSanitizer(target)
else:
# TODO: throw exception here once all script hacks have been converted.
return BaseActionSanitizer(target)
def create_action_foreach_modules(blueprint, target):
""" The following assumes that rebase_path exists in the args.
The args of an action_foreach contains hints about which output files are generated
by which source files.
This is copied directly from the args
"gen/net/base/registry_controlled_domains/{{source_name_part}}-reversed-inc.cc"
So each source file will generate an output whose name is the {source_name-reversed-inc.cc}
"""
new_args = []
for i, src in enumerate(sorted(target.sources)):
# don't add script arg for the first source -- create_action_module
# already does this.
if i != 0:
new_args.append('&& python3 $(location %s)' %
gn_utils.label_to_path(target.script))
for arg in target.args:
if '{{source}}' in arg:
new_args.append('$(location %s)' % (gn_utils.label_to_path(src)))
elif '{{source_name_part}}' in arg:
source_name_part = src.split("/")[-1] # Get the file name only
source_name_part = source_name_part.split(".")[0] # Remove the extension (Ex: .cc)
file_name = arg.replace('{{source_name_part}}', source_name_part).split("/")[-1]
# file_name represent the output file name. But we need the whole path
# This can be found from target.outputs.
for out in target.outputs:
if out.endswith(file_name):
new_args.append('$(location %s)' % out)
else:
new_args.append(arg)
target.args = new_args
return create_action_module(blueprint, target, 'cc_genrule')
def create_action_module(blueprint, target, type):
sanitizer = get_action_sanitizer(target, type)
sanitizer.sanitize()
module = Module(type, sanitizer.get_name(), target.name)
module.cmd = sanitizer.get_cmd()
module.out = sanitizer.get_outputs()
if sanitizer.is_header_generated():
module.genrule_headers.add(module.name)
module.srcs = sanitizer.get_srcs()
module.tool_files = sanitizer.get_tool_files()
blueprint.add_module(module)
return module
def _get_cflags(cflags, defines):
cflags = {flag for flag in cflags if flag in cflag_allowlist}
# Consider proper allowlist or denylist if needed
cflags |= set("-D%s" % define.replace("\"", "\\\"") for define in defines)
return cflags
def set_module_flags(module, cflags, defines):
module.cflags.update(_get_cflags(cflags, defines))
# TODO: implement proper cflag parsing.
for flag in cflags:
if '-std=' in flag:
module.cpp_std = flag[len('-std='):]
if '-fexceptions' in flag:
module.cppflags.add('-fexceptions')
def add_genrule_per_arch(module, dep_module, type):
module.generated_headers.update(dep_module.genrule_headers)
# If the module is a static library, export all the generated headers.
if type == 'cc_library_static':
module.export_generated_headers.update(dep_module.genrule_headers)
module.srcs.update(dep_module.genrule_srcs)
module.shared_libs.update(dep_module.genrule_shared_libs)
module.header_libs.update(dep_module.genrule_header_libs)
def set_module_include_dirs(module, cflags, include_dirs):
for flag in cflags:
if '-isystem' in flag:
module.local_include_dirs.add(flag[len('-isystem../../'):])
# Adding local_include_dirs is necessary due to source_sets / filegroups
# which do not properly propagate include directories.
# Filter any directory inside //out as a) this directory does not exist for
# aosp / soong builds and b) the include directory should already be
# configured via library dependency.
module.local_include_dirs.update([gn_utils.label_to_path(d)
for d in include_dirs
if not re.match('^//out/.*', d)])
def create_modules_from_target(blueprint, gn, gn_target_name):
"""Generate module(s) for a given GN target.
Given a GN target name, generate one or more corresponding modules into a
blueprint. The only case when this generates >1 module is proto libraries.
Args:
blueprint: Blueprint instance which is being generated.
gn: gn_utils.GnParser object.
gn_target_name: GN target for module generation.
"""
bp_module_name = label_to_module_name(gn_target_name)
if bp_module_name in blueprint.modules:
return blueprint.modules[bp_module_name]
target = gn.get_target(gn_target_name)
log.info('create modules for %s (%s)', target.name, target.type)
if target.type == 'executable':
if target.testonly:
module_type = 'cc_test'
else:
# Can be used for both host and device targets.
module_type = 'cc_binary'
module = Module(module_type, bp_module_name, gn_target_name)
elif target.type == 'static_library':
module = Module('cc_library_static', bp_module_name, gn_target_name)
elif target.type == 'shared_library':
module = Module('cc_library_shared', bp_module_name, gn_target_name)
elif target.type == 'source_set':
module = Module('cc_object', bp_module_name, gn_target_name)
elif target.type == 'group':
# "group" targets are resolved recursively by gn_utils.get_target().
# There's nothing we need to do at this level for them.
return None
elif target.type == 'proto_library':
module = create_proto_modules(blueprint, gn, target)
if module is None:
return None
elif target.type == 'action':
module = create_action_module(blueprint, target, 'cc_genrule')
elif target.type == 'action_foreach':
module = create_action_foreach_modules(blueprint, target)
elif target.type == 'copy':
# TODO: careful now! copy targets are not supported yet, but this will stop
# traversing the dependency tree. For //base:base, this is not a big
# problem as libicu contains the only copy target which happens to be a
# leaf node.
return None
elif target.type == 'java_group':
# Java targets are handled outside of create_modules_from_target.
return None
else:
raise Error('Unknown target %s (%s)' % (target.name, target.type))
blueprint.add_module(module)
module.init_rc = target_initrc.get(target.name, [])
module.srcs.update(
gn_utils.label_to_path(src)
for src in target.sources
if is_supported_source_file(src) and not src.startswith("//out/test"))
# Add arch-specific properties
for arch_name, arch in target.arch.items():
module.target[arch_name].srcs.update(
gn_utils.label_to_path(src)
for src in arch.sources
if is_supported_source_file(src) and not src.startswith("//out/test"))
module.rtti = target.rtti
if target.type in gn_utils.LINKER_UNIT_TYPES:
set_module_flags(module, target.cflags, target.defines)
set_module_include_dirs(module, target.cflags, target.include_dirs)
# TODO: set_module_xxx is confusing, apply similar function to module and target in better way.
for arch_name, arch in target.arch.items():
set_module_flags(module.target[arch_name], arch.cflags, arch.defines)
# -Xclang -target-feature -Xclang +mte are used to enable MTE (Memory Tagging Extensions).
# Flags which does not start with '-' could not be in the cflags so enabling MTE by
# -march and -mcpu Feature Modifiers. MTE is only available on arm64. This is needed for
# building //base/allocator/partition_allocator:partition_alloc for arm64.
if '+mte' in arch.cflags and arch_name == 'android_arm64':
module.target[arch_name].cflags.add('-march=armv8-a+memtag')
set_module_include_dirs(module.target[arch_name], arch.cflags, arch.include_dirs)
module.host_supported = target.host_supported()
module.device_supported = target.device_supported()
if module.is_genrule():
module.apex_available.add(tethering_apex)
if module.is_compiled():
# Don't try to inject library/source dependencies into genrules or
# filegroups because they are not compiled in the traditional sense.
module.defaults = [defaults_module]
for lib in target.libs:
# Generally library names should be mangled as 'libXXX', unless they
# are HAL libraries (e.g., android.hardware.health@2.0) or AIDL c++ / NDK
# libraries (e.g. "android.hardware.power.stats-V1-cpp")
android_lib = lib if '@' in lib or "-cpp" in lib or "-ndk" in lib \
else 'lib' + lib
if lib in shared_library_allowlist:
module.add_android_shared_lib(android_lib)
if lib in static_library_allowlist:
module.add_android_static_lib(android_lib)
# Remove prohibited include directories
module.local_include_dirs = [d for d in module.local_include_dirs
if d not in local_include_dirs_denylist]
# If the module is a static library, export all the generated headers.
if module.type == 'cc_library_static':
module.export_generated_headers = module.generated_headers
if module.name == 'cronet_aml_components_cronet_android_cronet':
if target.output_name is None:
raise Error('Failed to get output_name for libcronet name')
# .so file name needs to match with CronetLibraryLoader.java (e.g. libcronet.109.0.5386.0.so)
# So setting the output name based on the output_name from the desc.json
module.stem = 'lib' + target.output_name
# dep_name is an unmangled GN target name (e.g. //foo:bar(toolchain)).
# Currently, only one module is generated from target even target has multiple toolchains.
# And module is generated based on the first visited target.
# Sort deps before iteration to make result deterministic.
all_deps = sorted(target.deps | target.source_set_deps | target.transitive_proto_deps)
for dep_name in all_deps:
# |builtin_deps| override GN deps with Android-specific ones. See the
# config in the top of this file.
if dep_name in builtin_deps:
builtin_deps[dep_name](module)
continue
dep_module = create_modules_from_target(blueprint, gn, dep_name)
if dep_module is None:
continue
# TODO: Proper dependency check for genrule.
# Currently, only propagating genrule dependencies.
# Also, currently, all the dependencies are propagated upwards.
# in gn, public_deps should be propagated but deps should not.
# Not sure this information is available in the desc.json.
# Following rule works for adding android_runtime_jni_headers to base:base.
# If this doesn't work for other target, hardcoding for specific target
# might be better.
if module.is_genrule() and dep_module.is_genrule():
module.genrule_headers.add(dep_module.name)
module.genrule_headers.update(dep_module.genrule_headers)
# For filegroups, and genrule, recurse but don't apply the
# deps.
if not module.is_compiled() or module.is_genrule():
continue
if dep_module.type == 'cc_library_shared':
module.shared_libs.add(dep_module.name)
elif dep_module.type == 'cc_library_static':
module.static_libs.add(dep_module.name)
elif dep_module.type == 'cc_object':
module.merge_attribute('generated_headers', dep_module, target.arch.keys())
if module.type != 'cc_object':
if dep_module.has_input_files():
# Only add it as part of srcs if the dep_module has input files otherwise
# this would throw an error.
module.srcs.add(":" + dep_module.name)
module.merge_attribute('export_generated_headers', dep_module,
target.arch.keys(), 'generated_headers')
elif dep_module.type == 'cc_genrule':
module.merge_attribute('generated_headers', dep_module, [], 'genrule_headers')
module.merge_attribute('srcs', dep_module, [], 'genrule_srcs')
module.merge_attribute('shared_libs', dep_module, [], 'genrule_shared_libs')
module.merge_attribute('header_libs', dep_module, [], 'genrule_header_libs')
if module.type not in ["cc_object"]:
module.merge_attribute('export_generated_headers', dep_module, [],
'genrule_headers')
elif dep_module.type == 'cc_binary':
continue # Ignore executables deps (used by cmdline integration tests).
else:
raise Error('Unknown dep %s (%s) for target %s' %
(dep_module.name, dep_module.type, module.name))
for arch_name, arch in target.arch.items():
for dep_name in arch.deps:
dep_module = create_modules_from_target(blueprint, gn, dep_name)
# Arch-specific dependencies currently only include cc_library_static.
# Revisit this approach once we need to support more target types.
if dep_module.type == 'cc_library_static':
module.target[arch_name].static_libs.add(dep_module.name)
elif dep_module.type == 'cc_genrule':
if dep_module.name.endswith(arch_name):
module.target[arch_name].generated_headers.update(dep_module.genrule_headers)
module.target[arch_name].srcs.update(dep_module.genrule_srcs)
module.target[arch_name].shared_libs.update(dep_module.genrule_shared_libs)
module.target[arch_name].header_libs.update(dep_module.genrule_header_libs)
if module.type not in ["cc_object"]:
module.target[arch_name].export_generated_headers.update(
dep_module.genrule_headers)
else:
raise Error('Unsupported arch-specific dependency %s of target %s with type %s' %
(dep_module.name, target.name, dep_module.type))
for dep_name in arch.source_set_deps:
dep_module = create_modules_from_target(blueprint, gn, dep_name)
if dep_module.type == 'cc_object':
if module.type != 'cc_object':
# We only want to bubble up cc_objects for modules that are not cc_objects
# otherwise they'd be recompiled and that would cause multiple symbol redefinitions.
if dep_module.has_input_files():
# Only add it as part of srcs if the dep_module has input files otherwise
# this would throw an error.
module.target[arch_name].srcs.add(":" + dep_module.name)
else:
raise Error('Unsupported arch-specific dependency %s of target %s with type %s' %
(dep_module.name, target.name, dep_module.type))
return module
def create_java_module(blueprint, gn):
bp_module_name = module_prefix + 'java'
module = Module('java_library', bp_module_name, '//gn:java')
module.srcs.update([gn_utils.label_to_path(source) for source in gn.java_sources])
for dep in gn.java_actions:
dep_module = create_action_module(blueprint, gn.get_target(dep), 'java_genrule')
blueprint.add_module(module)
def update_jni_registration_module(module, gn):
# TODO: deny list is in the arg of jni_registration_generator.py. Should not be hardcoded
deny_list = [
'//base/android/java/src/org/chromium/base/library_loader/LibraryLoader.java',
'//base/android/java/src/org/chromium/base/library_loader/LibraryPrefetcher.java',
'//base/android/java/src/org/chromium/base/process_launcher/ChildProcessService.java',
'//base/android/java/src/org/chromium/base/SysUtils.java']
# TODO: java_sources might not contain all the required java files
module.srcs.update([gn_utils.label_to_path(source)
for source in gn.java_sources if source not in deny_list])
def create_blueprint_for_targets(gn, targets):
"""Generate a blueprint for a list of GN targets."""
blueprint = Blueprint()
# Default settings used by all modules.
defaults = Module('cc_defaults', defaults_module, '//gn:default_deps')
defaults.cflags = [
'-DGOOGLE_PROTOBUF_NO_RTTI',
'-Wno-error=return-type',
'-Wno-non-virtual-dtor',
'-Wno-macro-redefined',
'-Wno-missing-field-initializers',
'-Wno-sign-compare',
'-Wno-sign-promo',
'-Wno-unused-parameter',
'-Wno-null-pointer-subtraction', # Needed to libevent
'-Wno-deprecated-non-prototype', # needed for zlib
'-fvisibility=hidden',
'-Wno-ambiguous-reversed-operator', # needed for icui18n
'-Wno-unreachable-code-loop-increment', # needed for icui18n
'-O2',
'-fPIC',
]
# Chromium builds do not add a dependency for headers found inside the
# sysroot, so they are added globally via defaults.
defaults.target['android'].header_libs = [
'media_ndk_headers',
'jni_headers',
]
defaults.target['host'].cflags = [
# -DANDROID is added by default but target.defines contain -DANDROID if
# it's required. So adding -UANDROID to cancel default -DANDROID if it's
# not specified.
# Note: -DANDROID is not consistently applied across the chromium code
# base, so it is removed unconditionally for host targets.
'-UANDROID',
]
defaults.stl = 'none'
defaults.min_sdk_version = 29
defaults.apex_available.add(tethering_apex)
blueprint.add_module(defaults)
for target in targets:
create_modules_from_target(blueprint, gn, target)
create_java_module(blueprint, gn)
for module in blueprint.modules.values():
if 'cronet_jni_registration' in module.name:
update_jni_registration_module(module, gn)
# Merge in additional hardcoded arguments.
for module in blueprint.modules.values():
for key, add_val in additional_args.get(module.name, []):
curr = getattr(module, key)
if add_val and isinstance(add_val, set) and isinstance(curr, set):
curr.update(add_val)
elif isinstance(add_val, str) and (not curr or isinstance(curr, str)):
setattr(module, key, add_val)
elif isinstance(add_val, bool) and (not curr or isinstance(curr, bool)):
setattr(module, key, add_val)
elif isinstance(add_val, dict) and isinstance(curr, dict):
curr.update(add_val)
elif isinstance(add_val, dict) and isinstance(curr, Target):
curr.__dict__.update(add_val)
else:
raise Error('Unimplemented type %r of additional_args: %r' %
(type(add_val), key))
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 "//*".' +
'You can specify multiple --desc options for different target_cpu',
required=True,
action='append'
)
parser.add_argument(
'--extras',
help='Extra targets to include at the end of the Blueprint file',
default=os.path.join(gn_utils.repo_root(), 'Android.bp.extras'),
)
parser.add_argument(
'--output',
help='Blueprint file to create',
default=os.path.join(gn_utils.repo_root(), 'Android.bp'),
)
parser.add_argument(
'-v',
'--verbose',
help='Print debug logs.',
action='store_true',
)
parser.add_argument(
'targets',
nargs=argparse.REMAINDER,
help='Targets to include in the blueprint (e.g., "//:perfetto_tests")'
)
args = parser.parse_args()
if args.verbose:
log.basicConfig(format='%(levelname)s:%(funcName)s:%(message)s', level=log.DEBUG)
targets = args.targets or default_targets
gn = gn_utils.GnParser()
for desc_file in args.desc:
with open(desc_file) as f:
desc = json.load(f)
for target in targets:
gn.parse_gn_desc(desc, target)
blueprint = create_blueprint_for_targets(gn, targets)
project_root = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
tool_name = os.path.relpath(os.path.abspath(__file__), project_root)
# Add any proto groups to the blueprint.
for l_name, t_names in proto_groups.items():
create_proto_group_modules(blueprint, gn, l_name, t_names)
output = [
"""// Copyright (C) 2022 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)
]
blueprint.to_string(output)
if os.path.exists(args.extras):
with open(args.extras, 'r') as r:
for line in r:
output.append(line.rstrip("\n\r"))
out_files = []
# Generate the Android.bp file.
out_files.append(args.output + '.swp')
with open(out_files[-1], 'w') as f:
f.write('\n'.join(output))
# Text files should have a trailing EOL.
f.write('\n')
return 0
if __name__ == '__main__':
sys.exit(main())