4.6.2 Defining, Registering & Accessing Toolchains

A rule that needs a compiler does not name the compiler. It names an interface — a toolchain_type — and Bazel supplies a concrete implementation at analysis time, chosen for the current platforms. This is the same move as coding against a Java interface instead of a concrete class: the caller commits to a contract, not an implementation, and something else injects the right one.1 The analogy stops there: unlike a Java interface, the type target declares no method or field schema, and Bazel performs the injection during analysis, not at runtime. Mechanically it is four pieces on the producing side and two on the consuming side, all threaded through that single type label.2,3

The consequence worth stating first is the one that most often trips people up: a rule and the toolchain that satisfies it never reference each other. They share exactly one symbol — the toolchain_type label — and Bazel connects them. The toolchain implementation does not import or know about the rules that consume it, and a consuming rule does not import the toolchain that satisfies it.

That split answers the question a newcomer actually has — what is built for me, and what must I write? Bazel owns the resolution machinery. You provide the producer and consumer endpoints.

RoleYou writeBazel uses it to
Producertoolchain_type targetPublish the shared contract label.
ProducerImplementation rule returning ToolchainInfoProduce the contract's payload without creating actions.
Producertoolchain() targetMake a candidate by binding the type, implementation, and platform constraints.
Producerregister_toolchains() or --extra_toolchainsMake that toolchain() target eligible for selection.
Consumertoolchains = [type] on rule()Declare which contract the rule requires.
Consumerctx.toolchains[type] readReceive the selected implementation's ToolchainInfo.

Every section below fills in one of those workflow rows, roughly in the order you meet them.

The platform vocabulary used below — target platform, execution platform, exec_compatible_with, target_compatible_with — was introduced in 4.6.1 Platform Model for Rule Authors. How Bazel picks one candidate from many is the subject of 4.6.3 Toolchain Resolution. This article stays on the authoring side.

Every code block below is a trimmed view of a real, buildable file in the rules_glyph mini ruleset, a compact teaching ruleset for a fictional compiled language. Its glyph_library rule resolves a compiler toolchain the same way a real language rule does, so you can open each file, run bazel build //examples/basic:hello, and see the whole path work end to end instead of reading disconnected snippets.

Two Roles: Producer and Consumer

Almost every confusion about toolchains comes from mixing two jobs that happen to live near each other:

  • The producer publishes a toolchain type contract and one or more eligible candidates that implement it. This is the four-piece workflow, and it is most of this article.
  • The consumer is a rule that needs a tool from that family. It writes exactly two things: toolchains = ["//pkg:toolchain_type"] on rule(), and ctx.toolchains["//pkg:toolchain_type"] in its implementation.

The two roles are decoupled on purpose. A consumer depends on the type label, never on a specific implementation or toolchain() target. Registration, in contrast, names the toolchain() target — never the type target or the implementation target. Keep asking "am I producing or consuming right now?" — the answer tells you which labels you owe.

Define the Toolchain Type

A toolchain type is the shared public contract label. The toolchain_type rule creates a target with no behavior of its own — its purpose is to give every concrete toolchain in this family a shared name to declare against.4

# glyph/toolchains/BUILD.bazel
toolchain_type(name = "toolchain_type")

When a dedicated package publishes one toolchain family, the Bazel convention is to name the target simply toolchain_type and let the package path identify the family. The full label is therefore //glyph/toolchains:toolchain_type (BUILD.bazel).5 If one package must publish several distinct types, use role-qualified names such as compiler_toolchain_type and runtime_toolchain_type instead. Treat each label as part of the ruleset's public API: consumers will write it in rule declarations, and any third party shipping an alternative implementation will need to reference it. The type carries no schema — Starlark cannot enforce method or field names on a provider — so document the expected ToolchainInfo shape next to the toolchain_type declaration, in a README, or in the custom provider that lives next to it.6

Because that label is written in many places, rules_glyph binds it to a constant so a typo fails loudly at load time instead of silently missing resolution (toolchain.bzl):

GLYPH_TOOLCHAIN_TYPE = Label("//glyph/toolchains:toolchain_type")

Use the same constant everywhere Starlark accepts a label: in the toolchain() wrapper, in rule(toolchains = [...]), and as the key to ctx.toolchains[...]. The target declaration itself must still spell name = "toolchain_type", but every reference to that target can share one Label value.

Implement a Concrete Toolchain

The actual toolchain is a normal Starlark rule whose implementation returns a ToolchainInfo provider.7 ToolchainInfo is constructed with platform_common.ToolchainInfo(...) and accepts arbitrary keyword fields — the consuming rule reads them later.8

# glyph/toolchains/toolchain.bzl
GlyphToolchainInfo = provider(
    doc = "Executable tools needed by Glyph rules.",
    fields = {
        "compiler": "FilesToRunProvider for the one-shot Glyph compiler/linker.",
        "worker": "FilesToRunProvider for the persistent-worker adapter.",
    },
)

def _glyph_toolchain_impl(ctx):
    return [platform_common.ToolchainInfo(
        glyph = GlyphToolchainInfo(
            compiler = ctx.attr.compiler[DefaultInfo].files_to_run,
            worker = ctx.attr.worker[DefaultInfo].files_to_run,
        ),
    )]

The rule itself is just a 4.2.1 Rule Function that happens to return ToolchainInfo — there is nothing toolchain-specific about how it's declared (toolchain.bzl). The toolchain rule must not register any build actions. It only collects artifacts from other rules and forwards them to the rule that uses the toolchain.9 That keeps the toolchain rule cheap to analyze and pushes action creation back into the consuming rule, where the inputs are known.

Which "Info" Is Whose

That single return statement is where "Info" trips people up, because two different providers appear with almost the same name. Separate them by origin and role:

  • platform_common.ToolchainInfo is a Bazel built-in provider. It is the selected implementation's payload and injection envelope: a toolchain implementation must return it, and ctx.toolchains[type] hands exactly this object back. The toolchain_type label is the shared public contract. The expected fields inside ToolchainInfo are a ruleset convention that Bazel does not enforce. You never declare the built-in provider — Bazel owns it.
  • GlyphToolchainInfo is a custom provider you declared with provider() (4.2.7 Custom Provider Declaration). Your ruleset owns it. It exists only to give the payload a typed, documented shape (fields = {...}).
  • glyph (lowercase) is just a field name on ToolchainInfo whose value happens to be a GlyphToolchainInfo instance.

So the one line reads: "put my typed GlyphToolchainInfo under the field glyph inside Bazel's ToolchainInfo." A consumer later unwraps it in the mirror-image order: ctx.toolchains[GLYPH_TOOLCHAIN_TYPE].glyph.

Wrap It With a toolchain() Target

A toolchain() target is the candidate wrapper. It does three jobs at once: it points to the concrete implementation, it names the toolchain_type contract this implementation satisfies, and it declares which platforms it is suitable for via exec_compatible_with and target_compatible_with.10

# glyph/toolchains/BUILD.bazel
load(":toolchain.bzl", "GLYPH_TOOLCHAIN_TYPE", "glyph_toolchain")

glyph_toolchain(
    name = "source_toolchain_impl",
    compiler = "//compiler:glyphc",
    worker = "//glyph/worker:worker",
)

toolchain(
    name = "source_toolchain",
    toolchain = ":source_toolchain_impl",
    toolchain_type = GLYPH_TOOLCHAIN_TYPE,
    visibility = ["//visibility:public"],
)

rules_glyph is single-platform, so its toolchain() omits the constraints (BUILD.bazel). The moment you need per-platform variants, add them:

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,
)

exec_compatible_with constrains the execution platforms this toolchain can run on. target_compatible_with constrains the target platforms it can produce outputs for. The same implementation rule can be reused for several toolchain() targets — one per platform combination — and the three labels (toolchain_type, the concrete implementation, the toolchain() wrapper) do not have to live in the same package.11

Naming and API Conventions

Keep the public and private pieces recognizable:

  • Name a concrete producer target <variant>_toolchain_impl and its registrable toolchain() wrapper <variant>_toolchain. For example, linux_x86_64_toolchain_impl pairs with linux_x86_64_toolchain. Reserve _impl for the target that produces ToolchainInfo. Users register the wrapper without that suffix.
  • Export a shared type label as <DOMAIN>_TOOLCHAIN_TYPE, such as GLYPH_TOOLCHAIN_TYPE. Use a leading underscore, such as _SH_TOOLCHAIN_TYPE, only when the constant is private to one .bzl file.
  • Default the toolchain package to private visibility. Make the toolchain_type, registrable toolchain() wrappers, and intentional Starlark API entry points public. Keep concrete *_toolchain_impl targets private unless users are explicitly expected to reference them.

That last boundary prevents consumers from bypassing resolution. They may depend on the public type or register a public candidate, but they cannot accidentally wire the implementation target directly into unrelated BUILD APIs.

Register the Toolchain

Defining a toolchain() target creates a candidate, but does not make it eligible for analysis-time selection. Register the toolchain() target itself — not the toolchain_type target and not the implementation target — so Bazel includes it in the ordered candidate list.12 The primary registration path is register_toolchains() in MODULE.bazel:

# MODULE.bazel
register_toolchains("//glyph/toolchains:source_toolchain")

That label is the toolchain() target //glyph/toolchains:source_toolchain (MODULE.bazel). Registration makes this candidate eligible. register_toolchains accepts absolute target patterns.13 Patterns can add several candidates, whose ordering can affect selection. 4.6.3 Toolchain Resolution owns those ordering and ranking details.14

For one-off experiments and CI overrides, --extra_toolchains makes additional toolchain() targets eligible without editing MODULE.bazel.15 Its precedence is covered in 4.6.3 Toolchain Resolution.

Forgetting registration is the most common failure mode for a new toolchain. Running a target whose rule declares toolchains = ["//toolchain:demo_toolchain_type"] without any matching registered toolchain() fails analysis:

Won't build
bazel build //:demo
ERROR: .../BUILD.bazel:3:10: While resolving toolchains for target //:demo (...): No matching toolchains found for types:
  //toolchain:demo_toolchain_type
To debug, rerun with --toolchain_resolution_debug='//toolchain:demo_toolchain_type'
ERROR: Analysis of target '//:demo' failed; build aborted

Reproduce this error

Declare a Rule's Toolchain Requirements

This is the consumer role. A rule declares its requirements through the toolchains parameter of rule(). A bare label in that list declares a mandatory toolchain requirement: if Bazel cannot pick a candidate, analysis halts with the error above.16

# glyph/internal/rules.bzl
glyph_library = rule(
    implementation = _compile_impl,
    attrs = {"srcs": attr.label_list(allow_files = [".glyph"], mandatory = True), ...},
    toolchains = [GLYPH_TOOLCHAIN_TYPE],
)

glyph_library declares its own type here (rules.bzl) — the producer and consumer happen to be the same ruleset. But nothing forces that.

Consuming someone else's toolchain. To depend on a toolchain published by another ruleset, you write their type label — that is the entire coupling:

my_rule = rule(
    implementation = _my_rule_impl,
    toolchains = ["@rules_theirs//toolchains:their_toolchain_type"],
)

You do not import their implementation .bzl, and you do not define a toolchain() yourself. You need two things at build time: their toolchain_type label (public API) and some registered implementation of it — theirs, registered by their module, or one you register with register_toolchains()/--extra_toolchains. That is the payoff of the producer/consumer split: the type label is the only shared symbol, so you can "swallow" a foreign toolchain, and later swap the implementation, without touching your rule.

A bare type label is mandatory. For a rule that can operate without an implementation, declare the type as optional:

toolchains = [
    config_common.toolchain_type(
        "//pkg:toolchain_type",
        mandatory = False,
    ),
]

When no candidate matches, ctx.toolchains["//pkg:toolchain_type"] is None. When duplicate declarations mix mandatory and optional forms, mandatory wins.17,18 Aspects expose the same declaration and access API.19 Keep both cases out of the primary producer/consumer path until the ordinary mandatory rule works.

Consume the Resolved Toolchain

During analysis, Bazel selects an eligible candidate for the declared type. Inside the rule implementation, ctx.toolchains is a ToolchainContext indexed by that same type label. The result for a mandatory type is the selected implementation's ToolchainInfo.20

# glyph/internal/rules.bzl
def _compile_impl(ctx):
    toolchain = ctx.toolchains[GLYPH_TOOLCHAIN_TYPE].glyph  # -> GlyphToolchainInfo
    ctx.actions.run(
        executable = toolchain.compiler,  # a field on your custom provider
        ...
    )

Read this as the exact inverse of the return in the implementation rule: index Bazel's ToolchainInfo by the type label, then reach through your .glyph field to the GlyphToolchainInfo you constructed (rules.bzl). The consumer never sees the toolchain() wrapper, the constraints, or which platform won — resolution already collapsed all of that into one provider.

Optional types can yield None. Aspects expose related contexts, and ToolchainContext.toolchain_types() supports generic inspection.19,20,21 Rules whose actions need more than one execution platform read per-group toolchains instead. 4.6.5 Execution Groups & Auto Exec Groups owns that API and workflow.22

Give a Glyph rule the compiler it needs
The rule asks for a capability, not a specific compiler. A producer publishes candidates. Bazel selects one during analysis and gives its tool data to the rule.
The one shared public name
//glyph/toolchains:toolchain_type

Producer wrappers and consumer rules use this identical label. It names the compiler capability, not one compiler implementation or binary.

GLYPH_TOOLCHAIN_TYPE =
Label("//glyph/toolchains:toolchain_type")
Producer publish the type · create tool data · define a candidate · make it eligible
1
Declare the public type
BUILD.bazel · target declaration
toolchain_type(name = "toolchain_type")

This target creates the shared type label used at both ends. It defines no payload schema. Public API: toolchain_type label

2
Create one implementation target
BUILD.bazel · create the target
glyph_toolchain(
  name = "source_toolchain_impl",
  compiler = "//compiler:glyphc",
  worker = "//glyph/worker:worker",
)
toolchain.bzl · implementation returns its payload
return [platform_common.ToolchainInfo(
  glyph = GlyphToolchainInfo(
    compiler = …,
    worker = …,
  ),
)]

The BUILD target invokes the rule. Its implementation function returns Bazel's ToolchainInfo. The nested GlyphToolchainInfo documents this ruleset's payload. Private target: source_toolchain_impl

3
Wrap it as a selectable candidate
BUILD.bazel · platform-specific candidate
toolchain(
  name = "linux_x86_64_toolchain",
  toolchain = ":source_toolchain_impl",
  toolchain_type = GLYPH_TOOLCHAIN_TYPE,
  exec_compatible_with = [
    "@platforms//os:linux",
    "@platforms//cpu:x86_64",
  ],
  target_compatible_with = [
    "@platforms//os:linux",
    "@platforms//cpu:x86_64",
  ],
)
The wrapper points to the payload target. It does not construct the payload. Every candidate binds these three facts: toolchain_type capability it satisfies toolchain implementation target exec_compatible_with
target_compatible_with
platform fit, when needed
4
Make the candidate eligible
MODULE.bazel · registration
register_toolchains(
  "//glyph/toolchains:linux_x86_64_toolchain",
)

Register the public toolchain() wrapper, not the type and not the private implementation target. Eligible candidate: linux_x86_64_toolchain

Consumer · declare the need the Glyph rule requests the shared capability
5
Ask for the shared type
rules.bzl · rule declaration
glyph_library = rule(
  implementation = _compile_impl,
  toolchains = [GLYPH_TOOLCHAIN_TYPE],
  …
)

The rule names the type it needs. It never chooses a wrapper, implementation target, or compiler binary.

Bazel · analysis-time resolution
Bazel considers registered wrappers of the required type, checks their platform compatibility, and injects the selected implementation's ToolchainInfo. The rule stays unchanged when another compatible candidate wins.
Consumer · use the selected tool read the injected payload · create the compile action
6
Read the selected compiler
rules.bzl · _compile_impl
toolchain = ctx.toolchains[
  GLYPH_TOOLCHAIN_TYPE
].glyph
compiler = toolchain.compiler
Read the access path from left to right: ctx.toolchains[type] selected Bazel ToolchainInfo .glyph ruleset-owned GlyphToolchainInfo .compiler executable for the action
7
Run the compile action
rules.bzl · action creation
ctx.actions.run(
  executable = compiler,
  …
)

The consumer uses the executable that Bazel selected indirectly through the shared type.

Replace safely: keep the shared type label and payload contract. Then register a different compatible wrapper and implementation. The consumer rule does not change.

Two Ways to Shape the Payload

Nesting a custom provider is one convention, not a requirement. There are two shapes, and you should choose deliberately:

Pattern A — wrap a custom provider inside ToolchainInfo (what rules_glyph uses, shown above). You get a named handle (.glyph) whose fields are declared and documented by provider(fields = ...), so the payload's shape is self-describing and typos in field names surface at construction. The cost is one extra provider symbol.

A declared provider has identity, not merely a matching list of fields. An alternative Glyph toolchain must load and construct the public GlyphToolchainInfo symbol. Declaring another provider with the same field names creates a different provider. This is the same-symbol rule described in 4.2.7 Custom Provider Declaration. Publishing the provider beside GLYPH_TOOLCHAIN_TYPE therefore makes third-party implementations possible without weakening the payload to an untyped struct.

Pattern B — put fields and helpers directly on ToolchainInfo. Flatter, no extra symbol. A common production variant exposes helper functions next to opaque data so the consuming rule never reaches into internals:

return [platform_common.ToolchainInfo(
    compile = go_compile,
    link = go_link,
    internal = struct(go_cmd = go_cmd, env = env, tools = ctx.files.tools),
)]

Here compile and link form the toolchain's public surface. The internal struct holds files and metadata the helpers need but consumers should treat as opaque.23 Both patterns keep the type replaceable by a different vendor. Prefer Pattern A when you want the payload's schema written down in one place (glyph's reason: the tool surface stays named and documented). Prefer Pattern B when the payload is small or is mostly behavior (functions) rather than data.

That nested struct is only a private implementation record. It is neither the toolchain_type nor the provider Bazel requires from a toolchain implementation. The required outer value remains platform_common.ToolchainInfo.

The type target itself still does not enforce any payload schema. GlyphToolchainInfo checks instances created through that provider, but Bazel does not verify that every implementation registered for GLYPH_TOOLCHAIN_TYPE places one under .glyph. A production ruleset should run the same contract test against each supported implementation. In a mature API, a helper such as use_glyph_toolchain(mandatory = True) can also centralize the label and optionality. Keep the raw constant visible in introductory code so the underlying Bazel mechanism remains clear.

Dependencies and the Toolchain Transition

When the toolchain rule has dependencies (a real compiler binary, a system library, a builder helper), the cfg on its attr.label attributes behaves differently from an ordinary rule.24 The edge from a parent target to its resolved toolchain uses a special toolchain transition that pins the execution platform to the parent's. Inside the toolchain rule, cfg = "exec" dependencies build for that same execution platform — so a compiler attached as an exec dep is runnable from the parent's actions — while cfg = "target" dependencies build for the parent's target platform, which is the right configuration for runtime libraries the toolchain contributes to the final artifact.25 rules_glyph shows the common case: both the compiler and the worker are attr.label(executable = True, cfg = "exec"), because the parent's actions run them during the build (toolchain.bzl).

Lifting the Toolchain Into Its Own Repo

Nothing above assumed the toolchain lives next to the rules. Because the only shared symbol is the toolchain_type label, you can move the type, the implementation rule, the toolchain() targets, and their register_toolchains() call into a separate module and publish it. The consuming rule keeps working unchanged as long as it can still see that label — pull the ruleset in with bazel_dep, or register a different implementation of the same type yourself. It is the same four pieces, relocated.

When the toolchain additionally has to download and stage an SDK — generating toolchain() targets from a repository rule and registering them through a module extension — the pattern has a name: 4.11.2 Toolchainization. That builds directly on the four pieces above.

A production ruleset makes the separation concrete: rules_dotnet declares the public type target in dotnet/BUILD.bazel, while dotnet/toolchain.bzl defines DotnetInfo, the implementation rule, and the returned ToolchainInfo. Read that split as an example of the public type-label contract, not as a requirement to copy the .NET payload shape into another ruleset.26

key takeaway

A toolchain isn't a single object. The producer publishes a toolchain_type contract, returns platform_common.ToolchainInfo from an action-free implementation rule, wraps that implementation and its constraints in a candidate toolchain() target, then makes that wrapper target eligible with register_toolchains() or --extra_toolchains. The consumer writes only toolchains = [type_label] on rule() and ctx.toolchains[type_label] in the implementation. Bazel performs analysis-time selection and injects the selected implementation's ToolchainInfo. A provider like GlyphToolchainInfo is the producer's typed payload nested inside it.

Check your understanding · 4 questions

1.Match each piece of the toolchain authoring workflow to what it does:

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

Answers
toolchain_type target
Starlark rule returning platform_common.ToolchainInfo
toolchain() target
register_toolchains("//pkg:my_toolchain") in MODULE.bazel

2.A repository defines a toolchain_type, a Starlark rule returning ToolchainInfo, and a toolchain() target, but a consuming rule still reports No matching toolchains found. Which authoring step is most likely missing?

Select one answer

3.True or false: which statements describe the Starlark toolchain implementation rule correctly?

Choose True or False for each sentence

The implementation rule may register build actions like any other rule, since it is just a normal Starlark rule.
The implementation must return a platform_common.ToolchainInfo provider, but the fields inside ToolchainInfo are arbitrary and not enforced by Bazel.
Inside the implementation, cfg = "exec" dependencies build for the parent target's execution platform thanks to the toolchain transition.
The same implementation rule can be referenced from several toolchain() targets — one per platform combination — without duplicating the rule definition.

4.Which ToolchainInfo shape best keeps a toolchain type usable by alternative implementations?

Select one answer

0 of 4 answered

Footnotes

  1. Writing Bazel rules: platforms and toolchains — DI analogy: toolchain_type ≈ interface, toolchain returning ToolchainInfo@Provides, rule asking for it ≈ @Inject.

  2. Toolchains — end-to-end framework: motivation (rules depend on toolchain_type, not hardcoded tools), toolchains = [...] in rule(), ctx.toolchains access.

  3. Toolchains — "To define some toolchains for a given toolchain type, you need three things": the implementation rule, the language-specific targets, and the toolchain wrapper target.

  4. Toolchains — toolchain type is "a simple target that represents a class of tools that serve the same role for different platforms". Declared with toolchain_type(name = "...").

  5. Toolchains — convention: target named toolchain_type, distinguished by package path.

  6. Writing Bazel rules: platforms and toolchains — Starlark has no place to write down required method/field names. Document the expected shape in a dummy provider, README, or sibling docstring.

  7. Toolchains — the implementation rule "must return a ToolchainInfo provider, which becomes the object that the consuming rule retrieves using ctx.toolchains and the label of the toolchain type."

  8. platform_commonplatform_common.ToolchainInfo is the constructor/key for the ToolchainInfo provider. ToolchainInfo — provider returned by toolchain rules to share data with rules that depend on toolchains.

  9. Toolchains — defining toolchains: "The _toolchain rule cannot create any build actions. Rather, it collects artifacts from other rules and forwards them to the rule that uses the toolchain."

  10. Toolchains — the toolchain() target "provides metadata used by the toolchain framework" and refers to the toolchain_type plus the implementation, with exec_compatible_with / target_compatible_with constraints.

  11. Toolchains — "there's no reason the toolchain type, language-specific toolchain targets, and toolchain definition targets can't all be in separate packages."

  12. Toolchains — "you just need to make the toolchains available to Bazel's resolution procedure. This is done by registering the toolchain, either in a MODULE.bazel file using register_toolchains(), or by passing the toolchains' labels on the command line using the --extra_toolchains flag."

  13. MODULE.bazel filesregister_toolchains "specifies already-defined toolchains to be registered when this module is selected. Should be absolute target patterns."

  14. Toolchains — pattern-based registration: subpackage toolchains register before parent-package toolchains, and within a package they register in lexicographical order. MODULE.bazel files — patterns expanding to multiple targets register in lexicographical order by target name.

  15. Toolchains — "Toolchains registered using --extra_toolchains are added first" in the candidate set.

  16. Toolchains — "when a rule expresses a toolchain type dependency using a bare label … the toolchain type is considered to be mandatory. If Bazel is unable to find a matching toolchain … this is an error and analysis halts."

  17. config_commonconfig_common.toolchain_type(name, mandatory = True) declares a rule's dependency on a toolchain type with explicit mandatory/optional semantics.

  18. Toolchains — mixing forms is allowed. If the same toolchain type appears more than once, "it will take the most strict version, where mandatory is more strict than optional."

  19. Toolchains — "Aspects have access to the same toolchain API as rules: you can define required toolchain types, access toolchains via the context, and use them to generate new actions." 1 2

  20. ToolchainContext — indexing with a toolchain type label returns the selected ToolchainInfo, optional unresolved toolchains return None, and toolchain_types() returns resolved toolchain type labels. 1 2

  21. ToolchainContext — for aspects, ctx.rule.toolchains["//pkg:my_toolchain_type"] returns the list of providers from applying the aspect on those toolchain targets.

  22. Execution Groups — accessing the resolved toolchain of an execution group: ctx.exec_groups["link"].toolchains["//foo:toolchain_type"].

  23. Writing Bazel rules: platforms and toolchainsgo_toolchain exposes compile/link/build_test as the public surface and an internal struct for files and metadata that helpers use.

  24. Toolchains — "Toolchains and configurations": attr.label works as in a standard rule, but cfg interacts with the toolchain transition.

  25. Toolchains — toolchain transition pins the toolchain's execution platform to the parent's. cfg = "exec" deps build for that execution platform, cfg = "target" (the default) deps build for the parent's target platform.

  26. rules_dotnet repository mapdotnet/BUILD.bazel declares the public toolchain_type, while dotnet/toolchain.bzl defines DotnetInfo, the implementation, and its ToolchainInfo. Private repository generation and resolution tests are escalation evidence rather than downstream APIs.