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:

  • rustc compiles 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:

  1. pass compiler flags
  2. locate dependencies
  3. compile dependencies in the right order
  4. manage output paths
  5. separate dev/test/release workflows
  6. 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 test

Those 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.rs

That 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 run

Cargo will:

  • locate Cargo.toml
  • determine what package and targets exist
  • resolve dependencies
  • invoke rustc appropriately
  • 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_cargo

Directory layout:

hello_cargo/
ā”œā”€ā”€ Cargo.toml
└── src/
    └── main.rs

The 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.toml describes the package
  • src/main.rs is discovered by convention as a binary target
  • cargo run knows 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.rs means "this package has a binary target"
  • src/lib.rs means "this package has a library target"
  • src/bin/*.rs means "additional binaries"
  • examples/ contains example programs
  • tests/ contains integration tests
  • benches/ 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.rs

Because 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.rs

In 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.rs

Each of these serves a different role:

  • src/lib.rs: shared logic
  • src/main.rs: executable application
  • examples/quickstart.rs: runnable usage example
  • tests/integration_test.rs: integration tests that use the package from the outside
  • benches/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 quickstart

An integration test:

// tests/integration_test.rs
use calculator::add;
 
#[test]
fn adds_numbers() {
    assert_eq!(add(3, 4), 7);
}

Run tests with:

cargo test

A benchmark file might live here:

benches/speed.rs

Even 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 build

Cargo 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.rs

Cargo 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.rs

Manifest:

[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 test

This 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.rs

Top-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.toml plus 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 run

Then 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 show

Add an integration test:

// tests/square_test.rs
use cargo_model_demo::square;
 
#[test]
fn squares_correctly() {
    assert_eq!(square(6), 36);
}

Run tests:

cargo test

See that Cargo is managing a structured package with multiple targets, not just a single source file.