Cargo, workspaces, crates.io
Read the manifest. Pin the version. Share one lockfile across two crates.
<!-- hal:authoritative:yaml -->
Read the manifest. Pin the version. Share one lockfile across two crates.
§I - Frame
Duha session 16. Topics #16 after the 2026-09-23 ch13-03 interstitial left Cargo unmarked. Today is TRPL Chapter 14: more about Cargo and crates.io. You already build, run, and test with Cargo. Now you customize profiles, read a publishable Cargo.toml, and sketch a workspace.
Three moves land by the end:
- Read the manifest:
[package],[profile.*], and[dependencies]tell you what the crate is, how it builds, and what it pins. - Pin a version: a dependency string like
rand = "0.8.5"(and the packageversionfield) are deliberate, not defaults you ignore. - Sketch a workspace: a root
[workspace]with two members, one path dependency, one sharedCargo.lockandtarget/.
Done-criteria: Can read a Cargo.toml, pin a version, and sketch a two-crate workspace.
Do not publish to crates.io from this lesson. Do not open smart pointers (Ch.15).
§II - Profiles: dev vs release
Cargo picks a release profile when you compile. cargo build uses dev. cargo build --release uses release. The finished line names the profile:
$ cargo build
Finished `dev` profile [unoptimized + debuginfo] ...
$ cargo build --release
Finished `release` profile [optimized] ...
Defaults live in Cargo until you override them. opt-level is the usual lever (0 through 3). Development wants fast compile; release wants fast run:
# Cargo.toml — defaults if you omit the sections
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
Override only what you need. Setting opt-level = 1 under [profile.dev] keeps every other dev default and raises optimization one notch for local builds. Profiles are independent: changing dev does not touch release.
§III - Read Cargo.toml; pin a version
A crate you mean to share needs [package] metadata. Name must be unique on crates.io. version is the package version you publish. description and license are required before cargo publish succeeds:
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
description = "A fun game where you guess what number the computer has chosen."
license = "MIT OR Apache-2.0"
[dependencies]
SPDX identifiers fill license (MIT, Apache-2.0, or MIT OR Apache-2.0). A missing description or license fails publish with a 400 from the registry. Publish is permanent for that version: you cannot overwrite it; you publish a new version instead.
Pinning a dependency is the other version move. In a member crate:
[dependencies]
rand = "0.8.5"
That string is a Cargo version requirement. Cargo resolves it once into the workspace Cargo.lock. Reading a manifest means you can say what the package version is, what each dependency string requires, and whether profiles override defaults. You do not need to memorize every metadata key; you need to find name, version, description, license, and the dependency pins.
Doc comments (///, crate-level //!) and cargo doc --open belong to the publish chapter. Treat them as the public face of the same crate; skip rustdoc HTML layout here.
§IV - Two-crate workspace
A workspace is a set of packages that share one Cargo.lock and one target/ directory. Root Cargo.toml has no [package] section. It starts with [workspace]:
[workspace]
resolver = "3"
members = ["adder", "add_one"]
cargo new adder inside the workspace root creates the binary and appends it to members. cargo new add_one --lib adds the library the same way. Layout:
add/
├── Cargo.lock
├── Cargo.toml # workspace root
├── adder/
│ ├── Cargo.toml
│ └── src/main.rs
├── add_one/
│ ├── Cargo.toml
│ └── src/lib.rs
└── target/ # shared
Cargo does not assume members depend on each other. Wire the path explicitly in adder/Cargo.toml:
[dependencies]
add_one = { path = "../add_one" }
Library:
// add_one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
x + 1
}
Binary:
// adder/src/main.rs
fn main() {
let num = 10;
println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
Build from the workspace root. Run one package with -p:
$ cargo build
$ cargo run -p adder
One lockfile keeps every member on the same resolved versions when both list rand = "0.8.5". Shared target/ avoids rebuilding the same dependency once per member.
Brief chapter edges (name only): cargo install puts a binary crate into ~/.cargo/bin; a binary named cargo-something on your PATH shows up as cargo something. Neither is required for Done-criteria.
§V - One complete proof
- Point at
[package],[profile.dev](or say the default), and[dependencies]in a realCargo.tomland name what each block controls. - Write a dependency pin (
crate = "x.y.z") and say how it differs from the packageversionfield. - Sketch a workspace root with
members = ["adder", "add_one"], a path dep from binary to lib, and say whyCargo.lockandtarget/sit at the root. - Name
cargo run -p adderas the way to run one member from the root. - Say why you will not run
cargo publishfrom this lesson (permanence / out of scope).
When those five hold, session 16's selected depth is done. Smart pointers (Ch.15) wait for the next Topics row.
§VI - Closing
Cargo is more than build and test. Profiles trade compile time for run time. The manifest holds identity, license, and version pins. A workspace shares lock and target across crates you develop together. Read the file. Pin the version. Sketch the two-crate layout. Next TRPL depth is smart pointers when the syllabus seats it.
Done-criteria: Can read a Cargo.toml, pin a version, and sketch a two-crate workspace.