#!/usr/bin/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. from collections.abc import Sequence from functools import lru_cache import json import os from typing import Optional from cc.stub_generator import StubGenerator, GenCcStubsInput from cc.library import CompileContext, Compiler, LinkContext, Linker from build_file_generator import AndroidBpFile, AndroidBpModule, ConfigAxis from utils import ASSEMBLE_PHONY_TARGET ARCHES = ["arm", "arm64", "x86", "x86_64"] # Clang triples for cross-compiling for Android # TODO: Use a class? DEVICE_CLANG_TRIPLES = { "arm": "armv7a-linux-androideabi", "arm64": "aarch64-linux-android", "x86": "i686-linux-android", "x86_64": "x86_64-linux-android", } # API surfaces that should be imported into an inner tree. # TODO: Add `module-libapi` _SUPPORTED_API_SURFACES_FOR_IMPORT = { "publicapi", "vendorapi", "module-libapi" } class CcApiAssemblyContext(object): """Context object for managing global state of CC API Assembly.""" def __init__(self): self._stub_generator = StubGenerator() self._compiler = Compiler() self._linker = Linker() self._api_levels_file_added = False self._api_imports_module_added = False self._api_stub_library_bp_file = None def get_cc_api_assembler(self): """Return a callback to assemble CC APIs. The callback is a member of the context object, and therefore has access to its state.""" return self.assemble_cc_api_library @lru_cache(maxsize=None) def _api_imports_module(self, context, bp_file: AndroidBpFile) -> AndroidBpModule: """The wrapper api_imports module for all the stub libraries. This should be generated once. Args: context: Context for global state bp_file: top-level bp_file at out/api_surfaces Returns: api_imports AndroidBpModule object """ api_imports_module = AndroidBpModule( name="api_imports", module_type="api_imports", ) if not self._api_imports_module_added: bp_file.add_module(api_imports_module) self._api_imports_module_added = True return api_imports_module # Returns a handle to the AndroidBpFile containing _all_ `cc_api_library` # TODO (b/265881925): This needs to be in a parent directory of all # `cc_api_variant` modules, since `export_include_dirs` is currently # relative to the top-level module. def _get_api_stub_library_bp_file(self, context) -> AndroidBpFile: if self._api_stub_library_bp_file is not None: return self._api_stub_library_bp_file staging_dir = context.out.api_library_dir("", "", "") bp_file = AndroidBpFile(directory=staging_dir) bp_file.add_comment_string("WARNING: THIS IS AN AUTOGENERATED FILE.") self._api_stub_library_bp_file = bp_file return bp_file @lru_cache(maxsize=None) def _api_stub_library_module(self, context, build_file_generator, library_name) -> AndroidBpModule: """Initializes the AndroidBpModule object for the stub library. Also, 1. Adds the module to the global `api_imports` module 2. Adds the module to the build_file_generator object Returns: AndroidBpModule object """ stub_module = AndroidBpModule(name=library_name, module_type="cc_api_library") # TODO: min_sdk_version should not be enforced for stub libraries since # they cross the api domain boundary. Remove this. stub_module.add_property("min_sdk_version", "1") # TODO (b/254874489): Handle sdk_version for stub library variants. stub_module.add_property("sdk_version", "1") # TODO: This flag is necessary since we are creating variants of stub # libraries. Discuss if *_available flags will be used to determine # visibility/variant creation. stub_module.add_property("vendor_available", True) # Create Android.bp file # There should be one stub library definition per source library # Create the files in cc/ to prevent pollution of the top-level dir. bp_file = self._get_api_stub_library_bp_file(context) bp_file.add_module(stub_module) build_file_generator.add_android_bp_file(bp_file) # Add module to global api_imports module api_imports_module = self._api_imports_module(context, bp_file) # TODO: Add header_libs explicitly if necessary. api_imports_module.extend_property("shared_libs", [library_name]) api_imports_module.extend_property("apex_shared_libs", [library_name]) return stub_module def _api_stub_variant_module( self, context, build_file_generator, stub_library, stub_module: AndroidBpModule) -> AndroidBpModule: """Initializes the AndroidBpModule object for the stub library's specific variant. Also, 1. Adds modules to the `cc_api_library` module 2. Adds modules to the build_file_generator object Returns: AndroidBpModule object """ # Create Android.bp file staging_dir = context.out.api_library_dir( stub_library.api_surface, stub_library.api_surface_version, stub_library.name) bp_file = AndroidBpFile(directory=staging_dir) bp_file.add_comment_string("WARNING: THIS IS AN AUTOGENERATED FILE.") build_file_generator.add_android_bp_file(bp_file) stub_variant = AndroidBpModule(name=stub_library.name, module_type="cc_api_variant") if stub_library.api_surface == "vendorapi": stub_variant.add_property("variant", "llndk") stub_module.extend_property("variants", ["llndk"]) bp_file.add_module(stub_variant) return stub_variant if stub_library.api_surface == "publicapi": stub_variant.add_property("variant", "ndk") stub_variant.add_property("version", stub_library.api_surface_version) stub_module.extend_property( "variants", [f"ndk.{stub_library.api_surface_version}"]) bp_file.add_module(stub_variant) return stub_variant if stub_library.api_surface == "module-libapi": stub_variant.add_property("variant", "apex") stub_variant.add_property("version", stub_library.api_surface_version) stub_module.extend_property( "variants", [f"apex.{stub_library.api_surface_version}"]) bp_file.add_module(stub_variant) return stub_variant raise Exception( f"API surface {stub_library.api_surface} of library {stub_library.name} is not a recognized API surface." ) def assemble_cc_api_library(self, context, ninja, build_file_generator, stub_library): staging_dir = context.out.api_library_dir( stub_library.api_surface, stub_library.api_surface_version, stub_library.name) work_dir = context.out.api_library_work_dir( stub_library.api_surface, stub_library.api_surface_version, stub_library.name) # Generate Android.bp file for the stub library. # TODO : Handle other API types if stub_library.api_surface not in _SUPPORTED_API_SURFACES_FOR_IMPORT: return # TODO : Keep only one cc_api_library per target module stub_module = self._api_stub_library_module(context, build_file_generator, stub_library.name) stub_variant = self._api_stub_variant_module(context, build_file_generator, stub_library, stub_module) # Generate rules to copy headers. api_deps = [] system_headers = False for contrib in stub_library.contributions: for headers in contrib.library_contribution["headers"]: # Each header module gets its own include dir. # TODO: Improve the readability of the generated out/ directory. export_include_dir = headers["name"] include_dir = os.path.join(staging_dir, export_include_dir) export = "export_system_include_dirs" if headers[ "system"] else "export_include_dirs" # TODO : Implement export_system_headers to cc_api_variant. # TODO (b/265881925): Make this relative to cc_api_variant and # not cc_api_library export_include_dir_relative_to_cc_api_library = os.path.join( stub_library.api_surface, stub_library.api_surface_version, stub_library.name, export_include_dir) stub_variant.extend_property( prop="export_include_dirs", val=[export_include_dir_relative_to_cc_api_library], axis=ConfigAxis.get_axis(headers["arch"])) # TODO: Remove this and deprecate `src` and # `export_include_dirs` on cc_api_library. # module-libapi is not available for import, but cc_api_library # incorrectly tries to provide stub libraries to apex variants. # As a result, apex variants currently get an empty include path # and fail (when it should use the include dir _inside_ that # inner tree). # The hack is to provide the headers of either publicapi or # vendorapi.. # The headers of these two API surfaces and module-lib api do not overlap # completely. So this is not guaranteed to work for all # products. stub_module.extend_property( prop="export_include_dirs", val=[export_include_dir_relative_to_cc_api_library], axis=ConfigAxis.get_axis(headers["arch"])) # TODO : Set "export_headers_as_system" if it is defined from original library if headers["system"]: system_headers = True root = headers["root"] for file in headers["headers"]: # TODO: Deal with collisions of the same name from multiple # contributions. # Remove the root from the full filepath. # e.g. bionic/libc/include/stdio.h --> stdio.h relpath = os.path.relpath(file, root) include = os.path.join(include_dir, relpath) ninja.add_copy_file( include, os.path.join(contrib.inner_tree.root, file)) api_deps.append(include) api = contrib.library_contribution["api"] api_out = os.path.join(staging_dir, os.path.basename(api)) ninja.add_copy_file(api_out, os.path.join(contrib.inner_tree.root, api)) api_deps.append(api_out) # Generate rules to run ndkstubgen. extra_args = self._additional_ndkstubgen_args( stub_library.api_surface) for arch in ARCHES: inputs = GenCcStubsInput( arch=arch, version=stub_library.api_surface_version, version_map=self._api_levels_file(context), api=api_out, additional_args=extra_args, ) # Generate stub.c files for each arch. stub_outputs = self._stub_generator.add_stubgen_action( ninja, inputs, work_dir) # Compile stub .c files to .o files. # The cflags below have been copied as-is from # build/soong/cc/ndk_library.go. # TODO: Keep them in sync with the cflags used in single_tree. c_flags = " ".join([ # Allow redeclaration so that we can compile stubs of # standard libraries like libc. "-Wno-incompatible-library-redeclaration", "-Wno-incomplete-setjmp-declaration", "-Wno-builtin-requires-header", "-fno-builtin", # functions in stub.c files generated by ndkstubgen do not # have any return statement. Relax this clang check. "-Wno-invalid-noreturn", "-Wall", "-Werror", "-fno-unwind-tables", f"-target {DEVICE_CLANG_TRIPLES[arch]}", # Cross compile for android ]) object_file = stub_outputs.stub + ".o" compile_context = CompileContext( src=stub_outputs.stub, flags=c_flags, out=object_file, frontend=context.tools.clang(), ) self._compiler.compile(ninja, compile_context) # Link .o file to .so file. soname = stub_library.name + ".so" output_so = os.path.join(staging_dir, arch, soname) ld_flags = " ".join([ "--shared", f"-Wl,-soname,{soname}", f"-Wl,--version-script,{stub_outputs.version_script}", f"-target {DEVICE_CLANG_TRIPLES[arch]}", # Cross compile for android "-nostdlib", # Soong uses this option when building using bionic toolchain ]) link_context = LinkContext( objs=[object_file], flags=ld_flags, out=output_so, frontend=context.tools.clang_cxx(), ) link_context.add_implicits(api_deps) self._linker.link(ninja, link_context) # TODO: Short term hack to make the stub library available to # vendor's inner tree. # out/api_surfaces/stub.so is mounted into vendor's inner tree # as out/api_surfaces/stub.so. Create a phony edge so that # vendor binaries can link against the stub via the dep chain # vendor/out/vendor_bin -> # vendor/out/api_surfaces/stub.so -> # out/api_surfaces/stub.so # for tree in ["system", "vendor", "apexes"]: src_path_in_inner_tree = output_so.replace( context.out.api_surfaces_dir(), os.path.join(tree, context.out.root(), "api_surfaces")) ninja.add_global_phony(src_path_in_inner_tree, [output_so]) # Assemble the header files in out before compiling the rdeps ninja.add_global_phony(src_path_in_inner_tree, api_deps) # Add the prebuilt stub library as src to Android.bp # The layout is # out/../libfoo # arm/libfoo.so # arm64/libfo.so # ... # `src` property of cc_api_library is a no-op and will # eventually be removed. Point it to a non-existent file for # now. This will NOT exist during the combined execution. self._add_src_to_stub_module(stub_module, soname, arch) stub_variant.add_property("src", os.path.join(arch, soname), ConfigAxis.get_axis(arch)) # Mark as system headers. stub_variant.add_property("export_headers_as_system", val=system_headers) # Generate rules to build the API levels map. if not self._api_levels_file_added: self._add_api_levels_file(context, ninja) self._api_levels_file_added = True # Generate phony rule to build the library. # TODO: This name probably conflictgs with something. phony = "-".join([ stub_library.api_surface, str(stub_library.api_surface_version), stub_library.name ]) ninja.add_phony(phony, api_deps) # Add a global phony to assemnble all apis ninja.add_global_phony(ASSEMBLE_PHONY_TARGET, [phony]) # Add src to the top level `cc_api_library`. # This should happen once across all `cc_api_variant` modules. @lru_cache(maxsize=None) def _add_src_to_stub_module(self, stub_module, soname, arch): stub_module.add_property("src", os.path.join(arch, soname), ConfigAxis.get_axis(arch)) def _additional_ndkstubgen_args(self, api_surface: str) -> str: if api_surface == "vendorapi": return "--llndk" if api_surface == "module-libapi": # The "module-libapi" surface has contributions from the following: # 1. Apex, which are annotated as #apex in map.txt # 2. Platform, which are annotated as #sytemapi in map.txt # # Run ndkstubgen with both these annotations. return "--apex --systemapi" return "" def _add_api_levels_file(self, context, ninja): # ndkstubgen uses a map for converting Android version codes to a # numeric code. e.g. "R" --> 30 # The map contains active_codenames as well, which get mapped to a preview level # (9000+). # TODO: Keep this in sync with build/soong/android/api_levels.go. active_codenames = ["UpsideDownCake"] preview_api_level_base = 9000 api_levels = { "G": 9, "I": 14, "J": 16, "J-MR1": 17, "J-MR2": 18, "K": 19, "L": 21, "L-MR1": 22, "M": 23, "N": 24, "N-MR1": 25, "O": 26, "O-MR1": 27, "P": 28, "Q": 29, "R": 30, "S": 31, "S-V2": 32, "Tiramisu": 33, } for index, codename in enumerate(active_codenames): api_levels[codename] = preview_api_level_base + index file = self._api_levels_file(context) ninja.add_write_file(file, json.dumps(api_levels)) def _api_levels_file(self, context) -> str: """Returns a path in to generated api_levels map in the intermediates directory. This file is not specific to a single stub library, and can be generated once""" return context.out.api_surfaces_work_dir("api_levels.json")