The Cargo Guide

Core Cargo Command-Line Workflow

What the Core Cargo Workflow Is

Cargo's core command-line workflow is the set of commands you use repeatedly while developing, checking, running, testing, documenting, benchmarking, and cleaning a Rust package or workspace.

The central commands in this guide are:

  • cargo build
  • cargo check
  • cargo run
  • cargo test
  • cargo bench
  • cargo doc
  • cargo clean

A good mental model is that Cargo is not just a builder. It is the command-line front door for the day-to-day lifecycle of a Rust project.

That means beginners should not think only in terms of "compile the code." They should think in terms of:

  • validate quickly
  • run the program
  • test behavior
  • inspect docs
  • measure performance
  • manage build artifacts

A Small Package We Can Use Throughout

Assume a small package like this:

cargo new workflow_demo
cd workflow_demo

Generated layout:

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

Then expand it slightly:

// src/lib.rs
pub fn square(x: i32) -> i32 {
    x * x
}
// src/main.rs
fn main() {
    println!("{}", workflow_demo::square(8));
}
// tests/basic.rs
use workflow_demo::square;
 
#[test]
fn squares_correctly() {
    assert_eq!(square(5), 25);
}

This kind of package is enough to make Cargo's core commands concrete.

cargo build

cargo build compiles the package and its dependencies.

cargo build

For a basic package, this usually compiles in debug mode and places artifacts in target/debug/.

Typical beginner use:

cargo build

Typical result conceptually:

target/
└── debug/
    ā”œā”€ā”€ workflow_demo
    ā”œā”€ā”€ deps/
    ā”œā”€ā”€ incremental/
    └── build/

Use cargo build when you want actual compiled output but do not necessarily want to run the program immediately.

A useful distinction:

  • cargo build produces build artifacts
  • cargo check validates code much faster without fully producing the same final artifacts

cargo check

cargo check type-checks and validates the code without performing a full normal build for execution.

cargo check

This is one of the most important commands in Rust development because it gives fast feedback.

For example, suppose src/lib.rs contains an error:

pub fn square(x: i32) -> i32 {
    x * "oops"
}

Then:

cargo check

Cargo will report a compiler error quickly, without waiting for a full executable build cycle.

A strong workflow habit is:

  • use cargo check frequently while editing
  • use cargo build when you want build output
  • use cargo run when you want to execute the program

This is one of the most effective mental shifts for beginners.

cargo run

cargo run builds the selected binary target if needed and then runs it.

cargo run

Given this source:

fn main() {
    println!("{}", workflow_demo::square(8));
}

Running:

cargo run

will compile as needed and execute the default binary.

You can also pass arguments after --.

fn main() {
    let args: Vec<String> = std::env::args().collect();
    println!("args = {:?}", args);
}
cargo run -- hello world

That -- matters because it separates Cargo's own flags from the program's flags.

cargo test

cargo test builds and runs tests for the package.

cargo test

With this integration test:

use workflow_demo::square;
 
#[test]
fn squares_correctly() {
    assert_eq!(square(5), 25);
}

Cargo will compile the relevant targets and execute the test harness.

You can also filter tests by name:

cargo test squares_correctly

And you can pass arguments through to the test binary after --:

cargo test -- --nocapture

This is useful when you want printed output from tests to be shown.

cargo bench

cargo bench runs benchmark targets.

cargo bench

At a workflow level, the important point is that benchmarks are a first-class Cargo activity just like builds, tests, and docs.

A minimal bench target might be declared and managed by Cargo even if the actual benchmarking strategy becomes more advanced later.

Illustrative layout:

workflow_demo/
ā”œā”€ā”€ benches/
│   └── perf.rs
ā”œā”€ā”€ src/
│   ā”œā”€ā”€ lib.rs
│   └── main.rs
└── Cargo.toml

Illustrative source:

fn main() {
    println!("benchmark placeholder");
}

The beginner takeaway is that Cargo's command surface extends beyond compilation and testing into performance-oriented workflows too.

cargo doc

cargo doc builds package documentation.

cargo doc

For example, with library code:

/// Returns the square of a number.
pub fn square(x: i32) -> i32 {
    x * x
}

Then:

cargo doc

builds documentation into the target/doc/ area.

A very common command is:

cargo doc --open

That opens the generated documentation in a browser after building it.

This helps learners see that Cargo manages documentation generation as part of the normal package lifecycle.

cargo clean

cargo clean removes build artifacts from the target directory.

cargo clean

This is useful when:

  • you want to reclaim disk space
  • you want to force a fresh build
  • you are debugging stale artifact issues
  • you want to understand which files Cargo regenerates

After cleaning, the next build will have to recreate the removed artifacts.

A simple workflow contrast:

cargo build
cargo clean
cargo build

The second build will usually take longer than an incremental rebuild because the prior build outputs are gone.

Debug vs Release

Cargo uses different build profiles, with debug mode as the normal development default and release mode for optimized builds.

Default build:

cargo build

Optimized release build:

cargo build --release

Likewise:

cargo run --release
cargo test --release
cargo doc --release

In general:

  • debug builds favor faster compile times and development feedback
  • release builds favor runtime performance

Artifacts are typically separated like this:

target/
ā”œā”€ā”€ debug/
└── release/

Beginners should usually spend most of their day in debug mode and move to release mode when they care about final runtime behavior or performance.

What Goes Into target/

Cargo places build-related outputs under the target/ directory.

A typical shape looks like this:

target/
ā”œā”€ā”€ debug/
│   ā”œā”€ā”€ workflow_demo
│   ā”œā”€ā”€ deps/
│   ā”œā”€ā”€ incremental/
│   └── build/
ā”œā”€ā”€ release/
ā”œā”€ā”€ doc/
└── tmp/

The exact contents vary, but a useful beginner mental model is:

  • target/debug/: development build artifacts
  • target/release/: optimized build artifacts
  • target/doc/: generated documentation
  • target/debug/deps/: dependency artifacts
  • target/debug/incremental/: incremental compilation state

The target directory is Cargo's working artifact space. It is generated output, not source code.

Incremental Compilation

Incremental compilation means Cargo and the compiler reuse prior work when only part of the code has changed.

Typical experience:

cargo build
# first build may take longer
 
cargo build
# second build is often much faster if little changed

Now change just one function:

pub fn square(x: i32) -> i32 {
    x * x + 0
}

Then:

cargo build

Cargo can often reuse much of the earlier compilation work.

This is why repeated edit-build-check cycles are practical. It also explains why cargo clean removes more than just one executable: it removes the artifact history Cargo uses to speed up later builds.

Command Flags Change Behavior

Cargo subcommands share many behavioral patterns through flags.

Common examples:

cargo build --release
cargo test --release
cargo run --release

Target selection flags:

cargo run --bin admin_tool
cargo test --test api_checks
cargo run --example quickstart
cargo bench --bench perf

Feature flags:

cargo build --features serde
cargo test --all-features
cargo run --no-default-features

Verbosity flags:

cargo build -v
cargo build -vv

A useful learning principle is that Cargo has strong cross-command conventions. Once you understand a few patterns, many subcommands start to feel similar.

Package Selection in Workspaces

In a workspace, Cargo commands can target one package, several packages, or all packages.

Example layout:

my_workspace/
ā”œā”€ā”€ Cargo.toml
ā”œā”€ā”€ app/
│   ā”œā”€ā”€ Cargo.toml
│   └── src/main.rs
└── core/
    ā”œā”€ā”€ Cargo.toml
    └── src/lib.rs

Top-level manifest:

[workspace]
members = ["app", "core"]
resolver = "2"

Then from the workspace root:

cargo build
cargo build -p app
cargo test -p core
cargo check --workspace

Important beginner idea:

  • without package selection, the current manifest context matters
  • in a workspace, -p lets you target one member explicitly

Target Selection Across Subcommands

Cargo lets you select target kinds explicitly.

Examples:

cargo run --bin workflow_demo
cargo run --example demo
cargo test --test basic
cargo bench --bench perf

Suppose the package has this layout:

workflow_demo/
ā”œā”€ā”€ src/
│   ā”œā”€ā”€ lib.rs
│   ā”œā”€ā”€ main.rs
│   └── bin/
│       └── admin.rs
ā”œā”€ā”€ examples/
│   └── demo.rs
ā”œā”€ā”€ tests/
│   └── basic.rs
└── benches/
    └── perf.rs

Then each command can narrow to a specific target rather than acting on everything of that kind.

Message Formats and Machine-Friendly Output

Cargo can emit output in formats that are useful for tools, editors, and automation.

For normal humans, default output is often enough:

cargo check

For tool-oriented workflows, you may see options like message-format changes used in scripting and integrations.

Illustrative example:

cargo build --message-format=json

The exact downstream use depends on the tool consuming the output, but the key concept is that Cargo is not only a manual developer CLI. It is also an orchestration layer for machines and editor tooling.

Timing and Diagnostics-Oriented Usage

Cargo commands can be used not just to succeed or fail, but to learn something about the build.

For example, verbose output can help you understand what Cargo is doing:

cargo build -v

Higher verbosity can expose even more detail:

cargo build -vv

When diagnosing issues, developers often use combinations like:

cargo check -v
cargo test -- --nocapture
cargo build --release -v

The important learning point is that Cargo is not a black box. Its command-line surface supports investigation as well as execution.

Command Conventions Across Subcommands

Cargo's command set is large, but it becomes easier once you see the recurring conventions.

Some patterns repeat across many subcommands:

  • --release changes profile
  • -p selects a package
  • --features enables features
  • --all-features enables all features
  • --no-default-features disables default features
  • -v increases verbosity
  • --target selects a compilation target platform
  • -- passes remaining arguments through to the executed binary or test harness where appropriate

This means Cargo is easier to learn as a system than as a bag of unrelated commands.

For example:

cargo build --release -p core
cargo test -p app --features cli
cargo run --bin admin --release

Relevant Exit Codes in Tooling and CI

Cargo commands generally communicate success or failure through their process exit status.

At a practical level:

  • successful command execution returns success
  • build failures, test failures, and command errors return failure

This is why Cargo works cleanly in shell scripts and CI systems.

Simple shell example:

cargo check && cargo test && cargo doc

That chain only continues while each command succeeds.

A CI-style script might look like this:

#!/usr/bin/env sh
set -e
 
cargo check
cargo test
cargo build --release

Because nonzero exit status signals failure, Cargo integrates naturally into automation.

The beginner takeaway is not to memorize exact numeric codes, but to understand that Cargo is script-friendly because command success and failure are reliable process-level signals.

Cargo in a Small Continuous Workflow

A realistic daily loop often looks like this:

  1. edit code
  2. run cargo check
  3. run cargo test
  4. run cargo run
  5. occasionally run cargo doc --open
  6. use cargo build --release when checking optimized behavior

For example:

cargo check
cargo test
cargo run

Then after performance-sensitive changes:

cargo build --release
cargo run --release

And when docs matter:

cargo doc --open

This sequence teaches Cargo as a development rhythm rather than a static list of commands.

A More Complete Package Example

Suppose the package grows into this:

workflow_demo/
ā”œā”€ā”€ Cargo.toml
ā”œā”€ā”€ src/
│   ā”œā”€ā”€ lib.rs
│   ā”œā”€ā”€ main.rs
│   └── bin/
│       └── admin.rs
ā”œā”€ā”€ examples/
│   └── quickstart.rs
ā”œā”€ā”€ tests/
│   └── basic.rs
└── benches/
    └── perf.rs

Library:

pub fn square(x: i32) -> i32 {
    x * x
}

Main binary:

fn main() {
    println!("main: {}", workflow_demo::square(3));
}

Additional binary:

fn main() {
    println!("admin: {}", workflow_demo::square(4));
}

Example:

use workflow_demo::square;
 
fn main() {
    println!("example: {}", square(6));
}

Integration test:

use workflow_demo::square;
 
#[test]
fn squares_correctly() {
    assert_eq!(square(9), 81);
}

Useful commands now include:

cargo check
cargo build
cargo run
cargo run --bin admin
cargo run --example quickstart
cargo test
cargo doc --open

That is already a substantial command-line workflow surface from one package.

Common Beginner Mistakes

Mistake 1: using cargo build for every tiny edit.

Often cargo check is the faster and better first step.

Mistake 2: forgetting that cargo run builds before running.

You do not usually need cargo build first unless you specifically want the artifact.

Mistake 3: confusing Cargo arguments with program arguments.

Remember the separator:

cargo run -- --my-program-flag

Mistake 4: expecting target/ to be source-controlled project content.

It is generated build output.

Mistake 5: forgetting workspace and package selection.

In multi-package repositories, -p often matters.

Mistake 6: assuming Cargo is only for builds.

It also drives tests, docs, benchmarks, and automation-friendly workflows.

Hands-On Exercise

Create a package and walk through the core workflow deliberately.

Start here:

cargo new cargo_flow_lab
cd cargo_flow_lab

Add a library:

// src/lib.rs
pub fn cube(x: i32) -> i32 {
    x * x * x
}

Update main:

// src/main.rs
fn main() {
    println!("{}", cargo_flow_lab::cube(3));
}

Add a test:

// tests/basic.rs
use cargo_flow_lab::cube;
 
#[test]
fn cubes_correctly() {
    assert_eq!(cube(4), 64);
}

Now run these in order:

cargo check
cargo build
cargo run
cargo test
cargo doc --open

Then inspect the target directory:

find target -maxdepth 2 -type d | sort

Then try a release build:

cargo build --release
cargo run --release

Finally clean up:

cargo clean

This exercise helps connect commands, artifacts, and workflow habits into one coherent model.

Mental Model Summary

A strong mental model for Cargo's core command-line workflow is:

  • cargo check gives fast correctness feedback
  • cargo build produces build artifacts
  • cargo run builds and executes a binary target
  • cargo test runs test targets through the test workflow
  • cargo bench supports benchmarking workflows
  • cargo doc builds documentation
  • cargo clean removes generated artifacts
  • target/ is Cargo's artifact workspace
  • debug and release modes serve different purposes
  • shared flags and selection patterns make Cargo feel consistent across subcommands
  • reliable exit status makes Cargo easy to use in scripts and CI

Once this model is stable, Cargo stops feeling like a list of commands and starts feeling like a coherent development operating system for Rust projects.