[Pre-Pre-RFC] Reviving minimum Rust version (published as RFC 2495)

UPD: This proposal is published as RFC 2495.

Summary

Add rust field to package section which will be used to specify crate’s Minimum Supported Rust Version (MSRV):

[package]
name = "foo"
version = "0.1.0"
rust = "1.30"

Motivation

Currently crates have no way to formally specify MSRV. As a result users can’t be sure that crate can be built on their toolchain without building it. It also leads to the debate on how to handle crate version change on bumping MSRV, conservative approach is to consider such changes as breaking ones, which can hinder adoption of new features across ecosystem or result in version number inflation, which makes it harder to keep downstream crates up-to-date. More relaxed approach on another hand can result in broken crates for user of older compiler versions.

First stage: dumb field

We can start with the simple addition of rust field which should respect minimal requirements:

  • value should be a version in semver format or equal to “nigthly”
  • version should not be bigger than the current stable toolchain
  • version should not be smaller than 1.27 (version in which package.rust field became a warning instead of an error)

At first it can be simply a declarative field without any functionality behind it. The reason for it to reduce implementation cost of minimal viable version to the minimum and ideally ship it as part of Rust 2018. It will also allow crate authors who care about MSRV to start mark their crates early. Additionally cargo init will add rust field equal to a version of the used toolchain.

Using edition = "2018" will imply rust = "1.30" if not specified otherwise. In case of 2015 edition it will be rust="1.0". It will be an error to use rust="1.27" and edition="2018", but rust="1.40" and edition="2015" is a valid combination.

Second stage: versions resolution

Cargo will add rust field as a constraint to dependency versions resolution. If user uses e.g. Rust 1.20 and uses crate foo = "0.2", but all selected versions of foo specify MSRV say equal 1.21 or bigger (or even nightly) cargo will issue an error.

rust field value will be checked as well, on crate build cargo will check if all upstream dependencies can be built with the specified MSRV. (i.e. it will check if there is exists solution for given crates and Rust versions constraints)

Yanked crates will be ignored in this process.

Implementing this functionality will allow to close the debate regarding MSRV handling in crate versions and will allow crate authors to feel less restrictive about bumping their crate’s MSRV.

Third stage: better crate checks

Here we introduce two level checks for crates. First level will check if all used items were stabilised before or on given MSRV using #[stable(since=version)] attribute, issuing compile errors otherwise.

Second level will try to build crate with the specified MSRV on cargo publish, i.e. words it will be required to install MSRV toolchain. (this check can be disabled)

While these two checks will not replace proper CI testing, they will help to reduce number of improper MSRV configuration to the minimum.

Extension: nightly versions

For some bleeding-edge crates which experience frequent breaks on Nightly updates (e.g. rocket) it can be useful to specify exact Nigthly version(s) on which crate can be built. One way to achieve this is by using the following syntax:

  • single version: `rust = “nightly: 2018-01-01”
  • enumeration: “nightly: 2018-01-01, 2018-01-15”
  • (inclusive) range: “nightly: 2018-01-01…2018-01-15”
  • enumeration+range: “nightly: 2018-01-01, 2018-01-08…2018-01-15”

Such restrictions can be quite severe, but hopefully this functionality will be used only by handful of crates.

Backward compatibility

Since Rust 1.27 (or maybe earlier?) cargo issues only warning on unknown fields in Cargo.toml, so impact of introducing rust field will be minimal.

Unresolved questions

  • Naming: rust vs rustc
  • Should we add additional checks?
  • What MSRV value should cargo init use, current toolchain or edition min-version?
  • Better description of versions resolution algorithm.
  • How handle crate features which bump MSRV?

Previous proposals

14 Likes

I’d like to also point at RFC 2483, which proposes LTS trains that a library could target. In effect it makes an officially-suggested point at which libraries can upgrade their Minimum Supported Rust Version without it being considered breaking.

1 Like

I think that RFC 2483 and this proposal are orthogonal to each other, even with LTS releases you need a way to specify MSRV. But nevertheless LTS releases will be indeed a great synchronization point for crate authors.

I’ve drafted Pre-RFC. I will be glad to hear additional comments and accept PRs with edits!

Using edition = "2018" will imply rust = "1.30" if not specified otherwise. In case of 2015 edition it will be rust="1.0". It will be an error to use rust="1.27" and edition="2018", but rust="1.40" and edition="2015" is a valid combination.

I feel like the RFC will eventually need to include why Editions are not sufficient. Just the other day, I was thinking about this problem and was wondering if Editions solve it.

Also, we should decide if we are designing for the present for the future. The section I quoted is planning for the future as if everyone has always been acting as if this is the case and validating their project as such, making it safe to infer the rustc version from the edition. In the present, no 2015 Edition project supports 1.0, and of a policy like this would have gradual adoption. The best you can get is that edition implies the latest rustc version before the next Edition.

This assumes that features in new rustc that are compatible with previous Editions are not exposed in them. I've not looked too closely at the proposal to know if this is the case.

1 Like

I feel like the RFC will eventually need to include why Editions are not sufficient

As I see it two main reasons are:

  • Dependency on features introduced after edition release (e.g. it's not enough to say edition="2018" for crate which uses Rust 1.35 feature)
  • In my understanding of editions functionality, only small portion of new features will require migration to a new edition. For example if ARM intrinsics get stabilized in Rust 1.40 you will be able to use them in edition="2015" crate, thus the legality of rust="1.40" and edition="2015" combination.

In general MSRV and Edition have different roles.

I guess it is safe to assume that this feature will be implemented together of after Rust 2018 release, so it's mostly "for the future".

The best you can get is that edition implies the latest rustc version before the next Edition.

Maybe you wanted to say "oldest version of the current Edition"? If yes, then I've meant that edition=N in the absence of explicit rust field implies that it equals to an oldest Rust version of the specified edition.

If crate does not specify edition and rust the only valid approach to allow things work as they do now is to interpret it as edition="2015" and rust="1.0" (so essentially no restrictions will be applied to versions resolution), so this part of the RFC is mostly about backward compatability.

Unfortunately there is no safe way to specify rust="1.20", as Rust 1.20 will not be able to parse Cargo.toml with rust field. (though I'll need to check if it's indeed the case)

The best you can get is that edition implies the latest rustc version before the next Edition.

Maybe you wanted to say “oldest version of the current Edition”? If yes, then I’ve meant that edition=N in the absence of explicit rust field implies that it equals to an oldest Rust version of the specified edition.

Actually, I meant what I said. As I point out, people might be using newer features from an edition and during the transition to this feature, this feature will misguess the rustc version. So the safest option is to choose the most recent rustc version possible.

If crate does not specify edition and rust the only valid approach to allow things work as they do now is to interpret it as edition="2015" and rust="1.0" (so essentially no restrictions will be applied to versions resolution), so this part of the RFC is mostly about backward compatability.

(emphasis added)

Is there a special cases I missed where "1.0" to mean "*"?

But it will not be "the latest rustc version before the next Edition", as you can use new features by being on the previous edition. So essentially you proposal boils down to "if crate does not specify rust field it is equal to the current stable version of Rust", which I think will be overly-conservative approach. Do not forget that rust field does not guarantee anything, it's just a "best effort" tool which relies on crate authors to work properly.

I believe the better approach will be to do the following: if rust field is not specified, cargo publish will automatically insert version of the currently used toolchain without modifying local Cargo.toml file. (i.e. Cargo.toml which will be sent to crates.io will have automatically generated rust field)

I don't think so. I believe assumption about using only post-1.0 Rust is a safe one.

But it will not be “the latest rustc version before the next Edition”, as you can use new features by being on the previous edition. So essentially you proposal boils down to “if crate does not specify rust field it is equal to the current stable version of Rust”, which I think will be overly-conservative approach. Do not forget that rust field does not guarantee anything, it’s just a “best effort” tool which relies on crate authors to work properly.

Like I said, I hadn't looked into new features being available in old Editions and my proposal breaks down in that case.

I believe the better approach will be to do the following: if rust field is not specified, cargo publish will automatically insert version of the currently used toolchain without modifying local Cargo.toml file. (i.e. Cargo.toml which will be sent to crates.io will have automatically generated rust field)

This seems like a nice compromise.

Not speaking to any broader discussion of whether we should do this, but as the person most likely to have to implement it, I do not foresee this being hard to implement in cargo’s resolver. One way is we can just filter the result from querying the index to crates with msrv < our msrv. The tricky part would be, as usual, the error messages, but even that is doable.

1 Like

I think another aspect to consider is that not all crates will have a fixed minimal rustc/cargo version. Especially if a crate if fairly new an still in flux (and might stay in this state for a while) it totally makes sense to only commit just to an release channel or a version relative to one.

So I would do something like:

  • "X.Y" (e.g. "1.23") for having a minimal version
  • "stable","beta" for the last release in stable, beta at any time
  • "stable-1" (and smimilar) like “stable” but with a slack period of 1 release cycle (not sure if we need it, but having e.g. "stable-0.5" which says that it works with only with the newest stable release but gives you 3week time to upgrade seems not wrong)
  • "nightly" for the latest nightly snapshot at any time
  • "nightly: ..." se the first post in this thread

Also there are a lot of versioning related problems for which there isn’t a single good answer e.g. if bumping the minimal version is a breaking change. So we should avoid to enforce to many checks.

Additionally sometimes people aren’t aware that there crate works on earlier versions, especially if this is only true wrt. to some features not being enabled or some target arch, so we might want to add a way to disable the version error or by default only make it a warning.

Lastly the minimal version sometimes depends on the target arch, os and enabled features which can not be expressed through a simple rust version field.

Still I think having a simple rust version field (including “nightly” and “stable” as versions) which is checked but not necessarily enforced seems like an good idea.

I don’t think that having “stable”, “beta” and “nightly” mean the latest Rust version on the respective channel is a good idea. For example it will look quite strange for a crate published an year ago to require latest stable. i think for new crates aproach described earlier with using current toolchain version when publishing crate should solve your concerns.

I agree that option to disable MSRV checks for dependencies will be a good escape hatch and it will work nicely with the automatic rust selection. As for publishing crates we already have --no-verify option.

3 Likes

This is why a check minimal version is tricky, some crates will officially only support latest stable/beta/nightly and checking for this makes sense when using them through git,path dependencies. But if they publish a specific version of a crate this version is innmutable and through this implicitly compatible with the currently actual rust version and all newer ones (except maybe for nightly crates in some cases).

Maybe the cargo package command could check what “stable”/“beta” is at the point of packaging* and add this meta information to the package (which then is uploaded to cargo).

*(local on the system of the person doing the packagine i.e. in worst cast this would be the rust version used for packaging)

The point of having “stable” as a version is:

  • using a specific version number can be (miss-)interpreted as committing to a specific version
  • some would argue that changing the min. version is a braking change or at last requires bumping the minor version number
  • but if you develop a crate against only the newest version of stable (e.g. because it’s still in flux) then adding a patch might implicitly bump the min. version by you using a new feature, even through it might be internally only

(Without question having a explicit min. version for any half stable crate is recommended, but crates are published even before they reach this point)

I think it makes sense to declare that you support a channel, and have cargo translate that into a specific rustc version every time you publish. Then we’d recommend that you only declare a specific rustc version if you actually have a CI job running against that specific version, and everyone else defaults to whatever version they had installed when they ran cargo publish. That way every published version of a crate would have a single immutable minimum rustc version associated with it, and that version is about as likely to be the “true” minimum version as I think we can realistically hope to achieve here.

If rust is provided manually then author commits to supporting the selected version. if rust field is not provided (i.e. cargo uses current toolchain version each time crate is published), then automatically generated rust will mean "crate was build using this Rust version", it may work on older versions, but you'll have to test it. So crate author will implicitly commit to the version just by using it to develop the crate. Note that in the local Cargo.toml rust field still will not be present, so if author has updated toolchain, on the next cargo publish the new version for rust field will be selected.

I really hope that introduction of rust field will close this debate. With it bumping MSRV will not be a breaking change, as older compiler will simply select older crate version which support it.

I think you misunderstood how automatic selection will work. Local Cargo.toml will not contain automatically selected rust field, it will be just implictly added on cargo publish. (with an appropriate warning) So if you develop crate and update toolchain on each cargo publish wll be selected your current toolchain version.

For stable-n, How would one know when the "stable" was set?

I would prefer a simple minimum-version key in the Cargo.toml. For nightly/beta the user could either specify nightly or <version>-nightly.

1 Like

Hm, the only advantage which I can see is that it will allow to develop on Nightly, but cargo will select latest stable version known to it. Though I am not sure if it’s a good aproach, as I don’t think that we can assume that Nightly compiler will have the same behaviour as stable even if crate does not enable nightly features. Can you please argument a bit more why this apporach is preferable to “if rust is not provided, cargo publish will use current (local) toolchain version”?

But this misses which tool-chain the author made the crate for. Not having a filed should only be interpreted as not knowing if the author commited to any channel/version which isn't the same as a author committing to only support the newest version.

Which might not be a good idea, i.e. newer versions, especially patch versions might have closed security vulnerabilities or fixed other bugs, forgetting to upgrade rust might therefore lead to implicitly introducing already fixed security vulnerabilities in your system.

At the very least author made the crate for the toolchain which he uses (usuaully it will be latest stable or nigthly) and unfortunately it's the only reliable information which we'll have. If authors knows MSRV they'll provide rust field explicitly. As I see it rust="stable" will not bring anything new to the table here.

I don't think that this is a problem about rust field and tooling around it, but about authors responsibility and desire to backport such patches. Described functionality allows to do it, e.g. for pre-1.0 crates authors can publish 0.1.10 with backported updates and rust="1.30", and immideatly after 0.1.11 update for the main branch with rust="1.45". (it stretches semver quite a bit, but AFAIK unfortunately we can't use something like 0.1.3.1 for backports) And for post-1.0 crates convention can be to bump minor version on MSRV change (so it's not a breaking update) to allow publishing backports by bumping patch version.