1#!/usr/bin/python3 2# 3# Copyright (C) 2022 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17from collections.abc import Sequence 18from functools import lru_cache 19import json 20import os 21from typing import Optional 22 23from cc.stub_generator import StubGenerator, GenCcStubsInput 24from cc.library import CompileContext, Compiler, LinkContext, Linker 25from build_file_generator import AndroidBpFile, AndroidBpModule, ConfigAxis 26from utils import ASSEMBLE_PHONY_TARGET 27 28ARCHES = ["arm", "arm64", "x86", "x86_64"] 29 30# Clang triples for cross-compiling for Android 31# TODO: Use a class? 32DEVICE_CLANG_TRIPLES = { 33 "arm": "armv7a-linux-androideabi", 34 "arm64": "aarch64-linux-android", 35 "x86": "i686-linux-android", 36 "x86_64": "x86_64-linux-android", 37} 38 39# API surfaces that should be imported into an inner tree. 40# TODO: Add `module-libapi` 41_SUPPORTED_API_SURFACES_FOR_IMPORT = { 42 "publicapi", "vendorapi", "module-libapi" 43} 44 45 46class CcApiAssemblyContext(object): 47 """Context object for managing global state of CC API Assembly.""" 48 49 def __init__(self): 50 self._stub_generator = StubGenerator() 51 self._compiler = Compiler() 52 self._linker = Linker() 53 self._api_levels_file_added = False 54 self._api_imports_module_added = False 55 self._api_stub_library_bp_file = None 56 57 def get_cc_api_assembler(self): 58 """Return a callback to assemble CC APIs. 59 60 The callback is a member of the context object, 61 and therefore has access to its state.""" 62 return self.assemble_cc_api_library 63 64 @lru_cache(maxsize=None) 65 def _api_imports_module(self, context, 66 bp_file: AndroidBpFile) -> AndroidBpModule: 67 """The wrapper api_imports module for all the stub libraries. 68 This should be generated once. 69 70 Args: 71 context: Context for global state 72 bp_file: top-level bp_file at out/api_surfaces 73 Returns: 74 api_imports AndroidBpModule object 75 """ 76 api_imports_module = AndroidBpModule( 77 name="api_imports", 78 module_type="api_imports", 79 ) 80 if not self._api_imports_module_added: 81 bp_file.add_module(api_imports_module) 82 self._api_imports_module_added = True 83 84 return api_imports_module 85 86 # Returns a handle to the AndroidBpFile containing _all_ `cc_api_library` 87 # TODO (b/265881925): This needs to be in a parent directory of all 88 # `cc_api_variant` modules, since `export_include_dirs` is currently 89 # relative to the top-level module. 90 def _get_api_stub_library_bp_file(self, context) -> AndroidBpFile: 91 if self._api_stub_library_bp_file is not None: 92 return self._api_stub_library_bp_file 93 staging_dir = context.out.api_library_dir("", "", "") 94 bp_file = AndroidBpFile(directory=staging_dir) 95 bp_file.add_comment_string("WARNING: THIS IS AN AUTOGENERATED FILE.") 96 self._api_stub_library_bp_file = bp_file 97 return bp_file 98 99 @lru_cache(maxsize=None) 100 def _api_stub_library_module(self, context, build_file_generator, 101 library_name) -> AndroidBpModule: 102 """Initializes the AndroidBpModule object for the stub library. 103 104 Also, 105 1. Adds the module to the global `api_imports` module 106 2. Adds the module to the build_file_generator object 107 108 Returns: 109 AndroidBpModule object 110 """ 111 stub_module = AndroidBpModule(name=library_name, 112 module_type="cc_api_library") 113 # TODO: min_sdk_version should not be enforced for stub libraries since 114 # they cross the api domain boundary. Remove this. 115 stub_module.add_property("min_sdk_version", "1") 116 # TODO (b/254874489): Handle sdk_version for stub library variants. 117 stub_module.add_property("sdk_version", "1") 118 # TODO: This flag is necessary since we are creating variants of stub 119 # libraries. Discuss if *_available flags will be used to determine 120 # visibility/variant creation. 121 stub_module.add_property("vendor_available", True) 122 123 # Create Android.bp file 124 # There should be one stub library definition per source library 125 # Create the files in cc/ to prevent pollution of the top-level dir. 126 bp_file = self._get_api_stub_library_bp_file(context) 127 bp_file.add_module(stub_module) 128 build_file_generator.add_android_bp_file(bp_file) 129 130 # Add module to global api_imports module 131 api_imports_module = self._api_imports_module(context, bp_file) 132 # TODO: Add header_libs explicitly if necessary. 133 api_imports_module.extend_property("shared_libs", [library_name]) 134 api_imports_module.extend_property("apex_shared_libs", [library_name]) 135 return stub_module 136 137 def _api_stub_variant_module( 138 self, context, build_file_generator, stub_library, 139 stub_module: AndroidBpModule) -> AndroidBpModule: 140 """Initializes the AndroidBpModule object for the stub library's specific variant. 141 142 Also, 143 1. Adds modules to the `cc_api_library` module 144 2. Adds modules to the build_file_generator object 145 146 Returns: 147 AndroidBpModule object 148 """ 149 # Create Android.bp file 150 staging_dir = context.out.api_library_dir( 151 stub_library.api_surface, stub_library.api_surface_version, 152 stub_library.name) 153 bp_file = AndroidBpFile(directory=staging_dir) 154 bp_file.add_comment_string("WARNING: THIS IS AN AUTOGENERATED FILE.") 155 build_file_generator.add_android_bp_file(bp_file) 156 157 stub_variant = AndroidBpModule(name=stub_library.name, 158 module_type="cc_api_variant") 159 160 if stub_library.api_surface == "vendorapi": 161 stub_variant.add_property("variant", "llndk") 162 stub_module.extend_property("variants", ["llndk"]) 163 bp_file.add_module(stub_variant) 164 return stub_variant 165 166 if stub_library.api_surface == "publicapi": 167 stub_variant.add_property("variant", "ndk") 168 stub_variant.add_property("version", 169 stub_library.api_surface_version) 170 stub_module.extend_property( 171 "variants", [f"ndk.{stub_library.api_surface_version}"]) 172 bp_file.add_module(stub_variant) 173 return stub_variant 174 175 if stub_library.api_surface == "module-libapi": 176 stub_variant.add_property("variant", "apex") 177 stub_variant.add_property("version", 178 stub_library.api_surface_version) 179 stub_module.extend_property( 180 "variants", [f"apex.{stub_library.api_surface_version}"]) 181 bp_file.add_module(stub_variant) 182 return stub_variant 183 184 raise Exception( 185 f"API surface {stub_library.api_surface} of library {stub_library.name} is not a recognized API surface." 186 ) 187 188 def assemble_cc_api_library(self, context, ninja, build_file_generator, 189 stub_library): 190 staging_dir = context.out.api_library_dir( 191 stub_library.api_surface, stub_library.api_surface_version, 192 stub_library.name) 193 work_dir = context.out.api_library_work_dir( 194 stub_library.api_surface, stub_library.api_surface_version, 195 stub_library.name) 196 197 # Generate Android.bp file for the stub library. 198 199 # TODO : Handle other API types 200 if stub_library.api_surface not in _SUPPORTED_API_SURFACES_FOR_IMPORT: 201 return 202 203 # TODO : Keep only one cc_api_library per target module 204 stub_module = self._api_stub_library_module(context, 205 build_file_generator, 206 stub_library.name) 207 208 stub_variant = self._api_stub_variant_module(context, 209 build_file_generator, 210 stub_library, stub_module) 211 212 # Generate rules to copy headers. 213 api_deps = [] 214 system_headers = False 215 for contrib in stub_library.contributions: 216 for headers in contrib.library_contribution["headers"]: 217 # Each header module gets its own include dir. 218 # TODO: Improve the readability of the generated out/ directory. 219 export_include_dir = headers["name"] 220 include_dir = os.path.join(staging_dir, export_include_dir) 221 export = "export_system_include_dirs" if headers[ 222 "system"] else "export_include_dirs" 223 # TODO : Implement export_system_headers to cc_api_variant. 224 # TODO (b/265881925): Make this relative to cc_api_variant and 225 # not cc_api_library 226 export_include_dir_relative_to_cc_api_library = os.path.join( 227 stub_library.api_surface, stub_library.api_surface_version, 228 stub_library.name, export_include_dir) 229 stub_variant.extend_property( 230 prop="export_include_dirs", 231 val=[export_include_dir_relative_to_cc_api_library], 232 axis=ConfigAxis.get_axis(headers["arch"])) 233 # TODO: Remove this and deprecate `src` and 234 # `export_include_dirs` on cc_api_library. 235 # module-libapi is not available for import, but cc_api_library 236 # incorrectly tries to provide stub libraries to apex variants. 237 # As a result, apex variants currently get an empty include path 238 # and fail (when it should use the include dir _inside_ that 239 # inner tree). 240 # The hack is to provide the headers of either publicapi or 241 # vendorapi.. 242 # The headers of these two API surfaces and module-lib api do not overlap 243 # completely. So this is not guaranteed to work for all 244 # products. 245 stub_module.extend_property( 246 prop="export_include_dirs", 247 val=[export_include_dir_relative_to_cc_api_library], 248 axis=ConfigAxis.get_axis(headers["arch"])) 249 # TODO : Set "export_headers_as_system" if it is defined from original library 250 if headers["system"]: 251 system_headers = True 252 root = headers["root"] 253 254 for file in headers["headers"]: 255 # TODO: Deal with collisions of the same name from multiple 256 # contributions. 257 # Remove the root from the full filepath. 258 # e.g. bionic/libc/include/stdio.h --> stdio.h 259 relpath = os.path.relpath(file, root) 260 include = os.path.join(include_dir, relpath) 261 ninja.add_copy_file( 262 include, os.path.join(contrib.inner_tree.root, file)) 263 api_deps.append(include) 264 265 api = contrib.library_contribution["api"] 266 api_out = os.path.join(staging_dir, os.path.basename(api)) 267 ninja.add_copy_file(api_out, 268 os.path.join(contrib.inner_tree.root, api)) 269 api_deps.append(api_out) 270 271 # Generate rules to run ndkstubgen. 272 extra_args = self._additional_ndkstubgen_args( 273 stub_library.api_surface) 274 for arch in ARCHES: 275 inputs = GenCcStubsInput( 276 arch=arch, 277 version=stub_library.api_surface_version, 278 version_map=self._api_levels_file(context), 279 api=api_out, 280 additional_args=extra_args, 281 ) 282 # Generate stub.c files for each arch. 283 stub_outputs = self._stub_generator.add_stubgen_action( 284 ninja, inputs, work_dir) 285 # Compile stub .c files to .o files. 286 # The cflags below have been copied as-is from 287 # build/soong/cc/ndk_library.go. 288 # TODO: Keep them in sync with the cflags used in single_tree. 289 c_flags = " ".join([ 290 # Allow redeclaration so that we can compile stubs of 291 # standard libraries like libc. 292 "-Wno-incompatible-library-redeclaration", 293 "-Wno-incomplete-setjmp-declaration", 294 "-Wno-builtin-requires-header", 295 "-fno-builtin", 296 # functions in stub.c files generated by ndkstubgen do not 297 # have any return statement. Relax this clang check. 298 "-Wno-invalid-noreturn", 299 "-Wall", 300 "-Werror", 301 "-fno-unwind-tables", 302 f"-target {DEVICE_CLANG_TRIPLES[arch]}", # Cross compile for android 303 ]) 304 object_file = stub_outputs.stub + ".o" 305 compile_context = CompileContext( 306 src=stub_outputs.stub, 307 flags=c_flags, 308 out=object_file, 309 frontend=context.tools.clang(), 310 ) 311 self._compiler.compile(ninja, compile_context) 312 313 # Link .o file to .so file. 314 soname = stub_library.name + ".so" 315 output_so = os.path.join(staging_dir, arch, soname) 316 ld_flags = " ".join([ 317 "--shared", 318 f"-Wl,-soname,{soname}", 319 f"-Wl,--version-script,{stub_outputs.version_script}", 320 f"-target {DEVICE_CLANG_TRIPLES[arch]}", # Cross compile for android 321 "-nostdlib", # Soong uses this option when building using bionic toolchain 322 ]) 323 link_context = LinkContext( 324 objs=[object_file], 325 flags=ld_flags, 326 out=output_so, 327 frontend=context.tools.clang_cxx(), 328 ) 329 link_context.add_implicits(api_deps) 330 self._linker.link(ninja, link_context) 331 332 # TODO: Short term hack to make the stub library available to 333 # vendor's inner tree. 334 # out/api_surfaces/stub.so is mounted into vendor's inner tree 335 # as out/api_surfaces/stub.so. Create a phony edge so that 336 # vendor binaries can link against the stub via the dep chain 337 # vendor/out/vendor_bin -> 338 # vendor/out/api_surfaces/stub.so -> 339 # out/api_surfaces/stub.so 340 # 341 for tree in ["system", "vendor", "apexes"]: 342 src_path_in_inner_tree = output_so.replace( 343 context.out.api_surfaces_dir(), 344 os.path.join(tree, context.out.root(), "api_surfaces")) 345 ninja.add_global_phony(src_path_in_inner_tree, [output_so]) 346 # Assemble the header files in out before compiling the rdeps 347 ninja.add_global_phony(src_path_in_inner_tree, api_deps) 348 349 # Add the prebuilt stub library as src to Android.bp 350 # The layout is 351 # out/../libfoo 352 # arm/libfoo.so 353 # arm64/libfo.so 354 # ... 355 # `src` property of cc_api_library is a no-op and will 356 # eventually be removed. Point it to a non-existent file for 357 # now. This will NOT exist during the combined execution. 358 self._add_src_to_stub_module(stub_module, soname, arch) 359 stub_variant.add_property("src", os.path.join(arch, soname), 360 ConfigAxis.get_axis(arch)) 361 362 # Mark as system headers. 363 stub_variant.add_property("export_headers_as_system", 364 val=system_headers) 365 366 # Generate rules to build the API levels map. 367 if not self._api_levels_file_added: 368 self._add_api_levels_file(context, ninja) 369 self._api_levels_file_added = True 370 371 # Generate phony rule to build the library. 372 # TODO: This name probably conflictgs with something. 373 phony = "-".join([ 374 stub_library.api_surface, 375 str(stub_library.api_surface_version), stub_library.name 376 ]) 377 ninja.add_phony(phony, api_deps) 378 # Add a global phony to assemnble all apis 379 ninja.add_global_phony(ASSEMBLE_PHONY_TARGET, [phony]) 380 381 # Add src to the top level `cc_api_library`. 382 # This should happen once across all `cc_api_variant` modules. 383 @lru_cache(maxsize=None) 384 def _add_src_to_stub_module(self, stub_module, soname, arch): 385 stub_module.add_property("src", os.path.join(arch, soname), 386 ConfigAxis.get_axis(arch)) 387 388 def _additional_ndkstubgen_args(self, api_surface: str) -> str: 389 if api_surface == "vendorapi": 390 return "--llndk" 391 if api_surface == "module-libapi": 392 # The "module-libapi" surface has contributions from the following: 393 # 1. Apex, which are annotated as #apex in map.txt 394 # 2. Platform, which are annotated as #sytemapi in map.txt 395 # 396 # Run ndkstubgen with both these annotations. 397 return "--apex --systemapi" 398 return "" 399 400 def _add_api_levels_file(self, context, ninja): 401 # ndkstubgen uses a map for converting Android version codes to a 402 # numeric code. e.g. "R" --> 30 403 # The map contains active_codenames as well, which get mapped to a preview level 404 # (9000+). 405 # TODO: Keep this in sync with build/soong/android/api_levels.go. 406 active_codenames = ["UpsideDownCake"] 407 preview_api_level_base = 9000 408 api_levels = { 409 "G": 9, 410 "I": 14, 411 "J": 16, 412 "J-MR1": 17, 413 "J-MR2": 18, 414 "K": 19, 415 "L": 21, 416 "L-MR1": 22, 417 "M": 23, 418 "N": 24, 419 "N-MR1": 25, 420 "O": 26, 421 "O-MR1": 27, 422 "P": 28, 423 "Q": 29, 424 "R": 30, 425 "S": 31, 426 "S-V2": 32, 427 "Tiramisu": 33, 428 } 429 for index, codename in enumerate(active_codenames): 430 api_levels[codename] = preview_api_level_base + index 431 432 file = self._api_levels_file(context) 433 ninja.add_write_file(file, json.dumps(api_levels)) 434 435 def _api_levels_file(self, context) -> str: 436 """Returns a path in to generated api_levels map in the intermediates directory. 437 438 This file is not specific to a single stub library, and can be generated once""" 439 return context.out.api_surfaces_work_dir("api_levels.json") 440