Skip to content

Commit 81d1a04

Browse files
committed
cargo-rail: unify(msrv): write x.y.z, enforce inheritance option, and add fast CI MSRV gate
1 parent a9896a6 commit 81d1a04

File tree

20 files changed

+829
-260
lines changed

20 files changed

+829
-260
lines changed

.config/rail.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ detect_unused = true # Detect unused dependencies (default: true)
4343
remove_unused = true # Auto-remove unused deps when applying (default: true)
4444
max_backups = 2 # Number of backup files to keep (default: 3)
4545
prune_dead_features = true # Remove features never enabled in resolved graph (default: true)
46-
msrv_source = "max" # How to compute MSRV: deps, workspace, max (default: max)
46+
msrv_source = "workspace" # How to compute MSRV: deps, workspace, max (default: max)
4747
preserve_features = [] # Features to preserve from pruning (glob patterns)
4848
detect_undeclared_features = true # Detect features borrowed via Cargo unification (default: true)
4949
fix_undeclared_features = true # Auto-fix borrowed features (default: true)

.github/workflows/commit.yaml

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,64 @@ jobs:
5555
since: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
5656

5757
# ==========================================================================
58-
# Job 2: CI (skipped if docs-only)
58+
# Job 2: MSRV enforcement (fast fail)
59+
# ==========================================================================
60+
msrv:
61+
name: MSRV Check
62+
needs: detect
63+
if: needs.detect.outputs.docs-only != 'true'
64+
runs-on: ubuntu-latest
65+
timeout-minutes: 15
66+
steps:
67+
- name: Checkout
68+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
69+
with:
70+
persist-credentials: false
71+
72+
- name: Detect MSRV
73+
id: msrv
74+
shell: bash
75+
run: |
76+
msrv="$(python3 - <<'PY'
77+
import pathlib, tomllib
78+
79+
data = tomllib.loads(pathlib.Path("Cargo.toml").read_text())
80+
msrv = (
81+
data.get("workspace", {}).get("package", {}).get("rust-version")
82+
or data.get("package", {}).get("rust-version")
83+
)
84+
if isinstance(msrv, dict):
85+
msrv = None
86+
if not msrv:
87+
raise SystemExit("No rust-version found in Cargo.toml")
88+
print(msrv)
89+
PY
90+
)"
91+
92+
echo "msrv=$msrv" >> "$GITHUB_OUTPUT"
93+
echo "Using MSRV: $msrv"
94+
95+
- name: Install Rust MSRV Toolchain
96+
uses: dtolnay/rust-toolchain@0b1efabc08b657293548b77fb76cc02d26091c7e # master
97+
with:
98+
toolchain: ${{ steps.msrv.outputs.msrv }}
99+
100+
- name: Setup Rust Cache
101+
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
102+
with:
103+
shared-key: "cargo-rail-msrv-v1"
104+
key: msrv-${{ steps.msrv.outputs.msrv }}
105+
cache-on-failure: true
106+
107+
- name: Compile (MSRV)
108+
run: cargo check --workspace --all-targets --all-features --locked
109+
110+
# ==========================================================================
111+
# Job 3: CI (skipped if docs-only)
59112
# ==========================================================================
60113
ci:
61114
name: CI (${{ matrix.target.name }})
62-
needs: detect
115+
needs: [msrv, detect]
63116
if: needs.detect.outputs.docs-only != 'true'
64117
runs-on: ${{ matrix.target.runner }}
65118
timeout-minutes: 30

Cargo.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name = "cargo-rail"
33
version = "0.7.3"
44
edition = "2024"
5-
rust-version = "1.91.0"
5+
rust-version = { workspace = true }
66
license = "MIT"
77
authors = ["loadingalias"]
88
description = "Graph-aware testing, dependency unification, and crate extraction for Rust monorepos"
@@ -27,6 +27,13 @@ exclude = [
2727
"backlog/",
2828
]
2929

30+
[workspace]
31+
members = ["."]
32+
resolver = "3"
33+
34+
[workspace.package]
35+
rust-version = "1.91.0"
36+
3037
[dependencies]
3138
clap = { version = "4.5.53", features = ["derive", "cargo"] }
3239
clap_complete = "4.5.62"

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<a href="https://crates.io/crates/cargo-rail"><img src="https://img.shields.io/crates/v/cargo-rail.svg" alt="Crates.io"></a>
77
<a href="https://crates.io/crates/cargo-rail"><img src="https://img.shields.io/crates/d/cargo-rail.svg" alt="Downloads"></a>
88
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
9-
<a href="https://www.rust-lang.org"><img src="https://img.shields.io/badge/rust-1.85%2B-orange.svg" alt="Rust 1.85+"></a>
9+
<a href="https://www.rust-lang.org"><img src="https://img.shields.io/crates/msrv/cargo-rail" alt="MSRV"></a>
1010
</p>
1111

1212
<p align="center">
@@ -46,6 +46,15 @@ Optionally, install via the [pre-built binaries](https://github.com/loadingalias
4646

4747
---
4848

49+
## MSRV policy
50+
51+
- **MSRV source of truth**: `Cargo.toml` (`rust-version`, written as `major.minor.patch`)
52+
- **Cargo requirement**: Cargo shipped with that Rust release (newer Cargo is fine)
53+
- **CI**: builds on MSRV to prevent accidental bumps
54+
- **Workspaces**: `cargo rail unify` writes `[workspace.package].rust-version`; enable `[unify].enforce_msrv_inheritance = true` to set `[package].rust-version = { workspace = true }` in member crates
55+
56+
---
57+
4958
## Quick Start
5059

5160
```bash
@@ -168,6 +177,7 @@ detect_unused = true
168177
prune_dead_features = true
169178
170179
msrv = true
180+
enforce_msrv_inheritance = false
171181
msrv_source = "max" # deps | workspace | max
172182
173183
[release]

docs/config.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ Controls workspace dependency unification behavior. All options are optional wit
102102

103103
| Option | Type | Default | Description |
104104
|--------|------|---------|-------------|
105-
| `msrv` | `bool` | `true` | Compute and write MSRV to `[workspace.package].rust-version`. The MSRV is determined by `msrv_source`. |
105+
| `msrv` | `bool` | `true` | Compute and write MSRV to `[workspace.package].rust-version` (written as `major.minor.patch`). The MSRV is determined by `msrv_source`. |
106+
| `enforce_msrv_inheritance` | `bool` | `false` | Ensure every workspace member inherits MSRV by setting `[package].rust-version = { workspace = true }` in each member's `Cargo.toml`. This makes `[workspace.package].rust-version` actually apply across the workspace. |
106107
| `msrv_source` | `enum` | `"max"` | How to compute the final MSRV:<br>• `"deps"` - Use maximum from dependencies only (original behavior)<br>• `"workspace"` - Preserve existing rust-version, warn if deps need higher<br>• `"max"` - Take max(workspace, deps) - your explicit setting wins if higher |
107108
| `detect_unused` | `bool` | `true` | Detect dependencies declared in manifests but absent from the resolved cargo graph. |
108109
| `remove_unused` | `bool` | `true` | Automatically remove unused dependencies during unification. Requires `detect_unused = true`. |
@@ -150,6 +151,7 @@ major_version_conflict = "bump"
150151

151152
- In my experience, `major_version_conflict = "bump"` works in most cases; some may require code fixes
152153
- Use `"warn"` for safety, `"bump"` for the leanest build graph
154+
- If `[workspace.package].rust-version` is missing but root `[package].rust-version` is present, `unify` uses it as the baseline and writes it to `[workspace.package].rust-version` (consider enabling `enforce_msrv_inheritance` to avoid drift)
153155

154156
#### Dependency Selection
155157

@@ -193,6 +195,7 @@ transitive_host = "root"
193195
[unify]
194196
# Core options (defaults shown)
195197
msrv = true
198+
enforce_msrv_inheritance = false
196199
msrv_source = "max" # "deps" | "workspace" | "max"
197200
detect_unused = true
198201
remove_unused = true

src/cargo/manifest_writer.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ impl ManifestWriter {
186186
/// Write MSRV (rust-version) to workspace manifest
187187
///
188188
/// Writes to [workspace.package].rust-version so that members can inherit it
189-
/// via `rust-version.workspace = true`
189+
/// via `rust-version = { workspace = true }`
190190
pub fn write_workspace_msrv(&self, workspace_toml_path: &Path, msrv: &semver::Version) -> RailResult<()> {
191191
// Read workspace Cargo.toml
192192
let mut doc = manifest_ops::read_toml_file(workspace_toml_path)?;
@@ -198,8 +198,8 @@ impl ManifestWriter {
198198
let ws_package = manifest_ops::get_or_create_table(&mut doc, "workspace.package")
199199
.context("Failed to create [workspace.package]")?;
200200

201-
// Format MSRV as "major.minor" (standard rust-version format)
202-
let msrv_str = format!("{}.{}", msrv.major, msrv.minor);
201+
// Format MSRV as "major.minor.patch" (explicit and unambiguous)
202+
let msrv_str = format!("{}.{}.{}", msrv.major, msrv.minor, msrv.patch);
203203

204204
// Insert or update rust-version
205205
ws_package.insert("rust-version", toml_edit::value(&msrv_str));
@@ -211,6 +211,25 @@ impl ManifestWriter {
211211
Ok(())
212212
}
213213

214+
/// Ensure a member manifest inherits `rust-version` from `[workspace.package]`.
215+
///
216+
/// Sets `[package].rust-version = { workspace = true }`.
217+
pub fn enforce_member_msrv_inheritance(&self, member_toml_path: &Path) -> RailResult<()> {
218+
let mut doc = manifest_ops::read_toml_file(member_toml_path)?;
219+
220+
let Some(pkg) = doc.get_mut("package").and_then(|p| p.as_table_like_mut()) else {
221+
return Ok(());
222+
};
223+
224+
let mut tbl = toml_edit::InlineTable::new();
225+
tbl.insert("workspace", true.into());
226+
pkg.insert("rust-version", toml_edit::value(toml_edit::Value::InlineTable(tbl)));
227+
228+
self.formatter.format_manifest(&mut doc)?;
229+
manifest_ops::write_toml_file(member_toml_path, &doc)?;
230+
Ok(())
231+
}
232+
214233
/// Remove an unused dependency from a member's Cargo.toml
215234
///
216235
/// # Arguments

0 commit comments

Comments
 (0)