4.9.6 getenv() vs os.environ Pitfall
recommendedRepository rules can read the host environment before normal build actions exist. That makes environment variables part of the repository rule's input model, not just a convenience API. The safe path is repository_ctx.getenv("NAME"): it returns the variable value and records that this repository depends on that variable, so a later value change causes the repository to be fetched again.1 Reading repository_ctx.os.environ may return the same string, but it does not create that dependency.2
The external repo may stay stale until another input forces evaluation.
The changed env input invalidates the repo and triggers re-evaluation.
Same Value, Different Incrementality
The bug is easy to miss because both APIs look like ordinary environment access:
def _bad_sdk_repo_impl(rctx):
sdk_home = rctx.os.environ.get("SDK_HOME")
rctx.file("BUILD.bazel", _build_file_for(sdk_home))
bad_sdk_repo = repository_rule(
implementation = _bad_sdk_repo_impl,
)
Trace: SDK_HOME changes from /opt/sdk-1 to /opt/sdk-2. You rerun the build, but the generated external repository still points at the old SDK. Why did the environment change without triggering a refetch?
Reveal
The rule read host state, but it did not record that host state as a repository-rule input. After a repository is fetched, Bazel can reuse the generated repository until one of the inputs it knows about changes: the rule's attrs, the Starlark implementation, variables read through repository_ctx.getenv(), watched files, and similar explicit refetch triggers.
repository_ctx.os.environ.get("SDK_HOME") is only a dictionary lookup on the repository rule's process environment. It can return /opt/sdk-1, but it does not add SDK_HOME to the recorded input set. When the shell later changes to /opt/sdk-2, Bazel compares the inputs it recorded, sees no SDK_HOME dependency, and has no reason to rerun the repository rule.
That is a correctness problem, not just a performance quirk: the visible build graph can now reflect stale host state. If an environment value affects generated repository contents, make the value part of the repository rule's tracked input story.
Use getenv() for the value you branch on or write into generated files:
def _sdk_repo_impl(rctx):
sdk_home = rctx.getenv("SDK_HOME")
if sdk_home == None:
fail("SDK_HOME must be set")
rctx.file("BUILD.bazel", _build_file_for(sdk_home))
sdk_repo = repository_rule(
implementation = _sdk_repo_impl,
)
The API reference states the incremental contract directly: when building incrementally, a change to the named variable's value causes the repository to be re-fetched.1 The repository_os.environ reference states the inverse just as directly: retrieving a variable from that dictionary does not establish a dependency. Use repository_ctx.getenv or module_ctx.getenv when lookup should be tracked.2
That explicitness is deliberate. If every repository rule implicitly depended on the entire host environment, harmless changes to PWD, TMPDIR, credentials, shell-local variables, or PATH would make external repositories refetch constantly and would make cross-machine reproducibility worse. Bazel therefore makes environment tracking opt-in: use getenv() when the value is part of the repository's meaning, prefer attrs or module configuration for stable user choices, and reserve raw os.environ for diagnostic reads that do not affect generated files.
Why Repository Rules Need This Explicitness
Repository rules run during loading when Bazel needs the repository.3 Their repository_ctx can download files, execute host commands, read and write files in the external repository, and inspect the host system.4 Those powers are useful for dependency fetching, but they sit outside the execution sandbox that catches undeclared action inputs in 2.3.2 Sandboxing.
That is why this pitfall is part of the reproducibility model in 4.9.5 Reproducibility Concerns. A repo rule that silently reads ambient environment state can generate different repository contents on two machines while presenting the same apparent dependency declaration to the rest of the build. Repository rules are Bazel's reproducibility blind spot for exactly this reason: they can depend on shell environment, filesystem state, and network calls unless the rule author deliberately models those dependencies.5
The same thinking applies to less obvious environment reads. If a repository rule discovers a tool through PATH, chooses behavior based on CI, or passes selected variables into repository_ctx.execute(), treat those values as inputs. The important design question is not "can Starlark see this value?" but "will Bazel know when this value changes?"
What About environ On repository_rule()?
Older repository rules often declare environment dependencies on the rule definition:
sdk_repo = repository_rule(
implementation = _sdk_repo_impl,
environ = ["SDK_HOME"],
)
That form tells Bazel the repository depends on those variables, but current API docs mark environ as deprecated and say to migrate to repository_ctx.getenv instead.6 For new code, prefer local getenv() calls at the point where the value is needed. They keep the dependency close to the branch or generated file that uses it, and they avoid a long top-level allowlist that drifts away from the implementation.
For existing code, environ = [...] is still a useful clue during review: it means the rule author knew the repository should be invalidated by environment changes. The cleanup is to replace broad declarations and untracked os.environ reads with tracked getenv() calls, then remove stale entries once the implementation no longer relies on them.
Review Pattern
When reviewing a repository rule, scan environment access before reading the rest of the implementation:
rctx.getenv("NAME")is the normal tracked form.rctx.os.environ[...]or.get(...)is only acceptable for diagnostic or truly non-input reads. If the value affects generated repository contents, replace it.repository_rule(environ = [...])is historical API. Do not introduce it in new code.- Host-tool discovery and
repository_ctx.execute()calls should make their relevant environment explicit, because they can otherwise smugglePATH, credentials, or CI-only state into generated repository contents.
The adjacent file-dependency version of the same rule-authoring discipline is 4.9.7 watch() / watch_tree() File Dependencies: if a repo rule reads host files, declare those file dependencies too.
Environment variables are repository-rule inputs when they affect the generated external repository. Read them with repository_ctx.getenv() so Bazel can refetch the repository when they change.
Treat repository_ctx.os.environ as an untracked escape hatch. It may give you the value, but it does not give Bazel the dependency edge needed for correct incremental behavior.
Check your understanding · 3 questions
1.Why should a repository rule use repository_ctx.getenv("SDK_HOME") when SDK_HOME affects generated repository contents?
Select one answer
2.True or false: environment access in repository rules.
Choose True or False for each sentence
repository_ctx.os.environ can expose environment variables without creating a tracked repository-rule dependency.repository_rule(environ = [...]) is the preferred API for new repository-rule code.3.Which review findings should raise concern in a repository rule?
Select all that apply
Footnotes
-
repository_ctx —
getenv()return value and incremental re-fetch contract for changed environment variables. ↩1 ↩2 -
repository_os —
environdictionary note: reading it does not establish a repository-rule or module-extension dependency. ↩1 ↩2 -
.bzl files —
repository_rule()implementation functions receiverepository_ctxfor repository materialization. ↩ -
Writing Bazel rules: repository rules — repository rules use
repository_ctx, can access the host system, and need extra care around host-dependent state. ↩ -
Determinism in a Non-Hermetic World - Albert Lloveras, Canva — repository rules can depend on shell environment, filesystem, and network calls. Environment reads must be tracked to avoid stale repository results. ↩
-
.bzl files —
repository_rule(environ = ...)is deprecated and points users torepository_ctx.getenv. ↩