4.9.5 Reproducibility Concerns
Repository rules are the part of a Bazel ruleset where reproducibility is easiest to lose. They run during loading when Bazel needs to materialize an external repository, before target analysis and outside action sandboxing. That makes 2.3 Hermeticity & Sandboxing more literal here: every network result, environment value, local file, helper program, and generated label that influences the repository has to be part of the repository rule's declared input story.1
External-Setup Code With Host Access
Building on 4.9.4 Repository Rule API, a normal custom rule declares actions. Those actions later run with declared inputs, tools, and execution strategy. A repository rule is different. Its implementation function receives repository_ctx during loading and creates the external repository on disk when Bazel needs a target from it.2 It has no configured-target providers or action-registration API. Instead, it can download files, extract archives, read and write files, create symlinks, and execute host commands.3
That power is the point. Repository rules let a ruleset turn an SDK archive, package-manager lockfile, local system installation, or generated BUILD file into a Bazel repository. It is also the risk. Because repository_ctx gives direct access to the host system without sandboxing, directory names, environment variables, timestamps, and installed tools can slip into the generated repository if the implementation is not careful.4
def _sdk_repo_impl(rctx):
rctx.download_and_extract(
url = rctx.attr.urls,
sha256 = rctx.attr.sha256,
strip_prefix = rctx.attr.strip_prefix,
)
rctx.template(
"BUILD.bazel",
rctx.attr._build_tpl,
substitutions = {"{repo_name}": rctx.original_name},
)
return rctx.repo_metadata(reproducible = True)
This shape is boring in the best way: the archive is addressed by a checksum, the generated BUILD file comes from a declared template label, and the repository name used in generated text is the apparent name the caller provided, not Bazel's internal canonical spelling.
Refetching Is Not Magic
Bazel does not refetch a repository just because "something outside" changed. It reruns the repository rule when an input it tracks changes, such as the rule's attributes or implementation, a tracked environment value, or a watched filesystem dependency.5
The sharp edge is that a repository rule can observe more host state than Bazel tracks. If environment or filesystem state affects the generated repository without becoming a recorded input, Bazel may reuse stale output. 4.9.6 getenv() vs os.environ Pitfall explains the environment-API distinction, while 4.9.7 watch() / watch_tree() File Dependencies covers the file and directory watching APIs. Here the broader rule is enough: every observed value that affects repository contents must have an invalidation edge.
Reproducible Means Cacheable Across Workspaces
repository_ctx.repo_metadata(reproducible = True) is a promise: refetching the repository with the same attributes, rule definition, watched files, and watched environment variables will produce the same repository contents even if untracked conditions change.6 That is stronger than "works on my machine." It means internet state, workspace-root paths, arbitrary executable output, and other untracked facts cannot affect the result.
If the rule is not yet reproducible, attrs_for_reproducibility can tell Bazel which attributes would make it reproducible. The common example is a Git-like rule: a floating branch is not reproducible, but a resolved commit can be. The rule can report the fixed attribute set rather than pretending the floating input is stable.6
This is also why archive downloads with sha256 or integrity are the default design for fetched dependencies. The official repository_ctx.download() and download_and_extract() docs warn that omitting a checksum is a security risk and, at best, non-hermetic. With a checksum, Bazel can check the repository cache before touching the network and add successful downloads to that cache afterward.7 A private-data repo rule should still go through Bazel's downloader so repository cache, credential helpers, remote downloader configuration, and offline workflows can participate.8
Names Must Survive Bzlmod
Bzlmod made repository names more explicit, but it also made old name assumptions fragile. repository_ctx.name is the canonical repository name: unique, internal, and not specified as a stable format.9 repository_ctx.original_name is the name originally passed to the repo rule's name attribute.9 When generated files need the user-facing repo name, use original_name. Do not parse canonical names or embed their current spelling.
The rule-maintainer prime directive is blunt: refer to repositories by apparent names, not canonical names, because canonical names exist so Bazel can store repositories as a flat unique set and may change format.10 The same compatibility lesson shows up in legacy WORKSPACE macros that computed repo names, wrapped them in Label, and then broke when called from module extensions because the generated repo had not been brought into scope with use_repo().11
For new repository rules, prefer dependency attributes such as attr.label or attr.label_list over constructing labels from computed strings when one generated repository needs information from another.11 That keeps the dependency visible to Bazel and gives the implementation a target whose label accessors are already resolved in the right repository context. If you maintain an older ruleset, this concern connects directly to 4.10.1 Module Extension Fundamentals and the Bzlmod-specific name handling in 4.10.5 Repo Name Handling.
Classify: A repository rule downloads an archive with curl, reads
SDK_HOME from the process environment, probes a local directory, and embeds
repository_ctx.name in generated labels. Which reproducibility boundary does
each choice cross, and what should replace it?
Reveal
Use Bazel's downloader with sha256 or integrity for the archive. Read
result-affecting environment values through the tracked API from
4.9.6 getenv() vs os.environ Pitfall, and record local files or directories with the watch
APIs from 4.9.7 watch() / watch_tree() File Dependencies. Generate user-facing labels from apparent
names such as repository_ctx.original_name or from label attributes rather
than parsing canonical names.
If one of those inputs cannot be represented, the rule may work locally but is not yet a stable ruleset API. Host-tool discovery patterns continue in 4.9.8 Hermetic Host Tooling, while full external evaluators such as Nix or CMake have the additional cost profile in 4.9.11 Non-incremental External Evaluation.
Treat repository rules as trusted bootstrap code. They run early, outside the action sandbox, and can see the host. A reproducible repo rule converts every host, network, and naming dependency into declared attributes, checksums, watched files, tracked environment variables, or stable apparent names.
Check your understanding · 3 questions
1.Which inputs can cause Bazel to refetch a repository rule result when they change?
Select all that apply
2.When may a repository rule return attrs_for_reproducibility in repo_metadata()?
Select one answer
3.True or false: repository rule reproducibility and names
Choose True or False for each sentence
repo_metadata(reproducible = True) promises that refetching with the same tracked inputs yields the same repository contents, even if untracked conditions change.repository_ctx.name is the stable apparent name users wrote in MODULE.bazel or a WORKSPACE macro.Footnotes
-
Repository Rules — repository implementation functions and refetching based on declared rule inputs ↩
-
Repository Rules — fetching lifecycle and repository implementation function ↩
-
repository_ctx — download, extract, file, symlink, template, and execute APIs ↩
-
Writing Bazel rules: repository rules — host-system access, no sandboxing, and reproducibility warnings ↩
-
Repository Rules — refetch triggers for attributes, implementation code, environment variables, watched paths, and
bazel fetch --force↩ -
repository_ctx —
repo_metadata(reproducible, attrs_for_reproducibility)contract ↩1 ↩2 -
repository_ctx — checksum and repository-cache behavior for
download()anddownload_and_extract()↩ -
Fetching Private Data with Repo Rules and MODULE Extensions - Malte Poll, Modus Create — Bazel downloader, repository cache, credential helper, and why
genrulepluswgetis the wrong shape ↩ -
repository_ctx —
nameas canonical name andoriginal_nameas originally specified rule name ↩1 ↩2 -
Migrating to Bazel Modules (a.k.a. Bzlmod) - Repo Names, Again... — apparent-name prime directive and
repository_ctx.original_name↩ -
Migrating to Bazel Modules (a.k.a. Bzlmod) - Maintaining Compatibility, Part 2 — dependency attributes and computed repo-name compatibility fixes ↩1 ↩2