Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 23: Working with Cargo

Cargo is Rust’s official build system and package manager, integral to the Rust development experience. It streamlines essential tasks such as creating new projects, managing dependencies, compiling code, running tests, and publishing packages to the central registry, Crates.io. While previous chapters introduced basic Cargo usage for building and running code (Chapter 4) and managing dependencies (Chapter 17), this chapter delves deeper.

We will explore Cargo’s command-line interface (CLI), the standard project structure it encourages (often managed with version control systems like Git), dependency version management, and the distinction between building libraries and binary applications. Further topics include publishing your own packages, customizing build configurations (profiles), organizing larger projects with workspaces, and generating project documentation.

Cargo is a powerful tool with many features; this chapter focuses on the capabilities most relevant for developers, particularly those coming from C or C++ backgrounds where build systems (like Make or CMake) and package managers (like Conan or vcpkg) are often separate entities. For exhaustive details, refer to the official Cargo Book.

Note that Cargo’s testing and benchmarking features (cargo test, cargo bench) are covered in the next chapter.

A brief note on terminology: In Rust, the terms crate and package are often used interchangeably in common conversation, but they have distinct meanings in the context of Cargo. A package is a unit that Cargo builds, publishes, and downloads. It contains a Cargo.toml file (the manifest) and one or more crates. A package must contain at least one library crate or one binary crate. It can contain a library crate and multiple binary crates. Crates are the fundamental units of compilation (a library crate or a binary crate). When people refer to downloading a dependency from Crates.io, they are technically downloading a package, although it is common to hear this referred to as downloading a “crate”. Since most library packages contain only a single library crate, using “crate” when “package” is meant is often not significantly misleading. The term project is also often used synonymously with package, particularly when referring to the directory structure created by cargo new. This chapter will use the terms package and crate more precisely where the distinction is relevant, but acknowledge the common community usage.


23.1 Overview

Cargo automates and standardizes many aspects of Rust development. Its core functions include:

  • Project Scaffolding: Creating new library or binary projects (packages) with a consistent directory structure (cargo new, cargo init). cargo new also initializes a Git repository by default, reflecting the common practice of using Git for version control in Rust projects.
  • Dependency Management: Automatically downloading and integrating required packages from Crates.io or other sources (e.g., Git repositories) based on declarations in the Cargo.toml manifest file. These dependencies provide crates that your package can use.
  • Building and Running: Compiling code from crates with different optimization levels (debug vs. release), managing incremental builds, and executing binaries (cargo build, cargo run).
  • Testing and Benchmarking: Discovering and executing tests and benchmarks (cargo test, cargo bench). (Covered in Chapter 24).
  • Packaging and Publishing: Preparing packages for distribution and uploading them to Crates.io (cargo package, cargo publish).
  • Tooling Integration: Acting as a frontend for other development tools like the formatter (cargo fmt), linter (cargo clippy), and documentation generator (cargo doc).

Comparison with C/C++ Build Systems and Package Managers

Coming from C or C++, you might be accustomed to using separate tools:

  • Build Systems: Make, CMake, Meson, Ninja, etc., manage the compilation and linking process. Configuration can be complex, especially for cross-platform projects.
  • Package Managers: Conan, vcpkg, Hunter, or system package managers (like apt, yum, brew) handle external library dependencies. Integrating these with the build system often requires manual effort.

Cargo unifies these roles. It manages both the build process (invoking the Rust compiler rustc with appropriate flags for your crates) and dependency resolution in a single, integrated tool with a consistent interface across all Rust packages. This significantly simplifies project setup and maintenance compared to the fragmented C/C++ ecosystem.


23.2 The Cargo Command-Line Interface (CLI)

Cargo is primarily used via the command line. You can verify your installation and see available commands:

cargo --version
cargo --help

Below are some of the most frequently used Cargo commands.

23.2.1 cargo new and cargo init

These commands initialize a new Rust project (package).

  • cargo new <project_name>: Creates a new directory named <project_name> containing a minimal Cargo.toml file and a src/ directory with a basic main.rs (for a binary package/crate) or lib.rs (for a library package/crate). Crucially, it also initializes a Git repository by default (complete with a .gitignore file), as most Rust projects are managed with Git. This practice facilitates collaboration and version tracking, often on platforms like GitHub.
  • cargo init [<path>]: Initializes a Cargo package structure within an existing directory. If <path> is omitted, it uses the current directory. It will also create a .gitignore file if a Git repository is not already present but will not initialize a new Git repository if one doesn’t exist.

Use the --lib flag to create a library package instead of the default binary (application) package:

# Create a new binary application package named 'hello_world'
cargo new hello_world

# Create a new library package named 'my_utils'
cargo new my_utils --lib

# Initialize the current directory as a Cargo package (defaults to binary)
cargo init

# Initialize './existing_lib_dir' as a library package
cargo init --lib ./existing_lib_dir

23.2.2 cargo build and cargo run

These commands compile and execute your code.

  • cargo build: Compiles the current package, including its crates. By default, it builds in debug mode, which prioritizes faster compilation times over runtime performance and includes debugging information. Output artifacts are placed in the target/debug/ directory.
  • cargo run: Compiles the package (if necessary) and then executes the resulting binary (only applicable to binary packages/crates). Also defaults to debug mode.
# Build the package in debug mode
cargo build

# Build and run the package's default binary in debug mode
cargo run

Release Mode

For production builds or performance testing, use release mode. This enables more aggressive compiler optimizations, resulting in slower compilation but faster runtime performance and smaller binaries. Debug information is typically omitted.

# Build with release optimizations
cargo build --release

# Build and run in release mode
cargo run --release

Release artifacts are placed in a separate target/release/ directory. Incremental compilation behaves differently in release mode, as discussed in Section 23.5.1.

23.2.3 cargo check

This command quickly checks your code for compilation errors without generating any executable code. It performs parsing, type checking, and borrow checking on the crates within your package.

cargo check

cargo check is significantly faster than cargo build, especially for larger projects, because it skips the code generation (LLVM) phase. It’s useful for getting rapid feedback during development. It also benefits from incremental checking.

23.2.4 cargo clean

Removes the target/ directory, deleting all compiled artifacts (executables, libraries, intermediate files) for the current package.

cargo clean

This is useful when you suspect build issues might be related to stale artifacts, need to force a full rebuild of the package’s crates, or want to free up disk space.

23.2.5 cargo add, cargo remove, cargo upgrade

These commands manage dependencies listed in your Cargo.toml. They operate on dependency packages.

  • cargo add <package_name>: Adds a dependency on the latest compatible version of the package <package_name> from Crates.io to your Cargo.toml.
  • cargo remove <package_name>: Removes a dependency package from Cargo.toml.
  • cargo upgrade: Updates dependencies in Cargo.toml to their latest compatible versions according to SemVer rules. (Note: This command is provided by the external cargo-edit tool, see Section 23.2.10).
# Add the 'serde' package as a dependency
cargo add serde

# Add 'rand' as a development-only dependency package (for tests, examples)
cargo add rand --dev

# Add a specific version of the 'serde' package with a feature enabled
cargo add serde --version "1.0.150" --features "derive"

# Remove the 'rand' package
cargo remove rand

These commands modify Cargo.toml and automatically update Cargo.lock (see Section 23.4.3). Before Rust 1.62, cargo add and remove were part of the external cargo-edit tool. They are now built-in.

23.2.6 cargo fmt

Formats your package’s Rust code according to the community-standard style guidelines using the rustfmt tool.

cargo fmt

Running cargo fmt regularly helps maintain a consistent code style across the project’s crates, reducing cognitive load and preventing style-related noise in code reviews and version control history.

23.2.7 cargo clippy

Runs Clippy, Rust’s official collection of lints. Clippy provides suggestions to improve code correctness, performance, style, and idiomatic usage within your package’s crates.

cargo clippy

Clippy often catches potential bugs or suggests better ways to express logic. It’s highly recommended to run clippy as part of your development workflow and CI process.

23.2.8 cargo fix

Automatically applies suggestions made by the Rust compiler (rustc) or Clippy to fix warnings or simple errors in your package’s code.

# Apply compiler suggestions
cargo fix

# Apply suggestions, even with uncommitted changes (use with caution)
# (This assumes changes are staged or you accept working with a dirty tree)
cargo fix --allow-dirty

Always review the changes made by cargo fix before committing them to your version control system.

23.2.9 cargo doc

Generates HTML documentation for your package and its dependencies based on documentation comments in the source code of its crates.

# Generate documentation (output in target/doc/)
cargo doc

# Generate documentation and open the main page in a browser
cargo doc --open

# Generate docs only for your package's own crates (not dependencies)
cargo doc --no-deps

Documentation generation is covered further in Section 23.8.

23.2.10 Extending Cargo: cargo install and External Tools

Cargo can be extended with custom subcommands. You can install additional tools distributed as binary packages using cargo install.

  • cargo install <package_name>: Downloads and installs a binary package globally (typically in ~/.cargo/bin/). Cargo builds the binary crate within the package and places the resulting executable. Ensure this directory is in your system’s PATH.
  • External Subcommands: If you install a binary named cargo-foo, you can invoke it as cargo foo.

Examples of useful tools installable via cargo install:

  • cargo-edit: Provides cargo upgrade, cargo set-version, and other convenient commands for managing Cargo.toml dependencies.
  • cargo-outdated: Checks for dependency packages that have newer versions available on Crates.io than specified in Cargo.lock.
  • cargo-audit: Audits Cargo.lock for dependency packages with known security vulnerabilities reported to the RustSec Advisory Database.
  • cargo-expand: Shows the result of macro expansion within your code.
  • cargo-miri: Runs your code (including unsafe code) in an interpreter (Miri) to detect certain kinds of Undefined Behavior (UB). Requires installing the Miri component: rustup component add miri.
# Install the cargo-edit package (and its binary crate)
cargo install cargo-edit

# Now you can use 'cargo upgrade'
cargo upgrade

# Install Miri and run
rustup component add miri
cargo miri run

23.3 Standard Project Directory Structure

cargo new and cargo init create a standard directory layout for a package:

my_package/
├── .git/            # Git repository data (if initialized by `cargo new`)
├── .gitignore       # Git ignore file (typically includes /target/)
├── Cargo.toml       # Package manifest file
├── Cargo.lock       # Locked dependency versions (generated after first build/add)
├── src/             # Source code directory for crates
│   └── main.rs      # Crate root for a binary crate
│   # Or:
│   └── lib.rs       # Crate root for a library crate
└── target/          # Build artifacts (compiled code, cache) - not version controlled
  • Cargo.toml: The manifest file defining the package metadata, dependencies, and build settings. (See Section 23.4).
  • Cargo.lock: An auto-generated file recording the exact versions of all dependency packages (direct and transitive) used in a build. This ensures reproducible builds. (See Section 23.4.3).
  • src/: Contains the Rust source code for the package’s crates.
    • main.rs: The crate root for the default binary application crate within the package. Must contain a fn main().
    • lib.rs: The crate root for the default library crate within the package.
    • Subdirectories within src/ can contain modules (e.g., src/module_name.rs or src/module_name/mod.rs) that belong to the main crate (either lib or main) or separate binary crates (see below).
  • target/: Where Cargo places all build output (compiled code for crates, downloaded dependencies, intermediate files). This directory should generally be excluded from version control. cargo new automatically creates a suitable .gitignore file for this purpose.
  • .git/ and .gitignore: Created by cargo new to facilitate version control with Git. Rust projects/packages are typically managed with Git and hosted on platforms like GitHub or GitLab.
  • Other optional directories:
    • tests/: Contains integration tests for the package’s library crate.
    • benches/: Contains benchmarks for the package’s library crate.
    • examples/: Contains example programs using the library crate.
    • src/bin/: Can contain multiple binary crates within the same package (e.g., src/bin/cli_tool.rs creates a binary named cli_tool).

23.4 The Manifest: Cargo.toml

The Cargo.toml file is the heart of a Rust package. It uses the TOML (Tom’s Obvious, Minimal Language) format to define metadata and dependencies.

23.4.1 Common Sections

A typical Cargo.toml includes several sections:

[package]
name = "my_package" # Name of the package (often matches the main crate name)
version = "0.1.0"
edition = "2024" # Specifies the Rust edition
authors = ["Your Name <you@example.com>"]
description = "A short description of what my_package does."
license = "MIT OR Apache-2.0" # SPDX license expression
repository = "https://github.com/your_username/my_package" #Optional: URL to source
readme = "README.md" # Optional: Path to README file
keywords = ["cli", "utility"] # Optional: Keywords for Crates.io search

[dependencies]
# Lists packages needed to compile and run the package's code
serde = { version = "1.0", features = ["derive"] } # Example with version and features
rand = "0.8"
log = "0.4"

[dev-dependencies]
# Lists packages needed only for tests, examples, and benchmarks
assert_cmd = "2.0"
criterion = "0.4"

[build-dependencies]
# Lists packages needed by build scripts (build.rs)
# Example: cc = "1.0"

[features]
# Defines optional features for conditional compilation
default = ["std_feature"] # Default features enabled if none specified
std_feature = []
serde_support = ["dep:serde"] # Feature enabling an optional dependency package

[profile.release]
# Customizes the 'release' build profile (e.g., for optimizations)
opt-level = 3        # Optimization level (0-3, 's', 'z')
lto = true           # Enable Link-Time Optimization
codegen-units = 1    # Fewer codegen units for potentially better optimization

# See also: [profile.dev], [profile.test], [profile.bench]

[[bin]]
name = "my_cli" # Define an additional binary crate within this package
path = "src/bin/cli.rs"
  • [package]: Core metadata about the package.

    • name, version, authors, description, license: These fields are essential, especially if publishing to Crates.io. The repository field is highly recommended to point to the source code, e.g., on GitHub.
    • edition: Specifies the Rust edition the package’s crates are written against (e.g., "2015", "2018", "2021", or "2024"). As of May 2025, the latest stable edition is 2024, released in February 2025. Rust editions are a powerful mechanism that allows the language to evolve by introducing changes (like new keywords or different interpretations of syntax) that might otherwise be breaking, without invalidating older code. New editions are typically released every three years.
      • How it works: The edition tells the Rust compiler which set of language rules and idioms to apply when compiling the crates within that specific package. Older code continues to compile correctly under its declared edition, even as newer editions introduce changes.
      • Interoperability: Crates compiled with different editions can seamlessly depend on each other and be linked into the same final binary. For example, your package containing a crate using edition = "2024" can depend on a library package whose crate was written using edition = "2021" (or even "2018"), and all can be compiled correctly by the latest Rust compiler.
      • Forward Compatibility: The Rust compiler maintains support for all past stable editions. This means a future compiler (e.g., one supporting a hypothetical edition = "2027") will still correctly compile your edition = "2024" package, as well as packages written for the 2015, 2018, and 2021 editions. Your code doesn’t break as the language and compiler evolve over time.
      • Opt-In Evolution: Migrating a package to a newer edition is an explicit, opt-in process (often assisted by cargo fix --edition). This gives package authors control over when to adopt new idioms or potentially breaking syntax changes introduced in a new edition.
      • Not for Nightly Features: Editions define a coherent, stable set of language semantics for a particular era of Rust. They are distinct from enabling experimental, unstable features typically found only on the nightly compiler.
  • [dependencies]: Lists the packages your package depends on to run. Cargo downloads these from Crates.io by default. (See Section 23.4.2 for details on versioning).

  • [dev-dependencies]: Packages needed only for development tasks like running tests, benchmarks, or examples for your package’s crates. They are not included when someone uses your package as a dependency.

  • [build-dependencies]: Packages required by a build.rs script (a script Cargo runs before compiling your package’s crates, often used for code generation or compiling C code).

  • [features]: Allows defining optional features that enable conditional compilation within your crates, often used to toggle functionality or optional dependencies on other packages.

  • [profile.*]: Sections for customizing build profiles (dev, release, test, bench). (See Section 23.6).

  • [[bin]]: Allows defining additional binary crates within the same package beyond the default src/main.rs. Similarly, [[lib]] can define multiple library crates within a package, although this is less common.

23.4.2 Specifying Dependencies

Dependencies are listed under the [dependencies] (or [dev-dependencies], [build-dependencies]) section. Each dependency specifies the package name and a version requirement.

Cargo and the broader Rust ecosystem adhere to Semantic Versioning (SemVer), as defined at semver.org. SemVer versions are typically in the format MAJOR.MINOR.PATCH:

  • MAJOR version (e.g., 1.0.0 -> 2.0.0): Incremented when you make incompatible API changes (breaking changes).
  • MINOR version (e.g., 1.1.0 -> 1.2.0): Incremented when you add functionality in a backward-compatible manner.
  • PATCH version (e.g., 1.1.1 -> 1.1.2): Incremented when you make backward-compatible bug fixes.

A key point in the SemVer specification (specifically rule #4) is that “Major version zero (0.y.z) is for initial development. Anything MAY change at any time. The public API SHOULD NOT be considered stable.” This means, strictly by SemVer, a 0.1.0 version can have breaking changes introduced in 0.2.0 or even 0.1.1.

However, the Rust community has adopted a common convention for 0.y.z versions that offers more practical stability:

  • Many Rust packages, even widely used ones, remain at version 0.y.z for extended periods, indicating a generally stable API but acknowledging that some churn is still possible before a 1.0.0 release.
  • For a 0.y.z package, a change in y (e.g., from 0.1.5 to 0.2.0) is conventionally treated as a breaking change.
  • A change in z (e.g., from 0.1.5 to 0.1.6) is expected to be backward-compatible (bug fixes or minor additions).

This convention allows Cargo to provide sensible default dependency update behavior. When you specify a version for a dependency package:

[dependencies]
regex = "1.5"    # For versions >= 1.0.0
serde = "0.8.2"  # For versions < 1.0.0

Cargo interprets these version strings using a caret requirement (^) by default:

  • "1.5" is shorthand for "^1.5.0", which means Cargo will accept any version v of the regex package where 1.5.0 <= v < 2.0.0. It allows compatible MINOR and PATCH updates but not MAJOR version 2.0.0 or higher, which would imply breaking changes in the crate’s API.
  • "0.8.2" (or just "0.8") is shorthand for "^0.8.2", which means Cargo will accept any version v of the serde package where 0.8.2 <= v < 0.9.0. It allows compatible PATCH updates (and minor additions if the package author follows the spirit of non-breaking changes for the PATCH digit) but not version 0.9.0 or higher, respecting the convention that a 0.y.z to 0.(y+1).0 change is breaking for the crate’s API.

This default behavior allows you to receive compatible updates automatically while guarding against breaking changes. Other common version specifiers offer more control:

  • Tilde requirement: "~1.5.2" allows only PATCH updates if a MINOR version is specified (>=1.5.2, <1.6.0). If only MAJOR and MINOR are specified, like "~1.5", it behaves like ^1.5.
  • Exact version: "=1.5.2" requires exactly version 1.5.2.
  • Explicit range: ">=1.5.0, <1.6.0" specifies an explicit range.
  • Wildcard: "1.*" is equivalent to ">=1.0.0, <2.0.0". "*" accepts any version (use with caution, often only for examples or very unstable dependencies).

You can also specify dependencies from other sources:

[dependencies]
# From a Git repository containing the package
some_lib = { git = "https://github.com/user/some_lib.git", branch = "main" }

# From a local path (useful during development or in workspaces)
local_util = { path = "../local_util" }

# With optional features enabled
# Here, "1.0" implies "^1.0.0"
serde_json = { version = "1.0", features = ["raw_value"] }

# Marked as optional (only included if a feature in your crate enables it)
# In [dependencies]:
#    mio = { version = "0.8", optional = true } # "0.8" implies "^0.8.0"
# In [features]:
#    network = ["dep:mio"]

Understanding these conventions is crucial for managing dependency packages effectively and ensuring your project remains buildable and stable as your dependencies evolve.

23.4.3 The Cargo.lock File

When you build your package for the first time, or after modifying dependencies in Cargo.toml, Cargo resolves all dependency packages (including transitive ones) and records the exact versions used in the Cargo.lock file.

  • Purpose: Ensures reproducible builds. Anyone building the package with the same Cargo.lock file will use the exact same dependency package versions, preventing unexpected changes due to automatic updates.
  • Management: Cargo.lock is automatically generated and updated by Cargo commands like build, check, add, remove, or update. You should not edit it manually.
  • Version Control:
    • For binary applications (packages): Always commit Cargo.lock to version control (e.g., Git). This guarantees that every developer, CI system, and deployment uses the same dependency set. When you run cargo build or cargo run in a package where Cargo.lock is present, Cargo automatically uses the exact versions specified in Cargo.lock to ensure build reproducibility.
    • For libraries (packages): Committing Cargo.lock is optional and debated.
      • Pro-Commit: Ensures the library package’s own tests run with a consistent set of dependencies in CI.
      • Anti-Commit: Libraries are typically used as dependencies themselves. The downstream application package’s Cargo.lock will ultimately determine the versions used. Committing the library package’s Cargo.lock doesn’t affect consumers and might cause merge conflicts. Many library package authors choose not to commit Cargo.lock.

23.4.4 Updating Dependencies

  • cargo update: Reads Cargo.toml and updates dependencies listed in Cargo.lock to the latest compatible versions allowed by the version specifications in Cargo.toml. It does not change Cargo.toml itself.
    • cargo update -p <package_name>: Updates only a specific dependency package and its dependents.
  • Upgrading Dependencies (Major Versions): To use a new major version of a dependency package (e.g., moving from serde “1.0” to “2.0”), you must manually edit the version requirement in Cargo.toml. Tools like cargo-edit (cargo upgrade) can assist with this.
  • Checking for Outdated Dependencies: Use cargo outdated (from the cargo-outdated tool) to see which dependency packages have newer versions available than what’s currently in Cargo.lock.

23.5 Building and Running Projects

As discussed in Section 23.2.2, cargo build compiles your package’s crates, and cargo run compiles and then executes the default binary crate. Both default to debug mode unless --release is specified.

23.5.1 Build Cache and Incremental Compilation

Cargo employs several caching mechanisms to speed up builds:

  • Dependency Caching: Once a specific version of a dependency package (and its contained crates) is compiled, Cargo caches the result. Subsequent builds reuse the cached artifact as long as the dependency package version and features remain unchanged in Cargo.lock. This avoids recompiling external packages repeatedly.
  • Incremental Compilation: When you modify your own package’s source code, Cargo attempts to recompile only the changed parts of your crates and their dependents, rather than the entire package.
    • Incremental compilation is enabled by default for debug builds (cargo build). This significantly speeds up compilation during typical development cycles by reusing intermediate artifacts from previous compilations for unchanged code sections.
    • For release builds (cargo build --release), incremental compilation is disabled by default. While it could potentially speed up release build times in some scenarios, it is often disabled because:
      • It can sometimes interfere with the more aggressive optimizations performed during release builds, potentially leading to slightly less optimal runtime performance or larger binary sizes.
      • The primary goal of a release build is the quality of the final artifact, and disabling incremental compilation ensures the compiler has the fullest scope for optimization without being constrained by previous partial compilation states.
    • You can explicitly control incremental compilation in Cargo.toml profiles if needed (e.g., [profile.dev] incremental = true), but the defaults are generally sensible.

These mechanisms significantly reduce build times during typical development workflows.

23.5.2 Cross-Compilation

Cargo can compile the crates within your package for different target architectures (e.g., ARM for Raspberry Pi from an x86 machine) using the --target flag. You first need to add the target via rustup:

# Add the ARMv7 Linux target
rustup target add armv7-unknown-linux-gnueabihf

# Build the package for that target
cargo build --target armv7-unknown-linux-gnueabihf

Cross-compilation might require setting up appropriate linkers for the target system.


23.6 Build Profiles

Build profiles allow you to configure compiler settings for different scenarios when building your package’s crates. Cargo defines four profiles by default: dev, release, test, and bench. The dev and release profiles are the most commonly used.

  • dev: The default profile used by cargo build and cargo run. Optimized for fast compilation times.
    • opt-level = 0 (no optimization)
    • debug = true (include debug info)
    • incremental = true (use incremental compilation)
  • release: Used when the --release flag is passed. Optimized for runtime performance.
    • opt-level = 3 (maximum optimization)
    • debug = false (omit debug info by default)
    • incremental = false (do not use incremental compilation by default)

You can customize these profiles in Cargo.toml under [profile.*] sections:

[profile.dev]
opt-level = 1    # Enable basic optimizations even in debug builds
# debug = 2      # Use '2' for full debug info, '1' for line tables only, '0' for none
# incremental = true # Default for dev

[profile.release]
lto = "fat"      # Enable "fat" LTO for potentially better performance/size
codegen-units = 1 # Reduce parallelism, potentially better optimization (slower build)
panic = 'abort'   # Abort on panic instead of unwinding (can reduce binary size)
# strip = true    # Strip symbols from the binary (requires Rust 1.59+)
# incremental = false # Default for release

Key profile settings include:

  • opt-level: Controls the level of optimization (0, 1, 2, 3, s for size, z for more size).
  • debug: Controls the amount of debug information included (true/2, false/0, 1).
  • lto: Enables Link-Time Optimization (false, true/“thin”, "fat", "off"). Can improve performance but increases link times.
  • codegen-units: Number of parallel code generation units for compiling the package’s crates. More units mean faster compilation but potentially less optimal code. 1 can yield the best optimizations.
  • panic: Strategy for handling panics ('unwind' or 'abort') in the compiled code.
  • incremental: Explicitly enable or disable incremental compilation (true or false).

Profile settings in a dependency package’s Cargo.toml are ignored; only the settings in the top-level package’s Cargo.toml (the one being built directly) are used.


23.7 Testing and Benchmarking (Overview)

Cargo provides first-class support for running tests and benchmarks defined within your package’s crates, which are covered in detail in the next chapter.

  • cargo test: Discovers and runs tests annotated with #[test] within your src/ directory (unit tests), functions in the tests/ directory (integration tests), and code examples in documentation comments (doc tests).
  • cargo bench: Discovers and runs benchmarks annotated with #[bench]. Requires nightly Rust for the built-in harness; stable Rust typically uses external packages like criterion for benchmarking crates.

23.8 Generating Documentation

Rust places a strong emphasis on documentation, and Cargo makes generating and viewing it easy for your package’s crates.

23.8.1 Documentation Comments

Rust uses specific comment styles for documentation, written in Markdown:

  • ///: Outer documentation comment, documenting the item following it (function, struct, enum, module, etc.) within a crate.
  • //!: Inner documentation comment, documenting the item containing it (typically used at the top of lib.rs or main.rs to document the entire crate, or inside a mod { ... } block to document the module).
#![allow(unused)]
fn main() {
//! This package contains a crate that provides utility functions for string
//! manipulation. Use `add_prefix` from the `my_string_utils` crate to prepend text.

/// Adds a prefix to the given string.
///
/// # Examples
///
/// ```
/// let result = my_string_utils::add_prefix("world", "hello ");
/// assert_eq!(result, "hello world");
/// ```
///
/// # Panics
///
/// This function does not panic.
///
/// # Errors
///
/// This function does not return errors.
pub fn add_prefix(s: &str, prefix: &str) -> String {
    format!("{}{}", prefix, s)
}
}

Good documentation explains the purpose, parameters, return values, potential errors or panics, usage examples (which double as doc tests), and safety considerations (especially for unsafe code).

23.8.2 cargo doc

The cargo doc command invokes the rustdoc tool to extract these comments and generate HTML documentation for your package’s public crates.

# Generate documentation (output in target/doc/)
cargo doc

# Generate documentation and open the main page in a browser
cargo doc --open

# Generate docs only for your package's own crates (not dependencies)
cargo doc --no-deps

The generated documentation, located in target/doc, provides a navigable interface for your package’s public API (the public items within its library crate(s)) and the APIs of its dependency packages.

23.8.3 Re-exporting for API Design

As mentioned in Chapter 17, you can use pub use statements within a library crate to re-export items from modules or dependency crates, creating a cleaner and more stable public API surface for your library package. This also affects how the API appears in the generated documentation.


23.9 Publishing Packages to Crates.io

Crates.io is the official Rust package registry. Publishing your library package allows others to easily use its contained crate(s) as dependencies. Most Rust projects (packages) are managed using Git for version control, and it’s common practice to host them on platforms like GitHub or GitLab. Before publishing, ensure your package is in a clean state in your version control system.

23.9.1 Prerequisites

  1. Account: Create an account on Crates.io, usually via GitHub authentication.
  2. API Token: Generate an API token in your account settings on Crates.io.
  3. Login via Cargo: Authenticate your local Cargo installation with the token:
    cargo login <your_api_token>
    # Paste the token when prompted or provide it directly (less secure)
    
    This stores the token locally (typically in ~/.cargo/credentials.toml).

23.9.2 Preparing Cargo.toml

Before publishing, ensure your Cargo.toml contains the required metadata in the [package] section for the package you want to publish:

  • name: The package name (must be unique on Crates.io).
  • version: The initial version (e.g., "0.1.0"), following SemVer.
  • license or license-file: A valid SPDX license identifier (e.g., "MIT OR Apache-2.0") or the path to a license file.
  • description: A brief summary of the package’s purpose.
  • At least one of documentation, homepage, or repository: Links providing more information. A repository link to your Git host (e.g., GitHub) is highly recommended.
  • authors, readme, keywords, categories are also highly recommended.

23.9.3 The Publishing Process

  1. Version Control: Ensure all changes intended for the release are committed to your Git repository. The source code published to Crates.io should ideally match a tagged commit in your repository for easy reference.
  2. Package (Optional but Recommended): Simulate the packaging process to check for errors and see exactly which files will be included in the archive that gets uploaded:
    cargo package
    
    Cargo uses .gitignore (and potentially a .cargoignore file if you need finer control beyond what .gitignore offers for packaging) to exclude unnecessary files. Review the generated .crate file (a compressed archive) in target/package/ if needed.
  3. Publish: Upload the package to Crates.io:
    cargo publish
    

Once published, the specific version of the package is permanent (though it can be “yanked”). Other users can now add your package as a dependency:

[dependencies]
your_package_name = "0.1.0"

23.9.4 Updating and Yanking

  • Updating: To publish a new version of your package, increment the version field in its Cargo.toml (following SemVer rules). It’s standard practice to commit these changes to your version control system (e.g., Git) before running cargo publish again. This ensures that the published package corresponds to a specific state in your project’s history.
  • Yanking: If you discover a critical issue (e.g., a security vulnerability) in a published version of your package, you can “yank” it. Yanking prevents new projects from depending on that specific version by default, but does not remove it or break existing projects that already have it in their Cargo.lock.
    # Yank version 0.1.1 of your package
    cargo yank --vers 0.1.1 your_package_name
    
    # Un-yank (undo a yank)
    cargo unyank --vers 0.1.1 your_package_name
    

23.9.5 Deleting Packages

Published package versions cannot be deleted from Crates.io to ensure builds that depend on them remain reproducible. Yanking is the standard mechanism for indicating problematic versions. In truly exceptional circumstances, you might contact the Crates.io team.


23.10 Binary vs. Library Crates

Cargo distinguishes between packages that produce executable binary crates and those that produce library crates:

  • Binary Packages/Crates: Compile to an executable file. They must have a src/main.rs file containing a fn main() function, which serves as the program’s entry point for the main binary crate. cargo new <name> creates a binary package by default, containing one binary crate.
  • Library Packages/Crates: Compile to a Rust library file (.rlib or .dylib) intended to be used as a dependency by other packages. They typically have a src/lib.rs file as their crate root. cargo new <name> --lib creates a library package, containing one library crate.

A single package can contain both a library crate and one or more binary crates:

  • Define the library crate in src/lib.rs.
  • Define the main binary crate in src/main.rs.
  • Define additional binary crates in src/bin/another_bin.rs, src/bin/yet_another.rs, etc.

Cargo will build the library crate and all specified binary crates within the package. This pattern is common for packages that provide both a reusable library API and a command-line tool interface (as separate crates within the same package).


23.11 Cargo Workspaces

Workspaces allow you to manage multiple related packages within a single top-level structure. All packages in a workspace share a single target/ directory (for build artifacts from all their contained crates) and a single Cargo.lock file (ensuring consistent dependency versions across all member packages).

23.11.1 Use Cases

Workspaces are useful for:

  * Large Projects: Breaking down a complex application or library into smaller, more manageable internal packages, each containing one or more crates.   * Related Packages: Developing several packages (e.g., a core library, a CLI frontend, a web server) that depend on each other.   * Monorepos: Managing multiple distinct but potentially related projects/packages in one repository.

23.11.2 Setting Up a Workspace

1.  Create a top-level directory for the workspace. 2.  Inside it, create a Cargo.toml file that defines the workspace members (packages). This file typically doesn’t define a [package] itself (it’s a “virtual manifest”), only the [workspace] section. 3.  You can either place the directories of the individual member packages (each containing its own Cargo.toml) directly inside the workspace directory, or you can specify paths to these package directories within the members array in the workspace’s Cargo.toml if they reside elsewhere relative to the workspace root.

my_workspace/
├── Cargo.toml         # Workspace root manifest
├── member_lib/        # A library package
│   ├── Cargo.toml
│   └── src/lib.rs     # The library crate root
└── member_bin/        # A binary package using the library package
    ├── Cargo.toml
    └── src/main.rs    # The binary crate root
# Shared target and lock file will appear here after build:
# ├── Cargo.lock
# └── target/

my_workspace/Cargo.toml:

[workspace]
members = [
    "member_lib",
    "member_bin",
    # You can also use globs: "packages/*"
    # Or specify paths to members outside the immediate workspace directory:
    # "../another_project_package",
]

# Optional: Define settings shared across the workspace
[workspace.dependencies]
# Define common dependency packages once here
# Example:
# serde = { version = "1.0", features = ["derive"] }

# Member packages can then inherit this:
# In member_bin/Cargo.toml under [dependencies]:
# serde = { workspace = true } # Inherits version and other details from workspace

# Optional: Configure dependency resolution strategy
resolver = "3" # Use the version 3 feature resolver (default for Rust 2024 edition)

The resolver field specifies which dependency resolution algorithm Cargo should use. Version “2” was the default since Rust 1.51, and resolver = "3" is the default for the Rust 2024 edition.

my_workspace/member_bin/Cargo.toml:

[package]
name = "member_bin"
version = "0.1.0"
edition = "2021"

[dependencies]
# Reference the library package within the workspace via path
member_lib = { path = "../member_lib" }
# Or if 'serde' was defined in [workspace.dependencies]:
# serde = { workspace = true } # Inherits version and other details from workspace

When you create a new package inside a workspace using cargo new <package-name>, Cargo automatically adds it to the members key in the workspace’s Cargo.toml.

23.11.3 Working with Workspaces

  * Cargo commands run from the workspace root directory (e.g., cargo build, cargo test, cargo check) operate on all member packages by default, building their respective crates.   * Use the -p <package_name> or --package <package_name> flag to target a specific member package:     ```bash     # Build only the member_bin package     cargo build -p member_bin

    # Run the default binary crate from the member_bin package     cargo run -p member_bin

    # Test only the member_lib package     cargo test -p member_lib     ```

23.11.4 Publishing Packages from a Workspace

Workspaces themselves are not published to Crates.io as a single unit. Instead, individual member packages within a workspace can be published if they are configured as such (i.e., they have the necessary [package] metadata and are not marked publish = false in their respective Cargo.toml files).

  * To publish a specific package from a workspace, navigate to that package’s directory and run cargo publish, or run cargo publish -p <package_name> from the workspace root.   * If you run cargo publish from the workspace root without the -p flag, Cargo will attempt to publish all publishable member packages in the dependency order.

Workspaces are primarily an organizational and build management tool for local development and repository structure. They help ensure that related packages and their contained crates are built and tested together with consistent dependencies.

23.11.5 Benefits

  * Shared Build Cache: Dependency packages are compiled only once for the entire workspace, saving time and disk space.   * Consistent Dependency Versions: A single Cargo.lock at the workspace root ensures all member packages use the exact same resolved versions of external dependencies.   * Easier Inter-Package Development: Changes in one member package are immediately available to other member packages in the workspace that depend on it (via their contained crates), without needing to publish intermediate versions or use path overrides extensively if not for a workspace setup. This allows you to work on interdependent crates within the workspace as if they were a single project.   * Atomic Operations: Running tests, checks, or builds across the entire collection of related packages is straightforward.


Some more Details about Dependency Management and Resolution

Internal Workspace Dependencies

When a member package in a workspace depends on another member package (e.g., member_bin depending on member_lib), you must specify this dependency using a path as shown: member_lib = { path = "../member_lib" }. This tells Cargo to look for member_lib at the specified relative path within the workspace. Using just the package name alone (e.g., member_lib = "0.1.0") would instruct Cargo to look for member_lib on Crates.io, which is not the intention for an internal workspace dependency.

External Dependencies and [workspace.dependencies]

When multiple packages in your workspace depend on the same external package (e.g., serde), you have two primary options for declaring these dependencies:

  1. Individual Declarations: Each member package can declare the dependency in its own Cargo.toml file under [dependencies]:

    # member_bin/Cargo.toml
    [dependencies]
    serde = "1.0"
    
    # member_lib/Cargo.toml
    [dependencies]
    serde = "1.0"
    

    Cargo’s dependency resolution, managed by the single Cargo.lock file at the workspace root, ensures that only one compatible version of serde is used across the entire workspace. For instance, if member_bin requires serde = "1.0" and member_lib requires serde = "1.0.10", Cargo will attempt to find a single version that satisfies both, such as 1.0.10. If incompatible versions are requested (e.g., 1.0 and 2.0), Cargo will report an error. While Cargo can sometimes resolve multiple major versions of the same crate if their feature sets do not conflict, it is generally recommended to standardize on a single major version across your workspace to avoid increased build times and binary size.

  2. Workspace Inheritance ([workspace.dependencies]): This is a more convenient and robust approach for managing common external dependencies. By defining serde (or any other external dependency) once in [workspace.dependencies] in the workspace’s Cargo.toml:

    # my_workspace/Cargo.toml
    [workspace.dependencies]
    serde = { version = "1.0", features = ["derive"] }
    

    Member packages can then inherit this definition by using serde = { workspace = true } in their own Cargo.toml:

    # member_bin/Cargo.toml
    [dependencies]
    serde = { workspace = true }
    
    # member_lib/Cargo.toml
    [dependencies]
    serde = { workspace = true }
    

    This mechanism ensures that all member packages using serde = { workspace = true } will use the exact same version and features as defined in the workspace root. This is particularly beneficial for consistency and preventing subtle version mismatches across your related packages.

    When you publish a package (e.g., member_lib) that uses serde = { workspace = true }, Cargo automatically resolves this during the packaging process. The Cargo.toml file within the published .crate archive will not contain serde = { workspace = true }. Instead, Cargo replaces it with the actual version and details that were inherited from the workspace’s Cargo.toml. For example, if my_workspace/Cargo.toml specified serde = { version = "1.0", features = ["derive"] }, the published member_lib/Cargo.toml would contain serde = { version = "1.0", features = ["derive"] }. This ensures that the published package is self-contained and can be used as a dependency by other projects, even outside your workspace, without needing the workspace context.


23.12 Installing Binary Packages with cargo install

Besides building your own projects, you can install Rust applications published on Crates.io directly using cargo install. This command downloads the specified package, builds its default binary crate (usually in release mode), and places the resulting executable in your Cargo bin directory.

cargo install ripgrep # Installs the 'ripgrep' fast search tool package
cargo install fd-find # Installs the 'fd' find alternative package

Cargo downloads the package’s source code, compiles its binary crate in release mode, and places the resulting binary in ~/.cargo/bin/. Ensure this directory is included in your system’s PATH environment variable to run the installed commands directly (e.g., rg, fd).

Important note regarding Cargo.lock and cargo install: When you run cargo install, it builds the specified binary package from source. Unlike cargo build or cargo run (which, for a local project, will automatically use an existing Cargo.lock file to ensure reproducible builds), cargo install by default ignores the Cargo.lock file that might be present in the downloaded package’s source. Instead, it resolves dependencies based on Cargo.toml and creates a new Cargo.lock for the installation process.

This behavior is generally safe for libraries (where the exact dependency versions are less critical for the library itself, as the downstream application’s Cargo.lock will govern). However, for installing application binaries, it means you might get a different set of dependency versions than what the application developer used or tested with, potentially leading to different behavior or even breakage if a dependency introduced an incompatible change.

To ensure a reproducible installation that uses the exact dependency versions specified by the application’s developer (i.e., those recorded in the Cargo.lock file shipped with the package), you should use the --locked flag:

# Install with reproducible dependencies as specified by the package author
cargo install ripgrep --locked

Using --locked is highly recommended for installing application binary packages to ensure you get the same executable artifact that the author intended. If the package’s Cargo.lock file is missing or out of sync with Cargo.toml, cargo install --locked will fail, prompting the package author to fix their distribution.

Use cargo install --list to see installed packages. To update an installed package, run cargo install again with the same package name (or cargo install <package_name> --locked for reproducibility). To uninstall, use cargo uninstall <package_name>.


23.13 Security Considerations

While Crates.io and Cargo provide a convenient way to share and use code, dependency packages introduce potential security risks (supply chain attacks).

  • Vet Dependencies: Before adding a new dependency package, especially from less-known authors, check its source repository (e.g., on GitHub), download count on Crates.io, and community feedback if possible.
  • Keep Dependencies Updated: Regularly update dependencies using cargo update to receive bug fixes and security patches in dependency packages. Use cargo outdated to identify packages needing updates.
  • Audit Dependencies: Use tools like cargo audit (from the rustsec/cargo-audit package) to check your Cargo.lock file against the RustSec Advisory Database for known vulnerabilities in your dependency packages. Integrate this into your CI pipeline.
    cargo install cargo-audit # Install the audit package
    cargo audit
    
  • Minimize Dependencies: Avoid adding dependency packages unnecessarily. Fewer dependencies mean a smaller attack surface. Review dependencies periodically and remove unused ones (cargo-machete package can help find unused dependencies).

23.14 Summary

Cargo is the cornerstone of the Rust development workflow, integrating build automation, dependency management, and various development tools into a single, cohesive system. Key takeaways include:

  • Unified Tooling: Combines build system and package manager roles, simplifying project setup compared to C/C++ ecosystems. cargo new often sets up a Git repository for the new package, aligning with common version control practices.
  • Core Commands: new, init, build, run, check, test, doc, publish.
  • Manifest: Cargo.toml defines package metadata, dependencies (other packages), features, and build profiles.
  • Reproducibility: Cargo.lock ensures consistent dependency package versions across local builds and within workspaces. For cargo install of application binaries, the --locked flag is crucial to use the package’s Cargo.lock for reproducible installations.
  • Build Profiles: dev (fast compiles, incremental by default) and release (optimized runtime, incremental off by default) with customization options for building package crates.
  • Extensibility: Supports custom subcommands and integration with tools like rustfmt, clippy, miri, and rustdoc (which documents package crates).
  • Workspaces: Efficiently manage multi-package projects with shared dependencies and build outputs. Individual packages within a workspace are published, not the workspace itself.
  • Distribution: Easily publish packages and install binary packages via Crates.io. Commit changes to version control before publishing new versions.

Mastering Cargo is essential for productive Rust development. Its conventions and capabilities foster consistency, reliability, and collaboration within the Rust ecosystem.