Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
128: Build Scripts (build.rs)
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.rsfile. - In the build script, use
std::time::SystemTime(or a crate likechronoif you prefer, thoughstdis 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 theenv!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.rsor simply omit the rerun trigger to see how Cargo handles it, but for this exercise, make sure the timestamp updates on acargo clean && cargo run).
There are no comments for now.