Rust Cargo: Package Manager and Build System
Cargo is Rust's official package manager and build system, installed automatically when you install Rust via rustup. Unlike ecosystems where you might use npm for packages but webpack for builds, or...
Key Insights
- Cargo handles both package management and build orchestration in a single tool, eliminating the need for separate dependency managers and build systems that plague other ecosystems
- The Cargo.toml manifest uses semantic versioning with intelligent defaults (^ operator) that balance stability and updates, while Cargo.lock ensures reproducible builds across environments
- Workspaces enable monorepo-style development with shared dependencies and unified build commands, making multi-crate projects significantly easier to manage than in most other languages
Introduction to Cargo
Cargo is Rust’s official package manager and build system, installed automatically when you install Rust via rustup. Unlike ecosystems where you might use npm for packages but webpack for builds, or pip for dependencies but make for compilation, Cargo handles everything. This consolidation isn’t just convenient—it’s fundamental to Rust’s philosophy of providing excellent tooling out of the box.
When you run cargo new my_project, you get a complete, buildable project structure with sensible defaults. No configuration paralysis, no hunting for the “right” build tool for your use case.
$ cargo --version
cargo 1.75.0 (1d8b05cdd 2023-11-20)
$ cargo new hello_world
Created binary (application) `hello_world` package
$ cargo new my_library --lib
Created library `my_library` package
The distinction between --bin (default) and --lib matters: binaries produce executables, libraries produce reusable code for other Rust projects. Cargo sets up the appropriate structure for each.
Project Structure and Cargo.toml
A basic Cargo project has a predictable structure:
my_project/
├── Cargo.toml
├── Cargo.lock
└── src/
└── main.rs (or lib.rs)
The Cargo.toml file is your project’s manifest—it describes everything Cargo needs to know. The Cargo.lock file pins exact dependency versions for reproducible builds. You commit both to version control for applications, but only Cargo.toml for libraries (to allow downstream users flexibility).
Here’s a comprehensive Cargo.toml example:
[package]
name = "web_service"
version = "0.2.1"
edition = "2021"
authors = ["Your Name <you@example.com>"]
license = "MIT OR Apache-2.0"
description = "A high-performance web service"
repository = "https://github.com/username/web_service"
[dependencies]
# Regular dependencies - available in all builds
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.35", features = ["full"] }
axum = "0.7"
[dev-dependencies]
# Only available during testing
mockito = "1.2"
criterion = "0.5"
[build-dependencies]
# Only available in build.rs scripts
cc = "1.0"
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
The edition field is crucial—it specifies which Rust edition you’re using, allowing the language to evolve without breaking existing code. The 2021 edition is current as of this writing.
Dependency Management
Adding dependencies is straightforward, but understanding version specifiers prevents surprises. Cargo uses semantic versioning (semver) with these operators:
[dependencies]
# Caret (default): allows updates that don't break semver
# ^1.2.3 allows >=1.2.3 but <2.0.0
serde = "^1.0.195"
# Equivalent to:
serde = "1.0.195"
# Tilde: allows patch-level updates only
# ~1.2.3 allows >=1.2.3 but <1.3.0
regex = "~1.10"
# Wildcard: use with caution
# 1.* allows >=1.0.0 but <2.0.0
log = "1.*"
# Exact version: no flexibility
rand = "=0.8.5"
# Git dependencies for unreleased code
my_fork = { git = "https://github.com/user/repo", branch = "fix-123" }
# Local path dependencies for development
shared_utils = { path = "../shared_utils" }
# Optional dependencies become features
clap = { version = "4.4", optional = true }
[features]
default = ["cli"]
cli = ["clap"]
Feature flags are powerful. They let you conditionally compile code, reducing binary size and compile times. The default feature activates automatically unless users opt out with default-features = false.
After modifying Cargo.toml, run cargo build to fetch and compile dependencies. Cargo caches compiled artifacts in target/, so subsequent builds are incremental and fast.
Building and Running Projects
Cargo’s build commands are the heart of your development workflow:
# Development build (fast, includes debug symbols)
$ cargo build
Compiling my_project v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 2.34s
# Optimized release build (slow compile, fast runtime)
$ cargo build --release
Finished release [optimized] target(s) in 45.67s
# Build and run in one command
$ cargo run
Finished dev target(s)
Running `target/debug/my_project`
# Run specific binary in workspace
$ cargo run --bin server
# Type-check without code generation (fast)
$ cargo check
Checking my_project v0.1.0
Finished dev target(s) in 0.89s
# Run tests
$ cargo test
# Run benchmarks
$ cargo bench
The cargo check command is underrated. It performs type checking and borrow checking without generating machine code, making it 3-5x faster than cargo build. Use it during development for rapid feedback.
Custom profiles let you tune compilation for specific scenarios:
[profile.dev]
opt-level = 0 # No optimization for fast compiles
[profile.release]
opt-level = 3
lto = true # Link-time optimization
codegen-units = 1 # Better optimization, slower compile
[profile.release-with-debug]
inherits = "release"
debug = true # Debug symbols in optimized builds
Activate custom profiles with cargo build --profile release-with-debug.
Workspaces and Multi-Crate Projects
Workspaces solve the monorepo problem elegantly. When you have multiple related crates, a workspace shares a single target/ directory and Cargo.lock file, ensuring consistent dependency versions.
Root Cargo.toml for a workspace:
[workspace]
members = [
"server",
"client",
"shared",
]
[workspace.dependencies]
# Shared dependency versions across all workspace members
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.35", features = ["full"] }
[workspace.package]
version = "0.3.0"
edition = "2021"
license = "MIT"
Individual crate Cargo.toml files become simpler:
[package]
name = "server"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
shared = { path = "../shared" }
serde.workspace = true
tokio.workspace = true
axum = "0.7"
Workspace commands operate on all members:
$ cargo build --workspace
$ cargo test --workspace
$ cargo build -p server # Build specific package
This approach scales well. The Rust compiler itself uses a workspace with over 100 crates.
Custom Build Scripts and Advanced Features
Build scripts (build.rs) run before your crate compiles, enabling code generation, native library linking, and environment-specific configuration. Place build.rs at your project root:
// build.rs
use std::env;
use std::path::PathBuf;
fn main() {
// Link native C library
println!("cargo:rustc-link-lib=ssl");
println!("cargo:rustc-link-search=/usr/local/lib");
// Set environment variable for compile-time
println!("cargo:rustc-env=BUILD_TIME={}",
chrono::Utc::now().to_rfc3339());
// Rerun if these files change
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=wrapper.h");
// Generate bindings to C code
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
}
Conditional compilation based on target platform:
[target.'cfg(unix)'.dependencies]
libc = "0.2"
[target.'cfg(windows)'.dependencies]
winapi = "0.3"
Cargo extensions expand functionality. Install with cargo install cargo-watch and use with cargo watch -x run for automatic rebuilds on file changes.
Publishing to Crates.io
Before publishing, ensure your Cargo.toml includes required metadata:
[package]
name = "my_awesome_crate"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <you@example.com>"]
license = "MIT OR Apache-2.0"
description = "A concise, accurate description"
repository = "https://github.com/username/my_awesome_crate"
documentation = "https://docs.rs/my_awesome_crate"
readme = "README.md"
keywords = ["web", "http", "async"]
categories = ["web-programming"]
Publishing workflow:
# Login to crates.io (once)
$ cargo login <your-api-token>
# Dry run to check for issues
$ cargo publish --dry-run
# Publish to crates.io
$ cargo publish
Once published, versions are immutable—you cannot delete or modify them. Use cargo yank --vers 0.1.0 to prevent new projects from using a broken version, but existing users can still access it.
Document your public API thoroughly. Cargo automatically generates documentation from doc comments with cargo doc --open. This documentation appears on docs.rs when you publish.
Cargo represents Rust’s commitment to developer experience. It eliminates entire categories of problems that plague other languages: dependency hell, build configuration complexity, and toolchain fragmentation. Master Cargo, and you’ve mastered a significant portion of productive Rust development.