Cargo — The Build Tool
Cargo is Rust's official package manager and build system. It handles compiling your code, downloading libraries your project depends on, and building those libraries. Almost every Rust project uses Cargo — it is the standard tool for the entire ecosystem.
What Cargo does
Creates and manages project structure
Compiles your Rust code
Downloads and compiles dependencies from crates.io
Runs tests with cargo test
Generates documentation with cargo doc
Publishes libraries to crates.io with cargo publish
Runs linting with cargo clippy and formatting with cargo fmt
Creating a new project
Use cargo new to create a new project with a ready-to-go directory structure, or cargo init to initialize Cargo inside an existing directory.
# Create a new binary (executable) project cargo new my_project # Create a new library project cargo new my_library --lib # Initialize Cargo in the current directory cargo init # Initialize a library in the current directory cargo init --lib
After running cargo new my_project, you get this structure:
my_project/
├── Cargo.toml ← package metadata and dependencies
├── Cargo.lock ← exact dependency versions (auto-generated)
└── src/
└── main.rs ← entry point for binary projectssrc/main.rs vs src/lib.rs
File | Purpose | When to use |
|---|---|---|
src/main.rs | Entry point for an executable | CLI tools, applications, programs users run directly |
src/lib.rs | Root of a library crate | Reusable code other projects import as a dependency |
Both present | Binary + library in one package | CLI tool that also exposes a public API |
Cargo.toml — package manifest
Cargo.toml is the heart of every Cargo project. It declares the package name, version, Rust edition, and all dependencies.
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <you@example.com>"]
description = "A short description of the project"
license = "MIT"
readme = "README.md"
homepage = "https://example.com"
repository = "https://github.com/you/my_project"
keywords = ["cli", "tool"]
categories = ["command-line-utilities"]
[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
anyhow = "1"
[dev-dependencies]
# Only compiled for tests and benchmarks
tempfile = "3"
mockall = "0.11"
[features]
# Optional feature flags
default = []
async-support = ["tokio"]
[[bin]]
# Optional: declare additional binaries in the same package
name = "my_tool"
path = "src/bin/my_tool.rs"Declaring dependencies
Cargo fetches dependencies from crates.io, Rust's public package registry. You can also depend on Git repositories or local paths.
[dependencies]
# Simple version requirement
serde = "1"
# Explicit version with features
serde = { version = "1.0", features = ["derive"] }
# Minimum version (caret — compatible with 1.x)
tokio = "^1.0"
# Tilde — only patch updates (1.2.x)
log = "~1.2"
# Exact version
anyhow = "=1.0.68"
# From a Git repository
my_crate = { git = "https://github.com/user/my_crate" }
# From a Git repository at a specific branch or tag
my_crate = { git = "https://github.com/user/my_crate", branch = "main" }
# From a local path (useful for monorepos or local development)
utils = { path = "../utils" }
[dev-dependencies]
# These are only used when running tests or benchmarks
pretty_assertions = "1"
criterion = "0.5"Semantic versioning in Cargo
Cargo uses Semantic Versioning (SemVer). The caret (^) is the default specifier and means "compatible with this version". For versions 1.0 and above, a caret allows any update that does not change the leftmost non-zero digit.
Specifier | Allows | Example |
|---|---|---|
^1.2.3 | =1.2.3 and <2.0.0 | Any 1.x that is at least 1.2.3 |
^0.2.3 | =0.2.3 and <0.3.0 | Pre-1.0 — minor is breaking |
~1.2.3 | =1.2.3 and <1.3.0 | Patch updates only |
=1.2.3 | Exactly 1.2.3 | Pin to one exact version |
Any version | Almost never use this |
Cargo.lock — reproducible builds
Cargo.lock records the exact version of every dependency (and transitive dependency) that was resolved during the last build. This guarantees that every developer on the team, and every CI run, uses identical crate versions.
Commit Cargo.lock for binary projects (applications, CLI tools) — ensures reproducible builds across machines
Do NOT commit Cargo.lock for library crates — let downstream consumers choose compatible versions
Never edit Cargo.lock manually — Cargo manages it automatically
Run cargo update to refresh Cargo.lock to the latest compatible versions
Essential Cargo commands
Command | What it does |
|---|---|
cargo build | Compile the project (debug mode) |
cargo build --release | Compile with full optimisations (slower compile, fast binary) |
cargo run | Build and immediately run the binary |
cargo run -- --flag arg | Pass arguments to the binary after -- |
cargo check | Type-check without producing a binary (very fast) |
cargo test | Compile and run all tests |
cargo test my_fn | Run only tests whose name contains "my_fn" |
cargo clippy | Run the Clippy linter for idiomatic warnings |
cargo fmt | Format all source files with rustfmt |
cargo doc --open | Build HTML docs and open in browser |
cargo add serde | Add a dependency (requires cargo-edit plugin) |
cargo update | Update Cargo.lock to latest compatible versions |
cargo clean | Delete the target/ build directory |
cargo publish | Publish this crate to crates.io |
cargo check — the fastest feedback loop
cargo check is the command you will use most while writing code. It type-checks your entire project and reports errors without spending time generating a binary. It is typically 2–5× faster than cargo build.
# Fast: only checks types, no binary produced cargo check # Slower: full compile, produces a binary in target/debug/ cargo build # Fastest runtime, slowest compile: optimised binary in target/release/ cargo build --release
Build profiles — dev vs release
Cargo has two built-in build profiles that control optimisation level, debug symbols, and overflow checks.
Profile | Command | Opt level | Debug info | Best for |
|---|---|---|---|---|
dev | cargo build | 0 (none) | Full | Fast iteration during development |
release | cargo build --release | 3 (max) | Stripped | Shipping to production or benchmarking |
You can customise profiles in Cargo.toml:
[profile.dev] opt-level = 0 # no optimisation, fast compile debug = true # include debug symbols overflow-checks = true # panic on integer overflow [profile.release] opt-level = 3 # maximum optimisation debug = false # strip debug symbols lto = true # link-time optimisation (even smaller/faster binary) codegen-units = 1 # slower compile, better optimisation panic = "abort" # smaller binary, slightly faster [profile.dev.package."*"] # Optimise all dependencies even in dev mode (useful for image/crypto crates) opt-level = 3
A realistic Cargo.toml example
Here is a complete Cargo.toml for a real async web-scraping CLI tool using tokio and serde:
[package]
name = "web-scraper"
version = "1.0.0"
edition = "2021"
authors = ["Alice <alice@example.com>"]
description = "Async CLI web scraper"
license = "MIT OR Apache-2.0"
[dependencies]
# Async runtime
tokio = { version = "1", features = ["full"] }
# HTTP client (async, with JSON support)
reqwest = { version = "0.11", features = ["json", "gzip"] }
# Serialise/deserialise JSON and TOML
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Error handling
anyhow = "1"
thiserror = "1"
# HTML parsing
scraper = "0.17"
# CLI argument parsing
clap = { version = "4", features = ["derive"] }
# Logging
tracing = "0.1"
tracing-subscriber = "0.3"
[dev-dependencies]
tokio-test = "0.4"
wiremock = "0.5"
[profile.release]
opt-level = 3
lto = truecargo doc
Cargo can generate beautiful HTML documentation for your project and all its dependencies directly from source code doc comments.
# Build documentation cargo doc # Build and immediately open in your browser cargo doc --open # Include documentation for private items too cargo doc --document-private-items # Build docs for a specific package in a workspace cargo doc -p my_crate --open