Cargo and packaging
Rust’s package and module system has two layers: Cargo (the package manager and build tool) and the module system (the in-source organisation). A crate is the smallest compilation unit — a single library or binary; a package is a Cargo project containing one or more crates with a Cargo.toml manifest. The module system organises items within a crate into a tree of modules (mod declarations), with visibility (pub) controlling what crosses module boundaries. The conventional Rust ecosystem revolves around crates.io (the public package registry) and Cargo’s substantial command surface (cargo new, cargo build, cargo test, cargo doc, cargo publish).
This page covers Cargo workflows, the module system, visibility, and the conventional patterns.
Cargo
Creating a project
cargo new myproject # binary
cargo new mylib --lib # library
A new project’s structure:
myproject/
├── Cargo.toml
├── Cargo.lock
└── src/
└── main.rs (or lib.rs for libraries)
Cargo.toml
The package manifest:
[package]
name = "myproject"
version = "0.1.0"
edition = "2024"
authors = ["Author <author@example.com>"]
description = "A project"
license = "MIT"
repository = "https://github.com/user/myproject"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
thiserror = "1"
[dev-dependencies]
criterion = "0.5"
[build-dependencies]
cc = "1"
[features]
default = []
async = ["tokio"]
The principal sections:
[package]— metadata.[dependencies]— runtime dependencies.[dev-dependencies]— only forcargo testandcargo bench.[build-dependencies]— forbuild.rsscripts.[features]— opt-in compilation features.
Dependency forms
[dependencies]
# From crates.io
serde = "1.0"
serde = { version = "1.0", features = ["derive"] }
# From git
my_lib = { git = "https://github.com/user/my_lib", branch = "main" }
my_lib = { git = "https://github.com/user/my_lib", rev = "abc123" }
# Local path
my_other = { path = "../my_other" }
# Optional (for features)
tokio = { version = "1", optional = true }
Cargo commands
cargo build # debug build
cargo build --release # optimised build
cargo run # build and execute
cargo run --release
cargo run -- arg1 arg2 # pass args to the program
cargo check # type-check without producing a binary
cargo test # run tests
cargo test some_test # run only matching tests
cargo doc # generate HTML documentation
cargo doc --open # generate and open in browser
cargo new project # create new project
cargo init # init in existing directory
cargo add serde # add dependency
cargo add serde --features derive
cargo remove serde # remove dependency
cargo update # update Cargo.lock
cargo fmt # format the source
cargo clippy # lint the source
cargo publish # publish to crates.io
cargo install ripgrep # install a binary crate
Cargo workspaces
Multiple-crate projects (a workspace) share one Cargo.lock and target/:
# Cargo.toml at the workspace root:
[workspace]
members = [
"core",
"api",
"cli",
]
[workspace.dependencies]
serde = "1.0"
# core/Cargo.toml:
[package]
name = "core"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = { workspace = true }
The mechanism admits substantial reuse for monorepos and multi-crate libraries.
Modules
The module system organises code within a crate into a tree.
Inline modules
The mod keyword introduces a module:
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
pub mod advanced {
pub fn power(base: i32, exp: u32) -> i32 {
base.pow(exp)
}
}
}
fn main() {
println!("{}", math::add(2, 3));
println!("{}", math::advanced::power(2, 10));
}
The pub admits external visibility; without it, the item is module-private.
File-based modules
Modules conventionally live in their own files:
src/
├── main.rs (declares: mod math;)
└── math.rs (the math module)
// main.rs:
mod math;
fn main() {
println!("{}", math::add(2, 3));
}
// math.rs:
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
Submodules
Submodules can be inline or in a directory:
src/
├── main.rs (mod math;)
└── math/
├── mod.rs (mod basic; pub mod advanced;)
├── basic.rs
└── advanced.rs
Or, the more contemporary form (Rust 2018+):
src/
├── main.rs (mod math;)
├── math.rs (mod basic; pub mod advanced;)
└── math/
├── basic.rs
└── advanced.rs
The 2018+ form does not require mod.rs; math.rs declares the submodules in math/.
pub and visibility
The conventional visibility modifiers:
| Modifier | Visible to |
|---|---|
| (none) | Same module |
pub | Anywhere |
pub(crate) | Same crate |
pub(super) | Parent module |
pub(in path) | Specified module |
mod a {
pub fn public() {} // visible everywhere
pub(crate) fn crate_only() {} // visible in the crate
pub(super) fn parent_only() {} // visible in the parent
fn private() {} // only in module a
pub mod b {
pub fn nested() {
super::private(); // OK; same crate
}
}
}
The conventional discipline is to make items as private as possible; expose only the necessary surface.
use
The use keyword imports items into scope:
use std::collections::HashMap;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
// Then:
let mut m: HashMap<String, i32> = HashMap::new();
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;
The conventional patterns:
use std::fmt; // imports the module; fmt::Display
use std::fmt::Display; // imports the trait directly
use std::collections::*; // imports everything (rarely used)
use std::collections::{HashMap, HashSet}; // multiple items from one path
use std::io::{self, BufRead}; // imports io and io::BufRead
use std::path::PathBuf as Path; // alias
The conventional discipline is to import the immediate parent (e.g., use std::fmt; and use fmt::Display); for traits commonly used as bare names (Read, Write, Iterator), import them directly.
Re-exports
The pub use admits exposing items at a different path:
mod implementation {
pub fn do_thing() {}
}
pub use implementation::do_thing; // exposes do_thing at the crate root
The mechanism admits a clean public API even when the implementation is split across modules.
// In a library's lib.rs:
mod parser;
mod lexer;
mod ast;
pub use parser::parse; // public API
pub use ast::{Expr, Stmt};
// User code:
use my_lib::{parse, Expr, Stmt}; // simple paths
Crates and binaries
A crate is a compilation unit. The two principal kinds:
- Library crates —
src/lib.rsis the root; produce a library. - Binary crates —
src/main.rsis the root; produce an executable.
A package may contain both:
myproject/
├── Cargo.toml
└── src/
├── lib.rs (the library — reusable code)
└── main.rs (the binary — entry point)
Multiple binaries:
myproject/
├── Cargo.toml
└── src/
├── lib.rs
└── bin/
├── tool1.rs
└── tool2.rs
Each src/bin/*.rs is a separate binary; cargo run --bin tool1 runs the specific binary.
The standard prelude
The std::prelude::v1 is automatically imported into every module:
// Pre-imported (no use needed):
Option, Some, None
Result, Ok, Err
Box, String, Vec
Drop, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug
Iterator, IntoIterator, FromIterator, Extend, DoubleEndedIterator, ExactSizeIterator
ToString, AsRef, AsMut, From, Into, TryFrom, TryInto
println!, print!, eprintln!, eprint!, format!, write!, writeln!
vec!, panic!, assert!, assert_eq!, assert_ne!, debug_assert!, dbg!, todo!, unimplemented!, unreachable!
For everything else, explicit use is required.
Common patterns
Library structure
A small library’s typical layout:
mylib/
├── Cargo.toml
├── src/
│ ├── lib.rs (public API; re-exports)
│ ├── parser.rs
│ ├── error.rs
│ └── types.rs
└── tests/
└── integration.rs
// lib.rs:
mod parser;
mod error;
mod types;
pub use parser::parse;
pub use error::{Error, Result};
pub use types::{Token, Expr};
Binary with shared library
myproject/
├── Cargo.toml
└── src/
├── lib.rs (reusable logic)
└── main.rs (CLI entry point)
// lib.rs:
pub fn process(input: &str) -> String {
// ...
}
// main.rs:
use myproject::process;
fn main() {
let input = std::env::args().nth(1).unwrap();
println!("{}", process(&input));
}
Workspace
myproject/
├── Cargo.toml (workspace root)
├── core/
│ ├── Cargo.toml
│ └── src/lib.rs
├── api/
│ ├── Cargo.toml (depends on core)
│ └── src/lib.rs
└── cli/
├── Cargo.toml (depends on api, core)
└── src/main.rs
The mechanism admits substantial reuse and clean separation.
Tests
Unit tests within the same file:
// In src/math.rs:
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds() {
assert_eq!(add(2, 3), 5);
}
}
Integration tests in tests/:
// In tests/integration.rs:
use mylib::add;
#[test]
fn integration_test() {
assert_eq!(add(2, 3), 5);
}
The cargo test runs both kinds.
Documentation tests
Code in doc-comments runs as tests:
/// Adds two numbers.
///
/// # Examples
///
/// ```
/// use mylib::add;
/// assert_eq!(add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }
The cargo test includes doc tests; cargo doc produces HTML documentation including the examples.
Features
Optional functionality controlled by feature flags:
[features]
default = ["json"]
json = ["serde_json"]
yaml = ["serde_yaml"]
[dependencies]
serde_json = { version = "1", optional = true }
serde_yaml = { version = "0.9", optional = true }
// In code:
#[cfg(feature = "json")]
mod json_support;
#[cfg(feature = "yaml")]
mod yaml_support;
The user enables features at build time:
cargo build --features yaml
cargo build --no-default-features --features yaml
Conditional compilation
The #[cfg] attribute admits platform- and configuration-specific code:
#[cfg(target_os = "linux")]
fn linux_specific() { /* ... */ }
#[cfg(target_os = "windows")]
fn windows_specific() { /* ... */ }
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn unix_like() { /* ... */ }
#[cfg(test)]
mod tests { /* ... */ } // only compiled for tests
#[cfg(debug_assertions)]
fn debug_only() { /* ... */ } // only in debug builds
A note on crates.io
The public package registry. The conventional discipline:
- Package names are kebab-case —
my-lib, notmy_lib. - Versions follow semver —
1.0.0,0.1.0-alpha.1. - Documentation is hosted at docs.rs — automatically built and published.
- Versioning discipline matters — pre-1.0 (
0.x) admits breaking changes; post-1.0 requires major-version bumps for breaking changes.
Publishing:
cargo login # authenticate
cargo publish # publish current package
A note on the 2024 edition
The Rust 2024 edition (the conventional contemporary edition since late 2024) introduced:
- Stabilised
let-else. - Improved
if-letchains. - Improved Cargo features.
- Several minor breaking changes (admitted under edition gating).
The edition is declared in Cargo.toml; new projects from 2024 onwards default to edition = "2024". Crates of different editions interoperate.
A note on the conventional discipline
The contemporary Rust modules-and-packages advice:
- One crate per package for libraries; admit binaries alongside if useful.
- Use workspaces for multi-crate projects.
- Keep
lib.rssmall — re-export the public API; defer details to submodules. - Use file-based modules (Rust 2018+ form, no
mod.rsrequired). - Use
pub(crate)for crate-internal helpers;pubfor the external API. - Document with
///—cargo docproduces excellent docs. - Test with
#[cfg(test)] mod testsfor unit tests;tests/for integration. - Use
cargo fmtandcargo clippy— the conventional formatting and linting. - Use
cargo addandcargo remove(since 1.62) — admits managing dependencies without manual edits.
The combination — Cargo’s substantial workflow surface, the module system, crates.io, the standard prelude, edition-based versioning — is the substance of Rust’s package ecosystem. The mechanism admits substantial robustness, reuse, and tooling.