The Cargo Guide
Package Selection and Command Scoping
Why Package Selection and Command Scoping Matter
Once a repository contains more than one package or more than one target, Cargo commands stop meaning just "build this project". They begin to mean "build which package, from which manifest context, and for which targets?"
A useful mental model is:
- package selection decides which package or packages Cargo operates on
- target selection decides which targets inside those packages Cargo operates on
- command scoping depends on where you invoke Cargo from and whether you override that with flags
The Three Main Scoping Layers
Cargo command scoping usually has three layers.
First, manifest scope:
- which manifest Cargo is using
Second, package scope:
- which package or workspace members are selected
Third, target scope:
- which library, binary, example, test, or benchmark targets inside those packages are selected
Understanding all three layers makes commands like cargo build, cargo test, and cargo run much easier to reason about in multi-package repositories.
A Small Workspace Example
Suppose you have this workspace:
workspace_demo/
āāā Cargo.toml
āāā app/
ā āāā Cargo.toml
ā āāā src/main.rs
āāā core/
ā āāā Cargo.toml
ā āāā src/lib.rs
āāā tools/
āāā Cargo.toml
āāā src/main.rsRoot manifest:
[workspace]
members = ["app", "core", "tools"]
default-members = ["app", "core"]
resolver = "3"This workspace is enough to demonstrate package selection and command scoping clearly.
Default Behavior Depends on the Selected Manifest
When you run a Cargo command without explicit package-selection flags, the default selected packages depend on the selected manifest.
A useful mental model is:
- if the selected manifest is the root of a workspace, Cargo uses workspace selection rules
- otherwise Cargo operates on the single package defined by that manifest
This is why current directory and --manifest-path matter so much.
Working from the Workspace Root
Suppose you run this from the workspace root:
cd workspace_demo
cargo buildBecause the selected manifest is the workspace root, Cargo uses workspace package-selection behavior.
With this root manifest:
[workspace]
members = ["app", "core", "tools"]
default-members = ["app", "core"]
resolver = "3"that plain cargo build targets app and core by default.
Working from a Member Subdirectory
Now suppose you run Cargo from inside a member directory:
cd workspace_demo/app
cargo buildIn that case, the selected manifest is the app package manifest, so Cargo operates on that package by default.
This is one of the most important practical distinctions in Cargo command scoping: the same command text can select different packages depending on which manifest Cargo is using.
The -p or --package Flag
The -p and --package flags explicitly select one package.
Examples:
cargo build -p app
cargo test -p core
cargo doc --package toolsA useful mental model is:
-poverrides default package selection- it is especially important at workspace roots and in larger monorepos
This is often the cleanest way to make a command unambiguous.
Selecting All Workspace Members with --workspace
The --workspace flag selects all workspace members.
Examples:
cargo build --workspace
cargo test --workspace
cargo doc --workspaceThis is useful when you want a full repository-wide action rather than the root defaults or one package.
In a virtual workspace without default-members, plain root invocation can already behave similarly to --workspace, but using --workspace makes your intent explicit.
Excluding Members with --exclude
The --exclude flag removes selected packages from a workspace-wide operation.
Examples:
cargo build --workspace --exclude tools
cargo test --workspace --exclude appThis is especially useful in large workspaces where one or more members are slow, experimental, platform-specific, or not relevant to the current task.
A good mental model is:
--workspacebroadens selection to all members--excludetrims that broad selection down
Why default-members Changes Root-Level Behavior
The default-members field lets a workspace choose which members root-level commands operate on when package-selection flags are not used.
Example:
[workspace]
members = ["app", "core", "tools"]
default-members = ["app", "core"]
resolver = "3"Now these commands from the workspace root target only app and core by default:
cargo build
cargo test
cargo docThis is a powerful way to keep common root-level commands focused in larger workspaces.
Root Package vs Virtual Workspace Behavior
There is an important difference between a virtual workspace and a workspace that also has a root package.
In a virtual workspace with no root package, root-level commands without package-selection flags default to all workspace members unless default-members is set.
In a non-virtual workspace with a root package, plain root-level commands default to the root package, and you use --workspace or --package to select other members.
This is one reason virtual workspaces often feel clearer in large monorepos.
Target Selection Inside a Package
Once Cargo has selected a package, target-selection flags decide which targets inside that package to operate on.
Common target-selection flags include:
--lib--bin--bins--example--examples--test--tests--bench--benches
These are especially important for commands like cargo build, cargo test, cargo bench, cargo doc, and cargo rustdoc.
Selecting a Binary Target
Suppose a package has this layout:
app/
āāā Cargo.toml
āāā src/
āāā main.rs
āāā bin/
āāā admin.rsNow you can select binaries explicitly:
cargo build -p app --bin admin
cargo run -p app --bin admin
cargo build -p app --binsA useful mental model is:
- package selection chooses
app - target selection chooses which binary inside
app
Selecting Examples
Suppose a package has:
app/
āāā examples/
ā āāā quickstart.rs
ā āāā demo.rs
āāā Cargo.tomlYou can select examples like this:
cargo run -p app --example quickstart
cargo build -p app --examplesExamples are part of the target-selection surface, not separate packages.
Selecting Tests and Benches
Suppose a package has these targets:
app/
āāā tests/
ā āāā api.rs
āāā benches/
āāā perf.rsYou can scope commands to them explicitly:
cargo test -p app --test api
cargo test -p app --tests
cargo bench -p app --bench perf
cargo bench -p app --benchesThis is useful when a package has many targets and you do not want the command to act on everything.
Glob Patterns for Target Selection
Several target-selection flags support common Unix glob patterns.
Examples:
cargo build --bin 'adm*'
cargo test --test 'api*'
cargo bench --bench 'perf*'A practical note is that these patterns should be quoted so your shell does not expand them before Cargo sees them.
How cargo run Is Scoped
cargo run is a little more specific than commands like cargo build or cargo test because it runs one binary or example.
Examples:
cargo run -p app
cargo run -p app --bin admin
cargo run -p app --example quickstartIf the selected package has only one runnable binary target, cargo run can often infer it. If there are several, being explicit is usually better.
How cargo test Is Broader by Default
cargo test has broader default target behavior than cargo run.
When no target-selection flags are given, it may build and run several target categories for the selected packages, including unit tests, integration tests, examples for compilation checks, and doc tests.
That is why target-selection flags are especially useful when you want a narrower test action in a larger package.
Manifest Path Control with --manifest-path
The --manifest-path flag lets you tell Cargo exactly which manifest file to use.
Examples:
cargo build --manifest-path app/Cargo.toml
cargo test --manifest-path core/Cargo.toml
cargo doc --manifest-path Cargo.toml --workspaceThis is one of the most important ways to control command scope programmatically or from scripts, because it decouples Cargo selection from the current working directory.
Why --manifest-path Is So Useful
--manifest-path is especially useful when:
- scripts run from arbitrary directories
- tooling needs to operate on a specific package without
cd - monorepos have many possible Cargo entry points
- you want command behavior to be explicit rather than location-dependent
A practical mental model is:
- current directory is implicit manifest selection
--manifest-pathis explicit manifest selection
Working from Subdirectories Intentionally
Cargo can often locate the relevant workspace context even when you run it from inside a member directory.
For example:
cd workspace_demo/core
cargo testThis usually selects the core package manifest, while still understanding that the package belongs to a surrounding workspace.
This makes subdirectory-based workflows convenient, but it also means that two developers running commands from different directories may unintentionally scope commands differently unless they are explicit.
A Concrete Scoping Comparison
Suppose the workspace root manifest is:
[workspace]
members = ["app", "core", "tools"]
default-members = ["app", "core"]
resolver = "3"Then compare these:
cd workspace_demo
cargo buildThis builds app and core.
cd workspace_demo/tools
cargo buildThis builds only tools by default.
cd workspace_demo
cargo build -p toolsThis builds only tools from the root.
The command text is similar, but the scope changes because the selected manifest context changes.
Selecting Packages and Targets Together
Package selection and target selection often work together.
Examples:
cargo build -p app --bin admin
cargo run -p app --example quickstart
cargo test -p core --lib
cargo bench -p app --bench perfThis is one of the most useful Cargo habits in larger repositories: specify both package and target when you want a command to be completely unambiguous.
Locating Projects Programmatically with cargo metadata
When tools need to discover the current package, workspace members, target layout, or resolved graph in a structured way, cargo metadata is the main command.
Example:
cargo metadata --format-version 1This outputs JSON describing the workspace and package graph.
A useful mental model is:
cargo treeis for human-oriented graph inspectioncargo metadatais for tool-oriented project discovery and programmatic integration
Using cargo metadata with --manifest-path
You can combine project discovery with explicit manifest selection.
Example:
cargo metadata --format-version 1 --manifest-path app/Cargo.tomlThis is useful for scripts, editor integrations, code generators, or custom tooling that need a stable way to discover Cargo project structure without depending on the current working directory.
A Small Programmatic Discovery Workflow
Suppose a tool wants to know which workspace it is in and which packages exist.
A stable shell-level approach is:
cargo metadata --format-version 1 --no-depsAnd if the tool needs to anchor discovery to a specific manifest:
cargo metadata --format-version 1 --no-deps --manifest-path Cargo.tomlThis is far more reliable than manually walking directories and guessing where workspaces begin.
A Full Multi-Package Example
Suppose you have this workspace:
workspace_demo/
āāā Cargo.toml
āāā app/
ā āāā Cargo.toml
ā āāā src/
ā ā āāā main.rs
ā ā āāā bin/
ā ā āāā admin.rs
ā āāā examples/
ā ā āāā quickstart.rs
ā āāā tests/
ā ā āāā api.rs
ā āāā benches/
ā āāā perf.rs
āāā core/
ā āāā Cargo.toml
ā āāā src/lib.rs
āāā tools/
āāā Cargo.toml
āāā src/main.rsUseful scoped commands from the root might include:
cargo build
cargo build --workspace
cargo build --workspace --exclude tools
cargo build -p app --bin admin
cargo run -p app --example quickstart
cargo test -p app --test api
cargo bench -p app --bench perf
cargo metadata --format-version 1 --no-depsThis is the kind of command surface where package selection and target selection become a normal part of everyday workflow.
Command Scoping in Scripts
In scripts, it is usually better to make scoping explicit.
Example:
#!/usr/bin/env sh
set -e
cargo build --manifest-path app/Cargo.toml
cargo test --manifest-path core/Cargo.toml
cargo build --manifest-path Cargo.toml --workspace --exclude toolsThis is often more robust than relying on cd plus implicit root detection, especially in CI or automation.
Common Beginner Mistakes
Mistake 1: assuming cargo build means the same thing from every directory.
Mistake 2: forgetting that root package, virtual workspace, and default-members change root-level behavior.
Mistake 3: using target-selection flags without noticing which package was selected.
Mistake 4: using --workspace but forgetting that --exclude may be needed in large repos.
Mistake 5: writing scripts that depend on current working directory instead of using --manifest-path.
Mistake 6: trying to discover workspaces or packages programmatically without using cargo metadata.
Hands-On Exercise
Create a small workspace and practice command scoping deliberately.
Root Cargo.toml:
[workspace]
members = ["app", "core", "tools"]
default-members = ["app", "core"]
resolver = "3"Then try these commands from the root:
cargo build
cargo build --workspace
cargo build --workspace --exclude tools
cargo build -p app
cargo metadata --format-version 1 --no-depsThen from inside app/:
cargo build
cargo run --bin admin
cargo run --example quickstart
cargo test --test apiThen compare with explicit manifest control:
cargo build --manifest-path app/Cargo.toml
cargo test --manifest-path core/Cargo.tomlThis exercise makes the interaction between manifest context, package selection, and target selection very clear.
Mental Model Summary
A strong mental model for package selection and command scoping in Cargo is:
- Cargo first decides which manifest is selected
- then it decides which package or packages are selected
- then it decides which targets inside those packages are selected
-pselects a package explicitly--workspaceselects all workspace members--excludetrims a workspace-wide selection- target flags like
--bin,--example,--test, and--benchnarrow the action inside a package --manifest-pathis the main way to make manifest scope explicitcargo metadatais the main way to discover projects programmatically
Once this model is stable, Cargo commands in multi-package repositories become much easier to read as scoped operations rather than as magic.
