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

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.