4.6 Toolchains & Platform Resolution

Why the BUILD target does not name a compiler

A BUILD author should be able to ask for a binary without choosing the exact compiler that produces it. The target names sources, dependencies, and other properties of the output. It does not need to know whether the compiler comes from the root repository, a language ruleset, or a platform-specific SDK.

The rule implementation should not hard-code that choice either. A compiler that runs on a Linux x86-64 worker might produce a binary for Linux ARM64. Another registered implementation might serve a developer building the same target locally. Bazel therefore separates what the output is for, the target platform, from where the build-time tool runs, the execution platform. The rule asks for a kind of compiler, and analysis chooses an implementation that fits both sides.

That separation is necessary, but an artifact-validity contract has three parts:

  1. Where does the tool execute? Its executable and runtime inputs must work on the selected execution platform.
  2. Which target platform does it produce for? Its output must match the target platform's declared compatibility dimensions.
  3. Where must the result actually run? The deployed runtime must satisfy every relevant part of that target contract, including dimensions that the build models outside the basic OS and CPU vocabulary.

The third question prevents a successful action from being mistaken for proof that its output is usable. Toolchain resolution can select a compiler that runs on one platform and targets another, but the ruleset still has to model the compatibility dimensions that matter and the product still needs target-side evidence.

C++ and Python expose the same boundary through different language contracts. A C++ toolchain's compiler may execute on Linux x86-64 while its sysroot, startup objects, and libc target Linux ARM64. The resulting binary must then load and run in that target runtime. L2.4 Cross-Compilation & Targets develops that C++ sysroot/libc contract. A native Python package builder may also execute on one platform while producing a wheel for another, but that wheel must match the target interpreter's Python ABI and platform tags and must import in the deployed runtime. L3.9 Native Python Dependencies & Wheel Compatibility develops wheel selection, source builds, repair, and runtime import checks. Those packaging mechanics remain language-specific. The shared platform model supplies the three questions used to reason about both.

A toolchain is a contract plus replaceable implementations

The stable public contract is a toolchain_type target. A consuming rule requests that type instead of depending on one compiler label. Competing implementation rules return the compiler executable, files, flags, and helpers in platform_common.ToolchainInfo.

An implementation does not become a resolution candidate by itself. A toolchain() target binds three facts together: the requested type, the implementation target that provides ToolchainInfo, and the constraints describing where that implementation can run and what it can build for. Several toolchain() targets can bind different implementations to the same type without changing the consuming rule.

Registration makes implementations eligible

Defining a toolchain() target only creates a possible candidate. Registration makes that target eligible for resolution and gives it a deterministic position in the ordered candidate list. There is no user-visible global toolchain registry analogous to the Bazel Central Registry: Bazel assembles the list for the current build from registration directives and command-line inputs.

For Bazel 9, the precedence is --extra_toolchains candidates first, then register_toolchains() candidates from the root module, then registrations from dependency modules in the resolved Bzlmod graph. Where a toolchain is physically defined is a separate question: the target may live in the root repository, a dependency ruleset, or a repository generated by a module extension. In every case, the toolchain() target must enter the ordered candidate list before analysis can select it.

What Bazel does during analysis

For the compiler rule, analysis combines four inputs: the requested toolchain_type, the ordered registered candidates, the target platform, and the available execution platforms. Bazel selects one execution platform and one compatible implementation for each mandatory type. An optional type (mandatory = False) may remain unresolved. In that case, ctx.toolchains[type] is None. Each selected implementation becomes a configured-target dependency, so ctx.toolchains[type] receives its ToolchainInfo. Actions registered by the rule then run on the selected execution platform.

The complete path is:

rule requests toolchain_typetoolchain() candidates bind implementations and constraintsregistration creates an ordered candidate listanalysis selects an execution platform and implementations for mandatory typesctx.toolchains[type] receives ToolchainInfoactions run on the selected execution platform.

How does Bazel turn “I need a compiler” into the right tool?
Follow one request from a stable toolchain type to the selected compiler and execution platform.
BUILD target//app:serverDoes not name a compiler.
Output is forLinux ARM64This is the target platform.
Build tools (for example, the compiler) can run onLinux x86-64This is the available execution platform in this example.
1 · PUBLISH THE CONTRACT AND IMPLEMENTATIONS
The ruleset defines one stable type and replaceable candidates
A toolchain() wrapper connects the type, an implementation target, and the platform constraints it supports.
toolchain_type(name = "toolchain_type")
glyph_toolchain(
name = "x86_to_arm_toolchain_impl",
compiler = ":glyphc",
)
toolchain(
name = "x86_to_arm_toolchain",
toolchain_type = GLYPH_TOOLCHAIN_TYPE,
toolchain = ":x86_to_arm_toolchain_impl",
exec_compatible_with = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
target_compatible_with = [
"@platforms//os:linux",
"@platforms//cpu:arm64",
],
)
TypeA stable label shared by toolchain producers and consuming rules. Here GLYPH_TOOLCHAIN_TYPE = Label("//glyph/toolchains:toolchain_type"). The type target itself does not define payload fields.
ImplementationA rule target that returns the compiler and other tool data inside ToolchainInfo. It does not decide whether it fits the current platforms.
WrapperA selectable candidate that connects the type, implementation, and supported platforms. Defining it does not yet register or select it.
2 · REGISTER CANDIDATES FOR THIS BUILD
Registration creates an ordered eligibility list
There is no permanent user-visible global registry. Bazel assembles this view for the current invocation.
register_toolchains(
"//glyph/toolchains:x86_to_arm_toolchain",
)
Candidate priority, highest first 1. Candidates from --extra_toolchains 2. Root module registrations 3. Dependency module registrations
InputRegistered toolchain() target labels from flags and the resolved module graph.
EffectThe wrapper becomes eligible and receives a deterministic position.
Not yetNo compiler is selected or executed during registration.
3 · RESOLVE DURING ANALYSIS
Bazel matches the requested type against platforms and ordered candidates
The concrete example keeps the target and execution checks separate.
Requested capabilityGLYPH_TOOLCHAIN_TYPE
Output must run onLinux ARM64 target platform
Available execution platformsOnly Linux x86-64 in this example
Candidates are checked intheir registered order
arm_native_toolchain Compiler runs on ARM64 and produces ARM64 output. Execution compatibility: FAIL + Target compatibility: PASS REJECT
x86_to_arm_toolchain Compiler runs on x86-64 and produces ARM64 output. Execution compatibility: PASS + Target compatibility: PASS SELECT
ResultBazel chooses a compatible execution platform and implementation, considering candidate ordering.
GraphThe selected implementation becomes a configured dependency of the consuming target.
4 · DELIVER THE SELECTED PAYLOAD
The rule reads ToolchainInfo without naming the concrete compiler
Actions use the selected payload and run on the execution platform chosen during resolution.
tc = ctx.toolchains[GLYPH_TOOLCHAIN_TYPE]
ctx.actions.run(
executable = tc.compiler,
...
)
Payloadctx.toolchains[GLYPH_TOOLCHAIN_TYPE] receives the selected implementation's provider.
Action runs onLinux x86-64.
Artifact is forLinux ARM64.
Optional typeWith mandatory = False, an unresolved request may yield None.
Inspect the decision
Why accepted or rejected?
TYPE='//glyph/toolchains:toolchain_type'bazel build //app:server \ --toolchain_resolution_debug="$TYPE"
Which configured dependency?
bazel cquery \ 'deps(//app:server, 1)' \ --transitions=lite
Mental model: publish replaceable implementations → register eligible wrappers for this build → resolve during analysis → deliver ToolchainInfo to the rule and run its actions on the selected execution platform.

4.6.2 Defining, Registering & Accessing Toolchains turns this model into the concrete producer-and-consumer Starlark workflow. When you need to understand compatibility filtering, ordering, and the exact selection algorithm, continue with 4.6.3 Toolchain Resolution.

The inputs a build can control

Each resolution input answers a different question:

  • The rule's requested toolchain types say which contracts its implementation needs.
  • The target platform says what the output is for. The top-level build selects it, commonly through --platforms.
  • Registered execution platforms say where build-time tools and actions are allowed to run.
  • Registered toolchain() candidates say which implementations are eligible. Their target and execution constraints say which platform pairs they support.

These are declarative inputs to analysis, not platform branches hidden inside the rule implementation. Changing registrations or the target platform can select a different compiler while the BUILD target and consuming rule remain unchanged.

This visibility matters. If a rule hides tool selection in if linux branches or hard-coded labels, Bazel cannot use that choice to place actions, remote workers cannot match it as a platform requirement, and users cannot replace the tool without changing the rule. Toolchains make the choice part of the build graph.

When this model needs to grow

If a rule owns one fixed implementation helper and never needs users to replace it, a private executable attribute is often enough. Building that helper with cfg = "exec" puts it on the execution side. 4.6.4 Execution Configuration for Tools (cfg = "exec") shows the complete pattern.

The base model gives a configured target one selected execution platform. If its compile, signing, packaging, or testing actions need different machines, 4.6.5 Execution Groups & Auto Exec Groups explains how to give those action groups separate placement requirements.

Start with the shared OS and CPU vocabulary from @platforms. If compatibility also depends on a real dimension such as CUDA capability, libc family, or SDK generation, 4.6.6 Custom Constraints & Custom Platforms shows how to model that dimension explicitly. If you encounter Bazel's incompatibility marker while inspecting configured targets, 4.6.7 IncompatiblePlatformProvider explains why it is read-only and how compatibility should be declared instead.

When resolution finds no implementation or chooses a surprising one, the complete --toolchain_resolution_debug investigation is in 5.6.4 Toolchain Resolution Debugging. The diagnostic starts from the same four inputs shown above. It does not require a different mental model.

A published ruleset may need many platform-specific declarations without putting every SDK payload in one repository. A lightweight, often generated hub repository can contain the toolchain() declarations while the heavy SDK repositories stay separate. 4.11.2 Toolchainization develops that distribution pattern.

think

Choose the mechanism: A rule needs (1) one fixed helper, (2) a compiler that users can replace, and (3) compile and signing actions that must run on different machines. Which Bazel mechanism fits each case?

Reveal

Use a private executable attribute with cfg = "exec" for the fixed helper. Use a toolchain for the replaceable compiler. Use execution groups when actions from one target need different execution platforms.

The mini-ruleset is a runnable companion. Its toolchain definition and platform declarations show the same model in a small build.

key takeaway

A consuming rule requests a stable toolchain_type. Registered toolchain() candidates connect that contract to replaceable implementations and their platform constraints. During analysis, Bazel combines the requested types, ordered candidates, target platform, and execution platforms. It selects an execution platform and an implementation for each mandatory type, injects each selected implementation's ToolchainInfo through ctx.toolchains[type], and places the rule's actions on the selected execution platform. An optional type may instead produce None.

For every selected tool, keep three questions together: where the tool executes, which target platform it produces for, and where the result must run. A completed action answers only the production part. Artifact validity also depends on the target runtime contract and target-side evidence.