Skip to Content
Course content

128: Build Scripts (build.rs)

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Sometimes, the standard cargo build flow isn't enough. You might need to compile a C library, generate some Rust code based on a schema, or—as we're going to do today—pull some information from the environment or a file and bake it directly into your binary at compile time. That's where build.rs comes in.

A build script is just a small Rust program that Cargo compiles and runs before it compiles your actual crate. It communicates with Cargo by printing specific instructions to stdout. Let's build a system that reads a version string from a simple text file and makes it available to our application code.

Setting up the build script

First, I'll create a file called version.txt in the root of my project. This mimics a scenario where a CI/CD pipeline or a git tag might update a version file independently of the source code.

# version.txt
1.2.3-beta

Now, I need to create the build script. The file must be named build.rs and live in the root directory (next to Cargo.toml). I'll write a simple function to read that file and tell Cargo to set an environment variable.

// build.rs
use std::fs;
use std::env;

fn main() {
    // Read the version from the file
    let version = fs::read_to_string("version.txt")
        .expect("Unable to read version.txt");
    
    // Trim whitespace/newlines
    let version = version.trim();

    // This is the "magic" part. Printing to stdout in a specific format
    // tells Cargo to set an environment variable for the actual compilation.
    println!("cargo:rustc-env=APP_VERSION={}", version);
}

Pulling the version into our code

Now that the build script is telling Cargo to set APP_VERSION, I can use the env! macro in my main.rs to grab that value. This macro is evaluated at compile time, so it doesn't add any runtime overhead.

// src/main.rs
fn main() {
    let version = env!("APP_VERSION");
    println!("Starting application version: {}", version);
}

If I run cargo run now, it works perfectly. It prints "Starting application version: 1.2.3-beta". But here is where I usually trip up when I'm in a hurry.

The "Why isn't it updating?" problem

I'm going to change version.txt to 1.2.4-alpha and run cargo run again. To my surprise, the output still says 1.2.3-beta. Why?

Cargo is smart—too smart, in this case. It looks at build.rs and sees that the build.rs file itself hasn't changed. Therefore, it assumes the build script doesn't need to run again, and it skips it to save time. It has no idea that build.rs depends on version.txt.

To fix this, I need to explicitly tell Cargo to watch that file. I'll add one more print statement to my build.rs:

// build.rs (updated)
fn main() {
    let version = fs::read_to_string("version.txt")
        .expect("Unable to read version.txt");
    
    let version = version.trim();

    println!("cargo:rustc-env=APP_VERSION={}", version);
    
    // Tell Cargo: "If version.txt changes, run this script again"
    println!("cargo:rerun-if-changed=version.txt");
}

Now, when I change the version file and run the app, Cargo sees the change, re-executes the build script, updates the environment variable, and recompiles main.rs. Everything is in sync.

A note on the OUT_DIR

In this example, we used environment variables, which are great for simple strings. However, if you were generating entire .rs files, you shouldn't write them back into your src/ folder. That would be messy and could trigger infinite rebuild loops.

Instead, Cargo provides an environment variable called OUT_DIR. Your build script should write generated files there, and you can include them in your code using the include!(concat!(env!("OUT_DIR"), "/generated.rs")); macro. It keeps the project directory clean and keeps Cargo's build cache happy.




📋 Practical Task

Embedding a Compile-Time Timestamp

In a real production app, it's incredibly useful to know exactly when a binary was built without relying on the file system's metadata. Your task is to modify a project to include a build timestamp.

Requirements:

  • Create a build.rs file.
  • In the build script, use std::time::SystemTime (or a crate like chrono if you prefer, though std is sufficient) to get the current date and time.
  • Pass this timestamp to the compiler as an environment variable named BUILD_TIME.
  • In your main.rs, use the env! macro to print "Binary built on: [timestamp]" when the program runs.
  • Ensure that the build script runs every time you build (since the time always changes, you can use cargo:rerun-if-changed=build.rs or simply omit the rerun trigger to see how Cargo handles it, but for this exercise, make sure the timestamp updates on a cargo clean && cargo run).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.