Skip to content

Nothing gates the closure MSRV: it moves on every cargo update with no repo diff #369

Description

@avrabe

The gap

spar has an MSRV. Nothing in the repo states it, and nothing checks it.

  • [workspace.package] in the root Cargo.toml declares no rust-version.
  • There is no rust-toolchain.toml.

So the effective floor is not a repo property at all. It is

max(rust-version) over the entire RESOLVED DEPENDENCY CLOSURE

— an emergent property of Cargo.lock. It moves on every cargo update, with
no diff anywhere in this repo
, and it is discovered at build time by whichever
consumer happens to have the oldest toolchain.

Re-derive it, never retype it:

cargo metadata --format-version 1 --locked \
  | jq -r '.packages[] | select(.rust_version) | .rust_version' \
  | sort -V | tail -1

Measured today: 152 of 235 packages declare rust-version. The max is
1.89 (smol_str@0.3.6); the next tier is 1.87.0 (wit-bindgen, wasip2).
A single transitive bump of smol_str moves the number with nothing in this
repo changing.

This already caused a defect, which is why it's worth a gate

In #364 the tools/fixture-vm nixpkgs pin was moved 24.05 → 25.05 and recorded
in the flake as "✓ cannot compile → can compile." That was false, and I
wrote it.

The reasoning was: edition 2024 needs rustc ≥ 1.85, 25.05 ships 1.86.0, 1.86 ≥
1.85, done. Every step is true. The conclusion is still wrong, because the
edition floor is a necessary condition that got mistaken for the
condition. Cargo said so one CI dispatch later:

error: rustc 1.86.0 is not supported by the following package:
  smol_str@0.3.6 requires rustc 1.89

The failure mode worth naming: the probe genuinely ran and genuinely returned
a true value.
rustc.version = 1.86.0 was real, and it cleared the bound it
was compared against. A one-variable check that comes back true is the most
convincing way to not verify something — it produces the feeling of
verification while silently scoping the claim to the one variable you thought of.

Why nothing catches it

surface catches a raised closure floor?
cargo build in CI No — runners use current stable, always above the floor
rivet validate No — not a Rust concern
Cargo.toml diff review No — the floor isn't written anywhere
tools/fixture-vm nightly Only here, and only as a build-time error inside a nix derivation

That last row is the whole problem: the sole detector is a nightly that was
itself red for 60 consecutive runs (#362), so the detector was dark for two
months. And it fails at build time, not evaluation time, so it survives every
local check the flake permits.

Two honest options

A. Declare rust-version in [workspace.package].
Cheap, one line, makes the floor a reviewable repo fact. Cargo then enforces it
for consumers. Downside: it is a declaration, so it can itself go stale
relative to the closure — it would need the check below anyway to stay honest.

B. A CI check comparing the flake channel's rustc against the closure max.
Directly gates the thing that actually broke:

CLOSURE=$(cargo metadata --format-version 1 --locked \
  | jq -r '.packages[]|select(.rust_version)|.rust_version' | sort -V | tail -1)
CHANNEL=$(nix eval --raw \
  github:NixOS/nixpkgs/<channel>#legacyPackages.x86_64-linux.rustc.version)
# fail if CHANNEL < CLOSURE

Catches the drift on the PR that introduces it rather than on a nightly two
months later. Costs a nix evaluation in CI.

A and B are complementary, not alternatives — A makes the floor visible, B
keeps it true. Recommend both, B first, since B is what would actually have
caught the #364 defect.

Verifying whatever lands

Two-sidedly, or it's decorative:

  • Positive: current Cargo.lock + nixos-25.11 (rustc 1.91.1) → must pass.
  • Negative: same lock pinned against nixos-25.05 (rustc 1.86.0) → must fail
    and name smol_str@0.3.6. If the negative case passes, the check cannot
    fire and is worse than nothing, because it reads as coverage.

Scope

CI plumbing + one Cargo.toml line. No rivet artifact, matching the
#353 / #363 / #364 / #366 precedent — this repo carries no REQ-CI-* ids.
Flagged so the omission reads as a choice rather than an oversight.

Suggested for v0.36.0 alongside #358 and #366.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions