blob: 73d0bf1ca2d13fa1c9421602a21bb9a8f2c52c1d [file] [log] [blame]
#!/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")