cargo-rail 0.7.1

Graph-aware testing, dependency unification, and crate extraction for Rust monorepos
Documentation

What It Replaces

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
Dead features cargo-features-manager, manual audit cargo rail unify
MSRV computation cargo-msrv, compile-and-fail loops cargo rail unify
CI waste paths-filter + shell scripts cargo rail affected
CI costs Test everything, bill for everything Test what changed
Crate extraction git subtree, git-filter-repo, Google's Copybara cargo rail split
Release orchestration release-plz, cargo-release, git-cliff cargo rail release

11 dependencies. One config file.


Install

cargo install cargo-rail

Optionally, install via the pre-built binaries or cargo binstall cargo-rail


Quick Start

cargo rail init              # generate .config/rail.toml
cargo rail unify --check     # preview what would change (read-only)
cargo rail unify             # apply changes

Demo using ripgrep codebase.

cargo rail unify on ripgrep — 9 deps unified, 6 dead features pruned


Commands

affected / test

Graph-aware change detection. Only test what's affected:

cargo rail affected                    # list affected crates
cargo rail affected --merge-base       # compare against merge-base (CI)
cargo rail affected -f cargo-args      # output: -p crate1 -p crate2
cargo rail affected -f github-matrix   # output: JSON matrix for Actions
cargo rail test                        # run tests for affected crates
cargo rail test --explain              # show why each crate is affected

CI Integration:

- uses: loadingalias/cargo-rail-action@v1
  id: rail

- run: cargo nextest run ${{ steps.rail.outputs.cargo-args }}
  if: steps.rail.outputs.should-test == 'true'

unify

Dependency unification based on Cargo's resolved output:

cargo rail unify --check    # preview changes (exits 1 if drift detected)
cargo rail unify            # apply to workspace
cargo rail unify --explain  # show reasoning for each change
cargo rail unify undo       # restore from backup

What it does:

  • Unifies versions — writes to [workspace.dependencies], converts members to workspace = true
  • Prunes dead features — removes features never enabled in the resolved graph
  • Fixes undeclared features — adds missing feature declarations to member manifests
  • Detects unused deps — flags dependencies not used anywhere (auto-removes on apply)
  • Computes MSRV — derives minimum Rust version from dependency graph
  • Pins transitives — replaces cargo-hakari without a workspace-hack crate

Multi-target aware: runs cargo metadata per target triple in parallel, computes feature intersections not unions.

split / sync

Extract crates with full git history. Bidirectional sync with 3-way conflict resolution:

cargo rail split init crate/s         # configure extraction
cargo rail split run crate/s          # extract with history
cargo rail split run crate/s --check  # preview (dry-run)

cargo rail sync crate/s               # bidirectional sync
cargo rail sync crate/s --to-remote   # push changes to split repo
cargo rail sync crate/s --from-remote # pull changes (creates PR branch)

Three modes:

  • single — one crate → one repo (most common)
  • combined — multiple crates → one repo (shared utilities)
  • workspace — multiple crates → workspace structure (mirrors monorepo)

Safety: refuses dirty worktree by default. --allow-dirty to override, --yes for CI.

release

Dependency-order publishing with changelog generation:

cargo rail release check crate/s              # validate release readiness
cargo rail release run crate/s --bump minor   # bump, tag, publish
cargo rail release run crate/s --check        # preview release plan

Safety: detects default branch, refuses detached HEAD, warns on non-default branch.

config

Manage configuration:

cargo rail init              # generate .config/rail.toml
cargo rail config locate     # print active config path
cargo rail config print      # print effective config with defaults
cargo rail config validate   # check for errors and unknown keys
cargo rail config sync       # update config with detected targets (incredibly useful on update)

Configuration

Generated by cargo rail init at .config/rail.toml:

targets = ["x86_64-unknown-linux-gnu", "aarch64-apple-darwin"]

[unify]
pin_transitives = false      # enable for hakari replacement
detect_unused = true
prune_dead_features = true

msrv = true
msrv_source = "max"          # deps | workspace | max

[release]
tag_format = "{crate}-{prefix}{version}"
publish_delay = 5            # seconds between publishes

[change-detection]
infrastructure = [".github/**", "scripts/**", "*.sh"]

Full reference: docs/config.md


Real-World Results

Tested on production workspaces:

Repo Crates Deps Unified Dead Features
tikv 72 61 3
meilisearch 19 46 1
helix 12 16 1
tokio 10 10 0
ripgrep 10 9 6
polars 33 2 9
ruff 43 0 0

Demo recordings: examples/


Migrating from cargo-hakari

Create a branch. Run --check first. Review the diff. This touches your entire workspace.

git checkout -b migrate-to-rail
rm -rf crates/workspace-hack
cargo rail init
# set pin_transitives = true in rail.toml
cargo rail unify --check        # review first
cargo rail unify                # apply
cargo check --workspace && cargo test --workspace

Full guide: docs/migrate-hakari.md


Design Decisions

Resolution-based — Uses Cargo's actual resolver output, not syntax parsing. If Cargo resolves it, cargo-rail sees it.

Multi-target — Runs cargo metadata --filter-platform per target in parallel. Computes feature intersections, not unions, w/ guardrails where it counts.

System git — Uses your git binary directly. No libgit2, no gitoxide. Deterministic SHAs.

Lossless TOML — Preserves comments and formatting via toml_edit.

Minimal deps — 11 direct dependencies. Built the release workflow specifically to avoid 200+ dep toolchains.


FAQ

cargo-hakari creates a workspace-hack crate. cargo-rail writes unified versions directly to [workspace.dependencies] — no extra crate. Enable pin_transitives = true for equivalent behavior w/o the added CI check and lockfile steps.

Yes. Writes to [workspace.dependencies] and converts member manifests to { workspace = true }.

Supported. For pin_transitives, cargo-rail auto-selects a workspace member as the transitive host (or configure transitive_host explicitly).

Works via cargo metadata, which respects .cargo/config.toml.

For pure Rust workspaces, yes... it can. cargo-rail provides graph-aware testing, dependency unification, and crate extraction without learning a new build system. If you're using Bazel/Buck2 only for Rust (not polyglot builds), cargo-rail gives you the key benefits — affected analysis, hermetic builds via lockfiles, crate distribution — while staying in Cargo's ecosystem. I'm exploring the best way to build a proper cache feature (local will come first; remote will follow), as well.

Cargo workspaces are the foundation. cargo-rail adds what's missing: automatic version unification across the resolver's actual output, dead feature detection/pruning, MSRV computation from the dependency graph w/ options for how you use it, unused dep detection/removal, and graph-aware change detection. These require analysis Cargo doesn't do.

Depends on your workspace. In a 30-crate workspace where a PR touches 3 crates, you test 3 crates + their dependents instead of 50. I've seen 60-80% reductions in CI minutes for my own workspaces; teams with large workspaces and frequent, focused PRs will likely experience similar numbers.

Cargo unifies deps across your workspace. If crate-a depends on serde and crate-b depends on serde with features = ["derive"], Cargo builds serde once with derive enabled for both. Now crate-a can use #[derive(Serialize)] even though it never declared that feature — it's "borrowing" from crate-b.

This works fine until: (1) you test crate-a in isolation, (2) you publish crate-a, or (3) crate-b drops the feature. Then crate-a breaks with cryptic compile errors.

cargo rail unify detects these borrowed features and auto-fixes them by adding the missing declarations to each crate's Cargo.toml. Cleaner graphs, safer publishes, tests that actually test what you ship.


Documentation


Contributing

Issues, PRs, and feedback welcome.