Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cc/toolchains/cc_toolchain_info.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ ToolInfo = provider(
"exe": "(File) The file corresponding to the tool",
"runfiles": "(runfiles) The files required to run the tool",
"execution_requirements": "(Sequence[str]) A set of execution requirements of the tool",
"env": "(dict[str, str]) Environment variables applied when using this 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.",
},
Expand Down
1 change: 1 addition & 0 deletions cc/toolchains/impl/collect.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def collect_tools(ctx, targets, fail = fail):
exe = info.files_to_run.executable,
runfiles = collect_data(ctx, [target]),
execution_requirements = tuple(),
env = {},
allowlist_include_directories = depset(),
capabilities = tuple(),
))
Expand Down
38 changes: 38 additions & 0 deletions cc/toolchains/impl/label_utils.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright 2026 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.
"""Helpers for passing possibly duplicated labels through rule attributes."""

def deduplicate_label_list(name, labels):
"""Deduplicates a label list while preserving the original indexes.

Args:
name: Name of the macro target using this helper.
labels: Labels to normalize and deduplicate.

Returns:
A struct with deduplicated labels and original-to-deduplicated indexes.
"""
deduplicated_labels = {}
index_for_label = []

for label in labels:
package_relative_label = native.package_relative_label(label)
if package_relative_label not in deduplicated_labels:
deduplicated_labels[package_relative_label] = len(deduplicated_labels)
index_for_label.append(deduplicated_labels[package_relative_label])

return struct(
labels = deduplicated_labels.keys(),
indexes = index_for_label,
)
21 changes: 20 additions & 1 deletion cc/toolchains/impl/legacy_converter.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ def _convert_args_sequence(args_sequence, strip_actions = False):

return struct(flag_sets = flag_sets, env_sets = env_sets)

def _convert_tool_env(tool, action_name):
if not tool.env:
return None

return legacy_env_set(
actions = [action_name],
env_entries = [
legacy_env_entry(key = key, value = value)
for key, value in sorted(tool.env.items())
],
)

def convert_feature(feature, enabled = False):
if feature.external:
return None
Expand Down Expand Up @@ -188,6 +200,13 @@ def convert_toolchain(toolchain):
args.flag_sets.extend(new_args.flag_sets)
args.env_sets.extend(new_args.env_sets)

toolchain_args = _convert_args_sequence(toolchain.args.args)
tool_env_sets = []
for action_type in sorted(toolchain.tool_map.configs.keys(), key = lambda action: action.name):
env_set = _convert_tool_env(toolchain.tool_map.configs[action_type], action_type.name)
if env_set:
tool_env_sets.append(env_set)

action_configs, cap_features = _convert_tool_map(toolchain.tool_map, args_by_action)
features = [
convert_feature(feature, enabled = feature in toolchain.enabled_features)
Expand All @@ -200,7 +219,7 @@ def convert_toolchain(toolchain):
# conflict with the name of a feature the user creates.
name = "implied_by_always_enabled_env_sets",
enabled = True,
env_sets = _convert_args_sequence(toolchain.args.args).env_sets,
env_sets = tool_env_sets + toolchain_args.env_sets,
))

cxx_builtin_include_directories = [
Expand Down
127 changes: 126 additions & 1 deletion cc/toolchains/tool.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

load("@bazel_skylib//rules/directory:providers.bzl", "DirectoryInfo")
load("//cc/toolchains/impl:collect.bzl", "collect_data", "collect_provider")
load("//cc/toolchains/impl:label_utils.bzl", "deduplicate_label_list")
load("//cc/toolchains/impl:nested_args.bzl", "format_dict_values")
load(
":cc_toolchain_info.bzl",
"ToolCapabilityInfo",
Expand All @@ -30,12 +32,22 @@ def _cc_tool_impl(ctx):
else:
fail("Expected cc_tool's src attribute to be either an executable or a single file")

format_targets = {}
for i in range(len(ctx.attr.format_keys)):
format_targets[ctx.attr.format_keys[i]] = ctx.attr.format_values[ctx.attr.format_value_indexes[i]]
env, _ = format_dict_values(
env = ctx.attr.env,
format = format_targets,
must_use = format_targets.keys(),
)

runfiles = collect_data(ctx, ctx.attr.data + [ctx.attr.src])
tool = ToolInfo(
label = ctx.label,
exe = exe,
runfiles = runfiles,
execution_requirements = tuple(ctx.attr.tags),
env = env,
allowlist_include_directories = depset(
direct = [d[DirectoryInfo] for d in ctx.attr.allowlist_include_directories],
),
Expand All @@ -59,7 +71,7 @@ def _cc_tool_impl(ctx):
),
]

cc_tool = rule(
_cc_tool = rule(
implementation = _cc_tool_impl,
# @unsorted-dict-items
attrs = {
Expand Down Expand Up @@ -98,6 +110,31 @@ add them to 'data' as well.
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)`.
""",
),
"env": attr.string_dict(
doc = """Environment variables to apply when running this tool.

Format expansion is performed on values using the format attribute.
""",
),
"format_keys": attr.string_list(
doc = """Format strings used in substitutions for env values.

Use the cc_tool macro's format attribute instead.
""",
),
"format_values": attr.label_list(
allow_files = True,
doc = """Targets used in substitutions for env values.

Use the cc_tool macro's format attribute instead.
""",
),
"format_value_indexes": attr.int_list(
doc = """Index of the target in format_values for each format key.

Use the cc_tool macro's format attribute instead.
""",
),
"capabilities": attr.label_list(
Expand Down Expand Up @@ -136,3 +173,91 @@ cc_tool(
""",
executable = True,
)

def cc_tool(
Comment thread
dzbarsky marked this conversation as resolved.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From internal review:

Mid term we might want to consider converting this into a symbolic macro.

(maybe add a TODO?)

*,
name,
src = None,
data = None,
allowlist_include_directories = None,
env = None,
format = {},
capabilities = None,
**kwargs):
"""Declares a tool for use by toolchain actions.

`cc_tool` rules are used in a `cc_tool_map` rule to ensure all files and
metadata required to run a tool are available when constructing a `cc_toolchain`.

In general, include all files that are always required to run a tool (e.g. libexec/** and
cross-referenced tools in bin/*) in the [data](#cc_tool-data) attribute. If some files are only
required when certain flags are passed to the tool, consider using a `cc_args` rule to
bind the files to the flags that require them. This reduces the overhead required to properly
enumerate a sandbox with all the files required to run a tool, and ensures that there isn't
unintentional leakage across configurations and actions.

Example:
```
load("//cc/toolchains:tool.bzl", "cc_tool")

cc_tool(
name = "clang",
src = "@llvm_toolchain//:bin/clang",
# Suppose clang needs libc to run.
data = ["@llvm_toolchain//:lib/x86_64-linux-gnu/libc.so.6"]
capabilities = ["//cc/toolchains/capabilities:supports_pic"],
)
```

Args:
name: (str) The name of the target.
src: (Label) The underlying binary that this tool represents.
Usually just a single prebuilt (e.g. @toolchain//:bin/clang), but may be any
executable label.
data: (List[Label]) Additional files that are required for this tool to run.
Frequently, clang and gcc require additional files to execute as they often shell out to
other binaries (e.g. `cc1`).
allowlist_include_directories: (List[Label]) Include paths implied by using this tool.
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.

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.

These files are not automatically passed to each action. If they need to be,
add them to 'data' as well.

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)`.
env: (Dict[str, str]) Environment variables to apply when running this tool.
Format expansion is performed on values using `format`.
format: (Dict[str, Label]) A mapping of format strings to the label of a corresponding
target. This target can be a `directory`, `subdirectory`, or a single file that the
value should be pulled from. All instances of `{variable_name}` in the `env` dictionary
values will be replaced with the expanded value in this dictionary.
capabilities: (List[Label]) Declares that a tool is capable of doing something.
For example, `@rules_cc//cc/toolchains/capabilities:supports_pic`.
**kwargs: [common attributes](https://bazel.build/reference/be/common-definitions#common-attributes)
that should be applied to this rule.
"""
format_keys = format.keys()
format_values = deduplicate_label_list(
name = name,
labels = [format[key] for key in format_keys],
)
return _cc_tool(
name = name,
src = src,
data = data,
allowlist_include_directories = allowlist_include_directories,
env = env,
format_keys = format_keys,
format_values = format_values.labels,
format_value_indexes = format_values.indexes,
capabilities = capabilities,
**kwargs
)
19 changes: 8 additions & 11 deletions cc/toolchains/tool_map.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ load(
"collect_provider",
"collect_tools",
)
load("//cc/toolchains/impl:label_utils.bzl", "deduplicate_label_list")
load(
":cc_toolchain_info.bzl",
"ActionTypeSetInfo",
Expand Down Expand Up @@ -112,20 +113,16 @@ def cc_tool_map(name, tools, **kwargs):
and the `cc_tool` or executable target that implements that action.
**kwargs: [common attributes](https://bazel.build/reference/be/common-definitions#common-attributes) that should be applied to this rule.
"""
actions = []
tool_index_for_action = []
deduplicated_tools = {}
for action, tool in tools.items():
actions.append(action)
label = native.package_relative_label(tool)
if label not in deduplicated_tools:
deduplicated_tools[label] = len(deduplicated_tools)
tool_index_for_action.append(deduplicated_tools[label])
actions = tools.keys()
deduplicated_tools = deduplicate_label_list(
name = name,
labels = [tools[action] for action in actions],
)

_cc_tool_map(
name = name,
actions = actions,
tools = deduplicated_tools.keys(),
tool_index_for_action = tool_index_for_action,
tools = deduplicated_tools.labels,
tool_index_for_action = deduplicated_tools.indexes,
**kwargs
)
Loading