Minimum Supported Rust Version (MSRV) Policies · rust-lang/api-guidelines · Discussion #231 (original) (raw)

cc @matklad

#227 suggested the following policy with respect to MSRVs:


A crate should clearly document its Minimal Supported Rust Version:

Compliance with a crate’s stated MSRV should be tested in CI.

The API guidelines tentatively suggest that, for libraries, an MSRV increase should
not be considered a semver-breaking change. Specifically, when increasing
MSRV:

This reduces the amount of ecosystem-wide work for MSRV upgrades and prevents incompatibilities. It also is a de-facto practice for many cornerstone crates. This policy gives more power to library consumers to manually select working combinations of library and compiler versions, at the cost of breaking cargo update workflow for older compilers.

However, do not increase MSRV without a good reason, and, if possible, batch MSRV increases with semver-breaking changes.

Nonetheless, some crates intentionally choose to treat MSRV increases as a semver breaking change. This is also a valid strategy, but it is not recommended as the default choice.


We aren't quite ready to adopt a guideline around MSRVs so this discussion is here to continue what was started in the original PR.

You must be logged in to vote

+ a practical advice


To reliably test MSRV on CI, use a dedicated `Cargo.lock` file with dependencies pinned to minimal versions:

$ cp ci/Cargo.lock.min ./Cargo.lock $ cargo +$MSRV build


You must be logged in to vote

0 replies

You must be logged in to vote

3 replies

@BurntSushi

I agree, and I think we need to put this particular point to bed and establish a strong ecosystem recommendation based on both theory and practice.

@joshtriplett @m-ou-se Do you have any thought about how we might go about this? This particular repo is not particularly active, and MSRV is not really part of the "API" of a crate. Yet, this feels like something we might want to weigh in on.

@joshtriplett

Summarizing one novel bit of the rationale from those links:

If crates bump semver major versions with an MSRV change, then that counterproductively makes it harder for other parts of the ecosystem to support older Rust versions, because other crates will have to move to the new major version in order to get updates, which prevents them from running with older versions. At least in theory, maintaining semver compatibility allows a crate to depend on only the version it needs, and still get fixes in new versions.

Crates can and do, of course, declare dependencies on newer versions, perhaps because they're using newer APIs or relying on specific fixes. But in general, semver seems like the wrong tool for handling Rust versions.

@matklad

Do you have any thought about how we might go about this?

I think submitting PR against this repo is the right outcome:

I am not sure what's the best way to get sufficiently visible consensus here, but I think:

would get us sufficiently covered.

Another thought: a common objection to "MSRV is not breaking" policy is a somewhat literal interpretation of sevmer: this change breaks my build, hence its a breaking change, hence it's a major change.

There's a similarly literal counter-argument (I don't think it's a good argument, but it might be persuasive because it uses the same reasoning machinery (literal interpretation)):

When Rust releases a new version, this is a new minor version: 1.60 -> 1.61 -> 1.62. And a library depends on Rust. Upgrading a minor version of dependency is not considered a breaking change.

Indeed, when library A upgrades it dependency on library B from 1.x to 1.x+1, library A can no longer be build with B 1.x.. Its on the consumer of A to ensure they use at least B 1.x+1. This is exactly the same situation if it is Rust language instead of "library B". The difference is in the tooling support -- Cargo would automatically upgrade B, Cargo doesn't automatically download the new version of Rust.

You must be logged in to vote

6 replies

@epage

Another thought: a common objection to "MSRV is not breaking" policy is a somewhat literal interpretation of sevmer: this change breaks my build, hence its a breaking change, hence it's a major change.

Also, if we took this stance, we would run into this in elsewhere like rustc requiring new Android NDK or glibc and Linux kernals.

@est31

I don't like the counter argument, because those minor version upgrades might not be as smooth. There is a long list of legitimate reasons to stay on older compilers, including that you can experience breakage because you e.g. use some nightly feature or there is a regression that upstream is not fixing, or other reasons. The whole point of a semver policy is not to exist as a goal in itself but so that people don't experience breakage, so I'm not sure why this is a particularly literal interpretation: it's just what semver is about.

On the other hand, this argument gets weaker the less people experience breakage. The further you go back in time, the less people will be disrupted by you increasing your MSRV. That's why IMO it should rather be that increasing your MSRV to a release from 2 weeks ago is a semver breaking change, while to a Rust release from 2 years ago is not. That's I think, in general, why sliding window approaches are a great idea in theory (but not neccessarily in practice, for other reasons).

And even further, if you had a way to have cargo create lockfiles with old versions of dependencies, then yes, IMO MSRV upgrades should not constitute semver breaking changes. However, you should ideally still not immediately jump on new language features shortly after a release because of the negative impact of putting a large fraction of your users onto outdated versions of your library: maybe there is a bugfix or something.

@BurntSushi

(I do definitely agree that N-2 for a crate like time "feels" too aggressive. But I don't know ehat the right answer is. I try to target a year for regex.)

@epage

imo when discussing policies on MSRV policies, we should talk about users, rather than time frames.

Examples

I also wonder if an MSRV-aware resolver will make people more open to shorter MSRV policies.

@est31

Agreed @epage , I think the user point of view is the most important one.

I also wonder if an MSRV-aware resolver will make people more open to shorter MSRV policies.

More open, yes, but I still don't think it's good as a general rule.

A MSRV aware resolver is a good tool to help you get older releases of a small percentage of your crate graph, and certainly users won't be as frustrated by single crates having a short MSRV policy any more. But if those users have bugs that are fixed on the latest release, they still won't be happy. If a majority of your crates is outdated because of your MSRV choices, then you will not like it.

For example, recently I found a bug a few weeks after I upgraded one of my dependencies. I looked at the source code on docs.rs of the latest version and I was confused how the bug could happen. Turns out I had an outdated version and there had been a patch release in the meantime with a bugfix and a cargo update fixed the bug.

Now, that project of mine targets latest stable but if I have had a strict MSRV policy, N-2 releases, and the crate decided to increase their MSRV to N-1 in the patch release prior to that, maybe as parts of some unrelated refactor, then I wouldn't have been able to get the bugfix.

I think this concern is usually used to dismiss MSRV-aware resolvers entirely. It's certainly a valid concern, but I don't agree with the conclusion.

To me, "Cargo doesn't automatically download the new version of Rust." is precisely why it is a breaking change -- whether we would like it to be or not.

You must be logged in to vote

1 reply

@Enet4

Even the official Rust documents have a soft interpretation of what constitutes a breaking change (with the distinction between major and minor breaking changes; the Rust compiler may allow the latter in new minor versions). Trying to avoid every single known case where changing something will prevent one hypothetical Rust project in the world from compiling as-is is impractical.

Updating the Rust compiler is mostly frictionless, and seldom results in further compilation errors or incompatible binaries. As such, it is the weakest link in the list of compatibility concerns.

Nonetheless, some crates intentionally choose to treat MSRV increases as a semver breaking change. This is also a valid strategy, but it is not recommended as the default choice.

I think this needs much stronger wording. Doing this creates horrible incompatibilities similar to Python 2 vs 3 fiasco.

Example:

Whenever B or C decides to upgrade A it has to also issue breaking release. Further D can't upgrade just B or C alone because the traits don't match and the programmer gets super-confusing type error which he then has to solve by making a wrapper type and a bunch of boilerplate.

And literally the only thing this solves is not having to track the version in Cargo toml. 🤦‍♂️
Of course there's one problem: --locked is not default when using cargo install. But still not worth wasting ton of time here.

The only case where this could work is in crates that contain functions only - no type or trait definitions. These are quite uncommon.

You must be logged in to vote

2 replies

@BurntSushi

Yeah this is the public dependency problem. It's not the only downside of "msrv bump is breaking" strategy. For example, if MSRV were a breaking change, then the regex crate would probably be on version 5.x.y by now. Since regex isn't commonly used as a public dependency, you can have multiple semver incompatible versions in a single dependency tree. Everything builds fine and so it isn't a critical issue all on its own. But I promise you some projects would be compiling at least 3 different versions of the regex crate. That alone is going to have a huge impact on compilation times. Now think about the fact that if everyone treated MSRV bumps as breaking changes, then the compilation time problem could easily become critical.

Of course, it isn't guaranteed that's how things will play out. Perhaps folks would be extremely conservative about bumping MSRV if it were a breaking change to do so. In which case you have one of two alternatives: a maintenance nightmare with a monstrous build script and conditional compilation everywhere (like libc) or you just never use new Rust features and stagnate instead.

@Kixunil

Oh, good point! So not even function-only crates should do this. There are also people who deny duplicates in their CI (and I co-maintain one such project).

could easily become critical.

TBH, having duplicates pisses me off already. :) If this happened, I would give up on crates and NIH everything.

I see an issue with MSRV bumps in patch releases. IMHO it voids the concept of having a well-defined MSRV for library crates at all, as soon as there are dependency chains:

Imagine you have 3 library crates in a chain: A depends on B, B depends on C.
B and C allow for MSRV bumps in patch versions.
Now, if C releases a new patch version with a MSRV bump, the MSRV of B would change implicitly without a version change at all.
Therefore, it's impossible to specify a well defined MSRV for A, and A can't even work around that by specifying a dependency on an exact version of B. The only workaround would be adding a direct dependency on a specific version of C.

In effect, that would mean that a library that wants to have a well-defined MSRV would need to add direct, exact dependencies on every single transitive dependency in its dependency tree. That's surely not a scalable solution.

You must be logged in to vote

6 replies

@jannic

Yes. I should have written "non-major version" or similar. The important thing is that it's a newer version that would be used automatically by cargo.

@BurntSushi

Requiring major version bumps for increases to MSRV doesn't scale, and it's not something that any other software project that I'm aware of practices either. This has already been discussed above: #231 (reply in thread)

There's also an implicit assumption in your commentary that that "MSRV of A" depends on the MSRVs of its dependencies. But that's not actually totally clear to me. Users of A are welcome to use a Cargo.lock to set the versions of their dependencies in a way that conforms to their requirements.

I tried the "MSRV bump requires major bump" philosophy many moons ago. I almost immediately abandoned it because it was unworkable in practice. I don't ever see us (the libs-api team) ever recommending that an MSRV bump require a major semver bump.

@jannic

I don't want to require a major version bump either. It would only work if most of the ecosystem used the same policy, avoiding MSRV bumps unless they do a major version anyway, and that would mean that new rust features would not be used for a long time. I don't think anybody would like that.

But the "use a Cargo.lock" strategy isn't convincing either. It works fine for existing binary crates. But it's difficult to construct a Cargo.lock manually if you need to start a new binary with an older rust version:
Imagine you start a new project, that needs to compile on some LTS distribution with a given version of rustc. You check that your direct dependencies have a fitting MSRV, perhaps even actively choosing older versions of those dependencies. But some transitive dependencies happen to have bumped their MSRVs in the mean time. How do you make sure that for those, older versions are also used? And then, if MSRV bumps can even happen for patch versions, how do you make sure that you are not missing security updates?

In short: I think this makes MSRVs nearly useless in practice. What useful guarantees do you get from a library crate specifying a MSRV, if it depends on other crates that may require a newer rustc version?

All you get (and that's of course great!) are better error messages, like error: package rustix v0.38.2 cannot be built because it requires rustc 1.63 or newer, while the currently active rustc version is 1.62.1 instead of some random compiler errors.

@ChrisDenton

I think this is more about Cargo's support for MSRV (or rather the lack thereof). Crates alone can't really solve that problem.

@BurntSushi

But it's difficult to construct a Cargo.lock manually if you need to start a new binary with an older rust version

Yes, I know it's tedious and difficult. But it's possible. There's a path forward there for folks who insist on running on older Rust toolchains.

And as @ChrisDenton said, that process can be automated to some degree. But even if you do that, there's the question of support for those older releases. I think that's going to fall to the shoulders of folks who want crates that work with older toolchains. Which really isn't that much different from today.