[1/5] support C++20 Modules, add module_interfaces attr

I split the XXL PR https://github.com/bazelbuild/bazel/pull/19940 into several small patches.
This is the first patch of Support C++20 Modules, I add `module_interfaces` attr only

example

-  foo.cppm
```
// foo.cppm
export module foo;
// ...
```
- BUILD.bazel

```
cc_library(
    name="foo",
    copts=["-std=c++20"],
    module_interfaces=["foo.cppm"],
    # features=["cpp20_module"]
)

```

build failed with the following message

```
➜  bazel build :foo
ERROR: bazel_demo/BUILD.bazel:1:11: in cc_library rule //:foo:
Traceback (most recent call last):
        File "/virtual_builtins_bzl/common/cc/cc_library.bzl", line 40, column 42, in _cc_library_impl
        File "/virtual_builtins_bzl/common/cc/semantics.bzl", line 123, column 13, in _check_can_module_interfaces
Error in fail: attribute module_interfaces: requires --experimental_cpp20_modules
ERROR: bazel_demo/BUILD.bazel:1:11: Analysis of target '//:foo' failed
ERROR: Analysis of target '//:foo' failed; build aborted
INFO: Elapsed time: 0.106s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
ERROR: Build did NOT complete successfully
```
To build with C++20 Modules, the flag `--experimental_cpp20_modules` must be added.

```
➜  bazel build :foo --experimental_cpp20_modules
ERROR: bazel_demo/BUILD.bazel:1:11: in cc_library rule //:foo:
Traceback (most recent call last):
        File "/virtual_builtins_bzl/common/cc/cc_library.bzl", line 41, column 34, in _cc_library_impl
        File "/virtual_builtins_bzl/common/cc/cc_helper.bzl", line 1225, column 13, in _check_cpp20_modules
Error in fail: to use C++20 Modules, the feature cpp20_modules must be enabled
ERROR: bazel_demo/BUILD.bazel:1:11: Analysis of target '//:foo' failed
ERROR: Analysis of target '//:foo' failed; build aborted
INFO: Elapsed time: 0.091s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
ERROR: Build did NOT complete successfully
```

To build with C++20 Modules, the feature `cpp20_modules` must be enabled.

```
bazel build :foo --experimental_cpp20_modules --features cpp20_modules
```

the flag `--experimental_cpp20_modules` works on global and
the feature `cpp20_modules` work on each target

but in this patch, do nothing with C++20 Module Interfaces.

Closes #22425.

PiperOrigin-RevId: 643303029
Change-Id: I08d8a1186d2ddd1c632f1e768442e504b87a0691
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationHelper.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationHelper.java
index 74dbb15..97f3ece 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCompilationHelper.java
@@ -269,6 +269,7 @@
   private final List<PathFragment> additionalExportedHeaders = new ArrayList<>();
   private final List<CppModuleMap> additionalCppModuleMaps = new ArrayList<>();
   private final LinkedHashMap<Artifact, CppSource> compilationUnitSources = new LinkedHashMap<>();
+  private final LinkedHashMap<Artifact, CppSource> moduleInterfaceSources = new LinkedHashMap<>();
   private ImmutableList<String> copts = ImmutableList.of();
   private CoptsFilter coptsFilter = CoptsFilter.alwaysPasses();
   private final Set<String> defines = new LinkedHashSet<>();
@@ -518,6 +519,27 @@
     return addSources(Arrays.asList(sources));
   }
 
+  @CanIgnoreReturnValue
+  public CcCompilationHelper addModuleInterfaceSources(Collection<Artifact> sources) {
+    for (Artifact source : sources) {
+      addModuleInterfaceSource(source, label);
+    }
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  public CcCompilationHelper addModuleInterfaceSources(Iterable<Pair<Artifact, Label>> sources) {
+    for (Pair<Artifact, Label> source : sources) {
+      addModuleInterfaceSource(source.first, source.second);
+    }
+    return this;
+  }
+
+  private void addModuleInterfaceSource(Artifact source, Label label) {
+    Preconditions.checkNotNull(featureConfiguration);
+    moduleInterfaceSources.put(source, CppSource.create(source, label, CppSource.Type.SOURCE));
+  }
+
   /** Add the corresponding files as non-header, non-source input files. */
   @CanIgnoreReturnValue
   public CcCompilationHelper addAdditionalInputs(Collection<Artifact> inputs) {
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcModule.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcModule.java
index 95461fc..1ab8951 100755
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcModule.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcModule.java
@@ -2063,6 +2063,7 @@
       Object purposeObject,
       Object coptsFilterObject,
       Object separateModuleHeadersObject,
+      Sequence<?> moduleInterfacesUnchecked, // <Artifact> expected
       Object nonCompilationAdditionalInputsObject,
       StarlarkThread thread)
       throws EvalException, InterruptedException {
@@ -2151,23 +2152,37 @@
                 configuration.getFragment(CppConfiguration.class)));
     boolean tuple =
         (!sourcesUnchecked.isEmpty() && sourcesUnchecked.get(0) instanceof Tuple)
+            || (!moduleInterfacesUnchecked.isEmpty()
+                && moduleInterfacesUnchecked.get(0) instanceof Tuple)
             || (!publicHeadersUnchecked.isEmpty() && publicHeadersUnchecked.get(0) instanceof Tuple)
             || (!privateHeadersUnchecked.isEmpty()
                 && privateHeadersUnchecked.get(0) instanceof Tuple);
     if (tuple) {
       ImmutableList<Pair<Artifact, Label>> sources = convertSequenceTupleToPair(sourcesUnchecked);
+      ImmutableList<Pair<Artifact, Label>> moduleInterfaces =
+          convertSequenceTupleToPair(moduleInterfacesUnchecked);
       ImmutableList<Pair<Artifact, Label>> publicHeaders =
           convertSequenceTupleToPair(publicHeadersUnchecked);
       ImmutableList<Pair<Artifact, Label>> privateHeaders =
           convertSequenceTupleToPair(privateHeadersUnchecked);
-      helper.addPublicHeaders(publicHeaders).addPrivateHeaders(privateHeaders).addSources(sources);
+      helper
+          .addPublicHeaders(publicHeaders)
+          .addPrivateHeaders(privateHeaders)
+          .addSources(sources)
+          .addModuleInterfaceSources(moduleInterfaces);
     } else {
       List<Artifact> sources = Sequence.cast(sourcesUnchecked, Artifact.class, "srcs");
+      List<Artifact> moduleInterfaces =
+          Sequence.cast(moduleInterfacesUnchecked, Artifact.class, "module_interfaces");
       List<Artifact> publicHeaders =
           Sequence.cast(publicHeadersUnchecked, Artifact.class, "public_hdrs");
       List<Artifact> privateHeaders =
           Sequence.cast(privateHeadersUnchecked, Artifact.class, "private_hdrs");
-      helper.addPublicHeaders(publicHeaders).addPrivateHeaders(privateHeaders).addSources(sources);
+      helper
+          .addPublicHeaders(publicHeaders)
+          .addPrivateHeaders(privateHeaders)
+          .addSources(sources)
+          .addModuleInterfaceSources(moduleInterfaces);
     }
 
     List<String> includes =
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppConfiguration.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppConfiguration.java
index f75e4b7..f4ec901 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppConfiguration.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppConfiguration.java
@@ -924,10 +924,20 @@
     return experimentalCcImplementationDeps();
   }
 
+  @StarlarkMethod(name = "experimental_cpp_modules", documented = false, useStarlarkThread = true)
+  public boolean experimentalCppModulesForStarlark(StarlarkThread thread) throws EvalException {
+    CcModule.checkPrivateStarlarkificationAllowlist(thread);
+    return experimentalCppModules();
+  }
+
   public boolean experimentalCcImplementationDeps() {
     return cppOptions.experimentalCcImplementationDeps;
   }
 
+  public boolean experimentalCppModules() {
+    return cppOptions.experimentalCppModules;
+  }
+
   public boolean getExperimentalCppCompileResourcesEstimation() {
     return cppOptions.experimentalCppCompileResourcesEstimation;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppOptions.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppOptions.java
index 8b9b65f..b6871ea 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppOptions.java
@@ -1039,6 +1039,23 @@
   public boolean experimentalCcImplementationDeps;
 
   @Option(
+      name = "experimental_cpp_modules",
+      defaultValue = "false",
+      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+      effectTags = {
+        OptionEffectTag.LOADING_AND_ANALYSIS,
+        OptionEffectTag.EXECUTION,
+        OptionEffectTag.CHANGES_INPUTS
+      },
+      metadataTags = {OptionMetadataTag.EXPERIMENTAL},
+      help =
+          "Enables experimental C++20 modules support. Use it with `module_interfaces` attribute on"
+              + " `cc_binary` and `cc_library`. While the support is behind the experimental flag,"
+              + " there are no guarantees about incompatible changes to it or even keeping the"
+              + " support in the future. Consider those risks when using it.")
+  public boolean experimentalCppModules;
+
+  @Option(
       name = "experimental_link_static_libraries_once",
       defaultValue = "true",
       documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppRuleClasses.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppRuleClasses.java
index bed2699..67f604e 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppRuleClasses.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppRuleClasses.java
@@ -107,6 +107,9 @@
    */
   public static final String MODULE_MAPS = "module_maps";
 
+  /** A string constant for the cpp_modules feature. */
+  public static final String CPP_MODULES = "cpp_modules";
+
   /**
    * A string constant for the random_seed feature. This is used by gcc and Clangfor the
    * randomization of symbol names that are in the anonymous namespace but have external linkage.
diff --git a/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/cpp/CcModuleApi.java b/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/cpp/CcModuleApi.java
index ada290f..bc53c21 100755
--- a/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/cpp/CcModuleApi.java
+++ b/src/main/java/com/google/devtools/build/lib/starlarkbuildapi/cpp/CcModuleApi.java
@@ -361,6 +361,14 @@
             allowedTypes = {@ParamType(type = Sequence.class)},
             defaultValue = "unbound"),
         @Param(
+            name = "module_interfaces",
+            doc =
+                "The list of module interfaces source files to be compiled. Note: this is an"
+                    + " experimental feature, only enabled with --experimental_cpp_modules",
+            positional = false,
+            named = true,
+            defaultValue = "unbound"),
+        @Param(
             name = "non_compilation_additional_inputs",
             positional = false,
             named = true,
@@ -405,6 +413,7 @@
       Object purposeObject,
       Object coptsFilterObject,
       Object separateModuleHeadersObject,
+      Sequence<?> moduleInterfacesUnchecked, // <Artifact> expected
       Object nonCompilationAdditionalInputsObject,
       StarlarkThread thread)
       throws EvalException, InterruptedException;
diff --git a/src/main/starlark/builtins_bzl/common/builtin_exec_platforms.bzl b/src/main/starlark/builtins_bzl/common/builtin_exec_platforms.bzl
index 6823a3a..7ebdd28 100644
--- a/src/main/starlark/builtins_bzl/common/builtin_exec_platforms.bzl
+++ b/src/main/starlark/builtins_bzl/common/builtin_exec_platforms.bzl
@@ -277,6 +277,7 @@
         "//command_line_option:target libcTop label",
         "//command_line_option:experimental_link_static_libraries_once",
         "//command_line_option:experimental_cc_implementation_deps",
+        "//command_line_option:experimental_cpp_modules",
         "//command_line_option:start_end_lib",
         "//command_line_option:experimental_inmemory_dotd_files",
         "//command_line_option:incompatible_disable_legacy_cc_provider",
diff --git a/src/main/starlark/builtins_bzl/common/cc/attrs.bzl b/src/main/starlark/builtins_bzl/common/cc/attrs.bzl
index c7e3717..f02ab98 100644
--- a/src/main/starlark/builtins_bzl/common/cc/attrs.bzl
+++ b/src/main/starlark/builtins_bzl/common/cc/attrs.bzl
@@ -84,6 +84,23 @@
 </p>
 """,
     ),
+    "module_interfaces": attr.label_list(
+        allow_files = True,
+        doc = """
+The list of files are regarded as C++20 Modules Interface.
+
+<p>
+C++ Standard has no restriction about module interface file extension
+<ul>
+<li>Clang use cppm </li>
+<li>GCC can use any source file extension </li>
+<li>MSVC use ixx </li>
+</ul>
+</p>
+<p>The use is guarded by the flag
+<code>--experimental_cpp_modules</code>.</p>
+        """,
+    ),
     "data": attr.label_list(
         allow_files = True,
         flags = ["SKIP_CONSTRAINTS_OVERRIDE"],
diff --git a/src/main/starlark/builtins_bzl/common/cc/cc_binary.bzl b/src/main/starlark/builtins_bzl/common/cc/cc_binary.bzl
index d2343bd..eaf01fb 100644
--- a/src/main/starlark/builtins_bzl/common/cc/cc_binary.bzl
+++ b/src/main/starlark/builtins_bzl/common/cc/cc_binary.bzl
@@ -487,6 +487,9 @@
         requested_features = features,
         unsupported_features = disabled_features,
     )
+
+    cc_helper.check_cpp_modules(ctx, feature_configuration)
+
     all_deps = ctx.attr.deps + semantics.get_cc_runtimes(ctx, _is_link_shared(ctx))
     compilation_context_deps = [dep[CcInfo].compilation_context for dep in all_deps if CcInfo in dep]
 
@@ -508,6 +511,7 @@
         public_hdrs = cc_helper.get_public_hdrs(ctx),
         copts_filter = cc_helper.copts_filter(ctx, additional_make_variable_substitutions),
         srcs = cc_helper.get_srcs(ctx),
+        module_interfaces = cc_helper.get_cpp_module_interfaces(ctx),
         compilation_contexts = compilation_context_deps,
         code_coverage_enabled = cc_helper.is_code_coverage_enabled(ctx = ctx),
     )
diff --git a/src/main/starlark/builtins_bzl/common/cc/cc_common.bzl b/src/main/starlark/builtins_bzl/common/cc/cc_common.bzl
index 3b67687..b37eb48 100644
--- a/src/main/starlark/builtins_bzl/common/cc/cc_common.bzl
+++ b/src/main/starlark/builtins_bzl/common/cc/cc_common.bzl
@@ -676,6 +676,7 @@
         purpose = _UNBOUND,
         copts_filter = _UNBOUND,
         separate_module_headers = _UNBOUND,
+        module_interfaces = _UNBOUND,
         non_compilation_additional_inputs = _UNBOUND):
     if module_map != _UNBOUND or \
        additional_module_maps != _UNBOUND or \
@@ -688,6 +689,7 @@
        implementation_compilation_contexts != _UNBOUND or \
        copts_filter != _UNBOUND or \
        separate_module_headers != _UNBOUND or \
+       module_interfaces != _UNBOUND or \
        non_compilation_additional_inputs != _UNBOUND:
         cc_common_internal.check_private_api(allowlist = _PRIVATE_STARLARKIFICATION_ALLOWLIST)
 
@@ -713,10 +715,12 @@
         copts_filter = None
     if separate_module_headers == _UNBOUND:
         separate_module_headers = []
+    if module_interfaces == _UNBOUND:
+        module_interfaces = []
     if non_compilation_additional_inputs == _UNBOUND:
         non_compilation_additional_inputs = []
 
-    has_tuple = _check_all_sources_contain_tuples_or_none_of_them([srcs, private_hdrs, public_hdrs])
+    has_tuple = _check_all_sources_contain_tuples_or_none_of_them([srcs, module_interfaces, private_hdrs, public_hdrs])
     if has_tuple:
         cc_common_internal.check_private_api(allowlist = _PRIVATE_STARLARKIFICATION_ALLOWLIST)
 
@@ -726,6 +730,7 @@
         cc_toolchain = cc_toolchain,
         name = name,
         srcs = srcs,
+        module_interfaces = module_interfaces,
         public_hdrs = public_hdrs,
         private_hdrs = private_hdrs,
         textual_hdrs = textual_hdrs,
diff --git a/src/main/starlark/builtins_bzl/common/cc/cc_helper.bzl b/src/main/starlark/builtins_bzl/common/cc/cc_helper.bzl
index 78a47e9..5d14be2 100644
--- a/src/main/starlark/builtins_bzl/common/cc/cc_helper.bzl
+++ b/src/main/starlark/builtins_bzl/common/cc/cc_helper.bzl
@@ -944,26 +944,37 @@
         result.append((k, v))
     return result
 
-# Returns a list of (Artifact, Label) tuples. Each tuple represents an input source
-# file and the label of the rule that generates it (or the label of the source file itself if it
-# is an input file).
+def _calculate_artifact_label_map(attr_list, attr_name):
+    """
+    Converts a label_list attribute into a list of (Artifact, Label) tuples.
+
+    Each tuple represents an input source file and the label of the rule that generates it
+    (or the label of the source file itself if it is an input file).
+    """
+    artifact_label_map = {}
+    for attr in attr_list:
+        if DefaultInfo in attr:
+            for artifact in attr[DefaultInfo].files.to_list():
+                if "." + artifact.extension not in CC_HEADER:
+                    old_label = artifact_label_map.get(artifact, None)
+                    artifact_label_map[artifact] = attr.label
+                    if old_label != None and not _are_labels_equal(old_label, attr.label) and ("." + artifact.extension in CC_AND_OBJC or attr_name == "module_interfaces"):
+                        fail(
+                            "Artifact '{}' is duplicated (through '{}' and '{}')".format(artifact, old_label, attr),
+                            attr = attr_name,
+                        )
+    return artifact_label_map
+
 def _get_srcs(ctx):
     if not hasattr(ctx.attr, "srcs"):
         return []
+    artifact_label_map = _calculate_artifact_label_map(ctx.attr.srcs, "srcs")
+    return _map_to_list(artifact_label_map)
 
-    # "srcs" attribute is a LABEL_LIST in cc_rules, which might also contain files.
-    artifact_label_map = {}
-    for src in ctx.attr.srcs:
-        if DefaultInfo in src:
-            for artifact in src[DefaultInfo].files.to_list():
-                if "." + artifact.extension not in CC_HEADER:
-                    old_label = artifact_label_map.get(artifact, None)
-                    artifact_label_map[artifact] = src.label
-                    if old_label != None and not _are_labels_equal(old_label, src.label) and "." + artifact.extension in CC_AND_OBJC:
-                        fail(
-                            "Artifact '{}' is duplicated (through '{}' and '{}')".format(artifact, old_label, src),
-                            attr = "srcs",
-                        )
+def _get_cpp_module_interfaces(ctx):
+    if not hasattr(ctx.attr, "module_interfaces"):
+        return []
+    artifact_label_map = _calculate_artifact_label_map(ctx.attr.module_interfaces, "module_interfaces")
     return _map_to_list(artifact_label_map)
 
 # Returns a list of (Artifact, Label) tuples. Each tuple represents an input source
@@ -1176,6 +1187,17 @@
         )
     )
 
+def _check_cpp_modules(ctx, feature_configuration):
+    if len(ctx.files.module_interfaces) == 0:
+        return
+    if not ctx.fragments.cpp.experimental_cpp_modules():
+        fail("requires --experimental_cpp_modules", attr = "module_interfaces")
+    if not cc_common.is_enabled(
+        feature_configuration = feature_configuration,
+        feature_name = "cpp_modules",
+    ):
+        fail("to use C++ modules, the feature cpp_modules must be enabled")
+
 cc_helper = struct(
     CPP_TOOLCHAIN_TYPE = _CPP_TOOLCHAIN_TYPE,
     merge_cc_debug_contexts = _merge_cc_debug_contexts,
@@ -1225,6 +1247,7 @@
     get_local_defines_for_runfiles_lookup = _get_local_defines_for_runfiles_lookup,
     are_labels_equal = _are_labels_equal,
     get_srcs = _get_srcs,
+    get_cpp_module_interfaces = _get_cpp_module_interfaces,
     get_private_hdrs = _get_private_hdrs,
     get_public_hdrs = _get_public_hdrs,
     report_invalid_options = _report_invalid_options,
@@ -1242,4 +1265,5 @@
     package_source_root = _package_source_root,
     tokenize = _tokenize,
     should_use_pic = _should_use_pic,
+    check_cpp_modules = _check_cpp_modules,
 )
diff --git a/src/main/starlark/builtins_bzl/common/cc/cc_library.bzl b/src/main/starlark/builtins_bzl/common/cc/cc_library.bzl
index 7295719..1d5b5be 100755
--- a/src/main/starlark/builtins_bzl/common/cc/cc_library.bzl
+++ b/src/main/starlark/builtins_bzl/common/cc/cc_library.bzl
@@ -37,6 +37,8 @@
         unsupported_features = ctx.disabled_features,
     )
 
+    cc_helper.check_cpp_modules(ctx, feature_configuration)
+
     precompiled_files = cc_helper.build_precompiled_files(ctx = ctx)
 
     semantics.validate_attributes(ctx = ctx)
@@ -63,6 +65,7 @@
         copts_filter = cc_helper.copts_filter(ctx, additional_make_variable_substitutions),
         purpose = "cc_library-compile",
         srcs = cc_helper.get_srcs(ctx),
+        module_interfaces = cc_helper.get_cpp_module_interfaces(ctx),
         private_hdrs = cc_helper.get_private_hdrs(ctx),
         public_hdrs = cc_helper.get_public_hdrs(ctx),
         code_coverage_enabled = cc_helper.is_code_coverage_enabled(ctx),
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/mock/cc_toolchain_config.bzl b/src/test/java/com/google/devtools/build/lib/analysis/mock/cc_toolchain_config.bzl
index da292f9..19b9be5 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/mock/cc_toolchain_config.bzl
+++ b/src/test/java/com/google/devtools/build/lib/analysis/mock/cc_toolchain_config.bzl
@@ -32,6 +32,7 @@
 )
 
 _FEATURE_NAMES = struct(
+    cpp_modules = "cpp_modules",
     generate_pdb_file = "generate_pdb_file",
     no_legacy_features = "no_legacy_features",
     do_not_split_linking_cmdline = "do_not_split_linking_cmdline",
@@ -123,6 +124,11 @@
     generate_linkmap = "generate_linkmap",
 )
 
+_cpp_modules_feature = feature(
+    name = _FEATURE_NAMES.cpp_modules,
+    enabled = False,
+)
+
 _no_copts_tokenization_feature = feature(name = _FEATURE_NAMES.no_copts_tokenization)
 
 _disable_pbh_feature = feature(name = _FEATURE_NAMES.disable_pbh)
@@ -1365,6 +1371,7 @@
 )
 
 _feature_name_to_feature = {
+    _FEATURE_NAMES.cpp_modules: _cpp_modules_feature,
     _FEATURE_NAMES.no_legacy_features: _no_legacy_features_feature,
     _FEATURE_NAMES.do_not_split_linking_cmdline: _do_not_split_linking_cmdline_feature,
     _FEATURE_NAMES.supports_dynamic_linker: _supports_dynamic_linker_feature,
diff --git a/src/test/java/com/google/devtools/build/lib/packages/util/mock/osx_cc_toolchain_config.bzl b/src/test/java/com/google/devtools/build/lib/packages/util/mock/osx_cc_toolchain_config.bzl
index abf6d81..94d8adf 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/util/mock/osx_cc_toolchain_config.bzl
+++ b/src/test/java/com/google/devtools/build/lib/packages/util/mock/osx_cc_toolchain_config.bzl
@@ -7195,7 +7195,13 @@
     else:
         include_system_dirs_feature = None
 
+    cpp_modules_feature = feature(
+        name = "cpp_modules",
+        enabled = False,
+    )
+
     features = [
+        cpp_modules_feature,
         default_compile_flags_feature,
         default_link_flags_feature,
         no_legacy_features_feature,
diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/BUILD b/src/test/java/com/google/devtools/build/lib/rules/cpp/BUILD
index 2245f20..f8f3f1e 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/cpp/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/BUILD
@@ -745,3 +745,16 @@
         "//third_party:truth",
     ],
 )
+
+java_test(
+    name = "CppModulesConfiguredTargetTest",
+    srcs = ["CppModulesConfiguredTargetTest.java"],
+    deps = [
+        "//src/main/java/com/google/devtools/build/lib/rules/cpp",
+        "//src/test/java/com/google/devtools/build/lib/analysis/util",
+        "//src/test/java/com/google/devtools/build/lib/packages:testutil",
+        "//third_party:guava",
+        "//third_party:junit4",
+        "//third_party:truth",
+    ],
+)
diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/CppModulesConfiguredTargetTest.java b/src/test/java/com/google/devtools/build/lib/rules/cpp/CppModulesConfiguredTargetTest.java
new file mode 100644
index 0000000..00b26dd
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/CppModulesConfiguredTargetTest.java
@@ -0,0 +1,271 @@
+// Copyright 2024 The Bazel Authors. All rights reserved.
+//
+// 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.
+package com.google.devtools.build.lib.rules.cpp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.analysis.util.AnalysisMock;
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import com.google.devtools.build.lib.packages.util.Crosstool;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class CppModulesConfiguredTargetTest extends BuildViewTestCase {
+  void useFeatures(String... args) throws Exception {
+    AnalysisMock.get()
+        .ccSupport()
+        .setupCcToolchainConfig(
+            mockToolsConfig, Crosstool.CcToolchainConfig.builder().withFeatures(args));
+  }
+
+  @Test
+  public void testCppModulesCcBinaryConfigurationNoFlags() throws Exception {
+    scratch.file(
+        "foo/BUILD",
+        """
+        cc_library(
+            name = 'lib',
+            module_interfaces = ["foo.cppm"],
+        )
+        """);
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//foo:lib");
+    assertContainsEvent("requires --experimental_cpp_modules");
+  }
+
+  @Test
+  public void testCppModulesCcLibraryConfigurationNoFlags() throws Exception {
+    scratch.file(
+        "foo/BUILD",
+        """
+        cc_binary(
+            name = 'bin',
+            module_interfaces = ["foo.cppm"],
+        )
+        """);
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//foo:bin");
+    assertContainsEvent("requires --experimental_cpp_modules");
+  }
+
+  @Test
+  public void testCppModulesCcTestConfigurationNoFlags() throws Exception {
+    scratch.file(
+        "foo/BUILD",
+        """
+        cc_test(
+            name = 'test',
+            module_interfaces = ["foo.cppm"],
+        )
+        """);
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//foo:test");
+    assertContainsEvent("requires --experimental_cpp_modules");
+  }
+
+  @Test
+  public void testCppModulesCcLibraryConfigurationNoFeatures() throws Exception {
+    scratch.file(
+        "foo/BUILD",
+        """
+        cc_library(
+            name = 'lib',
+            module_interfaces = ["foo.cppm"],
+        )
+        """);
+    useConfiguration("--experimental_cpp_modules");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//foo:lib");
+    assertDoesNotContainEvent("requires --experimental_cpp_modules");
+    assertContainsEvent("the feature cpp_modules must be enabled");
+  }
+
+  @Test
+  public void testCppModulesCcBinaryConfigurationNoFeatures() throws Exception {
+    scratch.file(
+        "foo/BUILD",
+        """
+        cc_binary(
+            name = 'bin',
+            module_interfaces = ["foo.cppm"],
+        )
+        """);
+    useConfiguration("--experimental_cpp_modules");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//foo:bin");
+    assertDoesNotContainEvent("requires --experimental_cpp_modules");
+    assertContainsEvent("the feature cpp_modules must be enabled");
+  }
+
+  @Test
+  public void testCppModulesCcTestConfigurationNoFeatures() throws Exception {
+    scratch.file(
+        "foo/BUILD",
+        """
+        cc_test(
+            name = 'test',
+            module_interfaces = ["foo.cppm"],
+        )
+        """);
+    useConfiguration("--experimental_cpp_modules");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//foo:test");
+    assertDoesNotContainEvent("requires --experimental_cpp_modules");
+    assertContainsEvent("the feature cpp_modules must be enabled");
+  }
+
+  @Test
+  public void testCppModulesCcLibraryConfigurationWithFeatures() throws Exception {
+    scratch.file(
+        "foo/BUILD",
+        """
+        cc_library(
+            name = 'lib',
+            module_interfaces = ["foo.cppm"],
+        )
+        """);
+    useFeatures(CppRuleClasses.CPP_MODULES);
+    useConfiguration("--experimental_cpp_modules", "--features=cpp_modules");
+
+    ImmutableSet<String> features = getRuleContext(getConfiguredTarget("//foo:lib")).getFeatures();
+    assertThat(features).contains("cpp_modules");
+    assertDoesNotContainEvent("requires --experimental_cpp_modules");
+    assertDoesNotContainEvent("the feature cpp_modules must be enabled");
+  }
+
+  @Test
+  public void testCppModulesCcBinaryConfigurationWithFeatures() throws Exception {
+    scratch.file(
+        "foo/BUILD",
+        """
+        cc_binary(
+            name = 'bin',
+            module_interfaces = ["foo.cppm"],
+        )
+        """);
+    useFeatures(CppRuleClasses.CPP_MODULES);
+    useConfiguration("--experimental_cpp_modules", "--features=cpp_modules");
+
+    ImmutableSet<String> features = getRuleContext(getConfiguredTarget("//foo:bin")).getFeatures();
+    assertThat(features).contains("cpp_modules");
+    assertDoesNotContainEvent("requires --experimental_cpp_modules");
+    assertDoesNotContainEvent("the feature cpp_modules must be enabled");
+  }
+
+  @Test
+  public void testCppModulesCcTestConfigurationWithFeatures() throws Exception {
+    scratch.file(
+        "foo/BUILD",
+        """
+        cc_test(
+            name = 'test',
+            module_interfaces = ["foo.cppm"],
+        )
+        """);
+    useFeatures(CppRuleClasses.CPP_MODULES);
+    useConfiguration("--experimental_cpp_modules", "--features=cpp_modules");
+
+    ImmutableSet<String> features = getRuleContext(getConfiguredTarget("//foo:test")).getFeatures();
+    assertThat(features).contains("cpp_modules");
+    assertDoesNotContainEvent("requires --experimental_cpp_modules");
+    assertDoesNotContainEvent("the feature cpp_modules must be enabled");
+  }
+
+  @Test
+  public void testSameModuleInterfacesFileInCcLibraryTwice() throws Exception {
+    scratch.file(
+        "a/BUILD",
+        """
+        filegroup(
+          name = "a1",
+          srcs = ["a.cppm"],
+        )
+        filegroup(
+          name = "a2",
+          srcs = ["a.cppm"],
+        )
+        cc_library(
+          name = "lib",
+          module_interfaces = ["a1", "a2"],
+        )
+        """);
+
+    useFeatures(CppRuleClasses.CPP_MODULES);
+    useConfiguration("--experimental_cpp_modules", "--features=cpp_modules");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//a:lib");
+    assertContainsEvent("Artifact '<source file a/a.cppm>' is duplicated");
+  }
+
+  @Test
+  public void testSameModuleInterfacesFileInCcBinaryTwice() throws Exception {
+    scratch.file(
+        "a/BUILD",
+        """
+        filegroup(
+          name = "a1",
+          srcs = ["a.cppm"],
+        )
+        filegroup(
+          name = "a2",
+          srcs = ["a.cppm"],
+        )
+        cc_binary(
+          name = "bin",
+          module_interfaces = ["a1", "a2"],
+        )
+        """);
+
+    useFeatures(CppRuleClasses.CPP_MODULES);
+    useConfiguration("--experimental_cpp_modules", "--features=cpp_modules");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//a:bin");
+    assertContainsEvent("Artifact '<source file a/a.cppm>' is duplicated");
+  }
+
+  @Test
+  public void testSameModuleInterfacesFileInCcTestTwice() throws Exception {
+    scratch.file(
+        "a/BUILD",
+        """
+        filegroup(
+          name = "a1",
+          srcs = ["a.cppm"],
+        )
+        filegroup(
+          name = "a2",
+          srcs = ["a.cppm"],
+        )
+        cc_test(
+          name = "test",
+          module_interfaces = ["a1", "a2"],
+        )
+        """);
+
+    useFeatures(CppRuleClasses.CPP_MODULES);
+    useConfiguration("--experimental_cpp_modules", "--features=cpp_modules");
+
+    reporter.removeHandler(failFastHandler);
+    getConfiguredTarget("//a:test");
+    assertContainsEvent("Artifact '<source file a/a.cppm>' is duplicated");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/rules/cpp/StarlarkCcCommonTest.java b/src/test/java/com/google/devtools/build/lib/rules/cpp/StarlarkCcCommonTest.java
index fd83cf6..ebbcd01 100755
--- a/src/test/java/com/google/devtools/build/lib/rules/cpp/StarlarkCcCommonTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/cpp/StarlarkCcCommonTest.java
@@ -7383,6 +7383,7 @@
             "build_test_dwp()",
             "grte_top()",
             "experimental_cc_implementation_deps()",
+            "experimental_cpp_modules()",
             "share_native_deps()",
             "experimental_platform_cc_test()");
     scratch.file(
diff --git a/tools/cpp/unix_cc_toolchain_config.bzl b/tools/cpp/unix_cc_toolchain_config.bzl
index 61f6561..cec23f9 100644
--- a/tools/cpp/unix_cc_toolchain_config.bzl
+++ b/tools/cpp/unix_cc_toolchain_config.bzl
@@ -1429,11 +1429,22 @@
         ],
     )
 
+    # Tell bazel we support C++ modules now
+    cpp_modules_feature = feature(
+        name = "cpp_modules",
+        # set default value to False
+        # to enable the feature
+        # use --features=cpp_modules
+        # or add cpp_modules to features attr
+        enabled = False,
+    )
+
     # TODO(#8303): Mac crosstool should also declare every feature.
     if is_linux:
         # Linux artifact name patterns are the default.
         artifact_name_patterns = []
         features = [
+            cpp_modules_feature,
             dependency_file_feature,
             serialized_diagnostics_file_feature,
             random_seed_feature,
@@ -1501,6 +1512,7 @@
             ),
         ]
         features = [
+            cpp_modules_feature,
             macos_minimum_os_feature,
             macos_default_link_flags_feature,
             libtool_feature,