
Rust Project Setup
- 56 installs
- 74 repo stars
- Updated July 21, 2026
- existential-birds/beagle
Helps with ai & agent building tasks.
About
rust-project-setup is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- rust-project-setup
- AI & Agent Building
- AI-coding skill
Rust Project Setup by the numbers
- 56 all-time installs (skills.sh)
- Ranked #6,750 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/existential-birds/beagle --skill rust-project-setupAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 56 |
|---|---|
| repo stars | ★ 74 |
| Last updated | July 21, 2026 |
| Repository | existential-birds/beagle ↗ |
What it does
Helps with ai & agent building tasks.
Files
Rust Project Setup
Step-by-step guidance for setting up new Rust projects with proper configuration, linting, and CI.
Quick Reference
| Topic | Reference |
|---|---|
| Cargo.toml configuration, profiles, dependencies | references/cargo-config.md |
| Workspace organization, member layout, shared deps | references/workspace-layout.md |
| GitHub Actions CI, caching, MSRV checks | references/ci-setup.md |
| Feature flags, conditional compilation, build scripts | references/features-conditional.md |
| no_std development, embedded targets, cross-compilation | references/no-std.md |
New Project Checklist
1. Create the Project
# Binary
cargo init my-app
# Library
cargo init --lib my-lib
# Workspace (create Cargo.toml manually)
mkdir my-workspace && cd my-workspace2. Configure Cargo.toml
Set edition, rust-version (MSRV), and metadata:
[package]
name = "my-app"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"3. Set Up Linting
Add clippy and rustfmt configuration:
# Cargo.toml
[lints.clippy]
all = { level = "deny", priority = 10 }
pedantic = { level = "warn", priority = 3 }
[lints.rust]
future-incompatible = "warn"
nonstandard_style = "deny"
# unsafe_op_in_unsafe_fn is deny-by-default in edition 2024 — no need to set itEdition 2024 lint defaults:unsafe_op_in_unsafe_fnis deny by default. Unsafe operations insideunsafe fnrequire explicitunsafe {}blocks. Thegenkeyword is reserved — user#genif needed as an identifier.
# rustfmt.toml
edition = "2024"
reorder_imports = true
imports_granularity = "Crate"
group_imports = "StdExternalCrate"4. Configure Profiles
[profile.release]
lto = true
codegen-units = 1
strip = true5. Set Up CI
Add GitHub Actions workflow for check, clippy, test, and fmt. See references/ci-setup.md.
6. Cargo.lock Policy
- Binaries: Commit
Cargo.lock(reproducible builds) - Libraries: Do NOT commit
Cargo.lock(consumers resolve their own versions) - Add to
.gitignorefor libraries:Cargo.lock
7. Documentation Setup
For library crates, enable doc lints:
// src/lib.rs
#![deny(missing_docs)]Prefer #[expect(lint)] over #[allow(lint)] for temporary suppressions — it warns when the suppression becomes unnecessary:
#[expect(dead_code, reason = "used in next PR")]
fn upcoming_feature() {}Workspace vs Single Crate
| Use | When |
|---|---|
| Single crate | Small project, CLI tool, simple library |
| Workspace | Multiple related crates, shared dependencies, separate compile targets |
Workspaces reduce compile times by sharing dependencies and build artifacts across members.
Project Structure
Binary
my-app/
Cargo.toml
rustfmt.toml
src/
main.rs
lib.rs # separate logic from entry point
tests/
integration_test.rsLibrary
my-lib/
Cargo.toml
rustfmt.toml
src/
lib.rs
module_a.rs
module_b/
mod.rs
types.rs
tests/
api_test.rs
examples/
basic_usage.rsWorkspace
my-workspace/
Cargo.toml # [workspace] definition
rustfmt.toml # shared formatting
crates/
core/ # shared types and logic
api/ # HTTP server
cli/ # command-line interfaceDependency Best Practices
- Pin exact versions for binaries:
serde = "=1.0.210" - Use version ranges for libraries:
serde = "1" - Group features explicitly:
tokio = { version = "1", features = ["rt-multi-thread", "macros"] } - Use
[dev-dependencies]for test-only crates - Review
cargo treefor duplicate versions - Run
cargo auditfor security vulnerabilities - Replace
once_cell/lazy_staticwithstd::sync::LazyLock(stable since Rust 1.80)
Edition 2024 Migration Notes
When migrating existing projects to edition 2024:
unsafe fnbodies now require explicitunsafe {}blocks around unsafe operationsextern "C" {}blocks must be written asunsafe extern "C" {}#[no_mangle]and#[export_name]require#[unsafe(no_mangle)]and#[unsafe(export_name)]genis a reserved keyword — rename anygenidentifiers tor#genor choose a different name-> impl Traitcaptures all in-scope lifetimes by default; use+ use<'a>for precise control!(never type) falls back to!instead of()— review match arms and diverging expressions- Temporaries in
if letand tail expressions drop earlier — review code holding locks or guards in these positions
Run cargo fix --edition to auto-fix most mechanical changes.
Setup completion gates
Use these as objective pass conditions after the checklist—not informal “looks done.”
1. Manifest loads — From the project or workspace root, run cargo metadata --format-version 1. Pass: exit code 0 and the output lists your crate(s) (package name matches what you expect). 2. Lint and format — Run cargo clippy --all-targets (add -- -D warnings if warnings must fail) and cargo fmt --check. Pass: both exit 0 before you treat CI as authoritative. 3. CI present — You committed the workflow you intend to run (see references/ci-setup.md). Pass: at least one pipeline run finishes green for check, clippy, tests, and fmt (or the subset you defined). 4. Lockfile policy — Binary crate: Cargo.lock is committed (git ls-files Cargo.lock prints Cargo.lock). Library crate: Cargo.lock is not tracked (empty git ls-files Cargo.lock, or file gitignored and never added). Pass: the index matches that policy with no surprise Cargo.lock changes.
Related Skills
- rust-best-practices — idiomatic patterns and edition 2024 coding guidance
- rust-code-review — code review covering ownership, unsafe, and trait design
Cargo.toml Configuration
Package Metadata
[package]
name = "my-crate"
version = "0.1.0"
edition = "2024" # latest stable edition
rust-version = "1.85" # minimum supported Rust version (MSRV)
description = "What this crate does"
license = "MIT OR Apache-2.0"
repository = "https://github.com/org/repo"Edition Selection
| Edition | When to Use |
|---|---|
| 2024 | New projects (latest, best defaults) |
| 2021 | Projects supporting older Rust versions |
| 2018 | Legacy compatibility only |
Each edition enables new language features and changes some defaults. Editions are opt-in and backward compatible.
Edition 2024 Key Behavioral Changes
- `unsafe_op_in_unsafe_fn` = deny: Unsafe operations inside
unsafe fnrequire explicitunsafe {}blocks - `unsafe extern` blocks:
extern "C" {}must beunsafe extern "C" {} - `unsafe` attributes:
#[no_mangle]becomes#[unsafe(no_mangle)], same for#[export_name] - `gen` keyword reserved: Use
r#genif you have identifiers namedgen - RPIT lifetime capture:
-> impl Traitcaptures all in-scope lifetimes; use+ use<'a, T>for precise control - `never_type_fallback`:
!falls back to!instead of() - Temporary drop scopes: Temporaries in
if letconditions and tail expressions drop earlier - `IntoIterator` for `Box<[T]>`: Now available without explicit conversion
Run cargo fix --edition to auto-migrate most mechanical changes when upgrading.
MSRV (Minimum Supported Rust Version)
Set rust-version to declare the oldest Rust version your crate supports. CI should test against this version.
rust-version = "1.85"Dependencies
Version Specification
[dependencies]
# Libraries: semver range (compatible updates)
serde = "1"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
# Binaries: exact pinning (reproducible builds)
reqwest = "=0.12.5"
# Git dependencies (development only, never publish with these)
my-fork = { git = "https://github.com/user/fork", branch = "fix" }
# Path dependencies (workspace members)
shared-types = { path = "../shared-types" }
[dev-dependencies]
insta = { version = "1", features = ["yaml"] }
rstest = "0.23"
pretty_assertions = "1"
tokio = { version = "1", features = ["test-util"] }
[build-dependencies]
# Only for build.rs scriptsVersion Specifier Patterns
| Specifier | Meaning | Use When |
|---|---|---|
"1" | >=1.0.0, <2.0.0 | Library deps (wide compatibility) |
"1.4" | >=1.4.0, <2.0.0 | You need features added in 1.4 |
"1.4.3" (or "^1.4.3") | >=1.4.3, <2.0.0 | Default caret behavior |
"~1.4.3" | >=1.4.3, <1.5.0 | Lock to a specific minor version |
"=1.4.3" | Exactly 1.4.3 | Binary pinning, reproducibility |
">=1.4, <1.7" | Range | Avoid known-broken versions |
Set the minimum version that actually works, not the latest. Use cargo +nightly -Zminimal-versions check to verify your lower bounds are correct. If your code needs something added in 1.6, don't specify "1" when "1.6" is the honest minimum.
Patching Dependencies
Override any dependency source temporarily for testing fixes or unreleased changes:
[patch.crates-io]
regex = { path = "/home/dev/regex" }
serde = { git = "https://github.com/serde-rs/serde.git", branch = "fix" }Patches apply globally across the dependency graph but are not carried into published crates. Use for development only.
Feature Flags
Define optional features to reduce compile time and binary size:
[features]
default = ["json"]
json = ["dep:serde_json"]
full = ["json", "yaml", "toml-support"]
yaml = ["dep:serde_yaml"]
toml-support = ["dep:toml"]Use dep: prefix (Rust 1.60+) to avoid implicit feature names from optional dependencies.
Feature Composability Rules
Features must be additive. Enabling a feature should never remove types, modules, or function signatures. Cargo takes the union of all requested features when multiple crates depend on the same dependency with different features — mutually exclusive features break downstream builds.
Key gotchas:
- Conditional public items: If a public struct field or enum variant is gated by a feature, mark the type
#[non_exhaustive]. Otherwise, dependents without the feature may stop compiling when another crate enables it. - Feature-gated trait impls: Adding a trait impl behind a feature is safe. Removing one is breaking.
- Test all combinations: Use
cargo hack check --feature-powerset --no-dev-depsto verify every combination compiles.
Workspace Dependency Inheritance
Define dependency versions once at the workspace root, reference in members:
# Root Cargo.toml
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tracing = "0.1"# Member Cargo.toml
[dependencies]
serde.workspace = true
tokio = { workspace = true, features = ["test-util"] } # extend features per-memberMembers can add features on top of the workspace baseline. Version and base features stay consistent across the workspace.
Deprecated Dependency Replacements
With Rust 1.80+ (required for edition 2024), several popular crates have stdlib replacements:
| Crate | Replacement | Since |
|---|---|---|
once_cell | std::sync::LazyLock, std::cell::LazyCell | 1.80 |
lazy_static | std::sync::LazyLock | 1.80 |
// BAD: external dependency for edition 2024 projects
use once_cell::sync::Lazy;
static CONFIG: Lazy<Config> = Lazy::new(|| Config::load());
// GOOD: stdlib LazyLock (stable since 1.80)
use std::sync::LazyLock;
static CONFIG: LazyLock<Config> = LazyLock::new(|| Config::load());Profiles
Release Profile
[profile.release]
lto = true # link-time optimization (slower build, faster binary)
codegen-units = 1 # single codegen unit (slower build, better optimization)
strip = true # strip debug symbols (smaller binary)
panic = "abort" # smaller binary, no unwinding (not for libraries)Development Profile
[profile.dev]
opt-level = 0 # fast compilation (default)
[profile.dev.package."*"]
opt-level = 2 # optimize dependencies but not your codeTest Profile
[profile.test]
opt-level = 1 # slightly faster test executionCustom Profiles
Define profiles beyond dev/release for specialized builds:
[profile.profiling]
inherits = "release"
debug = true # debug symbols for perf/flamegraph
strip = false
[profile.embedded]
inherits = "release"
opt-level = "s" # optimize for binary size
lto = true
codegen-units = 1
panic = "abort"Use with cargo build --profile profiling. Each profile gets its own target/<profile-name>/ output directory.
Per-Dependency Profile Overrides
Optimize specific dependencies differently from your own code:
[profile.dev.package.serde]
opt-level = 3 # full optimization for serde even in debug
[profile.dev.package."*"]
opt-level = 2 # moderate optimization for all other depsUseful when a dependency is prohibitively slow in debug mode (compression, video encoding, crypto). Note: generic code monomorphized in your crate uses your crate's profile settings, not the dependency override.
Supply Chain Auditing with cargo-deny
Configure cargo-deny for automated dependency auditing in CI:
cargo install cargo-deny
cargo deny init # creates deny.toml
cargo deny check # run all checks# deny.toml
[licenses]
allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause"]
[bans]
multiple-versions = "warn"
wildcards = "deny"
[advisories]
vulnerability = "deny"
unmaintained = "warn"
[sources]
allow-git = []Also run cargo audit for security vulnerability checks. Both tools complement each other: cargo-deny covers licenses and duplicates, cargo-audit focuses on CVEs.
Clippy and Lint Configuration
Package-Level Lints
[lints.clippy]
all = { level = "deny", priority = 10 }
redundant_clone = { level = "deny", priority = 9 }
pedantic = { level = "warn", priority = 3 }
[lints.rust]
future-incompatible = "warn"
nonstandard_style = "deny"
unsafe_code = "deny" # for crates that should never use unsafe
# unsafe_op_in_unsafe_fn is deny-by-default in edition 2024 — no explicit entry neededEdition 2024 Lint Defaults
These lints are deny-by-default in edition 2024 and do not need explicit configuration:
| Lint | Effect |
|---|---|
unsafe_op_in_unsafe_fn | Unsafe ops in unsafe fn require explicit unsafe {} blocks |
never_type_fallback_flowing_into_unsafe | Prevents ! fallback into unsafe contexts |
Use #[expect(lint)] instead of #[allow(lint)] for temporary suppressions — it warns when the suppression becomes unnecessary:
#[expect(clippy::needless_pass_by_value, reason = "required by framework trait")]
fn handler(req: Request) -> Response { /* ... */ }Workspace-Level Lints
Define once, inherit everywhere:
# Root Cargo.toml
[workspace.lints.clippy]
all = { level = "deny", priority = 10 }
pedantic = { level = "warn", priority = 3 }# Member Cargo.toml
[lints]
workspace = truerustfmt.toml
edition = "2024"
max_width = 100
reorder_imports = true
imports_granularity = "Crate"
group_imports = "StdExternalCrate"
use_field_init_shorthand = truePlace in the repository root. Runs automatically with cargo fmt.
Cargo.lock Policy
| Project Type | Commit Cargo.lock? | Reason |
|---|---|---|
| Binary / Application | Yes | Reproducible builds |
| Library | No | Consumers resolve their own versions |
| Workspace with binaries | Yes | Binary members need reproducible builds |
For libraries, add to .gitignore:
Cargo.lockUseful Commands
cargo check # fast type checking without building
cargo build --release # optimized build
cargo test # run all tests
cargo doc --open # generate and view documentation
cargo tree # show dependency tree
cargo audit # check for security vulnerabilities
cargo update # update dependencies within semver constraints
cargo clippy --fix # auto-fix clippy suggestionsCI Setup
GitHub Actions for Rust
Complete Workflow
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Dwarnings
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo check --workspace --all-targets
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --workspace --all-targets --all-features -- -D warnings
# Edition 2024: unsafe_op_in_unsafe_fn is warn-by-default (not deny).
# With `-D warnings` above, clippy will fail on these. For projects
# mixing editions, add explicit flags:
# -- -D warnings -W unsafe_op_in_unsafe_fn
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test --workspace
fmt:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --all --check
msrv:
name: MSRV
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.85" # match rust-version in Cargo.toml
- uses: Swatinem/rust-cache@v2
- run: cargo check --workspaceKey Actions
dtolnay/rust-toolchain
Installs a specific Rust toolchain. More reliable than actions-rs:
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmtFor MSRV testing:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.85"Swatinem/rust-cache
Caches Cargo registry, build artifacts, and target directory:
- uses: Swatinem/rust-cache@v2Automatic cache key based on Cargo.lock and toolchain. Typical speedup: 2-5x on subsequent runs.
Options:
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true # cache even if build fails
shared-key: "shared" # share cache across jobsMSRV Testing
Test against the minimum supported Rust version declared in Cargo.toml:
# Cargo.toml
rust-version = "1.85"The MSRV job uses that exact version. If it breaks, either:
- Fix the code to work on the MSRV
- Bump
rust-versionin Cargo.toml
Edition 2024 MSRV
Edition 2024 requires Rust 1.85 or later. For projects using edition 2024, the MSRV cannot be lower than 1.85. If your workspace mixes editions (e.g., a member still on edition 2021), the MSRV job should test against the highest edition's minimum:
msrv:
name: MSRV
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.85" # edition 2024 minimum
- uses: Swatinem/rust-cache@v2
- run: cargo check --workspaceConsider a matrix strategy if you support multiple toolchain versions:
msrv:
name: MSRV
strategy:
matrix:
toolchain: ["1.85", "stable"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- uses: Swatinem/rust-cache@v2
with:
shared-key: "msrv-${{ matrix.toolchain }}"
- run: cargo check --workspaceDoc Tests
cargo nextest does not run doc tests. Run them separately:
doc-test:
name: Doc Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test --doc --workspaceSecurity Audit
Check dependencies for known vulnerabilities:
audit:
name: Security Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}Release Builds
For release pipelines, optimize build settings:
release:
name: Release Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo build --release
- uses: actions/upload-artifact@v4
with:
name: binary
path: target/release/my-appCross-Compilation
Build for multiple targets:
cross:
name: Cross-compile
strategy:
matrix:
target:
- x86_64-unknown-linux-gnu
- aarch64-unknown-linux-gnu
- x86_64-apple-darwin
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- run: cargo check --target ${{ matrix.target }}For full cross-compilation builds, consider the cross tool:
cargo install cross
cross build --target aarch64-unknown-linux-gnu --releaseCaching Strategy
| What | How | Impact |
|---|---|---|
| Cargo registry | Swatinem/rust-cache (automatic) | Avoids re-downloading crates |
| Build artifacts | Swatinem/rust-cache (automatic) | Avoids recompiling unchanged deps |
| sccache | Manual setup with RUSTC_WRAPPER=sccache | Shares cache across branches |
For large workspaces, consider sccache for cross-branch cache sharing.
Workflow Tips
- Run
cargo checkbeforecargo test-- it is faster and catches most issues - Use
RUSTFLAGS: -Dwarningsin env to fail on warnings across all jobs - Keep MSRV job separate -- it runs less often and has different cache needs
- Use
cargo nextestfor faster test execution (parallel, better output)
Features and Conditional Compilation
Feature Flag Design
Features must be additive and composable. Enabling a feature should never remove functionality or break compilation. If crate A compiles with some set of features on crate C, it must also compile with all features enabled on crate C.
Cargo takes the union of all requested features when multiple dependents enable different features on the same crate. Mutually exclusive features break downstream builds.
Default Features
Curate defaults for the common case. Let users opt out of heavy dependencies:
[features]
default = ["json", "logging"]
json = ["dep:serde_json"]
logging = ["dep:tracing"]
full = ["json", "logging", "yaml", "compression"]
yaml = ["dep:serde_yaml"]
compression = ["dep:flate2"]Use dep: prefix (Rust 1.60+) to avoid implicit feature names from optional dependencies.
std Feature Pattern for no_std Crates
Use an additive std feature, not a subtractive no-std feature:
[features]
default = ["std"]
std = []
alloc = []#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(feature = "std")]
pub fn read_file(path: &str) -> std::io::Result<Vec<u8>> {
std::fs::read(path)
}This way, any crate in the dependency graph enabling std adds functionality rather than removing it.
Feature Documentation
Document what each feature enables. Users should not have to read source to understand features:
[package.metadata.docs.rs]
all-features = true # build docs with all features enabledTesting Feature Combinations
Use cargo-hack to verify all feature combinations compile:
cargo install cargo-hack
cargo hack check --feature-powerset --no-dev-depsConfigure CI to run this check. Any combination of features must compile.
Conditional Compilation
#[cfg(...)] Attribute
Place on items (functions, types, impl blocks, modules, use statements, struct fields):
#[cfg(feature = "metrics")]
mod metrics;
#[cfg(target_os = "linux")]
fn platform_init() { /* linux-specific */ }
#[cfg(all(feature = "std", target_arch = "x86_64"))]
fn optimized_path() { /* ... */ }cfg_attr for Conditional Attributes
Apply attributes only when a condition holds:
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Config {
pub name: String,
}
#[cfg_attr(miri, ignore)]
#[test]
fn expensive_test() { /* skipped under Miri */ }Common cfg Options
| Option | Example | Use |
|---|---|---|
feature = "name" | cfg(feature = "json") | Feature-gated code |
target_os | cfg(target_os = "macos") | OS-specific code |
unix / windows | cfg(unix) | OS family shorthand |
target_arch | cfg(target_arch = "aarch64") | Architecture-specific |
test | cfg(test) | Test-only code (current crate only) |
debug_assertions | cfg(debug_assertions) | Debug mode only |
Combine with all(), any(), not():
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn unix_like_setup() { /* ... */ }Conditional Dependencies
Gate platform-specific dependencies in Cargo.toml:
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["winuser"] }
[target.'cfg(unix)'.dependencies]
nix = "0.29"Note: only target-based cfg options work here. Feature and context options are not available at dependency resolution time.
Build Scripts (build.rs)
Use build scripts for compile-time code generation and native library compilation:
// build.rs
fn main() {
// Link a native library
println!("cargo:rustc-link-lib=static=mylib");
println!("cargo:rustc-link-search=native=/usr/local/lib");
// Set a custom cfg option
println!("cargo:rustc-cfg=has_feature_x");
// Rerun only when this file changes
println!("cargo:rerun-if-changed=wrapper.h");
}Declare build script dependencies separately:
[build-dependencies]
cc = "1" # compile C/C++ code
bindgen = "0.72" # generate FFI bindingsProject Directory Organization
Examples Directory
Place runnable examples in examples/:
examples/
basic.rs # cargo run --example basic
advanced/
main.rs # cargo run --example advanced
helper.rsBenchmarks Directory
Place benchmarks in benches/:
benches/
throughput.rs # cargo bench --bench throughputUse criterion for stable benchmarks (the built-in #[bench] is nightly-only):
[[bench]]
name = "throughput"
harness = false
[dev-dependencies]
criterion = { version = "0.8", features = ["html_reports"] }Dependency Auditing
See cargo-config.md for cargo-deny setup covering license compliance, duplicate detection, and vulnerability scanning.
Additive Features Only
Cargo unifies feature sets across the dependency graph: if any crate in the build enables feature X on crate C, every dependent of C sees X enabled. Features must therefore be purely additive — enabling one must never remove, replace, or change the signature of behavior another caller relies on. Mutually exclusive features (std vs no_std, tokio vs async-std, sync vs async) silently break downstream consumers the moment two transitive deps disagree.
A common anti-pattern is a "default-on" / "default-off" pair where enabling both yields broken behavior:
# Anti-pattern: mutually exclusive runtimes
[features]
default = ["tokio"]
tokio = ["dep:tokio"]
async-std = ["dep:async-std"] # enabling BOTH compiles two runtimesThe correct shape splits incompatible behavior into separate crates (mycrate-tokio, mycrate-async-std) or picks one runtime as the only supported choice. See workspace-layout.md for the multi-crate facade pattern.
Optional Dependencies Are Features
Adding optional = true to a dependency automatically creates a same-named feature. Cargo 1.60+ supports dep: syntax to suppress the implicit feature and keep dep names out of the feature namespace:
[dependencies]
serde = { version = "1", optional = true }
serde_json = { version = "1", optional = true }
[features]
default = []
serialization = ["dep:serde", "dep:serde_json"]Without dep:, serde becomes a public feature whether you wanted it or not, and renaming the dep becomes a breaking change. Pair this with cargo-hack to test the full feature powerset in CI:
cargo hack check --feature-powerset --no-dev-depsWorkspace vs Published Version Type Identity
A workspace member that depends on a sibling via path = "../othercrate" produces a different type identity than the same crate downloaded from crates.io. If your published mycrate lists othercrate = "1.0" and a downstream consumer pulls othercrate = "1.1", types crossing that boundary are not the same — even though the source is identical. The symptom is a baffling expected Foo, found Foo error.
Rule: use path deps between workspace members only for unpublished changes. Once a sibling has a release that matches, switch to version = "1.0" (or keep both: { path = "../other", version = "1.0" }). CI strategy: add a "consumer build" job that depends on the published crate via crates.io alongside the workspace-internal build. See workspace-layout.md for the dual-CI matrix.
MSRV (Minimum Supported Rust Version) Discipline
Declare MSRV in the package manifest; Cargo refuses to compile on older toolchains:
[package]
rust-version = "1.85"Policy rules:
- Bumping MSRV is a minor-version break. Bump the minor (
2.6.0→2.7.0), not the patch, so users pinned to the old MSRV can stay on2.6.xand still receive security patches. - CI must test MSRV. Add a job:
rustup install 1.85 && cargo +1.85 check --all-targets --all-features. - Minimal-version testing. Run
cargo +nightly update -Z minimal-versions && cargo +nightly checkto resolve every dep to the lowest version matching its semver range. Catches the bug where you wroteserde = "1"but actually use a method added in1.0.150.
Conditional Compilation Hygiene
[target.cfg(...).dependencies] is evaluated before features and contexts are known. Only built-in target cfgs are available: unix, windows, target_os, target_arch, target_pointer_width, target_env, target_endian. Feature cfgs and test are silently ignored.
# BROKEN: silently ignored, dep never pulled in
[target.'cfg(feature = "compression")'.dependencies]
flate2 = "1"
# CORRECT: optional dep with feature gate
[dependencies]
flate2 = { version = "1", optional = true }
[features]
compression = ["dep:flate2"]#[cfg(test)] is set only when compiling the current crate as a test binary. It is not visible to integration tests (tests/) compiled against your library, and not visible to dependents. Use a dedicated testing feature or pub(crate) test helpers instead.
Review Checks (Features and Versioning)
- [Cargo.toml:LINE] MUTUALLY_EXCLUSIVE_FEATURES — feature pair where enabling both breaks the build (e.g.
tokio+async-std,std+no-std). Cargo unifies feature sets; the broken combination will appear in some downstream graph. Split into separate crates. - [.github/workflows/ci.yml:LINE] NO_FEATURE_POWERSET_IN_CI — CI runs only
cargo buildorcargo test --all-features; addcargo hack check --feature-powerset --no-dev-depsto catch combinations that don't compile. - [Cargo.toml:LINE] WORKSPACE_PATH_ONLY_NO_CONSUMER_BUILD — workspace member depends on sibling via
path = "..."with noversion, and CI never tests the published-version path. Add a consumer-build job or pinversion = "..."alongside the path. - [.github/workflows/ci.yml:LINE] MSRV_CLAIMED_BUT_NOT_TESTED —
rust-version = "1.85"set in Cargo.toml but CI matrix has onlystable. Add a1.85job; otherwise the MSRV claim drifts silently. - [Cargo.toml:LINE] FEATURE_CFG_IN_TARGET_DEPS —
[target.'cfg(feature = "x")'.dependencies]is silently ignored at dep resolution. Move to[dependencies]withoptional = trueand a[features]entry usingdep:syntax. - [src/lib.rs:LINE] DEFAULT_FEATURE_GATES_BEHAVIOR_REMOVAL — disabling a default feature removes a public method or changes a type signature; non-additive. Restructure so the feature only adds items.
- [Cargo.toml:LINE] OPTIONAL_DEP_NO_DEP_PREFIX —
[features] foo = ["serde"]instead of["dep:serde"]leaks the dep name as a public feature. Usedep:(requires MSRV>= 1.60). - [CHANGELOG.md:LINE] MSRV_BUMP_PATCH_RELEASE — MSRV raised from
1.80to1.85in version2.6.1(patch). Bump minor instead so pinned users stay on2.6.xfor security fixes. - [src/lib.rs:LINE] PER_FEATURE_DOCTEST_MISSING — public item gated
#[cfg(feature = "json")]has a doctest that only runs with default features; add#[cfg_attr(not(feature = "json"), doc = "...")]or run doctests undercargo hack. - [src/lib.rs:LINE] FEATURE_GATED_ITEM_DOC_DESYNC —
pub fnis#[cfg(feature = "x")]-gated but rustdoc shows it unconditionally (or vice versa); add#[doc(cfg(feature = "x"))](nightly) and set[package.metadata.docs.rs] all-features = trueso docs.rs renders the gate. - [Cargo.toml:LINE] DEP_VERSION_TOO_LAX_FOR_API_USED — listed
serde = "1"but code calls a method added in1.0.150; verify viacargo +nightly -Z minimal-versions updateand tighten to"1.0.150". - [Cargo.toml:LINE] PUBLIC_FEATURE_GATED_ITEM_NOT_NON_EXHAUSTIVE —
pub enum E { #[cfg(feature = "x")] Variant }without#[non_exhaustive]; transitive enabling will break downstreammatcharms.
no_std Development
Opting Out of the Standard Library
#![no_std] switches the crate prelude from std::prelude to core::prelude, preventing accidental dependence on OS-provided functionality:
#![no_std]
// core types (Option, Result, Iterator) are available through the prelude
// std types (File, HashMap, println!) are notThe attribute only changes the prelude. You can still explicitly use std:: if needed, which enables the common pattern of offering both no_std and std APIs through feature flags.
Three Library Tiers
| Tier | Provides | Requires |
|---|---|---|
core | Fundamental types (Option, Result), iterators, sorting, atomics, marker types | Nothing beyond the hardware |
alloc | Vec, String, Box, Arc, Rc, BTreeMap, format! | A memory allocator |
std | File, net, HashMap, println!, time, threads | An operating system |
std re-exports everything from core and alloc. Most types you access through std:: actually live in core:: or alloc::.
What Each Tier Excludes
- core only: no heap allocation, no collections, no String, no I/O
- core + alloc: no HashMap (requires OS randomness), no filesystem, no networking, no threads
- std: full functionality, requires OS support
Using alloc in no_std
Opt into heap-allocated types without pulling in the full standard library:
#![no_std]
extern crate alloc;
use alloc::vec::Vec;
use alloc::string::String;
use alloc::boxed::Box;
use alloc::sync::Arc;
use alloc::collections::BTreeMap;Replace use std:: with use alloc:: for heap types. Note: HashMap is not in alloc because it requires OS-provided randomness for key hashing.
Custom Allocator
Define a global allocator when the platform has no default:
use core::alloc::{GlobalAlloc, Layout};
struct MyAllocator;
unsafe impl GlobalAlloc for MyAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
// platform-specific allocation
# unimplemented!()
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
// platform-specific deallocation
# unimplemented!()
}
}
#[global_allocator]
static ALLOCATOR: MyAllocator = MyAllocator;Embedded Binary Setup
For targets without an OS, opt out of both the standard library and the Rust runtime:
#![no_std]
#![no_main]
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {} // halt on panic; alternatives: abort, reset device
}
#[unsafe(no_mangle)] // edition 2024 syntax
pub extern "C" fn main() -> ! {
// entry point — never returns
loop {}
}#![no_main]removes the Rust runtime (lang_start), so no command-line arg setup, no signal handlers, no stdout flushing#[panic_handler]is required: defines what happens on panic (must diverge with-> !)- The entry point signature must match the target platform's expectations
Volatile Memory Access
Use volatile reads and writes for memory-mapped hardware registers. The compiler cannot elide or reorder volatile operations:
use core::ptr;
const GPIO_REG: *mut u32 = 0x4000_0000 as *mut u32;
fn set_pin_high(pin: u8) {
unsafe {
let current = ptr::read_volatile(GPIO_REG);
ptr::write_volatile(GPIO_REG, current | (1 << pin));
}
}Use volatile operations when:
- Hardware registers have side effects on read
- Interrupt handlers access shared memory
- Memory-mapped device state must be read/written in exact order
Type-Safe Hardware State Machines
Use PhantomData and zero-sized types to enforce valid hardware states at compile time:
use core::marker::PhantomData;
pub struct On;
pub struct Off;
pub struct Led<State>(PhantomData<State>);
impl Led<Off> {
pub fn turn_on(self) -> Led<On> {
// write to hardware register
Led(PhantomData)
}
}
impl Led<On> {
pub fn turn_off(self) -> Led<Off> {
// write to hardware register
Led(PhantomData)
}
}Methods consume self and return the new state type, making invalid transitions unrepresentable. The PhantomData carries no runtime cost.
Fixed-Size Stack Collections
When heap allocation is unavailable, use stack-allocated collections with const generics. The arrayvec crate provides production-ready ArrayVec<T, N> and ArrayString<N> types that store elements inline with a compile-time capacity limit and fail gracefully when full.
Cross-Compilation
Targets follow the format machine-vendor-os (e.g., thumbv7m-none-eabi, x86_64-unknown-linux-musl):
rustup target add thumbv7m-none-eabi
cargo build --target thumbv7m-none-eabiVerifying no_std Compatibility
Build against a bare-metal target to catch accidental std usage in your code and dependencies:
cargo check --target thumbv7m-none-eabiAdd this to CI for no_std crates. For custom targets without a prebuilt standard library:
rustup component add rust-src
cargo build -Z build-std=core,alloc --target my-custom-target.jsonStatic Memory Preferences
In embedded contexts, prefer static and stack allocations:
| Strategy | When to Use |
|---|---|
const / static | Global configuration, lookup tables, singleton hardware handles |
Stack arrays ([T; N]) | Fixed-size buffers with known bounds |
ArrayVec<T, N> | Variable-length data with a compile-time maximum |
alloc types | Only when dynamic sizing is essential and an allocator is available |
For fallible allocation in alloc-using code, prefer try_ variants (Vec::try_reserve, Box::try_new) over panicking methods when targeting environments where out-of-memory must be handled gracefully.
Workspace Layout
When to Use Workspaces
Use a workspace when you have multiple related crates that:
- Share dependencies (reduces compile time and disk usage)
- Need coordinated versioning
- Have separate build targets (binary + library, multiple binaries)
- Benefit from a shared CI pipeline
Don't use a workspace for a single crate. The overhead isn't worth it.
Basic Structure
my-workspace/
Cargo.toml # workspace root
rustfmt.toml # shared formatting
.github/
workflows/ci.yml # shared CI
crates/
core/ # shared types and logic
Cargo.toml
src/lib.rs
api/ # HTTP server binary
Cargo.toml
src/main.rs
cli/ # CLI binary
Cargo.toml
src/main.rs
tests/ # workspace-level integration tests (optional)Workspace Cargo.toml
[workspace]
resolver = "3" # default for edition 2024; explicit for clarity
members = [
"crates/core",
"crates/api",
"crates/cli",
]
[workspace.package]
edition = "2024"
rust-version = "1.85"
license = "MIT"
repository = "https://github.com/org/repo"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
thiserror = "2"
tracing = "0.1"
[workspace.lints.clippy]
all = { level = "deny", priority = 10 }
pedantic = { level = "warn", priority = 3 }
[workspace.lints.rust]
future-incompatible = "warn"Member Cargo.toml
Members inherit from workspace:
[package]
name = "my-api"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
my-core = { path = "../core" } # path dependency to workspace member
serde.workspace = true # inherit version and features
tokio.workspace = true
axum = "0.8" # member-specific dependency
[lints]
workspace = true # inherit lint configEdition Inheritance in Workspaces
Edition 2024 introduces important workspace-level behaviors:
- Edition inherits from workspace: Members using
edition.workspace = trueinherit the workspace edition. All members get edition 2024 semantics (unsafe block requirements, lifetime capture rules, etc.) - Mixed editions: Members can override with a local
edition = "2021"if needed, but this creates inconsistent behavior across crates — avoid when possible - Resolver: Edition 2024 defaults to resolver
"3"(MSRV-aware); edition 2021 defaults to"2". Setting it explicitly in the workspace root is good practice for clarity - Lint inheritance:
[workspace.lints.rust]applies uniformly, but edition 2024 deny-by-default lints (likeunsafe_op_in_unsafe_fn) activate per-member based on that member's edition
# Root Cargo.toml — all members inherit edition 2024
[workspace.package]
edition = "2024"
rust-version = "1.85"
# Member Cargo.toml — inherits edition 2024 and MSRV
[package]
name = "my-crate"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = trueShared Dependencies
Define versions once in [workspace.dependencies], reference with .workspace = true in members:
# Root Cargo.toml
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
# Member Cargo.toml
[dependencies]
serde.workspace = trueTo add features for a specific member:
[dependencies]
tokio = { workspace = true, features = ["test-util"] }Path Dependencies
Members reference each other with path dependencies:
[dependencies]
core = { path = "../core" }These are resolved at build time. Cargo ensures all workspace members use compatible versions.
Feature Flags Across Workspace
Define features in individual crates and propagate through path dependencies:
# crates/core/Cargo.toml
[features]
default = []
postgres = ["dep:sqlx"]
metrics = ["dep:prometheus"]
# crates/api/Cargo.toml
[dependencies]
core = { path = "../core", features = ["postgres", "metrics"] }Running Commands
# Run across all members
cargo check --workspace
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
# Run for a specific member
cargo test -p my-api
cargo run -p my-cli
# Build specific binary
cargo build --release -p my-apiCommon Patterns
Shared Types Crate
A core or types crate containing shared types, error definitions, and traits:
crates/core/src/
lib.rs # re-exports
error.rs # shared error types
types.rs # domain types
traits.rs # shared trait definitionsBinary + Library Split
Separate the binary entry point from logic for testability:
crates/api/src/
main.rs # entry point, minimal
lib.rs # all logic, imported by main.rs and testsInternal Crates
Mark crates as internal (not published) by omitting version or adding publish = false:
[package]
name = "internal-utils"
publish = falseProfile Tuning for Release Builds
The default [profile.release] leaves serious performance on the table. The knobs that matter for shipped binaries:
opt-level = 3— full optimization (default). Drop to"s"or"z"only for size-constrained targets (wasm, embedded).lto = "thin"— parallel cross-crate inlining. The sweet spot for most projects. Use"fat"(ortrue) for whole-program LTO when binary size and runtime matter more than build time.codegen-units = 1— sacrifices build parallelism for better optimization. Combine with LTO. Only worth it for the final shipped artifact.panic = "abort"— smaller binary, no unwinding tables. Destructors do not run on panic andcatch_unwindbecomes a no-op. The setting is global across all deps — audit every dep's reliance on unwinding before flipping.strip = "symbols"(Cargo 1.59+) — removes debug symbols from the final binary, often shrinking it 50-80%.
[profile.release]
opt-level = 3 # full speed
lto = "thin" # cross-crate inlining
codegen-units = 1 # max optimization for shipped binary
panic = "abort" # audit deps first!
strip = "symbols" # smaller binary, lose backtrace namesSee cargo-config.md for RUSTFLAGS interactions with these knobs.
profile.dev.package Overrides for Slow Deps
Heavy dependencies stay painful in debug mode unless overridden. Compile expensive deps in release mode once; they cache in target/ and never recompile slowly again:
[profile.dev.package."*"]
opt-level = 0 # default: dev profile for our code
[profile.dev.package.serde_derive]
opt-level = 3 # proc-macro compiled once, reused forever
[profile.dev.package.regex]
opt-level = 3 # CPU-bound, kills test wall time in debugBest targets: proc-macro deps (serde_derive, tokio-macros, async-trait) and CPU-bound deps (regex, image, zstd, ring). Caveat: overrides only affect code compiled inside that crate — generics monomorphized in your crate use your profile.
Workspace Compile-Time Budgets at Scale
When a workspace crosses ~20 members and 100k LOC, build time dominates dev iteration. Strategies that compound:
- Split feature flags so dev disables expensive deps (typed-builder, derive-heavy serde features). See features-conditional.md.
- Shared target dir:
CARGO_TARGET_DIR=/shared/targetacross workspaces, orsccachefor cross-machine caching. - `cargo-nextest` for test parallelism — scales beyond
cargo test's per-binary model on workspaces with many crates. - Member partitioning: heavy proc-macro deps live in a single leaf crate; leaves only rebuild on
cargo build -p leaf. - `RUSTFLAGS="-Zthreads=8"` on nightly enables the parallel rustc frontend, a measurable win on workspaces with many small crates.
Cargo.toml Metadata Completeness
For any crate intended for crates.io, the [package] block must be filled out completely. Missing fields silently make your crate undiscoverable or ship the wrong files.
[package]
name = "mycrate"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
description = "Concise one-line summary." # required for publish
license = "MIT OR Apache-2.0"
repository = "https://github.com/org/repo"
documentation = "https://docs.rs/mycrate" # explicit, not inferred
readme = "README.md"
keywords = ["cli", "parser"] # max 5
categories = ["command-line-utilities"] # from crates.io/category_slugs
include = ["src/**/*", "Cargo.toml", "README.md", "LICENSE-*"]Missing categories/keywords and crates.io search ranks you nowhere. Missing include and cargo publish ships your target/, .env, fixture data, and any dotfile not in .gitignore.
Additional Review Checks
- [Cargo.toml] PANIC_ABORT_IN_RELEASE_WITHOUT_AUDIT —
panic = "abort"set globally; review everycatch_unwindsite andDropimpl that performs cleanup. The setting is global across all deps. - [Cargo.toml] MISSING_LTO_IN_RELEASE_PROFILE — release profile without
lto = "thin"or"fat"leaves cross-crate inlining on the table; add when shipping a binary or hot library. - [Cargo.toml] CODEGEN_UNITS_NOT_TUNED_FOR_BINARY — final binary uses default
codegen-units = 16; consider1plus LTO for the shipped artifact. - [Cargo.toml] PROC_MACRO_DEP_NOT_OVERRIDDEN — heavy proc-macro dep (
serde_derive,tokio-macros) without[profile.dev.package.X] opt-level = 3; debug builds pay the cost every clean rebuild. - [Cargo.toml] MISSING_METADATA_FOR_PUBLICATION — package missing
description,license, orrepositoryfor crates.io; publish will fail or the crate will be undiscoverable. - [Cargo.toml] MISSING_INCLUDE_OR_EXCLUDE — no
includeorexclude;cargo publishships build artifacts, dotfiles, and fixtures not in.gitignore. - [Cargo.toml] STRIP_NOT_SET_IN_RELEASE — release binary keeps debug symbols;
strip = "symbols"reduces binary size 50-80% for typical Rust artifacts. - [Cargo.toml] EDITION_NOT_DECLARED —
edition = "..."missing from[package]; silently defaults to 2015 and disables most modern idioms.