Support optional toolchains with `find_cpp_toolchain`

Rules that want to use optional toolchains but still support builds that don't use C++ toolchain resolution should point their `_cc_toolchain` attribute to the new
`@bazel_tools//tools/cpp:optional_current_cc_toolchain` target.

Fixes #16966

Closes #16968.

PiperOrigin-RevId: 505707967
Change-Id: I251749348f982ae063b51f7d9cc0078a1b61a948
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchainAliasRule.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchainAliasRule.java
index 8ed40b6..5fb5f9b 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchainAliasRule.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcToolchainAliasRule.java
@@ -33,6 +33,7 @@
 import com.google.devtools.build.lib.analysis.TemplateVariableInfo;
 import com.google.devtools.build.lib.analysis.platform.ToolchainInfo;
 import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.Type;
 import javax.annotation.Nullable;
 
 /** Implementation of the {@code cc_toolchain_alias} rule. */
@@ -50,6 +51,7 @@
         .add(
             attr(CcToolchain.CC_TOOLCHAIN_TYPE_ATTRIBUTE_NAME, NODEP_LABEL)
                 .value(CppRuleClasses.ccToolchainTypeAttribute(env)))
+        .add(attr("mandatory", Type.BOOLEAN).value(true))
         .requiresConfigurationFragments(PlatformConfiguration.class)
         .addToolchainTypes(CppRuleClasses.ccToolchainTypeRequirement(env))
         .build();
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 31eb6f4..7c2e208 100644
--- a/src/main/starlark/builtins_bzl/common/cc/cc_helper.bzl
+++ b/src/main/starlark/builtins_bzl/common/cc/cc_helper.bzl
@@ -201,18 +201,22 @@
 
 _CPP_TOOLCHAIN_TYPE = "@" + objc_semantics.get_repo() + "//tools/cpp:toolchain_type"
 
-def _find_cpp_toolchain(ctx):
+def _find_cpp_toolchain(ctx, *, mandatory = True):
     """
     Finds the c++ toolchain.
 
     If the c++ toolchain is in use, returns it.  Otherwise, returns a c++
-    toolchain derived from legacy toolchain selection.
+    toolchain derived from legacy toolchain selection, constructed from
+    the CppConfiguration.
 
     Args:
       ctx: The rule context for which to find a toolchain.
+      mandatory: If this is set to False, this function will return None rather
+        than fail if no toolchain is found.
 
     Returns:
-      A CcToolchainProvider.
+      A CcToolchainProvider, or None if the c++ toolchain is declared as
+      optional, mandatory is False and no toolchain has been found.
     """
 
     # Check the incompatible flag for toolchain resolution.
@@ -221,6 +225,9 @@
             fail("In order to use find_cpp_toolchain, you must include the '//tools/cpp:toolchain_type' in the toolchains argument to your rule.")
         toolchain_info = ctx.toolchains[_CPP_TOOLCHAIN_TYPE]
         if toolchain_info == None:
+            if not mandatory:
+                return None
+
             # No cpp toolchain was found, so report an error.
             fail("Unable to find a CC toolchain using toolchain resolution. Target: %s, Platform: %s, Exec platform: %s" %
                  (ctx.label, ctx.fragments.platform.platform, ctx.fragments.platform.host_platform))
diff --git a/src/main/starlark/builtins_bzl/common/cc/cc_toolchain_alias.bzl b/src/main/starlark/builtins_bzl/common/cc/cc_toolchain_alias.bzl
index a46de25..34c7c94 100644
--- a/src/main/starlark/builtins_bzl/common/cc/cc_toolchain_alias.bzl
+++ b/src/main/starlark/builtins_bzl/common/cc/cc_toolchain_alias.bzl
@@ -22,7 +22,9 @@
 ToolchainInfo = _builtins.toplevel.platform_common.ToolchainInfo
 
 def _impl(ctx):
-    cc_toolchain = cc_helper.find_cpp_toolchain(ctx)
+    cc_toolchain = cc_helper.find_cpp_toolchain(ctx, mandatory = ctx.attr.mandatory)
+    if not cc_toolchain:
+        return []
     make_variables = cc_toolchain.get_additional_make_variables()
     cc_provider_make_variables = cc_helper.get_toolchain_global_make_variables(cc_toolchain)
     template_variable_info = TemplateVariableInfo(make_variables | cc_provider_make_variables)
@@ -43,6 +45,7 @@
     implementation = _impl,
     fragments = ["cpp", "platform"],
     attrs = {
+        "mandatory": attr.bool(default = True),
         "_cc_toolchain": attr.label(default = configuration_field(fragment = "cpp", name = "cc_toolchain"), providers = [CcToolchainInfo]),
         "_cc_toolchain_type": attr.label(default = "@" + semantics.get_repo() + "//tools/cpp:toolchain_type"),
     },
diff --git a/src/test/shell/bazel/cc_integration_test.sh b/src/test/shell/bazel/cc_integration_test.sh
index db19f50..9bcb7be 100755
--- a/src/test/shell/bazel/cc_integration_test.sh
+++ b/src/test/shell/bazel/cc_integration_test.sh
@@ -1910,4 +1910,71 @@
   expect_log "runtime error: index 10 out of bounds"
 }
 
+function setup_find_optional_cpp_toolchain() {
+  mkdir -p pkg
+
+  cat > pkg/BUILD <<'EOF'
+load(":rules.bzl", "my_rule")
+
+my_rule(
+    name = "my_rule",
+)
+
+platform(
+    name = "exotic_platform",
+    constraint_values = [
+        "@platforms//cpu:wasm64",
+        "@platforms//os:windows",
+    ],
+)
+EOF
+
+  cat > pkg/rules.bzl <<'EOF'
+load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain", "use_cpp_toolchain")
+
+def _my_rule_impl(ctx):
+    out = ctx.actions.declare_file(ctx.attr.name)
+    toolchain = find_cpp_toolchain(ctx, mandatory = False)
+    if toolchain:
+        ctx.actions.write(out, "Toolchain found")
+    else:
+        ctx.actions.write(out, "Toolchain not found")
+    return [DefaultInfo(files = depset([out]))]
+
+my_rule = rule(
+    implementation = _my_rule_impl,
+    attrs = {
+        "_cc_toolchain": attr.label(
+            default = "@bazel_tools//tools/cpp:optional_current_cc_toolchain",
+        ),
+    },
+    toolchains = use_cpp_toolchain(mandatory = False),
+)
+EOF
+}
+
+function test_find_optional_cpp_toolchain_present_without_toolchain_resolution() {
+  setup_find_optional_cpp_toolchain
+
+  bazel build //pkg:my_rule --noincompatible_enable_cc_toolchain_resolution \
+    &> "$TEST_log" || fail "Build failed"
+  assert_contains "Toolchain found" bazel-bin/pkg/my_rule
+}
+
+function test_find_optional_cpp_toolchain_present_with_toolchain_resolution() {
+  setup_find_optional_cpp_toolchain
+
+  bazel build //pkg:my_rule --incompatible_enable_cc_toolchain_resolution \
+    &> "$TEST_log" || fail "Build failed"
+  assert_contains "Toolchain found" bazel-bin/pkg/my_rule
+}
+
+function test_find_optional_cpp_toolchain_not_present_with_toolchain_resolution() {
+  setup_find_optional_cpp_toolchain
+
+  bazel build //pkg:my_rule --incompatible_enable_cc_toolchain_resolution \
+    --platforms=//pkg:exotic_platform &> "$TEST_log" || fail "Build failed"
+  assert_contains "Toolchain not found" bazel-bin/pkg/my_rule
+}
+
 run_suite "cc_integration_test"
diff --git a/tools/cpp/BUILD.tools b/tools/cpp/BUILD.tools
index 1e292c3..41b5d66 100644
--- a/tools/cpp/BUILD.tools
+++ b/tools/cpp/BUILD.tools
@@ -57,12 +57,9 @@
 
 cc_toolchain_alias(name = "current_cc_toolchain")
 
-# In future versions of Bazel, this target will not fail if no C++ toolchain is
-# available. Instead, it will not advertise the cc_common.CcToolchainInfo
-# provider.
-alias(
+cc_toolchain_alias(
     name = "optional_current_cc_toolchain",
-    actual = ":current_cc_toolchain",
+    mandatory = False,
 )
 
 cc_host_toolchain_alias(name = "current_cc_host_toolchain")
diff --git a/tools/cpp/toolchain_utils.bzl b/tools/cpp/toolchain_utils.bzl
index 1583aed..9bf5bc4 100644
--- a/tools/cpp/toolchain_utils.bzl
+++ b/tools/cpp/toolchain_utils.bzl
@@ -29,12 +29,14 @@
 
     Args:
       ctx: The rule context for which to find a toolchain.
-      mandatory: This is currently a no-op. In future releases of Bazel, if this
-        is set to False, this function will return None rather than fail if no
-        toolchain is found.
+      mandatory: If this is set to False, this function will return None rather
+        than fail if no toolchain is found. To use this parameter, the calling
+        rule should have a `_cc_toolchain` label attribute with default
+        `@bazel_tools//tools/cpp:optional_current_cc_toolchain`.
 
     Returns:
-      A CcToolchainProvider.
+      A CcToolchainProvider, or None if the c++ toolchain is declared as
+      optional, mandatory is False and no toolchain has been found.
     """
 
     # Check the incompatible flag for toolchain resolution.
@@ -43,6 +45,9 @@
             fail("In order to use find_cpp_toolchain, you must include the '%s' in the toolchains argument to your rule." % CPP_TOOLCHAIN_TYPE)
         toolchain_info = ctx.toolchains[CPP_TOOLCHAIN_TYPE]
         if toolchain_info == None:
+            if not mandatory:
+                return None
+
             # No cpp toolchain was found, so report an error.
             fail("Unable to find a CC toolchain using toolchain resolution. Target: %s, Platform: %s, Exec platform: %s" %
                  (ctx.label, ctx.fragments.platform.platform, ctx.fragments.platform.host_platform))