展示 HN: Cargo-rail:面向 Rust 的图感知单仓库工具;11 个依赖项
Show HN: Cargo-rail: graph-aware monorepo tooling for Rust; 11 deps

原始链接: https://github.com/loadingalias/cargo-rail

## Cargo Rail:简化 Rust 工作区 Cargo Rail 是一款旨在优化 Rust 工作区的工具,通过自动化依赖管理、CI 效率和发布流程来实现。它统一工作区中的依赖版本,修剪未使用的特性并检测未声明的特性,同时计算最小支持 Rust 版本 (MSRV)。它取代了像 `cargo-hakari` 这样的工具,直接修改 `workspace.dependencies` 而不是创建临时的 crates。 **主要特性:** * **依赖管理:** 统一版本,检测/移除未使用的依赖,并固定传递依赖。 * **CI 优化:** 根据变更识别受影响的 crates,大幅缩短 CI 时间,仅测试必要的部分。 * **Crate 提取/同步:** 支持拆分和同步 crates,并保留完整的 git 历史记录。 * **发布编排:** 通过生成变更日志和按依赖顺序发布来简化发布流程。 * **配置:** 通过 `.config/rail.toml` 文件管理,提供广泛的自定义选项。 Cargo Rail 利用 Cargo 的解析器来保证准确性,并直接与你的 git 仓库交互。它已被证明可以在大型工作区中将 CI 时间减少 60-80%,并为纯 Rust 项目提供了一个比 Bazel/Buck2 更强大的替代方案。它可以通过预构建的二进制文件或 `cargo binstall` 获取。 **了解更多:** [https://github.com/loadingalias/cargo-rail](https://github.com/loadingalias/cargo-rail)

## Cargo-rail:简化 Rust 单仓库 Cargo-rail 是一款新的 Rust 工具,旨在管理大型、复杂的单仓库。由 LoadingALIAS 创建,它解决了大型 Rust 工作区中笨拙的 `justfile`、巨大的依赖图以及难以拆分 crate 等挑战。 该工具专注于四个关键领域:**依赖统一**(用精简的图取代 `cargo-hakari`)、**变更检测**(使用感知图的“affected”命令进行有针对性的测试)、**拆分/同步**(提取带有完整 git 历史记录并双向同步的 crate)以及 **发布/发布**(按依赖顺序发布,并生成变更日志)。 一个核心原则是最小化依赖——Cargo-rail 仅利用 11 个核心依赖项来减少供应链攻击面。它利用现有的工具,如 `git` 和 `Nextest`,来实现确定性构建和高效测试。 在 Tikv 和 Meilisearch 等项目中的早期测试表明,性能有了显著提升,包括降低了 CI 成本(高达 80%)以及更精简、更快的构建速度。
相关文章

原文

cargo-rail

Crates.io Downloads License: MIT Rust 1.85+

InstallQuick StartCommandsResultsGitHub Action


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.


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


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


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'

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.

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.

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.

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)

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


Tested on production workspaces:

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


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.


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. Enable pin_transitives = true for equivalent behavior w/o the added CI check and lockfile steps.

Does it work with workspace inheritance?

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

Virtual workspaces?

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

Private registries?

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

Does this replace Bazel/Buck2 for Rust teams?

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.

Why not just use cargo's built-in workspace features?

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.

How much CI time does this actually save?

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.

What are "undeclared features" and why should I care?

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.



Issues, PRs, and feedback welcome.


GitHubcrates.ioGitHub Action

联系我们 contact @ memedata.com