17.1 Packages: Bundling Crates with Cargo

A package is the fundamental unit Cargo works with. It represents a Rust project, containing the source code, configuration, dependencies, and metadata necessary to build one or more crates. Every package is defined by its Cargo.toml manifest file located at the package root.

17.1.1 Creating a New Package

Cargo provides convenient commands to initialize a new package structure:

# Create a new package for a binary executable
cargo new my_executable_project

# Create a new package for a library
cargo new my_library_project --lib

For a binary package my_executable_project, Cargo generates:

my_executable_project/
├── Cargo.toml      # Package manifest
└── src/
    └── main.rs     # Crate root for the primary binary crate

For a library package my_library_project, it generates:

my_library_project/
├── Cargo.toml      # Package manifest
└── src/
    └── lib.rs      # Crate root for the library crate

17.1.2 Anatomy of a Package

A typical Rust package consists of:

  • Cargo.toml: The manifest file. It contains metadata (name, version, authors, license), lists dependencies on other packages (crates), and specifies various package settings (features, build targets, etc.).
  • src/: The directory containing the source code.
    • It must contain at least one crate root: src/main.rs for the main binary crate or src/lib.rs for the library crate.
    • It can contain other source files organized into modules (see Section 17.3).
    • It may contain src/bin/ for additional binary crates (see Section 17.1.4).
  • Cargo.lock: An automatically generated file recording the exact versions of all dependencies resolved during a build. This ensures reproducible builds. It’s recommended to commit Cargo.lock for binary packages but often excluded (.gitignore) for library packages to allow downstream users flexibility in version resolution (though practices vary).
  • Optional Directories:
    • tests/: For integration tests (each file is treated as a separate crate).
    • examples/: For example programs demonstrating the library’s usage (each file is a separate binary crate).
    • benches/: For benchmark code (each file is compiled like a test).
  • target/: A directory created by Cargo during builds. It stores intermediate compilation artifacts and the final executables or libraries, typically organized into debug/ and release/ subdirectories. This directory should always be excluded from version control.

17.1.3 Workspaces: Managing Multiple Packages

For larger projects involving several interdependent packages, Cargo offers workspaces. A workspace allows multiple packages to share a single Cargo.lock file (ensuring consistent dependency versions across the workspace) and a common target/ build directory (potentially speeding up compilation by sharing compiled dependencies).

A workspace is defined by a root Cargo.toml that designates member packages. The member packages still have their own individual Cargo.toml files for package-specific metadata and dependencies.

my_workspace/
├── Cargo.toml        # Workspace manifest (defines members)
├── package_a/        # Member package (e.g., a library)
│   ├── Cargo.toml
│   └── src/
│       └── lib.rs
└── package_b/        # Member package (e.g., a binary depending on package_a)
    ├── Cargo.toml
    └── src/
        └── main.rs

The root Cargo.toml (in my_workspace/) specifies the members:

[workspace]
members = [
    "package_a",
    "package_b",
    # Can also use glob patterns like "crates/*"
]

# Optional: Define shared profile settings for all members
# [profile.release]
# opt-level = 3

# Note: Dependencies defined here are NOT automatically inherited by members.
# Each member package lists its own dependencies in its own Cargo.toml.
# However, a [workspace.dependencies] table can define shared versions
# that members can inherit explicitly.

Running cargo build, cargo test, etc., from the workspace root (my_workspace/) will operate on all member packages.

17.1.4 Multiple Binaries within a Package

A single package can produce multiple executables.

  • The file src/main.rs defines the primary binary crate, which typically shares the package name.
  • Any .rs file placed inside the src/bin/ directory defines an additional binary crate. Each file is compiled into a separate executable named after the file (e.g., src/bin/tool_a.rs compiles to an executable named tool_a).
my_package/
├── Cargo.toml
└── src/
    ├── main.rs         # Compiles to 'my_package' executable
    └── bin/
        ├── cli_tool.rs # Compiles to 'cli_tool' executable
        └── server.rs   # Compiles to 'server' executable
  • Build all binaries: cargo build (or cargo build --bins)
  • Build a specific binary: cargo build --bin cli_tool
  • Run a specific binary: cargo run --bin cli_tool

This structure is useful for packaging a collection of related tools together. Both src/main.rs and the files in src/bin/ can share code from src/lib.rs if it exists in the same package.

17.1.5 Distinguishing Packages and Crates

It’s crucial to understand the distinction:

  • A crate is a single unit of compilation, resulting in one library or one executable.
  • A package is a unit managed by Cargo, defined by Cargo.toml. It contains the source code and configuration to build one or more crates.

Specifically, a single package can contain:

  • Zero or one library crate (whose root is src/lib.rs). A package cannot have more than one library crate defined this way.
  • Any number of binary crates (defined by src/main.rs and files in src/bin/).

In simple projects with only src/main.rs or src/lib.rs, the package effectively contains just one crate. The distinction becomes important in larger projects, libraries with associated binaries, or workspaces where Cargo orchestrates the building of packages which, in turn, produce compiled crates.