Cargo Build Scripts
A Cargo build script (build.rs placed at the root of a package next to Cargo.toml) is a small Rust program that Cargo compiles and executes before compiling the package's library and binary targets. Build scripts are used to generate Rust source code at build time (such as Protocol Buffer bindings or lookup tables), compile bundled C/C++ libraries, link native system libraries, or emit custom conditional compilation flags.
`cargo build` Execution Lifecycle with `build.rs`:
1. Compile `build.rs` (for Host Architecture, linking `[build-dependencies]`)
2. Run `build.rs` executable:
- Reads `CARGO_*` and `OUT_DIR` environment variables
- Writes generated files into `$OUT_DIR/generated.rs`
- Prints `cargo::...` instructions to `stdout`
3. Compile `src/lib.rs` / `src/main.rs` (for Target Architecture):
- Includes `$OUT_DIR/generated.rs` via `include!(concat!(env!("OUT_DIR"), "/generated.rs"))`
- Applies `--cfg`, env vars, and native linker flags emitted by `build.rs`
build.rs Output Instructions (cargo:: Syntax)
A build script communicates with Cargo by printing lines prefixed with cargo:: to standard output (println!("cargo::...")). Note that Cargo 1.77+ and the Rust 2024 edition standardize on the double-colon cargo:: syntax (replacing the older single-colon cargo: syntax):
stdout Instruction from build.rs |
Effect on Package Compilation |
|---|---|
cargo::rerun-if-changed=PATH |
Tells Cargo to re-run build.rs only if PATH changes (always emit this so build.rs doesn't re-run on every single source edit!). |
cargo::rerun-if-env-changed=VAR |
Tells Cargo to re-run build.rs if the environment variable VAR changes. |
cargo::rustc-cfg=KEY / KEY="VAL" |
Passes --cfg KEY to rustc when compiling the package. |
cargo::rustc-check-cfg=cfg(KEY) |
Registers KEY as a valid expected cfg name for Rust 2024's unexpected_cfgs lint check! |
cargo::rustc-env=VAR=VALUE |
Sets a compile-time environment variable readable via env!("VAR") in src/. |
cargo::rustc-link-lib=z |
Instructs rustc to link against native library libz (-l z). |
cargo::rustc-link-search=native=DIR |
Adds DIR to the native library linker search path (-L native=DIR). |
cargo::warning=MESSAGE |
Displays a compiler warning line in cargo build output. |
Complete Example: Generating Code into OUT_DIR
Build scripts must never modify or write files inside src/. All generated source files must be written to the directory specified by the OUT_DIR environment variable, and then included into src/lib.rs or src/main.rs using include!(concat!(env!("OUT_DIR"), "/...")).
1. Inside build.rs:
use std::env;
use std::fs;
use std::path::PathBuf;
// In a real `build.rs`, this `generate_build_info` runs as `fn main()`:
fn generate_build_info(out_dir: &PathBuf) {
// 1. Only re-run build.rs if `build.rs` itself changes
println!("cargo::rerun-if-changed=build.rs");
// 2. Register and enable a custom cfg flag cleanly for Rust 2024 `check-cfg`
println!("cargo::rustc-check-cfg=cfg(has_lookup_table)");
println!("cargo::rustc-cfg=has_lookup_table");
// 3. Write generated Rust code into `OUT_DIR`
let dest_path = out_dir.join("squares.rs");
let generated_code = "pub const SQUARES: [u32; 5] = [0, 1, 4, 9, 16];\n";
fs::write(&dest_path, generated_code).expect("failed to write squares.rs");
}
fn main() {
let simulated_out_dir = env::temp_dir().join("rust_ch56_build_rs_demo");
fs::create_dir_all(&simulated_out_dir).unwrap();
generate_build_info(&simulated_out_dir);
let generated = fs::read_to_string(simulated_out_dir.join("squares.rs")).unwrap();
println!("Generated in OUT_DIR: {}", generated.trim());
let _ = fs::remove_dir_all(&simulated_out_dir);
}
cargo::rerun-if-changed=build.rs
cargo::rustc-check-cfg=cfg(has_lookup_table)
cargo::rustc-cfg=has_lookup_table
Generated in OUT_DIR: pub const SQUARES: [u32; 5] = [0, 1, 4, 9, 16];
2. Inside src/lib.rs:
// include!(concat!(env!("OUT_DIR"), "/squares.rs"));