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 build does not pick up new DATABASE_URL or API_KEY
  • Version string embedded at compile time does not update
  • Works with cargo clean && cargo build but 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.rs reads env::var() without emitting cargo:rerun-if-env-changed
  • Build script only watches source files, not environment variables
  • cargo:rerun-if-changed is used but env vars do not correspond to files
  • Build script uses rerun-if-env-changed with wrong variable name
  • CI system changes environment between builds without cleaning

Step-by-Step Fix

  1. 1.Declare environment variables in build.rs:
  2. 2.```rust
  3. 3.// build.rs
  4. 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. 1.**Use rerun-if-env-changed with rustc-env together**:
  2. 2.```rust
  3. 3.// build.rs
  4. 4.fn main() {
  5. 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. 1.Watch entire environment for development:
  2. 2.```rust
  3. 3.// build.rs - WARNING: reruns on EVERY build
  4. 4.fn main() {
  5. 5.// Use during development only
  6. 6.println!("cargo:rerun-if-env-changed=BUILD_TIMESTAMP");

let timestamp = chrono::Utc::now().to_rfc3339(); println!("cargo:rustc-env=BUILD_TIMESTAMP={}", timestamp); } ```

  1. 1.Force rebuild when needed:
  2. 2.```bash
  3. 3.# Clean and rebuild (when env vars changed without proper rerun directives)
  4. 4.cargo clean
  5. 5.cargo build

# Or touch the build.rs to force rerun touch build.rs cargo build ```

  1. 1.Use a configuration file instead of env vars in build.rs:
  2. 2.```rust
  3. 3.// build.rs
  4. 4.fn main() {
  5. 5.// Watch the config file instead of individual env vars
  6. 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 with rerun-if-env-changed
  • Document which environment variables affect the build
  • Use cargo:rerun-if-changed for file-based configuration
  • Add CI steps that verify build script rerun behavior
  • Consider runtime configuration (.env files, config servers) instead of compile-time
  • Use cargo build -vv to see if build.rs is being rerun