Musings on Rust RFC #3537
I wrote about 600 words on Rust RFC #3537, but before I could do that, I wrote almost 2000 words working through my vague worries in different scenarios. This blog is a lightly edited version of my initial braindump.
I am, in principle, supportive of adding a new resolver version that is Rust version aware, although I’m unsure if resolver.precedence = "rust-version"
is the right default. (I won’t rehash this in any detail; others have covered the same ground. I basically agree with @mitsuhiko here that shipping just the new resolver as an opt-in — maybe even requiring an explicit resolver.precedence
initially — would be a good way of gathering more data.)
I also strongly support the changes to cargo update
and friends that will result in more obvious notices to users when they have old versions of dependencies, and the idea of making a MSRV violation an overridable lint.
From where I sit, I see three distinct problems with the current RFC, all around rust-version
.
rust-version
becomes semantically overloaded
rust-version
is currently clearly defined as the minimum version supported by a crate.
This clarity is removed in the proposed documentation update, by weakening the definition of the field to “what version of the Rust language and compiler your package can be compiled with”.
It feels like rust-version
now has three different meanings, depending on which part of the RFC you’re reading:
- The existing meaning: the minimum supported Rust version.
- A version that just happens to work with the given package.
- With
auto
, possibly even a statement of policy (latest only)?
These are different concerns, and shouldn’t be overloaded into a single field.
The last point could be dealt with by introducing (in a later RFC, presumably) the hinted at rust-version-policy
field, which would be a good piece of policy documentation that could be surfaced on crates.io alongside the minimum version, since it tells the reader both what the current MSRV is and what to expect on the next release of a crate.
For the other meanings, I would argue that the minimum version is still the definition we should use for rust-version
, and that this RFC shouldn’t change that.
rust-version = "auto"
isn’t an expression of a minimum version policy
OK, so if you accept the previous section’s contention that rust-version
should continue to only express the minimum supported Rust version1, then something else falls out: auto
isn’t a valid expression of a minimum version.
The persona that develops a quick and dirty crate, starting with cargo new
, and throws it over the wall at crates.io probably isn’t thinking about minimum supported Rust versions. If cargo new
adds rust-version = "auto"
2, some users may take that as a hint to figure out their MSRV, but I suspect many would either miss it, or they may see auto
as something that’s probably super safe and not worth investigating further.3
I would like people to think about their MSRV. I’d support changing the template used by cargo new
to include a commented out rust-version
field along with a link to best practice documentation.4
But the problem I have with rust-version = "auto"
is how its behaviour shifts based on its environment, and environments are implicit, not explicit. With auto
, and assuming I run rustup update
reasonably often, if I publish my-shiny-new-crate 0.1.0
today, I’m saying that the MSRV is 1.76.0[^rustup]. If I fix a bug in six months and publish my-shiny-new-crate 0.1.1
, now my MSRV is going to be something like 1.80.05, even if I changed one inconsequential line.
If auto
doesn’t exist, one of two things happens:
- I may have taken the hint from the template, or read some documentation, and declared
rust-version = "1.76.0"
when I published the first version, This is probably still true, and it’s right in front of me in the manifest to change if it’s not. (And, if I’m wrong, it’s a really straightforward bug report from a user down the line, and an obvious fix for me as the maintainer: I can either make it compatible, or bumprust-version
and republish.) - If I didn’t specify
rust-version
at all, then that’s definitely still true. Caveat emptor for my users. And that’s OK: as a crate consumer, I’d much rather know that the maintainer hasn’t thought about MSRV at all (through the omission ofrust-version
) than get a fake value that doesn’t really express their opinion.
In practice, I also think that the motivation for auto
(which, as I perceive it, is basically a lack of rust-version
usage thus far) will eventually correct itself.
Going back to PHP, composer init
6 doesn’t include a PHP requirement at all in its default output, and there’s no equivalent to rust-version = "auto"
in Composer. Yet ~78%7 of PHP packages on Packagist do include minimum PHP requirements, expressed as a semver constraint, likely because it’s prominently documented on the first real page of Composer documentation. The situation is different in some ways — Composer always (to my recollection) supported PHP requirements, so users learned to write Composer manifests with PHP requirements from early on — but I think over time crate authors would do the same thing. We just need to bring the documentation to the users, provide tooling to help, and be patient.
rust-version = "auto"
can cause security problems for crate consumers
To shorten this section, let’s just assume I agree with basically everything @jonhoo said.
But, at the risk of repeating others, the specific scenario — related to the above — that worries me is this:
- Maintainer Alex publishes
buffer-thing 1.0.0
withrust-version = "auto"
. They use Rust 1.76, so that’s what the crate MSRV is set to. - Consumer Bobbie adds
buffer-thing = "1.0.0"
as a dependency in their project that uses Rust 1.77. No problem. - Alex fixes a security bug and publishes
buffer-thing = "1.0.1"
, but publishes from an environment (whether their workstation or CI) that uses Rust 1.78.cargo publish
sets the MSRV to 1.78. - Bobbie hasn’t updated their Rust version for whatever reason, rebuilds their project on Rust 1.77, and still gets the vulnerable version even if they run
cargo update
.
Yes, Bobbie will get a warning here from cargo update
, and that’s good! But now they’re in a difficult position:
- Bobbie could use
--ignore-rust-version
, but they don’t have any way of knowing ifbuffer-thing 1.0.1
actually works on Rust 1.77, and they have what seems like an authoritative statement from Alex on crates.io that it actually requires Rust 1.78.8 They’re probably not going to do this. - Bobbie could upgrade their local toolchain, but they may not be able to — it may be a certified fork like Ferrocene, or something else might not work with a newer version, or they just might need to conform with Big Company policies that they don’t necessarily even agree with but have no control over.9
- If there are more changes than just the security fix, Bobbie could try to fork the last version that supported Rust 1.77, cherry pick the security fix in, and vendor that. That is both time-consuming and error-prone.
Again, all of this goes away if rust-version = "auto"
doesn’t exist. Alex either published buffer-thing 1.0.0
with an explicit rust-version
, which probably only gets incremented when it actually needs to be or per a clear policy that Bobbie hopefully already knew about, or they published it without a rust-version
at all, in which case Bobbie gets to try it and can then decide to either report or fix a build failure. In some respects, this is essentially the same decision tree as above, but without the uncertainty of having a piece of seemingly authoritative information that purports to state Alex’s intentions around the minimum version. Whatever they choose, their path is clearer.
OK, that was a lot of words. I’m sorry. Really.
To put this into something actionable: my suggestion is that rust-version = "auto"
should be dropped from the RFC. I also agree with @mitsuhiko that it probably makes sense to slow roll these changes more generally: it makes sense to me to ship the v3 resolver as soon as it’s ready, but I think it’s premature to commit to making it the default in the next edition without some real world experience reports, particularly around whether the default resolver.precedence
is appropriate. (Could we ship it and require resolver.precedence
initially, and ask for feedback?)
-
I mean, I don’t actually expect many (most?) people reading this to agree with everything I just said. I’m just trying to follow it through to what I see as the logical conclusions of that train of thought. ↩
-
It’s unclear to me whether
cargo new
settingrust-version = "auto"
is actually being proposed or not. The guide level explanation says that it is, but some recent comments suggest that it’s not. If it’s not… well, I’d still argue against it in a future RFC, but you can probably ignore most of this section. ↩ -
I don’t really want to bikeshed the name, since I disagree with the whole concept, but
auto
specifically could imply “Cargo will figure out what the minimum version actually is”, which is even more misleading than what it actually does. If the concept did stay, I agree with the unresolved question around naming, and would probably vote fortoolchain
orcurrent
of the listed alternatives. ↩ -
Some of which already exists, in fact, in the
rust-version
documentation. ↩ -
The specific version number is a rough guess. I didn’t actually look at a calendar, so if I’m off by one I apologise. ↩
-
Roughly equivalent to
cargo init
, but with more interactive questions, likepoetry new
from the Python world. ↩ -
Analysis performed on the full Packagist index as of today, Feburary 22nd, 2024. ↩
-
I studied film at university, and one of the things that you spend a lot of time on in your documentary units is the concept of “verisimilitude”: how real or truthful something appears. The moment we have
cargo publish
start inserting version numbers that get displayed on crates.io, users are — rightly — going to assume that they’re truthful, because crates.io is the authoritative source of crate information. ↩ -
Ask me about my stories from New Relic. ↩