Create a concept of a tool capability.

This should make the concept of "sentinel features" work correctly with known_features and enabled_features, rather than enabling them directly in features.

PiperOrigin-RevId: 681688437
Change-Id: I29184a2079ccfd0eb3a275439508a66ca61109af
diff --git a/cc/toolchains/capabilities/BUILD b/cc/toolchains/capabilities/BUILD
new file mode 100644
index 0000000..8c55804
--- /dev/null
+++ b/cc/toolchains/capabilities/BUILD
@@ -0,0 +1,19 @@
+load("//cc/toolchains:tool_capability.bzl", "cc_tool_capability")
+
+package(default_visibility = ["//visibility:public"])
+
+cc_tool_capability(
+    name = "supports_start_end_lib",
+)
+
+cc_tool_capability(
+    name = "supports_interface_shared_libraries",
+)
+
+cc_tool_capability(
+    name = "supports_dynamic_linker",
+)
+
+cc_tool_capability(
+    name = "supports_pic",
+)
diff --git a/cc/toolchains/cc_toolchain_info.bzl b/cc/toolchains/cc_toolchain_info.bzl
index 2325ddb..17881a8 100644
--- a/cc/toolchains/cc_toolchain_info.bzl
+++ b/cc/toolchains/cc_toolchain_info.bzl
@@ -156,6 +156,16 @@
         "runfiles": "(runfiles) The files required to run the tool",
         "execution_requirements": "(Sequence[str]) A set of execution requirements of the tool",
         "allowlist_include_directories": "(depset[DirectoryInfo]) Built-in include directories implied by this tool that should be allowlisted in Bazel's include checker",
+        "capabilities": "(Sequence[ToolCapabilityInfo]) Capabilities supported by the tool.",
+    },
+)
+
+ToolCapabilityInfo = provider(
+    doc = "A capability associated with a tool (eg. supports_pic).",
+    # @unsorted-dict-items
+    fields = {
+        "label": "(Label) The label defining this provider. Place in error messages to simplify debugging",
+        "feature": "(FeatureInfo) The feature this capability defines",
     },
 )
 
diff --git a/cc/toolchains/features/BUILD b/cc/toolchains/features/BUILD
index 6c6088b..22c3519 100644
--- a/cc/toolchains/features/BUILD
+++ b/cc/toolchains/features/BUILD
@@ -42,35 +42,11 @@
 )
 
 cc_external_feature(
-    name = "supports_start_end_lib",
-    feature_name = "supports_start_end_lib",
-    overridable = True,
-)
-
-cc_external_feature(
-    name = "supports_interface_shared_libraries",
-    feature_name = "supports_interface_shared_libraries",
-    overridable = True,
-)
-
-cc_external_feature(
-    name = "supports_dynamic_linker",
-    feature_name = "supports_dynamic_linker",
-    overridable = True,
-)
-
-cc_external_feature(
     name = "static_link_cpp_runtimes",
     feature_name = "static_link_cpp_runtimes",
     overridable = True,
 )
 
-cc_external_feature(
-    name = "supports_pic",
-    feature_name = "supports_pic",
-    overridable = True,
-)
-
 cc_feature_set(
     name = "all_non_legacy_builtin_features",
     all_of = [
@@ -80,11 +56,7 @@
         ":static_linking_mode",
         ":dynamic_linking_mode",
         ":per_object_debug_info",
-        ":supports_start_end_lib",
-        ":supports_interface_shared_libraries",
-        ":supports_dynamic_linker",
         ":static_link_cpp_runtimes",
-        ":supports_pic",
     ],
     visibility = ["//visibility:private"],
 )
diff --git a/cc/toolchains/impl/collect.bzl b/cc/toolchains/impl/collect.bzl
index f41aa7d..e242d91 100644
--- a/cc/toolchains/impl/collect.bzl
+++ b/cc/toolchains/impl/collect.bzl
@@ -107,6 +107,7 @@
                 runfiles = collect_data(ctx, [target]),
                 execution_requirements = tuple(),
                 allowlist_include_directories = depset(),
+                capabilities = tuple(),
             ))
         else:
             fail("Expected %s to be a cc_tool or a binary rule" % target.label)
diff --git a/cc/toolchains/impl/documented_api.bzl b/cc/toolchains/impl/documented_api.bzl
index a863290..f1f634e 100644
--- a/cc/toolchains/impl/documented_api.bzl
+++ b/cc/toolchains/impl/documented_api.bzl
@@ -22,12 +22,14 @@
 load("//cc/toolchains:mutually_exclusive_category.bzl", _cc_mutually_exclusive_category = "cc_mutually_exclusive_category")
 load("//cc/toolchains:nested_args.bzl", _cc_nested_args = "cc_nested_args")
 load("//cc/toolchains:tool.bzl", _cc_tool = "cc_tool")
+load("//cc/toolchains:tool_capability.bzl", _cc_tool_capability = "cc_tool_capability")
 load("//cc/toolchains:tool_map.bzl", _cc_tool_map = "cc_tool_map")
 load("//cc/toolchains/impl:external_feature.bzl", _cc_external_feature = "cc_external_feature")
 load("//cc/toolchains/impl:variables.bzl", _cc_variable = "cc_variable")
 
 cc_tool_map = _cc_tool_map
 cc_tool = _cc_tool
+cc_tool_capability = _cc_tool_capability
 cc_args = _cc_args
 cc_nested_args = _cc_nested_args
 cc_args_list = _cc_args_list
@@ -46,6 +48,7 @@
 DOCUMENTED_TOOLCHAIN_RULES = [
     "cc_tool_map",
     "cc_tool",
+    "cc_tool_capability",
     "cc_args",
     "cc_nested_args",
     "cc_args_list",
diff --git a/cc/toolchains/impl/legacy_converter.bzl b/cc/toolchains/impl/legacy_converter.bzl
index 7197716..64fea95 100644
--- a/cc/toolchains/impl/legacy_converter.bzl
+++ b/cc/toolchains/impl/legacy_converter.bzl
@@ -131,16 +131,30 @@
         with_features = [],
     )
 
+def convert_capability(capability):
+    return legacy_feature(
+        name = capability.name,
+        enabled = False,
+    )
+
 def _convert_tool_map(tool_map):
-    return [
-        legacy_action_config(
+    action_configs = []
+    caps = {}
+    for action_type, tool in tool_map.configs.items():
+        action_configs.append(legacy_action_config(
             action_name = action_type.name,
             enabled = True,
-            tools = [convert_tool(tool_map.configs[action_type])],
-            implies = [],
-        )
-        for action_type in tool_map.configs.keys()
+            tools = [convert_tool(tool)],
+            implies = [cap.feature.name for cap in tool.capabilities],
+        ))
+        for cap in tool.capabilities:
+            caps[cap] = None
+
+    cap_features = [
+        legacy_feature(name = cap.feature.name, enabled = False)
+        for cap in caps
     ]
+    return action_configs, cap_features
 
 def convert_toolchain(toolchain):
     """Converts a rule-based toolchain into the legacy providers.
@@ -155,6 +169,8 @@
         convert_feature(feature, enabled = feature in toolchain.enabled_features)
         for feature in toolchain.features
     ]
+    action_configs, cap_features = _convert_tool_map(toolchain.tool_map)
+    features.extend(cap_features)
     features.append(convert_feature(FeatureInfo(
         # We reserve names starting with implied_by. This ensures we don't
         # conflict with the name of a feature the user creates.
@@ -167,7 +183,6 @@
         external = False,
         allowlist_include_directories = depset(),
     )))
-    action_configs = _convert_tool_map(toolchain.tool_map)
 
     cxx_builtin_include_directories = [
         d.path
diff --git a/cc/toolchains/impl/toolchain_config_info.bzl b/cc/toolchains/impl/toolchain_config_info.bzl
index 7e68b15..0fa499d 100644
--- a/cc/toolchains/impl/toolchain_config_info.bzl
+++ b/cc/toolchains/impl/toolchain_config_info.bzl
@@ -54,9 +54,9 @@
     # This should be sufficiently unique.
     return (feature.label, feature.name)
 
-def _get_known_features(features, fail):
+def _get_known_features(features, capability_features, fail):
     feature_names = {}
-    for ft in features:
+    for ft in capability_features + features:
         if ft.name in feature_names:
             other = feature_names[ft.name]
             if other.overrides != ft and ft.overrides != other:
@@ -113,7 +113,10 @@
     _validate_implies(self, known_features, fail = fail)
 
 def _validate_toolchain(self, fail = fail):
-    known_features = _get_known_features(self.features, fail = fail)
+    capabilities = []
+    for tool in self.tool_map.configs.values():
+        capabilities.extend([cap.feature for cap in tool.capabilities])
+    known_features = _get_known_features(self.features, capabilities, fail = fail)
 
     for feature in self.features:
         _validate_feature(feature, known_features, fail = fail)
diff --git a/cc/toolchains/tool.bzl b/cc/toolchains/tool.bzl
index 0dc309d..a3536cf 100644
--- a/cc/toolchains/tool.bzl
+++ b/cc/toolchains/tool.bzl
@@ -14,9 +14,10 @@
 """Implementation of cc_tool"""
 
 load("@bazel_skylib//rules/directory:providers.bzl", "DirectoryInfo")
-load("//cc/toolchains/impl:collect.bzl", "collect_data")
+load("//cc/toolchains/impl:collect.bzl", "collect_data", "collect_provider")
 load(
     ":cc_toolchain_info.bzl",
+    "ToolCapabilityInfo",
     "ToolInfo",
 )
 
@@ -38,6 +39,7 @@
         allowlist_include_directories = depset(
             direct = [d[DirectoryInfo] for d in ctx.attr.allowlist_include_directories],
         ),
+        capabilities = tuple(collect_provider(ctx.attr.capabilities, ToolCapabilityInfo)),
     )
 
     link = ctx.actions.declare_file(ctx.label.name)
@@ -96,6 +98,13 @@
 (if these are builtin files, make sure these paths are in your toolchain)`.
 """,
         ),
+        "capabilities": attr.label_list(
+            providers = [ToolCapabilityInfo],
+            doc = """Declares that a tool is capable of doing something.
+
+For example, `//cc/toolchains/capabilities:supports_pic`.
+""",
+        ),
     },
     provides = [ToolInfo],
     doc = """Declares a tool for use by toolchain actions.
@@ -120,6 +129,7 @@
     # Suppose clang needs libc to run.
     data = ["@llvm_toolchain//:lib/x86_64-linux-gnu/libc.so.6"]
     tags = ["requires-network"],
+    capabilities = ["//cc/toolchains/capabilities:supports_pic"],
 )
 ```
 """,
diff --git a/cc/toolchains/tool_capability.bzl b/cc/toolchains/tool_capability.bzl
new file mode 100644
index 0000000..60b0f59
--- /dev/null
+++ b/cc/toolchains/tool_capability.bzl
@@ -0,0 +1,85 @@
+# 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.
+"""Implementation of the cc_tool_capability rule."""
+
+load(
+    ":cc_toolchain_info.bzl",
+    "ArgsListInfo",
+    "FeatureConstraintInfo",
+    "FeatureInfo",
+    "ToolCapabilityInfo",
+)
+
+def _cc_tool_capability_impl(ctx):
+    ft = FeatureInfo(
+        name = ctx.attr.feature_name or ctx.label.name,
+        label = ctx.label,
+        enabled = False,
+        args = ArgsListInfo(
+            label = ctx.label,
+            args = (),
+            files = depset(),
+            by_action = (),
+            allowlist_include_directories = depset(),
+        ),
+        implies = depset(),
+        requires_any_of = (),
+        mutually_exclusive = (),
+        # Mark it as external so that it doesn't complain if we say
+        # "requires" on a constraint that was never referenced elsewhere
+        # in the toolchain.
+        external = True,
+        overridable = True,
+        overrides = None,
+        allowlist_include_directories = depset(),
+    )
+    return [
+        ToolCapabilityInfo(label = ctx.label, feature = ft),
+        # Only give it a feature constraint info and not a feature info.
+        # This way you can't imply it - you can only require it.
+        FeatureConstraintInfo(label = ctx.label, all_of = depset([ft])),
+    ]
+
+cc_tool_capability = rule(
+    implementation = _cc_tool_capability_impl,
+    provides = [ToolCapabilityInfo, FeatureConstraintInfo],
+    doc = """A capability is an optional feature that a tool supports.
+
+For example, not all compilers support PIC, so to handle this, we write:
+
+```
+cc_tool(
+    name = "clang",
+    src = "@host_tools/bin/clang",
+    capabilities = [
+        "//cc/toolchains/capabilities:supports_pic",
+    ],
+)
+
+cc_args(
+    name = "pic",
+    requires = [
+        "//cc/toolchains/capabilities:supports_pic"
+    ],
+    args = ["-fPIC"],
+)
+```
+
+This ensures that `-fPIC` is added to the command-line only when we are using a
+tool that supports PIC.
+""",
+    attrs = {
+        "feature_name": attr.string(doc = "The name of the feature to generate for this capability"),
+    },
+)
diff --git a/cc/toolchains/toolchain_api.md b/cc/toolchains/toolchain_api.md
index 418ef52..d5b3be1 100644
--- a/cc/toolchains/toolchain_api.md
+++ b/cc/toolchains/toolchain_api.md
@@ -380,7 +380,7 @@
 ## cc_tool
 
 <pre>
-cc_tool(<a href="#cc_tool-name">name</a>, <a href="#cc_tool-src">src</a>, <a href="#cc_tool-data">data</a>, <a href="#cc_tool-allowlist_include_directories">allowlist_include_directories</a>)
+cc_tool(<a href="#cc_tool-name">name</a>, <a href="#cc_tool-src">src</a>, <a href="#cc_tool-data">data</a>, <a href="#cc_tool-allowlist_include_directories">allowlist_include_directories</a>, <a href="#cc_tool-capabilities">capabilities</a>)
 </pre>
 
 Declares a tool for use by toolchain actions.
@@ -405,6 +405,7 @@
     # Suppose clang needs libc to run.
     data = ["@llvm_toolchain//:lib/x86_64-linux-gnu/libc.so.6"]
     tags = ["requires-network"],
+    capabilities = ["@rules_cc//cc/toolchains/capabilities:supports_pic"],
 )
 ```
 
@@ -417,6 +418,49 @@
 | <a id="cc_tool-src"></a>src |  The underlying binary that this tool represents.<br><br>Usually just a single prebuilt (eg. @toolchain//:bin/clang), but may be any executable label.   | <a href="https://bazel.build/concepts/labels">Label</a> | optional |  `None`  |
 | <a id="cc_tool-data"></a>data |  Additional files that are required for this tool to run.<br><br>Frequently, clang and gcc require additional files to execute as they often shell out to other binaries (e.g. `cc1`).   | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional |  `[]`  |
 | <a id="cc_tool-allowlist_include_directories"></a>allowlist_include_directories |  Include paths implied by using this tool.<br><br>Compilers may include a set of built-in headers that are implicitly available unless flags like `-nostdinc` are provided. Bazel checks that all included headers are properly provided by a dependency or allowlisted through this mechanism.<br><br>As a rule of thumb, only use this if Bazel is complaining about absolute paths in your toolchain and you've ensured that the toolchain is compiling with the `-no-canonical-prefixes` and/or `-fno-canonical-system-headers` arguments.<br><br>This can help work around errors like: `the source file 'main.c' includes the following non-builtin files with absolute paths (if these are builtin files, make sure these paths are in your toolchain)`.   | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional |  `[]`  |
+| <a id="cc_tool-capabilities"></a>capabilities |  Declares that a tool is capable of doing something.<br><br>For example, `@rules_cc//cc/toolchains/capabilities:supports_pic`.   | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional |  `[]`  |
+
+
+<a id="cc_tool_capability"></a>
+
+## cc_tool_capability
+
+<pre>
+cc_tool_capability(<a href="#cc_tool_capability-name">name</a>, <a href="#cc_tool_capability-feature_name">feature_name</a>)
+</pre>
+
+A capability is an optional feature that a tool supports.
+
+For example, not all compilers support PIC, so to handle this, we write:
+
+```
+cc_tool(
+    name = "clang",
+    src = "@host_tools/bin/clang",
+    capabilities = [
+        "@rules_cc//cc/toolchains/capabilities:supports_pic",
+    ],
+)
+
+cc_args(
+    name = "pic",
+    requires = [
+        "@rules_cc//cc/toolchains/capabilities:supports_pic"
+    ],
+    args = ["-fPIC"],
+)
+```
+
+This ensures that `-fPIC` is added to the command-line only when we are using a
+tool that supports PIC.
+
+**ATTRIBUTES**
+
+
+| Name  | Description | Type | Mandatory | Default |
+| :------------- | :------------- | :------------- | :------------- | :------------- |
+| <a id="cc_tool_capability-name"></a>name |  A unique name for this target.   | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required |  |
+| <a id="cc_tool_capability-feature_name"></a>feature_name |  The name of the feature to generate for this capability   | String | optional |  `""`  |
 
 
 <a id="cc_args"></a>
diff --git a/tests/rule_based_toolchain/generate_factory.bzl b/tests/rule_based_toolchain/generate_factory.bzl
index c58bb51..ea9dc58 100644
--- a/tests/rule_based_toolchain/generate_factory.bzl
+++ b/tests/rule_based_toolchain/generate_factory.bzl
@@ -67,7 +67,7 @@
             meta.add_failure("Wanted a %s but got" % name, value)
         got_keys = sorted(structs.to_dict(value).keys())
         subjects.collection(got_keys, meta = meta.derive(details = [
-            "Value was not a %s - it has a different set of fields" % name,
+            "Value %r was not a %s - it has a different set of fields" % (value, name),
         ])).contains_exactly(want_keys).in_order()
 
     def type_factory(value, *, meta):
diff --git a/tests/rule_based_toolchain/subjects.bzl b/tests/rule_based_toolchain/subjects.bzl
index e741d67..be36b1c 100644
--- a/tests/rule_based_toolchain/subjects.bzl
+++ b/tests/rule_based_toolchain/subjects.bzl
@@ -26,6 +26,7 @@
     "FeatureSetInfo",
     "MutuallyExclusiveCategoryInfo",
     "NestedArgsInfo",
+    "ToolCapabilityInfo",
     "ToolConfigInfo",
     "ToolInfo",
     "ToolchainConfigInfo",
@@ -179,6 +180,15 @@
 )
 
 # buildifier: disable=name-conventions
+_ToolCapabilityFactory = generate_factory(
+    ToolCapabilityInfo,
+    "ToolCapabilityInfo",
+    dict(
+        name = _subjects.str,
+    ),
+)
+
+# buildifier: disable=name-conventions
 _ToolFactory = generate_factory(
     ToolInfo,
     "ToolInfo",
@@ -187,6 +197,7 @@
         runfiles = runfiles_subject,
         execution_requirements = _subjects.collection,
         allowlist_include_directories = _FakeDirectoryDepset,
+        capabilities = ProviderSequence(_ToolCapabilityFactory),
     ),
 )
 
diff --git a/tests/rule_based_toolchain/tool/BUILD b/tests/rule_based_toolchain/tool/BUILD
index 67ce625..daa617a 100644
--- a/tests/rule_based_toolchain/tool/BUILD
+++ b/tests/rule_based_toolchain/tool/BUILD
@@ -6,6 +6,7 @@
 cc_tool(
     name = "tool",
     src = "//tests/rule_based_toolchain/testdata:bin_wrapper.sh",
+    capabilities = ["//cc/toolchains/capabilities:supports_pic"],
     data = ["//tests/rule_based_toolchain/testdata:bin"],
     tags = ["requires-network"],
 )
diff --git a/tests/rule_based_toolchain/toolchain_config/BUILD b/tests/rule_based_toolchain/toolchain_config/BUILD
index b002eff..6d894a9 100644
--- a/tests/rule_based_toolchain/toolchain_config/BUILD
+++ b/tests/rule_based_toolchain/toolchain_config/BUILD
@@ -47,6 +47,7 @@
     name = "c_compile_tool",
     src = "//tests/rule_based_toolchain/testdata:bin_wrapper",
     allowlist_include_directories = ["//tests/rule_based_toolchain/testdata:subdirectory_3"],
+    capabilities = ["//cc/toolchains/capabilities:supports_pic"],
 )
 
 cc_sysroot(
diff --git a/tests/rule_based_toolchain/toolchain_config/toolchain_config_test.bzl b/tests/rule_based_toolchain/toolchain_config/toolchain_config_test.bzl
index 71a05cf..f54fb52 100644
--- a/tests/rule_based_toolchain/toolchain_config/toolchain_config_test.bzl
+++ b/tests/rule_based_toolchain/toolchain_config/toolchain_config_test.bzl
@@ -208,6 +208,10 @@
             )],
         ),
         legacy_feature(
+            name = "supports_pic",
+            enabled = False,
+        ),
+        legacy_feature(
             name = "implied_by_always_enabled",
             enabled = True,
             flag_sets = [
@@ -252,6 +256,7 @@
             action_name = "c_compile",
             enabled = True,
             tools = [legacy_tool(tool = exe)],
+            implies = ["supports_pic"],
         ),
         legacy_action_config(
             action_name = "cpp_compile",