Cargo Workspaces & Publishing
Reviewed & published by Brayan K
Real Rust projects rarely stay one crate. Workspaces let you manage many related packages together, and crates.io lets you share your work with the world — both powered by Cargo and a few lines of TOML.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll structure a multi-crate workspace with path dependencies and a shared lockfile, then publish a crate to crates.io with proper metadata, semantic versioning, yanking, and docs.rs.
What You'll Learn in This Lesson
- Defining a [workspace] and its members
- A shared Cargo.lock and target/ directory
- Path dependencies between members
- Publishing with cargo publish
- Required metadata and semantic versioning
- cargo yank and docs.rs
1️⃣ Defining a Workspace
A workspace is a root Cargo.toml with a [workspace] table listing its members. All members share one Cargo.lock and one target/ directory, so versions stay consistent and builds are reused.
# A workspace groups related packages under one root.
# Root Cargo.toml (this directory has NO [package], only [workspace]):
[workspace]
resolver = "2"
members = [
"app", # a binary crate
"core", # a shared library crate
]
# File layout:
# my_workspace/
# Cargo.toml <- this root manifest
# Cargo.lock <- ONE shared lockfile
# target/ <- ONE shared build output
# app/Cargo.toml + src/
# core/Cargo.toml + src/Commands like cargo build --workspace and cargo test --workspace then act on every member at once.
2️⃣ Path Dependencies Between Members
Members reference each other with path dependencies: point at the sibling crate's directory and Cargo builds it from local source. This makes cross-crate changes instant — edit the library and the binary sees it immediately.
# A member depends on a SIBLING member via a path dependency.
# app/Cargo.toml:
[package]
name = "app"
version = "0.1.0"
edition = "2021"
[dependencies]
# Point at the sibling crate's directory inside the workspace.
core = { path = "../core" }
# Now in app/src/main.rs you can use it:
# use core::greet;
# fn main() { println!("{}", greet("world")); }
#
# cargo test --workspace runs every member's tests together.When you later publish such a crate, you usually give the dependency both a path (used locally) and a version (used by downstream users from crates.io).
3️⃣ Publishing to crates.io
crates.io is Rust's package registry. Publishing needs a unique name, a semver version, plus a description and license. Published versions are permanent; cargo yank only blocks new use.
# Publishing to crates.io needs complete metadata.
# core/Cargo.toml:
[package]
name = "my_unique_crate" # must be unique on crates.io
version = "0.1.0" # semantic versioning: MAJOR.MINOR.PATCH
edition = "2021"
description = "A short, clear summary of what this crate does."
license = "MIT OR Apache-2.0" # required: an SPDX license expression
repository = "https://github.com/you/my_unique_crate"
readme = "README.md"
# Workflow:
# cargo login <token> # one time, from your crates.io account
# cargo publish --dry-run # verify packaging without uploading
# cargo publish # upload; versions are permanent
#
# Mistake in a release? Stop new users from picking it:
# cargo yank --version 0.1.0 # block new resolutions
# cargo yank --version 0.1.0 --undo # reverse a yankAfter a successful cargo publish, docs.rs automatically builds and hosts your crate's documentation — no setup required.
Mini-Challenge: A Two-Crate Workspace
Sketch a workspace with two members and a path dependency, and recall the publish metadata.
# 🎯 MINI-CHALLENGE: sketch a two-crate workspace.
#
# 1. Write a root Cargo.toml with a [workspace] table whose members are
# "cli" and "lib".
# 2. In cli/Cargo.toml, add a path dependency on the sibling: { path = "../lib" }.
# 3. List the three metadata fields crates.io requires before publishing lib.
#
# Expected members: ["cli", "lib"]
[workspace]
# your members hereCommon Errors (and the fix)
- "missing field license" — Add a license (or license-file) and a description before publishing.
- "crate name is already taken" — Names on crates.io are unique and first-come; pick another.
- "failed to select a version" — Give a path dependency a version too, so published builds resolve it.
- Need to undo a bad release — You can't delete it; use cargo yank and publish a fixed version.
Pro Tips
- 💡 Dry-run first: cargo publish --dry-run catches packaging mistakes before they're permanent.
- 💡 Respect semver: bump MAJOR for breaking changes so downstream users aren't surprised.
- 💡 Share metadata in the workspace: use [workspace.package] to set license and version once.
📋 Quick Reference — Workspaces & Publishing
| Goal | Cargo Syntax |
|---|---|
| Declare workspace | [workspace] members = ["app", "core"] |
| Sibling dependency | core = { path = "../core" } |
| Build all | cargo build --workspace |
| Publish | cargo publish |
| Yank a version | cargo yank --version 0.1.0 |
| Version scheme | MAJOR.MINOR.PATCH |
🎉 Lesson Complete — and You've Finished the Rust Course!
- ✅ A [workspace] groups members with a shared Cargo.lock and target/
- ✅ Members depend on each other through path dependencies
- ✅ cargo publish ships a crate to crates.io with proper metadata and semantic versioning
- ✅ cargo yank retires a bad release without breaking existing users; docs.rs hosts the docs
- 🎓 Congratulations — you've completed the advanced Rust track! From Serde and error handling through concurrency, macros, benchmarking, and now workspaces and publishing, you have the full toolkit to build and ship production-grade Rust. Go publish something great.
Practice quiz
What is a Cargo workspace?
- A set of packages managed together with a shared output and lockfile
- A single binary crate
- A test framework
- A cloud build server
Answer: A set of packages managed together with a shared output and lockfile. A workspace groups multiple related packages that share one Cargo.lock and one target directory.
Which table in the root Cargo.toml declares a workspace?
- [package]
- [dependencies]
- [workspace]
- [lib]
Answer: [workspace]. A root manifest with a [workspace] table (and its members) defines the workspace.
What do all members of a workspace share?
- One source file
- The same crate name
- A single Cargo.lock and target/ directory
- Nothing
Answer: A single Cargo.lock and target/ directory. Workspace members share one Cargo.lock and one build output directory for consistency and speed.
How does one workspace member depend on a sibling member?
- By copying its source
- With a path dependency like { path = "../other" }
- Only through crates.io
- It cannot
Answer: With a path dependency like { path = "../other" }. Local members reference each other with a path dependency pointing at the sibling's directory.
Which command uploads your crate to crates.io?
- cargo deploy
- cargo upload
- cargo release
- cargo publish
Answer: cargo publish. cargo publish packages and uploads the current crate to the crates.io registry.
Which fields are required in [package] before you can publish?
- name and version, plus metadata like license/description
- just authors
- none
- only name
Answer: name and version, plus metadata like license/description. crates.io requires name and version, and in practice a description and license (or license-file) too.
Rust crates follow which versioning scheme?
- Random versioning
- Semantic versioning (MAJOR.MINOR.PATCH)
- Alphabetical versioning
- Calendar versioning
Answer: Semantic versioning (MAJOR.MINOR.PATCH). Cargo expects semantic versioning, where the numbers signal breaking, additive, and fix-level changes.
What does cargo yank do to a published version?
- Renames the crate
- Makes it private
- Deletes it permanently
- Prevents NEW projects from selecting it while existing users keep working
Answer: Prevents NEW projects from selecting it while existing users keep working. Yanking blocks new dependency resolutions on that version but does not delete it, so existing builds still work.
Where is documentation for published crates automatically built and hosted?
- readthedocs.io
- crates.io/docs
- docs.rs
- github.io
Answer: docs.rs. docs.rs automatically builds and hosts rustdoc documentation for every crate published to crates.io.
Under semver, a backward-compatible new feature bumps which number?
- MAJOR
- MINOR
- PATCH
- BUILD
Answer: MINOR. Adding functionality without breaking existing users increments the MINOR version.
Continue this course
- Previous: Benchmarking with Criterion
- Quick reference: Rust cheat sheet
Frequently asked questions
What is a Cargo workspace and why use one?
A workspace is a collection of related packages, called members, that are developed and built together under a single root. The root Cargo.toml contains a [workspace] table listing the members instead of (or alongside) a [package] section. The big wins are consistency and efficiency: every member shares one Cargo.lock, so all crates resolve to exactly the same dependency versions, and they share one target/ directory, so a dependency compiled for one member is reused by the others instead of being rebuilt. Workspaces are ideal when you split an application into a binary plus several internal libraries, or when you maintain a family of crates that evolve in lockstep, because commands like cargo build --workspace and cargo test --workspace operate across all of them at once.
How do workspace members depend on each other?
Members reference one another with path dependencies. In a member's Cargo.toml you add an entry such as core = { path = "../core" }, pointing at the sibling crate's directory within the workspace. Cargo then builds that local crate from source rather than fetching it from crates.io, which makes iterating across crates seamless — a change in core is immediately visible to app. When you publish a crate that has path dependencies, you typically also give those dependencies a version requirement so that the published package can resolve them from crates.io; Cargo lets you specify both a path (used locally) and a version (used by downstream users) on the same dependency.
What does it take to publish a crate to crates.io?
crates.io is the official Rust package registry. To publish, your [package] section must include at least a unique name and a version, and in practice crates.io also requires a description and a license (an SPDX expression like "MIT OR Apache-2.0", or a license-file). You authenticate once with cargo login using a token from your crates.io account, and it is wise to run cargo publish --dry-run first to confirm the package builds and includes the right files. Then cargo publish uploads it. An important rule is that published versions are permanent and immutable — you cannot overwrite or truly delete a version — so you fix mistakes by publishing a new version rather than editing an old one. After publishing, docs.rs automatically builds and hosts your crate's API documentation.
How do semantic versioning and cargo yank work?
Cargo follows semantic versioning, where a version is MAJOR.MINOR.PATCH: you bump PATCH for backward-compatible bug fixes, MINOR for backward-compatible new features, and MAJOR for breaking changes. This lets downstream users opt into updates safely, because Cargo's version requirements assume these rules. If you discover a serious bug or a flawed release, cargo yank --version X.Y.Z marks that version as unavailable for new dependency resolutions while leaving it downloadable for projects whose Cargo.lock already pins it — so you never break existing builds, you just steer new users away. Yanks are reversible with --undo. Yanking is not deletion; the version's contents remain on the registry, which is why publishing a corrected new version is still the right way to actually ship a fix.