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

Repository names have different audiences
Bazel interprets an apparent name through the current repository's mapping. Canonical identity stays internal.
Apparent name
What users and generated BUILD files type
scoped public API
import use_repo(maven_ext, "maven")
label @maven//:defs

The current repository mapping resolves this spelling. Document it, accept it in tags, and emit it into public generated files.

Canonical name
What Bazel uses for internal identity
not a public contract
source repository_ctx.name
rule do not parse or emit

It lets Bazel keep every external repository unambiguous, but its spelling can change.

Original rule name
What the repo rule call specified
public-name source
call name = "maven"
impl repository_ctx.original_name

Use it, or carry an explicit attr, when generated target names need the public spelling.

Boundary crossing
Convert label strings in the context that should own them
before crossing macro, module, or generated-repo boundaries
Ruleset-owned constant
Label("//:toolchain_type")

resolved from the .bzl repo

Caller-relative macro string
native.package_relative_label(p)

resolved from the BUILD caller

Repository rule dependency
attr.label(...)

resolved by Bazel as an attr

Central rule: apparent names are API; canonical names are not. Convert labels at scope boundaries instead of splicing repo-name strings.

For rule authors, keep these roles separate:

NameWho should see itWhat it is for
Apparent nameUsers, tags, docs, generated BUILD filesThe name used in labels such as @maven//... after use_repo() brings the repo into scope.
Canonical nameBazel internalsA globally unique storage and resolution identity. Do not parse or document it.
Original repo rule nameRepository rule implementationsThe 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.

key takeaway

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.
It is safe to parse an apparent name back out of a canonical repository name.
A generated repository can refer to other repositories created by the same module extension by their apparent names.

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

0 of 4 answered

Footnotes

  1. 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

  2. repository_ctxname is canonical. original_name is the originally specified repo rule name. 1 2 3 4

  3. 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

  4. 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