Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release-0.13.x] fix(CI): Commit an MSRV compatible Cargo.lock #492

Closed
wants to merge 3 commits into from

Conversation

ijackson
Copy link
Contributor

See individual commit messages for rationale, and how I made this lockfile.

We want a later version than 1.0 since earlier versions have various
bugs, and also dependency versions which are insufficient to rule out
bugs in lower crates.

Emprically, 1.30 requires a newer Rust than our MSRV.
Therefore, request 1.29.

Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
This will allow us to pin the dependencies in CI, avoiding
uncontrolled changes from our dependencies causing our workflows to
break.

Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
This is not so trivial.  In theory,
    cargo +nightly update -Z minimal-versions generate-lockfile
ought to work.

However, there are rather many places in our dependency tree where
some crate A depends on crate B, version V, but it actualloy doesn't
build with version V, but requires something considerably newer.
There seemed to be particular problems in the parts of the dependency
graph near "pest".  My atteempts at starting with the minimal versions
and upgrading crates as needed, were not successful.

However, I was able to converge reasonably quickly from the other end:
starting with the lockfile generated by 1.59, using recent versions,
and then repeatedly downgrading crates whose (actual, or declared)
MSRV was too high.

the following recipe generates a working lockfile:
    cargo +1.59 generate-lockfile
    cargo +nightly update -Z minimal-versions -p linux-raw-sys
    cargo +nightly update -Z minimal-versions -p tokio
    cargo +nightly update -Z minimal-versions -p rustix
    cargo +nightly update -Z minimal-versions -p tempfile
    cargo +nightly update -Z minimal-versions -p reqwest
    cargo +nightly update -Z minimal-versions -p byteorder
    cargo +nightly update -Z minimal-versions -p h2
    cargo +nightly update -Z minimal-versions -p log
    cargo +nightly update -Z minimal-versions -p tokio
    cargo +nightly update -Z minimal-versions -p tokio-util@0.7.9

So that is what we commit here.

Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
@ijackson
Copy link
Contributor Author

I put the recipe for generate the lockfile in the commit message, rather than in a script to run from CI, because, sadly, it will rot. As newer dependencies get published, the list of things to downgrade will grow (and possibly things will get more complicated).

The -Z minimal-versions generate-lockfile approach wouldn't have suffered from that problem.

Copy link
Member

@matthiasbeyer matthiasbeyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love the detailed commit messages, thank you very much for this awesome PR!

I have one question left, that does not block this PR though.

@@ -41,7 +41,7 @@ pathdiff = "0.2"
serde_derive = "1.0.8"
float-cmp = "0.9"
chrono = { version = "0.4", features = ["serde"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "io-util", "time"]}
tokio = { version = "1.29", features = ["rt-multi-thread", "macros", "fs", "io-util", "time"]}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really necessary? I wouldn't mind this change at all on master, but I am way more conservative with patchreleases.

I wouldn't argue too hard here, just want to make sure that you really think it is necessary here...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH that was from my attempts at the other approach. I'm trying it without.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thank you!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is going to work. Should I rebase this branch without that commit? I then need to rerun the generation script which, in practice, ends up with a Cargo.lock using tokio 1.28 rather than 1.29, so the tip will be different.

Alternatively I could push a revert of the tokio dep change, and leave the lockfile alone.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I expected my test passed. (https://github.com/ijackson/config-rs/actions/runs/6629032741, but that branch is ... rather odd.) So we should drop the change to tokio in Cargo.toml.

Should we (1) ship the Cargo.lock from this MR which was made with the tokio change using the recipe in the commit message (ie, git revert the tokio dependency commit), or (2) ship a Cargo.lock which was made without the tokio change, for somewhat greater coherency?

If 1, feel free to push that revert yourself. It should pass CI and we can merge this.

If 2, LMK and I'll prepare the branch and force push. (Or you can do it, e.g. to verify my work....)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm rebasing I'll fix the commit message typos @polarathene spotted. If we're doing the "revert" approach, I could still do that. Please let me know.

FTAOD I think either of the approaches above are fine and there is little to choose between them.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to emphasize, if you don't revert this change (or use a min version of 1.27 instead), expect the fragile tokio-util pinning requirement.

Alternatively, raising warp will remove the 0.6 series of tokio-util, making it easier to pin tokio-util without a version ref.

  • 0.7.8 is compatible with Rust 1.56.0 and will resolve to that implicitly if tokio is < 1.28. Otherwise it requires Rust 1.60.
  • Or you can take the only pin some deps to their minimal versions approach instead that's been proposed. Even though that does not accurately represent using the latest compatible version possible for bugfixes / security patches that users on the MSRV likely would want and tests should be intended for ensuring is valid? 🤷‍♂️

@polarathene

This comment was marked as outdated.

@polarathene
Copy link
Collaborator

polarathene commented Oct 24, 2023

I put the recipe for generate the lockfile in the commit message, rather than in a script to run from CI, because, sadly, it will rot.
As newer dependencies get published, the list of things to downgrade will grow (and possibly things will get more complicated).

Assuming the MSRV is intended as 1.56.1, it would seem more reliable to use cargo-msrv (as I have suggested earlier) to determine this than the approach you've noted will be a maintenance burden?

Comment on lines +41 to +43
[[package]]
name = "async-trait"
version = "0.1.74"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just did cargo +1.55.0 check and it failed due to this dependency like cargo-msrv did. So it'd seem that the expected MSRV of this Cargo.lock is 1.56.1 after all?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused, where are you getting 1.55 from? I think the MSRV of the Cargo.lock I'm proposing is 1.56 (probably 1.56.0, although one should be using 1.56.1 anyway), and 1.56 is what's mentioned in the docs.

Copy link
Collaborator

@polarathene polarathene Oct 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I didn't see this reply earlier, 1.55 was from the cargo-msrv bisect I think, or it was just due to it settling on 1.56.1, so I tried 1.55.0 to check if it builds, but it failed as mentioned due to async-trait.

As you cleared up for me, tokio wasn't an issue at 1.33.0 because dev-dependencies were ignored unless running cargo check --tests, so the failure was only regarding a release build (_without dev-dependencies tokio isn't present in cargo tree / Cargo.lock either 👍 )

Comment on lines +1559 to +1561
[[package]]
name = "tokio"
version = "1.29.0"
Copy link
Collaborator

@polarathene polarathene Oct 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to know how 1.33.0 is not compatible with an MSRV of 1.56.1?

EDIT: Because cargo +1.56.1 check --tests fails, MSRV for tokio 1.33.0 is 1.63.0.

@ijackson
Copy link
Contributor Author

Do I understand this correctly, instead of installing the semver version in Cargo.toml as the minimum supported dependency, preferring the newest in semver, you're generating a lock with the minimal version?

You note though that this is a bit tricky to manage this way, and the main goal is to ensure the Cargo.lock respects a supported MSRV?

I'm not quite sure I follow the question but I will try to answer it anyway :-).

The problem I am trying to solve with this MR is that CI is failing with the declared MSRV of 1.56. We want to test our MSRV in CI as otherwise it's almost certain we'll accidentally violate it.

Proper approach (rejected)

In principle the correct solution to testing with the MSRV is as follows:

  1. Make sure that all dependency definitions are accurate: specifically, that we can actually build with the earliest version of each dependency, that is permitted by the relevant Cargo.tomls.
  2. cargo +nightly update -Z minimal-versions to generate a lockfile using the lowest version permitted by our Cargo.toml

This is what I do in (for example) derive-adhoc.

However, 1 must be done transitively: if any of our dependencies have too-relaxed versions, then we need to get the dependency updated; and we would then need to update our dependency edge to declare a requirement on the version which is fixed. And this applies all the way to the bottom of the stack.

This might involve patch releases of multiple crates throughout our dependency stack. My investigations (or random flailing, depending how you look at it) led me to suspect that this proper approach wouldn't yield success, at least not for the 0.13.x release branch of config-rs.

It's easier to do this kind of cleanup to all of our upstreams if we'd be working on their mainlines. But, the job is a big one and might involve starting at the bottom. Realistically, a good place to start would be to try to get pest to build with -Z minimal-versions.

Minor bodging (attempted, failed)

The next most principled approach is to generate a lockfile with -Z minimal-versions and then update individual crates (deep in our stack) that, in practice, are broken. (For direct dependencies, we could easily, and should, just change our Cargo.toml.)

I tried this. But it got badly out of hand.

Major bodging

So instead, what I did was, by hook or by crook, generate a Cargo.lock that can be used to test that we do build with our MSRV. That will prevent us from adding MSRV breaking to config-rs.

Downstreams with a comparable MSRV might have to redo some of this work. My particular prompt is the Arti project, where we have an MSRV of 1.65, and we have already adopted the "minor bodging" approach described above. In practice that seems to be working for us.

But, at least in a release branch, I don't think we will often want to update our Cargo.lock in CI.

@ijackson
Copy link
Contributor Author

@polarathene

Assuming the MSRV is intended as 1.56.1, it would seem more reliable to use cargo-msrv (as I have suggested earlier) to determine this than the approach you've noted will be a maintenance burden?

Sorry for not replying to that suggestion earlier. I had read it, but it didn't seem directly on-point. Let me explain why. (I have read enough of the cargo-msrv docs to feel I know what it does.)

Firstly, I don't think we are trying to determine our MSRV. We are trying to assure that we do not break our intended MSRV. The basic principle is that we should advance our MSRV deliberately, and ideally avoid doing that on a release branch.

To do that, we do not need to build and test with many different toolchains. Nor do we need to inspect the rust-version value in any of our (transitive) dependencies. (Those are the things cargo-msrv can do that aren't easy without it.)

We just need to test with our intended MSRV. If that passes, then the Rust projects' compatibility guarantees mean that we should build with later versions too (and, in practice, that always is the case).

And, whether we use cargo-msrv or just test with our MSRV in CI, we still need a suitable lockfile. Since cargo dosn't do MSRV-aware resolution, this is not straightforward.

I am not sure what the issue was with the tokio version? With an MSRV of 1.56.1 it is compatible with tokio v1.33.0? 🤔

According to Tokio's README the MSRV of Tokio 1.30 onwards is 1.63.

Empirically, cargo +1.59 clippy --locked --offline --all-targets --all-features -- -D warnings failed with Tokio 1.30.

Note that in order to properly check our MSRV, the CI doesn't just build the package. It runs the tests too. I think this is correct.

@polarathene

This comment was marked as outdated.

@ijackson
Copy link
Contributor Author

It seems that when you throw tests into the mix that is not the case, and I could not seem to find much information at a quick glance with some search queries if MSRV is meant to also cover dev-dependencies, or only the release crate that would be pulled in as a dependency to other projects. I'd assume that's what MSRV is generally intended for though?

Downstreams don't run the tests so they might be OK with it just building. But we want to detect semantic as well as syntactic problems in our CI. That will help assure that downstream users get not only a build that completes, but also working programs.

Declaring the MSRV with rust-version in the Cargo.toml is probably a good idea, but IMO (i) probably not on the stable release branch, where we should avoid unecessary changes (and especially, build system changes) and (ii) even on the main branch, not until we have checked that the documented MSRV isn't unecessarily high (since adding the rust-version will break things that might have previously been working).

@ijackson
Copy link
Contributor Author

@matthiasbeyer I think this is just blocking on your decision about #492 (comment)

@matthiasbeyer
Copy link
Member

No time to read this all now, because I am traveling until Sunday. Just wanted to say that I don't mind typos in commit messages or doc comments! So these should not block merges! I don't think anyone intended to block a merge here because of commits, I just wanted this to be noted so everyone knows! 😆

Thanks for your contributions, all of you! I hope I can read through all of the above next week.

@polarathene

This comment was marked as off-topic.

@polarathene
Copy link
Collaborator

Been writing this up for a while, fairly tedious and I don't expect anyone else to read through it all, but it serves as documentation on issues being tackled/discussed by the PR.

  • Might be a bit disorganized, I don't really have the time to justify revising it further 😅
  • A shorter summary / review comment will eventually follow with @ pings for going forward.
  • There is still some remaining concerns to cover such as dependabot / renovatebot being said to treat a lib project as an app project when Cargo.lock exists, which adjusts how the bots engage with PRs which may not be desirable.
  • There is a TL;DR summary of this particular comment, and a follow-up comment responding to earlier feedback. Neither is the "shorter summary / review comment" planned.

json5 (pest) with -Z minimal-versions.

Comment:

Realistically, a good place to start would be to try to get pest to build with -Z minimal-versions.

This actually seems to be due to json5 (parent dependency) pulling in pest allowing a semver with minimum versions that are incompatible with the MSRV of config-rs?

The json5 crate does not appear to be actively maintained anymore, so trying to address that may not be successful?

  • Last commit Sep 2021.
  • Since then, the maintainer appears to have only engaged recently in Aug 2023 with a brief comment on a PR (that renames the license file).
  • They don't seem to have participated in any other activity on the repo for 2 years? 🤔

Perhaps the issue does bubble further down into the pest crates.. EDIT: pest_meta min version was the problem

  • pest deps chain resolve workspace pest deps via path with 2.0 min bound (which is problematic for -Z minimal versions on pest_meta).
  • So that is not resolvable just via adjusting json5 (other than perhaps raising the min bound even higher, but json5 could otherwise support as low of an MSRV as Rust 1.31, aka edition = "2018").

Nightly resolver options

-Z msrv-policy vs -Z minimal-versions:

  • cargo +nightly update -Z msrv-policy (paired with rust-version = "1.56.0" in Cargo.toml and json5 = "0.4"), you'll find it resolves pest to 2.5.7 and serde to 1.0.190.
  • cargo +nightly update -Z minimal-versions will resolve to pest 2.0.0 and serde 1.0.0 (unrelated to config-rs, but a minimal Cargo.toml for isolating resolver scope to json5). Not as useful.

You could manage minimal versions for only direct dependencies (only those directly in our Cargo.toml) with cargo +nightly update -Z direct-minimal-versions?

  • However, since that is using +nightly toolchain it resolves the semver of implicit dependencies for that toolchain, which is problematic since -Z only works with nightly (EDIT: Partially a valid concern, only because of the later mentioned Rust < 1.60.0 resolver behaviour issue).
Hidden as outdated

json5 will also resolve to an MSRV compatible release with cargo +1.56.1 update that in this case is equivalent (EDIT: This was due to Rust < 1.60.0 resolver incompatibility) to cargo +nightly update -Z msrv-policy (when rust-version = "1.56.1" is set in Cargo.toml):

# Only uses minimal versions declared in Cargo.toml,
# resolves remaining implicit deps normally for the specified toolchain (nightly):
$ cargo +nightly update -Z direct-minimal-versions

# Pest version is too new:
$ cargo tree
app v0.1.0 (/app)
└── json5 v0.4.0
    ├── pest v2.7.5
    │   ├── memchr v2.6.4
    │   ├── thiserror v1.0.50
    │   │   └── thiserror-impl v1.0.50 (proc-macro)
    │   │       ├── proc-macro2 v1.0.69
    │   │       │   └── unicode-ident v1.0.12
    │   │       ├── quote v1.0.33
    │   │       │   └── proc-macro2 v1.0.69 (*)
    │   │       └── syn v2.0.38
    │   │           ├── proc-macro2 v1.0.69 (*)
    │   │           ├── quote v1.0.33 (*)
    │   │           └── unicode-ident v1.0.12
    │   └── ucd-trie v0.1.6
    ├── pest_derive v2.7.5 (proc-macro)
    │   ├── pest v2.7.5 (*)
    │   └── pest_generator v2.7.5
    │       ├── pest v2.7.5 (*)
    │       ├── pest_meta v2.7.5
    │       │   ├── once_cell v1.18.0
    │       │   └── pest v2.7.5 (*)
    │       │   [build-dependencies]
    │       │   └── sha2 v0.10.8
    │       │       ├── cfg-if v1.0.0
    │       │       ├── cpufeatures v0.2.11
    │       │       └── digest v0.10.7
    │       │           ├── block-buffer v0.10.4
    │       │           │   └── generic-array v0.14.7
    │       │           │       └── typenum v1.17.0
    │       │           │       [build-dependencies]
    │       │           │       └── version_check v0.9.4
    │       │           └── crypto-common v0.1.6
    │       │               ├── generic-array v0.14.7 (*)
    │       │               └── typenum v1.17.0
    │       ├── proc-macro2 v1.0.69 (*)
    │       ├── quote v1.0.33 (*)
    │       └── syn v2.0.38 (*)
    └── serde v1.0.190

# Failure:
$ cargo +1.56.1 check
    Updating crates.io index
error: failed to select a version for the requirement `pest = "=2.7.5"`
candidate versions found which didn't match: 2.5.7, 2.5.6, 2.5.5, ...
location searched: crates.io index
required by package `json5 v0.4.0`
    ... which satisfies dependency `json5 = "=0.4.0"` of package `app v0.1.0 (/app)`

# Compatible versions for the MSRV toolchain are resolved (best effort, not always reliable):
# EDIT: Disregard this, crates published with 1.60.0 Cargo.toml features are dismissed by the resolver
$ cargo +1.56.1 update
    Updating crates.io index
    Updating json5 v0.4.0 -> v0.4.1
    Removing memchr v2.6.4
    Updating once_cell v1.18.0 -> v1.17.2
    Updating pest v2.7.5 -> v2.5.7
    Updating pest_derive v2.7.5 -> v2.5.7
    Updating pest_generator v2.7.5 -> v2.5.7
    Updating pest_meta v2.7.5 -> v2.5.7

# No adjustments, (`-Z msrv-policy` resolver does not stop at parents with compatible `rust-version`):
# EDIT: "stop at parents" resolver behaviour was a misunderstanding on my part, as other edits point out
$ cargo +nightly update -Z msrv-policy
    Updating crates.io index
EDIT: Inaccurate as this was only true for `-Z msrv-policy`, see next section instead

pest 2.5.x declares rust-version = "1.56.0" (introduced in pest 2.2.0), this carries over to 2.6.x series (but that series was yanked), meanwhile 2.7.0 introduces an MSRV bump with rust-version = "1.60.0". This is why 2.5.7 is resolved despite the wider 2.0.0 semver range.

While the other sibling dependency (serde) currently resolves to 1.0.190 (latest release) also declares a 1.31.0 MSRV via rust-version that still appears compatible / valid (the bump was since 1.0.180, where it was previously Rust 1.19.0 for serde 1.0.179).


Breakdown of problematic dependency resolutions

json5 => pest

pest 2.5.7 is the last valid release resolved (ignoring the 2.6.x yanked series) due to this Cargo.toml line:

Minimal versions:

  • You can lower down to pest 2.0.2 and serde 1.0.69 for json5 0.4.0 to build successfully on Rust 1.56.1.
  • If you also lower the pest dep of ucd-trie to it's minimum with cargo +nightly update -Z minimal-versions --package ucd-trie (0.1.1, anything above 0.1.2 introduces MSRV blockers due to edition = "2018" / edition = "2021")
  • -Z minimal-versions applies all the way down the dependency chain, affecting it's reliability:
  • -Z direct-minimal-versions works a bit better, but as implicit dependencies resolve semver on nightly toolchain:
    • This does not stop at pest 2.5.7, and resolves to pest 2.7.5 which is not compatible on the MSRV of Rust 1.56.1.
    • Latest pest release (2.7.5) requires Rust 1.61.0.
# `-Z minimal-versions` compatibility for json5:

json5 = "0.4.0"
# json5 relies on these dep versions at a minimum:
pest = "2.0.2"
serde = "1.0.69"
# 2.0.0 min version brings in 1.x pest versions, avoid that:
pest_meta = "2.0.3"

# Pinning this dep additionally supports earlier than Rust 1.56 (down to 1.31):
# 0.1.3 requires 1.31 (edition 2018)
# 0.1.6 requires 1.56 (edition 2021)
#ucd-trie = "=0.1.2"

rust-ini => ordered-multimap => hashbrown => ahash

Meanwhile ahash 0.7.6 also adopted the dep: syntax feature too. ahash 0.7.6 got yanked, so 0.7.7 is the earliest and current release with that change.

And that is how the resolving happened 😮‍💨

  • hashbrown 0.12.3 is the last release that declared a rust-version of 1.56.0 before bumping it, but the 0.12.x series never raised it in response to ahash (not that it would help, as -Z msrv-policy would rollback to an earlier 0.12.3 that claims compatibility, but the newer ahash 0.7 dependency releases breaking that).
  • Technically due to dependencies like ahash (introducing MSRV bumps with the point releases) that's not really avoidable and affects earlier releases just the same. hashbrown has no control over its upstreams affecting semver in that way.
  • I think this still would have worked resolving an earlier 0.11.x / 0.12.x release of hashbrown:
    • If the ahash crate had not yanked earlier versions (without the Cargo.toml syntax change that Rust 1.56.1 cannot understand, but the yank was necessary AFAIK).
    • If it was a more typical MSRV incompatibility, this would not work regardless and is where rust-version becomes useful.

Latest rust-ini 0.20.0 bumps to ordered-multimap 0.7 which currently requires Rust 1.71.1 with no use of rust-version by either crate to guard against that. Meanwhile rust-ini 0.19.0 bumped to ordered-multimap 0.6 which has an MSRV of 1.64 (due to dlv-list 0.5).


TL;DR

I initially investigated with cargo-minimal-versions and learned about -Z msrv-policy as a better way of resolving a Cargo.lock that is compatible with the MSRV, but not foolproof (even with broader adoption, see the hashbrown 0.14.2 example at the link).

Thankfully pest has been declaring rust-version since their 2.2.0 release with 1.56.0, and bumping that appropriately to 1.60.0 for the 2.7.0 release.

  • Thus this resolves well with -Z msrv-policy.
  • Alternative nightly resolver options are not reliable with pest due to incompatible min bounds (-Z minimal-versions) or target MSRV being too old for implicit deps semver (-Z direct-minimal-versions) as both were noted earlier.

Unfortunately, since rust-version is not widely adopted yet, -Z msrv-policy can fail to resolve Cargo.lock in a compatible manner (as noted earlier with ahash).

Due to the resolver bug with crates that adopted 1.60.0 weak (?) or namespaced (dep:) feature syntax:

  • config-rs will resolve for cargo +1.56.1 check just fine without the nightly resolver options. -Z direct-minimal-versions may still be useful to test.
  • We'll likely encounter these issues in releases of config-rs that raise the MSRV from 1.56.1 where the resolver issue is no longer present blocking semver resolution for those 1.60.0 syntax feature crates.

Dev dependencies fail to resolve correctly for the intended MSRV of 1.56.1 however

  • This is mainly a concern for CI running tests to verify against the intended MSRV.
  • A common practice for this is to commit a Cargo.lock, or sometimes one that is tailored to the MSRV referred to as Cargo.lock.msrv.

@polarathene
Copy link
Collaborator

polarathene commented Nov 4, 2023

Comment:

I put the recipe for generate the lockfile in the commit message, rather than in a script to run from CI, because, sadly, it will rot.
As newer dependencies get published, the list of things to downgrade will grow (and possibly things will get more complicated).

The -Z minimal-versions generate-lockfile approach wouldn't have suffered from that problem.

Reference: Commit message with mentioned recipe

Commit: 5c4c72d

This is not so trivial. In theory, cargo +nightly update -Z minimal-versions generate-lockfile ought to work.

  • However, there are rather many places in our dependency tree where some crate A depends on crate B, version V, but it actually doesn't build with version V, but requires something considerably newer.
  • There seemed to be particular problems in the parts of the dependency graph near "pest". My attempts at starting with the minimal versions and upgrading crates as needed, were not successful.
  • However, I was able to converge reasonably quickly from the other end. Starting with the lockfile generated by 1.59, using recent versions, and then repeatedly downgrading crates whose (actual, or declared) MSRV was too high.

The following recipe generates a working lockfile:

cargo +1.59 generate-lockfile
cargo +nightly update -Z minimal-versions -p linux-raw-sys
cargo +nightly update -Z minimal-versions -p rustix
cargo +nightly update -Z minimal-versions -p tempfile
cargo +nightly update -Z minimal-versions -p reqwest
cargo +nightly update -Z minimal-versions -p byteorder
cargo +nightly update -Z minimal-versions -p h2
cargo +nightly update -Z minimal-versions -p log
cargo +nightly update -Z minimal-versions -p tokio
cargo +nightly update -Z minimal-versions -p tokio-util@0.7.9
  • tokio-util@0.7.9 change is too fragile, it has to match a version that is resolved at the time.
  • Avoidable if pinning h2 to a release before it bumped to tokio-util 0.7, but then that conflicts with hyper semver.. (EDIT: Unless tokio is below 1.28, which then resolves to compatible tokio-util 0.7.8)

With a slight adjustment to Cargo.toml (which still respects the MSRV constraints), we can then minimize this down to:

# Minimal versions in Cargo.toml only, resolve the rest by rust-version
cargo +nightly update -Z direct-minimal-versions -Z msrv-policy

# Pins required after `-Z msrv-policy` due to lack of implicit deps using rust-version:
# nom, warp => multipart, futures-util (many parent deps):
cargo update --package memchr --precise 2.5.0
# rust-ini:
cargo update --package ordered-multimap --precise 0.4.0

Review comment:

Is this really necessary? I wouldn't mind this change at all on master, but I am way more conservative with patch releases.

Reference: Associated commit with message

Earlier commit with tokio min version set to 1.29:

We want a later version than 1.0 since earlier versions have various bugs, and also dependency versions which are insufficient to rule out bugs in lower crates.

It's a dev dependency, so I really don't see any issue with that especially when it's within the MSRV.

It should be a minimum version of 1.27, but could be up to 1.29.1 (last supported tokio release for MSRV of 1.56). That would require some extra explicit pinning, thus preferring tokio ="1.27" is better. Next paragraph with bullet points explains why.

Unfortunately from tokio 1.28 the resolver will then allow an incompatibility due to h2 => tokio-util.

  • Pinning h2 0.3.12 would keep tokio-util on 0.6.x. While h2 could support up to 0.3.20.. the 0.7.x series of tokio-util introduced a change in 0.7.9 that is not compatible with rust-version = "1.56" - but that was not bumped upon package release.
  • Pinning tokio-util 0.7.8 would work too, but parents like warp => hyper can resolve to a release of hyper that raises the min semver of h2 to 0.3.17, but prior to warp 0.3.3 there is a direct dependency on tokio-util 0.6.x which would make cargo update for this transitive dependency a bit of a hassle over time (requires cargo update --package to reference the resolved tokio-util version to adjust via --precise).

warp can be bumped up to 0.3.5 which makes it easier to pin tokio-util 0.7.8:

  • The pinning could be avoided if using tokio = "1.27" in Cargo.toml paired with cargo +nightly update -Z direct-minimal-versions -Z msrv-policy.
  • We can build successfully with just -Z msrv-policy and adding the tokio-util 0.7.8 pin, that works well too and likely results in a better dependency tree.

Comment:

The problem I am trying to solve with this MR is that CI is failing with the declared MSRV of 1.56.
We want to test our MSRV in CI as otherwise it's almost certain we'll accidentally violate it.

I'm not sure if I agree with the need to run tests on the MSRV:

  • Are there any examples you know of where that would behave differently when the Cargo.lock ensures the main dependencies are compatible? If not, would it be wrong to take the approach of testing with dev-dependencies unsupported on MSRV until proven otherwise?
  • Probably valid for verifying that examples compile, but I'm a bit conflicted on the importance of the examples being constrained to the MSRV too 🤷‍♂️ (supporting a minimum version of rust to compile a crate is one thing, enforcing it on the examples and tests another)

Presently, the main dependencies build fine without a lockfile, but this is only because newer releases of some crates are ignored as the MSRV considers the Cargo.toml invalid. At some point an implicit dependency bump could break that and require maintaining a Cargo.lock that the downstream needs to manage to maintain the MSRV.

Many downstream aren't likely stuck on 1.56.1, so while that possibility could occur at some point, the impact on downstream users is unclear.

The main concern isn't so much upstreams breaking MSRV then:

  • As that can happen post-publishing of a release and the response to that is embracing Cargo.lock or alternatively raising MSRV.
  • The CI concern then is more that the project itself does not update Cargo.toml or introduce any changes into it's own source that would raise the MSRV accidentally.
    • Cargo.lock helps verify the crate can build on the MSRV constraint with a known set of compatible dependencies.
    • dev-dependencies are used for both examples and tests, but aren't part of the published crate for downstreams to use. MSRV could break there:
      • Upstreams that respect semver shouldn't break their public API.
      • Breakage from MSRV bumps tend to be from crates internally expecting to be built on a newer rust toolchain. Usually resolvable by new pinning/constraints for Cargo.lock.

Since running the examples on the target MSRV can require version pinning with Cargo.lock, the user would need to look at how we manage that (if we document it), or figure it out for themselves.

As opposed to keeping examples and tests independent of that MSRV constraint, and just focusing on correctness which is far less of a maintenance burden?

I've managed to satisfy the MSRV with reduced effort, but it appears quite a few implicit deps made changes in recent months that would have been missed if this was tackled before then, so I don't consider it worthwhile as opposed to an MSRV bump.


Proper approach (rejected)

Agree, this is not really feasible to resolve properly, not for MSRV of 1.56.0. It's more at risk of breaking again too if upstreams aren't careful with their updates.

Minor bodging (attempted, failed)
Major bodging

Not too different vs "Proper approach", but easier to implement, however maintenance burden may be a bit concerning (could be a signal for bumping MSRV).

Not sure what the difference between the two bodging descriptions is?

The concern with downstreams appears to be the same as with MSRV bumps that commonly ripple up the chain, bumping MSRVs or requiring those with older MSRV requirements to enjoy maintaining a Cargo.lock (as version pinning from Cargo.toml is strongly discouraged from what I've seen).


Comment:

In principle the correct solution to testing with the MSRV is as follows:

This seems good 👍

I think given an MSRV as a baseline, Cargo.toml can then express the minimum versions semver as what satisfies the MSRV, no earlier since that's already a hard requirement? (at least edition sets a hard boundary on MSRV, and the rust-version field can too although opt-out is supported)

This would not require implicit dependencies to build with -Z minimal-versions - especially when rust-version info can be leveraged by the resolver (EDIT: Only valid with -Z msrv-policy).

UPDATE: Might be best served with: cargo +nightly update -Z direct-minimal-versions -Z msrv-policy

@ijackson
Copy link
Contributor Author

ijackson commented Nov 4, 2023

tl;dr:

@matthiasbeyer Please would you merge this MR now, despite @polarathene's interventions, which I have tried to summarise and briefly analyse, below.

Summary of @polarathene 's technical points, and my responses

I have read these messages. It's not so easy to see what they're trying to achieve, but I think I have grasped the main points.

The bulk of the messages consists of detailed analysis of the bugs in our dependencies, which are causing a plain -Z minimal-versions to fail. There are some observations about the maintenance status of our dependencies. I don't think this is actionable; at least, not on the config-rs stable release branch. It might be actionable as MRs against some of our dependencies, or as a programme of improvement for our mainline.

Also, @polarathene doubts whether it is a good idea to run the tests in our MSRV CI check. I think running the tests in CI, even in the MSRV check, is important. But I also think this is not something we should be changing on the stable branch at this stage. Committing a Cargo.lock is a simple intervention which alllows us to continue to run those tests; which has been recognised by Rust upstream as a good approach; and which has no impact on our users. A proposal to not run the tests during MSRV checks should be made on the mainline, in its own MR, and shouldn't form part of fixing the CI on the release branch.

I'm not sure whether these interventions are intended to be blocking. But I am requesting that you proceed as if they aren't.

Personal, nontechnical, response to @polarathene

Thanks for your enthusiasm, but I am finding your contributions here very frustrating.

It is quite unclear whether you intend to block this MR to fix the CI on the stable branch? Because that is the effect your messages are having. Even your "tl;dr" is 11 paragraphs and needs a "tl;dr"!

In practical terms, the maintainers are naturally going to be reluctant to proceed unless they can see whether there is consensus on my proposal, and they feel they understand the underlying issues. But that requires reading and comprehend everything you've written. But the rate at which you are producing text seems to exceed the rate at which the maintainers are able to digest it.

The maintainers must also figure out whether they think you are objecting, and, if so, whether your objections are sound. But it is very unclear from your messges whether you intend to object. That important framing is lacking.

There are probably improvements that could be made to the approach I have taken here. But I don't think your messages offer a viable alternative. We need the CI fixed on the stable branch. Further improvements could be made in followup MRs. Therefore, with some reluctance, I am inviting @matthiasbeyer to disregard your comments.

Your messages would have been much less problematic if they had started with:

tl;dr:
Please merge this MR

Summary

I have done some investigation of the problems with our dependencies and our MSRV. The full report of my findings is below. You don't need to read it, but I wanted to write it down for posterity and in case we might want to try to improve things further.

I've put it in this MR since I wasn't sure I wanted to file a separate ticket and I'm not sure ithe information is actionable.

If indeed that was your intent.

@polarathene
Copy link
Collaborator

polarathene commented Nov 4, 2023

There are probably improvements that could be made to the approach I have taken here. But I don't think your messages offer a viable alternative.
We need the CI fixed on the stable branch. Further improvements could be made in followup MRs.
I am inviting @matthiasbeyer to disregard your comments.

  • Your documented approach (via commit message) is 9 deps being updated with -Z minimal-versions.
  • One of which is relying on a version ref (fragile), tokio-util@0.7.9.

While my 2nd message opens early with:

cargo +nightly update -Z direct-minimal-versions -Z msrv-policy
cargo update --package memchr --precise 2.5.0
cargo update --package ordered-multimap --precise 0.4.0
  • Two deps that need explicit pinning via cargo update instead of 10.
    • Much simpler to maintain.
    • Original snippet documents why these deps and why version pins are acceptable (latest compatible).
  • No version ref required for tokio-util (requires bumping warp to warp = "=0.3.5", which should be acceptable, otherwise tokio = "1.27" min version in Cargo.toml`).

Your messages would have been much less problematic if they had started with:

I prefaced the two messages with a disclaimer that:

  • It wasn't necessary to read all of it, but that it was shared should it benefit anyone else.
  • I could not justify spending any more time refining the content further, but tried to organize it along the way.

The maintainers must also figure out whether they think you are objecting, and, if so, whether your objections are sound.
But it is very unclear from your messages whether you intend to object. That important framing is lacking.

  • I am objecting to the current lockfile generation, there is a simpler approach.
  • Unfortunately, despite the opening disclaimer that I would follow-up with a more meaningful (short) message to summarize my final input and ping you, you respond with complaints to something I stated was not important to digest right now.
  • While I disagree that cargo check --tests or similar need to run with the MSRV, I am not blocking on that, it is just an opinion.
  • I am in agreement about Cargo.lock being worthwhile, I raised a concern with how dependabot may unintentionally adjust when Cargo.lock is committed and behave as if config-rs is an app, rather than a library. I expressed that I'd like to look into that first.

@matthiasbeyer Please would you merge this MR now, despite @polarathene's interventions, which I have tried to summarise and briefly analyse, below.

While I apologize for the misunderstanding / confusion my messages caused you, I find this a tad disrespectful to request (I was notified around 2am).

Since your applying pressure, I am responding early with the above (before I was ready for my follow-up comment later on today). Likewise with the following suggestion (verified to work).

Here are the minimal changes that will work:

git clone https://github.com/mehcode/config-rs -b release-0.13.x --depth 1 .

# Requires `rust-version = "1.56.0" in Cargo.toml, and `warp = "=0.3.5"`:
cargo +nightly update -Z msrv-policy

# 3 pins for MSRV 1.56 compatibility (_due to lack of support for `-Z msrv-policy`_):
cargo update --package memchr --precise 2.5.0
cargo update --package ordered-multimap --precise 0.4.0
cargo update --package tokio-util --precise 0.7.8

# These both pass successfully:
cargo +1.56.1 check --tests
cargo +1.56.1 clippy --locked --offline --all-targets --all-features -- -D warnings

Ignore the earlier -Z direct-minimal-versions suggestion as Cargo.toml presently misrepresents many direct deps supported min version for the MSRV. I have resolved what those are if they'd be of interest.

@polarathene
Copy link
Collaborator

  • Your documented approach (via commit message) is 9 deps being updated with -Z minimal-versions.

  • One of which is relying on a version ref (fragile), tokio-util@0.7.9.

For reference, this already fails within 2 weeks since PR was opened:

$ cargo +nightly update -Z minimal-versions -p tokio-util@0.7.9

error: package ID specification `tokio-util@0.7.9` did not match any packages
Did you mean one of these?

  tokio-util@0.6.10
  tokio-util@0.7.10

# Fragile:
$ cargo +nightly update -Z minimal-versions -p tokio-util@0.7.10

Also:

# Still broken:
$ cargo +1.56.1 check --tests
error: package `scoped-tls v1.0.1` cannot be built because it requires rustc 1.59 or newer, while the currently active rustc version is 1.56.1

$ cargo +1.56.1 clippy --locked --offline --all-targets --all-features -- -D warnings
error: package `chrono v0.4.31` cannot be built because it requires rustc 1.57.0 or newer, while the currently active rustc version is 1.56.1

cargo +nightly update -Z minimal-versions -p chrono will fail without using --precise or updating the min version in Cargo.toml since the actual min compatible version for MSRV tests to pass is 0.4.23, and 0.4.26 is the last release supporting MSRV of 1.56.

EDIT: Oh, not once was I corrected about the MSRV being 1.59. The README on the branch and prior discussion in this PR I asked about the MSRV multiple times and came to the conclusion of it being 1.56.1 since that's what cargo-msrv deduced and what the README documents.

So if anything, not only is my alternative lockfile generation simpler, it allows to continue supporting the original MSRV of 1.56.1 instead of the pattern of bumping the MSRV for CI to pass.

@polarathene polarathene changed the title Fix CI by committing a Cargo.lock which works with our declared MSRV [release-0.13.x] fix(CI): Commit an MSRV compatible Cargo.lock Nov 5, 2023
@ijackson
Copy link
Contributor Author

ijackson commented Nov 6, 2023

I'm going to try to limit myself to comments which will hopefully allow us to move forward.

@polarathene Thanks for the alternative MR #495.

Thanks also for the investigations. I agree that it's likely that the actual MSRV is 1.56 rather than 1.59. I didn't set out, with this MR, to lower the MSRV of the 0.13.x branch. But that seems like it would be a good thing to do.

I do think that we should continue to run tests and examples with our MSRV in CI. I think we must do that with a fixed Cargo.lock, since otherwise our CI is likely to (once again) break due to changes in our dependency stack.

It is not so easy to produce a working Cargo.lock. I note that #495 does not produce a Cargo.lock capable of running the tests and examples. I used the runes in #495 (comment), with that branch, and it didn't work for me. (And, CI on that MR is failing, but that's partly due to that branch not having a Cargo.lock at all.)

My approach to producing a working Cargo.lock was indeed not very principled. My own commit message was not intended as a recipe for future use. It was intended as an explanation of what I had done, for traceability and documentation.

I am very open to an alternative method of generating a working Cargo.lock. In whatever way we produce it, the method that was used generating it ought to be written down somewhere. (And as I say I think it needs to be committed to the tree and used for our MSRV CI.)

If we can make a stable and reproducible way of generating a working Cargo.lock that would be ideal; we could even commit the generation script. That's what we do in the Arti project. But my attempts to do that for config-rs were not successful.

The concern about dependabot could perhaps be resolved by committing the Cargo.lock under a different name. In derive-adhoc I have a Cargo.lock.minimal. We would have to change the CI to cp Cargo.lock.minimal Cargo.lock in the MSRV tests.

@polarathene
Copy link
Collaborator

polarathene commented Nov 6, 2023

It is not so easy to produce a working Cargo.lock. I note that #495 does not produce a Cargo.lock capable of running the tests and examples.

I used the runes in #495 (comment), with that branch, and it didn't work for me.

How did you test? I had verified with:

cargo +1.56.1 check --tests
cargo +1.56.1 clippy --locked --offline --all-targets --all-features -- -D warnings

It's possible that something may have changed upstream since I last tested which may have introduced another resolver issue, I will verify it again soon.

If there is a different command that fails, please let me know and I'll sort that out. If I'm unable to reproduce the failure I would be very interested in knowing what is different with our environments (I used the official Rust Docker image, cloned the release branch and generated Cargo.lock with commands I shared on the updated Cargo.toml).


The concern about dependabot could perhaps be resolved by committing the Cargo.lock under a different name.

Yes, I have seen Cargo.lock.msrv is quite common.

I haven't had time yet to continue looking into that concern, I should be able today. I've not configured dependabot before but assume there's some explicit option to alleviate the implicit detection concern 👍


I do think that we should continue to run tests and examples with our MSRV in CI. I think we must do that with a fixed Cargo.lock, since otherwise our CI is likely to (once again) break due to changes in our dependency stack.

I am all good with that 👍

If we can make a stable and reproducible way of generating a working Cargo.lock that would be ideal; we could even commit the generation script.

Documenting the generation is probably sufficient for now 👍


How would you like to proceed?

I could commit Cargo.lock similar to what you've done here on my PR if you're ok with that approach? It may help with verifying the lock works on your end and the CI should then pass.

EDIT: Just noticed #496 reading that now 😅

@ijackson
Copy link
Contributor Author

ijackson commented Nov 7, 2023

How would you like to proceed?

EDIT: Just noticed #496 reading that now 😅

Right. Thanks for the thumbs-up there.

I could commit Cargo.lock similar to what you've done here on my PR if you're ok with that approach? It may help with verifying the lock works on your end and the CI should then pass.

I suggest we merge #496 and then you consider rebasing #495 on top of it. If #495's Cargo.toml is compatible its CI will pass, then.

Otherwise maybe it will be somehow possible to make a Cargo.lock that does work with #495. AFAICT from reading #495, the process of making a working Cargo.lock may be easier with those dependency updates. Like I say in #496, if we can come up with a better way of generating a working Cargo.lock then I'm all in favour. I just think it should be committed. If we merge #496, committed as Cargo.lock.msrv.

@polarathene
Copy link
Collaborator

polarathene commented Nov 7, 2023

How did you test? I had verified with:

I didn't get a response to this, I have verified again.

Here are the exact steps:

# Reproduction environment:
docker run --rm -it --workdir /tmp/reproduction rust bash

# Prepare:
# Clone PR branch: https://github.com/mehcode/config-rs/pull/495
git clone https://github.com/mehcode/config-rs -b 'chore/0.13.x-min-versions' --depth 1 .
rustup toolchain install nightly 1.56.1
rustup component add --toolchain 1.56.1 clippy

# Create lockfile and pin deps with bad msrv-policy support:
cargo +nightly update -Z msrv-policy
cargo update --package memchr --precise 2.5.0
cargo update --package ordered-multimap --precise 0.4.0
cargo update --package tokio-util --precise 0.7.8

# Verify tests pass:
cargo +1.56.1 check --tests
cargo +1.56.1 clippy --locked --offline --all-targets --all-features -- -D warnings

I suggest we merge #496 and then you consider rebasing #495 on top of it. If #495's Cargo.toml is compatible its CI will pass, then.

I am fine with this 👍

EDIT: Done, #495 has been updated and awaiting review: #495 (comment)

@ijackson
Copy link
Contributor Author

Closing since #492 was merged.

@ijackson ijackson closed this Nov 21, 2023
@ijackson
Copy link
Contributor Author

I mean #496

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants