Cargo in depth
Manage multi-crate workspaces, conditional compilation with features, build scripts, and publishing crates to crates.io.
- Set up a Cargo workspace with multiple member crates
- Define and activate optional features with [features]
- Write a basic build.rs build script
- Publish a crate to crates.io with cargo publish
- Generate and view documentation with cargo doc
You've been using Cargo since cargo new. This lesson covers the parts that matter once a project grows beyond a single library: workspaces for multi-crate repos, feature flags for conditional compilation, build scripts for code generation and linking, and the publishing workflow for sharing your work with the community.
Cargo workspaces
A workspace is a directory containing multiple related crates that share a single target/ output directory and Cargo.lock. This avoids recompiling common dependencies for each sub-crate:
my-project/
Cargo.toml ← workspace root
crates/
core/ ← library crate
Cargo.toml
src/lib.rs
cli/ ← binary crate
Cargo.toml
src/main.rsThe root Cargo.toml declares the workspace:
[workspace]
members = ["crates/core", "crates/cli"]
resolver = "2"Each member is an ordinary crate. Dependencies shared between members are compiled once. Run cargo build from the workspace root to build everything.
Workspaces are how nearly all non-trivial Rust projects are organised. The Rust compiler itself, Tokio, and the standard library toolchain all use workspaces. When you see a repo with multiple Cargo.toml files, that's a workspace.
Optional features
Features are a Cargo mechanism for conditional compilation. You define them in Cargo.toml and compile code or dependencies only when a feature is enabled:
[features]
default = ["logging"]
logging = ["dep:tracing"]
tls = ["dep:rustls"]
[dependencies]
tracing = { version = "0.1", optional = true }
rustls = { version = "0.22", optional = true }In source code, use #[cfg(feature = "tls")]:
#[cfg(feature = "tls")]
pub fn tls_connect(addr: &str) {
// Only compiled when the "tls" feature is active
}Activate features from the command line or in dependents' Cargo.toml:
cargo build --features tls
cargo build --no-default-features --features logging# In a crate that depends on yours:
[dependencies]
my-crate = { version = "1", features = ["tls"] }Features let you ship a library where heavy dependencies (cryptography, async runtimes, serialization) are opt-in rather than always present.
Build scripts (build.rs)
A build script is a Rust program that runs before your crate compiles. Name it build.rs in the crate root:
// build.rs
fn main() {
// Tell Cargo to re-run this if the file changes
println!("cargo:rerun-if-changed=build.rs");
// Set a compile-time environment variable
println!("cargo:rustc-env=BUILD_DATE=2026-06-09");
// Link a native library
println!("cargo:rustc-link-lib=z"); // links libz
}In your crate's code, read build-time env vars:
const BUILD_DATE: &str = env!("BUILD_DATE");Common uses: generating code from protobuf or C headers, linking to native libraries, embedding version information.
Build scripts run arbitrary code and have full access to the filesystem and network. Review build scripts in dependencies carefully — they're a common attack vector in supply-chain exploits. Use cargo audit to check for known vulnerabilities.
cargo doc
Generate HTML documentation from your doc comments:
cargo doc --no-deps --open # build and open in browserDoc comments use /// for items and //! for modules:
/// Computes the factorial of `n`.
///
/// # Panics
///
/// Panics if `n` > 20 (would overflow u64).
///
/// # Examples
///
/// ```
/// assert_eq!(factorial(5), 120);
/// ```
pub fn factorial(n: u64) -> u64 {
(1..=n).product()
}Code examples in doc comments are run as tests by cargo test --doc. This ensures your documentation examples are always correct.
Publishing to crates.io
# One-time setup
cargo login # enter your crates.io API token
# Check what would be published
cargo package --list
# Dry run — validates but doesn't publish
cargo publish --dry-run
# Actually publish
cargo publishRequired fields in Cargo.toml before publishing:
[package]
name = "my-crate"
version = "0.1.0"
edition = "2021"
description = "A one-line summary"
license = "MIT OR Apache-2.0"
repository = "https://github.com/you/my-crate"Once published, a version is permanent — you can yank a version to prevent new downloads, but existing downloads aren't affected. Version pinning in Cargo.lock is the safety net.
Check your understanding
Knowledge check
- 1.What is shared across all crates in a Cargo workspace?
- 2.A build.rs script is compiled and run during the crate build process, before the main crate source is compiled.
- 3.A user declares
my-crate = { version = "1" }with no features listed. Which features are active?
Do it yourself
Create a workspace with two members: my-lib (a library) and my-cli (a binary that depends on my-lib). Add an optional verbose feature to my-lib that prints extra output, and activate it from my-cli's Cargo.toml.
mkdir my-workspace && cd my-workspace
cargo new --lib crates/my-lib
cargo new crates/my-cli
# Create workspace root Cargo.toml manually
cargo buildWhere to go next
Cargo organises the project; testing ensures it's correct. Next: writing unit tests, integration tests, and doc tests — and running them effectively.