Execution Groups
A configured target normally has one execution platform, and every action the target registers runs there. Execution groups let one target use several execution platforms. A rule declares groups, each group gets its own execution platform, and each action says which group it runs in.
Use an execution group when different actions of one target need different machines. For example:
- Resource compilation needs a macOS tool, but packaging can run on Linux.
- Compilation can run remotely, but the link must run locally.
Without groups, a single macOS-only tool moves the whole target to macOS. The usual workaround is to split the target in two. With groups, the macOS-only tool constrains only the group that runs it.
Declaring groups
Create a group with exec_group() and assign it to a top-level global. The
name of the global is the name of the group:
Link = exec_group()
exec_group() takes no arguments. Groups are values of type ExecGroup, so
you can define them in a shared .bzl file and load() them. Loading a group
under another name does not rename it. A group is identified by its name and
the file that defines it.
A rule lists the groups it uses in exec_groups:
my_binary = rule(
impl = _my_binary_impl,
attrs = {...},
exec_groups = [DefaultExecGroup, Link],
)
DefaultExecGroup is the built-in default group. Its platform is the target's
usual execution platform. Every exec dep, toolchain dep and action that does
not name a group is in the default group.
- A rule without named groups uses the default group implicitly and needs no changes.
- A rule with named groups can use the default group only if it lists
DefaultExecGroupinexec_groups. Leave it out to make sure that every action names a group. - Every group in
exec_groupsmust have a distinct name. - The name
DefaultExecGroupis reserved:DefaultExecGroup = exec_group()is an error. - A rule with named groups and
uses_pluginsmust listDefaultExecGroup, because plugins are deps of the default group.
Putting deps in a group
Pass exec_group to attrs.exec_dep() or attrs.toolchain_dep(). The dep is
configured for that group's platform, and it constrains only that group's
platform resolution:
Resources = exec_group()
attrs = {
"_compiler": attrs.exec_dep(default = "//tools:compiler", exec_group = DefaultExecGroup),
"_linker": attrs.exec_dep(default = "//tools:linker", exec_group = Link),
"_resources": attrs.toolchain_dep(default = "//toolchains:resources", exec_group = Resources),
}
In a rule with named groups, every attrs.exec_dep() and
attrs.toolchain_dep() must pass exec_group, including nested ones such as
attrs.list(attrs.exec_dep(exec_group = Link)). Tools for the default group
pass exec_group = DefaultExecGroup. A missing exec_group is an error when
the .bzl file loads, so a forgotten mark cannot silently put a tool in the
default group. exec_group = None counts as no mark, which lets
shared helpers pass an optional group through.
attrs.arg() also takes exec_group. It applies to the tools that the arg
refers to with $(exe ...), $(location_exec ...) and
$(location_toolchain ...). Other macros, such as $(location ...), are not
affected. In a rule with named groups, an attrs.arg() without exec_group
may hold flags but not tools. A tool macro in a target's value is an error
when the target is created, and a tool macro in the attribute's default is an
error when the .bzl file loads. The fix goes in the rule, not in the BUCK
file:
attrs = {
"link_cmd": attrs.arg(exec_group = Link), # may refer to tools
"link_flags": attrs.list(attrs.arg(), default = []), # flags only
}
Each attribute is in one group. If two groups need the same tool, use two attributes that point at the same target. When both groups resolve to the same platform, Buck2 configures the tool once.
Constraining a group
A target's exec_compatible_with applies to every group. To let a target
constrain a single group, the rule adds an attrs.exec_compatible_with()
attribute for that group:
attrs = {
"link_exec_compatible_with": attrs.exec_compatible_with(exec_group = Link, default = []),
}
The constraints a target sets in this attribute apply only to Link, in
addition to the target's exec_compatible_with. The value can be a list or a
select(). With exec_group = DefaultExecGroup, the constraints apply only to
the default group. A rule may have at most one such attribute per group, and it
cannot be nested inside another attribute.
Running actions in a group
Pass exec_group to ctx.actions.run():
ctx.actions.run(cmd, category = "link", exec_group = Link)
ctx.actions.write() and ctx.actions.write_json() take exec_group too.
A write renders artifact paths for its platform, for example with that
platform's path separator, so write a file in the group of the actions that
read it.
Actions without an exec_group parameter, such as copy_file and
symlinked_dir, are always registered in the default group, even in a rule
that does not list DefaultExecGroup. aquery shows DefaultExecGroup for
them. They do not run a command on the platform, so this is harmless.
A run, write or write_json without exec_group (or with None) runs in
the default group. In a rule that declares named groups but not
DefaultExecGroup, that is an error. An action in a group that the rule does
not declare fails at analysis with
Execution group `Other` is not declared by this rule. Declared groups: [...].
An action in a group that has no platform (see
How resolution works) fails at the call, and the
error names the group and the action.
Run each action with tools from attributes of its own group. Buck2 does not check this; see Limitations. Outputs of the target's own actions can be used by actions in any group.
ctx.actions.run() inside a dynamic output takes
exec_group in the same way.
Tests
ExternalRunnerTestInfo and InternalRunnerTestInfo take exec_group. The
test runs on the executor of that group when the test runner picks no executor
override:
ExternalRunnerTestInfo(type = "custom", command = [...], exec_group = TestRun)
- When the test info is created, the executor of its group is recorded in
default_executorif the creating rule declares that group and the group has a platform. A rule that returns another target's test info, such asalias, then keeps the executor of the creating rule's group. - Otherwise nothing is recorded, and the group, or the default group for a test
info without
exec_group, is resolved against the tested target when the test runs. For example, a forwarding rule that declares named groups but notDefaultExecGroupcannot forward a test info that uses the default group: the build succeeds, andbuck2 testfails because the tested target's rule does not declare the default group. - A test info that names a group its rule does not declare is created without
error. The build succeeds, and the test fails at
buck2 test. - If a test info sets both
default_executorandexec_group,default_executorwins. - In a rule that declares named groups but not
DefaultExecGroup, a test info must setexec_groupordefault_executor. Otherwise its creation fails.executor_overridesalone is not enough, because the test runs in the default group whenever the test runner picks no override.
See Test Execution for executors and overrides.
Anon targets
ctx.actions.anon_target() and ctx.actions.anon_targets() take exec_group.
The anon target then uses that group's platform instead of the target's
default platform:
anon = ctx.actions.anon_target(_link_rule, {"linker": ctx.attrs._linker}, exec_group = Link)
- Exec deps passed in must be built for the anon target's platform, so pass the
group of the attribute the tool comes from. The same check applies to tools
in args passed in: always to the tools of an arg with a named
exec_group, and to every arg tool when the calling rule declares named groups or the anon target is created withexec_group. - The rule given to
anon_target()must not declare named execution groups (exec_groups = [DefaultExecGroup]is accepted). Anon targets have no groups of their own. - In a rule that declares named groups but not
DefaultExecGroup,exec_groupis required. - Creating an anon target in a group that has no platform fails at the call.
See Anonymous Targets.
How resolution works
Buck2 resolves a platform for each group independently, with the same algorithm it uses for a target's execution platform. It picks the first registered execution platform that satisfies the group's constraints and on which every dep of the group is compatible, or uses the fallback. Only the inputs differ:
| Group | Constraints | Deps |
|---|---|---|
| Default | The target's exec_compatible_with, plus its attrs.exec_compatible_with(exec_group = DefaultExecGroup) | Exec deps, toolchain deps and arg tools in the default group, and plugins |
Named group G | The target's exec_compatible_with, plus its attrs.exec_compatible_with(exec_group = G) | Only exec deps, toolchain deps and arg tools with exec_group = G |
- The deps of one group do not affect any other group, including the default group.
- A group does not prefer the default group's platform. A named group already
gets the target's
exec_compatible_with, but not the default group's deps. A group that must land on the same platform as the default group needs its own constraints (for example itsattrs.exec_compatible_with(exec_group = ...)) or deps that rule out the other platforms. Otherwise it takes the first compatible registered platform. - Every declared group is resolved, even one with no deps.
- A rule without named groups resolves a single execution platform, as described in Configurations.
- When
build.execution_platformsis not set, every group gets the legacy execution platform, the same one as the target.
When no platform fits a group, the group follows the fallback of the
ExecutionPlatformRegistrationInfo that registers the execution platforms, as
the default group does:
fallback = "error": the target fails to configure, with an error that names the group.fallbackset to anExecutionPlatformInfo: the group uses that platform.- No
fallback, orfallback = "use_unspecified": the group has no platform.buck.exec_group_platformsshows<NONE>for it, and the target still configures and builds. An action or anon target in the group fails at the call, and a test in the group fails when it runs. The group's deps are configured without an execution platform, so a dep that checks its configuration, for example withtarget_compatible_with, fails to configure.
Example
A rule that compiles in the default group and links in a Link group:
# my_binary.bzl
Link = exec_group()
def _my_binary_impl(ctx):
obj = ctx.actions.declare_output("main.o")
ctx.actions.run(
cmd_args(ctx.attrs._compiler[RunInfo], ctx.attrs.src, obj.as_output()),
category = "compile",
)
out = ctx.actions.declare_output(ctx.label.name)
ctx.actions.run(
cmd_args(ctx.attrs._linker[RunInfo], obj, out.as_output()),
category = "link",
exec_group = Link,
)
return [DefaultInfo(default_output = out)]
my_binary = rule(
impl = _my_binary_impl,
attrs = {
"link_exec_compatible_with": attrs.exec_compatible_with(exec_group = Link, default = []),
"src": attrs.source(),
"_compiler": attrs.exec_dep(default = "//tools:compiler", exec_group = DefaultExecGroup),
"_linker": attrs.exec_dep(default = "//tools:linker", exec_group = Link),
},
exec_groups = [DefaultExecGroup, Link],
)
Targets do not need to know about groups. They only set the attributes the rule exposes:
# BUCK
load(":my_binary.bzl", "my_binary")
my_binary(
name = "app",
src = "main.c",
)
my_binary(
name = "app_local_link",
src = "main.c",
# Only the link must run on a local platform.
link_exec_compatible_with = ["//config:local"],
)
Inspecting groups
The output below comes from the example above, with two registered execution platforms and a compiler that is compatible only with remote execution:
# config/BUCK
load(":defs.bzl", "constraint_setting", "constraint_value", "platform", "platforms")
constraint_setting(name = "os")
constraint_value(name = "linux", setting = ":os")
constraint_setting(name = "executor")
constraint_value(name = "local", setting = ":executor")
constraint_value(name = "remote", setting = ":executor")
platform(name = "linux_local", configuration = [":linux", ":local"])
platform(name = "linux_remote", configuration = [":linux", ":remote"], remote = True)
# Resolution picks the first platform that fits, so the order matters.
platforms(name = "platforms", platforms = [":linux_remote", ":linux_local"])
# tools/BUCK
load(":defs.bzl", "tool")
tool(name = "compiler", target_compatible_with = ["//config:remote"], visibility = ["PUBLIC"])
tool(name = "linker", visibility = ["PUBLIC"])
# .buckconfig
[build]
execution_platforms = root//config:platforms
These are not prelude rules. constraint_setting, constraint_value,
platform and platforms come from a small local config/defs.bzl, like the
one in Buck2's
execution group tests.
platform returns an ExecutionPlatformInfo and a PlatformInfo for its
constraints. Its remote attribute is a test helper: remote = True gives the
platform a remote-only executor, and the default is a local-only one.
platforms returns an ExecutionPlatformRegistrationInfo. tool comes from a
local tools/defs.bzl and is a rule that returns a RunInfo:
tool = rule(impl = lambda ctx: [DefaultInfo(), RunInfo(args = cmd_args("true"))], attrs = {})
buck2 cquery shows the platform of each named group in
buck.exec_group_platforms, or <NONE> for a group that has no platform. The
attribute is present only for rules that declare named groups.
buck.execution_platform is still the platform of the default group.
$ buck2 cquery //app:app_local_link --target-platforms //config:linux_local --output-attribute 'buck.execution_platform|buck.exec_group_platforms'
{
"root//app:app_local_link (root//config:linux_local#...)": {
"buck.execution_platform": "root//config:linux_remote",
"buck.exec_group_platforms": {
"Link": "root//config:linux_local"
}
}
}
buck2 aquery shows the group of each action in buck.exec_group, as
DefaultExecGroup or the group's name. Only actions of rules that declare named
groups have this attribute.
$ buck2 aquery 'deps(//app:app_local_link)' --target-platforms //config:linux_local --output-attribute 'category|buck.exec_group'
{
...
"(target: `root//app:app_local_link (root//config:linux_local#...)`, id: `0`)": {
"category": "compile",
"buck.exec_group": "DefaultExecGroup"
},
"(target: `root//app:app_local_link (root//config:linux_local#...)`, id: `1`)": {
"category": "link",
"buck.exec_group": "Link"
}
}
buck2 audit execution-platform-resolution prints one section per group, with
that group's platform, exec deps, toolchain deps, and the platforms it skipped
and why. The default group's section is headed Execution platform:, and each
named group's section is headed Execution group `G`:. Use it when a group
resolves to an unexpected platform or to none.
$ buck2 audit execution-platform-resolution //app:app_local_link --target-platforms //config:linux_local
Checking each target against execution platforms defined by root//config:platforms
root//app:app_local_link (root//config:linux_local#...):
Execution platform: root//config:linux_remote
Execution platform configuration: root//config:linux_remote#...
Execution deps:
root//tools:compiler (root//config:linux_remote#...)
Toolchain deps:
Configuration deps:
Execution group `Link`: root//config:linux_local
Execution deps:
root//tools:linker (root//config:linux_local#...)
Toolchain deps:
Skipped root//config:linux_remote
exec_compatible_with requires `root//config:local` but it was not satisfied
Limitations
- Buck2 does not check that an action only runs tools from its own group. An action that runs a tool built for another group's platform may fail when it executes.
- A toolchain dep joins a group as a whole. Every exec dep of the toolchain constrains that group's platform, even tools the group never runs.
- Toolchain rules and configuration rules cannot declare execution groups, not
even
[DefaultExecGroup]. The exec deps of a toolchain belong to the group that the depending rule puts the toolchain in. - A rule that lists only named groups still resolves a default platform, and
buck.execution_platformstill shows it. No action, test info or anon target can run there; see Running actions in a group for the actions that are registered there anyway.