• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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