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:
- Where does the tool execute? Its executable and runtime inputs must work on the selected execution platform.
- Which target platform does it produce for? Its output must match the target platform's declared compatibility dimensions.
- 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_type → toolchain() candidates bind implementations and constraints → registration creates an ordered candidate list → analysis selects an execution platform and implementations for mandatory types → ctx.toolchains[type] receives ToolchainInfo → actions run on the selected execution platform.
toolchain() wrapper connects the type, an implementation target, and the platform constraints it supports.GLYPH_TOOLCHAIN_TYPE = Label("//glyph/toolchains:toolchain_type") . The type target itself does not define payload fields.ToolchainInfo. It does not decide whether it fits the current platforms.--extra_toolchains
2. Root module registrations
3. Dependency module registrations
toolchain() target labels from flags and the resolved module graph.GLYPH_TOOLCHAIN_TYPEToolchainInfo without naming the concrete compilerctx.toolchains[GLYPH_TOOLCHAIN_TYPE] receives the selected implementation's provider.mandatory = False, an unresolved request may yield None.TYPE='//glyph/toolchains:toolchain_type'bazel build //app:server \ --toolchain_resolution_debug="$TYPE"bazel cquery \ 'deps(//app:server, 1)' \ --transitions=lite4.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.
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.
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.
Sections in this chapter · 7
Reframing Level 3 platform basics from the perspective of rule authors and tool resolution.
Declaring toolchain types, publishing concrete toolchains, and reading resolved ToolchainInfo.
How Bazel matches toolchain types against target and execution platform constraints.
cfg = "exec")Switch a dependency from target to execution configuration for build-time tools.
Per-action execution platform selection within a single rule, plus when automatic execution groups are enough.
Extending the platform model when OS and CPU from @platforms are not enough.
Read-only marker Bazel attaches to incompatible configured targets. Detectable via cquery.