4.10.2 Implementation Function
The implementation function is where a module extension stops being a schema and becomes dependency logic. module_extension() points at one Starlark function that receives module_ctx. That function reads the tags collected from the resolved module graph, decides which external repositories should exist, usually creates those repositories by calling repository rules, and may return metadata that helps Bazel maintain the caller's use_repo() declarations.1
This is module-extension evaluation, not target analysis. During an ordinary build, Bazel typically evaluates an extension when loading needs one of the repositories that the extension defines. A package that references such a repository can therefore trigger the evaluation. Commands such as bazel mod deps can also evaluate extensions directly.
The implementation receives module_ctx and tags, and it can call APIs that define repositories. It does not receive configured Target objects, return target providers, or register build actions. It may declare repositories and observe the host or network before Bazel analyzes a BUILD target that depends on one of those repositories. A conflict between extension tags therefore fails before target analysis.
The Shape Of The Function
A module extension declaration is a global .bzl symbol. Its implementation parameter must be a function that takes one module_ctx argument, and Bazel calls that function to determine the set of repositories made available by the extension.2
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
_artifact = tag_class(attrs = {
"name": attr.string(),
"url": attr.string(),
"sha256": attr.string(),
})
def _archives_impl(ctx):
root_repos = []
seen = {}
for mod in ctx.modules:
for tag in mod.tags.artifact:
key = (tag.url, tag.sha256)
if tag.name in seen:
if seen[tag.name] != key:
fail("repo '{}' was declared with conflicting attrs".format(tag.name))
continue
seen[tag.name] = key
if mod.is_root:
root_repos.append(tag.name)
http_file(
name = tag.name,
url = tag.url,
sha256 = tag.sha256,
)
return ctx.extension_metadata(
root_module_direct_deps = root_repos,
root_module_direct_dev_deps = [],
)
archives = module_extension(
implementation = _archives_impl,
tag_classes = {"artifact": _artifact},
)
This is the same boundary introduced in 4.10.1 Module Extension Fundamentals, but now the important point is control flow: the implementation loops over modules, inspects tags, validates conflicts, and calls a repository rule such as http_file. Repository rules remain the mechanism that materializes external repositories. The extension is the graph-aware layer that decides which calls to make.3 For the repository-rule API itself, keep the details in 4.9.4 Repository Rule API.
The mini-ruleset's complete _glyph_deps_impl follows the same loop: it aggregates requests across module_ctx.modules, rejects conflicting versions, calls one repository rule per resolved package, and returns only the root module's direct repos in extension_metadata().
Reading module_ctx.modules
module_ctx.modules is the extension's view of the external dependency graph. It contains only modules that use this extension, and each entry is a bazel_module object with fields such as is_root, name, version, and tags.4 The list order is guaranteed to be breadth-first from the root module, which means the root appears first, but production code is clearer when it uses mod.is_root for root-specific policy instead of relying on a positional convention.5
Tags are grouped by tag class. If the extension declaration contains tag_classes = {"artifact": _artifact}, then a module's artifact tags are available as mod.tags.artifact. When an extension must merge tags from different classes while preserving the module-file and breadth-first ordering model, module_ctx.tag_sort_key(tag) gives sorted(..., key = ...) an opaque stable key for that ordering.5 This is why a repository-rule API often changes shape when it becomes an extension API: a single list attribute such as packages = [...] in a repository rule can become many per-module tags, so each dependency can state its own direct need and the extension can combine them later.6
The implementation defines the conflict policy. Some extensions choose the highest requested version across all modules. Others let only the root module set global configuration, or allow dependency modules to add packages but not override settings. The Haskell and Nix migration examples use this distinction to avoid the old WORKSPACE problem where macro order changed which dependency configuration won.7
Calling Repository Rules
Inside the implementation function, repository rules are ordinary loaded Starlark symbols. Calling http_file(name = ..., ...) or a custom repository rule does not create a target in the current package. It declares an external repository that will be part of this extension's repository namespace.8
The common pattern is:
- Collect all relevant tags from
ctx.modules. - Normalize and validate them into one resolved plan.
- Call repository rules to create the repositories in that plan.
- Return
ctx.extension_metadata(...)when Bazel should help maintainuse_repo()declarations.
A Go toolchain extension follows exactly this flow: read all download tags, select the highest requested Go version, fetch a manifest, instantiate per-platform download repositories, and create a toolchain repository that exposes the selected toolchains.9 The important lesson is not Go-specific. The extension can make one graph-wide decision, then delegate file creation, downloads, and generated BUILD files to repository rules.
module_ctx also has repo-rule-like helpers such as download, read, execute, file, path, watch, and which.10 Use them when the extension itself must resolve data before it can decide which repositories to create. Keep the host-dependent parts narrow: execute(), os, path(), and unchecked downloads quickly become hermeticity concerns, which is the focus of 4.10.6 Hermeticity Considerations.
Returning extension_metadata
use_repo() is not decoration. Module extensions are lazy: Bazel normally evaluates an extension only when some repository it generates is brought into scope and then referenced. Since Bazel cannot know every generated repo name before evaluating the extension, the caller names the repos it expects with use_repo().11
That creates a maintenance problem for extensions that generate many repositories. ctx.extension_metadata() is the author-side answer: the implementation can return the repositories it considers direct dependencies of the root module, split into normal and dev dependencies. If the root module's use_repo() calls do not match, Bazel warns and tells the user to run bazel mod tidy, which can update those calls automatically.12
The return value does not make repositories visible by itself. It tells Bazel what the visibility list should be. This distinction matters in code review: a missing use_repo() is a caller import problem. A wrong root_module_direct_deps list is an extension API problem.
The implementation function is the module extension's resolver. Treat module_ctx.modules as the input stream, repository-rule calls as the output plan, and extension_metadata as the maintenance contract with the root module's use_repo() list.
Two Boundaries To Keep Separate
A module extension can load() repository rules and helper code from repositories already visible to the module that hosts the extension, but it cannot create a repository and then load() from that same newly-created repository inside the same extension. The fix is to split configuration generation and dependency creation into separate extensions, avoiding a circular repository definition.13
extension_metadata(facts = ...) is also deliberately out of scope for the basic implementation loop. Facts persist data across extension reevaluations and are useful for stateful resolution. They are covered next in 4.10.3 Facts (State Persistence).14
Check your understanding · 4 questions
1.Which responsibilities belong specifically inside a module extension implementation function?
Select all that apply
2.Which field should implementation code use for root-module-specific policy?
Select one answer
3.True or false: extension_metadata() and use_repo()
Choose True or False for each sentence
ctx.extension_metadata(root_module_direct_deps = ...) can tell bazel mod tidy which generated repos should appear in root use_repo() calls.extension_metadata() makes those repositories visible even when use_repo() is missing.root_module_direct_deps and root_module_direct_dev_deps separate normal and dev-only repository imports.extension_metadata() replaces the need to call repository rules from the implementation.4.Why cannot a module extension implementation return DefaultInfo for a generated repository?
Select one answer
Footnotes
-
Module extensions — extension definition and implementation function overview. ↩
-
.bzl files —
module_extension()signature and implementation parameter. ↩ -
Module extensions — implementation functions call repository rules to generate repos. ↩
-
bazel_module —
is_root,name,version, andtagsfields. ↩ -
module_ctx —
moduleslist, breadth-first iteration order, andtag_sort_key()for stable ordering when merging tags across classes. ↩1 ↩2 -
A new way to manage dependencies: How we extended bzlmod — mapping repository-rule attributes to module-extension tags. ↩
-
A new way to manage dependencies: How we extended bzlmod — root/core-module scope decisions for Haskell and Nix extensions. ↩
-
Module extensions — generated repositories and extension repository namespace. ↩
-
Writing Bazel Rules: Module Extensions — Go toolchain extension implementation flow. ↩
-
module_ctx — helper methods shared with repository-rule-like work. ↩
-
Frequently asked questions — why
use_repo()is required for lazy extension evaluation. ↩ -
module_ctx —
extension_metadata()parameters andbazel mod tidyintegration. ↩ -
Migrating to Bazel Modules (a.k.a. Bzlmod) - Module Extensions — circular definition constraint and split-extension workaround. ↩
-
module_ctx —
factsparameter and persisted data across extension executions. ↩