4.10 Authoring Module Extensions

Authoring module extensions starts where consuming one stops. In 3.1.2 Using Extensions, a maintainer binds an extension, supplies tags, imports the generated repositories, and diagnoses the resolved result. Here, the ruleset author designs and implements the tag API, graph-wide resolution policy, generated-repository surface, and reproducibility contract behind that consumer workflow.

The trap in module extensions is mistaking a MODULE.bazel call site for the whole system. A root module may type only use_extension(...), a few tag calls, and use_repo(...), but the extension identity is shared across the selected module graph. Other modules can contribute tags, Bazel gathers them before the implementation runs, and the implementation decides which repositories should exist before ordinary packages can load labels from them.

Read this section as four public surfaces around one resolver: the tag API users type, the implementation loop in module_ctx, the repository names imported through use_repo(), and the reproducibility story behind lockfiles and host inputs. Treating an extension as "just a nicer WORKSPACE macro" misses all four.

Surface 1: The Tags Users Type

4.10.1 Module Extension Fundamentals supplies the shape: use_extension() creates an extension proxy, calls on that proxy create tags defined by tag_class(), and use_repo() imports selected generated repositories into the current module's apparent repository namespace. The key shift is that the extension decides which repositories should exist after seeing module-graph input. Repository rules from 4.9 Repository Rules still do the directory materialization.

This is the part users experience first, so it is already API design. A tag class is not a private helper signature. It is the setup language other modules will write.

Surface 2: The Resolver Loop

4.10.2 Implementation Function is where the extension stops being an API schema and becomes dependency logic. Read it as the resolver loop: inspect module_ctx.modules, aggregate tags, apply conflict policy, call repository rules, and return extension_metadata() when Bazel should help maintain the root module's use_repo() declarations.

4.10.4 Extension Design Patterns gives the shapes that keep that resolver maintainable: split configuration from dependency creation when a generated repo would otherwise create a circular loading problem, generate toolchain repositories instead of forcing users to import every private tool repo, wrap legacy WORKSPACE setup code without preserving WORKSPACE ordering assumptions, and return tidy metadata for generated repo imports.

Do not start with those patterns until the basic lifecycle is clear. 4.10.1 Module Extension Fundamentals and 4.10.2 Implementation Function give the complete tag-to-repo flow: users call the proxy, modules contribute tags, the implementation aggregates graph input, and repository rules still materialize the directories.

Surface 3: The Names Users Import

4.10.5 Repo Name Handling is where many migrations really fail. Generated repositories live in extension namespaces, users import apparent names with use_repo(), and canonical names are Bazel implementation details. A legacy macro that computes a repository name, wraps it in Label(), or writes it into generated files may be preserving assumptions that no longer hold under Bzlmod.

This is why design patterns and naming discipline belong together. An extension can hide private helper repositories, expose a small set of stable apparent names, and let the implementation reorganize without making every downstream MODULE.bazel file depend on internals.

Most real WORKSPACE-to-Bzlmod migration problems are not about spelling module_extension() correctly. They are about old setup macros depending on ordering, generated repository names, labels resolved in the wrong context, or toolchain repositories that should be exposed through a smaller public surface. The publishing and compatibility story continues in 4.11 Ruleset Packaging & Publishing.

Surface 4: The Inputs That Make It Reproducible

Module extensions run before ordinary build actions and can observe the host through module_ctx.execute(), module_ctx.os, environment reads, file reads, downloads, and repository-rule-like helpers. 4.10.6 Hermeticity Considerations is therefore the review article for the section, not an afterthought.

The practical question is the same one from 4.9.5 Reproducibility Concerns: what facts can change the repositories this extension creates? If the answer includes an environment variable, read it with module_ctx.getenv(). If it includes a local file or directory, watch it. If it includes host OS or CPU, declare that with os_dependent or arch_dependent. If the extension returns reproducible = True, make sure the promise is backed by tracked inputs, checksums, or persisted facts rather than by hope.

4.10.3 Facts (State Persistence) and 4.10.7 Extension Repository Manipulation make sense only after that review frame is visible. Facts are lockfile-persisted values for stable external truths the extension may want to reuse on later evaluations, not a general cache and not a place to hide mutable state. override_repo() and inject_repo() are surgical root-module controls for one extension scope, useful for vendoring, patched generated repos, and migration debugging, but not substitutes for ordinary module overrides, tracked inputs, or a well-designed extension API.

If you are reviewing an extension before shipping it, read 4.10.6 Hermeticity Considerations before approving clever resolution code. Then use 4.10.3 Facts (State Persistence) only if the extension is persisting stable discovered data, and 4.10.7 Extension Repository Manipulation only when the root module needs a temporary or surgical override inside one extension's repository namespace.

think

Classify: A module extension needs a user-facing configuration API, generated repositories, materialization logic, and host or network observations. Which contract should represent each part before the extension is published?

Reveal

Define typed tag classes for user input and stable apparent repository names for the repos users import with use_repo(). Keep byte fetching and repository construction in focused repository rules called by the extension's resolver.

Record outside observations through the appropriate module_ctx inputs: tracked environment reads, watched paths, checksummed downloads, and declared OS or architecture dependence. If the tags, import names, materializers, or external inputs cannot be named precisely, the extension's public and reproducibility contracts are not stable yet.

Inspect the mini-ruleset's complete glyph_deps extension: typed tags feed a resolver loop, repository rules materialize the plan, and extension metadata records imported names.

key takeaway

Module extensions are graph-aware dependency resolvers for Bzlmod. They collect typed tags across modules, use module_ctx to decide a repository plan, call repository rules to materialize that plan, and expose selected repos through apparent names. Good extension design is API design, repository-name discipline, and reproducibility review at the boundary between the module graph and external repositories.