The Cargo Guide
Cargo Mental Model and Package System
Why Cargo Exists
Cargo is Rust's standard build tool, package manager, and workflow driver. It sits above rustc, which is the Rust compiler.
A useful mental model is:
rustccompiles Rust source code into artifacts- Cargo decides what to compile, in what order, with which dependencies, using which profiles, and with which project conventions
If you were working without Cargo, even a small project would require you to manually:
- pass compiler flags
- locate dependencies
- compile dependencies in the right order
- manage output paths
- separate dev/test/release workflows
- package or publish code
Cargo standardizes all of that.
For a beginner, the most important insight is this: Cargo is not just a command runner. It is the system that gives Rust projects a predictable shape.
cargo build
cargo run
cargo testThose commands feel simple because Cargo is carrying a lot of structure for you.
Cargo and rustc
It is common to confuse Cargo with the compiler, so it helps to separate their responsibilities.
rustc is the compiler. It takes Rust source and compiles it.
rustc hello.rsThat can work for a single file, but it does not scale well once you introduce dependencies, tests, multiple binaries, or reusable libraries.
Cargo orchestrates rustc.
cargo new hello_cargo
cd hello_cargo
cargo runCargo will:
- locate
Cargo.toml - determine what package and targets exist
- resolve dependencies
- invoke
rustcappropriately - place build output in
target/
This means Cargo is best understood as the project and build coordinator, while rustc is the actual compiler engine.
The Smallest Useful Cargo Project
A new binary project created with Cargo looks like this:
cargo new hello_cargoDirectory layout:
hello_cargo/
āāā Cargo.toml
āāā src/
āāā main.rsThe generated manifest:
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2024"
[dependencies]The generated Rust program:
fn main() {
println!("Hello, world!");
}This already demonstrates Cargo's core model:
Cargo.tomldescribes the packagesrc/main.rsis discovered by convention as a binary targetcargo runknows what to build without extra configuration
That convention-first behavior is one of Cargo's biggest strengths.
Convention Over Configuration
Cargo prefers standard file locations and names so that most projects require very little setup.
For example:
src/main.rsmeans "this package has a binary target"src/lib.rsmeans "this package has a library target"src/bin/*.rsmeans "additional binaries"examples/contains example programstests/contains integration testsbenches/contains benchmarks
A package with both a library and a binary might look like this:
my_app/
āāā Cargo.toml
āāā src/
ā āāā lib.rs
ā āāā main.rs
āāā examples/
ā āāā demo.rs
āāā tests/
ā āāā api_test.rs
āāā benches/
āāā parser_bench.rsBecause Cargo knows these conventions, it can often discover your project structure automatically.
This reduces boilerplate, but more importantly it lets Rust developers quickly understand unfamiliar repositories.
Package vs Crate vs Target vs Workspace
These terms are related, but they are not interchangeable.
A package is what Cargo.toml describes. It is the unit Cargo works with when building, testing, publishing, and resolving dependencies.
A crate is a compilation unit in Rust. A crate can be a binary crate or a library crate.
A target is something Cargo can build from a package, such as a library, binary, test, example, or benchmark.
A workspace is a group of related packages managed together.
A simple way to think about the hierarchy is:
- workspace: groups packages
- package: described by one
Cargo.toml - target: one buildable output within a package
- crate: the Rust compilation unit behind a target
Example package with one library and two binaries:
my_tool/
āāā Cargo.toml
āāā src/
āāā lib.rs
āāā main.rs
āāā bin/
āāā helper.rsIn this layout:
- there is one package:
my_tool - there is one library target from
src/lib.rs - there is one default binary target from
src/main.rs - there is one additional binary target from
src/bin/helper.rs - each target corresponds to a crate compiled by Rust
That distinction matters because people often say "crate" when they really mean package.
Binary Crates and Library Crates
A binary crate produces an executable program. A library crate produces reusable code that other Rust code can call.
Binary crate example:
fn main() {
println!("I am an executable");
}Library crate example:
pub fn greet(name: &str) -> String {
format!("Hello, {name}!")
}If a package contains only src/main.rs, it is just a binary package.
If it contains only src/lib.rs, it is just a library package.
If it contains both, then the package can expose reusable code and also ship a runnable program.
Example:
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}// src/main.rs
fn main() {
let sum = my_app::add(2, 3);
println!("2 + 3 = {sum}");
}This is a very common pattern in Rust because it keeps most logic in the library, which is easier to test and reuse.
Targets: More Than Just the Main Program
Beginners often think a Cargo project builds one thing. In practice, Cargo can manage multiple kinds of targets within a single package.
Common target types:
- library target
- binary target
- example target
- test target
- benchmark target
Example layout:
calculator/
āāā Cargo.toml
āāā src/
ā āāā lib.rs
ā āāā main.rs
āāā examples/
ā āāā quickstart.rs
āāā tests/
ā āāā integration_test.rs
āāā benches/
āāā speed.rsEach of these serves a different role:
src/lib.rs: shared logicsrc/main.rs: executable applicationexamples/quickstart.rs: runnable usage exampletests/integration_test.rs: integration tests that use the package from the outsidebenches/speed.rs: performance measurement code
This matters because Cargo is designed around the full lifecycle of a Rust package, not just compilation.
Examples, Tests, and Benches as First-Class Parts of a Package
Cargo treats examples, tests, and benches as normal parts of project structure rather than afterthoughts.
An example program:
// examples/quickstart.rs
use calculator::add;
fn main() {
println!("3 + 4 = {}", add(3, 4));
}Run it with:
cargo run --example quickstartAn integration test:
// tests/integration_test.rs
use calculator::add;
#[test]
fn adds_numbers() {
assert_eq!(add(3, 4), 7);
}Run tests with:
cargo testA benchmark file might live here:
benches/speed.rsEven before a learner studies benchmarks deeply, seeing them in the package model helps them understand that Cargo is organizing many development activities under one system.
How Cargo Discovers a Project
Cargo generally starts by looking for Cargo.toml in the current directory.
pwd
# /home/user/projects/hello_cargo
cargo buildCargo reads Cargo.toml, then interprets the package and target layout using both manifest contents and conventional paths.
For many projects, that means you do not need to explicitly list every target.
For example, this works with no extra configuration:
my_tool/
āāā Cargo.toml
āāā src/
āāā lib.rs
āāā main.rs
āāā bin/
āāā admin.rsCargo can infer:
- library target from
src/lib.rs - default binary target from
src/main.rs - extra binary target from
src/bin/admin.rs
This discovery model is one reason Cargo feels simple at first. The simplicity is not because the system is small; it is because the conventions are strong.
Reading Cargo.toml as a Mental Model
A beginner often sees Cargo.toml as just a config file. A better mental model is that it is the package contract.
Simple binary package:
[package]
name = "notes_app"
version = "0.1.0"
edition = "2024"
[dependencies]Library package with a dependency:
[package]
name = "text_utils"
version = "0.1.0"
edition = "2024"
[dependencies]
regex = "1"What Cargo gets from this:
- package identity
- package version
- Rust edition
- dependency requirements
From there, Cargo combines manifest information with directory conventions to decide what to build.
That combination is central to understanding Cargo: the manifest declares intent, and the file layout supplies structure.
A Concrete Multi-Target Example
Consider a package named weather_tools.
weather_tools/
āāā Cargo.toml
āāā src/
ā āāā lib.rs
ā āāā main.rs
ā āāā bin/
ā āāā convert.rs
āāā examples/
ā āāā sample_report.rs
āāā tests/
āāā temp_test.rsManifest:
[package]
name = "weather_tools"
version = "0.1.0"
edition = "2024"
[dependencies]Library code:
// src/lib.rs
pub fn c_to_f(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}Main binary:
// src/main.rs
fn main() {
let f = weather_tools::c_to_f(20.0);
println!("20C = {f}F");
}Additional binary:
// src/bin/convert.rs
fn main() {
let f = weather_tools::c_to_f(30.0);
println!("30C = {f}F");
}Example:
// examples/sample_report.rs
use weather_tools::c_to_f;
fn main() {
println!("Forecast conversion: {}", c_to_f(12.5));
}Integration test:
// tests/temp_test.rs
use weather_tools::c_to_f;
#[test]
fn freezing_point_converts_correctly() {
assert_eq!(c_to_f(0.0), 32.0);
}Useful commands:
cargo run
cargo run --bin convert
cargo run --example sample_report
cargo testThis single package demonstrates the difference between reusable logic, runnable programs, examples, and tests.
Workspace Preview
A workspace is a collection of related packages managed together.
This is important once a project outgrows a single package.
Example layout:
my_workspace/
āāā Cargo.toml
āāā app/
ā āāā Cargo.toml
ā āāā src/main.rs
āāā core_lib/
āāā Cargo.toml
āāā src/lib.rsTop-level workspace manifest:
[workspace]
members = ["app", "core_lib"]
resolver = "2"A workspace lets related packages share tooling and often a lockfile and build context.
For now, the key beginner insight is just this: a workspace is not another kind of crate. It is an organizational layer above packages.
What Cargo Gives You Conceptually
By this point, the main ideas should fit together:
- Cargo organizes Rust projects around packages
- packages contain targets
- targets are backed by crates
- Cargo uses
Cargo.tomlplus standard layout conventions - Cargo orchestrates
rustc - Cargo scales from one-file learning projects to multi-package workspaces
A strong mental model is:
"Cargo is the system that turns a Rust codebase into a buildable, testable, runnable, publishable project."
That model is more useful than memorizing commands in isolation.
Common Beginner Confusions
Confusion 1: "Cargo is the compiler."
Not quite. Cargo drives the workflow; rustc does the compilation.
Confusion 2: "Package and crate mean the same thing."
Not always. One package can contain multiple targets, and each target is typically a crate.
Confusion 3: "A project has only one executable."
Not necessarily. A package can contain multiple binaries.
Confusion 4: "Tests and examples are outside the main package model."
They are part of it. Cargo knows how to build and run them.
Confusion 5: "I must configure everything manually."
Usually not. Cargo heavily relies on convention, which is why a lot of Rust projects look structurally similar.
Hands-On Reinforcement
A good way to lock in the mental model is to create a package and expand it gradually.
Start here:
cargo new cargo_model_demo
cd cargo_model_demo
cargo runThen add a library:
// src/lib.rs
pub fn square(x: i32) -> i32 {
x * x
}Use it from the binary:
// src/main.rs
fn main() {
println!("{}", cargo_model_demo::square(5));
}Add another binary:
// src/bin/show.rs
fn main() {
println!("{}", cargo_model_demo::square(9));
}Run it:
cargo run --bin showAdd an integration test:
// tests/square_test.rs
use cargo_model_demo::square;
#[test]
fn squares_correctly() {
assert_eq!(square(6), 36);
}Run tests:
cargo testSee that Cargo is managing a structured package with multiple targets, not just a single source file.
