| #!/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"} |
| |
| |
| 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_library_src_added = { |
| "arm": set(), |
| "arm64": set(), |
| "x86": set(), |
| "x86_64": set(), |
| } |
| 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]) |
| 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) |
| bp_file.add_module(stub_variant) |
| return stub_variant |
| |
| # TODO : Handle other variants |
| return None |
| |
| 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(), |
| ) |
| 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) |
| |
| # TODO: Hack to make analysis of apex modules pass. |
| # Tracking bug: b/264963986 |
| # Creates an so file as `src` for the top-level |
| # `cc_api_library`. |
| # This is necessary since SystemApi import for apexes is WIP, |
| # and the mainline module variants (incorrectly) try to link |
| # against the `src` of the top-level `cc_api_library`. |
| # This property should be a no-op anyways, so hardcode it to |
| # arm64 for now. |
| if stub_library.name not in self._api_library_src_added[arch]: |
| for tree in ["system", "vendor", "apexes"]: |
| # Copy the file to the top-level cc_api_library in out. |
| # e.g. src: |
| # out/api_surfaces/publicapi/current/libc/arm/libc.so |
| # e.g. dst: out/api_surfaces/arm/libc.so |
| top_level_output_so = os.path.join( |
| context.out.api_surfaces_dir(), arch, soname) |
| ninja.add_copy_file(top_level_output_so, output_so) |
| # Create a phony target of this to inner tree |
| # e.g. src: out/api_surfaces/arm/libc.so |
| # e.g. dst: vendor/out/api_surfaces/arm/libc.so |
| ninja.add_global_phony( |
| os.path.join(tree, top_level_output_so), |
| [top_level_output_so]) |
| self._api_library_src_added[arch].add( |
| stub_library.name) |
| |
| # 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") |