Pre-RFC: MSRV-aware resolver

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

Note: cargo install is intentionally left out for now to decouple discussions on how to handle the security ramifications.

Motivation

Let's step through a simple scenario where a developer can develop 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)

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 new glibc, AndroiNDK, 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

Another way the status quo exhibits pain on the ecosystem is long arguments over what is the right policy for updating a minimum-supported Rust version (MSRV), wearing on all parties. For example:

Supporting older MSRVs means maintainers don't have access to all of the latest resources for improving their project. This indirectly affects users as it can slow maintainers down. This can also directly affect users. For example, by clap updating its MSRV from 1.64.0 to 1.70.0, it was able to drop the large is-terminal dependency, cutting the build time from 6s to 3s. So if we can find a solution that allows maintainers to move forward, helping users more on the edge, while not impacting users on older rust version, would be a big help.

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 development version can be used without upgrading the MSRV.

In solving this, we need to keep in mind

  • Users need to be aware when they are on old versions for evaluating security risk and when debugging issues
  • We don't want to end up like other ecosystems where no one can use new features because users are stuck on 3-20 year old versions of the language specification. The compatibility story is fairly strong with Rust, helping us keep compiler and dependency upgrades cheap.
  • Some people keep their development and production MSRVs the same while others keep them separate, like with a Cargo.msrv.lock
  • A Cargo.lock should not resolve differently when upgrading Rust without any other action.

Guide-level explanation

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 your package can be compiled 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.

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.

[package]
# ...
rust-version = "1.56"

The Rust version must 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 may be ignored using the --ignore-rust-version option.

Setting the rust-version key in [package] will affect all targets/crates in the package, including test suites, benchmarks, binaries, examples, etc.

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.

build.resolver.precedence

(update to Configuration)

  • Type: string
  • Default: "rust-version=package"
  • Environment: CARGO_BUILD_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 dependencies
  • minimum: Prefer the lowest versions of dependencies
  • rust-version=package: Prefer dependencies where their rust-version is compatible with package.rust-version
  • rust-version=rustc: Prefer dependencies where their rust-version is compatible with rustc --version
  • rust-version=<X>[.<Y>[.<Z>]]: Prefer dependencies where their rust-version is compatible with the specified version

rust-version= values can be overridden with --ignore-rust-version which will fallback to maximum.

Reference-level explanation

Cargo Resolver

Cargo's resolver will be updated to prefer MSRV compatible versions over incompatible versions when resolving versions. Dependencies without package.rust-version will be preferred over those without an MSRV but less than those with one. The exact details for how preferences are determined may change over time but, since the currently resolved dependencies always get preference, this shouldn't affect existing Cargo.lock files.

This can be overridden with --ignore-rust-version and config's build.resolver.precedence.

Implications

  • If you use do 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 of Cargo.toml or cargo update run
  • If a dependency doesn't specify package.rust-version but its transitive dependencies specify an incompatible package.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.

If package.rust-version is unset among all workspace members, we'll fallback to rustc --version, ensuring a build that at least works for the current system. As this is just a preference for resolving dependencies, rather than prescriptive, this shouldn't cause churn. We already call rustc for feature resolution, so hopefully this won't have a performance impact.

The resolver will only do this for local packages and not for cargo install.

cargo update

cargo update will inform users when an MSRV or semver incompatible version is available. cargo update -n will also report this information so that users can check on the status of this at any time.

Note: other operations that cause Cargo.lock entries to be changed (like editing Cargo.toml and running cargo check) will not inform the user. If they want to check the status of things, they can run cargo update -n.

cargo add

cargo add <pkg> (no version) will pick a version requirement that is low enough so that when it resolves, it will pick a dependency that is MSRV-compatible. cargo add will warn when it does this.

This behavior can be bypassed with --ignore-rust-version

Cargo config

We'll add a build.resolver.precedence field to .cargo/config.toml that will control the control pick the mechanisms for preferring one compatible version over another.

[build]
resolver.precedence = "rust-version=package"  # Default

with support values being:

  • maximum: behavior today
  • minimum (unstable): -Zminimal-versions
    • As this just just precedence, -Zdirect-minimal-versions doesn't fit into this
  • rust-version= (assumes maximum is the fallback)
    • package: what is defined in the package (default)
    • rustc: the current running version
      • Needed for "separate development / publish MSRV" workflow
    • <x>[.<y>[.<z>]] (future possibility): manually override the version used

If a rust-version= value is used, we'd switch to maximum when --ignore-rust-version is set. This will let users effectively pass --ignore-rust-version to all commands, without having to support the flag on every single command.

Drawbacks

Maintainers that commit their Cargo.lock and verify their latest dependencies will need to set CARGO_BUILD_RESOLVER_PRECEDENCE=rust-version=rustc in their environment. See Alternatives for more on this.

While we hope this will give maintainers more freedom to upgrade their MSRV, this could instead further entrench rust-version stagnation in the ecosystem.

Rationale and alternatives

Misc

  • Config was put under build to associate it with local development, as compared with install which could be supported in the future
  • 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_BUILD_RESOLVER_PRECEDENCE=rust-version=* assumes maximal resolution as generally minimal resolution will pick packages with compatible rust-versions as rust-version tends to (but doesn't always) increase over time.
    • cargo add selecting rust-version-compatible minimum bounds helps
    • 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.

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.

Make this opt-in

As proposed, CI that tries to verify against the latest dependencies will no longer do so. Instead, they'll have to make a change to their CI, like setting CARGO_BUILD_RESOLVER_PRECEDENCE=maximum.

If we consider this a major incompatibility, then it needs to be opted into. As cargo fix can't migrate a user's CI, this would be out of scope for migrating to this with a new Edition.

I would argue that the number of maintainers verifying latest dependencies is relatively low and they are more likely to be "in the know", making them less likely to be negatively affected by this. Therefore, I propose we consider this a minor incompatibility

If we do a slow roll out (opt-in then opt-out), the visibility for the switch to opt-out will be a lot less than the initial announcement and we're more likely to miss people compared to making switch over when this gets released.

If we change behavior with a new edition (assuming we treat this as a minor incompatibility), we get the fanfare needed but it requires waiting until people bump their MSRV, making it so the people who need it the most are the those who will least benefit.

Make rust-version=rustc the default

This proposal elevates "shared development / publish rust-version" workflow over "separate development and publish rust-version" workflow. We could instead do the opposite, picking rust-version=rustc as a "safe" default for assuming the development rust-version. Users of the "shared development / publish rust-version" workflow could either set the config or use a rust-toolchain.toml file.

The reasons we didnn't go with this approach are

  • The user explicitly told us the MSRV for the project; we do not have the granularity for different MSRVs for different workflows (or features) and likely the complexity would not be worth it.
  • Split MSRV workflows are inherently more complex to support with more caveats of where they apply, making single MSRV workflows the path of least resistance for users.
  • Without configuration, defaulting to single MSRV workflows will lead to the least number of errors from cargo as the resulting lockfile is compatible with the split MSRV workflows.
  • Single MSRV workflows catch too-new API problems sooner
  • We want to encourage developing on the latest version of rustc/cargo to get all of the latest workflow improvements (e.g. error messages, sparse registry for cargo, etc), rather than lock people into the MSRV with rust-toolchain.toml
    • The toolchain is another type of dependency so this might seem contradictory but we feel the value-add of a new toolchain outweighs the cost while the value add of new dependencies doesn't

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 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 the split MSRV workflow

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.

Hard-error

Instead of preferring MSRV-compatible dependencies, the resolver could hard error if only MSRV-incompatible versions are available. This means that we would also backtrack on transitive dependencies, trying alternative versions of direct dependencies, which would create an MSRV-compatible Cargo.lock in more cases.

Nothing in this solution changes our ability to do this later.

However, blocking progress on this approach would greatly delay stabilization of this because of bad error messages. This was supported in 1.74 and 1.75 nightlies under -Zmsrv-policy and the biggest problem was in error reporting. The resolver acted as if the MSRV-incompatible versions don't exist so if there was no solution, the error message was confusing:

$ cargo +nightly update -Z msrv-policy
    Updating crates.io index
error: failed to select a version for the requirement `hashbrown = "^0.14"`
candidate versions found which didn't match: 0.14.2, 0.14.1, 0.14.0, ...
location searched: crates.io index
required by package `app v0.1.0 (/app)`
perhaps a crate was updated and forgotten to be re-vendored?

It would also be a breaking change to hard-error. We'd need to provide a way for some people to opt-in while some people opt-out and remember that. We could add a sticky flag to Cargo.lock though that could also be confusing, see "Configuring the resolver mode on the command-line or Cargo.toml".

This would also error or pick lower versions more than it needs to when a workspace contains multiple MSRVs. We'd want to extend the resolver to treat Rust as yet another dependency and turn package.rust-version into dependencies on Rust. This could cause a lot more backtracking which could negatively affect resolver performance for people with lower MSRVs.

If no package.rust-version is specified, we wouldn't want to fallback to the version of rustc being used because that could cause Cargo.lock churn if contributors are on different Rust versions.

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

Unresolved questions

The config field is fairly rought

  • The location (within build) needs more consideration
  • The name isn't very clear
  • The values are awkward

Future possibilities

Improve the experience with lack of rust-version

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 could encourage people to set their MSRV by having cargo new default package.rust-version. However, if people aren't committed to verifying it, 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.

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 workaround it is to set CARGO_BUILD_RESOLVER_PRECEDENCE=maximum which will then lose all other protections.

When missing, cargo publish could inject based on the rustup toolchain file. However, this will err on the side of a higher MSRV than necessary as well.

When missing, cargo publish could inject package.rust-version inferred from package.edition and/or other Cargo.toml fields. However, this will err on the side of too low of an MSRV. While this might help with in this situation, it would lock us in to inaccurate information which might limit what analysis we could do in the future.

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.

On the resolver side, we 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.

Integrate cargo audit

If we integrate cargo audit, we can better help users on older dependencies identify security vulnerabilities.

"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 install.resolver.precedence, mirroring build.resolver.precedence.

See rust-lang/cargo#10903 for more discussion.

Note: rust-lang/cago#12798 (slated to be 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.

build.rust-version = "<x>.<y>"

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 Consider supporting MSRV in rustup · Issue #1484 · rust-lang/rustup · GitHub

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 lanuage) 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.

24 Likes

PR 12950 was just merged which will transition nightlies using -Zmsrv-policy from the prototype hard-error solution (mentioned under Alternatives) to the solution proposed in this Pre RFC.

It may take a few days for that to percolate to the nighties.

--ignore-rust-version and cargo update output changes are the only parts not available under -Zmsrv-policy (I think).

Sounds like a great improvement to cargo's behaviour, it's always been weird how little msrv annotations were used. I agree we should be trying to get away from "updating your msrv should be considered a major breaking change" because it's an absolutely toxic policy to language/ecosystem improvements (especially since, as you say, rust's toolchain upgrades are so easy that they make my poor release-engineer heart sing). This proposal seems like a good way to help everyone win!

In fact this proposal seems so straightforward that after writing several versions of this reply I realized it was mostly irrelevant rambling, and deleted most of it. What remains is basically just "vibe checking the context that implicitly hangs around this proposal that most people don't know". :smile:

As there is no workspace.rust-version

...is there a plan to change that?

Checking that the state of the art hasn't substantially changed since last I looked, in case it significantly alters the Implications of this proposal:

  • this is only keying off of the literal msrv field in Cargo.toml
  • cargo will never add msrv to a crate for you, so by default most crates will have no defined msrv
  • this does not look at rust-toolchain.toml (I assume cargo/rustc "aren't allowed" to look at that)
  • nothing here will be particularly aware of rust #[stable] annotations
  • no system in cargo ever persistently records "well i once saw a succesful build with this version"

Assuming all of those are still the case, this proposal seems relatively inoffensive, in that it will be a pretty niche power-user feature that most people won't even notice.

I also think changing the behaviour of cargo-install will be fine, --locked is the "I care about reliability" solution, everything else is just rolling opportunistic chaos already.

2 Likes

I wasn't even sure if I should bother with an RFC (opting for a major-change-proposal instead) but, in talking with others, I felt it would be good to make sure something this fundamental should get wider visibility, even if its straightforward.

Not at this time and the longer term, hard-error, plan calls for respecting the MSRV of the individual packages so a workspace-wide one might get in the way.

That said, if you set workspace.package.rust-version, cargo new will automatically add package.rust-version.workspace = true for you.

Pretty much.

I did call out the possibility of rustup telling us the rustoolchain is being used in the proposed (where it would likely help) and alternate solution (where it would likely cause problems).

I'd love a clippy lint that would let you know if you are using too-new of an API item from std but have had priorities elsewhere, like this. I think "compat" lint category and related lints? · Issue #6324 · rust-lang/rust-clippy · GitHub is the issue.

Anyone want to take this on?

1 Like

I do have a minor concern. I'm imagining a user who has an established slow-moving crate that uses MSRV. Being established in slow-moving, the maintainers don't pay a lot of attention to changes in the latest version of Cargo/Rust, so they don't really notice the discussions here. To maintain their MSRV policy they have two CI runs one that checks with latest and one that checks with their MSRV compatible lock file. That first CI run does update or generate-lockfile followed by a build. When this RFC stabilizes, that CI run goes from checking latest to checking MSRV compatible. Which feels like a breaking change that should be explored/acknowledged in the text.

7 Likes

As the author of the MSRV RFC I am really happy to see the progress on MSRV-aware resolver!

What about publishing crates without specified package.rust-version? I think that crates.io either should use user's rustc version (with warning on publishing using Nightly toolchain) or set rust-version to current stable version.

I am not sure about falling back by default to a version specified in rust-version of package being built. I think we should use toolchain version by default and specify the version in generated Cargo.lock. If a project with existing Cargo.lock will be built with an older toolchain (but with version higher than package's rust-version), user should get compilation error with suggestion to re-generate Cargo.lock.

In other words, if I use the latest stable toolchain to built a crate with rust-version equal to 1.56, I expect to get the freshest versions of compatible dependencies, not older ones frozen for 1.56. Same for toolchains in the middle, if I use Rust 1.65, I expect to get the freshest dependencies compatible with Rust 1.65.

I think the feature would have an opposite effect. Crate maintainers who were conservative with MSRV bumps would be much more willing to bump MSRV after the feature lands on stable. For example, in RustCrypto we have a strict MSRV policy (MSRV bumps are effectively breaking changes for pre-1.0 crates), so our hashes crates still have MSRV 1.41. Lack of MSRV-aware resolver is one of blocking issues on the path to 1.0 release of our crates.

After MSRV-aware resolver will land on stable and we will bump crates MSRV to a version equal or greater, I will be much more comfortable with bumping MSRV in non-breaking releases.

1 Like

Having a just published crate not able to be used by any stable toolchain with just a post-publish warning would be bad. I feel this sort of behavior should be opt-in because it is a big ecosystem shift, either something like package.rust-version = "auto-detect" or cargo publish --set-rust-version.

1 Like

Editions are the biggest driver of incompatibilities, so for crates without a rust-version Cargo/crates.io could at least translate edition into MSRV (I mean when publishing, in the transformed Cargo.toml in the package).

3 Likes

Yes. I was not clear enough, I meant that Nightly cargo should deny publishing packages without an explicitly provided flag. But in the future edition we probably should make the rust-version field mandatory with auto-detect "lazy" option.

Remember that you still can publish packages with pre-2018 edition, but with MSRV higher than a relatively recent stable (e.g. 1.70). This is why I think we should use toolchain version used for publishing.

1 Like

More generally, “default to rustc-version from workspace” changes ecosystem dynamics quite a bit, as we get less cross-testing of the HEAD versions of crate.

Eg, if I have some rustc-version specified in my crate.io crate and run cargo test on CI, I might be skipping the latest versions of my dependencies. So when someone else uses my crate in their project wihout rustc-version, they get the latest version of my dependency, and might be the first one to figure out that the combination doesn’t actually work.

This means that, to keep on top of my deps, I need to add —ignore-rustc-version to my CI

But we already somewhat gave up on the “test at HEAD” property with the recent retraction of the recommendation to not commit lock file for libraries, so perhaps that’s where the ecosystem goes anyway?


Consider the following approach for the “alternatives” section:

Do not change existing behavior. Add a new global cargo flag —rust-version, which would cause all commands generating a lockfile to skip known-incompatible crates.

That is, don’t guess what the user wants, require them to pass the version they need explicitly, if they need anything less than “latest stable”. This adds friction to using older Rust, but I think that was our historic policy anyway (e.g., we only support latest Rust and don’t do LTS)

What do you mean by "latest stable"? Is it currently used toolchain version? The freshest toolchain available locally? Stable version calculated based on release scheldule?

If it's the first option (which looks like the only sane one), I don't think it will introduce any friction. On an older (but MSRV-compatible) stable toolchain you would automatically get dependencies for your version. The only issues is that switching between toolchains may result in re-generation of Cargo.lock on each switch.

I've updated my proposal based on feedback

Sorry for not capturing this originally. Its now there!

In a way, the user told us what they want by setting package.rust-version, we just haven't been helping them as much as we should.

3 Likes

I've expanded on this in the Future Possibilities. I deferred this out as I don't think solving that problem is a blocker for this to move forward and I want to keep the scope small.

1 Like

There are two workflows I've seen around MSRV

  • Always be compatible with MSRV
  • Develop with latest dependencies but keep an MSRV lockfile.

The proposal aims for the first but it sounds like you want the second.

I think always compatible with MSRV is a safe default choice. I have extended the proposal with build.rust-version = <bool> so people can more easily opt into the other. I also added a future possibility for overriding the rust version used via config.

I have toyed with the idea of tracking the intended rust version in Cargo.lock. I have that under the "hard error" alternative. There are usability concerns around it that would bog down RFC approval and stabilization and it would require a lockfile update which adds further delays for use. I think we can get away with what we have now without going down that route yet.

Sorry, I wasn’t clear enough. What I mean that behavior we have today is compatible only with “latest stable”, in a sense that using any older rustc and running cargo update might break your build. In the proposed alternative, we keep that behavior as is, and introduce a flag to opt-into MSRV resolver.

package.rust-version has different semantics it seems? For my crates, I set rust-version so that others can use them with older compilers, but I personally use stable or beta, and would prefer to get latest deps when working on my library crates, even if they are compatible with older versions.

To rephrase this, people who already upgrade their rust installs regularly would probably benefit from defaulting to —ignore-rust-version.

No, in RustCrypto we use neither. We push Cargo.lock into our repositories and we periodically run cargo update on it. The lock files track the latest dependencies. We test our crates on both stable and MSRV toolchains (we also test -Z minimal-versions, but it's not relevant to this discussion). We are mostly able to get away with a strict MSRV policy because we are quite conservative about using third-party dependencies.

Our CI config looks roughly like this:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        rust:
          - 1.65.0
          - stable
    steps:
      - uses: actions/checkout@v4
      - uses: RustCrypto/actions/cargo-cache@master
      - uses: dtolnay/rust-toolchain@master
        with:
          toolchain: ${{ matrix.rust }}
      - uses: RustCrypto/actions/cargo-hack-install@master
      - run: cargo hack test --feature-powerset

Your approach would test the same dependency tree for both rust values. I would say it's an unexpected behavior. I could enable --ignore-rust-version on for the stable toolchain, but it will be a more complex solution with a certain fragility.

rust-version states lowest Rust version with which the package could be compiled. I don't see why compiling on a more modern compiler should by default pull dependencies required for an older compiler, thus degrading user experience.

Let's look at it like this: rust-version is in a certain sense similar to a library dependency. Toolchain used for compilation effectively places an upper bound on this "dependency". Finally, role of resolver is to find a version of dependency tree which satisfies all requirements with freshest possible versions.

Why do we need an opt-in flag on older toolchains? Why a toolchain can not automatically use its own version to resolve dependencies? Users of the latest stable would get the same experience they get today and users of slightly older toolchains will not be constantly bombarded with compilation failures because of newer dependencies.

4 Likes

That is also a reasonable alternative! The difference would be the absence of opt-in and associated friction. There are two ways the user can solve their problem:

  • use newer rustc
  • use older dependencies

Explicitly passing rust-version makes the first solution the default one. Implicitly using current version instead picks the second by default.

2 Likes

This sounds much saner to me to. I have much the same setup in my CI for my bin crates (except only going for N-2 MSRV). But I would likely remove the field from Cargo.toml to get the old behaviour when building on the newest stable rust. Sure I could add --ignore-rust-version to get the latest dependency versions for building binaries in CI for github releases. But what about people who build locally themselves (either via a package manager such as pacman/makepkg on Arch, or via cargo install)? Or when I use the binary myself.

Using older dependencies than needed on newer releases should probably be opt-in.

I am not sure I understand what is the "friction" in this scenario. You obviously can not use a library or a dependency which uses features from a version of the compiler newer than you have. We probably should warn users on older toolchains that they use outdated dependencies because of their toolchain, but I don't think we should make them jump through unnecessary hoops to fix compilation of a project which was perfectly compilable on their slightly outdated toolchain just recently.

It's still may be useful to have an opt-in for a version different from a currently used one. For example, it could be used to check that dependency tree is resolvable for compiler versions between MSRV and the latest stable. But I think it's a fairly niche feature.

The original MSRV RFC described (maybe somewhat poorly) exactly this approach in the "future work" section:

For example, let's imagine that your crate depends on crate foo with 10 published versions from 0.1.0 to 0.1.9, in versions from 0.1.0 to 0.1.5 rust field in the Cargo.toml sent to crates.io equals to "1.30" and for others to "1.40". Now if you'll build your project with e.g. Rust 1.33, cargo will select foo v0.1.5. foo v0.1.9 will be selected only if you'll build your project with Rust 1.40 or higher. But if you'll try to build your project with Rust 1.29 cargo will issue an error.

UPD: @matklad sorry, I though I was replying to @epage. :slight_smile: