Cargo

Cargo is Rust's official build system and package manager, and one of the most praised parts of the language. A single tool creates projects, resolves and downloads dependencies from crates.io, compiles code, runs tests and benchmarks, generates documentation, lints and formats code, and publishes libraries. There's no Makefile, CMake, or separate package manager to wire up.

Knowing Cargo well means faster builds, smaller binaries, cleaner multi-crate repositories, and well-behaved libraries. This page covers the manifest, dependency resolution, features, workspaces, profiles, and the everyday subcommands.

TL;DR

Quick Example

Core Concepts

Packages, Crates, and Targets

A package is defined by one Cargo.toml. It contains one or more crates (compilation units): at most one library (src/lib.rs) and any number of binaries (src/main.rs, src/bin/*.rs), plus examples (examples/), integration tests (tests/), and benchmarks (benches/). Cargo discovers these by convention, with no configuration needed.

Dependencies and Versions

Cargo assumes semver: within 1.x, updates must be compatible. Note that for 0.x versions, the minor number is treated as breaking ("0.8" means >=0.8.0, <0.9.0). Cargo's resolver may compile two incompatible major versions of the same crate side by side when different dependencies require them.

Cargo.lock

The lockfile records the exact version and checksum of every dependency in the graph. cargo update refreshes it within your declared ranges. Commit it for applications, and nowadays for libraries too (Cargo's current guidance), so CI builds are reproducible. Downstream users of a library still resolve fresh versions; the lockfile only affects your own builds.

Features

Features are named, additive flags that conditionally compile code:

Users choose features per dependency (tokio = { version = "1", features = ["rt-multi-thread", "macros"] }) and can opt out of defaults with default-features = false. Features must be additive: enabling one should never break code that works without it, because Cargo unifies features across the whole dependency graph.

Workspaces

All members share one Cargo.lock and one target/ directory, so shared dependencies compile once. workspace.dependencies keeps versions consistent. Run commands for one member with -p api. See monorepos.

Profiles

Tune release for size or speed: lto (link-time optimization), codegen-units = 1, panic = "abort", strip = true, opt-level = "s"/"z" for size. A handy dev tweak is [profile.dev.package."*"] opt-level = 2, which optimizes dependencies while keeping your own code fast to rebuild.

Build Scripts

A build.rs file runs before compiling the package. Use it to compile C code (via the cc crate), generate bindings (bindgen), or generate code from schemas (for example protobufs for gRPC). Build scripts should declare cargo::rerun-if-changed=... so they don't rerun on every build.

The Toolchain Around Cargo

Best Practices

Use cargo check in the Inner Loop

It skips code generation and is several times faster than cargo build. Editors running rust-analyzer do this continuously.

Minimize Default Features

Library authors should keep default features lean, and application authors should disable defaults on heavy dependencies and enable only what's needed. Fewer features mean faster compiles and smaller binaries.

Audit Dependencies

Run cargo audit or cargo deny check in CI to catch crates with known vulnerabilities, banned licenses, or duplicate versions. Every dependency is code you ship.

Speed Up CI

Cache ~/.cargo/registry, ~/.cargo/git, and target/ (the Swatinem/rust-cache action does this well), use cargo nextest, and consider a faster linker such as mold or lld. Compilation time is Rust's main productivity cost; see build tools.

Common Mistakes

Non-Additive Features

Design features so any combination compiles, or pick one backend at runtime.

Benchmarking Debug Builds

Debug builds can be 10–100× slower than release builds. Always measure performance with --release (or a custom profile with debug symbols plus optimizations for profiling).

Wildcard or Over-Tight Versions

version = "*" accepts anything, including future breaking releases. version = "=1.2.3" in a library blocks users from receiving patches. Use the default caret requirements.

FAQ

Should I commit Cargo.lock for a library?

Cargo's current recommendation is yes for all packages. It makes your own CI and development builds reproducible. Consumers of your library ignore it and resolve dependencies themselves, so it doesn't restrict them. Periodically run CI against the latest dependencies too, to catch breakage early.

Why are Rust builds slow, and how do I speed them up?

Monomorphization, heavy optimization, and large dependency graphs make full builds slow. Incremental debug builds are usually quick. Helpful levers: cargo check, splitting large crates in a workspace, trimming features, a faster linker, sccache for shared caching, and optimizing only dependencies in dev.

What's the difference between cargo install and cargo add?

cargo add adds a library dependency to your project's Cargo.toml. cargo install builds and installs a binary crate (a CLI tool such as ripgrep or cargo-nextest) into ~/.cargo/bin. cargo binstall fetches prebuilt binaries instead of compiling.

How do I publish a crate?

Fill in description, license, and repository in Cargo.toml, run cargo publish --dry-run, then cargo publish with a crates.io API token, or use trusted publishing from CI. Published versions are permanent; you can yank a broken version to stop new projects from depending on it, but not delete it.

Related Topics

References