BEGIN_PUBLIC
Implement cc_tool
END_PUBLIC

PiperOrigin-RevId: 609307150
Change-Id: I2e135a59e06a56ca8ec071254d340ac4b984b234
diff --git a/cc/toolchains/cc_toolchain_info.bzl b/cc/toolchains/cc_toolchain_info.bzl
index f7a69ba..5efdda9 100644
--- a/cc/toolchains/cc_toolchain_info.bzl
+++ b/cc/toolchains/cc_toolchain_info.bzl
@@ -124,7 +124,7 @@
     # @unsorted-dict-items
     fields = {
         "label": "(Label) The label defining this provider. Place in error messages to simplify debugging",
-        "exe": "(Optional[File]) The file corresponding to the tool",
+        "exe": "(File) The file corresponding to the tool",
         "runfiles": "(depset[File]) The files required to run the tool",
         "requires_any_of": "(Sequence[FeatureConstraintInfo]) A set of constraints, one of which is required to enable the tool. Equivalent to with_features",
         "execution_requirements": "(Sequence[str]) A set of execution requirements of the tool",
diff --git a/cc/toolchains/impl/collect.bzl b/cc/toolchains/impl/collect.bzl
index 77979b4..9d97471 100644
--- a/cc/toolchains/impl/collect.bzl
+++ b/cc/toolchains/impl/collect.bzl
@@ -16,15 +16,19 @@
 load(
     "//cc/toolchains:cc_toolchain_info.bzl",
     "ActionTypeSetInfo",
+    "ToolInfo",
 )
 
-visibility("//cc/toolchains/...")
+visibility([
+    "//cc/toolchains/...",
+    "//tests/rule_based_toolchain/...",
+])
 
 def collect_provider(targets, provider):
     """Collects providers from a label list.
 
     Args:
-        targets: (list[Target]) An attribute from attr.label_list
+        targets: (List[Target]) An attribute from attr.label_list
         provider: (provider) The provider to look up
     Returns:
         A list of the providers
@@ -35,7 +39,7 @@
     """Collects DefaultInfo from a label list.
 
     Args:
-        targets: (list[Target]) An attribute from attr.label_list
+        targets: (List[Target]) An attribute from attr.label_list
     Returns:
         A list of the associated defaultinfo
     """
@@ -53,3 +57,55 @@
 
 collect_action_types = _make_collector(ActionTypeSetInfo, "actions")
 collect_files = _make_collector(DefaultInfo, "files")
+
+def collect_data(ctx, targets):
+    """Collects from a 'data' attribute.
+
+    This is distinguished from collect_files by the fact that data attributes
+    attributes include runfiles.
+
+    Args:
+        ctx: (Context) The ctx for the current rule
+        targets: (List[Target]) A list of files or executables
+
+    Returns:
+        A depset containing all files for each of the targets, and all runfiles
+        required to run them.
+    """
+    return ctx.runfiles(transitive_files = collect_files(targets)).merge_all([
+        info.default_runfiles
+        for info in collect_defaultinfo(targets)
+        if info.default_runfiles != None
+    ])
+
+def collect_tools(ctx, targets, fail = fail):
+    """Collects tools from a label_list.
+
+    Each entry in the label list may either be a cc_tool or a binary.
+
+    Args:
+        ctx: (Context) The ctx for the current rule
+        targets: (List[Target]) A list of targets. Each of these targets may be
+          either a cc_tool or an executable.
+        fail: (function) The fail function. Should only be used in tests.
+
+    Returns:
+        A List[ToolInfo], with regular executables creating custom tool info.
+    """
+    tools = []
+    for target in targets:
+        info = target[DefaultInfo]
+        if ToolInfo in target:
+            tools.append(target[ToolInfo])
+        elif info.files_to_run != None and info.files_to_run.executable != None:
+            tools.append(ToolInfo(
+                label = target.label,
+                exe = info.files_to_run.executable,
+                runfiles = collect_data(ctx, [target]),
+                requires_any_of = tuple(),
+                execution_requirements = tuple(),
+            ))
+        else:
+            fail("Expected %s to be a cc_tool or a binary rule" % target.label)
+
+    return tools
diff --git a/cc/toolchains/tool.bzl b/cc/toolchains/tool.bzl
new file mode 100644
index 0000000..01ea481
--- /dev/null
+++ b/cc/toolchains/tool.bzl
@@ -0,0 +1,100 @@
+# Copyright 2023 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 cc_tool"""
+
+load("//cc/toolchains/impl:collect.bzl", "collect_data", "collect_provider")
+load(
+    ":cc_toolchain_info.bzl",
+    "FeatureConstraintInfo",
+    "ToolInfo",
+)
+
+def _cc_tool_impl(ctx):
+    exe = ctx.executable.executable
+    runfiles = collect_data(ctx, ctx.attr.data + [ctx.attr.executable])
+    tool = ToolInfo(
+        label = ctx.label,
+        exe = exe,
+        runfiles = runfiles,
+        requires_any_of = tuple(collect_provider(
+            ctx.attr.requires_any_of,
+            FeatureConstraintInfo,
+        )),
+        execution_requirements = tuple(ctx.attr.execution_requirements),
+    )
+
+    link = ctx.actions.declare_file(ctx.label.name)
+    ctx.actions.symlink(
+        output = link,
+        target_file = ctx.executable.executable,
+        is_executable = True,
+    )
+    return [
+        tool,
+        # This isn't required, but now we can do "bazel run <tool>", which can
+        # be very helpful when debugging toolchains.
+        DefaultInfo(
+            files = depset([link]),
+            runfiles = runfiles,
+            executable = link,
+        ),
+    ]
+
+cc_tool = rule(
+    implementation = _cc_tool_impl,
+    # @unsorted-dict-items
+    attrs = {
+        "executable": attr.label(
+            allow_files = True,
+            executable = True,
+            cfg = "exec",
+            doc = """The underlying binary that this tool represents.
+
+Usually just a single prebuilt (eg. @sysroot//:bin/clang), but may be any
+executable label.
+""",
+        ),
+        "data": attr.label_list(
+            allow_files = True,
+            doc = "Additional files that are required for this tool to run.",
+        ),
+        "execution_requirements": attr.string_list(
+            doc = "A list of strings that provide hints for execution environment compatibility (e.g. `requires-mac`).",
+        ),
+        "requires_any_of": attr.label_list(
+            providers = [FeatureConstraintInfo],
+            doc = """This will be enabled when any of the constraints are met.
+
+If omitted, this tool will be enabled unconditionally.
+""",
+        ),
+    },
+    provides = [ToolInfo],
+    doc = """Declares a tool that can be bound to action configs.
+
+A tool is a binary with extra metadata for the action config rule to consume
+(eg. execution_requirements).
+
+Example:
+```
+cc_tool(
+    name = "clang_tool",
+    executable = "@llvm_toolchain//:bin/clang",
+    # Suppose clang needs libc to run.
+    data = ["@llvm_toolchain//:lib/x86_64-linux-gnu/libc.so.6"]
+)
+```
+""",
+    executable = True,
+)
diff --git a/tests/rule_based_toolchain/args/args_test.bzl b/tests/rule_based_toolchain/args/args_test.bzl
index 6aab2e2..859cc6c 100644
--- a/tests/rule_based_toolchain/args/args_test.bzl
+++ b/tests/rule_based_toolchain/args/args_test.bzl
@@ -18,7 +18,7 @@
     "ArgsInfo",
 )
 
-visibility("//tests/rule_based_toolchain/...")
+visibility("private")
 
 def _test_simple_args_impl(env, targets):
     simple = env.expect.that_target(targets.simple).provider(ArgsInfo)
diff --git a/tests/rule_based_toolchain/subjects.bzl b/tests/rule_based_toolchain/subjects.bzl
index 87f6cb4..5e5ca62 100644
--- a/tests/rule_based_toolchain/subjects.bzl
+++ b/tests/rule_based_toolchain/subjects.bzl
@@ -34,6 +34,10 @@
 
 visibility("//tests/rule_based_toolchain/...")
 
+# The default runfiles subject uses path instead of short_path.
+# This makes it rather awkward for copybara.
+runfiles_subject = lambda value, meta: _subjects.depset_file(value.files, meta = meta)
+
 # buildifier: disable=name-conventions
 _ActionTypeFactory = generate_factory(
     ActionTypeInfo,
@@ -135,7 +139,7 @@
     "ToolInfo",
     dict(
         exe = _subjects.file,
-        runfiles = _subjects.depset_file,
+        runfiles = runfiles_subject,
         requires_any_of = ProviderSequence(_FeatureConstraintFactory),
         execution_requirements = _subjects.collection,
     ),
@@ -196,5 +200,6 @@
         result = result_subject,
         optional = optional_subject,
         struct = struct_subject,
+        runfiles = runfiles_subject,
     ) | {factory.name: factory.factory for factory in FACTORIES})
 )
diff --git a/tests/rule_based_toolchain/testdata/BUILD b/tests/rule_based_toolchain/testdata/BUILD
index 6007720..4bfb3e6 100644
--- a/tests/rule_based_toolchain/testdata/BUILD
+++ b/tests/rule_based_toolchain/testdata/BUILD
@@ -1,3 +1,5 @@
+load("@bazel_skylib//rules:native_binary.bzl", "native_binary")
+
 package(default_visibility = ["//tests/rule_based_toolchain:__subpackages__"])
 
 exports_files(
@@ -7,6 +9,13 @@
     ),
 )
 
+native_binary(
+    name = "bin_wrapper",
+    src = "bin_wrapper.sh",
+    out = "bin_wrapper",
+    data = [":bin"],
+)
+
 filegroup(
     name = "multiple",
     srcs = [
@@ -14,3 +23,10 @@
         "multiple2",
     ],
 )
+
+# Analysis_test is unable to depend on source files directly, but it can depend
+# on a filegroup containing a single file.
+filegroup(
+    name = "bin_filegroup",
+    srcs = ["bin"],
+)
diff --git a/tests/rule_based_toolchain/testdata/bin b/tests/rule_based_toolchain/testdata/bin
new file mode 100755
index 0000000..ff29c83
--- /dev/null
+++ b/tests/rule_based_toolchain/testdata/bin
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+echo "Running unwrapped tool"
diff --git a/tests/rule_based_toolchain/testdata/bin_wrapper.sh b/tests/rule_based_toolchain/testdata/bin_wrapper.sh
new file mode 100755
index 0000000..ce615a2
--- /dev/null
+++ b/tests/rule_based_toolchain/testdata/bin_wrapper.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+echo "Running tool wrapper"
diff --git a/tests/rule_based_toolchain/tool/BUILD b/tests/rule_based_toolchain/tool/BUILD
new file mode 100644
index 0000000..a6b01ea
--- /dev/null
+++ b/tests/rule_based_toolchain/tool/BUILD
@@ -0,0 +1,30 @@
+load("@rules_testing//lib:util.bzl", "util")
+load("//cc/toolchains:tool.bzl", "cc_tool")
+load("//tests/rule_based_toolchain:analysis_test_suite.bzl", "analysis_test_suite")
+load(":tool_test.bzl", "TARGETS", "TESTS")
+
+util.helper_target(
+    cc_tool,
+    name = "tool",
+    data = ["//tests/rule_based_toolchain/testdata:bin"],
+    executable = "//tests/rule_based_toolchain/testdata:bin_wrapper.sh",
+    execution_requirements = ["requires-mac"],
+)
+
+util.helper_target(
+    cc_tool,
+    name = "wrapped_tool",
+    executable = "//tests/rule_based_toolchain/testdata:bin_wrapper",
+)
+
+util.helper_target(
+    cc_tool,
+    name = "data_with_runfiles",
+    executable = "//tests/rule_based_toolchain/testdata:file1",
+)
+
+analysis_test_suite(
+    name = "test_suite",
+    targets = TARGETS,
+    tests = TESTS,
+)
diff --git a/tests/rule_based_toolchain/tool/tool_test.bzl b/tests/rule_based_toolchain/tool/tool_test.bzl
new file mode 100644
index 0000000..840ae84
--- /dev/null
+++ b/tests/rule_based_toolchain/tool/tool_test.bzl
@@ -0,0 +1,104 @@
+# 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.
+"""Tests for the cc_args rule."""
+
+load("//cc/toolchains:cc_toolchain_info.bzl", "ToolInfo")
+load("//cc/toolchains/impl:collect.bzl", _collect_tools = "collect_tools")
+load("//tests/rule_based_toolchain:subjects.bzl", "result_fn_wrapper", "subjects")
+
+visibility("private")
+
+collect_tools = result_fn_wrapper(_collect_tools)
+collection_result = subjects.result(subjects.collection)
+
+collect_tool = result_fn_wrapper(
+    lambda ctx, target, fail: _collect_tools(ctx, [target], fail = fail)[0],
+)
+tool_result = subjects.result(subjects.ToolInfo)
+
+# Generated by native_binary.
+_BIN_WRAPPER_SYMLINK = "tests/rule_based_toolchain/testdata/bin_wrapper"
+_BIN_WRAPPER = "tests/rule_based_toolchain/testdata/bin_wrapper.sh"
+_BIN = "tests/rule_based_toolchain/testdata/bin"
+
+def _tool_test(env, targets):
+    tool = env.expect.that_target(targets.tool).provider(ToolInfo)
+    tool.exe().short_path_equals(_BIN_WRAPPER)
+    tool.runfiles().contains_exactly([
+        _BIN_WRAPPER,
+        _BIN,
+    ])
+
+def _wrapped_tool_includes_runfiles_test(env, targets):
+    tool = env.expect.that_target(targets.wrapped_tool).provider(ToolInfo)
+    tool.exe().short_path_equals(_BIN_WRAPPER_SYMLINK)
+    tool.runfiles().contains_exactly([
+        _BIN_WRAPPER_SYMLINK,
+        _BIN,
+    ])
+
+def _collect_tools_collects_tools_test(env, targets):
+    env.expect.that_value(
+        value = collect_tools(env.ctx, [targets.tool, targets.wrapped_tool]),
+        factory = collection_result,
+    ).ok().contains_exactly(
+        [targets.tool[ToolInfo], targets.wrapped_tool[ToolInfo]],
+    ).in_order()
+
+def _collect_tools_collects_binaries_test(env, targets):
+    tool_wrapper = env.expect.that_value(
+        value = collect_tool(env.ctx, targets.bin_wrapper),
+        factory = tool_result,
+    ).ok()
+    tool_wrapper.label().equals(targets.bin_wrapper.label)
+    tool_wrapper.exe().short_path_equals(_BIN_WRAPPER_SYMLINK)
+    tool_wrapper.runfiles().contains_exactly([
+        _BIN_WRAPPER_SYMLINK,
+        _BIN,
+    ])
+
+def _collect_tools_collects_single_files_test(env, targets):
+    bin = env.expect.that_value(
+        value = collect_tool(env.ctx, targets.bin_filegroup),
+        factory = tool_result,
+        expr = "bin_filegroup",
+    ).ok()
+    bin.label().equals(targets.bin_filegroup.label)
+    bin.exe().short_path_equals(_BIN)
+    bin.runfiles().contains_exactly([_BIN])
+
+def _collect_tools_fails_on_non_binary_test(env, targets):
+    env.expect.that_value(
+        value = collect_tools(env.ctx, [targets.multiple]),
+        factory = collection_result,
+        expr = "multiple_non_binary",
+    ).err()
+
+TARGETS = [
+    "//tests/rule_based_toolchain/tool:tool",
+    "//tests/rule_based_toolchain/tool:wrapped_tool",
+    "//tests/rule_based_toolchain/testdata:bin_wrapper",
+    "//tests/rule_based_toolchain/testdata:multiple",
+    "//tests/rule_based_toolchain/testdata:bin_filegroup",
+]
+
+# @unsorted-dict-items
+TESTS = {
+    "tool_test": _tool_test,
+    "wrapped_tool_includes_runfiles_test": _wrapped_tool_includes_runfiles_test,
+    "collect_tools_collects_tools_test": _collect_tools_collects_tools_test,
+    "collect_tools_collects_binaries_test": _collect_tools_collects_binaries_test,
+    "collect_tools_collects_single_files_test": _collect_tools_collects_single_files_test,
+    "collect_tools_fails_on_non_binary_test": _collect_tools_fails_on_non_binary_test,
+}