Introduction
Cargo caches build script (build.rs) results and only reruns them when it detects changes to watched files or explicitly declared environment variables. If your build script reads environment variables (like database URLs, feature flags, or version info) without declaring them via cargo:rerun-if-env-changed, Cargo will not rerun the build script when those variables change, leading to stale compiled-in configuration.
Symptoms
- Application uses outdated configuration after env var change
cargo builddoes not pick up newDATABASE_URLorAPI_KEY- Version string embedded at compile time does not update
- Works with
cargo clean && cargo buildbut not with incremental build - CI/CD pipeline deploys with stale configuration values
Example scenario: ```bash # First build DATABASE_URL=postgres://localhost/db cargo build # Application correctly uses localhost
# Change env var DATABASE_URL=postgres://production/db cargo build # Application STILL uses localhost (build.rs was not rerun!) ```
Common Causes
build.rsreadsenv::var()without emittingcargo:rerun-if-env-changed- Build script only watches source files, not environment variables
cargo:rerun-if-changedis used but env vars do not correspond to files- Build script uses
rerun-if-env-changedwith wrong variable name - CI system changes environment between builds without cleaning
Step-by-Step Fix
- 1.Declare environment variables in build.rs:
- 2.```rust
- 3.// build.rs
- 4.use std::env;
fn main() { // Tell Cargo to rerun when these env vars change println!("cargo:rerun-if-env-changed=DATABASE_URL"); println!("cargo:rerun-if-env-changed=API_KEY"); println!("cargo:rerun-if-env-changed=APP_VERSION");
let database_url = env::var("DATABASE_URL") .expect("DATABASE_URL must be set");
// Generate code with the value println!("cargo:rustc-env=DATABASE_URL={}", database_url); } ```
- 1.**Use
rerun-if-env-changedwithrustc-envtogether**: - 2.```rust
- 3.// build.rs
- 4.fn main() {
- 5.let vars = ["DATABASE_URL", "REDIS_URL", "LOG_LEVEL"];
for var in &vars { println!("cargo:rerun-if-env-changed={}", var); if let Ok(value) = std::env::var(var) { println!("cargo:rustc-env={}={}", var, value); } } }
// In application code: fn database_url() -> &'static str { env!("DATABASE_URL") } ```
- 1.Watch entire environment for development:
- 2.```rust
- 3.// build.rs - WARNING: reruns on EVERY build
- 4.fn main() {
- 5.// Use during development only
- 6.println!("cargo:rerun-if-env-changed=BUILD_TIMESTAMP");
let timestamp = chrono::Utc::now().to_rfc3339(); println!("cargo:rustc-env=BUILD_TIMESTAMP={}", timestamp); } ```
- 1.Force rebuild when needed:
- 2.```bash
- 3.# Clean and rebuild (when env vars changed without proper rerun directives)
- 4.cargo clean
- 5.cargo build
# Or touch the build.rs to force rerun touch build.rs cargo build ```
- 1.Use a configuration file instead of env vars in build.rs:
- 2.```rust
- 3.// build.rs
- 4.fn main() {
- 5.// Watch the config file instead of individual env vars
- 6.println!("cargo:rerun-if-changed=config/build.toml");
let config = std::fs::read_to_string("config/build.toml") .expect("Failed to read config/build.toml");
// Parse and generate code... } ```
Prevention
- Always pair
env::var()in build.rs withrerun-if-env-changed - Document which environment variables affect the build
- Use
cargo:rerun-if-changedfor file-based configuration - Add CI steps that verify build script rerun behavior
- Consider runtime configuration (
.envfiles, config servers) instead of compile-time - Use
cargo build -vvto see if build.rs is being rerun