4.6.3 Toolchain Resolution

Toolchain resolution is the analysis-phase algorithm that turns a rule's declared toolchain type into one concrete ToolchainInfo provider for the current target. The rule says "I need a //glyph/toolchains:toolchain_type" and Bazel picks the registered toolchain() whose constraints match both the target platform and an available execution platform.1 The framework from 4.6.2 Defining, Registering & Accessing Toolchains sets the table. Resolution decides who sits at it.

This is where the target-vs-execution split from 4.6.1 Platform Model for Rule Authors becomes mechanical. A toolchain that can target the requested platform but cannot run on any available executor is rejected. So is a toolchain that runs everywhere but does not produce code for the requested target. Rule authors do not call this algorithm. They design rules that depend on its outcome.

What Goes In

Resolution takes four inputs per target:2

  • the set of toolchain types the rule declared in rule(toolchains = [...]),
  • the target platform (set by --platforms or inherited),
  • the list of available execution platforms,
  • the list of available toolchain() candidates for those types.

Available execution platforms and toolchains are gathered from register_execution_platforms() and register_toolchains() in the transitive MODULE.bazel graph, plus --extra_execution_platforms and --extra_toolchains on the command line. The host platform is always included as an execution platform candidate.2

A rule's exec_compatible_with (on the rule definition or the target) further filters available execution platforms before resolution starts. Toolchains that declare target_settings are dropped if those settings do not match the current configuration.3

Order is part of the input. In Bazel 9, Bazel builds one ordered candidate list from these sources:2

  1. --extra_toolchains candidates have highest priority. The last repeated flag entry wins.
  2. register_toolchains() entries come from the resolved Bzlmod graph.
  3. Root-module registrations precede dependency-module registrations.
  4. Within register_toolchains() ordering, the first compatible candidate wins.
  5. Expanded target patterns are deterministic: toolchains in subpackages register before those in the parent package, and targets within one package register in lexicographic order.2

The place a toolchain() target is physically defined does not determine this priority. Its registration entry mechanism does: a target defined in a dependency repository can still enter first through the root module's register_toolchains(), for example. Bazel does not expose a browsable global registry. It assembles this ordered candidate list as an input to resolution. If you ask "why did Bazel pick this toolchain?", start with the list and each entry's provenance.

extra

Bazel 8 and older with legacy WORKSPACE enabled. The older ordering also inserted registrations from the user's WORKSPACE, followed after dependency modules by a Bazel-supplied WORKSPACE suffix. That suffix was Bazel-owned code evaluated after the user's file to define or register defaults for bundled rules. It was not another file the user maintained. Bazel 8 disabled WORKSPACE by default, and Bazel 9 removed WORKSPACE support, including this suffix.4

The Two-Pass Algorithm

Resolution is presented as a single step, but it splits cleanly into two passes that are easier to reason about:5 one toolchain type at a time, then one target at a time.

Toolchain resolution is ordered filtering — not “best fit” scoring
For each requested type, Bazel maps compatible candidates to execution platforms. It then chooses one platform that satisfies the target’s complete toolchain request.
1 · Gather inputsRequested types, target platform, ordered execution platforms, ordered registered candidates, and current configuration.
2 · Filter candidatesKeep the right type and matching target_settings. Reject candidates whose target constraints do not match the target platform.
3 · Assign per executorFor each execution platform, keep the first candidate whose execution constraints match. Repeat independently for every type.
4 · Pick one platformDrop platforms that are missing a mandatory type. Prefer the platform with more resolved optional types. If several remain, use execution-platform order to break the tie.
Candidate order drives step 3--extra_toolchains → root registrations → dependency registrations. First compatible candidate wins per execution platform.
Execution-platform order drives step 4Registration order breaks a tie after mandatory coverage and optional-toolchain count have been compared.
No matching toolchains?Check registration → type → target_settings → target constraints → execution constraints. Run --toolchain_resolution_debug='//path:type'. Then confirm the chosen dependency with cquery --transitions=lite.
Result: analysis receives one execution platform and its selected ToolchainInfo values. A missing mandatory type stops analysis. An unmatched optional type becomes None.

First Pass: Per-Execution-Platform Assignment

For one toolchain type, filter the ordered candidate list to registered toolchain() targets that declare that type, preserving order. Reject a candidate that cannot target the target platform. For each remaining candidate, scan execution platforms. If a platform has no assignment yet and the candidate can run there, assign its toolchain implementation target. The result is a map: {exec_platform → implementation target} for that single type.5 First compatible candidate wins per execution platform. Repeat the same pass independently for every requested type.

The matching rule is straightforward: a target_compatible_with or exec_compatible_with clause matches a platform when every constraint_value in the clause is also on the platform. Constraint values that the platform has from settings the clause does not mention are ignored.6

Selection Stage: Pick One Execution Platform

A target may need several toolchain types (e.g., a Java toolchain plus a runtime), some mandatory and some optional via config_common.toolchain_type(..., mandatory = False).7 The selection stage picks one execution platform that satisfies every mandatory type and gets the best optional coverage.

Drop any execution platform that failed to find a mandatory toolchain in the first pass. Among the survivors, prefer the platform with the most resolved toolchain types. Because all survivors have the mandatory types, optional toolchains decide the count. Execution-platform registration order breaks ties.8 That platform becomes the target's execution platform, and its associated toolchains become the target's dependencies.

The chosen execution platform is then used for every action the target generates — unless the rule uses 4.6.5 Execution Groups & Auto Exec Groups, in which case each exec group runs both resolution stages independently and ends up with its own execution platform and toolchain set.9

Order Matters In Practice

Two different lists drive the two stages. The first pass walks the ordered candidate list assembled from --extra_toolchains and the resolved Bzlmod graph. The selection stage preserves execution-platform registration order (from register_execution_platforms() and --extra_execution_platforms).5 Mixing them up is the most common reason "the same MODULE.bazel picks differently than I expected."

A practical consequence: if multiple toolchains for the same type can target the same platform and run on the same executor, the first compatible candidate in the ordered candidate list wins — there is no "best fit" scoring. Rulesets that want a deterministic default usually publish their toolchains in a fixed order from a single register_toolchains() call. The rules_glyph MODULE.bazel is the minimal end of that spectrum: one registered toolchain() that resolves cleanly for the host — the success case behind the failure snippet below.

When Resolution Fails

Resolution failure is an analysis-phase error, not a configuration warning. A mandatory toolchain type with no compatible candidate halts analysis with a message like this:10

Won't build
demo_rule(
    name = "demo",
)
ERROR: BUILD.bazel:3:10: While resolving toolchains for target //:demo (680dcaf): No matching toolchains found for types:
  //toolchain:demo_toolchain_type
To debug, rerun with --toolchain_resolution_debug='//toolchain:demo_toolchain_type'

That output comes from a workspace that defines a rule with toolchains = ["//toolchain:demo_toolchain_type"] but never registers any concrete toolchain — reproduce it with snippets/toolchain-resolution-error. The fix is almost always one of three things: register the toolchain, relax exec_compatible_with / target_compatible_with, or declare the toolchain optional.7

Reproduce this error

Optional toolchains keep analysis alive: when no candidate matches, ctx.toolchains["//pkg:toolchain_type"] returns None instead of failing.7 That is the right call when a rule can degrade gracefully and the wrong call when the missing tool would cause a confusing failure later.

Debugging A Real Resolution

Three tools cover most resolution debugging:

--toolchain_resolution_debug=<regex> prints what Bazel considered. The flag takes a regex matched against toolchain types and target labels. .* prints everything.11 Bazel 7 restructured this output: one message per algorithm invocation, indentation that mirrors the loop, and early stopping when every execution platform has an assignment.5 The same flag now produces something readable instead of an expert-only wall of text.

For the failing snippet above, --toolchain_resolution_debug='//toolchain:demo_toolchain_type' shows the per-platform check directly:

INFO: ToolchainResolution: Performing resolution of //toolchain:demo_toolchain_type for target platform @@platforms//host:host
      ToolchainResolution: No //toolchain:demo_toolchain_type toolchain found for target platform @@platforms//host:host.

bazel cquery 'deps(//x:y, 1)' --transitions=lite answers "which of these dependencies came from toolchain resolution?" The transitions output tags toolchain dependencies and shows the configuration each one runs under, separating implicit toolchain edges from rule attributes you wrote.12 5.2.2 bazel cquery — Configured Graph is the dedicated chapter on the tool itself. Here it is the most direct way to see what the selection stage actually picked.

bazel config <hash> inspects the resolved configuration of a specific configured target — useful when two configurations of the same toolchain show up in cquery output and you need to know what differs between them. 5.2.4 bazel config — Configuration Inspection is the home of that workflow.

These commands are a starting point, not a complete diagnosis. 5.6.4 Toolchain Resolution Debugging gives the complete workflow for keeping one trace attached to the right configured target, comparing constraints on the correct axis, and confirming the resolved dependency.

Why The Indirection Is Worth It

Toolchain resolution is the mechanism that lets P.2.3 Core vs Rulesets actually work. Bazel Core never hardcodes which compiler runs for a given language. It gives rule authors an interface, gives users a registration list, and resolves the two at analysis time based on platform constraints. The selected toolchain is an implicit configured-target dependency in analysis, so rule implementations do not enumerate candidates and BUILD target authors do not choose a concrete implementation. Its files become action inputs or tools only when the rule registers an action that supplies them. That action declaration is what ordinary caching and remote execution see.

That indirection is also why "no matching toolchains found" is usually a registration or constraint problem rather than a code problem. The algorithm itself is small, deterministic, and order-driven. Once you can read the debug output for one execution platform, the rest scales.

key takeaway

Toolchain resolution is a deterministic, order-sensitive lookup, not a heuristic. The first pass picks one toolchain per execution platform from the ordered candidate list. The selection stage drops platforms missing required types, prefers the survivor with the most optional matches, and uses execution-platform registration order as the tie-breaker.

When it fails, the answer is almost always in the candidate list, the constraints, or the optionality flag — and --toolchain_resolution_debug will show which one.

Check your understanding · 4 questions

1.What drives the first pass of toolchain resolution (per-execution-platform assignment for one toolchain type)?

Select one answer

2.Which statements about the execution-platform selection stage and candidate-order precedence are correct?

Select all that apply

3.True or false: how does Bazel handle missing toolchains?

Choose True or False for each sentence

A mandatory toolchain type with no matching candidate halts analysis with a 'No matching toolchains found' error.
An optional toolchain (config_common.toolchain_type(..., mandatory = False)) returns None from ctx.toolchains[type] when nothing matches.
Bazel will silently fall back to the host platform's compiler when no registered toolchain matches.
If a rule uses execution groups, every group shares the same execution platform decision, so one missing toolchain affects all groups.

4.Match each debugging tool to what it answers:

Drag each answer onto the matching prompt, or click an answer and then click a prompt

Answers
--toolchain_resolution_debug=<regex>
bazel cquery 'deps(//x:y, 1)' --transitions=lite
bazel config <hash>
0 of 4 answered

Footnotes

  1. Toolchainsctx.toolchains[toolchain_type] returns the ToolchainInfo of the resolved target. Rule authors depend on resolution rather than calling it.

  2. Toolchains — inputs to resolution, ordered registration sources, reversed --extra_toolchains precedence, first-wins module registration order, and lexicographic target-pattern expansion. Its generic ordering list still includes legacy WORKSPACE entries. The Bazel 9 boundary is stated separately in 4. 1 2 3 4

  3. Toolchainsexec_compatible_with on the target/rule filters execution platforms before resolution. target_settings filters toolchains by current configuration.

  4. Bzlmod Migration Guide — WORKSPACE was disabled by default in Bazel 8 and removed in Bazel 9, making Bzlmod mandatory. 1 2

  5. Improved --Toolchain_resolution_debug'ing - Malte Poll, Modus Create — two-pass algorithm framing, declaration vs. registration order, Bazel 7 debug-output rewrite (one message per invocation, indentation, early stopping). 1 2 3 4

  6. Toolchains — constraint-matching rule: every constraint_value in the clause must be on the platform. Extra constraint values from unmentioned settings are ignored.

  7. Toolchains — mandatory vs. optional toolchain types. config_common.toolchain_type(..., mandatory = False) returns None from ctx.toolchains when no match. 1 2 3

  8. Improved --Toolchain_resolution_debug'ing - Malte Poll, Modus Create — the selection stage filters mandatory misses, then prefers the platform with the highest optional coverage and uses registration order as the tie-breaker. The public Toolchains page describes the mandatory-only degenerate case as "the first remaining" platform. Bazel 9.0.0's ToolchainResolutionFunction.java compares surviving platforms by resolved toolchain count and preserves encounter order for ties.

  9. Toolchains — when a rule uses execution groups, each group performs toolchain resolution separately with its own execution platform and toolchains.

  10. Reproduced locally with bazel build //:demo in snippets/toolchain-resolution-error on Bazel 9.0.0. Absolute path prefix shortened and the "For more information…" trailer omitted, otherwise verbatim from the failing build.

  11. Toolchains--toolchain_resolution_debug=<regex> matches toolchain types and target names. .* prints all resolution information.

  12. Configurable Query (cquery)--transitions=lite|full exposes attribute and rule-class transitions on dependency edges, including toolchain dependencies.