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
cargo fmt/cargo clippy -- -D warnings— formatting and lints, both non-negotiable in CI.cargo nextest— a test runner with per-test processes, which makes a flaky test a failure rather than a mystery.cargo audit/cargo deny— advisories and licence policy, the equivalent ofnpm auditand a lockfile check together.cargo tree -d— the duplicated-dependency report; when two majors of a crate appear, this is how you find who is holding it back.
Next: the boundary where Rust stops being safe, and how to make the unsafe part small, documented, and auditable.