The Ecosystem & Tooling - Cargo Does Everything
If you've come from a language where the toolchain is a pile of separate things you bolt together - a build tool here, a package manager there, a formatter you had to be talked into, a test runner with its own config file - Rust's tooling story feels like a vacation. There's basically one tool, Cargo, and it does almost everything. It came with your Rust install in Phase 1, and you'll spend your whole Rust life in it.
The point of this phase isn't to memorize commands - it's to give you the mental model, what each tool is for and why it exists, so the commands make sense and you reach for the right one without thinking.
📝 Terminology. A crate is Rust's word for a package - a library or program. crates.io is the public registry where the community publishes crates (like npm for JavaScript or PyPI for Python). A dependency is a crate your project uses. (The backend of this very project - The Missing Manual's search engine - is a Rust crate built and tested with exactly these commands.)
Cargo: your one tool
What it actually is. Cargo is Rust's build tool and package manager and test runner, rolled into one. Where other ecosystems make you assemble that toolchain yourself, Rust ships it as a single, opinionated program: every Rust project on earth is built and tested the same way, so you can drop into any repo already knowing the commands.
Here are the four you'll use constantly, each the same cargo <verb> shape.
cargo build - compile your project.
$ cargo build
Compiling myapp v0.1.0 (/home/you/myapp)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
What just happened: Cargo compiled your code (and any dependencies not yet built) and dropped the binary in target/debug/. "unoptimized + debuginfo" means a fast-to-compile debug build, great for development, not shipping. When ready to ship, cargo build --release produces an optimized binary in target/release/ that's much faster to run (and slower to compile). The runtime-overflow check from Phase 9 is on in debug and off in release, another reason the two profiles exist.
cargo run - build and run in one step.
$ cargo run
Compiling myapp v0.1.0 (/home/you/myapp)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.40s
Running `target/debug/myapp`
Hello, world!
What just happened: cargo run is cargo build plus "now execute the binary." The command you'll hammer most while developing - edit, cargo run, repeat. If nothing changed since the last build, it skips straight to "Running" without recompiling.
cargo add - add a dependency from crates.io.
$ cargo add serde
Updating crates.io index
Adding serde v1.0.219 to dependencies
What just happened: Cargo looked up serde (a popular serialization crate) on crates.io, picked a compatible version, and wrote it into Cargo.toml. The next cargo build downloads and compiles it. Before cargo add existed you edited Cargo.toml by hand and guessed the version string - now the tool does it correctly.
cargo test - run your tests (we'll come back to this below).
Cargo.toml and Cargo.lock: the manifest and the receipt
What Cargo.toml actually is. Your project's manifest - a small, human-edited file declaring the project's name, version, and dependencies. After cargo add serde, it looks like this:
[package]
name = "myapp"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = "1.0.219"
What just happened: [package] is your project's identity. [dependencies] lists the crates you depend on and the version ranges you accept ("1.0.219" means "1.0.219 or any compatible 1.x"). This is the file to read and edit to understand or change what a project depends on.
⚠️ Cargo.lock is different - don't hand-edit it. Alongside Cargo.toml, Cargo maintains Cargo.lock: the exact versions of every dependency (and sub-dependency) actually resolved. Think of Cargo.toml as "what I asked for" and Cargo.lock as "what I got, precisely." It's auto-generated; commit it (for applications) so teammates and CI build the identical dependency set, but never edit it yourself.
rustfmt: stop arguing about formatting
What it actually is. rustfmt is the official code formatter. Run cargo fmt and it rewrites your code into the standard Rust style - indentation, spacing, line breaks - instantly and consistently.
Why it exists. Formatting debates are a waste of a team's life. With one official formatter everyone runs, every codebase looks the same, diffs stay clean, and nobody reviews a PR complaining about brace placement. You stop thinking about formatting at all.
$ cargo fmt
What just happened: It said nothing and changed your files in place - that's success. cargo fmt is silent when it works. (In CI, cargo fmt --check instead reports any unformatted file and exits non-zero, without changing anything - that's how teams enforce it.) Run it before every commit and formatting stops being your problem.
clippy: the linter that teaches you Rust
What it actually is. Clippy is Rust's linter, genuinely one of the best in any language. Where the compiler tells you what's wrong, clippy tells you what's not idiomatic: code that works but that an experienced Rust developer would write differently. It's earned its reputation as a patient teacher, since each warning explains the better way and why.
$ cargo clippy
Checking myapp v0.1.0 (/home/you/myapp)
warning: length comparison to zero
--> src/main.rs:3:8
|
3 | if name.len() > 0 {
| ^^^^^^^^^^^^^^ help: using `!is_empty` is clearer and more explicit: `!name.is_empty()`
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.95.0/index.html#len_zero
= note: `#[warn(clippy::len_zero)]` on by default
What just happened: The code compiles fine - clippy isn't reporting an error. It's noticing name.len() > 0 is a roundabout way of saying "not empty," and suggests the clearer !name.is_empty(), with a link to a fuller explanation. Follow clippy's advice for a few weeks and you'll absorb idiomatic Rust by osmosis, like a senior reviewer commenting on every line, for free.
💡 Key point. The everyday loop: cargo fmt, then cargo clippy, then cargo test, before you commit. Format, lint, verify. Many teams wire all three into CI so nothing un-formatted, un-lint-clean, or un-tested can merge.
cargo test: tests live next to your code
What it actually is. Rust has testing built into the language and Cargo - no separate framework to install, no config file. Mark a function #[test], put your tests in a module beside the code they test, and cargo test finds and runs them all.
$ cargo test
Compiling myapp v0.1.0 (/home/you/myapp)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.13s
Running unittests src/main.rs (target/debug/deps/myapp-8e8c850d1f800cd0)
running 2 tests
test tests::adds ... ok
test tests::adds_negatives ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
What just happened: Cargo compiled a special test build, found both #[test] functions, ran them, and reported each one. assert_eq!(add(2, 2), 4) checks the two values are equal and fails the test (with a clear diff) otherwise. #[cfg(test)] on the module means "only compile this when testing" - tests add zero weight to your shipped binary. Sitting tests right next to the code they cover is deliberate: writing one is so easy you actually do it.
⏭️ This is just enough to run tests. For how to think about testing - what to test, how much, and why - see the Testing category guides.
Recap
- Cargo is the one tool:
cargo build(compile),cargo run(build + run),cargo add <crate>(add a dependency),cargo test(run tests). Cargo.tomlis the manifest you edit (name, version, dependencies);Cargo.lockis the auto-generated exact-versions receipt you commit but don't touch.- crates.io is the public crate registry;
cargo addpulls from it and updatesCargo.tomlfor you. cargo fmt(rustfmt) formats your code to the one standard style - silent when it succeeds.cargo clippyis the famously helpful linter: it teaches idiomatic Rust by explaining the better way, not just flagging the wrong way.cargo testruns#[test]functions that live right beside your code - testing is built in, no framework required.
← Phase 7: Errors & I/O · Phase 9: Idioms & Common Gotchas →
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
1. What is Cargo?
2. What is the difference between Cargo.toml and Cargo.lock?
3. What does clippy do that the compiler does not?