Mehdi Akiki
Rust Failure Atlas / Cargo and dependencies

RFA-099 · Case file with fixtures · Case 71 of 694 · Compiler evidence

When Rust env! Fails Because a Build Variable Is Missing

env! is a compile-time requirement, not runtime configuration. Decide whether metadata is required, optional, or runtime-provided, then encode that contract reproducibly in the build.

Reviewed
Rust
Rust 1.98.1
Targets
all targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
The `env!` macro reads during compilation and intentionally treats absence as an error, unlike runtime environment lookup or `option_env!`.
First discriminating check
Decide whether the value is required, optional, or runtime configuration, then enforce that contract in the build pipeline instead of relying on a developer shell.

Compile-time metadata is convenient until a clean machine does not have it:

const BUILD_SHA: &str = env!("RFA_BUILD_SHA");

Rust 1.98.1 aborts with environment variable RFA_BUILD_SHA not defined at compile time. The failing fixture guarantees the variable is absent from its evidence contract and captures the message.

The error is not about the environment of the final executable. env! runs as part of macro expansion during compilation and embeds a string into the binary.

Build-time and runtime configuration are different products

The standard env! macro reads a variable at compile time and produces a compile error when it is missing or invalid Unicode. option_env! embeds an Option<&'static str> instead.

std::env::var reads at runtime. These choices decide when configuration is observed, whether one artifact can move between environments, and whether changes require rebuilding.

A version stamp is often build metadata. A database URL or service endpoint is usually runtime configuration. Embedding secrets is unsafe operationally because they become part of the binary and build cache.

Optional metadata should look optional

The repaired fixture uses:

const BUILD_SHA: Option<&str> = option_env!("RFA_BUILD_SHA");

fn version() -> &'static str {
    BUILD_SHA.unwrap_or("unknown")
}

This is honest when local builds may lack a revision while release builds still supply one. The UI and telemetry should preserve “unknown” rather than pretending a made-up hash is real.

If the value is mandatory for every supported build, env! is correct. The repair belongs in the build system: define and validate the variable in one reproducible command. Changing to option_env! would weaken the contract.

Developer shells are hidden inputs

A local shell may export variables through profile files, direnv, an IDE, or a previous command. CI and container builds start with different environments. A build that succeeds only because of ambient state is difficult to reproduce.

I list required build variables in the build entry point and fail with a clear message before invoking Cargo. For optional variables I provide deterministic defaults. Container builds receive explicit arguments rather than inheriting the entire host environment.

The goal is not to put every variable in one .env file. It is to make the input contract visible and avoid accidental values.

Cargo rebuild tracking needs attention

When a build script reads an environment variable and emits generated Rust or cargo:rustc-env, it should tell Cargo when changes require a rerun. Cargo supports rerun-if-env-changed directives for this purpose.

Direct env! use is tracked by the compiler's dependency information, but complex wrappers and generated files can obscure inputs. I verify by building twice, changing only the variable, and checking both the rebuilt artifact and embedded value.

Git metadata has edge cases

Embedding a commit hash sounds simple. Source archives may not contain .git, shallow clones may lack history, and the working tree may be dirty. Reproducible-build systems may intentionally avoid invoking Git.

I define what the field means: source commit provided by CI, package version, build ID, or “unknown.” If dirty state matters, it becomes a separate field. I do not run arbitrary Git commands from every crate's build script.

Unicode and bytes are separate constraints

env! expects valid Unicode. Platform environment values are not universally guaranteed to be Unicode. Build identifiers should normally use a restricted ASCII form and be validated before compilation. If arbitrary bytes matter, an environment string may be the wrong transport.

Do not embed secrets

Compile-time strings can appear in binaries, debug information, incremental caches, artifact stores, crash dumps, and container layers. Using env! does not conceal a token. Secrets should enter through a runtime secret mechanism with access control and rotation.

I add checks preventing known secret variable names from becoming compiler inputs where possible.

My decision table

For every environment read I decide:

  1. Is the value build metadata or runtime configuration?
  2. Is absence valid?
  3. Must changing it rebuild the artifact?
  4. May it enter caches and distributed artifacts?
  5. How is it supplied in local, CI, container, and source-archive builds?

Then I choose env!, option_env!, a build script, or runtime lookup. The compiler failure is not something to bypass with a random export. It is evidence that the build-input contract was implicit. A strong repair makes that contract deterministic and safe for every place the artifact is produced.