Skip to content

Add guidance about bumping the minimum required `rustc` version #123

danburkert started this conversation in API Guidelines
Add guidance about bumping the minimum required `rustc` version #123
Sep 9, 2017 · 51 comments

As far as I know, this repository does not offer guidelines on when it is appropriate to bump a library crate's minimum required rustc version. I've found that authors of widely used and foundational crates do not consider bumping the minimum rustc version to be a SemVer incompatible or breaking change (1, 2). I think it would be appropriate to document what norms the Rust community expects on this issue, so that deviations from the norm can be fixed without having to re-litigate the meaning of SemVer.

My personal opinion on the matter is that bumping the required rustc version of a library is a backwards-incompatible SemVer change, since it requires applications depending on the library to follow suit. This change can be every bit as painful for application authors as introducing a breaking API change (or more). The minimum rustc version is 'viral', and a breaking change by a foundational crates forces all other downstream crates to in turn bump their minimum version. Cargo has good tools for introducing new features which may require a newer rustc version, while it has relatively poor support for resolving rustc version dependencies if the author of the library does not follow semantic versioning. Thus, unless Cargo's version resolution begins taking into account required rustc versions, it's better for library authors to put new code requiring new rustc versions behind a feature flag.

Replies

I think it's fair and simple to assume any change to the minimum rustc version is a breaking change.

What's less clear is when that happens. If you happen to have a minimum version of 1.8.0 for your library then bundle in a few ?s with other internal refactorings then you've just bumped your minimum version, possibly without even realising it.

So I think a good recommendation is to pin a rustc version in CI, serde tracks 1.13.0, when you want to stabilise your library and any changes that break your build on that pinned version need an appropriately breaking version bump, or gymnastics with flags.

0 replies

I am less of a hardliner on this, although I hear many people think I am wrong.

Serde tracks a Rust version in CI so that we don't accidentally raise the minimum, not because we never will.

Users should not have an expectation that they can selectively pin parts of their dependency graph while receiving updates to things that depend on those parts. In Serde's case, the serde and serde_derive crates are versioned in lockstep. Serde 1.0.5 and serde_derive 1.0.5 work together, and serde 1.0.10 and serde_derive 1.0.10 work together. There shouldn't be an expectation that you can pin serde to 1.0.5 and get to use serde_derive 1.0.10. Does that constitute a "breaking change" to serde_derive? Sure, technically.

Dependencies on the standard library or language should work analogously.

0 replies

I'm not sure I agree that this needs to be treated as a breaking change. On one hand, you have the cost of forcing people to upgrade their rustc when cargo updateing. On the other, you have the cost of forcing the ecosystem to propagate new major versions throughout it.

Judging by the recent Rust community survey survey responses, ~95% of respondents were targeting 2 stable releases back or newer.

I am looking forward to picking up the bitflags crate's new use of associated consts. The various flags are way more discoverable attached to the relevant type rather than floating around the containing module. I can perform that upgrade in an API-compatible way with deprecated reexports, but if the rustc dependency bump is going to count as a breaking change, I'm just not going to do it for a year or so, probably. The pain of migrating the ecosystem forward across that breaking change is just too painful. rust-openssl 0.9 was released last November and people are still migrating off of the older releases. There are 1-2 issues opened per week against the rust-openssl repo that boil down to "upgrade to 0.9".

0 replies

Usually I would expect bumping the minimum rustc version should not happen in bugfix release, e.g. 1.0.0 to 1.0.1, but certainly permissible when we have 1.0.0 to 1.1.0 as long as it is advertised as such.

0 replies

@BurntSushi
BurntSushi Nov 7, 2017
Collaborator

I have some thoughts on this, but I think that at a minimum, we should be strongly recommending that folks include a specific version of Rust in their CI builds. This doesn't imply any particular semver policy, but does at least make bumping the minimum Rust version a conscious change. As someone who has tried to play the conservative side of this fence (semver bump for minimum rustc bump), I will say that having a pinned Rust version in a CI config makes the research task of determining the release policy of your dependencies much easier, and typically instantly clarifies the choices I actually have available to me.

I've been trying to straddle this line for a long time now myself, and it is very hard to do so. The hardest part about this problem is that there are significant reasons for choosing either side of this, and I think those reasons have already been highlighted in this thread. Another difficult aspect of this problem is that since there is no consensus on whether a minimum rustc version bump is actually a semver breaking change or not, you end up with an ecosystem where one needs to be aware of the release policy for each dependency of your code, because that policy is not necessarily communicated through version numbers. Even worse, this doesn't just apply to direct dependencies, but to all transitive dependencies too.

I'd like to collect a few of my experiences with this topic.

  • There was actually a proposed RFC on this topic which I think basically says that rust-lang crates should support the previous two stable releases, but otherwise, there's no concrete policy. The RFC contains a discussion with a variety of opinions: rust-lang/rfcs#1619
  • I asked about the release policy of clap here: clap-rs/clap#740 --- @kbknapp was incredibly receptive and opening this issue led to a policy where a minimum rustc bump was considered a minor breaking change, so that dependents could use the ~x.y syntax in their Cargo.toml and be guaranteed that a patch release will never bump the minimum Rust version.
  • Unfortunately, the new release policy of clap led to @joshtriplett opening this bug against ripgrep: BurntSushi/ripgrep#271 --- Basically, the ~x.y syntax prevents ripgrep from compiling with newer versions of clap, even if it could. The interesting point brought up in this thread by @joshtriplett is the questioning of whether ripgrep itself should require a minimum Rust version. The thinking here is that the ability to compiler ripgrep master on a distro's (potentially) ancient version of Rust isn't particular valuable in and of itself, but only if that distro enables packaging Rust applications easily. In that sense, folks should be able to just install ripgrep from their distro and not need to compile it.
  • @alexcrichton also notes issues with ~ dependencies: https://github.com/kbknapp/clap-rs#warning-about--dependencies
  • I did eventually change the clap dependency to remove the ~ dependency: https://github.com/BurntSushi/ripgrep/blob/256aeb55468d57a179447c8b2cd64ada998987c5/Cargo.toml#L35 --- As a result, I almost never run cargo update, but rather, cargo update -p {crate}.
  • Maintaining a minimum Rust version in the face of dependencies without that same policy is a maintenance burden: clap-rs/clap#967 --- and that in turn sometimes has unintended consequences: clap-rs/clap#1003
  • A more circumspect interaction with the encoding_rs crate, where the outcome was this: https://github.com/hsivonen/encoding_rs#rust-version-compatibility --- Even getting specific pinned versions in the CI config was a huge win, but the policy here is, in short, "probably like clap's, but no promises."
  • Regardless of what ideals we all want to believe, users will try and use their distro's version of Rust to compile Rust applications, and when it fails, they will get frustrated: BurntSushi/ripgrep#302

I think that's it as far as my own experiences go. It has been a struggle, but sometimes I wonder whether it is a natural struggle or not. This is why I think making the release policy of a crate discoverable is a first good concrete step we can take that will improve the situation for everyone, and I think it should be uncontroversial. (I think a minimal release policy is putting a pinned Rust version in a CI config.)

My own personal views on this have historically been a little hardline in the sense that I thought it was a semver breaking change. But I've grown increasingly soft on that particular issue. The end result is that I am intensely ambivalent. Therefore, I wind up on the conservative side of things because I'm not sure what to do. That is, all of my crates are pinned to a specific Rust version and I only ever bump the minimum Rust version with a semver version bump.

My conservative stance on this is what has been preventing me from pulling the trigger to release regex 1.0, for example. I really want to use SIMD optimizations in regex, but if I release regex 1.0 now, then under my conservative policy, I wouldn't be able to use SIMD unless I either released regex 2.0 or put the support behind a feature flag that is disabled by default on older Rust versions but enabled otherwise. (At least, I think I could do that.) Either choice is a huge pain and not ideal at all. I suppose we shouldn't necessarily be afraid of making many semver version bumps ("so what if I release regex 2.0"), but every time I do that, some portion of the crate ecosystem is going to build both regex 1 and regex 2 into their crates, which aside from being just plain weird is going to hurt compile times quite a bit!

I have been considering what I think of as the clap compromise for regex though, because I also think it's weird to be holding back from regex 1.0 because of this. In particular:

  1. regex 1.x promises API compatibility.
  2. regex 1.x.y promises Rust compiler compatibility.

Invariably, this results in the same suggestion that clap makes: if you care about Rust compiler compatibility, then use ~1.x in your Cargo.toml, which in turn creates other problems I've highlighted elsewhere. I think my own personal take is that this is a decent compromise, and that I'd prefer the problems associated with the clap compromise over the problems associated with "be conservative" and the problems associated with "move fast and break things." I guess the key problem here is that if too many folks take me up on the Rust compiler compatibility offer, then they'll use ~1.x in their Cargo.toml, which will in turn cause bad things to happen with respect to @alexcrichton's comments mentioned above.

0 replies

I still wish that a crate's minimum rustc could be expressed to cargo, as well as minimum std and cargo if we don't conflate all of these. Then cargo could just use that for dep-solving, pruning out versions from the registry that are too new for the toolchain in use. That still requires crates to be diligent about declaring their requirement, hopefully with CI to prove it, but it would be better than the status quo.

There have been a few RFCs started along these lines, and I admit I just sat back on the assumption that things were headed on the right track. Can anyone summarize what's happened with that?

0 replies

@BurntSushi For things like SIMD optimizations that don't affect the public API, there is a third option. You can check the rustc version in a build script and set appropriate CFGs to enable or disable the SIMD codepaths automatically. This does mean that you now have two implementations of a bunch of stuff, but that may already be the case if you're specifically targeting AVX or whatever.

0 replies

@cuviper Rough summary: any kind of version specification depends on new features in Cargo and Cargo.toml, so these efforts blocked on things like Cargo unstable features and versioning of the Cargo schema itself. I think rust-lang/rfcs#2182 is the next step there, and once we have that we can introduce a means of handling rustc versions.

0 replies

For things like SIMD optimizations that don't affect the public API, there is a third option. You can check the rustc version in a build script and set appropriate CFGs to enable or disable the SIMD codepaths automatically. This does mean that you now have two implementations of a bunch of stuff, but that may already be the case if you're specifically targeting AVX or whatever.

Yeah that's what I meant by adding a feature. It's definitely a pain but yeah that might be what I end up doing.

0 replies

It does seem like a proper solution to this isn't possible without help from cargo, but it also seems like we can all agree that bumping the minimum rustc version should at least be a minor version bump, and I doubt that's going to change even when cargo has a great, stable solution for this. So for the purposes of the API guidelines, it seems safe to recommend that everyone:

  • explicitly test a specific rustc version in their CI (on top of stable/beta/nightly)
  • make that version easy to find in your documentation
  • never bump the minimum rustc version in a patch release

Anything beyond that is probably overkill for a typical crate, but this is enough to make upgrading to a more elaborate policy easy if your crate ever becomes atypically popular, and it's enough to avoid putting an undue research burden on anyone that does need to support specific rustc versions.


Actually, I just noticed the current guidelines don't talk about CI or versioning at all. Are those considered out of scope?

0 replies

I would personally like it if more crates were willing to release >1.0. In my opinion many crates are afraid to release 1.0 for fear of never being able to go beyond. Much of what is being discussed here could be alleviated if crates were more willing to bump the major.

I believe a major hesitation to many popular crates releasing a 1.0 is the fear of "getting something wrong" or not being able to take advantage of rustc advances by not being able to go beyond 1.0 as similarly described by @BurntSushi

I'm hoping as the "core" Rust crates start going beyond 1.0, it will become more acceptable, but don't see it happening without the "core" or very popular libraries doing it first as they generally set the tone and guidelines. Personally, I also think the, "Never a Rust 2.0" is partially contributing to this phenomenon.

This is by no means a call for crates to quickly release a 1.0 followed immediately by a 2.0. But I do wish more crates were willing to bump the major as issues like what's being discussed evolve naturally. I believe if we had more willingness to bump the major, this would probably a be a non-issue.

I'm also just as guilty, as even though clap went 2.0 quite quickly, I've been extremely hesitant to go 3.0 for fear of getting "too far ahead of the norm, and therefore something is wrong" which is admittedly a little silly. As mentioned above, I don't think the ~x.y should be the defacto solution, and perhaps create more problems than they solve. As @BurntSushi said, there are very valid reasons for both sides of the argument.

For now though, I see the above three bullet points mentioned by @Ixrec as being a better policy than none at all.

0 replies

A bump of the required rust version should be treated as a a breaking change of your library in the sense of semver. It breaks the cargo update workflow for anyone who is not always on latest Rust stable, and this is not good.

Is it enough if you bump the minor version? No! That means that anybody who has caret requirements anywhere inside their dependency chain might get their cargo update workflow broken, and caret requirements are definitely too popular inside the rust community.

If one day we might be getting a "minimum rust version required" field in Cargo.toml, and cargo update gets changed to not update a dependency if the newer version has a too new required rust version, I think that requirement can be loosened.

0 replies

@est31 I strongly disagree with any statements that a bump of required Rust version must always be released as a breaking change. Whatever advice we end up giving here will be primarily a discussion around evaluating tradeoffs, not a hard line.

If I have a library that miraculously compiles on Rust 1.3.0 and is used by thousands of downstream crates and I discover a performance problem that would be fixed by using Box::from_raw, my options are:

  1. Live with the poor performance for the rest of forever,
  2. Bump the minimum Rust version to 1.4.0 in a breaking change, forcing months of ongoing breakage as a thousand libraries upgrade,
  3. Write unsafe code to do it myself (a future version of Rust would be free to break my unsafe implementation and cause memory unsafety),
  4. Freely use Box::from_raw in a minor version. Zero people in the world still care about 1.3.0.

It is pretty clear to me which of these I would do and lose no sleep over.

0 replies

@dtolnay This is an argument of the form "if I remove this public function nothing bad will happen because who really uses it and doing a breaking change would require people to update".

I really hate how staying up to date on the current crates.io ecosystem is unusable if you have installed your rustc copy from your distro or are pinning your rust compiler for some other reason. I don't say that people should not rely on Rust 1.4.0 but at least don't break existing projects.

If you are on an older compiler you aren't even getting compilation errors like "sorry, but your Rust compiler is too old", the errors are more often of the form "function not found" or "this feature is unstable".

0 replies

And libc is literally inside the top two of most directly depended upon crates, so it is an exceptional example.

0 replies

Still in Serde 1.0.x, we add a build.rs script which uses version_check to detect when we are being built by rustc >= 1.29. If so, print the magic line that turns on the optional dependency on 2.0.

This shouldn't work as far as I know because whether a given dependency is enabled or disabled is resolved before build scripts are run.

0 replies

Slight modification that no longer requires rust-lang/cargo#5499: we could have 1.x unconditionally depend on 2.0 and add a build script to 2.0 as well, which sets a feature on old compilers that turns the entire 2.0 crate into an empty crate.

0 replies

I don't have strong feelings about the semver question here, but as someone maintaining a decent-sized Rust application I frequently run into situations where I update a dependency or add a new dependency and need to bump the minimum Rust version I support (which I test against in CI), but I find it virtually impossible to figure out what the actual required version is! It's easy to see that a PR breaks compiling against the pinned Rust version in CI, and often the error message will even name a specific feature that's unstable in that version, but there's no good way (AFAIK) to figure out what version stabilized that feature. (In other cases the error is not as clear and it's even harder.)

0 replies

@luser Yeah, I run into the same problem. This is why I've been advocating that everyone put a specific Rust version in their CI config so that others can go and look at it and know what version the project intends to be able to compile on. I think that is an uncontroversial and unambiguously good step forward, and in particular, does not require everyone to agree on the semver ramifications---folks are free to bump the minimum Rust version as much as they want, they just have to do so explicitly.

I think more and more projects are doing this, but unfortunately, many still aren't. At that point, you're right, it's a research task to figure out the minimum Rust version. There's probably a script someone could write that does a binary search for you, but this requires having all intermediate versions of Rust installed. (rustup makes this easy, though, may take up a bit of disk space!)

0 replies

(See also japaric/trust#103 about running CI with a rust-toolchain file present)

0 replies

See rust-lang/rfcs#2495 for a proposal which will allow authors to specify minimum supported Rust version (MSRV), which in combination with additional constraints in dependency versions resolution algorithm resolution algorithm will make changing MSRV backwards compatible change.

0 replies

@luser same here, although I must say that while this is pretty annoying when it happens, it doesn't happen often enough to me to be a continuous annoyance. As in, when it happens, I bisect the version required, and don't have to worry about it again for a couple of months.

0 replies

An interesting idea from @Eh2406 for a CI setup that should work today.

To test MSRV, one can use

cargo +nightly -Z minimal-versions generate-lockfile && cargo +MSRV test

Caveats:

  • your crate probably won't build with minimal-versions right off the bat, see 1 and 2 for the list of problems you may face.

  • -Z minimal-versions itself is unstable.

0 replies

I'm using latest stable as MSRV for the crates I maintain. Hopefully better tools will appear to handle this gracefully for both crate maintainers and old compiler users.

0 replies

TL;DR: Strong opinion; have better error message; not a breaking change


Like others said, the main problem is that you don't get a nice error message about it. This would remove all the pain for my personal use of Rust.

I don't think this should be considered as a change:

rustc is not gcc, we all? use rustup that allow to install and update rust on every common OS. People don't need to wait 1 year a .dep from debian stable to update rustc. I agree some project could have some trouble updating rust, but this should be settled by freezing the specific version of each dependency. Let's imagine ~5% of projects and I think it's much less, would have trouble to keep rustc up-to-date. That mean ~95% of users will "pay" for these ~5%, I don't think it's worth it when there is already an easy solution for these ~5%. (Also, I doubt these ~5% want to have a lot of dependency)

Semver is about API, it's doesn't have a way to express compiler version. rustc version is a meta dependency, this is not possible to include it in semver. "edition" in cargo should be enough, but in my opinion, it's clearly a way to hide a Rust 2.0. I mean, maybe I'm totally wrong, but between 2015 and 2018 edition, there is obvious breaking change. Ok, have edition make this smooth but if you try, for example, the diesel get started guide, you will have a cargo.toml with edition = 2018 but the guide didn't expect that, some code in the guide will not compile because diesel example use self in use statement. Also, you are forced to use "macro_use" even if you want to use 2018 edition because you can't import table macro.

2018 edition despite all these changes, I never saw someone complaining about update their code. I always saw, "wow nice this is so much better", I think Rust is too afraid of being seen as an unstable language because of pre-1.0. But Rust dev are in the majority not afraid of little change, on the contrary the community always asks to improve Rust. I agree Rust must not change like pre-1.0 but there is a big difference between change everything every month and change little thing sometime. Anyway, we already have this reputation, people often stick to their first impression.

I already see that some big crate live in the past (I don't need to name them and a lot of them use https://github.com/dtolnay/semver-trick) and I hope there will not limit themselves just for few use cases. Of course, I don't encourage to always use the last release, but I think using 3-6 months release is perfectly acceptable, for me these are 3 versions of rustc, stable, beta (who use beta :p) and nightly. And we should have a nice error message that just ask to update rustc.

0 replies

Here's a hacky PoC cargo-subcommand to generate a "quick" list of the MSRV by crate version for a particular crate. The idea being maintainers could list this data in their README and allow downstream consumers to make a more informed decision about upgrades and such.

https://github.com/kbknapp/cargo-msrv-table

0 replies

@kbknapp That's really neat! It'd be nice if there was a way for a crate to document what its MSRV is for each version in such a way that the tool could pick it up. That way, for "cooperating" crates, you wouldn't have to do the full search.

0 replies

Crates could specify it in Cargo.toml as package metadata? cargo metadata gives manifest paths to dependencies, so tool only needs to read manifests. Would Just Work among varying dependency versions, and where not specified the tool could just try compiling it based on your selected MSRV?

0 replies

@kbknapp nice tool! Some suggestions:

  • Usage of bisection instead of linear search
  • Maybe incorporating the versions found out for earlier crate releases? Usually it should be close, so maybe test the version itself as well as the version before and behind before starting bisection.
  • Right now cargo doesn't respect MSRV during resolution, so your results will show too recent MSRVs. One can patch cargo's resolver to treat cargo releases with an MSRV prior to a certain version as yanked, as long as you have MSRV info for those crates.
  • I recommend building a database instead of letting crate maintainers do it. This way, the new feature requires no buy in from the maintainers who are often even hostile to the MSRV concept (yet their code is still being relied upon). That database can be filled by automated tools running in the cloud or something.
0 replies

@est31 great suggestions! The tool was meant to be more a proof of concept than a full blown project. If someone wants to foster that command into a real solution I'd be happy to help along where I can and also grant full access.

I agree some sort of database would be more usable, even if it was some sort of combination of local/"cooperative" crates instead of a full search. However, that also adds a lot of complexity, and potentially cost to such project.

0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
💡
API Guidelines
Labels
None yet
Converted from issue