5 min read #rust #Cargo and Build
On this page

Cargo.toml Deep Dive

Cargo.toml is more than just "listing dependencies"—features handle conditional compilation and optional functionality, profiles control optimization levels for debug/release builds, and build scripts execute arbitrary code before compilation (generating bindings, compiling C libraries). The correct way to write each configuration corresponds to a common pitfall.

[dependencies]: Version Specification

[dependencies]
serde = "1"                          # ^1.0.0: >=1.0.0, <2.0.0 (semver compatible)
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"                       # ^1.0.0

[dev-dependencies]
criterion = "0.5"                    # Only available in tests/benches

[build-dependencies]
cc = "1"                             # Only available in build.rs

[target.'cfg(target_os = "linux")'.dependencies]
x11 = "2"                            # Linux only

Version Rules: Cargo uses semver (Semantic Versioning). "1.2.3" = exact match, ^1.2.3 (default) = >=1.2.3 and <2.0.0. Cargo.lock pins exact versions—library crates should not commit the lockfile, while binary crates should.

[features]: Conditional Compilation

[features]
default = ["std", "tls"]
std = []
tls = ["tokio", "rustls"]            # Activates tokio + rustls
json = ["serde", "serde_json"]
nightly = []

Usage: cargo build --no-default-features --features "json,nightly". Features are additive—you can only add code, not remove it. Two independent features cannot conflict with each other.

[profile]: Compilation Optimization

[profile.release]
opt-level = 3                        # Maximum optimization (0-3, s=size, z=size aggressively)
lto = "fat"                          # Link-Time Optimization
codegen-units = 1                    # Single codegen unit → more inlining, slower compilation
panic = "abort"                      # No unwinding (smaller binary, suitable for embedded)

[profile.dev]
opt-level = 0                        # Fast compilation, no optimization
debug = true

Release Configuration Trade-offs: opt-level=3 + lto=fat + codegen-units=1 = smallest and fastest binary, but longest compilation time (possibly 10-30 minutes). Use defaults for daily development (opt-level=3, thin lto, codegen-units=16), and highest settings in CI.

[workspace.dependencies]: Shared Versions

# root Cargo.toml:
[workspace.dependencies]
serde = "1"
tokio = "1"

[dependencies]
serde = { workspace = true }         # References the workspace version

This feature (1.64+) solves the classic problem of "inconsistent dependency versions across multiple crates"—upgrading serde requires changing only one place.

build.rs: Compile-Time Scripts

// build.rs — executed **before** Cargo compiles the main code:
fn main() {
    println!("cargo:rustc-link-lib=z");      // Link libz
    println!("cargo:rerun-if-changed=wrapper.h");  // Re-run only if the header changes
    cc::Build::new().file("src/c/wrapper.c").compile("wrapper");
}

build.rs is commonly used for: compiling C/C++ dependencies, detecting system libraries, code generation (protobuf, OpenAPI), and setting environment variables. Output is in the OUT_DIR environment variable—managed by Cargo, no manual cleanup needed.

References

  • Cargo Book: doc.rust-lang.org/cargo
  • The Cargo Book: profiles, features, build scripts

Keywords: Cargo.toml, features, dependencies, profile, LTO, build.rs, workspace