Rust 28 🦀 Packages, Crates, Workspaces, and Cargo.toml
A Rust project is not a single thing. It is a package, which contains one or more crates. Those crates are compiled from a tree of modules. When a project grows beyond a single package, a workspace manages multiple related packages that share a lockfile and an output directory. This chapter covers the hierarchy—package, crate, module—and the Cargo.toml file that declares all of it.
Key point: A package is a Cargo feature that bundles one or more crates. A crate is a tree of modules that produces a library or an executable. A workspace is a collection of packages that share a Cargo.lock and a target directory. The Cargo.toml file describes how the package is built, what it depends on, and how it relates to other packages.
Why the package, crate, and workspace hierarchy exists
The compilation unit problem. Rust compiles one crate at a time. The crate is the unit of compilation. A package is the unit of distribution and versioning. A workspace is the unit of coordination for multiple packages that evolve together. Each level serves a different purpose, and Cargo uses the distinction to decide what to compile, what to publish, and what to share.
The dependency sharing problem. A project with ten packages that all depend on serde should not download and compile serde ten times. A workspace shares a single Cargo.lock file, so every package uses the same version of every dependency. It also shares a single target directory, so a dependency compiled for one package is reused by the others.
The metadata duplication problem. Every package in a workspace needs a version, an edition, and an author. Writing them in every Cargo.toml is repetitive, and keeping them in sync is error-prone. Workspace inheritance lets the root Cargo.toml define the metadata once, and each member inherits it with .workspace = true.
The scale problem. A single package works for a small project. A monorepo with a library, a CLI, a server, and shared types needs structure. The workspace provides that structure without forcing everything into one package or one compilation unit.
a. Packages, crates, and the Cargo.toml manifest
A package is a Cargo feature that bundles one or more crates. It has a Cargo.toml file that describes how to build the crates. A package can contain at most one library crate and any number of binary crates.
A crate is a tree of modules that produces a single compilation unit. A library crate compiles to a .rlib or .so file and is used by other crates. A binary crate compiles to an executable and has a main function.
The src directory holds the source code. src/main.rs is the crate root of a binary crate with the same name as the package. src/lib.rs is the crate root of a library crate with the same name. Additional binary crates go in src/bin/.
The Cargo.toml file is the manifest. It has several sections:
| Section | Purpose |
|---|---|
[package] | Metadata: name, version, edition, authors, license |
[dependencies] | Runtime dependencies |
[dev-dependencies] | Test and example dependencies |
[build-dependencies] | Build script dependencies |
[features] | Optional features |
[lib] / [[bin]] | Target configuration |
[workspace] | Workspace definition |
The [package] section is required for a package. The name and version are required. The edition is recommended. The [dependencies] section lists the crates the package uses.
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
A dependency can be specified by version, path, git, or a combination. A path dependency refers to another package in the filesystem.
[dependencies]
myapp-core = { path = "../core" }
A virtual manifest is a Cargo.toml with a [workspace] section but no [package] section. It defines a workspace without a root package. This is used when there is no primary package and all packages are organized in separate directories.
b. Workspaces: members, excludes, and shared configuration
A workspace is a collection of packages that share a Cargo.lock file, a target directory, and various settings such as profiles. Packages in a workspace are called workspace members.
The [workspace] table defines the workspace. The members key lists the packages that belong to it. The exclude key removes paths from the workspace.
[workspace]
members = ["crates/core", "crates/api", "crates/cli"]
exclude = ["crates/experimental"]
resolver = "2"
All path dependencies that reside in the workspace directory automatically become members. Additional members are listed with members, which supports globs like crates/*.
The default-members key sets which members are built when a specific package is not selected. This is useful in a virtual workspace with many packages, where building all of them by default would be slow.
[workspace]
members = ["crates/*"]
default-members = ["crates/cli"]
When in the workspace root and no package is selected, Cargo builds the default members. If default-members is not specified, Cargo builds all members in a virtual workspace.
The resolver key sets the dependency resolver version. Workspaces should use resolver = "2" for Rust 2021 and resolver = "3" for Rust 2024. The resolver determines how features are unified across the dependency graph. Without it, Cargo uses the version-1 resolver, which can unify features incorrectly across build and normal dependencies.
The root Cargo.toml can define shared metadata in [workspace.package] and shared dependencies in [workspace.dependencies]. Member packages inherit these with .workspace = true.
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Alice <alice@example.com>"]
license = "MIT"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
A member package inherits the metadata and dependencies:
[package]
name = "myapp-core"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
[dependencies]
serde.workspace = true
tokio.workspace = true
The inheritance eliminates duplication. The version is defined once. The dependency versions are defined once. When a dependency is updated, the update happens in the workspace root, and every member gets the new version.
c. Package selection, locking, and publishing
In a workspace, package-related commands use the -p or --package flag to select a specific package, or --workspace to operate on all packages.
cargo build -p myapp-core
cargo test --workspace
cargo run -p myapp-cli
If neither flag is specified, Cargo uses the package in the current working directory. If the current directory is a virtual workspace root, Cargo uses the default members or all members.
The workspace shares a single Cargo.lock file at the workspace root. Every package in the workspace uses the same version of every dependency. The lockfile should be committed to version control for both binaries and libraries. For binaries, the lockfile ensures reproducible builds. For libraries, the consuming project’s lockfile wins, but the library’s own CI benefits from a committed lockfile.
The workspace shares a single target directory at the workspace root. This avoids recompiling dependencies for each package. When one package depends on another, the dependency is compiled once and reused.
When publishing packages in a workspace, the publish order matters. If myapp-cli depends on myapp-core, then myapp-core must be published first. Cargo does not automatically determine the order, so the publisher must establish it.
The [patch], [replace], and [profile.*] sections in Cargo.toml are only recognized in the root manifest. Member crates’ manifests ignore these sections. This means that a workspace root can override a dependency version or set a profile for all members, but a member cannot override them locally.
Complete Example Session
# ============================================
# PART 1: WORKSPACE ROOT (VIRTUAL MANIFEST)
# ============================================
# /Cargo.toml
[workspace]
members = ["crates/core", "crates/api", "crates/cli"]
resolver = "2"
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Alice <alice@example.com>"]
license = "MIT"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
# ============================================
# PART 2: MEMBER PACKAGE
# ============================================
# /crates/core/Cargo.toml
[package]
name = "myapp-core"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
[dependencies]
serde.workspace = true
anyhow.workspace = true
# ============================================
# PART 3: MEMBER WITH PATH DEPENDENCY
# ============================================
# /crates/api/Cargo.toml
[package]
name = "myapp-api"
version.workspace = true
edition.workspace = true
[dependencies]
myapp-core = { path = "../core" }
serde.workspace = true
tokio.workspace = true
# ============================================
# PART 4: BINARY PACKAGE
# ============================================
# /crates/cli/Cargo.toml
[package]
name = "myapp-cli"
version.workspace = true
edition.workspace = true
[dependencies]
myapp-core = { path = "../core" }
myapp-api = { path = "../api" }
anyhow.workspace = true
# ============================================
# PART 5: BUILDING THE WORKSPACE
# ============================================
cargo build --workspace
# Compiles all three packages
# Shared target directory at workspace root
# Shared Cargo.lock at workspace root
# ============================================
# PART 6: BUILDING ONE PACKAGE
# ============================================
cargo build -p myapp-core
# Compiles only myapp-core and its dependencies
# ============================================
# PART 7: RUNNING THE BINARY
# ============================================
cargo run -p myapp-cli
# Runs the binary in crates/cli
# ============================================
# PART 8: DEFAULT MEMBERS
# ============================================
# /Cargo.toml
[workspace]
members = ["crates/core", "crates/api", "crates/cli"]
default-members = ["crates/cli"]
# cargo build in root builds only cli
# ============================================
# PART 9: EXCLUDING A PATH
# ============================================
[workspace]
members = ["crates/*"]
exclude = ["crates/experimental"]
# experimental is not part of the workspace
# ============================================
# PART 10: PACKAGE WITH BOTH LIB AND BIN
# ============================================
# /crates/tool/Cargo.toml
[package]
name = "mytool"
version = "0.1.0"
[lib]
name = "mytool_lib"
path = "src/lib.rs"
[[bin]]
name = "mytool"
path = "src/main.rs"
The ten parts covered a virtual workspace root, a member package with inheritance, a member with a path dependency, a binary package, building the workspace, building one package, running the binary, default members, excluding a path, and a package with both a library and a binary.
Quick Reference
Cargo.toml Sections
| Section | Purpose |
|---|---|
[package] | Package metadata |
[dependencies] | Runtime dependencies |
[dev-dependencies] | Test and example dependencies |
[build-dependencies] | Build script dependencies |
[features] | Optional features |
[lib] / [[bin]] | Target configuration |
[workspace] | Workspace definition |
[workspace.package] | Shared metadata |
[workspace.dependencies] | Shared dependencies |
Workspace Keys
| Key | Purpose |
|---|---|
members | Packages in the workspace |
exclude | Packages excluded |
default-members | Default packages to build |
resolver | Dependency resolver version |
package | Shared metadata |
dependencies | Shared dependencies |
Package Selection
| Command | Effect |
|---|---|
cargo build -p name | Build one package |
cargo build --workspace | Build all packages |
cargo run -p name | Run one binary |
cargo test --workspace | Test all packages |
Inheritance Syntax
| Member | Inherits From |
|---|---|
version.workspace = true | [workspace.package] |
serde.workspace = true | [workspace.dependencies] |
Best Practices
✅ Do This:
# Use a virtual manifest for multi-package projects
[workspace]
members = ["crates/*"]
resolver = "2" # ✅
# Share metadata in the workspace root
[workspace.package]
version = "0.1.0"
edition = "2021" # ✅
# Share dependencies in the workspace root
[workspace.dependencies]
serde = { version = "1", features = ["derive"] } # ✅
# Inherit in member packages
version.workspace = true
serde.workspace = true # ✅
# Use default-members for a primary binary
default-members = ["crates/cli"] # ✅
# Commit Cargo.lock
# (for reproducible builds) # ✅
❌ Don’t Do This:
# Don't forget resolver in a virtual manifest
[workspace]
members = ["crates/*"]
# resolver missing # ⚠️
# Don't duplicate dependency versions in every member
[dependencies]
serde = "1" # in every member # ⚠️
# Don't use [package] in a virtual manifest
[workspace]
members = ["crates/*"]
[package] # not allowed in virtual manifest # ❌
# Don't define [patch] in a member package
[patch.crates-io]
# only recognized in the workspace root # ❌
# Don't publish dependent packages before their dependencies
# Publish myapp-core before myapp-cli # ⚠️
# Don't forget the path in path dependencies
myapp-core = { path = "core" } # wrong relative path # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Resolver warning | Missing resolver in workspace root | Add resolver = "2" |
| Member not found | Not listed in members | Add to members |
| Path dependency not in workspace | exclude includes it | Remove from exclude |
| Shared version not inherited | Missing .workspace = true | Add the inheritance |
| Package builds everything | No default-members | Add default-members |
| Lockfile conflicts | Separate lockfiles | Use one workspace root |
| Publish fails | Dependency not published | Publish in order |
Real-World Examples
1. Virtual Workspace
[workspace]
members = ["crates/*"]
resolver = "2"
2. Root Package Workspace
[package]
name = "myapp"
version = "0.1.0"
[workspace]
members = ["crates/*"]
3. Shared Metadata
[workspace.package]
version = "0.1.0"
edition = "2021"
license = "MIT"
4. Shared Dependencies
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = "1"
5. Member Inheritance
[package]
name = "core"
version.workspace = true
[dependencies]
serde.workspace = true
6. Path Dependency
[dependencies]
myapp-core = { path = "../core" }
7. Default Members
[workspace]
default-members = ["crates/cli"]
8. Exclude
[workspace]
members = ["crates/*"]
exclude = ["crates/experimental"]
9. Package Selection
cargo build -p myapp-core
cargo test --workspace
10. Lib and Bin in One Package
[lib]
name = "mytool_lib"
[[bin]]
name = "mytool"
Visual
Package, Crate, Module Hierarchy
┌─────────────────────────────────────────────────────────────┐
│ PACKAGE (Cargo.toml) │
│ ├── LIBRARY CRATE (src/lib.rs) │
│ │ ├── mod models │
│ │ ├── mod db │
│ │ └── mod api │
│ └── BINARY CRATE (src/main.rs) │
│ └── mod handlers │
│ │
│ Package: bundle of crates, described by Cargo.toml │
│ Crate: compilation unit, tree of modules │
│ Module: namespace, unit of privacy │
│ │
└─────────────────────────────────────────────────────────────┘
Workspace Structure
┌─────────────────────────────────────────────────────────────┐
│ workspace-root/ │
│ ├── Cargo.toml (workspace definition) │
│ ├── Cargo.lock (shared lockfile) │
│ ├── target/ (shared build output) │
│ └── crates/ │
│ ├── core/ │
│ │ ├── Cargo.toml │
│ │ └── src/lib.rs │
│ ├── api/ │
│ │ ├── Cargo.toml │
│ │ └── src/lib.rs │
│ └── cli/ │
│ ├── Cargo.toml │
│ └── src/main.rs │
│ │
│ One lockfile. One target directory. Shared dependencies. │
│ │
└─────────────────────────────────────────────────────────────┘
Workspace Inheritance
┌─────────────────────────────────────────────────────────────┐
│ ROOT Cargo.toml │
│ [workspace.package] │
│ version = "0.1.0" │
│ edition = "2021" │
│ │
│ [workspace.dependencies] │
│ serde = { version = "1", features = ["derive"] } │
│ │
│ │ │
│ ├──────────────────┬──────────────────┐ │
│ ▼ ▼ ▼ │
│ MEMBER A MEMBER B MEMBER C │
│ version.workspace version.workspace version.workspace│
│ = true = true = true │
│ │
│ serde.workspace serde.workspace serde.workspace │
│ = true = true = true │
│ │
│ Defined once in root. Inherited by all members. │
│ │
└─────────────────────────────────────────────────────────────┘
Package Selection
┌─────────────────────────────────────────────────────────────┐
│ cargo build -p myapp-core │
│ Builds only myapp-core and its dependencies. │
│ │
│ cargo build --workspace │
│ Builds all members. │
│ │
│ cargo build (in crates/cli/) │
│ Builds the package in the current directory. │
│ │
│ cargo build (in workspace root) │
│ Builds default-members, or all members if not set. │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Package | Bundle of crates, described by Cargo.toml |
| Crate | Compilation unit, tree of modules |
| Library crate | src/lib.rs |
| Binary crate | src/main.rs |
| Workspace | Collection of packages |
| Virtual manifest | [workspace] without [package] |
| Members | Packages in the workspace |
| Shared lockfile | Cargo.lock at workspace root |
| Shared target | target/ at workspace root |
| Inheritance | .workspace = true |
| Package selection | -p name, --workspace |
| Resolver | resolver = "2" |
Key takeaways:
- A package bundles one or more crates. It has a
Cargo.tomlthat describes how to build them. A package can contain at most one library crate and any number of binary crates. - A crate is a tree of modules that compiles as a unit. The crate root is
src/lib.rsfor a library orsrc/main.rsfor a binary. Modules organize the code within the crate. - A workspace is a collection of packages that share a lockfile and a target directory. All packages use the same version of every dependency and share the same build output. This avoids duplicate compilation and version drift.
- A virtual manifest defines a workspace without a root package. It has a
[workspace]section but no[package]section. It is used when there is no primary package. - Workspace inheritance eliminates duplication. The root
Cargo.tomldefines shared metadata in[workspace.package]and shared dependencies in[workspace.dependencies]. Member packages inherit them with.workspace = true. - The
resolverkey must be set in a virtual manifest. Without it, Cargo uses the version-1 resolver, which unifies features incorrectly across build and normal dependencies. Useresolver = "2"for Rust 2021. - Package selection uses
-por--workspace. In a workspace,cargo build -p namebuilds one package.cargo build --workspacebuilds all. In the workspace root,default-membersdetermines what is built when no package is selected. - The publish order matters. If one package depends on another, the dependency must be published first. Cargo does not determine the order automatically.
Remember: The Rust project hierarchy has three levels: package, crate, and module. A package is the unit of distribution. A crate is the unit of compilation. A module is the unit of privacy. A workspace coordinates multiple packages that evolve together, sharing a lockfile and a target directory. The Cargo.toml file declares all of it. Use a workspace when a project grows beyond a single package. Use inheritance to avoid duplicating metadata and dependency versions. Use -p and --workspace to control which packages are built. The hierarchy is not complexity for its own sake—it is what allows a large Rust project to be organized, compiled, and released as a coherent whole.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!