Code of the Day
AdvancedEcosystem & tooling

Cargo in depth

Manage multi-crate workspaces, conditional compilation with features, build scripts, and publishing crates to crates.io.

RustAdvanced12 min read
Recommended first
By the end of this lesson you will be able to:
  • 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: for multi- 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.rs

The 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 browser

Doc 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 publish

Required 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. 1.
    What is shared across all crates in a Cargo workspace?
  2. 2.
    A build.rs script is compiled and run during the crate build process, before the main crate source is compiled.
  3. 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 build

Where 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.

Finished reading? Mark it complete to track your progress.

On this page