What It Does
cargo-rail is a single tool that replaces a fragmented ecosystem:
| Problem | Before | After |
|---|---|---|
| Build graph drift | cargo-hakari, workspace-hack crates |
cargo rail unify |
| Unused deps | cargo-udeps, cargo-machete, cargo-shear |
cargo rail unify |
| MSRV guessing | cargo-msrv, compile-and-fail loops |
cargo rail unify |
| CI waste | paths-filter + 1k LoC shell |
cargo rail test |
| Crate extraction | git subtree, Copybara (Java) |
cargo rail split |
| Release chaos | release-plz (hundreds of deps), git-cliff |
cargo rail release |
11 core dependencies. 55 resolved. One config file.
Install
Pre-built binaries: Releases • cargo binstall cargo-rail
Quick Start
Drop into any Rust workspace:
&&
That's it. See what cargo-rail would unify — no changes made until you run cargo rail unify.
https://github.com/user-attachments/assets/93f34633-aa0e-4cde-8723-c81f3f474bac
cargo rail unify on ripgrep — 9 deps unified, 6 dead features pruned
Why cargo-rail
Most Rust monorepo tools solve one problem and introduce their own complexity. You end up with:
- 5+ config files across different tools
- Each tool parses
cargo metadatadifferently - No single source of truth for your dependency graph
- Shell scripts gluing everything together
cargo-rail takes a different approach:
- One tool, one config —
rail.tomldescribes everything - Resolution-based — Uses what Cargo actually resolved, not syntax parsing
- Multi-target aware — Runs
cargo metadata --filter-platformper target in parallel - Minimal footprint — 11 core deps vs 200-600+ in competing toolchains
Workflows
Change Detection
Graph-aware. Only test what's affected by your changes:
For CI: Use cargo-rail-action:
- uses: loadingalias/cargo-rail-action@latest
id: rail
with:
since: ${{ github.event.before }}
- run: cargo nextest run -p ${{ steps.rail.outputs.crates }}
if: steps.rail.outputs.docs-only != 'true'
Dependency Unification
Keep your workspace lean. Per target triple:
What it does:
- Unifies versions based on Cargo's resolver output
- Computes MSRV from the dependency graph
- Prunes dead features that are never enabled
- Detects unused deps (opt-in)
- Pins transitives — replaces
cargo-hakariwithout a workspace-hack crate
Split & Sync
Extract crates with full git history. Keep them in sync:
Three modes: single crate → new repo, multiple crates → new repo, or multiple crates → new workspace.
Release
Dependency-order publishing with changelogs:
Real-World Results
Tested on production Rust workspaces:
| Repo | Members | Deps Unified | Dead Features | Notes |
|---|---|---|---|---|
| tikv | 72 | 61 | 3 | Largest stress test |
| meilisearch | 19 | 46 | 1 | Significant unification |
| helix-db | 6 | 18 | 0 | Growing project |
| helix | 12 | 16 | 1 | Editor workspace |
| tokio | 10 | 10 | 0 | Core ecosystem |
| ripgrep | 10 | 9 | 6 | CLI baseline |
| polars | 33 | 2 | 9 | Already clean |
| ruff | 43 | 0 | 0 | Already unified |
| codex | 49 | 0 | 0 | Already unified |
Each link above points to a fork with cargo-rail configured. Clone and compare.
Demo videos: examples/
Migrating from cargo-hakari?
5 minutes:
# Edit rail.toml: set pin_transitives = true
Full guide: docs/migrate-hakari.md
Design Decisions
Multi-Target Resolution — Most tools run cargo metadata once. cargo-rail runs it per target in parallel, computes feature intersections (not unions). If something is marked unused, it's unused across all your targets.
System Git — Uses your git binary directly. No libgit2, no gitoxide. Real git, real history, deterministic SHAs.
Lossless TOML — Uses toml_edit to preserve comments and formatting. Your manifests stay readable.
Supply-Chain Safety — 11 core dependencies. I built the release workflow specifically because I was uncomfortable with hundreds of deps for release automation.
FAQ
How is this different from cargo-hakari?
cargo-hakari creates a workspace-hack crate. cargo-rail writes unified versions directly to [workspace.dependencies] — no extra crate, no hakari generate step. Enable pin_transitives = true for equivalent behavior. See the migration guide.
Does it work with workspace inheritance?
Yes. cargo-rail writes to [workspace.dependencies] and converts member manifests to { workspace = true }.
What about virtual workspaces?
Supported. For pin_transitives, cargo-rail auto-selects a workspace member as the transitive host (or set transitive_host explicitly).
Private registries?
Works via cargo metadata, which respects .cargo/config.toml. Private registry deps are pinned normally.
Documentation
Contributing
Issues, PRs, and feedback welcome. This is built for the Rust community.
Found this useful? Star the repo to help other Rust teams find it.