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 @@ ToolInfo = provider( # @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 @@ def collect_defaultinfo(targets): """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 @@ def _make_collector(provider, field): 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 ", 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 @@ load( "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 @@ load(":generics.bzl", "optional_subject", "result_subject", "struct_subject", _r 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 @@ _ToolFactory = generate_factory( "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 @@ subjects = struct( 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 @@ exports_files( ), ) +native_binary( + name = "bin_wrapper", + src = "bin_wrapper.sh", + out = "bin_wrapper", + data = [":bin"], +) + filegroup( name = "multiple", srcs = [ @@ -14,3 +23,10 @@ filegroup( "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, +}