Rust’s build system, Cargo, has a surprisingly flexible mechanism for injecting environment variables directly into your build process, allowing for dynamic configuration without hardcoding.

Let’s see it in action. Imagine you have a Rust project and you want to conditionally enable a feature or set a version number based on an environment variable.

Here’s a simple src/main.rs:

fn main() {
    #[cfg(feature = "my_feature")]
    println!("My feature is enabled!");

    let version = env!("CARGO_PKG_VERSION");
    println!("Building version: {}", version);

    #[cfg(debug_assertions)]
    println!("Debug build!");

    #[cfg(not(debug_assertions))]
    println!("Release build!");
}

And a Cargo.toml:

[package]
name = "env_build_example"
version = "0.1.0"
edition = "2021"

[dependencies]

[features]
my_feature = []

Now, let’s build this. By default, if you just run cargo build, you’ll see:

   Compiling env_build_example v0.1.0 (...)
    Finished dev [unoptimized +debuginfo] target(s) in X.XXs
     Running `target/debug/env_build_example`
Debug build!
Building version: 0.1.0

Notice "Debug build!" and "Building version: 0.1.0". The version comes from CARGO_PKG_VERSION, which Cargo automatically sets.

What if we want to enable my_feature? We can do this by setting an environment variable before running cargo build.

On Linux/macOS:

MY_FEATURE_ENABLED=1 cargo build --features my_feature

On Windows (cmd.exe):

set MY_FEATURE_ENABLED=1 && cargo build --features my_feature

On Windows (PowerShell):

$env:MY_FEATURE_ENABLED=1; cargo build --features my_feature

Now, when we run the executable:

target/debug/env_build_example

We’ll see:

My feature is enabled!
Building version: 0.1.0
Debug build!

The my_feature conditional compilation worked because we passed --features my_feature to Cargo. But how do we dynamically control features or other build-time aspects based on environment variables?

This is where build.rs scripts come in. If you have a build.rs file in your project’s root, Cargo will execute it before compiling your crate. You can use this script to print environment variables or other instructions to Cargo.

Let’s create build.rs:

use std::env;

fn main() {
    // Print the value of MY_BUILD_VAR to stdout. Cargo will capture this.
    if let Ok(val) = env::var("MY_BUILD_VAR") {
        println!("cargo:rustc-env=MY_BUILD_VAR={}", val);
    }

    // You can also set other Cargo flags.
    // For example, to conditionally enable a feature based on an env var:
    if env::var("ENABLE_SPECIAL_FEATURE").is_ok() {
        println!("cargo:rustc-cfg=special_feature_enabled");
    }

    // Cargo will automatically re-run this script if any of the
    // environment variables it depends on change.
    println!("cargo:rerun-if-env-changed=MY_BUILD_VAR");
    println!("cargo:rerun-if-env-changed=ENABLE_SPECIAL_FEATURE");
}

And modify src/main.rs to use these:

fn main() {
    #[cfg(feature = "my_feature")]
    println!("My feature is enabled!");

    #[cfg(special_feature_enabled)]
    println!("Special feature is enabled via build.rs!");

    let version = env!("CARGO_PKG_VERSION");
    println!("Building version: {}", version);

    // Access the environment variable set by build.rs
    if let Some(build_var_value) = env!("MY_BUILD_VAR") {
        println!("MY_BUILD_VAR is set to: {}", build_var_value);
    } else {
        println!("MY_BUILD_VAR is not set.");
    }

    #[cfg(debug_assertions)]
    println!("Debug build!");

    #[cfg(not(debug_assertions))]
    println!("Release build!");
}

Now, let’s build with these variables set.

On Linux/macOS:

MY_BUILD_VAR="hello_from_env" ENABLE_SPECIAL_FEATURE=1 cargo build

On Windows (cmd.exe):

set MY_BUILD_VAR=hello_from_env && set ENABLE_SPECIAL_FEATURE=1 && cargo build

On Windows (PowerShell):

$env:MY_BUILD_VAR="hello_from_env"; $env:ENABLE_SPECIAL_FEATURE=1; cargo build

Running the executable now:

target/debug/env_build_example

Output:

Special feature is enabled via build.rs!
Building version: 0.1.0
MY_BUILD_VAR is set to: hello_from_env
Debug build!

The build.rs script printed cargo:rustc-env=MY_BUILD_VAR=hello_from_env. Cargo interprets this as "set an environment variable named MY_BUILD_VAR with the value hello_from_env that will be available during the compilation of the current crate." This is why env!("MY_BUILD_VAR") works inside your Rust code.

Similarly, cargo:rustc-cfg=special_feature_enabled tells Cargo to define a compilation configuration flag special_feature_enabled for the current crate. This is what allows #[cfg(special_feature_enabled)] to work.

Cargo’s build scripts are executed in a sandboxed environment. They can’t directly access your crate’s code or dependencies. Instead, they communicate with Cargo by printing specific directives to standard output. These directives, like cargo:rustc-env=KEY=VALUE or cargo:rustc-cfg=FLAG, are how you influence the build. The rerun-if-env-changed directives are crucial for performance; they tell Cargo which environment variables, if changed, should trigger a rebuild of the crate, otherwise, Cargo will cache the build artifact.

The env! macro in Rust is a compile-time macro that can only access environment variables that are already set in the environment where cargo build is run or that have been explicitly defined using cargo:rustc-env= in a build script. It cannot dynamically fetch them at runtime.

This mechanism is powerful for managing build-time configuration, such as API keys for service integrations that only run during the build (e.g., fetching data), versioning information derived from Git, or enabling/disabling features based on the target environment.

The next step is often to explore how to manage these environment variables more systematically, perhaps using .env files with tools like dotenv in your build.rs script.

Want structured learning?

Take the full Cargo course →