xref: /aosp_15_r20/external/skia/toolchain/clang_layering_check.bzl (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1"""
2This file contains logic related to enforcing public API relationships, also known as
3layering checks.
4
5See also https://maskray.me/blog/2022-09-25-layering-check-with-clang and go/layering_check
6
7"""
8
9# https://github.com/bazelbuild/bazel/blob/master/tools/build_defs/cc/action_names.bzl
10load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES")
11
12# https://github.com/bazelbuild/bazel/blob/master/tools/cpp/cc_toolchain_config_lib.bzl
13load(
14    "@bazel_tools//tools/cpp:cc_toolchain_config_lib.bzl",
15    "feature",
16    "feature_set",
17    "flag_group",
18    "flag_set",
19)
20
21def make_layering_check_features():
22    """Returns a list of features which enforce "layering checks".
23
24    Layering checks catch two types of problems:
25      1) A cc_library using private headers from another cc_library.
26      2) A cc_library using public headers from a transitive dependency instead of
27         directly depending on that library.
28
29    This is implemented using Clang module maps, which are generated for each cc_library
30    as it is being built.
31
32    This implementation is very similar to the one in the default Bazel C++ toolchain
33    (which is not inherited by custom toolchains).
34    https://github.com/bazelbuild/bazel/commit/8b9f74649512ee17ac52815468bf3d7e5e71c9fa
35
36    Returns:
37        A list of Bazel "features", the primary one being one called "layering_check".
38    """
39    return [
40        feature(
41            name = "use_module_maps",
42            enabled = False,
43            requires = [feature_set(features = ["module_maps"])],
44            flag_sets = [
45                flag_set(
46                    actions = [
47                        ACTION_NAMES.c_compile,
48                        ACTION_NAMES.cpp_compile,
49                    ],
50                    flag_groups = [
51                        flag_group(
52                            flags = [
53                                "-fmodule-name=%{module_name}",
54                                "-fmodule-map-file=%{module_map_file}",
55                            ],
56                        ),
57                    ],
58                ),
59            ],
60        ),
61        # This feature name is baked into Bazel
62        # https://github.com/bazelbuild/bazel/blob/8f5b626acea0086be8a314d5efbf6bc6d3473cd2/src/main/java/com/google/devtools/build/lib/rules/cpp/CompileBuildVariables.java#L471
63        feature(name = "module_maps", enabled = True),
64        feature(
65            name = "layering_check",
66            # This is currently disabled by default (although we aim to enable it by default)
67            # because a previous build didn't support passing a flag along.
68            # TODO(kjlubick): enable this by default.
69            enabled = False,
70            implies = ["use_module_maps"],
71            flag_sets = [
72                flag_set(
73                    actions = [
74                        ACTION_NAMES.c_compile,
75                        ACTION_NAMES.cpp_compile,
76                    ],
77                    flag_groups = [
78                        flag_group(flags = [
79                            # Identify issue #1 (see docstring)
80                            "-Wprivate-header",
81                            # Identify issue #2
82                            "-fmodules-strict-decluse",
83                        ]),
84                        flag_group(
85                            iterate_over = "dependent_module_map_files",
86                            flags = [
87                                "-fmodule-map-file=%{dependent_module_map_files}",
88                            ],
89                        ),
90                    ],
91                ),
92            ],
93        ),
94    ]
95
96def generate_system_module_map(ctx, module_file, folders):
97    """Generates a module map [1] for all the "system" headers in the toolchain.
98
99    The generated map looks something like:
100        module "crosstool" [system] {
101            textual header "lib/clang/15.0.1/include/__clang_cuda_builtin_vars.h"
102            textual header "lib/clang/15.0.1/include/__clang_cuda_cmath.h"
103            ...
104            textual header "include/c++/v1/climits"
105            textual header "include/c++/v1/clocale"
106            textual header "include/c++/v1/cmath"
107            textual header "symlinks/xcode/MacSDK/usr/share/man/mann/zip.n"
108        }
109    Notice how all the file paths are relative to *this* directory, where
110    the toolchain_system_headers.modulemap. Annoyingly, Clang will silently
111    ignore a file that is declared if it does not actually exist on disk.
112
113    [1] https://clang.llvm.org/docs/Modules.html#module-map-language
114
115    Args:
116        ctx: A repository_ctx (https://bazel.build/rules/lib/repository_ctx)
117        module_file: The name of the modulemap file to create.
118        folders: List of strings corresponding to paths in the toolchain with system headers.
119
120    """
121
122    # https://github.com/bazelbuild/bazel/blob/8f5b626acea0086be8a314d5efbf6bc6d3473cd2/tools/cpp/generate_system_module_map.sh
123    script_path = ctx.path(Label("@bazel_tools//tools/cpp:generate_system_module_map.sh"))
124
125    # https://bazel.build/rules/lib/repository_ctx#execute
126    res = ctx.execute([script_path] + folders)
127    if res.return_code != 0:
128        fail("Could not generate module map")
129
130    # https://bazel.build/rules/lib/repository_ctx#file
131    ctx.file(
132        module_file,
133        content = res.stdout,
134        executable = False,
135    )
136