renovatebot / renovate

Home of the Renovate CLI: Cross-platform Dependency Automation by Mend.io
https://mend.io/renovate
GNU Affero General Public License v3.0
17.19k stars 2.25k forks source link

Extract rust-version as rust constraint from cargo.toml if present #26314

Open rarkins opened 9 months ago

rarkins commented 9 months ago

Describe the proposed change(s).

Ref: https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field

This should be extracted both as a dependency, as well as a constraint. In both cases it refers to the minimum version supported, similar to go.mod's go directive.

The dependency should not be bumped by default.

epage commented 9 months ago

There is another source for a potential rust constraint. rustup is a rust version manager and supports a rust-toolchain file which lets maintainers change which version of rustc / cargo gets run by default.

rust-version is more likely to be set by libraries being published to crates.io while rust-toolchain.toml is more likely to be used by applications that need validation before upgrading their version of rustc.

rust-toolchain.toml can be used in more advanced cases (nightly, custom toolchains) which would make it hard or impossible for your to extract a constraint.

I expect that not all maintainers with a rust-toolchain.toml file will want the constraint applied (though assuming they do is likely a safe default).

I'm leaving this note here as I'm unsure if you'd prefer to keep this in one Issue or split it to a separate issue.

rarkins commented 9 months ago

My assumptions had been that:

  1. rust-version in a Cargo.toml is meant to indicate compatibility and for libraries it's ideally as low (wide) as possible.
  2. Any rustup definition is meant to indicate the exact version which developers/CI should use when installing, testing and running. When in use it's likely a high/recent/latest version.

When it comes to rust-version, we shouldn't "bump" it by default, certainly not to latest. This would immediately make the library less compatible for downstream users, and potentially should even be considered a breaking change. Note: in the JS ecosystem, libraries usually do a major semver release when they drop support for older versions of Node.

It might be useful though for Renovate to bump rust-version to the earliest supported version of Rust, if that's a thing? e.g. like in Node, v16 is now unsupported so v18 is the earliest supported version.

Next, we need to think what to do with these rust versions.

For rustup, I think that's easy - that's the version we should install/use when running cargo commands.

For rust-version, my proposal is that's the constraint we can use to determine if updates of dependencies are compatible. e.g. if a library has rust-version=1.61 and one of its dependencies has rust-version=1.60, but a newer version of the dependency has rust-version=1.62 then that's not compatible, because it does not satisfy 1.61.

epage commented 9 months ago

Any rustup definition is meant to indicate the exact version which developers/CI should use when installing, testing and running. When in use it's likely a high/recent/latest version.

Depending on the domain, application authors have little reason to set package.rust-version (only matters for cargo install) and they may set rust-toolchain.toml file to describe the only version of rust they support (not bothering with older; newer requires a full re-validation). For example, they may be developing with the certified Rust toolchain, Ferrocene.

Note: in the JS ecosystem, libraries usually do a major semver release when they drop support for older versions of Node.

We generally recommend against that in Rust (semver guidelines).

It might be useful though for Renovate to bump rust-version to the earliest supported version of Rust, if that's a thing? e.g. like in Node, v16 is now unsupported so v18 is the earliest supported version.

At this time, the Rust Project only supports the latest toolchain release. For anything else, they are expected to either upgrade or get support (ie backporting security fixes) from their vendor (like Ferrocene)

For rust-version, my proposal is that's the constraint we can use to determine if updates of dependencies are compatible. e.g. if a library has rust-version=1.61 and one of its dependencies has rust-version=1.60, but a newer version of the dependency has rust-version=1.62 then that's not compatible, because it does not satisfy 1.61.

I agree though I would fallback to using rust-toolchain.toml for the constraint if package.rust-version is not present in the for the reasons given above. This would match the current iteration of the RFC for how we'll handle this stuff internally.

The question remains as for what to do with dependencies without a package.rust-version for checking compatibility. Currently, the RFC sorts dependency versions as:

  1. Most compatible first with highest version among most compatible
  2. Highest version among no package.rust-version
  3. Highest version among incompatible

We do want to explore finding good fallbacks for package.rust-version, like tracking what version of the toolchain was used when publishing the package (this would end if in the dependency data source, maybe as a new field).

In both cases, the benefit of matching what we eventually stabilize (the end goal of the RFC) is that users will get a consistent experience which will make it more predictable.

rarkins commented 9 months ago

Any rustup definition is meant to indicate the exact version which developers/CI should use when installing, testing and running. When in use it's likely a high/recent/latest version.

Depending on the domain, application authors have little reason to set package.rust-version (only matters for cargo install) and they may set rust-toolchain.toml file to describe the only version of rust they support (not bothering with older; newer requires a full re-validation). For example, they may be developing with the certified Rust toolchain, Ferrocene.

This makes sense to me. As an application developer supporting multiple language versions adds work but no additional functionality.

Note: in the JS ecosystem, libraries usually do a major semver release when they drop support for older versions of Node.

We generally recommend against that in Rust (semver guidelines).

I don't think the Rust ecosystem can consider itself genuinely semver-compliant though if minor releases can knowingly break things. I don't mind, but I think the Node.js ecosystem has it right here.

It might be useful though for Renovate to bump rust-version to the earliest supported version of Rust, if that's a thing? e.g. like in Node, v16 is now unsupported so v18 is the earliest supported version.

At this time, the Rust Project only supports the latest toolchain release. For anything else, they are expected to either upgrade or get support (ie backporting security fixes) from their vendor (like Ferrocene)

There seems like there should be a difference between "we won't backport features or fixes" versus "the instant we release a new version, the old ones are EOL". In software it's useful to know when you're running EOL software, but if users get ominous warnings about a Rust version which was perfect 10 seconds earlier then it will numb them to start ignoring all such warnings.

Anyway, it does seem like Renovate should bump Rust version in package files or rust toolchains by default, if that's the way the Rust ecosystem works? (unlike Go, where it would annoy people)

I agree though I would fallback to using rust-toolchain.toml for the constraint if package.rust-version is not present in the for the reasons given above. This would match the current iteration of the RFC for how we'll handle this stuff internally.

Does the following make sense?

In case both rust-toolchain and rust-version are present, which should we use? I would think it might happen for libraries where:

The does rust-version mean "version X or later" while rust-toolchain means "exactly this version"?

Ultimately for Renovate, we just want to end up with a "constraint" which tells us "this project needs dependencies satisfying this constraint".

The question remains as for what to do with dependencies without a package.rust-version for checking compatibility. Currently, the RFC sorts dependency versions as:

  1. Most compatible first with highest version among most compatible

What does "most compatible" mean?

  1. Highest version among no package.rust-version
  2. Highest version among incompatible
epage commented 8 months ago

Anyway, it does seem like Renovate should bump Rust version in package files or rust toolchains by default, if that's the way the Rust ecosystem works? (unlike Go, where it would annoy people)

There are a lot of opinions on the updating of Rust versions in package files and rust toolchain files such that it likely shouldn't be updated by default.

In case both rust-toolchain and rust-version are present, which should we use? I would think it might happen for libraries where:

package.rust-version should have precedence over rust toolchain files.

What does "most compatible" mean?

Sorry, that should just read "Any compatible"

FreezyLemon commented 7 months ago

Anyway, it does seem like Renovate should bump Rust version in package files or rust toolchains by default, if that's the way the Rust ecosystem works? (unlike Go, where it would annoy people)

Personally, I would prefer Renovate to just search for rust-version-compatible dependency upgrades by default, and not touch the Cargo.toml file. Both ways are arguably fine, but I think that library authors who specify a minimum supported rust-version (it's not required) generally don't want to increase this version without at least some consideration for the compatibility implications.

Either way, (and this is from the perspective of a user that doesn't know much about the internals of Renovate), it would be useful to have very explicit information about this. Something like "The minimum supported Rust version in Cargo.toml was increased from 1.60 to 1.70 to allow upgrading x, y, and z." and "x could not be upgraded from 1.2.3 to 1.2.4 because it requires a Rust version of 1.70, which is higher than this crate's minimum Rust version of 1.65."

If this information is provided somehow, the workflow could look like this (which would be quite nice IMO):

Unrelated: There's a significant number of projects (usually bigger ones) that have an explicit "MSRV policy" (e.g. "support at least the last 6 months of Rust releases"). Not sure if it's reasonably possible to account for something like this.

epage commented 7 months ago

There are two elements to an MSRV policy

Usually the community is focused on one or the other and sometimes people try to use a grace period to emulate support for a fixed version with mixed results.

So figuring out how to support an MSRV policy can be complicated.

Currently I use a grace period and rely on minimumReleaseAge to get that. I've been considering supporting fixed versions and creating repos that just proxy Rust releases but only when certain criteria are met (N%5=0, Debian stable was updated, etc) and so I could point RennovateBot at that.

rarkins commented 6 months ago

This issue has diverted a little, and I don't want it to get left behind.

If a Cargo.toml defines a rust version, we should do this:

We also need to decide what is our source of truth for Rust versions. For containerbase today we use the "rust" library image in Docker. Is that also suitable here or should we have a different source of truth for what is a valid/released Rust version?

FreezyLemon commented 6 months ago

Skimming through the Docker tags for the rust image, it looks like the docker releases are usually a few days/weeks after the real release. I measured the real release date by looking at the announcement posts on the Rust blog ("Announcing Rust 1.XY.Z"). A source that is probably easier to read programmatically is the rust-lang/rust git tags.

rarkins commented 6 months ago

On the other hand if Renovate discovers new rust releases almost immediately then the proposed update might fail in the user's environment if the tooling they use to install rust doesn't source directly from the GitHub repository either and also has hours or days delays.

epage commented 6 months ago

I currently use github releases as my source of truth for rust releases, see https://github.com/clap-rs/clap/blob/690f5557d7f25904c31ec9f2a3c3657cbb68c98e/.github/renovate.json5#L26-L27

In general, updating of package.rust-version shouldn't be done by default. If the user wants to stay on "latest", the now approved RFC includes support for a package.rust-version = "current" (field value is not finalized), negating the need for immediate updates. Most likely, people will have minimumReleaseAge set, so the difference between whether the official release is out and their chosen form of infrastructure is updated is not likely to be a problem.