Skip to content

The Rust Engineering Handbook

Appendix I — Cargo and Workspace Reference

Operate Rust packages and workspaces with explicit command scope, manifest ownership, feature, target, profile, packaging, and registry boundaries.

Start with the repository, not the command:

cargo-public-api-lab/
├── Cargo.toml                         virtual workspace root
├── Cargo.lock                         one resolved graph
└── crates/
    ├── report-contract/
    │   ├── Cargo.toml                 library package
    │   └── src/lib.rs                 library target
    └── report-cli/
        ├── Cargo.toml                 private binary package
        └── src/main.rs                binary target

This small tree has three distinct layers. The workspace coordinates packages. Each package is described by one manifest and is the unit Cargo can package or publish. Each target is a library, binary, example, test, benchmark, or build script compiled from that package. Confusing those layers produces plausible commands with the wrong scope: testing the default member while believing the workspace was tested, placing a profile in a member where Cargo ignores it, or inspecting a binary while shipping a library archive.

Treat a Cargo operation as a four-part decision:

operation = workspace root + selected packages + selected targets + selected configuration

The configuration includes resolver output, enabled features, target triple, profile, toolchain, lockfile state, environment, and hierarchical Cargo configuration. Record all four parts when a build is evidence for review or release.

An unscoped green command is the build equivalent of an unclassified error: it reports success without saying which decision that success authorizes. Scope the operation before interpreting its result.

Read the root before selecting a command

The fixture uses a virtual manifest: its root has [workspace] but no [package]. That is appropriate when no member is the repository’s primary package. A virtual workspace should name its resolver explicitly because there is no root package edition from which Cargo can infer it.

[workspace]
members = ["crates/report-contract", "crates/report-cli"]
default-members = ["crates/report-contract"]
resolver = "3"

members defines coordination membership. default-members defines the package set used by many package-aware commands invoked at the root without -p or --workspace. In this fixture, a bare cargo check at the root checks report-contract, not both packages. This is convenient for the common path and dangerous as unnamed CI policy.

Use package selection deliberately:

Intent Selection Review note
default root operation no selector depends on root package or default-members
one package -p report-contract package name, not directory name
all workspace members --workspace still subject to target and feature selection
exclude a member --workspace --exclude report-cli valid only with a workspace-wide selection
one manifest from elsewhere --manifest-path path/Cargo.toml selects a starting manifest, not a permanent policy

Target selectors are separate. --lib, --bin report-cli, --bins, --examples, --tests, --benches, and --all-targets answer what to compile inside the selected packages. cargo test --workspace builds the targets needed by the test command; it is not interchangeable with cargo check --workspace --all-targets. A build script and proc macro also run on the host even when their products support another target, so “we cross-compiled it” needs a host/target distinction.

The shared Cargo.lock records the selected dependency graph. Applications normally keep it under version control for reproducibility. Published libraries do not impose their lockfile on downstream resolution, but keeping a workspace lockfile still makes local and CI evidence repeatable. Do not edit it by hand. Use commands such as cargo update -p name --precise version when an intentionally narrow resolution change is required, and inspect the resulting diff.

The shared target/ directory is derived output. It can contain artifacts from multiple packages, targets, profiles, and feature sets; its presence does not prove which combination passed. Cache it as an optimization only. Never treat it as source evidence or package content.

Command families answer different questions

Cargo commands overlap, but their conclusions do not.

Command Primary question What it does not establish
cargo metadata --no-deps --format-version 1 what packages, targets, features, and workspace relationships does Cargo see? that code compiles or metadata is complete for a registry
cargo tree -e features which dependency edges enable which features in this invocation? isolated/no-default feature support
cargo check does analysis and type checking succeed quickly? test behavior or final code generation/linking
cargo build can selected targets be compiled and linked? tests, lints, docs, other features, or other targets
cargo test do selected unit, integration, and documentation-related test steps pass? every target/feature/platform combination
cargo test --doc do Rust code examples in documentation compile and run as doctests? documentation quality or private examples
cargo clippy do selected targets pass the selected lint policy? semantic correctness or a different selection
cargo doc --no-deps can rustdoc build the local public documentation? link usefulness, accuracy, or SemVer compatibility
cargo package --list which files would enter a package archive? that the archive builds or can be uploaded
cargo package can Cargo assemble and normally verify an archive locally? registry acceptance or consumer compatibility

cargo run and cargo install deserve special care. run selects a binary target and executes it with arguments after --; it is an application action, not a library verification step. install builds and places binary crates in an installation root; it does not add a dependency to the current package. Use cargo add to edit dependencies, and review the manifest and lockfile changes it creates.

Commands forward different arguments. cargo test filter -- --nocapture gives filter to Cargo’s test selection and sends --nocapture to the test binary. cargo run --bin report-cli -- --format json selects the Cargo binary before -- and passes the rest to the program. Record the complete command when output is evidence.

Manifest fields are contracts, not filing details

A package manifest describes identity, build topology, compatibility, distribution, and dependency policy. Review at least these groups:

  • [package]: name, version, edition, rust-version, description, license, repository, readme, categories, keywords, and publish policy;
  • targets: inferred src/lib.rs and src/main.rs, or explicit [[bin]], [[example]], [[test]], [[bench]], and [lib] entries when inference is insufficient;
  • dependency kinds: [dependencies], [dev-dependencies], and [build-dependencies], including target-specific forms;
  • [features]: named capability and dependency activation policy;
  • [lints]: package lint policy, optionally inherited from the workspace;
  • include or exclude: package archive selection, which must be checked through the actual file list;
  • links and build: native-library uniqueness and build-script behavior, when applicable.

edition selects language-edition behavior for that package. rust-version declares the minimum supported Rust version (MSRV) as a bare version. They are related but not substitutes: edition 2024 requires an enabling compiler baseline, while a package can deliberately support a later MSRV. Changing rust-version affects consumers and should follow a documented release policy.

Dependency requirements describe an allowed range, not the resolved version. A requirement like "1.4" normally uses caret semantics; Cargo chooses a compatible version and the lockfile records the current choice. Avoid *. Pinning every dependency exactly can prevent coordinated security and compatibility updates without providing the environmental reproducibility of a lockfile and controlled registry. For Git dependencies, a branch name can move; rev is more reproducible, but a released registry package cannot ship an unresolved dependency on arbitrary local paths.

Rename dependencies only when the local API genuinely benefits. The key in [dependencies] becomes the crate name used in source, while package = "registry-name" identifies the actual package. Reviewers otherwise mistake two local names for two resolved packages.

Put shared policy at the owner

Workspace inheritance removes repetition without erasing responsibility. The root may provide [workspace.package], [workspace.dependencies], and [workspace.lints]. A member opts in field by field:

[package]
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true

[lints]
workspace = true

For a shared dependency, the member writes dependency.workspace = true and may add supported member-level options such as features. The root declaration centralizes source and version requirements, but features requested by members still participate in feature unification. Inheritance is not textual substitution: consult the documented set of inheritable keys and inspect cargo metadata rather than guessing.

Some policy is root-owned whether or not members repeat it. [profile.*], [patch], and deprecated [replace] sections are recognized at the workspace root; member copies do not create member-specific release universes. Profile overrides such as [profile.release.package.report-contract] also live at the root.

This ownership is operationally useful. A dependency override changes the graph for the whole workspace. A release profile changes how selected artifacts are built. Those decisions need one review location and one evidence trail.

Resolver 3 and features require a matrix

Resolver version 3 is the Rust 2024 default and requires Rust 1.84 or newer. In a virtual workspace, state resolver = "3" explicitly. Among its behaviors, it changes the default handling of dependency versions incompatible with the selected Rust version toward fallback. It does not discover the project’s real MSRV policy or test old compilers for you.

Cargo feature resolution is additive. If two selected packages enable different features of the same dependency, the resolved build generally receives their union. Resolver 2 and later avoid some unwanted unification across target-specific and development/build edges, but a multi-package invocation can still unify features across selected members. If isolation matters, use separate invocations.

Feature names form public configuration. Define what each enables, whether it is default, whether it changes public items, which combinations are supported, and whether enabling it remains additive. Prefer names that state capability rather than vague labels such as unstable or everything.

A minimally credible matrix is:

cargo check -p report-contract --no-default-features
cargo check -p report-contract --no-default-features --features compact
cargo check -p report-contract --all-features
cargo test  -p report-contract --all-features

Add each important isolated feature and interaction. --all-features does not cover the absence of default features. The default build does not cover optional combinations. A workspace-wide all-feature build may mask member isolation through unification. Test the combinations the support contract names, and reject or make impossible combinations that have no coherent meaning.

Use cargo tree -e features and inverted views such as cargo tree -e features -i dependency to explain activation. Use cargo metadata for machine-readable tooling rather than parsing human command output.

Profiles, targets, and configuration are different axes

Profiles control compiler settings. The standard dev, release, test, and bench profiles provide defaults; custom profiles can inherit from another profile. Settings such as optimization level, debug information, overflow checks, LTO, codegen units, panic strategy, incremental compilation, and stripping affect cost and behavior. Do not equate profile name with a product environment. Record the actual settings that matter.

Profile settings can change observability, compile time, binary size, throughput, panic behavior, and arithmetic checks. Domain correctness must not rely on dev-profile overflow traps or release wrapping. Likewise, panic = "abort" changes containment and cleanup assumptions. Verify the supported profiles whose behavior differs materially.

Target selection answers where the product is compiled to run:

cargo check --target x86_64-unknown-linux-gnu

Installing a target’s standard library and successfully checking it do not test the resulting binary on that platform. Native dependencies, linkers, system libraries, CPU features, filesystem behavior, clocks, signals, and deployment packaging can still differ. Separate compile evidence from execution evidence.

Target-specific dependencies belong under a target table using a target triple or supported cfg(...) expression. Cargo evaluates these for dependency selection, but a build script’s own cfg describes the host unless it explicitly reads Cargo-provided target variables. Do not use feature flags as vague platform detection when a target predicate expresses the actual boundary.

Cargo configuration is hierarchical. Files such as .cargo/config.toml can contribute aliases, build targets, target linkers/runners, registry settings, environment, and network policy. Cargo also accepts environment variables and --config overrides. Configuration discovered from the invocation directory can differ when the command is run elsewhere or via --manifest-path; the nearest applicable configuration and precedence matter.

Commit reproducible, non-secret project configuration. Keep credentials in an approved credential provider or external secret store. Never place registry tokens in Cargo.toml, checked-in .cargo/config.toml, shell history, or command logs. CARGO_HOME is user/tooling state, not repository policy.

Stop at the registry boundary

Packaging is an inspection process before it is a distribution action. For each publishable package:

  1. confirm name, version, MSRV, license, repository, description, readme, and registry policy;
  2. inspect cargo package --list -p package for accidental secrets, generated bulk, missing licenses, and required source;
  3. create and verify the local package archive with the intended feature and target policy;
  4. inspect the archive or build from its contents, not only from the generous workspace checkout;
  5. compare the public API and behavior with the previous release;
  6. verify dependency requirements are valid for the registry and the release order handles workspace dependencies;
  7. only an authorized release owner crosses into authentication and upload.

cargo publish --dry-run performs publication checks without uploading, subject to command/version behavior and registry access. cargo package is the safer default for local archive rehearsal. An actual cargo publish changes external state, and --dry-run must be visibly present if a release script claims not to upload. Keep registry contact and publication outside an ordinary local verification run.

publish = false prevents accidental publication of a package through Cargo. A registry allowlist can restrict where a package may publish. Neither setting replaces access control, protected CI environments, human authorization, version ownership, or registry-side policy. Publishing is effectively irreversible as a coordination event even where yanking is possible: a yank influences new resolution but does not erase already downloaded code or make a reused version legitimate.

A Cargo workspace root fans into members and passes through resolve, feature, target, and package checks before a locked publish boundary.
The local evidence path ends at an inspected package. Publication remains a separately authorized operation.

Rehearse the operation without uploading

Use the companion fixture for these drills.

Predict selection before execution

For each of cargo check, cargo check --workspace, cargo test -p report-contract --all-features, and cargo check --workspace --all-targets, write the selected packages, targets, features, profile, host, and target. Run cargo metadata and cargo tree -e features; correct the prediction rather than merely recording pass/fail.

Audit manifest ownership

Move the root release profile into a member on a temporary branch or disposable copy. Observe Cargo’s warning or effective metadata, then restore it. Repeat conceptually for [patch]. Record why these settings need root ownership and which teams approve changes.

Build a support matrix

Define the fixture’s supported combinations: default std, no default features, compact without defaults, and all features. Add one target triple only if its standard library and linker requirements are available. For each row, distinguish compiled, tested, documented, and executed. Add the declared Rust 1.85 MSRV as an unexecuted row unless that toolchain is installed.

Inspect the archive boundary

Run only cargo package --list -p report-contract --allow-dirty. Verify that source, manifest, readme/license obligations, and no secret or target/ output appear. Explain why the fixture’s publish = false blocks a full package/publish path and why removing it requires release-owner review.

Cargo operation card

  • Name workspace root, selected packages, selected targets, feature set, profile, toolchain, host, and target.
  • Treat a virtual workspace’s resolver as explicit policy.
  • Confirm bare root commands match default-members; use --workspace when all members are intended.
  • Keep root-owned profiles and dependency overrides at the root.
  • Review inherited metadata, dependencies, and lints at both provider and consumer.
  • Inspect the resolved graph and feature activation; test no-default, isolated, default, and supported combined features.
  • Separate target compilation from execution on that platform.
  • Record configuration sources and keep credentials outside repository files and logs.
  • Inspect the package file list and archive from package contents.
  • Keep upload behind separate identity, authorization, and registry policy.

Cargo is not merely a command runner. It is the coordinator for a graph of packages, targets, configurations, artifacts, and distribution identities. Review becomes reliable when each setting has an owner and each command has an explicit selection. Appendix J takes the resulting package surface and asks the harder question: what promises do its public APIs make to callers?

Sources and version notes

  • The Cargo Workspaces reference defines members, default members, shared lock/output behavior, inheritance, and root-only sections.
  • The manifest format, Cargo targets, and specifying dependencies are authoritative for package metadata and target/dependency declarations.
  • The dependency resolver reference documents resolver versions, feature unification, target resolution, and Rust-version-aware selection. Resolver 3 requires Rust 1.84 or newer and is the edition 2024 default.
  • The features, profiles, and configuration chapters define their respective axes and precedence rules.
  • The Cargo command reference is authoritative for current command flags. Recheck scripts against the selected Cargo version rather than treating this card as a parser specification.
  • The package and publish command references define local packaging and registry operations. The exercises stop at local package inspection.
  • The companion fixture is examples/rust-engineering-handbook/appendices/cargo-public-api-lab/. It targets edition 2024, declares Rust 1.85 as MSRV, and was written for resolver 3. Verification records must name the locally installed Rust/Cargo version; the declared MSRV and outline snapshot are separate claims.