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 @@ -198,6 +198,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 @@ -106,6 +106,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
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
108 changes: 107 additions & 1 deletion cc/toolchains/tool.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

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

format_targets = {k: v for v, k in ctx.attr.format.items()}
Copy link
Collaborator

Choose a reason for hiding this comment

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

From internal review:

I think it's worth mentioning here in a comment that attr.format stores things in reverse, with labels as keys and format strings as values, with a pointer to the comment where this reversal is done.

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 +68,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 +107,20 @@ 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": attr.label_keyed_string_dict(
allow_files = True,
doc = """Variables to be used in substitutions for env values.

The keys are targets and the values are format strings. Use the cc_tool macro
to provide the inverted mapping from format string to target.
""",
),
"capabilities": attr.label_list(
Expand Down Expand Up @@ -136,3 +159,86 @@ cc_tool(
""",
executable = True,
)

def cc_tool(
Copy link
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.
"""
return _cc_tool(
name = name,
src = src,
data = data,
allowlist_include_directories = allowlist_include_directories,
env = env,
# We flip the key/value pairs in the dictionary here because Bazel doesn't have a
# string-keyed label dict attribute type.
Copy link
Collaborator

Choose a reason for hiding this comment

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

From internal review:

It may be worth also adding a pointer here to src/main/java/com/google/devtools/build/lib/packages/Type.java;l=61 where the cost of adding a new attribute type are spelled out. I had already started to think about adding one when I hit upon this guidance.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Admittedly, this string was copied from another such instance.

Bazel does support a string-keyed label, but not across all versions: https://bazel.build/rules/lib/toplevel/attr#string_keyed_label_dict

The version support for label_keyed_string_dict string is wider.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep! Tom and I iterated in DMs and he's going to import the version with comments addressed!

format = {k: v for v, k in format.items()},
Copy link
Contributor

Choose a reason for hiding this comment

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

Flipping the key-value pairs is insufficient as it won't work if you have the same value multiple times.

Rather, we should probably instead write

format_keys = list(format.keys())
format_values = list(format.values())

Then we can, in the rule itself, write format = {k: v for k, v in zip(ctx.attr.format_keys, ctx.attr.format_values)}.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good callout. Presumably though, if you had the same label used twice, you could use the same place-holder, too. Do you think it would be sufficient to error in that case or would you rather implement this?

Copy link
Collaborator

@armandomontanez armandomontanez Mar 11, 2026

Choose a reason for hiding this comment

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

cc_tool_map has a workaround for this problem. Probably makes sense to just break that out into a starlark helper.

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])

capabilities = capabilities,
**kwargs
)
98 changes: 51 additions & 47 deletions docs/toolchain_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,53 +474,6 @@ cc_feature(
| <a id="cc_mutually_exclusive_category-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"></a>

## cc_tool

<pre>
load("@rules_cc//cc/toolchains/impl:documented_api.bzl", "cc_tool")

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.

[`cc_tool`](#cc_tool) rules are used in a [`cc_tool_map`](#cc_tool_map) rule to ensure all files and
metadata required to run a tool are available when constructing a [`cc_toolchain`](#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`](#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("@rules_cc//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 = ["@rules_cc//cc/toolchains/capabilities:supports_pic"],
)
```

**ATTRIBUTES**


| Name | Description | Type | Mandatory | Default |
| :------------- | :------------- | :------------- | :------------- | :------------- |
| <a id="cc_tool-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-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>These files are not automatically passed to each action. If they need to be, add them to 'data' as well.<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
Expand Down Expand Up @@ -725,6 +678,57 @@ use this rule.
| <a id="cc_nested_args-kwargs"></a>kwargs | [common attributes](https://bazel.build/reference/be/common-definitions#common-attributes) that should be applied to this rule. | none |


<a id="cc_tool"></a>

## cc_tool

<pre>
load("@rules_cc//cc/toolchains/impl:documented_api.bzl", "cc_tool")

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-env">env</a>, <a href="#cc_tool-format">format</a>, <a href="#cc_tool-capabilities">capabilities</a>, <a href="#cc_tool-kwargs">**kwargs</a>)
</pre>

Declares a tool for use by toolchain actions.

[`cc_tool`](#cc_tool) rules are used in a [`cc_tool_map`](#cc_tool_map) rule to ensure all files and
metadata required to run a tool are available when constructing a [`cc_toolchain`](#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`](#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("@rules_cc//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 = ["@rules_cc//cc/toolchains/capabilities:supports_pic"],
)
```


**PARAMETERS**


| Name | Description | Default Value |
| :------------- | :------------- | :------------- |
| <a id="cc_tool-name"></a>name | (str) The name of the target. | none |
| <a id="cc_tool-src"></a>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. | `None` |
| <a id="cc_tool-data"></a>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`). | `None` |
| <a id="cc_tool-allowlist_include_directories"></a>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.<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>These files are not automatically passed to each action. If they need to be, add them to 'data' as well.<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)`. | `None` |
| <a id="cc_tool-env"></a>env | (Dict[str, str]) Environment variables to apply when running this tool. Format expansion is performed on values using `format`. | `None` |
| <a id="cc_tool-format"></a>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. | `{}` |
| <a id="cc_tool-capabilities"></a>capabilities | (List[Label]) Declares that a tool is capable of doing something. For example, `@rules_cc//cc/toolchains/capabilities:supports_pic`. | `None` |
| <a id="cc_tool-kwargs"></a>kwargs | [common attributes](https://bazel.build/reference/be/common-definitions#common-attributes) that should be applied to this rule. | none |


<a id="cc_tool_map"></a>

## cc_tool_map
Expand Down
1 change: 1 addition & 0 deletions tests/rule_based_toolchain/subjects.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ _ToolFactory = generate_factory(
exe = _subjects.file,
runfiles = runfiles_subject,
execution_requirements = _subjects.collection,
env = _subjects.dict,
allowlist_include_directories = _FakeDirectoryDepset,
capabilities = ProviderSequence(_ToolCapabilityFactory),
),
Expand Down
17 changes: 17 additions & 0 deletions tests/rule_based_toolchain/tool/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ cc_tool(
visibility = ["//tests/rule_based_toolchain:__subpackages__"],
)

cc_tool(
name = "tool_with_env",
src = "//tests/rule_based_toolchain/testdata:bin_wrapper.sh",
data = [
"//tests/rule_based_toolchain/testdata:bin",
"//tests/rule_based_toolchain/testdata:file1",
],
env = {
"STATIC": "value",
"TOOL_ENV": "{tool_env}",
},
format = {
"tool_env": "//tests/rule_based_toolchain/testdata:file1",
},
visibility = ["//tests/rule_based_toolchain:__subpackages__"],
)

cc_directory_tool(
name = "directory_tool",
data = ["bin"],
Expand Down
9 changes: 9 additions & 0 deletions tests/rule_based_toolchain/tool/tool_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ def _tool_with_allowlist_include_directories_test(env, targets):
_FILE1,
])

def _tool_env_expansion_test(env, targets):
tool = env.expect.that_target(targets.tool_with_env).provider(ToolInfo)
tool.env().contains_exactly({
"STATIC": "value",
"TOOL_ENV": "tests/rule_based_toolchain/testdata/file1",
})

def _collect_tools_collects_tools_test(env, targets):
env.expect.that_value(
value = collect_tools(env.ctx, [targets.tool, targets.wrapped_tool]),
Expand Down Expand Up @@ -108,6 +115,7 @@ TARGETS = [
"//tests/rule_based_toolchain/tool:wrapped_tool",
"//tests/rule_based_toolchain/tool:directory_tool",
"//tests/rule_based_toolchain/tool:tool_with_allowlist_include_directories",
"//tests/rule_based_toolchain/tool:tool_with_env",
"//tests/rule_based_toolchain/testdata:bin_wrapper",
"//tests/rule_based_toolchain/testdata:multiple",
"//tests/rule_based_toolchain/testdata:bin_filegroup",
Expand All @@ -124,4 +132,5 @@ TESTS = {
"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,
"tool_with_allowlist_include_directories_test": _tool_with_allowlist_include_directories_test,
"tool_env_expansion_test": _tool_env_expansion_test,
}
Loading