Cargo, Dependencies & Workspaces
Prerequisites: Modules, crates & packages
Cargo is Rust's build tool and package manager. Every package is described by
a Cargo.toml manifest, and third-party libraries (called crates) come
from the public registry at crates.io.
[package]names the package, its version and the Rust edition (the set of language rules it is compiled with).[dependencies]lists crates your code uses.[dev-dependencies]lists crates needed only by tests, examples and benchmarks.Cargo.lockrecords the exact version of every crate that was chosen, so every build uses the same code. Cargo creates and updates it for you; commit it to version control and don't edit it by hand.
Syntax
[package]
name = "weather"
version = "0.1.0"
edition = "2024"
[dependencies]
rand = "0.9" # any 0.9.x (0.9.0 up to, not including, 0.10.0)
serde = { version = "1.0", features = ["derive"] } # any 1.x from 1.0.0, with an optional feature on
greeting = { path = "../greeting" } # a crate on your own disk
[dev-dependencies]
pretty_assertions = "1"
cargo add rand # add the latest compatible version to Cargo.toml
cargo add serde --features derive # add with a feature enabled
cargo remove rand # take it out again
cargo update # move to newer compatible versions, rewriting Cargo.lock
cargo tree # show the full dependency tree
Version requirements
A version like "1.4.2" is shorthand for ^1.4.2: Cargo may pick any
semver-compatible release, meaning >=1.4.2 and <2.0.0. For versions below
1.0 the second number is treated as the breaking one, so "0.9" allows 0.9.x
but not 0.10. cargo update never crosses a breaking boundary; changing to a
new major version means editing Cargo.toml. Other forms exist ("=1.4.2"
for an exact pin, ">=1.2, <1.5" for a range) but are rarely needed.
Features
A feature is a named switch that turns optional code on. Crates declare
them in a [features] table and check them with cfg. Many crates keep extras
behind features (serde's derive macro is the classic example), so if the
documentation mentions something you can't find, check whether a feature is
needed.
Workspaces
A workspace is a set of packages that share one Cargo.lock and one
target/ build directory. It is the usual way to split a larger project into
a library and an application.
Example
greet-workspace/
├── Cargo.toml
├── app/
│ ├── Cargo.toml
│ └── src/main.rs
└── greeting/
├── Cargo.toml
└── src/lib.rs
greet-workspace/Cargo.toml (a workspace root has no [package]):
[workspace]
members = ["app", "greeting"]
resolver = "3"
greeting/Cargo.toml:
[package]
name = "greeting"
version = "0.1.0"
edition = "2024"
[features]
shout = []
greeting/src/lib.rs:
pub fn hello(name: &str) -> String {
let text = format!("Hello, {name}!");
if cfg!(feature = "shout") {
text.to_uppercase()
} else {
text
}
}
app/Cargo.toml:
[package]
name = "app"
version = "0.1.0"
edition = "2024"
[dependencies]
greeting = { path = "../greeting", features = ["shout"] }
app/src/main.rs:
fn main() {
println!("{}", greeting::hello("Ada"));
}
Run it from the workspace root with cargo run -p app.
Expected output
HELLO, ADA!
Remove features = ["shout"] from app/Cargo.toml and the output becomes
Hello, Ada!.
Common mistakes
- Using
"*"as a version. It accepts any release, including ones with breaking changes, and crates.io rejects packages that do it. - Forgetting a feature. With
serde = "1.0"(noderivefeature),#[derive(Serialize)]fails witherror: cannot find derive macro `Serialize` in this scope. - Expecting
cargo updateto jump to a new major version. It only moves within the range yourCargo.tomlallows. - Deleting
Cargo.lockto "fix" a build. That silently upgrades every dependency at once and makes bugs hard to trace. - Running
cargo runin a workspace root that has several binaries without-p <package>to say which one to run.
Practice
Create a new package, add the rand crate at version 0.9 with cargo add,
and print a random dice roll between 1 and 6.
Show solution
cargo new dice
cd dice
cargo add rand@0.9
src/main.rs:
fn main() {
let roll: u8 = rand::random_range(1..=6);
println!("You rolled {roll}");
}
The number changes on each run, for example You rolled 4.