Skip to content

Commit 2642dab

Browse files
committed
Support C++20 modules in the rules-based toolchain API
The rules-based cc/toolchains toolchain API was missing several pieces needed to drive Bazel's C++20 modules pipeline (clang-scan-deps -> .ddi -> aggregate-ddi -> generate-modmap -> module compile): - Add a cpp20_module_actions action type set grouping the module interface compile, codegen and dependency scanning actions. These are deliberately kept out of compile_actions/source_compile_actions so that flags incompatible with C++20 modules can keep targeting only the non-module compiles. - Register the cpp_module_output_file and cpp_module_modmap_file build variables and make dependency_file, output_file, source_file and user_compile_flags available to the C++20 module actions. - Include the C++20 module actions in the compiler_files legacy file group so their tools (e.g. the dependency scanner) are available to the compile actions. - Fix _create_scan_deps_action to pass needs_include_validation and to not request a .d dotd file (the scanner emits a P1689 .ddi file), and let _create_compile_action forward the C++20 module output/modmap arguments to the underlying action. The C++20 modules pipeline lives only in rules_cc's compile.bzl (Bazel's native rules do not implement it), and was previously untested here: Bazel's CppModulesConfiguredTargetTest only checks the --experimental_cpp_modules / cpp_modules gating, and the shell integration test self-skips unless a host clang >= 17 is present. Add an analysis test that drives the module interface compile + dependency scanning actions with a mock toolchain (no real compiler needed) and asserts the scanning action emits a .ddi and not a .d file. This test fails without the fixes above.
1 parent 6072dab commit 2642dab

7 files changed

Lines changed: 171 additions & 18 deletions

File tree

cc/private/compile/compile.bzl

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -527,21 +527,9 @@ def _create_scan_deps_action(
527527
ddi_file,
528528
ddi_output_name,
529529
progress_message_prefix):
530+
# The dependency scanner (e.g. clang-scan-deps) emits the P1689 .ddi file,
531+
# not a Make-style .d dependency file, so no dotd file is requested here.
530532
dotd_file = None
531-
if (
532-
dotd_files_enabled(language, cpp_configuration, feature_configuration) and
533-
_use_dotd_file(feature_configuration, source_artifact)
534-
):
535-
dotd_file = _get_compile_output_file(
536-
action_construction_context,
537-
label,
538-
configuration = configuration,
539-
output_name = _cc_internal.get_artifact_name_for_category(
540-
cc_toolchain = cc_toolchain,
541-
category = artifact_category.INCLUDED_FILE_LIST,
542-
output_name = ddi_output_name,
543-
),
544-
)
545533
specific_compile_build_variables = get_specific_compile_build_variables(
546534
feature_configuration,
547535
use_pic = use_pic,
@@ -579,6 +567,7 @@ def _create_scan_deps_action(
579567
compile_build_variables = compile_variables,
580568
action_name = ACTION_NAMES.cpp_module_deps_scanning,
581569
toolchain_type = _starlark_cc_semantics.toolchain,
570+
needs_include_validation = _starlark_cc_semantics.needs_include_validation(language),
582571
progress_message_prefix = progress_message_prefix,
583572
)
584573

@@ -2319,11 +2308,24 @@ def _create_compile_action(
23192308
progress_message_prefix = None,
23202309
source = None,
23212310
toolchain_type = None,
2322-
use_pic = False):
2311+
use_pic = False,
2312+
additional_outputs = [],
2313+
module_files = None,
2314+
modmap_file = None,
2315+
modmap_input_file = None):
23232316
# TODO(bgorshenev): use bazel_features checks
23242317
version_dependent_kwargs = {}
23252318
if progress_message_prefix:
23262319
version_dependent_kwargs["progress_message_prefix"] = progress_message_prefix
2320+
2321+
# Provide the C++20 module args conditionally as they require a recent
2322+
# version of Bazel.
2323+
if modmap_file:
2324+
version_dependent_kwargs["additional_outputs"] = additional_outputs
2325+
version_dependent_kwargs["module_files"] = module_files
2326+
version_dependent_kwargs["modmap_file"] = modmap_file
2327+
version_dependent_kwargs["modmap_input_file"] = modmap_input_file
2328+
23272329
return _cc_internal.create_cc_compile_action(
23282330
action_construction_context = action_construction_context,
23292331
action_name = action_name,

cc/toolchains/actions/BUILD

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,31 @@ cc_action_type_set(
272272
],
273273
)
274274

275+
# The C++20 module actions: module interface compilation, module codegen and
276+
# module dependency scanning. These are deliberately kept out of
277+
# `compile_actions`/`source_compile_actions` so that flags incompatible with
278+
# C++20 modules can keep targeting only the non-module compiles. Toolchains that
279+
# support C++20 modules typically combine this set with `compile_actions` or
280+
# `source_compile_actions` on the relevant `cc_args`, e.g.
281+
#
282+
# actions = [
283+
# "//cc/toolchains/actions:compile_actions",
284+
# "//cc/toolchains/actions:cpp20_module_actions",
285+
# ]
286+
#
287+
# The module interface compilation and codegen actions are ordinary C++ compiles
288+
# and the dependency scanning action reuses the regular compiler command line,
289+
# so toolchain flags that apply to compiles (target, sysroot, include search
290+
# paths, ...) generally need to apply to these actions as well.
291+
cc_action_type_set(
292+
name = "cpp20_module_actions",
293+
actions = [
294+
":cpp20_module_compile",
295+
":cpp20_module_codegen",
296+
":cpp_module_deps_scanning",
297+
],
298+
)
299+
275300
cc_action_type_set(
276301
name = "compile_actions_without_header_parsing",
277302
actions = [

cc/toolchains/legacy_file_group.bzl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ LEGACY_FILE_GROUPS = {
2828
Label("//cc/toolchains/actions:c_compile"),
2929
Label("//cc/toolchains/actions:cpp_compile"),
3030
Label("//cc/toolchains/actions:cpp_header_parsing"),
31+
# C++20 module actions: their tools (e.g. the dependency scanner) must
32+
# be part of compiler_files so they are available to compile actions.
33+
Label("//cc/toolchains/actions:cpp20_module_compile"),
34+
Label("//cc/toolchains/actions:cpp20_module_codegen"),
35+
Label("//cc/toolchains/actions:cpp_module_deps_scanning"),
3136
],
3237
# There are no actions listed for coverage and objcopy in action_names.bzl.
3338
"coverage_files": [],

cc/toolchains/variables/BUILD

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@ load("//cc/toolchains/impl:variables.bzl", "cc_builtin_variables", "cc_variable"
22

33
package(default_visibility = ["//visibility:public"])
44

5+
cc_variable(
6+
name = "cpp_module_modmap_file",
7+
actions = [
8+
"//cc/toolchains/actions:cpp_compile",
9+
"//cc/toolchains/actions:cpp20_module_compile",
10+
"//cc/toolchains/actions:cpp20_module_codegen",
11+
],
12+
type = types.option(types.file),
13+
)
14+
15+
cc_variable(
16+
name = "cpp_module_output_file",
17+
actions = ["//cc/toolchains/actions:cpp20_module_compile"],
18+
type = types.option(types.file),
19+
)
20+
521
cc_variable(
622
name = "cs_fdo_instrument_path",
723
actions = [
@@ -19,7 +35,10 @@ cc_variable(
1935

2036
cc_variable(
2137
name = "dependency_file",
22-
actions = ["//cc/toolchains/actions:compile_actions"],
38+
actions = [
39+
"//cc/toolchains/actions:compile_actions",
40+
"//cc/toolchains/actions:cpp20_module_actions",
41+
],
2342
type = types.option(types.file),
2443
)
2544

@@ -412,6 +431,7 @@ cc_variable(
412431
name = "output_file",
413432
actions = [
414433
"//cc/toolchains/actions:compile_actions",
434+
"//cc/toolchains/actions:cpp20_module_actions",
415435
"//cc/toolchains/actions:strip",
416436
],
417437
type = types.option(types.file),
@@ -473,7 +493,10 @@ cc_variable(
473493

474494
cc_variable(
475495
name = "source_file",
476-
actions = ["//cc/toolchains/actions:compile_actions"],
496+
actions = [
497+
"//cc/toolchains/actions:compile_actions",
498+
"//cc/toolchains/actions:cpp20_module_actions",
499+
],
477500
type = types.option(types.file),
478501
)
479502

@@ -554,7 +577,10 @@ cc_variable(
554577

555578
cc_variable(
556579
name = "user_compile_flags",
557-
actions = ["//cc/toolchains/actions:compile_actions"],
580+
actions = [
581+
"//cc/toolchains/actions:compile_actions",
582+
"//cc/toolchains/actions:cpp20_module_actions",
583+
],
558584
type = types.option(types.list(types.string)),
559585
)
560586

@@ -569,6 +595,8 @@ cc_builtin_variables(
569595
srcs = [
570596
":attr_linkopts",
571597
":cc_library_exec_paths",
598+
":cpp_module_modmap_file",
599+
":cpp_module_output_file",
572600
":cs_fdo_instrument_path",
573601
":def_file_path",
574602
":dep_linkopts",

tests/cc/common/BUILD

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ load(":cc_binary_configured_target_tests.bzl", "cc_binary_configured_target_test
22
load(":cc_common_test.bzl", "cc_common_tests")
33
load(":cc_import_configured_target_tests.bzl", "cc_import_configured_target_tests")
44
load(":cc_library_configured_target_tests.bzl", "cc_library_configured_target_tests")
5+
load(":cc_module_compile_configured_target_tests.bzl", "cc_module_compile_configured_target_tests")
56
load(":cc_objc_library_configured_target_tests.bzl", "cc_objc_library_configured_target_tests")
67

78
cc_binary_configured_target_tests(name = "cc_binary_configured_target_tests")
@@ -12,4 +13,6 @@ cc_import_configured_target_tests(name = "cc_import_configured_target_tests")
1213

1314
cc_library_configured_target_tests(name = "cc_library_configured_target_tests")
1415

16+
cc_module_compile_configured_target_tests(name = "cc_module_compile_configured_target_tests")
17+
1518
cc_objc_library_configured_target_tests(name = "cc_objc_library_configured_target_tests")
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Tests for C++20 module compilation in cc_library.
2+
3+
The C++20 modules pipeline (dependency scanning -> aggregate ddi -> generate
4+
modmap -> module interface compile) is implemented in rules_cc's compile.bzl and
5+
is not exercised by Bazel's own analysis tests (which only check the
6+
`--experimental_cpp_modules` / `cpp_modules` gating). These analysis tests cover
7+
the action-creation paths so that regressions in e.g. forwarding the module
8+
output/modmap arguments or the dependency-scanning action's outputs are caught
9+
without needing a real compiler.
10+
"""
11+
12+
load("@bazel_features//:features.bzl", "bazel_features")
13+
load("@rules_testing//lib:analysis_test.bzl", "test_suite")
14+
load("@rules_testing//lib:truth.bzl", "matching")
15+
load("@rules_testing//lib:util.bzl", "TestingAspectInfo", "util")
16+
load("//cc:cc_library.bzl", "cc_library")
17+
load("//tests/cc/testutil:cc_analysis_test.bzl", "cc_analysis_test")
18+
19+
# Action names of the C++20 module actions. Passing these to the mock toolchain's
20+
# with_action_configs makes it provide (dummy) tools for them so the actions can
21+
# be created during analysis.
22+
_CPP20_MODULE_ACTION_CONFIGS = [
23+
"c++-module-deps-scanning",
24+
"c++20-module-compile",
25+
"c++20-module-codegen",
26+
]
27+
28+
def _test_cpp20_module_interface_actions(name, **kwargs):
29+
util.helper_target(
30+
cc_library,
31+
name = name + "_subject",
32+
module_interfaces = ["mod.cppm"],
33+
)
34+
cc_analysis_test(
35+
name = name,
36+
impl = _test_cpp20_module_interface_actions_impl,
37+
target = name + "_subject",
38+
# Enables rules_cc's C++20 modules pipeline.
39+
test_features = ["cpp_modules"],
40+
# Provide (dummy) tools for the module actions so they can be created.
41+
with_action_configs = _CPP20_MODULE_ACTION_CONFIGS,
42+
# The module_interfaces attribute is gated behind this flag.
43+
config_settings = {
44+
"//command_line_option:experimental_cpp_modules": True,
45+
},
46+
**kwargs
47+
)
48+
49+
def _test_cpp20_module_interface_actions_impl(env, target):
50+
actions = target[TestingAspectInfo].actions
51+
52+
# Reaching the assertions at all means the dependency-scanning action and the
53+
# module interface compile action were created without error. That covers
54+
# regressions where the scanning action does not pass needs_include_validation
55+
# or where the module output / modmap arguments are not forwarded to the
56+
# compile action, both of which fail analysis.
57+
scan_actions = [a for a in actions if a.mnemonic == "CppDepsScanning"]
58+
env.expect.that_collection(scan_actions).has_size(1)
59+
60+
for scan in scan_actions:
61+
basenames = [f.basename for f in scan.outputs.to_list()]
62+
63+
# The scanner writes a P1689 .ddi file ...
64+
env.expect.that_collection(basenames).contains_predicate(
65+
matching.str_endswith(".ddi"),
66+
)
67+
68+
# ... and must not declare a Make-style .d dependency file, which the
69+
# scanner never produces.
70+
env.expect.that_collection(basenames).not_contains_predicate(
71+
matching.str_endswith(".d"),
72+
)
73+
74+
# The .ddi files are aggregated into the modules info before the modmap is
75+
# generated; assert the rest of the pipeline is wired up too.
76+
aggregate_actions = [a for a in actions if a.mnemonic == "CppAggregateDdi"]
77+
env.expect.that_collection(aggregate_actions).has_size(1)
78+
79+
def cc_module_compile_configured_target_tests(name):
80+
test_suite(
81+
name = name,
82+
tests = [
83+
_test_cpp20_module_interface_actions,
84+
] if bazel_features.cc.cc_common_is_in_rules_cc else [],
85+
)

tests/cc/common/mod.cppm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export module mod;
2+
3+
export int answer() {
4+
return 42;
5+
}

0 commit comments

Comments
 (0)