2. The toolchain: cargo, crates, editions, and what cargo build really does
2.1 The commands you will type every day
| Command | What it does |
|---|---|
cargo new app / cargo new --lib mylib |
create a binary or library crate |
cargo build / cargo build --release |
compile to target/debug/ or target/release/ |
cargo run -- args |
build and run; arguments after -- go to your program |
cargo check |
type-check and borrow-check without generating code; the fastest feedback loop |
cargo test |
build and run unit, integration, and doc tests |
cargo clippy |
lints, about 800 of them; treat its advice as code review |
cargo fmt |
format; like gofmt, nobody argues about style |
cargo doc --open |
build HTML docs for your crate and every dependency |
cargo add serde --features derive |
add a dependency to Cargo.toml |
cargo tree |
show the dependency graph |
cargo expand (plugin) |
show code after macro expansion |
2.2 What cargo build actually does
The C guide §2.1 splits compilation into preprocess → compile → assemble → link. Rust's pipeline has different stages, and the borrow checker sits in the middle:
source (.rs)
│ parse, expand macros (no textual preprocessor; macros work on tokens)
▼
AST ──▶ HIR (name resolution, type inference, trait resolution)
▼
MIR (the control-flow graph that the BORROW CHECKER runs on)
│ borrow check, const eval, MIR optimizations
▼
LLVM IR ──▶ LLVM optimizer ──▶ object files (.o) ← one per "codegen unit"
▼
linker (cc / ld / lld) ──▶ executable, statically linked with Rust deps, dynamically with libc
Consequences you will notice:
- The unit of compilation is the crate, not the file. A crate is a
library or binary built from a tree of modules. That is why
cargo checktype-checks your whole crate at once. - Generics are compiled per concrete type (monomorphization), so generic-heavy code is fast but compiles slowly and produces bigger binaries.
- Rust dependencies are linked statically into your binary.
ldd target/release/appon Linux shows only libc and friends.
cargo rustc --release -- --emit=asm # see the assembly (target/release/deps/*.s)
cargo rustc -- -Zunpretty=mir # MIR needs nightly; use https://play.rust-lang.org's "MIR" button
2.3 Cargo.toml, Cargo.lock, and editions
[package]
name = "hello"
version = "0.1.0"
edition = "2024" # language edition: 2015, 2018, 2021, 2024
[dependencies]
serde = { version = "1", features = ["derive"] } # "1" means ^1.0.0: any 1.x >= 1.0.0
[profile.release]
lto = "thin" # link-time optimization across crates
codegen-units = 1 # slower build, faster binary
panic = "abort" # smaller binary; no unwinding (see Ch 10.5)
Cargo.lockrecords the exact version of every transitive dependency. Commit it for applications and for libraries (current Cargo guidance). It does for Rust whatgo.sumdoes for Go, and it is the input tocargo audit(Ch 55).- Editions are opt-in language versions. They change syntax details, not the ABI. Crates from different editions link together freely, so the ecosystem never splits the way Python 2/3 did. Edition 2024 shipped in Rust 1.85 (February 2025).
2.4 Cross-compilation
rustup target add x86_64-unknown-linux-musl # fully static Linux binaries
cargo build --release --target x86_64-unknown-linux-musl
rustup target add aarch64-unknown-linux-gnu # needs a cross linker; see Ch 66 for `cross`
Compare Go §2.3
(GOOS=linux GOARCH=arm64 go build simply works) and the
C guide §41.1
(you need a full cross toolchain). Rust sits between them: the compiler can
target anything LLVM supports, but you still need a linker and a libc for
the target. Ch 66 covers cross and cargo-zigbuild, which solve this.