4.10.5 Repo Name Handling
Repository names in module extensions are an API design problem disguised as a string-formatting problem. The extension author chooses which names users type in use_repo(), which names generated repositories use to talk to each other, and which names must never leak into generated BUILD files. The rule is simple: expose apparent names, let Bazel own canonical names, and convert label strings in the context where they are meant to resolve.1
The Three Names You Meet
In the label basics from 0.2.1 Label Anatomy, @repo//pkg:target looks like one stable name. Module extensions add another layer: repos generated by an extension live in the extension's namespace, and Bazel gives them canonical names so the global external repository table stays unambiguous.1 That canonical spelling is not a public contract. The official docs explicitly say its format is not an API and may change.1
The current repository mapping resolves this spelling. Document it, accept it in tags, and emit it into public generated files.
It lets Bazel keep every external repository unambiguous, but its spelling can change.
Use it, or carry an explicit attr, when generated target names need the public spelling.
resolved from the .bzl repo
resolved from the BUILD caller
resolved by Bazel as an attr
For rule authors, keep these roles separate:
| Name | Who should see it | What it is for |
|---|---|---|
| Apparent name | Users, tags, docs, generated BUILD files | The name used in labels such as @maven//... after use_repo() brings the repo into scope. |
| Canonical name | Bazel internals | A globally unique storage and resolution identity. Do not parse or document it. |
| Original repo rule name | Repository rule implementations | The name value originally passed to the repo rule, available as repository_ctx.original_name when you need the user-facing repo rule name inside the implementation.2 |
This matters most because module extensions usually call repository rules, as covered in 4.9.4 Repository Rule API and 4.10.2 Implementation Function. In that path, repository_ctx.name is canonical, while repository_ctx.original_name is the originally specified name attribute.2 If a repo rule uses repository_ctx.name to generate a target name, a label, or user-visible file content, Bzlmod can turn a neat public name into an implementation detail.
Keep Canonical Names Out Of The API
The maintainer rule is stricter than the user rule: only refer to repositories by apparent name, never by canonical name.3 Canonical names exist so Bazel can store all external repositories in one flat, unique namespace. Depending on their shape is depending on an implementation detail.3
That includes your own ruleset. Internal load("@my_rules//...", ...) statements and hardcoded "@my_rules//..." labels are usually unnecessary inside the same repository. Prefer load("//...", ...) and fixed labels such as Label("//:toolchain_type") when the target belongs to the .bzl file's own repository.3 This keeps the ruleset easier to use when its apparent name changes through bazel_dep(repo_name = ...), local overrides, or migration shims.
Generated files are the common failure point. A module extension collects tags, calls repo rules, and those repo rules often write BUILD or .bzl files. If the generated text embeds a canonical name, the file may work today and break when Bazel's naming scheme changes. If it embeds an apparent name but resolves it in the wrong repository context, it may point at the caller's repo instead of the ruleset's repo.
Convert Labels In The Right Context
String labels are not neutral. A string like "//tools:runner" means different things depending on where Bazel later interprets it. The practical rule is to wrap fixed target string literals with Label() inside macro implementations, repository rules that inject labels into generated files, and calls to functions from other packages.3
# Fixed target in the ruleset that defines this .bzl file.
_TOOLCHAIN_TYPE = Label("//:toolchain_type")
Use that form for constants in the .bzl file's repository. Do not use it to reinterpret a user-supplied repo name as though it came from the user's BUILD file. For caller-relative strings in macros, normalize with native.package_relative_label(). That is the right shape when converting target patterns or backend repository labels supplied through BUILD-level macros.3
def _expand_patterns(patterns):
result = []
for p in patterns:
exclude = p.startswith("-")
p = p.lstrip("-")
expanded = str(native.package_relative_label(p)) if p else ""
result.append(("-" if exclude else "") + expanded)
return result
The distinction is especially sharp when wrapping legacy WORKSPACE macros. A macro may compute a repository name, instantiate that repository, and then construct a Label from the computed name. When the same macro is called from a module extension, that Label can fail unless the repo was brought into scope with use_repo().4 Prefer pushing the dependency into a repository rule attribute (attr.label, attr.label_list, or attr.label_keyed_string_dict) when possible. This requires Bazel 7.4.0 or later.4 Pass string label values such as "@backend_repo" to those attributes, not Label("@backend_repo"), so the repository rule receives resolved target information without constructing the label in the extension wrapper.4
Extension Repos Have Their Own Visibility Rules
Repos generated by one extension can see the repos visible to the module hosting the extension, plus other repos generated by the same extension.1 This makes extension internals convenient: one generated repo can refer to another by the repo rule call's apparent name, even if the second repo is declared later.1
There is one important conflict case. If the hosting module already sees a repo named foo, and the extension also generates a repo named foo, then labels in generated repos may resolve to the module-visible foo rather than the extension-generated foo.1 The official docs call out Label() as the escape hatch for labels passed to repository rule attributes when you need to force the module-visible repo in such a conflict.1
That is not a license to expose arbitrary repo names from every transitive module. The best-practices guidance says only the root module should directly affect repository names when an extension tag can produce repos shared across the module graph.1 If every dependency can choose the same generated repo name, the extension's namespace becomes a collision domain.
Repository Rules Need The Apparent Name Too
Older repository rules often used their own name to generate a default target. Under Bzlmod, reading repository_ctx.name gives the canonical name, not the public name.2 The migration pattern is to carry the desired public name explicitly, or use repository_ctx.original_name when the supported Bazel range has it.2 original_name is available in Bazel 8.1.0+, so multi-version rulesets should keep an explicit-attribute or getattr(rctx, "original_name", ...) fallback until their minimum Bazel version includes it.3
def sdk_repository(**kwargs):
generated_target_name = kwargs.pop("generated_target_name", kwargs.get("name"))
_sdk_repository(
generated_target_name = generated_target_name,
**kwargs
)
That wrapper pattern copies the repo rule call's apparent name into a normal attribute before the implementation runs.3 The repository implementation can then generate :sdk, aliases, or documentation from a stable public value instead of from Bazel's canonical storage name.
Treat repository names as scoped API, not as strings to splice. Apparent names belong in user-facing labels, tags, docs, and generated BUILD files. Canonical names belong to Bazel.
When a label crosses a module, macro, or generated-repository boundary, decide which package context should own it before converting it. Label("//..."), native.package_relative_label(), repository rule label attributes, and repository_ctx.original_name solve different parts of that problem.
Check your understanding · 4 questions
1.Which repository name should a module extension expose in user-facing APIs and generated BUILD files?
Select one answer
2.True or false: repo-name handling in module extensions.
Choose True or False for each sentence
repository_ctx.name is the canonical name of the external repository.repository_ctx.original_name is useful when generated targets need the repo rule's originally specified name.3.Which patterns help avoid label-resolution bugs when migrating legacy WORKSPACE macros to module extensions?
Select all that apply
4.An extension generates a repository named foo, and the hosting module already sees a different apparent repository named foo. When should the extension wrap a label with Label("@foo//:target") before passing it to a repository-rule label attribute?
Select one answer
Footnotes
-
Module extensions — generated repo names,
use_repo(), extension repo namespace, visibility rules, conflict behavior, and root-module naming guidance. ↩1 ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
repository_ctx —
nameis canonical.original_nameis the originally specified repo rulename. ↩1 ↩2 ↩3 ↩4 -
Migrating to Bazel Modules (a.k.a. Bzlmod) - Repo Names, Again... — apparent-name prime directive,
Label()wrapping guidance, generated-file pitfalls, pattern expansion, and repo rule wrapper pattern. ↩1 ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 -
Migrating to Bazel Modules (a.k.a. Bzlmod) - Maintaining Compatibility, Part 2 — legacy WORKSPACE macros with computed repo-name
Label()values and Bzlmod-compatible solution patterns. ↩1 ↩2 ↩3