4.6.1 Platform Model for Rule Authors
Level 3 introduced platforms as something a BUILD author can select on in 3.3.1 Configurable Attributes (select()), declare compatibility against in 3.3.4 Target Compatibility, and override with --platforms. For a rule author, the same vocabulary answers a different question: which tools should this rule receive, and where should the actions it creates run? The target platform describes the output you are producing. The execution platform describes the machine that runs the compiler, generator, packager, or test action that produces it.1
toolchains = [GLYPH_TOOLCHAIN_TYPE]The rule asks for a compiler interface, not one fixed compiler label.target_compatible_with constraints must match this platform.
exec_compatible_with constraints must match this platform.
exec_compatible_with is checked against Linux x86-64.target_compatible_with is checked against Linux ARM64.Same Words, Different Responsibility
The BUILD-author view from 3.3.2 Platform Vocabulary is still the foundation. Bazel recognizes the host platform where Bazel itself runs, execution platforms where actions run, and target platforms where built code is meant to run.1 A platform is a collection of constraints, and constraints are modeled with constraint_setting and constraint_value, as introduced in 3.3.3 Constraint Values.2
The responsibility changes at Level 4. A maintainer using select() asks, "which source file or dependency should this target use for the chosen target platform?" A rule author asks, "which build-time tools are valid for this target platform, and which of those tools can run on the selected execution platform?" That is the mental move that turns platforms from a configuration feature into a rule-design boundary.
The --platforms flag still sets the target platform for the top-level request, as covered in 3.3.5 --platforms Flag. If the flag is not set, Bazel defaults to @platforms//host, a platform that detects the OS and CPU of the machine running Bazel.3 Rule authors should not treat that default as "the local machine always builds everything." Remote execution, cross-compilation, and toolchain resolution can separate where Bazel runs, where actions run, and what the final artifact targets.4
Why Private Tools Stop Scaling
A small rule can use a private executable attribute for an implementation helper:
my_rule = rule(
implementation = _my_rule_impl,
attrs = {
"_compiler": attr.label(
default = Label("//tools:compiler"),
executable = True,
cfg = "exec",
),
},
)
This pattern is valid when the rule has a fixed helper in the same repository as the rule implementation. cfg = "exec" says the helper should be built for the execution side, because the action needs to run it during the build.5 4.6.4 Execution Configuration for Tools (cfg = "exec") covers the mechanics of private executable tool attributes. Here the point is why platform-varying tools graduate to toolchains. The rules documentation draws the boundary this way: same-repository implicit tools are fine, but tools supplied by the execution platform or by another repository should come through a toolchain.6
The hardcoded-tool pattern breaks down when the tool varies by OS, CPU, SDK, compiler version, or remote worker image. Imagine a language rule that hardcodes a //compiler:glyphc_linux compiler: it cannot work cleanly when different targets need different compilers for what they build for and what they build on. That is the failure mode the toolchains documentation teaches with its own example.7 Moving from an attribute to a toolchain changes the dependency from "this exact label" to "some implementation of this toolchain type that matches the current platforms."8
The Two Platform Checks
A toolchain target has two different compatibility surfaces:
load("//glyph/toolchains:toolchain.bzl", "GLYPH_TOOLCHAIN_TYPE")
toolchain(
name = "linux_x86_64_toolchain",
exec_compatible_with = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
target_compatible_with = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
toolchain = ":source_toolchain_impl",
toolchain_type = GLYPH_TOOLCHAIN_TYPE,
)
target_compatible_with describes the platforms the toolchain can produce outputs for. exec_compatible_with describes the platforms where the toolchain's tools can run.9 In a local single-platform build, those may happen to look the same. In a cross-compilation or remote-execution build, they are separate facts.
That separation is the core of the rule-author platform model. The final binary may target Linux ARM64. The compiler that produces it may need to run on a Linux x86_64 remote worker. A signing action may need a specific local execution platform because its signing tool or credential access cannot yet be modeled remotely. That should be declared as an intentional execution requirement, not hidden state. The rule implementation cannot reduce those decisions to one string like "linux" without losing information. It needs to tell Bazel which toolchain type it requires, then let resolution find the compatible implementation.
What Bazel Resolves For A Rule
When a rule declares toolchains, Bazel's toolchain resolution procedure receives the required toolchain types, the target platform, the available execution platforms, and the registered toolchains. It returns a concrete toolchain for each requested type and an execution platform for the configured target.10
That result becomes available during analysis:
load("//glyph/toolchains:toolchain.bzl", "GLYPH_TOOLCHAIN_TYPE")
glyph_library = rule(
implementation = _compile_impl,
attrs = {"srcs": attr.label_list(allow_files = [".glyph"])},
toolchains = [GLYPH_TOOLCHAIN_TYPE],
)
def _compile_impl(ctx):
toolchain = ctx.toolchains[GLYPH_TOOLCHAIN_TYPE].glyph
# Use the selected ToolchainInfo to register actions.
This is the real shape of the rules_glyph compile rule: it declares one toolchain type and reads the resolved toolchain in its implementation. The mechanics of defining the toolchain_type, returning platform_common.ToolchainInfo, registering concrete toolchains, and reading ctx.toolchains belong in 4.6.2 Defining, Registering & Accessing Toolchains, which walks that same file end to end. This article's narrower point is the model behind that API: the rule asks for an interface, and Bazel injects the compatible implementation for the current target/execution pair.11
By default, the chosen execution platform runs the actions the target generates.10 Later, 4.6.5 Execution Groups & Auto Exec Groups relaxes that single-execution-platform shape for rules that need different action groups to run in different places. Keep the base model simple first: one configured target, one target platform, one selected execution platform, and resolved toolchains that fit both.
Use Standard Constraints First
Most rules should start with the standard @platforms constraints for OS and CPU, not custom dimensions. The platform docs describe @platforms as the shared repository for common CPU and operating-system constraints, and the migration guide recommends common cross-language properties such as OS and CPU live there for ecosystem compatibility.12
Custom constraints are still a real extension point. A ruleset may eventually need to model CUDA capability, libc variant, SDK family, hardware generation, or another domain-specific fact. That is 4.6.6 Custom Constraints & Custom Platforms territory. Reach for it after the rule has a clear target-vs-execution story with standard constraints.
The same restraint applies to debugging. If resolution surprises you, --toolchain_resolution_debug can show which toolchains Bazel checked and skipped.13 But detailed resolution debugging belongs later in 4.6.3 Toolchain Resolution, 5.2.2 bazel cquery — Configured Graph, and 5.2.4 bazel config — Configuration Inspection. At this point, the useful design habit is simpler: whenever a rule needs a build-time tool, ask whether the tool is fixed, or whether it should be selected from a platform-aware toolchain.
A concrete vendor-specific reference is the
BuildBuddy toolchain map: its
templates/BUILD.tpl
generates platform constraints and remote-executor properties together, while
examples/bzlmod/
shows automatic C++ toolchain registration and an explicit
--extra_execution_platforms opt-in for execution platforms. Use it to inspect the layer boundary, not as a
universal RBE configuration: worker images, execution platforms, and language
toolchains remain distinct contracts.
A rule author sees platforms as a contract around tools and actions.
The target platform says what the output is for. The execution platform says where the tool runs. Toolchain resolution is the bridge: a rule asks for a toolchain type, and Bazel selects an implementation that can run on the execution platform and produce for the target platform.
Check your understanding · 4 questions
1.Match each platform role to the question it answers for a rule author.
Drag each answer onto the matching prompt, or click an answer and then click a prompt
2.When should a rule author prefer toolchain resolution over a private cfg = "exec" tool attribute?
Select one answer
3.True or false: toolchain resolution for rule authors.
Choose True or False for each sentence
target_compatible_with describes what platforms it can produce outputs for.exec_compatible_with describes where its tools can run.ctx.toolchains.4.Why is hard-coding a fixed compiler label inside a rule usually the wrong abstraction?
Select one answer
Footnotes
-
Platforms — host, execution, and target platform roles. ↩1 ↩2
-
Platforms — platform as a collection of constraints, modeled by
constraint_settingandconstraint_value. ↩ -
Platforms —
--platformsusage and default@platforms//hostbehavior. ↩ -
Platforms — single-platform, cross-compilation, and multi-platform build scenarios. ↩
-
Rules —
cfg = "exec"builds build-time tools for the execution platform. ↩ -
Rules — tools from the execution platform or another repository should come from a toolchain. ↩
-
Toolchains — hardcoded compiler labels fail when tools vary by target and execution platform. ↩
-
Toolchains — toolchain types let rules request an abstract tool family that Bazel resolves to a concrete toolchain. ↩
-
Migrating to Platforms — toolchains declare both
target_compatible_withandexec_compatible_withconstraints. ↩ -
Toolchains — toolchain resolution inputs, outputs, and selected execution platform. ↩1 ↩2
-
Writing Bazel rules: platforms and toolchains — toolchain type as interface, rule as consumer, and
ToolchainInfoas resolved implementation data. ↩ -
Migrating to Platforms — common platform properties should use
@platforms. Rule-specific properties belong to the ruleset. ↩ -
Toolchains —
--toolchain_resolution_debugreports toolchains checked and skipped during resolution. ↩