#!/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 json import logging as log import operator import os import re import sys import copy from typing import List, Dict, Set from pathlib import Path import gn_utils PARENT_ROOT = os.path.abspath( os.path.join(os.path.dirname(__file__), os.pardir)) sys.path.insert(0, os.path.join(PARENT_ROOT, "license")) import license_utils ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) CRONET_LICENSE_NAME = "external_cronet_license" # Default targets to translate to the blueprint file. DEFAULT_TARGETS = [ "//components/cronet/android:cronet_api_java", '//components/cronet/android:cronet', '//components/cronet/android:cronet_impl_native_java', '//components/cronet/android:cronet_jni_registration_java', ] DEFAULT_TESTS = [ '//components/cronet/android:cronet_unittests_android__library', '//net:net_unittests__library', '//components/cronet/android:cronet_tests', '//components/cronet/android:cronet', '//components/cronet/android:cronet_javatests', '//components/cronet/android:cronet_jni_registration_java', '//components/cronet/android:cronet_tests_jni_registration_java', '//testing/android/native_test:native_test_java', '//net/android:net_test_support_provider_java', '//net/android:net_tests_java', '//third_party/netty-tcnative:netty-tcnative-so', '//third_party/netty4:netty_all_java', "//build/rust/tests/test_rust_static_library:test_rust_static_library", # Added to make sure that rust still compiles ] EXTRAS_ANDROID_BP_FILE = "Android.extras.bp" CRONET_API_MODULE_NAME = "cronet_aml_api_java" # All module names are prefixed with this string to avoid collisions. module_prefix = 'cronet_aml_' REMOVE_GEN_JNI_JARJAR_RULES_FILE = ":remove_gen_jni_jarjar_rules" # Shared libraries which are directly translated to Android system equivalents. shared_library_allowlist = [ 'android', 'log', ] # A dictionary that adds extra content to a specific Android.bp according to the # provided path. # The path defined must be relative to the root-repo. BLUEPRINTS_EXTRAS = { "": ["build = [\"Android.extras.bp\"]"] } # A dictionary that specifies the relocation of modules from one blueprint to # another. # The default format is (relative_path_A -> relative_path_B), this means # that all targets which should live in relative_path_A/Android.bp will live # inside relative_path_B/Android.bp. BLUEPRINTS_MAPPING = { # An Android.bp already exists inside boringssl, creating another one will # lead to conflicts, add all of the boringssl generated targets to the # top-level Android.bp as they are only used for tests. "third_party/boringssl": "", # Moving is undergoing, see crbug/40273848 "buildtools/third_party/libc++": "third_party/libc++", # Moving is undergoing, see crbug/40273848 "buildtools/third_party/libc++abi": "third_party/libc++abi", } # Usually, README.chromium lives next to the BUILD.gn. However, some cases are # different, this dictionary allows setting a specific README.chromium path # for a specific BUILD.gn README_MAPPING = { # Moving is undergoing, see crbug/40273848 "buildtools/third_party/libc++": "third_party/libc++", # Moving is undergoing, see crbug/40273848 "buildtools/third_party/libc++abi": "third_party/libc++abi", } # Include directories that will be removed from all targets. include_dirs_denylist = [ 'external/cronet/third_party/zlib/', ] # Name of the module which settings such as compiler flags for all other # modules. cc_defaults_module = module_prefix + 'cc_defaults' # Name of the java default module for non-test java modules defined in Android.extras.bp java_framework_defaults_module = 'cronet_aml_java_framework_defaults' # Location of the project in the Android source tree. tree_path = 'external/cronet' # 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", # flags to reduce binary size "-O1", "-O2", "-O3", "-Oz", "-g1", "-g2", "-fdata-sections", "-ffunction-sections", "-fvisibility=hidden", "-fvisibility-inlines-hidden", "-fstack-protector", "-mno-outline", "-mno-outline-atomics", "-fno-asynchronous-unwind-tables", "-fno-unwind-tables", ] # Linker flags which are passed through to the blueprint. ldflag_allowlist = [ # flags to reduce binary size "-Wl,--as-needed", "-Wl,--gc-sections", "-Wl,--icf=all", ] def get_linker_script_ldflag(script_path): return f'-Wl,--script,{tree_path}/{script_path}' # Additional arguments to apply to Android.bp rules. additional_args = { 'cronet_aml_net_third_party_quiche_net_quic_test_tools_proto_gen_headers': [ ('export_include_dirs', { "net/third_party/quiche/src", }) ], 'cronet_aml_net_third_party_quiche_net_quic_test_tools_proto_gen__testing_headers': [ ('export_include_dirs', { "net/third_party/quiche/src", }) ], 'cronet_aml_third_party_quic_trace_quic_trace_proto_gen__testing_headers': [ ('export_include_dirs', { "third_party/quic_trace/src", }) ], # TODO: fix upstream. Both //base:base and # //base/allocator/partition_allocator:partition_alloc do not create a # dependency on gtest despite using gtest_prod.h. 'cronet_aml_base_base': [ ('header_libs', { 'libgtest_prod_headers', }), ('export_header_lib_headers', { 'libgtest_prod_headers', }), ], 'cronet_aml_base_allocator_partition_allocator_partition_alloc': [ ('header_libs', { 'libgtest_prod_headers', }), ], # TODO(b/309920629): Remove once upstreamed. 'cronet_aml_components_cronet_android_cronet_api_java': [ ('srcs', { 'components/cronet/android/api/src/org/chromium/net/UploadDataProviders.java', 'components/cronet/android/api/src/org/chromium/net/apihelpers/UploadDataProviders.java', }), ], 'cronet_aml_components_cronet_android_cronet_api_java__testing': [ ('srcs', { 'components/cronet/android/api/src/org/chromium/net/UploadDataProviders.java', 'components/cronet/android/api/src/org/chromium/net/apihelpers/UploadDataProviders.java', }), ], 'cronet_aml_components_cronet_android_cronet_javatests__testing': [ # Needed to @SkipPresubmit annotations ('static_libs', { 'net-tests-utils', }), # This is necessary because net-tests-utils compiles against private SDK. ('sdk_version', ""), ], 'cronet_aml_components_cronet_android_cronet__testing': [ ('target', ('android_riscv64', {'stem': "libmainlinecronet_riscv64"})), ('comment', """TODO: remove stem for riscv64 // This is essential as there can't be two different modules // with the same output. We usually got away with that because // the non-testing Cronet is part of the Tethering APEX and the // testing Cronet is not part of the Tethering APEX which made them // look like two different outputs from the build system perspective. // However, Cronet does not ship to Tethering APEX for RISCV64 which // raises the conflict. Once we start shipping Cronet for RISCV64, // this can be removed."""), ], 'cronet_aml_third_party_netty_tcnative_netty_tcnative_so__testing': [ ('cflags', { "-Wno-error=pointer-bool-conversion" }) ], 'cronet_aml_third_party_apache_portable_runtime_apr__testing': [ ('cflags', { "-Wno-incompatible-pointer-types-discards-qualifiers", }) ], # TODO(b/324872305): Remove when gn desc expands public_configs and update code to propagate the # include_dir from the public_configs # We had to add the export_include_dirs for each target because soong generates each header # file in a specific directory named after the target. 'cronet_aml_base_allocator_partition_allocator_src_partition_alloc_chromecast_buildflags': [ ('export_include_dirs', { "base/allocator/partition_allocator/src/", }) ], 'cronet_aml_base_allocator_partition_allocator_src_partition_alloc_chromecast_buildflags__testing': [ ('export_include_dirs', { "base/allocator/partition_allocator/src/", }) ], 'cronet_aml_base_allocator_partition_allocator_src_partition_alloc_chromeos_buildflags': [ ('export_include_dirs', { "base/allocator/partition_allocator/src/", }) ], 'cronet_aml_base_allocator_partition_allocator_src_partition_alloc_chromeos_buildflags__testing': [ ('export_include_dirs', { "base/allocator/partition_allocator/src/", }) ], 'cronet_aml_base_allocator_partition_allocator_src_partition_alloc_debugging_buildflags': [ ('export_include_dirs', { "base/allocator/partition_allocator/src/", }) ], 'cronet_aml_base_allocator_partition_allocator_src_partition_alloc_debugging_buildflags__testing': [ ('export_include_dirs', { "base/allocator/partition_allocator/src/", }) ], 'cronet_aml_base_allocator_partition_allocator_src_partition_alloc_partition_alloc_buildflags': [ ('export_include_dirs', { ".", "base/allocator/partition_allocator/src/", }) ], 'cronet_aml_base_allocator_partition_allocator_src_partition_alloc_partition_alloc_buildflags__testing': [ ('export_include_dirs', { ".", "base/allocator/partition_allocator/src/", }) ], 'cronet_aml_base_allocator_partition_allocator_src_partition_alloc_raw_ptr_buildflags': [ ('export_include_dirs', { "base/allocator/partition_allocator/src/", }) ], 'cronet_aml_base_allocator_partition_allocator_src_partition_alloc_raw_ptr_buildflags__testing': [ ('export_include_dirs', { "base/allocator/partition_allocator/src/", }) ], 'cronet_aml_base_base_java_test_support__testing': [ ('errorprone', ( 'javacflags', { "-Xep:ReturnValueIgnored:WARN", } ) ) ] # end export_include_dir. } _FEATURE_REGEX = "feature=\\\"(.+)\\\"" _RUST_FLAGS_TO_REMOVE = [ "--target", # Added by Soong "--color", # Added by Soong. "--edition", # Added to the appropriate field, must be removed from flags. "--sysroot", # Use AOSP's stdlib so we don't need any hacks for sysroot. "-Cembed-bitcode=no", # Not compatible with Thin-LTO which is added by Soong. "--cfg", # Added to the appropriate field. "--extern", # Soong automatically adds that for us when we use proc_macro "@", # Used by build_script outputs to have rustc load flags from a file. "-Z", # Those are unstable features, completely remove those. ] def always_disable(module, arch): return None def enable_zlib(module, arch): # Requires crrev/c/4109079 if arch == 'common': module.shared_libs.add('libz') else: module.target[arch].shared_libs.add('libz') def enable_boringssl(module, arch): # Do not add boringssl targets to cc_genrules. This happens, because protobuf targets are # originally static_libraries, but later get converted to a cc_genrule. if module.is_genrule(): return # Lets keep statically linking BoringSSL for testing target for now. This should be fixed. if module.name.endswith(gn_utils.TESTING_SUFFIX): return if arch == 'common': shared_libs = module.shared_libs else: shared_libs = module.target[arch].shared_libs shared_libs.add('//external/cronet/third_party/boringssl:libcrypto') shared_libs.add('//external/cronet/third_party/boringssl:libssl') shared_libs.add('//external/cronet/third_party/boringssl:libpki') def add_androidx_experimental_java_deps(module, arch): module.libs.add("androidx.annotation_annotation-experimental") def add_androidx_annotation_java_deps(module, arch): module.libs.add("androidx.annotation_annotation") def add_protobuf_lite_runtime_java_deps(module, arch): module.static_libs.add("libprotobuf-java-lite") def add_androidx_core_java_deps(module, arch): module.libs.add("androidx.core_core") def add_jsr305_java_deps(module, arch): module.libs.add("jsr305") def add_errorprone_annotation_java_deps(module, arch): module.libs.add("error_prone_annotations") def add_androidx_collection_java_deps(module, arch): module.libs.add("androidx.collection_collection") def add_junit_java_deps(module, arch): module.static_libs.add("junit") def add_truth_java_deps(module, arch): module.static_libs.add("truth") def add_hamcrest_java_deps(module, arch): module.static_libs.add("hamcrest-library") module.static_libs.add("hamcrest") def add_mockito_java_deps(module, arch): module.static_libs.add("mockito") def add_guava_java_deps(module, arch): module.static_libs.add("guava") def add_androidx_junit_java_deps(module, arch): module.static_libs.add("androidx.test.ext.junit") def add_androidx_test_runner_java_deps(module, arch): module.static_libs.add("androidx.test.runner") def add_androidx_test_rules_java_deps(module, arch): module.static_libs.add("androidx.test.rules") def add_android_test_base_java_deps(module, arch): module.libs.add("android.test.base") def add_accessibility_test_framework_java_deps(module, arch): # BaseActivityTestRule.java depends on this but BaseActivityTestRule.java is not used in aosp. pass def add_espresso_java_deps(module, arch): module.static_libs.add("androidx.test.espresso.contrib") def add_android_test_mock_java_deps(module, arch): module.libs.add("android.test.mock.stubs") def add_androidx_multidex_java_deps(module, arch): # Androidx-multidex is disabled on unbundled branches. pass def add_androidx_test_monitor_java_deps(module, arch): module.libs.add("androidx.test.monitor") def add_androidx_ui_automator_java_deps(module, arch): module.static_libs.add("androidx.test.uiautomator_uiautomator") def add_androidx_test_annotation_java_deps(module, arch): module.static_libs.add("androidx.test.rules") def add_androidx_test_core_java_deps(module, arch): module.static_libs.add("androidx.test.core") def add_androidx_activity_activity(module, arch): module.static_libs.add("androidx.activity_activity") def add_androidx_fragment_fragment(module, arch): module.static_libs.add("androidx.fragment_fragment") # Android equivalents for third-party libraries that the upstream project # depends on. This will be applied to normal and testing targets. _builtin_deps = { '//buildtools/third_party/libunwind:libunwind': always_disable, # This is a binary module that generates C++ binding files, Skip this # dependency completely as we construct the modules differently. '//third_party/rust/cxxbridge_cmd/v1:cxxbridge': always_disable, '//net/data/ssl/chrome_root_store:gen_root_store_inc': always_disable, '//third_party/zstd:headers': always_disable, '//testing/buildbot/filters:base_unittests_filters': always_disable, '//testing/buildbot/filters:net_unittests_filters': always_disable, '//third_party/boringssl/src/third_party/fiat:fiat_license': always_disable, '//net/tools/root_store_tool:root_store_tool': always_disable, '//third_party/zlib:zlib': enable_zlib, '//third_party/androidx:androidx_annotation_annotation_java': add_androidx_annotation_java_deps, '//third_party/android_deps:protobuf_lite_runtime_java': add_protobuf_lite_runtime_java_deps, '//third_party/androidx:androidx_annotation_annotation_experimental_java': add_androidx_experimental_java_deps, '//third_party/androidx:androidx_core_core_java': add_androidx_core_java_deps, '//third_party/android_deps:com_google_code_findbugs_jsr305_java': add_jsr305_java_deps, '//third_party/android_deps:com_google_errorprone_error_prone_annotations_java': add_errorprone_annotation_java_deps, '//third_party/androidx:androidx_collection_collection_java': add_androidx_collection_java_deps, '//third_party/junit:junit': add_junit_java_deps, '//third_party/google-truth:google_truth_java': add_truth_java_deps, '//third_party/hamcrest:hamcrest_core_java': add_hamcrest_java_deps, '//third_party/mockito:mockito_java': add_mockito_java_deps, '//third_party/android_deps:guava_android_java': add_guava_java_deps, '//third_party/androidx:androidx_test_ext_junit_java': add_androidx_junit_java_deps, '//third_party/androidx:androidx_test_runner_java': add_androidx_test_runner_java_deps, '//third_party/android_sdk:android_test_base_java': add_android_test_base_java_deps, '//third_party/accessibility_test_framework:accessibility_test_framework_java': add_accessibility_test_framework_java_deps, '//third_party/accessibility_test_framework:accessibility_core_java': add_accessibility_test_framework_java_deps, '//third_party/android_deps:espresso_java': add_espresso_java_deps, '//third_party/android_sdk:android_test_mock_java': add_android_test_mock_java_deps, '//third_party/androidx:androidx_multidex_multidex_java': add_androidx_multidex_java_deps, '//third_party/androidx:androidx_test_monitor_java': add_androidx_test_monitor_java_deps, '//third_party/androidx:androidx_test_annotation_java': add_androidx_test_annotation_java_deps, '//third_party/androidx:androidx_test_core_java': add_androidx_test_core_java_deps, '//third_party/androidx:androidx_test_uiautomator_uiautomator_java': add_androidx_ui_automator_java_deps, '//third_party/hamcrest:hamcrest_java': add_hamcrest_java_deps, '//third_party/androidx:androidx_activity_activity_java': add_androidx_activity_activity, '//third_party/androidx:androidx_fragment_fragment_java': add_androidx_fragment_fragment, '//third_party/androidx:androidx_test_rules_java': add_androidx_test_rules_java_deps, } builtin_deps = {"{}{}".format(key, suffix): value for key, value in _builtin_deps.items() for suffix in ["", gn_utils.TESTING_SUFFIX] } # Same as _builtin_deps but will only apply what is explicitly specified. builtin_deps.update({ '//third_party/boringssl:boringssl': enable_boringssl, '//third_party/boringssl:boringssl_asm': # Due to FIPS requirements, downstream BoringSSL has a different "shape" than upstream's. # We're guaranteed that if X depends on :boringssl it will also depend on :boringssl_asm. # Hence, always drop :boringssl_asm and handle the translation entirely in :boringssl. always_disable, }) # Name of tethering apex module tethering_apex = "com.android.tethering" # Name of cronet api target java_api_target_name = "//components/cronet/android:cronet_api_java" # Visibility set for package default package_default_visibility = ":__subpackages__" # Visibility set for modules used from Connectivity and within external/cronet root_modules_visibility = {"//packages/modules/Connectivity:__subpackages__", "//external/cronet:__subpackages__"} # ---------------------------------------------------------------------------- # End of configuration. # ---------------------------------------------------------------------------- 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, Module.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 Module(object): """A single module (e.g., cc_binary, cc_test) in a blueprint.""" 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.stl = None self.cppflags = set() self.include_dirs = set() self.generated_headers = set() self.export_generated_headers = set() self.ldflags = set() self.compile_multilib = None self.stem = "" self.edition = "" self.features = set() self.cfgs = set() self.flags = list() self.rustlibs = set() self.proc_macros = set() if name == 'host': self.compile_multilib = '64' 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, 'cppflags') self._output_field(nested_out, 'include_dirs') self._output_field(nested_out, 'generated_headers') self._output_field(nested_out, 'export_generated_headers') self._output_field(nested_out, 'ldflags') self._output_field(nested_out, 'stem') self._output_field(nested_out, "edition") self._output_field(nested_out, 'cfgs') self._output_field(nested_out, 'features') self._output_field(nested_out, 'flags', False) self._output_field(nested_out, 'rustlibs') self._output_field(nested_out, 'proc_macros') if nested_out: # This is added here to make sure it doesn't add a `host` arch-specific module just for # `compile_multilib` flag. self._output_field(nested_out, 'compile_multilib') 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) 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.tools = set() self.cmd = None self.host_supported = False self.device_supported = True 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.export_header_lib_headers = set() self.defaults = set() self.cflags = set() self.include_dirs = set() self.local_include_dirs = set() self.header_libs = 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'] = self.Target('android') self.target['android_x86'] = self.Target('android_x86') self.target['android_x86_64'] = self.Target('android_x86_64') self.target['android_arm'] = self.Target('android_arm') self.target['android_arm64'] = self.Target('android_arm64') self.target['android_riscv64'] = self.Target('android_riscv64') self.target['host'] = self.Target('host') self.target['glibc'] = self.Target('glibc') self.stl = None self.cpp_std = None self.strip = dict() self.data = set() self.apex_available = set() self.min_sdk_version = None self.proto = dict() self.linker_scripts = set() self.ldflags = 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.cppflags = set() self.rtti = False # Name of the output. Used for setting .so file name for libcronet self.libs = set() self.stem = None self.compile_multilib = None self.aidl = dict() self.plugins = set() self.processor_class = None self.sdk_version = None self.javacflags = set() self.c_std = None self.default_applicable_licenses = set() self.default_visibility = [] self.visibility = set() self.gn_type = None self.jarjar_rules = "" self.jars = set() self.build_file_path = None self.include_build_directory = None self.allow_rebasing = False self.license_kinds = set() self.license_text = set() self.errorprone = dict() self.crate_name = None # Should be arch-dependant self.crate_root = None self.edition = None self.rustlibs = set() self.proc_macros = set() 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, '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') 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, 'export_header_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, '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, 'proto') self._output_field(output, 'linker_scripts') self._output_field(output, 'ldflags') self._output_field(output, 'cppflags') self._output_field(output, 'libs') self._output_field(output, 'stem') self._output_field(output, 'compile_multilib') self._output_field(output, 'aidl') self._output_field(output, 'plugins') self._output_field(output, 'processor_class') self._output_field(output, 'sdk_version') self._output_field(output, 'javacflags') self._output_field(output, 'c_std') self._output_field(output, 'default_applicable_licenses') self._output_field(output, 'default_visibility') self._output_field(output, 'visibility') self._output_field(output, 'jarjar_rules') self._output_field(output, 'jars') self._output_field(output, 'include_build_directory') self._output_field(output, 'license_text') self._output_field(output, "license_kinds") self._output_field(output, "errorprone") self._output_field(output, 'crate_name') self._output_field(output, 'crate_root') self._output_field(output, 'rustlibs') self._output_field(output, 'proc_macros') 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_shared_lib(self, lib): if self.type.startswith('java'): raise Exception('Adding Android shared lib for java_* targets is unsupported') elif 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 is_test(self): if gn_utils.TESTING_SUFFIX in self.name: name_without_prefix = self.name[:self.name.find(gn_utils.TESTING_SUFFIX)] return any([name_without_prefix == label_to_module_name(target) for target in DEFAULT_TESTS]) return False 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): if self.type in ["java_library", "java_import"]: return True if len(self.srcs) > 0: return True if any([len(target.srcs) > 0 for target in self.target.values()]): return True # Allow cc_static_library with export_generated_headers as those are crucial for # the depending modules return len(self.export_generated_headers) > 0 class Blueprint(object): """In-memory representation of an Android.bp file.""" def __init__(self, buildgn_directory_path: str = ""): self.modules = {} # Holds the BUILD.gn path which resulted in the creation of this Android.bp. self._buildgn_directory_path = buildgn_directory_path self._readme_location = buildgn_directory_path self._package_module = None self._license_module = None 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 set_package_module(self, module): self._package_module = module def set_license_module(self, module): self._license_module = module def get_license_module(self): return self._license_module def set_readme_location(self, readme_path: str): self._readme_location = readme_path def get_readme_location(self): return self._readme_location def get_buildgn_location(self): return self._buildgn_directory_path def to_string(self): ret = [] if self._package_module: self._package_module.to_string(ret) if self._license_module: self._license_module.to_string(ret) for m in sorted(self.modules.values(), key=lambda m: m.name): if m.type != "cc_library_static" or m.has_input_files(): # Don't print cc_library_static with empty srcs. These attributes are already # propagated up the tree. Printing them messes the presubmits because # every module is compiled while those targets are not reachable in # a normal compilation path. m.to_string(ret) return ret 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', '.aidl', '.rs'] def normalize_rust_flags(rust_flags: List[str]) -> Dict[str, Set[str] | None]: """ Normalizes the rust params where it tries to put (key, value) param as a dictionary key. A key without value will have None as value. An example of this would be: Input: ["--cfg=feature=\"float_roundtrip\"", "--cfg=feature=\"std\"", "--edition=2021", "-Cforce-unwind-tables=no", "-Dwarnings"] Output: { "--cfg": [feature=\"float_roundtrip\", feature=\"std\"], "--edition": [2021], "-Cforce-unwind-tables": [no], "-Dwarnings": None } :param rust_flags: List of rust flags. :return: Dictionary of rust flags where each key will point to a list of values. """ args_mapping = {} previous_key = None for rust_flag in rust_flags: if not rust_flag.startswith("-"): # This might be a key on its own, rustc supports params with no keys # such as (@path). if rust_flag.startswith("@"): args_mapping[rust_flag] = None if previous_key: args_mapping[previous_key] = None else: # This is the value to the previous key (eg: ["--cfg", "val"]) if not previous_key: raise ValueError( f"Field {rust_flag} does not relate to any key. Rust flags found: {rust_flags}") if previous_key not in args_mapping: args_mapping[previous_key] = set() args_mapping[previous_key].add(rust_flag) previous_key = None else: if previous_key: # We have a previous key, that means that the previous key is # a no-value key. args_mapping[previous_key] = None previous_key = None # This can be a key-only string or key=value or # key=foo=value (eg:--cfg=feature=X) or key and value in different strings. if "=" in rust_flag: # We found an equal, this is probably a key=value string. rust_flag_split = rust_flag.split("=") if len(rust_flag_split) > 3: raise ValueError(f"Could not normalize flag {rust_flag} as it has multiple equal signs.") if rust_flag_split[0] not in args_mapping: args_mapping[rust_flag_split[0]] = set() args_mapping[rust_flag_split[0]].add("=".join(rust_flag_split[1:])) else: # Assume this is a key-only string. This will be resolved in the next # iteration. previous_key = rust_flag if previous_key: # We have a previous key without a value, this must be a key-only string. args_mapping[previous_key] = None return args_mapping def _set_rust_flags(module: Module.Target, rust_flags: List[str], arch_name: str) -> None: rust_flags_dict = normalize_rust_flags(rust_flags) if "--edition" in rust_flags_dict: module.edition = list(rust_flags_dict["--edition"])[0] for cfg in rust_flags_dict.get("--cfg", set()): feature_regex = re.match(_FEATURE_REGEX, cfg) if feature_regex: module.features.add(feature_regex.group(1)) else: module.cfgs.add(cfg.replace("\"", "\\\"")) pre_filter_flags = [] for (key, values) in rust_flags_dict.items(): if values is None: pre_filter_flags.append(key) else: pre_filter_flags.extend(f"{key}={param_val}" for param_val in values) flags_to_remove = _RUST_FLAGS_TO_REMOVE # AOSP compiles everything for host under panic=unwind instead of abort. # In order to be consistent with the ecosystem, remove the -Cpanic flag. if arch_name == "host": flags_to_remove.append("-Cpanic") # Remove restricted flags for pre_filter_flag in pre_filter_flags: if not any([pre_filter_flag.startswith(restricted_flag) for restricted_flag in flags_to_remove]): module.flags.append(pre_filter_flag) def get_protoc_module_name(gn): protoc_gn_target_name = gn.get_target('//third_party/protobuf:protoc').name return label_to_module_name(protoc_gn_target_name) def create_rust_cxx_module(blueprint, target): """Generate genrules for a CXX GN target GN actions are used to dynamically generate files during the build. The Soong equivalent is a genrule. Currently, Chromium GN targets generates both .cc and .h files in the same target, we have to split this up to be compatible with Soong. CXX bridge binary is used from AOSP instead of compiling Chromium's CXX bridge. Args: blueprint: Blueprint instance which is being generated. target: gn_utils.Target object. Returns: The source_genrule module. """ header_genrule = Module("cc_genrule", label_to_module_name(target.name) + "_header", target.name) header_genrule.tools = {"cxxbridge"} header_genrule.cmd = "$(location cxxbridge) $(in) --header > $(out)" header_genrule.srcs = set([gn_utils.label_to_path(src) for src in target.sources]) # The output of the cc_genrule is the input + ".h" suffix, this is because # the input to a CXX genrule is just one source file. header_genrule.out = set([f"{gn_utils.label_to_path(out)}.h" for out in target.sources]) cc_genrule = Module("cc_genrule", label_to_module_name(target.name), target.name) cc_genrule.tools = {"cxxbridge"} cc_genrule.cmd = "$(location cxxbridge) $(in) > $(out)" cc_genrule.srcs = set([gn_utils.label_to_path(src) for src in target.sources]) cc_genrule.genrule_srcs = {f":{cc_genrule.name}"} # The output of the cc_genrule is the input + ".cc" suffix, this is because # the input to a CXX genrule is just one source file. cc_genrule.out = set([f"{gn_utils.label_to_path(out)}.cc" for out in target.sources]) cc_genrule.genrule_headers.add(header_genrule.name) blueprint.add_module(cc_genrule) blueprint.add_module(header_genrule) return cc_genrule 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_module_name = get_protoc_module_name(gn) tools = {protoc_module_name} cpp_out_dir = '$(genDir)/%s/' % (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)] for proto_path in target.proto_paths: cmd += [f'--proto_path={tree_path}/{proto_path}'] 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.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 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) 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] else: raise Exception('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' % src.replace('.proto', '.%s.cc' % sfx) for src in source_module.srcs) header_module.out.update('%s' % src.replace('.proto', '.%s.h' % sfx) for src in header_module.srcs) # This has proto files that will be used for reference resolution # but not compiled into cpp files. These additional sources has no output. proto_data_sources = sorted([gn_utils.label_to_path(proto_src) for proto_src in target.inputs if proto_src.endswith(".proto")]) source_module.srcs.update(proto_data_sources) header_module.srcs.update(proto_data_sources) # Allow rebasing proto genrules according to their proper path. source_module.allow_rebasing = True header_module.allow_rebasing = True header_module.build_file_path = target.build_file_path source_module.build_file_path = target.build_file_path return source_module def create_gcc_preprocess_modules(blueprint, target): # gcc_preprocess.py internally execute host gcc which is not allowed in genrule. # So, this function create multiple modules and realize equivalent processing assert (len(target.sources) == 1) source = list(target.sources)[0] assert (Path(source).suffix == '.template') stem = Path(source).stem bp_module_name = label_to_module_name(target.name) # Rename .template to .cc since cc_preprocess_no_configuration does not accept .template file as # srcs rename_module = Module('genrule', bp_module_name + '_rename', target.name) rename_module.srcs.add(gn_utils.label_to_path(source)) rename_module.out.add(stem + '.cc') rename_module.cmd = 'cp $(in) $(out)' blueprint.add_module(rename_module) # Preprocess template file and generates java file preprocess_module = Module( 'cc_preprocess_no_configuration', bp_module_name + '_preprocess', target.name ) # -E: stop after preprocessing. # -P: disable line markers, i.e. '#line 309' preprocess_module.cflags.update(['-E', '-P', '-DANDROID']) preprocess_module.srcs.add(':' + rename_module.name) defines = ['-D' + target.args[i+1] for i, arg in enumerate(target.args) if arg == '--define'] preprocess_module.cflags.update(defines) blueprint.add_module(preprocess_module) # Generates srcjar using soong_zip module = Module('genrule', bp_module_name, target.name) module.srcs.add(':' + preprocess_module.name) module.out.add(stem + '.srcjar') module.cmd = NEWLINE.join([ f'cp $(in) $(genDir)/{stem}.java &&', f'$(location soong_zip) -o $(out) -srcjar -C $(genDir) -f $(genDir)/{stem}.java' ]) module.tools.add('soong_zip') blueprint.add_module(module) return module class BaseActionSanitizer(): def __init__(self, target, arch): # Just to be on the safe side, create a deep-copy. self.target = copy.deepcopy(target) if arch: # Merge arch specific attributes self.target.sources |= arch.sources self.target.inputs |= arch.inputs self.target.outputs |= arch.outputs self.target.script = self.target.script or arch.script self.target.args = self.target.args or arch.args self.target.response_file_contents = \ self.target.response_file_contents or arch.response_file_contents 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 _update_arg_at(self, position, func): self.target.args[position] = func(self.target.args[position]) 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_pre_cmd(self): pre_cmd = [] out_dirs = [out[:out.rfind("/")] for out in self.target.outputs if "/" in out] # Sort the list to make the output deterministic. for out_dir in sorted(set(out_dirs)): pre_cmd.append("mkdir -p $(genDir)/{} && ".format(out_dir)) return NEWLINE.join(pre_cmd) def get_base_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_cmd(self): return self.get_pre_cmd() + self.get_base_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_tools(self): return set() 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_inputs(self): pass def get_deps(self): return self.target.deps def sanitize(self): self._sanitize_args() 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 GnRunBinarySanitizer(BaseActionSanitizer): def __init__(self, target, arch): super().__init__(target, arch) self.binary_to_target = { "clang_x64/transport_security_state_generator": "cronet_aml_net_tools_transport_security_state_generator_transport_security_state_generator__testing", } self.binary = self.binary_to_target[self.target.args[0]] def _replace_gen_with_location_tag(self, arg): if arg.startswith("gen/"): return "$(location %s)" % arg.replace("gen/", "") return arg def _replace_binary(self, arg): if arg in self.binary_to_target: return '$(location %s)' % self.binary return arg def _remove_python_args(self): self.target.args = [arg for arg in self.target.args if "python3" not in arg] def _sanitize_args(self): self._update_all_args(self._sanitize_filepath_with_location_tag) self._update_all_args(self._replace_gen_with_location_tag) self._update_all_args(self._replace_binary) self._remove_python_args() super()._sanitize_args() def get_tools(self): tools = super().get_tools() tools.add(self.binary) return tools def get_cmd(self): # Remove the script and use the binary right away return self.get_pre_cmd() + NEWLINE.join(self.target.args) class JniGeneratorSanitizer(BaseActionSanitizer): def __init__(self, target, arch, is_test_target): self.is_test_target = is_test_target super().__init__(target, arch) def get_srcs(self): all_srcs = super().get_srcs() all_srcs.update({gn_utils.label_to_path(file) for file in self.target.transitive_jni_java_sources if is_supported_source_file(file)}) return set(src for src in all_srcs if src.endswith(".java")) 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