- Feature Name:
msrv-resolver
- Start Date: 2023-11-14
- Pre-RFC: internals
- RFC PR: rust-lang/rfcs#3537
- Rust Issue: rust-lang/cargo#9930
Summary
Provide a happy path for developers needing to work with older versions of Rust by
- Preferring MSRV (minimum-supported-rust-version) compatible dependencies when Cargo resolves dependencies
- Ensuring compatible version requirements when
cargo add
auto-selects a version - Smoothing out the path for setting and maintaining a verified MSRV so the above will be more likely to pick a working version.
Note: cargo install
is intentionally left out for now to decouple discussions on how to handle the security ramifications.
Note: Approval of this RFC does not mean everything is set in stone, like with all RFCs.
This RFC will be rolled out gradually as we stabilize each piece.
In particular, we expect to make the cargo new
change last as it is dependent on the other changes to work well.
In evaluating stabilization, we take into account changes in the ecosystem and feedback from testing unstable features.
Based on that evaluation, we may make changes from what this RFC says.
Whether we make changes or not, stabilization will then require approval of the cargo team to merge
(explicit acknowledgement from all but 2 members with no concerns from any member)
followed by a 10 days Final Comment Period (FCP) for the remaining 2 team members and the wider community.
Cargo FCPs are now tracked in This Week in Rust to ensure the community is aware and can participate.
Even then, a change like cargo new
can be reverted without an RFC,
likely only needing to follow the FCP process.
Motivation
Status Quo
Ensuring you have a `Cargo.lock` with dependencies compatible with your minimum-supported Rust version (MSRV) is an arduous task of running `cargo update --precise ` until it works.
Let’s step through a simple scenario where a user develops with the latest Rust version but production uses an older version:
$ cargo new msrv-resolver
Created binary (application) `msrv-resolver` package
$ cd msrv-resolver
$ # ... add `package.rust-version = "1.64.0"` to `Cargo.toml`
$ cargo add clap
Updating crates.io index
Adding clap v4.4.8 to dependencies.
Features:
...
Updating crates.io index
$ git commit -a -m "WIP" && git push
...
After 30 minutes, CI fails. The first step is to reproduce this locally
$ rustup install 1.64.0
...
$ cargo +1.64.0 check
Updating crates.io index
Fetch [===============> ] 67.08%, (28094/50225) resolving deltas
After waiting several minutes, cursing being stuck on a version from before sparse registry support was added…
$ cargo +1.64.0 check
Updating crates.io index
Downloaded clap v4.4.8
Downloaded clap_builder v4.4.8
Downloaded clap_lex v0.6.0
Downloaded anstyle-parse v0.2.2
Downloaded anstyle v1.0.4
Downloaded anstream v0.6.4
Downloaded 6 crates (289.3 KB) in 0.35s
error: package `clap_builder v4.4.8` cannot be built because it requires rustc 1.70.0 or newer, while the currently ac
tive rustc version is 1.64.0
Thankfully, crates.io now shows supported Rust versions, so I pick v4.3.24.
$ cargo update -p clap_builder --precise 4.3.24
Updating crates.io index
error: failed to select a version for the requirement `clap_builder = "=4.4.8"`
candidate versions found which didn't match: 4.3.24
location searched: crates.io index
required by package `clap v4.4.8`
... which satisfies dependency `clap = "^4.4.8"` (locked to 4.4.8) of package `msrv-resolver v0.1.0 (/home/epage/src/personal/dump/msrv-resolver)`
perhaps a crate was updated and forgotten to be re-vendored?
After browsing on some forums, I edit my Cargo.toml
to roll back to clap = "4.3.24"
and try again
$ cargo update -p clap --precise 4.3.24
Updating crates.io index
Downgrading anstream v0.6.4 -> v0.3.2
Downgrading anstyle-wincon v3.0.1 -> v1.0.2
Adding bitflags v2.4.1
Downgrading clap v4.4.8 -> v4.3.24
Downgrading clap_builder v4.4.8 -> v4.3.24
Downgrading clap_lex v0.6.0 -> v0.5.1
Adding errno v0.3.6
Adding hermit-abi v0.3.3
Adding is-terminal v0.4.9
Adding libc v0.2.150
Adding linux-raw-sys v0.4.11
Adding rustix v0.38.23
$ cargo +1.64.0 check
Downloaded clap_builder v4.3.24
Downloaded errno v0.3.6
Downloaded clap_lex v0.5.1
Downloaded bitflags v2.4.1
Downloaded clap v4.3.24
Downloaded rustix v0.38.23
Downloaded libc v0.2.150
Downloaded linux-raw-sys v0.4.11
Downloaded 8 crates (2.8 MB) in 1.15s (largest was `linux-raw-sys` at 1.4 MB)
error: package `anstyle-parse v0.2.2` cannot be built because it requires rustc 1.70.0 or newer, while the currently a
ctive rustc version is 1.64.0
Again, consulting crates.io
$ cargo update -p anstyle-parse --precise 0.2.1
Updating crates.io index
Downgrading anstyle-parse v0.2.2 -> v0.2.1
$ cargo +1.64.0 check
error: package `clap_lex v0.5.1` cannot be built because it requires rustc 1.70.0 or newer, while the currently active
rustc version is 1.64.0
Again, consulting crates.io
$ cargo update -p clap_lex --precise 0.5.0
Updating crates.io index
Downgrading clap_lex v0.5.1 -> v0.5.0
$ cargo +1.64.0 check
error: package `anstyle v1.0.4` cannot be built because it requires rustc 1.70.0 or newer, while the currently active
rustc version is 1.64.0
Again, consulting crates.io
cargo update -p anstyle --precise 1.0.2
Updating crates.io index
Downgrading anstyle v1.0.4 -> v1.0.2
$ cargo +1.64.0 check
Downloaded anstyle v1.0.2
Downloaded 1 crate (14.0 KB) in 0.60s
Compiling rustix v0.38.23
Checking bitflags v2.4.1
Checking linux-raw-sys v0.4.11
Checking utf8parse v0.2.1
Checking anstyle v1.0.2
Checking colorchoice v1.0.0
Checking anstyle-query v1.0.0
Checking clap_lex v0.5.0
Checking strsim v0.10.0
Checking anstyle-parse v0.2.1
Checking is-terminal v0.4.9
Checking anstream v0.3.2
Checking clap_builder v4.3.24
Checking clap v4.3.24
Checking msrv-resolver v0.1.0 (/home/epage/src/personal/dump/msrv-resolver)
Finished dev [unoptimized + debuginfo] target(s) in 2.96s
Success! Mixed with many tears and less hair.
How wide spread is this? Take this with a grain of salt but based on crates.io user agents:
Common MSRVs | % Compatible Requests |
---|---|
N (1.73.0 ) | 47.432% |
N-2 (1.71.0 ) | 74.003% |
~6 mo (1.69.0 ) | 93.272% |
~1 year (1.65.0 ) | 98.766% |
Debian (1.63.0 ) | 99.106% |
~2 years (1.56.0 ) | 99.949% |
(source)
This was aided by the presence of package.rust-version
.
Of all packages (137,569), only 8,857 (6.4%) have that field set.
When limiting to the 61,758 “recently” published packages (an upload since the start of 2023),
only 8,550 (13.8%) have the field set.
People have tried to reduce the pain from MSRV with its own costs:
- Treating it as a breaking change:
- This leads to extra churn in the ecosystem when a fraction of users are likely going to benefit
- We have the precedence elsewhere in the Rust ecosystem for build and runtime system requirement changes not being breaking, like when rustc requires newer versions of glibc, Android NDK, etc.
- Adding upper limits to version requirements:
- This fractures the ecosystem by making packages incompatible with each other and the Cargo team discourages doing this
- Avoiding dependencies, re-implementing it themselves at the cost of their time and the risk for bugs, especially if
unsafe
is involved - Ensuring dependencies have a more inclusive MSRV policy then themselves
The sooner we improve the status quo, the better, as it can take years for these changes to percolate out to those exclusively developing with an older Rust version (in contrast with the example above). This delay can be reduced somewhat if a newer toolchain can be used for development version without upgrading the MSRV.
Workflows
In solving this, we need to keep in mind how people are using Cargo and how to prioritize when needs of different workflows conflict. We will then look at the potential designs within the context of this framework.
Some design criteria we can use for evaluating workflows:
- Cargo should not make major breaking changes
- Low barrier to entry
- The costs of “non-recommended” setups should focused on those that need them
- Encourage a standard of quality within the ecosystem, including
- Assumption that things will work as advertised (e.g. our success with MSRV)
- A pleasant experience (e.g. meaningful error messages)
- Secure
- Every feature has a cost and we should balance the cost against the value we expect
- Features can further constrain what can be done in the future due to backwards compatibility
- Features increase maintenance burden
- The larger the user-facing surface, the less likely users will find the feature they need and instead use the quickest shortcut
- Being transparent makes debugging easier, helps in evaluating risks (including security), and builds confidence in users
- Encourage progress and avoid stagnation
- Proactively upgrading means the total benefit to developers from investments made in Rust is higher
- Conversely, when most of the community is on old versions, it has a chilling effect on improving Rust
- This also means feedback can come more quickly, making it easier and cheaper to pivot with user needs
- Spreading the cost of upgrades over time makes forced-upgrades (e.g. for a security vulnerability) less of an emergency
- Our commitment to compatibility helps keep the cost of upgrade low
- When not competing with the above, we should do the right thing for the user rather than disrupt their flow to tell them what they should instead do
And keeping in mind
- The Rust project only supports the latest version (e.g bug and security fixes) and the burden for support for older versions is on the vendor providing the older Rust toolchain.
- Even keeping upgrade costs low, there is still a re-validation cost that mission critical applications must pay
- Dependencies in
Cargo.lock
are not expected to change from contributors using different versions of the Rust toolchain without an explicit action like changingCargo.toml
or runningcargo update
- e.g. If the maintainer does
cargo add foo && git commit && git push
, then a contributor doinggit pull && cargo check
should not have a different selection of dependencies, independent of their toolchain versions (which might mean the second user sees an error about an incompatible package).
- e.g. If the maintainer does
Some implications:
- “Support” in MSRV implies the same quality and responsiveness to bug reports, regardless of Rust version
- MSRV applies to all interactions with a project within the maintainers control
(including as a registry dependency,
cargo install --locked
, as a git dependency, contributor experience; excluding transitive dependencies, rust-analyzer, etc), unless documented otherwise like- Some projects may document that enabling a feature will affect the MSRV (e.g. moka)
- Some projects may have a higher MSRV for building the repo (e.g.
Cargo.lock
with newer dependencies, reliance on cargo features that get stripped on publish)
- We should focus the cost for maintaining support for older versions of Rust on the user of the old version and away from the maintainer or the other users of the library or tool
- Costs include lower developer productivity due to lack of access to features,
APIs that don’t integrate with the latest features,
and slower build times due to pulling in extra code to make up for missing features
(e.g. clap dropping its dependency on
is-terminal in favor of
IsTerminal
cut build time from 6s to 3s)
- Costs include lower developer productivity due to lack of access to features,
APIs that don’t integrate with the latest features,
and slower build times due to pulling in extra code to make up for missing features
(e.g. clap dropping its dependency on
is-terminal in favor of
Latest Rust with no MSRV
A user runs cargo new
and starts development.
A maintainer may also want to avoid constraining their dependents, for a variety of reasons, and leave MSRV support as a gray area.
Priority 1 because:
- ✅ No MSRV is fine as pushing people to have an MSRV would lead to either
- an inaccurate reported MSRV from it going stale which would lower the quality of the ecosystem
- raise the barrier to entry by requiring more process for packages and pushing the cost of old Rust versions on people who don’t care
Pain points:
The Rust toolchain does not provide a way to help users know new dependency versions are available, to support the users in staying up-to-date.
MSRV build errors from new dependency versions is one way to do it though not ideal as this disrupts the user.
Otherwise, they must actively run rustup update
or follow Rust news.
For dependents, this makes it harder to know what versions are “safe” to use.
Latest Rust as the MSRV
A maintainer regularly updates their MSRV to latest. They can choose to provide a level of support for old MSRVs by reserving MSRV changes to minor version bumps, giving them room to backport fixes. Due to the pain points listed below, the target audience for this workflow is likely small, likely pushing them to not specify their MSRV.
Priority 2 because:
- ✅ Low barrier to maintaining a high quality of support for their MSRV
- ✅ Being willing to advertising an MSRV, even if latest, improves the information available to developers, increasing the quality of the ecosystem
- ✅ Costs for dealing with old Rust toolchains is shifted from the maintainer and the users on a supported toolchain to those on an unsupported toolchain
- ✅ By focusing new development on latest MSRV, this provides a carrot to encourage others to actively upgrading
Pain points (in addition to the prior workflow):
In addition to their toolchain version, the Rust toolchain does not help these users with keeping their MSRV up-to-date. They can use other tools like RenovateBot though that causes extra churn in the repo.
A package could offer a lower MSRV in an unofficial capacity or with a lower quality of support
but the requirement that dependents always pass --ignore-rust-version
makes this disruptive.
Extended MSRV
This could be people exclusively running one version or that support a range of versions. So why are people on old versions?
- Not everyone is focused on Rust development and might only touch their Rust code once every couple of months,
making it a pain if they have to update every time.
- Think back to slow git index updates when you’ve stepped away and consider people who we’d be telling to run
rustup update
every time they touch Rust
- Think back to slow git index updates when you’ve stepped away and consider people who we’d be telling to run
- While a distribution provides rust to build other packages in the distribution,
users might assume that is a version to use, rather than getting Rust through
rustup
- Re-validation costs for updating core parts of the image for an embedded Linux developers can be high, keeping them on older versions
- Updates can be slow within tightly controlled environments (airgaps, paperwork, etc)
- Qualifying Rust toolchains takes time and money, see Ferrocene
- Built on or for systems that are no longer supported by rustc (e.g. old glibc, AndroidNDK, etc)
- Library and tool maintainers catering to the above use cases
The MSRV may extend back only a couple releases or to a year+ and they may choose to update on an as-need basis or keep to a strict cadence.
Depending on the reason they are working with an old version,
they might be developing the project with it or they might be using the latest toolchain.
For some of these use cases, they might controlling their “MSRV” via rust-toolchain.toml
, rather than package.rust-version
, as its their only supported Rust version (e.g. an application with a vetted toolchain).
When multiple Rust versions are supported, like with library and tool maintainers, they will need to verify at least their MSRV and latest. Ideally, they also verify their latest dependencies though this is already a recommended practice when people follow the default choice to commit their lockfile. The way they verify dependencies is restricted as they can’t rely on always updating via Dependabot/RenovateBot as a way to verify them. Maintainers likely only need to do a compilation check for MSRV as their regular CI runs ensure that the behavior (which is usually independent of rust version) is correct for the MSRV-compatible dependencies.
Priority 3 because:
- ✅ Several use cases for this workflow have little alternative
- ✅ MSRV applies to all interactions to the project which also means that the level of “support” is consistent
- ❌ This implies stagnation and there are cases where people could more easily use newer toolchains, like Debian users, but that is less so the case for other users
- ❌ For library and tool maintainers, they are absorbing costs from these less common use cases
- They could shift these costs to those that need old versions by switching to the “Latest MSRV” workflow by allowing their users to backport fixes to prior MSRV releases
Pain points:
Maintaining a working Cargo.lock
is frustrating, as demonstrated earlier.
When developing with the latest toolchain, feedback is delayed until CI or an embedded image build process which can be frustrating (using too-new dependencies, using too-new Cargo or Rust features, etc).
Extended published MSRV w/ latest development MSRV
This is where the published package for a project claims an extended MSRV but interactions within the repo require the latest toolchain.
The requirement on the latest MSRV could come from the Cargo.lock
containing dependencies with the latest MSRV or they could be using Cargo features that don’t affect the published package.
In some cases, the advertised MSRV might be for a lower tier of support than what is supported for the latest version.
For instance, a project might intentionally skip testing against their MSRV because of known bugs that will fail the test suite.
In some cases, the MSRV-incompatible dependencies might be restricted to dev-dependencies
.
Though local development can’t be performed with the MSRV,
the fact that the tests are verifying (on a newer toolchain) that the build/normal dependencies work gives a good amount of confidence that they will work on the MSRV so long as they compile.
Compared to the above workflow, this is likely targeted at just library and tool maintainers as other use cases don’t have access to the latest version or they are needing the repo to be compatible with their MSRV.
Priority 4 because:
- ❌ The MSRV has various carve outs, providing an inconsistent experience compared to other packages using other workflows and affecting the quality of the ecosystem
- For workspaces with bins,
cargo install --locked
is expected to work with the MSRV but won’t - If they use new Cargo features, then
[patch]
ing in a git source for the dependency won’t work - For contributors, they must be on an unspecified Rust toolchain version
- For workspaces with bins,
- ❌ The caveats involved in this approach (see prior item) would lead to worse documentation which lowers the quality to users
- ❌ This still leads to stagnation despite being able to use the latest dependencies as they are limited in what they can use from them and they can’t use features from the latest Rust toolchain
- ❌ These library and tool maintainers are absorbing costs from the less common use cases of their dependents
- They could shift these costs to those that need old versions by switching to the “Latest MSRV” workflow by allowing their users to backport fixes to prior MSRV releases
Pain points:
Like the prior workflow, when developing with the latest toolchain, feedback is delayed until CI or an embedded image build process which can be frustrating (using too-new dependencies, using too-new Cargo or Rust features, etc).
When Cargo.lock
is resolved for latest dependencies, independent of MSRV,
verifying the MSRV becomes difficult as they must either juggle two lockfiles, keeping them in sync, or use the unstable -Zminimal-versions
.
The two lockfile approach also has all of the problems shown earlier in writing the lockfile.
When only keeping MSRV-incompatible dev-dependencies
,
one lockfile can be used but it can be difficult to edit the Cargo.lock
to ensure you get new dev-dependencies
without infecting other dependency types.
Guide-level explanation
We are introducing several new concepts
- A v3 resolver (
package.resolver
) that will prefer packages compatible with yourpackage.rust-version
over those that aren’t- If
package.rust-version
is unset, then your current Rust toolchain version will be used - This resolver version will be the default for the next edition
- A
.cargo/config.toml
field will be added to disable this, e.g. for CI
- If
- Cargo will ensure users are aware their dependencies are behind the latest in a unobtrusive way
cargo add
will select version requirements that can be met by a dependency with a compatible version- A new value for
package.rust-version
,"tbd-name-representing-currently-running-rust-toolchain"
, which will advertise in your published package your current toolchain version as the minimum-supported Rust versioncargo new
will default topackage.rust-version = "tbd-name-representing-currently-running-rust-toolchain"
- A deny-by-default lint will replace the build error from a package having an incompatible Rust version, allowing users to opt-in to overriding it
Example documentation updates
The rust-version
field
(update to manifest documentation)
The rust-version
field is an optional key that tells cargo what version of the
Rust language and compiler you support compiling your package with. If the currently
selected version of the Rust compiler is older than the stated version, cargo
will exit with an error, telling the user what version is required.
To support this, Cargo will prefer dependencies that are compatible with your rust-version
.
[package]
# ...
rust-version = "1.56"
The Rust version can be a bare version number with two or three components; it
cannot include semver operators or pre-release identifiers. Compiler pre-release
identifiers such as -nightly will be ignored while checking the Rust version.
The rust-version
must be equal to or newer than the version that first
introduced the configured edition
.
The Rust version can also be "tbd-name-representing-currently-running-rust-toolchain"
.
This will act the same as if it was set to the version of your Rust toolchain.
Your published manifest will have "tbd-name-representing-currently-running-rust-toolchain"
replaced with the version of your Rust toolchain.
Setting the rust-version
key in [package]
will affect all targets/crates in
the package, including test suites, benchmarks, binaries, examples, etc.
Note: The first version of Cargo that supports this field was released with Rust 1.56.0. In older releases, the field will be ignored, and Cargo will display a warning.
Rust Version
(update to Dependency Resolution’s Other Constraints documentation)
When multiple versions of a dependency satisfy all version requirements,
cargo will prefer those with a compatible package.rust-version
over those that
aren’t compatible.
Some details may change over time though cargo check && rustup update && cargo check
should not cause Cargo.lock
to change.
resolver.precedence
(update to Configuration)
- Type: string
- Default: “rust-version”
- Environment:
CARGO_RESOLVER_PRECEDENCE
Controls how Cargo.lock
gets updated on changes to Cargo.toml
and with cargo update
. This does not affect cargo install
.
maximum
: prefer the highest compatible versions of dependenciesrust-version
: prefer dependencies where theirpackage.rust-version
is less than or equal to yourpackage.rust-version
rust-version
can be overridden with --ignore-rust-version
which will fallback to maximum
.
Example workflows
We’ll step through several scenarios to highlight the changes in the user experience.
Latest Rust with MSRV
I’m learning Rust and wanting to write my first application.
The book suggested I install using rustup
.
Expand for step through of this workflow
I’ve recently updated my toolchain
$ rustup update
Downloading and install 1.92
At some point, I start a project:
$ cargo new foo
$ cat foo/Cargo.toml
[package]
name = "foo"
version = "0.1.0"
edition = "2024"
rust-version = "tbd-name-representing-currently-running-rust-toolchain"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
$ cargo add clap -F derive
Adding clap 5.10.30
(note: this user would traditionally be a “Latest Rust” user but package.rust-version
automatically them moved to “Latest Rust with MSRV” without extra validation effort or risk of their MSRV going stale)
After some time, I get back to my project and decide to add completion support:
$ cargo add clap_complete
Adding clap_complete 5.10.40
warning: clap_complete 5.11.0 exists but requires Rust 1.93 while you are running 1.92.
To use the clap_complete@5.11.0 with a compatible Rust version, run `rustup update && cargo add clap_complete@5.11.0`.
To force the use of clap_complete@5.11.0 independent of your toolchain, run `cargo add clap_complete@5.11.0`
Wanting to be on the latest version, I run
$ rustup update
Downloading and install 1.94
$ cargo update
Updating clap v5.10.30 -> v5.11.0
Updating clap_complete v5.10.40 -> v5.11.0
Alternate: But what if I manually edited Cargo.toml
instead of cargo add
?
Here, we can shortcut some questions about version requirements because clap aligns on minor releases.
[package]
name = "foo"
version = "0.1.0"
edition = "2024"
rust-version = "tbd-name-representing-currently-running-rust-toolchain"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "5.10.30", features = ["derive"] }
clap_complete = "5.10" # <-- new
And away I go:
$ cargo check
Warning: adding clap_complete@5.10.40 because 5.11.0 requires Rust 1.93 while you are running 1.92.
To use the clap_complete@5.11.0 with a compatible Rust version, run `rustup update && cargo update`.
To force the use of clap_complete@5.11.0 independent of your toolchain, run `cargo update --ignore-rust-version`
But I am in a hurry and don’t want to disrupt my flow.
clap_complete@5.10.40
is likely fine.
I am running clap@5.10.30
and that has been working for me.
I might even run cargo deny
to see if there are known vulnerabilities.
So I continue development.
Later I run:
$ cargo update
Name Current Latest Note
============= ======= ====== ==================
clap 5.10.30 5.11.0 requires Rust 1.93
clap_complete 5.10.40 5.11.0 requires Rust 1.93
note: To use the latest depednencies, run `rustup update && cargo update`.
To force the use of the latest dependencies, independent of your toolchain, run `cargo update --ignore-rust-version`
$ rustup update
Downloading and install 1.94
$ cargo update
Updating clap v5.10.30 -> v5.11.0
Updating clap_complete v5.10.40 -> v5.11.0
At this point, I want to publish
$ cargo publish
... crates.io error about missing fields
$ $EDITOR `Cargo.toml`
$ cargo publish
Published foo 0.1.0
If I look on crates.io, the new 0.1.0 version shows up with a rust-version of 1.94
without me having to manual update the field and
relying on the cargo publish
s verify step to verify the correctness of that MSRV.
Extended “MSRV” with an application
I am developing an application using a certified toolchain.
I specify this toolchain using a rust-toolchain.toml
file.
Rust 1.94 is the latest but my certified toolchain is 1.92.
Expand for step through of this workflow
At some point, I start a project:
$ cargo new foo
$ cat foo/Cargo.toml
[package]
name = "foo"
version = "0.1.0"
edition = "2024"
rust-version = "tbd-name-representing-currently-running-rust-toolchain"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
$ cargo add clap -F derive
Adding clap 5.10.30
warning: clap 5.11.0 exists but requires Rust 1.93 while you are running 1.92.
To use the clap@5.11.0 with a compatible Rust version, run `rustup update && cargo add clap@5.10.0`.
To force the use of clap_complete@5.11.0 independent of your toolchain, run `cargo add clap@5.10.0`
At this point, I have a couple of options
- I check and clap advertises that they “support” Rust 1.92 by cherry-picking fixes into 5.10 and I feel comfortable with that
- I check
cargo deny
and don’t see any vulnerabilities and that is good enough for me, knowing that the majority of my users are likely on newer versions - I decide that clap doesn’t align with my interests and use something else
Assuming (1) or (2) applies, I ignore the warning and move on.
Extended MSRV with an application targeting multiple Rust versions
(this is a re-imagining of the Motivation’s example)
I’m building an application that is deployed to multiple embedded Linux targets. Each target’s image builder uses a different Rust toolchain version to avoid re-validating the image.
Expand for step through of this workflow
I’ve recently updated my toolchain
$ rustup update
Downloading and install 1.94
At some point, I start a project:
$ cargo new foo
$ cat foo/Cargo.toml
[package]
name = "foo"
version = "0.1.0"
edition = "2024"
rust-version = "tbd-name-representing-currently-running-rust-toolchain"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
$ cargo add clap -F derive
Adding clap 5.11.0
I send this to my image builder and I get this failure for one of my embedded targets:
$ cargo build
error: clap 5.11.0 requires Rust 1.93.0 while you are running 1.92.0
note: downgrade to 5.10.30 for a version compatible with Rust 1.92.0
note: set `package.rust-version = "1.92.0"` to ensure compatible versions are selected in the future
note: lint `cargo::incompatible-msrv` is denied by default
I make the prescribed changes:
[package]
name = "foo"
version = "0.1.0"
edition = "2024"
rust-version = "1.92" # <-- was "tbd-name-representing-currently-running-rust-toolchain" before I edited it
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "5.10.30", features = ["derive"] } # <-- downgraded
And my image build works!
After some time, I run:
$ cargo update
Name Current Latest Note
============= ======= ====== ==================
clap 5.10.30 5.11.0 requires Rust 1.93
clap_complete 5.10.40 5.11.0 requires Rust 1.93
note: To use the latest depednencies, run `rustup update && cargo update`.
To force the use of the latest dependencies, independent of your toolchain, run `cargo update --ignore-rust-version`
We’ve EOLed the last embedded target that supported 1.92 and so we can update our package.rust-version
,
so we can update it and our dependencies:
$ cargo update --update-rust-version
Updating clap 5.10.30 to 5.11.0
Updating foo's rust-version from 1.92 to 1.93
Extended MSRV for a Library
I’m developing a new library and am willing to take on some costs for supporting people on older toolchains.
Expand for step through of this workflow
I’ve recently updated my toolchain
$ rustup update
Downloading and install 1.94
At some point, I start a project:
$ cargo new foo --lib
I’ve decided on an “N-2” MSRV policy:
[package]
name = "foo"
version = "0.1.0"
edition = "2024"
rust-version = "1.92" # <-- was "tbd-name-representing-currently-running-rust-toolchain" before I edited it
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
$ cargo add clap -F derive
Adding clap 5.10.30
warning: clap 5.11.0 exists but requires Rust 1.93 while `foo` has `package.rust-version = "1.92"`
To use clap@5.11.0 with a compatible package.rust-version, run `cargo add clap@5.11.0 --update-rust-version`
To force the use of clap@5.11.0 independent of your toolchain, run `cargo add clap@5.11.0`
At this point, I have a couple of options
- I check and clap advertises that they “support” Rust 1.92 by cherry-picking fixes into 5.10 and I feel comfortable with that
- I check
cargo deny
and don’t see any vulnerabilities and that is good enough for me, knowing that the majority of my users are likely on newer versions - I decide that clap doesn’t align with my interests and use something else
Assuming (1) or (2) applies, I ignore the warning and move on.
After some time, I run:
$ cargo update
Name Current Latest Note
============= ======= ====== ==================
clap 5.10.30 5.11.0 requires Rust 1.93
clap_complete 5.10.40 5.11.0 requires Rust 1.93
note: To use the latest depednencies, run `rustup update && cargo update`.
To force the use of the latest dependencies, independent of your toolchain, run `cargo update --ignore-rust-version`
At this point, 1.95 is out, so I’m fine updating my MSRV and I run:
$ cargo update --update-rust-version
Updating clap 5.10.30 to 5.11.0
Updating foo's rust-version from 1.92 to 1.93
Instead, if a newer clap version was out needing 1.94 or 1.95, I would instead edit Cargo.toml
myself.
Reference-level explanation
We expect these changes to be independent enough and beneficial on their own that they can be stabilized as each is completed.
Cargo Resolver
We will be adding a v3 resolver, specified through workspace.resolver
/ package.resolver
.
This will become default with the next Edition.
When resolver = "3"
is set, Cargo’s resolver will change to prefer MSRV compatible versions over
incompatible versions when resolving new dependencies, except for cargo install
.
Initially, dependencies without package.rust-version
will be preferred over
MSRV-incompatible packages but less than those that are compatible.
The exact details for how preferences are determined may change over time,
particularly when no MSRV is specified,
but this shouldn’t affect existing Cargo.lock
files since the currently
resolved dependencies always get preference.
This can be overridden with --ignore-rust-version
and config’s resolver.precedence
.
Implications
- If you use
cargo update --precise <msrv-incompatible-ver>
, it will work - If you use
--ignore-rust-version
once, you don’t need to specify it again to keep those dependencies though you might need it again on the next edit ofCargo.toml
orcargo update
run - If a dependency doesn’t specify
package.rust-version
but its transitive dependencies specify an incompatiblepackage.rust-version
, we won’t backtrack to older versions of the dependency to find one with a MSRV-compatible transitive dependency. - A package with multiple MSRVs, depending on the features selected, can still do this as version requirements can still require versions newer than the MSRV and
Cargo.lock
can depend on those as well.
As there is no workspace.rust-version
,
the resolver will pick the lowest version among workspace members.
This will be less optimal for workspaces with multiple MSRVs and dependencies unique to the higher-MSRV packages.
Users can workaround this by raising the version requirement or using cargo update --precise
.
When rust-version
is unset,
we’ll fallback to rustc --version
if its not a pre-release.
This is primarily targeted at helping users with a
rust-toolchain.toml
file
(to reduce duplication)
though this would also help users who happen to be on an old rustc, for whatever reason.
As this is just a preference for resolving dependencies, rather than prescriptive,
this shouldn’t cause churn of the Cargo.lock
file.
We already call rustc
for feature resolution, so hopefully this won’t have a performance impact.
Cargo config
We’ll add a resolver.precedence
field to .cargo/config.toml
which will control the package version prioritization policy.
[build]
resolver.precedence = "rust-version" # Default with `v3`
with potential values being:
maximum
: behavior today (default for v1 and v2 resolvers)- Needed for verifying latest dependencies
minimum
(unstable):-Zminimal-versions
- As this is just precedence,
-Zdirect-minimal-versions
doesn’t fit into this
- As this is just precedence,
rust-version
: what is defined in the package (default for v3 resolver)rust-version=
(future possibility)package
: long form ofrust-version
rustc
: the current running version- Needed for “separate development / publish MSRV” workflow
<x>[.<y>[.<z>]]
: manually override the version used
If a rust-version
value is used, we’d switch to maximum
when --ignore-rust-version
is set.
cargo build
The MSRV-compatibility build check will be demoted from an error to a deny
-by-default workspace
diagnostic,
allowing users to intentionally use dependencies on an unsupported (or less supported) version of Rust
without requiring --ignore-rust-version
on every invocation.
Ideally, we present all of the MSRV issues upfront to be resolved together. At minimum, we should present a top-down message, rather than bottom up.
If package.rust-version
is unset or "tbd-name-representing-currently-running-rust-toolchain"
, the diagnostic should suggest setting it
to help raise awareness of package.rust-version
being able to reduce future
resolution errors.
This would benefit from knowing the oldest MSRV.
cargo update
cargo update
will inform users when an MSRV or semver incompatible version is available.
cargo update --dry-run
will also report this information so that users can check on the status of this at any time.
Users may pass
--ignore-rust-version
to pick the latest dependencies, ignoring allrust-version
fields (your own and from dependencies)--update-rust-version
to pick therustc --version
-compatible dependencies, updating yourpackage.rust-version
if needed to match the highest of your dependencies<pkgname> --precise <version>
to pick a specific version, independent of therust-version
field
We expect the notice to inform users of these options for allowing them to upgrade.
Those flags will also be added to cargo generate-lockfile
Syncing Cargo.toml
to Cargo.lock
on any Cargo command
In addition to the cargo update
output to report when things are held back (both MSRV and semver),
we will try having dependency resolves highlight newly selected dependency versions that were held back due to MSRV or semver.
Whether we do this and how much will be subject to factors like noisy output, performance, etc.
Some approaches we can take for doing this include:
After resolving, we can do a depth-first diff of the trees, stopping and reporting on the first different node.
This would let us report on any command that changes the way the tree is resolved
(from explicit changes with cargo update
to cargo build
syncing Cargo.toml
changes to Cargo.lock
).
We’d likely want to limit the output to only the sub-tree that changed.
If there wasn’t previously a Cargo.lock
, this would mean everything.
We could either always do the second resolve or only do the second resolve if the resolver changed anything, whichever is faster.
Its unknown whether making the inputs available for multiple resolves would have a performance impact.
While a no-change resolve is fast, if this negatively impacts it enough, we could explore hashing the resolve inputs and storing that in the lockfile, allowing us to detect if the inputs have changed and only resolving then.
cargo add
cargo add <pkg>
(no version) will pick a version requirement that is low
enough so that when it resolves, it can pick a dependency that is
MSRV-compatible.
cargo add
will warn when it does this.
Users may pass
--ignore-rust-version
to pick the latest dependencies, ignoring allrust-version
fields (your own and from dependencies)--update-rust-version
to pick therustc --version
-compatible dependencies, updating yourpackage.rust-version
if needed to match the highest of your dependencies
cargo publish
package.rust-version
will gain support for an "tbd-name-representing-currently-running-rust-toolchain"
value, in addition to partial versions.
On cargo publish
/ cargo package
, the generated *.crate
s Cargo.toml
will have "tbd-name-representing-currently-running-rust-toolchain"
replaced with rustc --version
.
If rustc --version
is a pre-release, publish will fail.
cargo new
will include package.rust-version = "tbd-name-representing-currently-running-rust-toolchain"
.
Drawbacks
Users upgrading to the next Edition (or changing to resolver = '3"
), will have to manually update their CI to test the latest dependencies with CARGO_RESOLVER_PRECEDENCE=maximum
.
Workspaces have no edition
, so its easy for users to not realize they need to set resolver = "3"
or to update their resolver = "2"
to "3"
(Cargo only warns on virtual manifests without an explicit workspace.resolver
).
While we hope this will give maintainers more freedom to upgrade their MSRV, this could instead further entrench rust-version stagnation in the ecosystem.
For projects with larger MSRVs than their dependencies, this introduces another form of drift from the latest dependencies (in addition to lockfiles). However, we already recommend people verify their latest dependencies, so the only scenario this further degrades is when lockfiles are verified by always updating to the latest, like with RenovateBot, and only in the sense that the user needs to know to explicitly take action to add another verification job to CI.
Rationale and alternatives
Misc alternatives
- Dependencies with unspecified
package.rust-version
: we could mark these as always-compatible or always-incompatible; there really isn’t a right answer here. - The resolver doesn’t support backtracking as that is extra complexity that we can always adopt later as we’ve reserved the right to make adjustments to what
cargo generate-lockfile
will produce over time. CARGO_RESOLVER_PRECEDENCE
is used, rather than a CLI option (e.g. ensuring every command has--ignore-rust-version
or a--rust-version <x.y.z>
)- This is unlikely to be used in one-off cases but across whole interactions which is better suited for config / env variables, rather than CLI options
- Minimize CLI clutter
CARGO_RESOLVER_PRECEDENCE=rust-version
implies maximal resolution among MSRV-compatible dependencies. Generally MSRV doesn’t decrease over versions, so minimal resolution will likely pick packages with compatible rust-versions.cargo add
helps by selecting rust-version-compatible minimum bounds- This bypasses a lot of complexity either from exploding the number of states we support or giving users control over the fallback by making the field an array of strategies.
- Instead of
resolver = "3"
, we could just change the default for everyone- The number of maintainers verifying latest dependencies is likely relatively low and they are more likely to be “in the know”, making them less likely to be negatively affected by this. Therefore, we could probably get away with treating this as a minor incompatibility
- Either way, the concern is to ensure that the change receives attention. We wouldn’t want this to be like sparse registries where a setting exists and we change the default and people hardly notice (besides any improvements)
cargo build
will treat incompatible MSRVs as a workspace-level lint, rather than a package level lint, to avoid the complexity of mapping the dependency to a workspace-member to select[lint]
tables to respect and then dealing with unifying conflicting levels in between[lint]
tables among members.--ignore-rust-version
picks absolutely the latest dependencies to support both users on latest rustc and users wanting “unsupported” dependencies, at the cost of users not on the latest rustc but still wanting latest more up-to-date dependencies than their MSRV allows- Compilation commands (e.g.
cargo check
) will take on two meanings for--ignore-rust-version
, (1)allow
the workspace diagnostic and (2) resolve changed dependencies to latest when syncingCargo.toml
toCargo.lock
.- This expansion of scope is for consistency
- Being a flag to turn the
deny
into anallow
is a high friction workflow that we expect users to not be too negatively impacted by this expansion. - With the resolver config and the configurable lint, we also expect the flag on compilation commands to be diminished in value. Maybe in the future we could even deprecate it and/or hide it.
--update-rust-version
picksrustc --version
-compatible dependencies so users can easily walk the treadmill of updating their dependencies / MSRV , no matter theirrustc
version.- There is little reason to select an MSRV higher than their Rust toolchain
- We should still be warning the user that new dependencies are available if they upgrade their Rust toolchain
- This comes at the cost of inconsistency with
--ignore-rust-version
.
- Nightly
cargo publish
with"tbd-name-representing-currently-running-rust-toolchain"
fails because there isn’t a good value to use and this gives us flexibility to change it later (e.g. just leaving therust-version
as unset).
Ensuring the registry Index has rust-version
without affecting quality
The user experience for this is based on the extent and quality of the data.
Ensuring we have package.rust-version
populated more often (while maintaining
quality of that data) is an important problem but does not have to be solved to
get value out of this RFC and can be handled separately.
We chose an opt-in for populating package.rust-version
based on rustc --version
("tbd-name-representing-currently-running-rust-toolchain"
).
This will encourage a baseline of quality as users are developing with that version and cargo publish
will do a verification step, by default.
This will help seed the Index with more package.rust-version
data for the resolver to work with.
The downside is that the package.rust-version
will likely be higher than it absolutely needs.
However, considering our definition of “support” and that the user isn’t bothering to set an MSRV themself,
aggressively updating is likely fine in this case, especially since we’ll let dependents override the build failure for MSRV-incompatible packages.
Some alternative solutions include:
When missing, cargo publish
could inject package.rust-version
using the version of rustc used during publish.
However, this will err on the side of a higher MSRV than necessary and the only way to
work around it is to set CARGO_RESOLVER_PRECEDENCE=maximum
which will then lose
all other protections.
As we said, this is likely fine but then there will be no way to opt-out for the subset of maintainers who want to keep their support definition vague.
As things evolve, we could re-evaluate making "tbd-name-representing-currently-running-rust-toolchain"
the default.
We could encourage people to set their MSRV by having
However, if people aren’t committed to verifying that was implicitly set,
it is likely to go stale and will claim an MSRV much older than what is used in practice.
If we had the hard-error resolver mode and
clippy warning people when using API items stabilized after their MSRV,
this will at least annoy people into either being somewhat compatible or removing the field.cargo new
default package.rust-version
.
When missing,
However, this will err on the side of too low of an MSRV.
These fields have an incomplete picture.
While this helps ensure there is more data for the MSRV-aware resolver,
future analysis wouldn’t be able to distinguish between inferred and explicit cargo publish
could inject package.rust-version
inferred from
package.edition
and/or other Cargo.toml
fields.package.rust-version
s.
We’d also need an explicit opt-out for those who intentionally don’t want one set.
Alternatively, cargo publish
/ the registry could add new fields to the Index
to represent an inferred MSRV, the published version, etc
so it can inform our decisions without losing the intent of the publisher.
We could help people keep their MSRV up to date, by letting them specify a policy
(e.g. rust-version-policy = "stable - 2"
or rust-version-policy = "stable"
);
then, every time the user runs cargo update
,
we could automatically update their rust-version
field as well.
This would also be an alternative to --update-rust-version
that can be further explored in the future if desired.
There are aspects of this that need to be worked out before going down this route
- Without gating this behind a flag, this will push people away from bumping their MSRV only on minor version bumps.
- Tying this to
cargo update
encourages other side effects by default (--workspace
flag would be needed to do no other update) which pushes people to a more casual approach to MSRV updating, even if we have a flag - We need to figure out what policies are appropriate and what syntax to use for them
- While a continuous sliding window (
N-M
) is most commonly used today, it is unclear if that is the right policy to bake in compared to others like periodic updates (*/M
in cron syntax) to be helping the “Extended MSRV” users along with everyone else.
- While a continuous sliding window (
- Is
stable
clear enough to mean “current version a time ofcargo update
with ratcheting semantics”? What name can work best?
When there still isn’t an MSRV set, the resolver could
- Assume the MSRV of the next published package with an MSRV set
- Sort no-MSRV versions by minimal versions, the lower the version the more likely it is to be compatible
- This runs into quality issues with version requirements that are likely too low for what the package actually needs
- For dependencies that never set their MSRV, this effectively switches us from maximal versions to minimal versions.
Configuring the resolver mode on the command-line or Cargo.toml
The Cargo team is very interested in moving project-specific config to manifests. However, there is a lot more to define for us to get there. Some routes that need further exploration include:
- If its a CLI flag, then its transient, and its unclear which modes should be transient now and in the future
- We could make it sticky by tracking this in
Cargo.lock
but that becomes less obvious what resolver mode you are in and how to change
- We could make it sticky by tracking this in
- We could put this in
Cargo.toml
but that implies it unconditionally applies to everything- But we want
cargo install
to use the latest dependencies so people get bug/security fixes - This gets in the way of “Extended published MSRV w/ latest development MSRV” being able to change it in CI to verify MSRV and “Extended MSRV” being able to change it in CI to verify latest dependencies
- But we want
By relying on config we can have a stabilized solution sooner and we can work out more of the details as we better understand the relevant problems.
Add workspace.rust-version
Instead of using the lowest MSRV among workspace members, we could add workspace.rust-version
.
This opens its own set of questions
- Do packages implicitly inherit this?
- What are the semantics if its unset?
- Would it be confusing to have this be set in mixed-MSRV workspaces? Would blocking it be incompatible with the semantics when unset?
- In mixed-MSRV workspaces, does it need to be the highest or lowest MSRV of your packages?
- For the resolver, it would need to be the lowest but there might be other use cases where it needs to be the highest
The proposed solution does not block us from later going down this road but allows us to move forward without having to figure out all of these details.
Resolver behavior
Effects of current solution on workflows (including non-resolver behavior):
- Latest Rust with no MSRV
- ✅
cargo new
settingpackage.rust-version = "tbd-name-representing-currently-running-rust-toolchain"
moves most users to “Latest Rust as the MSRV” with no extra maintenance cost - ✅ Dealing with incompatible dependencies will have a friendlier face because the hard build error after changing dependencies is changed to a notification during update suggesting they upgrade to get the new dependency because we fallback to
rustc --version
whenpackage.rust-version
is unset (as a side effect of us capturingrust-toolchain.toml
)
- Latest Rust as the MSRV
- ✅ Packages can more easily keep their MSRV up-to-date with
package.rust-version = "tbd-name-representing-currently-running-rust-toolchain"
(no policy around when it is changed) though this is dependent on your Rust toolchain being up-to-date (see “Latest Rust with no MSRV” for more)cargo update --update-rust-version
(e.g. when updating minor version) though this is dependent on what you dependencies are using for an MSRV
- ✅ Packages can more easily offer unofficial support for an MSRV due to shifting the building with MSRV-incompatible dependencies from an error to a
deny
diagnostic
- Extended MSRV
- ✅
Cargo.lock
will Just Work
- Extended published MSRV w/ latest development MSRV
- ❌ Maintainers will have to opt-in to latest dependencies, in a
.cargo/config.toml
- ✅ Verifying MSRV will no longer require juggling
Cargo.lock
files or using unstable features
A short term benefit (hence why this is separate) is that an MSRV-aware resolver by default is that we can use it as a polyfill for
cfg(version)
(which will likely need a lot of work in cargo after we finish stabilizing it for rustc).
A polyfill package can exist that has multiple maintained semver-compatible versions with different MSRVs with the older ones leveraging external libraries while the newer ones leverage the standard library.
Make this opt-in rather than opt-out
Instead of adding resolver = "3"
, we could keep the default resolver the same as today but allow opt-in to MSRV-aware resolver via CARGO_RESOLVER_PRECEDENCE=rust-version
.
- When building with old Rust versions, error messages could suggest re-resolving with
CARGO_RESOLVER_PRECEDENCE=rust-version
. The next corrective step (and suggestion from cargo) depends on what the user is doing and could be eithergit checkout main -- Cargo.lock && cargo check
cargo generate-lockfile
- We’d drop from this proposal
cargo update [--ignore-rust-version|--update-rust-version]
as they don’t make sense with this new default
This has no impact on the other proposals (cargo add
picking compatible versions, package.rust-version = "tbd-name-representing-currently-running-rust-toolchain"
, cargo build
error to diagnostic).
Effects on workflows (including non-resolver behavior):
- Latest Rust with no MSRV
- ✅
cargo new
settingpackage.rust-version = "tbd-name-representing-currently-running-rust-toolchain"
moves most users to “Latest Rust as the MSRV” with no extra maintenance cost - 🟰
Dealing with incompatible dependencies will have a friendlier face because the hard build error after changing dependencies is changed to a notification during update suggesting they upgrade to get the new dependency because we fallback torustc --version
whenpackage.rust-version
is unset (as a side effect of us capturingrust-toolchain.toml
)
- Latest Rust as the MSRV
- ✅ Packages can more easily keep their MSRV up-to-date with
package.rust-version = "tbd-name-representing-currently-running-rust-toolchain"
(no policy around when it is changed) though this is dependent on your Rust toolchain being up-to-date (see “Latest Rust with no MSRV” for more)cargo update --update-rust-version
(e.g. when updating minor version) though this is dependent on what you dependencies are using for an MSRV
- ❌ Without
cargo update --update-rust-version
,"tbd-name-representing-currently-running-rust-toolchain"
will be more of a default path, leading to more maintainers updating their MSRV more aggressively and waiting until minors - ✅ Packages can more easily offer unofficial support for an MSRV due to shifting the building with MSRV-incompatible dependencies from an error to a
deny
diagnostic
- Extended MSRV
- ✅ Users will be able to opt-in to MSRV-compatible dependencies, in a
.cargo/config.toml
- ❌ Users will be frustrated that the tool knew what they wanted and didn’t do it
- Extended published MSRV w/ latest development MSRV
- 🟰
Maintainers will have to opt-in to latest dependencies, in a.cargo/config.toml
- ✅ Verifying MSRV will no longer require juggling
Cargo.lock
files or using unstable features
Make CARGO_RESOLVER_PRECEDENCE=rustc
the default
Instead of resolver = "3"
changing the behavior to CARGO_RESOLVER_PRECEDENCE=rust-version
,
it is changed to CARGO_RESOLVER_PRECEDENCE=rustc
where the resolver selects packages compatible with current toolchain,
matching the cargo build
incompatible dependency error.
- We would still support
CARGO_RESOLVER_PRECEDENCE=rust-version
to help “Extended MSRV” users - We’d drop from this proposal
cargo update [--ignore-rust-version|--update-rust-version]
as they don’t make sense with this new default
This has no impact on the other proposals (cargo add
picking compatible versions, package.rust-version = "tbd-name-representing-currently-running-rust-toolchain"
, cargo build
error to diagnostic).
This is an auto-adapting variant where
- If they are on the latest toolchain, they get the current behavior
- If their toolchain matches their MSRV, they get an MSRV-aware resolver
Effects on workflows (including non-resolver behavior):
- Latest Rust with no MSRV
- ✅
cargo new
settingpackage.rust-version = "tbd-name-representing-currently-running-rust-toolchain"
moves most users to “Latest Rust as the MSRV” with no extra maintenance cost - ✅ Dealing with incompatible dependencies will have a friendlier face because the hard build error after changing dependencies is changed to a notification during update suggesting they upgrade to get the new dependency because we fallback to
rustc --version
whenpackage.rust-version
is unset (as a side effect of us capturingrust-toolchain.toml
)
- Latest Rust as the MSRV
- ✅ Packages can more easily keep their MSRV up-to-date with
package.rust-version = "tbd-name-representing-currently-running-rust-toolchain"
(no policy around when it is changed) though this is dependent on your Rust toolchain being up-to-date (see “Latest Rust with no MSRV” for more)cargo update --update-rust-version
(e.g. when updating minor version) though this is dependent on what you dependencies are using for an MSRV
- ❌ Without
cargo update --update-rust-version
,"tbd-name-representing-currently-running-rust-toolchain"
will be more of a default path, leading to more maintainers updating their MSRV more aggressively and waiting until minors - ✅ Packages can more easily offer unofficial support for an MSRV due to shifting the building with MSRV-incompatible dependencies from an error to a
deny
diagnostic
- Extended MSRV
- ✅ Users will be able to opt-in to MSRV-compatible dependencies, in a
.cargo/config.toml
- ❌ Users will be frustrated that the tool knew what they wanted and didn’t do it
- ❌ This may encourage maintainers to develop using their MSRV, reducing the quality of their experience (not getting latest lints, not getting latest cargo features like “wait for publish”, etc)
- Extended published MSRV w/ latest development MSRV
- ❌ Maintainers will have to opt-in to ensure they get the latest dependencies in a
.cargo/config.toml
- ✅ Verifying MSRV will no longer require juggling
Cargo.lock
files or using unstable features
Hard-error
Instead of preferring MSRV-compatible dependencies, the resolver could hard error if only MSRV-incompatible versions are available.
--ignore-rust-version
would need to be “sticky” in theCargo.lock
to avoid the next run command from rolling back theCargo.lock
which might be confusing because it is “out of sight; out of mind”.- To avoid
Cargo.lock
churn, we can’t fallback torustc --version
whenpackage.rust-version
is not present
In addition to errors, differences from the “preference” solutions include:
- Increase the chance of an MSRV-compatible
Cargo.lock
because the resolver can backtrack on MSRV-incompatible transitive dependencies, trying alternative versions of direct dependencies - When workspace members have different MSRVs, dependencies exclusive to a higher MSRV package can use higher versions
To get the error reporting to be of sufficient quality will require major work in a complex, high risk area of Cargo (the resolver). This would block stabilization indefinitely. We could adopt this approach in the future, if desired
Effects on workflows (including non-resolver behavior):
- Latest Rust with no MSRV
- ✅
cargo new
settingpackage.rust-version = "tbd-name-representing-currently-running-rust-toolchain"
moves most users to “Latest Rust as the MSRV” with no extra maintenance cost - ❌ Dealing with incompatible dependencies will have a friendlier face because the hard build error after changing dependencies is changed to a notification during update suggesting they upgrade to get the new dependency because we fallback to
rustc --version
whenpackage.rust-version
is unset (as a side effect of us capturingrust-toolchain.toml
)
- Latest Rust as the MSRV
- ✅ Packages can more easily keep their MSRV up-to-date with
package.rust-version = "tbd-name-representing-currently-running-rust-toolchain"
(no policy around when it is changed) though this is dependent on your Rust toolchain being up-to-date (see “Latest Rust with no MSRV” for more)cargo update --update-rust-version
(e.g. when updating minor version) though this is dependent on what you dependencies are using for an MSRV
- ✅ Packages can more easily offer unofficial support for an MSRV due to shifting the building with MSRV-incompatible dependencies from an error to a
deny
diagnostic
- Extended MSRV
- ✅
Cargo.lock
will Just Work forpackage.rust-version
- ❌ Application developers using
rust-toolchain.toml
will have to duplicate that inpackage.rust-version
and keep it in sync
- Extended published MSRV w/ latest development MSRV
- ❌ A design not been worked out to allow this workflow
- ❌ If this is done unconditionally, then the
Cargo.lock
will change on upgrade - ❌ This is incompatible with per-
feature
MSRVs
Prior art
- Python: instead of tying packages to a particular tooling version, the community instead focuses on their equivalent of the
rustversion
crate combined with tool-version-conditional dependencies that allow polyfills.- We have cfg_accessible as a first step though it has been stalled
- These don’t have to be mutually exclusive solutions as conditional compilation offers flexibility at the cost of maintenance. Different maintainers might make different decisions in how much they leverage each
- One big difference is Python continues to support previous releases which sets a standard within the community for “MSRV” policies.
- PHP Platform Packages is a more general mechanism than MSRV that allows declaring dependencies on external runtime requirements, like the interpreter version, interpreter extensions presence and version, or even whether the interpreter is 64-bit.
- Resolves to current system
- Can be overridden to so current system is always considered compatible
- Not tracked in their lockfile
- When run on an incompatible system, it will error and require running a command to re-resolve the dependencies for the current system
- One difference is that PHP is interpreted and that their lockfile must encompass not just development dependencies but deployment dependencies. This is in contrast to Rust which has development and deployment-build dependencies tracked with a lockfile while deployment uses OS-specific dependencies, like shared-object dependencies of ELF binaries which are not locked by their nature but instead developers rely on other technologies like docker or Nix (not even static linking can help as they that still leaves them subject to the kernel version in non-bare metal deployments).
Unresolved questions
The config field is fairly rough
- The name isn’t very clear
- The values are awkward
- Should we instead just have a
resolver.rust-version = true
?- If we later add “resolve to toolchain” version, this might be confusing.
- Maybe enumeration, like
resolver.rust-version = <manifest|toolchain|ignore>
?
rust-version = "tbd-name-representing-currently-running-rust-toolchain"
’s field name is unsettled and deciding on it is not blocking for stabilization.
Ideally, we make it clear that this is not inferred from syntax,
that this is the currently running toolchain,
that we ignore pre-release toolchains,
and the name works well for resolver config if we decide to add “resolve to toolchain version” and want these to be consistent.
Some options include:
"tbd-name-representing-currently-running-rust-toolchain"
can imply “infer from syntactic minimum”latest
can imply “latest globally (ie from rust-lang.org)stable
can imply “latest globally (ie from rust-lang.org)toolchain
might look weird?local
implies aremote
current
is likelatest
but a little softer and might work
Resolving with an unset package.rust-version
falls back to rustc --version
only if its a non-pre-release.
Should we instead pick the previous stable release (e.g. nightly 1.77 would resolve for 1.76)?
Whether we report stale dependencies only on cargo update
or on every command.
See “Syncing Cargo.toml
to Cargo.lock
on any Cargo command”.
Future possibilities
Integrate cargo audit
If we integrate cargo audit
,
we can better help users on older dependencies identify security vulnerabilities,
reducing the risks associated with being on older versions.
“cargo upgrade”
As we pull cargo upgrade
into cargo,
we’ll want to make it respect MSRV as well
cargo install
cargo install
could auto-select a top-level package that is compatible with the version of rustc that will be used to build it.
This could be controlled through a config field and
a smaller step towards this is we could stabilize the field
without changing the default away from maximum
,
allowing people to intentionally opt-in to auto-selecting a compatible top-level paclage.
Dependency resolution could be controlled through a config field install.resolver.precedence
,
mirroring resolver.precedence
.
The value add of this compared to --locked
is unclear.
See rust-lang/cargo#10903 for more discussion.
Note: rust-lang/cago#12798
(released in 1.75) made it so cargo install
will error upfront,
suggesting a version of the package to use and to pass --locked
assuming the
bundled Cargo.lock
has MSRV compatible dependencies.
cargo publish
If you publish a library using your MSRV and MSRV-incompatible dependencies exist, the publish verification step will fail. You can workaround this by
- Upgrading
- Running with
--no-verify
resolver.precedence = "rust-version=<X>[.<Y>[.<Z>]]"
We could allow people setting an effective rust-version within the config.
This would be useful for people who have a reason to not set package.rust-version
as well as to reproduce behavior with different Rust versions.
rustup supporting +msrv
See https://github.com/rust-lang/rustup/issues/1484#issuecomment-1494058857
Language-version lints
We could make developing with the latest toolchain with old MSRVs easier if we provided lints. Due to accuracy of information, this might start as a clippy lint, see #6324. This doesn’t have to be perfect (covering all facets of the language) to be useful in helping developers identify their change is MSRV incompatible as early as possible.
If we allowed this to bypass caplints, then you could more easily track when a dependency with an unspecified MSRV is incompatible.
Language-version awareness for rust-analyzer
rust-analyzer could mark auto-complete options as being incompatible with the MSRV and
automatically bump the MSRV if selected, much like auto-adding a use
statement.
Establish a policy on MSRV
For us to say “your MSRV should be X” would likely be both premature and would have a lot of caveats for different use cases.
With rust-lang/cargo#13056, we at least made it explicit that people should verify their MSRV.
Ideally, we’d at least facilitate people in setting their MSRV. Some data that could help includes:
- A report of rust-versions used making requests to crates.io as determined by the user-agent
- A report of
package.rust-version
for the latest versions of packages on crates.io - A report of
package.rust-version
for the recently downloaded versions of packages on crates.io
Once people have more data to help them in picking an MSRV policy, it would help to also document trade-offs on whether an MSRV policy should proactive or reactive on when to bump it.
Warn when adding dependencies with unspecified MSRVs
When adding packages without an MSRV, its not clear whether it will work with your project. Knowing that they haven’t declared support for your toolchain version could be important, after we’ve made it easier to declare an MSRV.
Track version maintenance status on crates.io
If you cargo add
a dependency and it says that a newer version is available but it supports a dramatically different MSRV than you,
it would be easy to assume there is a mismatch in expectations and you shouldn’t use that dependency.
However, you may still be supported via an LTS but that information can only be captured in documentation which is not within the flow of the developer.
If crates.io had a mutable package and package version metadata database, maintainers could report the maintenance status of specific versions (or maybe encode their maintenance policy), allowing cargo to report not just whether you are on latest, but whether you are supported.