Trust is earned, not given

A different perspective

2024-06-25 · Projects

Rust, part 10: Cargo as a build system — features, profiles, workspaces, cross-compilation

Part 10from the Rust series · 15 parts in all

In most ecosystems the build tool is an afterthought you learn the three commands of. In Rust, Cargo is closer to part of the language, and a surprising amount of "Rust is hard" turns out to be "feature flags are hard". Part 10 is the parts of Cargo that decide how your project builds and what ships.

Features are additive, and that is a design constraint

# Cargo.toml
[features]
default = ["tls"]
tls = ["dep:rustls", "dep:webpki-roots"]
metrics = ["dep:prometheus"]

[dependencies]
rustls = { version = "0.23", optional = true }

The rule that matters: features are unified across the dependency graph and must be additive. If any crate in the build asks for tls, you get tls. So a feature must never remove or replace behaviour — an "enable-insecure-mode" flag cannot be used to turn security off by default, because a sibling dependency can switch it back on. Model features as additions, or you will ship a configuration you never tested.

Workspaces, profiles, and what a release build actually turns on

[workspace]
members = ["crates/*", "apps/cli"]
resolver = "2"        # a workspace root defaults to resolver 1 - set this explicitly

[profile.release]
lto = "thin"          # cross-crate inlining; "fat" is slower to build, faster to run
codegen-units = 1     # one LLVM module: better optimization, worse build time
panic = "abort"       # no unwinding tables: smaller binary, no catch_unwind
strip = "symbols"     # smaller still; keep a separate copy for backtraces

Each of those is a real trade and none is a default. panic = "abort" in particular removes your ability to catch a panic, which is fine for a CLI and wrong for a server that should not die because one request panicked.

Cross-compilation is a toolchain problem, not a Cargo one

rustup target add aarch64-unknown-linux-musl
cargo build --release --target aarch64-unknown-linux-musl

# After the first build, .cargo/config.toml can make the flag permanent:
# [build]
# target = "aarch64-unknown-linux-musl"

The target triple names the whole environment, which is why -musl is the interesting suffix: a statically linked Linux binary that runs on any distribution with no glibc version to match. Practical notes: the ring/openssl family needs a cross C toolchain (cross is the crate that packages this in Docker), and cargo-zigbuild is the lighter alternative.

The tooling that is worth adopting

Next: the boundary where Rust stops being safe, and how to make the unsafe part small, documented, and auditable.