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-workspace
2. 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 it
Edition 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 = true
5. 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.rs
Library
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.rs
Workspace
my-workspace/
Cargo.toml # [workspace] definition
rustfmt.toml # shared formatting
crates/
core/ # shared types and logic
api/ # HTTP server
cli/ # command-line interface
Dependency 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.”
- 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) (packagenamematches what you expect). - Lint and format — Run
cargo clippy --all-targets(add-- -D warningsif warnings must fail) andcargo fmt --check. Pass: both exit 0 before you treat CI as authoritative. - 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).
- Lockfile policy — Binary crate:
Cargo.lockis committed (git ls-files Cargo.lockprintsCargo.lock). Library crate:Cargo.lockis not tracked (emptygit ls-files Cargo.lock, or file gitignored and never added). Pass: the index matches that policy with no surpriseCargo.lockchanges.
Related Skills
beagle-rust:rust-best-practices— idiomatic patterns and edition 2024 coding guidancebeagle-rust:rust-code-review— code review covering ownership, unsafe, and trait design