Mostly unprompted feedback from a random GitHub user:
cargo upgrade
is pretty meh since it mixes both semver compatible and semver incompatible changes - you usually want to upgrade semver incompatible stuff one by one.
Mostly unprompted feedback from a random GitHub user:
cargo upgrade
is pretty meh since it mixes both semver compatible and semver incompatible changes - you usually want to upgrade semver incompatible stuff one by one.
… you usually want to upgrade semver incompatible stuff one by one.
While I also would prefer to do one incompatible upgrade at a time (and commit its compatibility-fixes separately from any other change), I wonder: how could a tool succeed at make that easier than editing Cargo.toml
? It would have to arbitrarily pick one dep to upgrade, and that might also break things when related deps must match.
At the moment, cargo upgrade
does satisfy this need with the --package
and --exclude
flags. You can do cargo upgrade --dry-run
, see what needs upgrading, and then do cargo upgrade --package clap --package clap_complete
.
I find most breaking changes are minor enough that I can do them in bulk, so I do the opposite with cargo upgrade --exclude notify
(where notify
is a dep that will take more work so I break it out).
I've been experimenting with two use cases within this thread to see how they work and so I can consider how we can support them within cargo-upgrade.
I've been getting frustrated with Dependabot sending a flood of emails for (2) and (1) is incompatible with it, so I decided to try out Renovate and so far it seems to be working out. I figured I'd share my config in case its useful for anyone else.
rust-cli/argfile/.github/renovate.json5
(library):
{
"schedule": [
"before 3am on the first day of the month"
],
"semanticCommits": "enabled",
"configMigration": true,
"packageRules": [
// Goals:
// - Keep version reqs low, ignoring compatible normal/build dependencies
// - Take advantage of latest dev-dependencies
// - Rollup safe upgrades to reduce CI runner load
// - Help keep number of versions down by always using latest breaking change
// - Have lockfile and manifest in-sync
{
"matchManagers": ["cargo"],
"matchDepTypes": ["build-dependencies", "dependencies"],
"matchCurrentVersion": ">=0.1.0",
"matchUpdateTypes": ["patch"],
"enabled": false,
},
{
"matchManagers": ["cargo"],
"matchDepTypes": ["build-dependencies", "dependencies"],
"matchCurrentVersion": ">=1.0.0",
"matchUpdateTypes": ["minor"],
"enabled": false,
},
{
"matchManagers": ["cargo"],
"matchDepTypes": ["dev-dependencies"],
"matchCurrentVersion": ">=0.1.0",
"matchUpdateTypes": ["patch"],
"automerge": true,
"groupName": "compatible (dev)",
},
{
"matchManagers": ["cargo"],
"matchDepTypes": ["dev-dependencies"],
"matchCurrentVersion": ">=1.0.0",
"matchUpdateTypes": ["minor"],
"automerge": true,
"groupName": "compatible (dev)",
},
],
}
crate-ci/committed/.github/renovate.json5
(bin):
{
"schedule": [
"before 3am on the first day of the month"
],
"semanticCommits": "enabled",
"configMigration": true,
"packageRules": [
// Goals:
// - Rollup safe upgrades to reduce CI runner load
// - Have lockfile and manifest in-sync
{
"matchManagers": ["cargo"],
"matchCurrentVersion": ">=0.1.0",
"matchUpdateTypes": ["patch"],
"automerge": true,
"groupName": "compatible",
},
{
"matchManagers": ["cargo"],
"matchCurrentVersion": ">=1.0.0",
"matchUpdateTypes": ["minor"],
"automerge": true,
"groupName": "compatible",
},
],
}
(groupName
causes matching dependencies to be rolled up into a single PR)
The main things I can think to improve are
package.rust-version
in the index)EDIT: The one annoying part is that when you add the app to an org, it creates PRs against each repo to add configuration files.
In a recent cargo PR, we were discussing clap putting deprecated
attributes behind a feature flag.
And yea, I imagine communicating to users to use that feature is extremely difficult. I think this is an area where there is opportunity for better options. I keep thinking it is so very similar to
cargo fix --edition
for migrating editions, it would be nice if there was something similar for migrating to new major versions of a dependency. Or at least have some form of better communication.
This got me thinking, what if cargo upgrade --incompatible
had a --verify
step like cargo publish
that effectively does a RUST_FLAGS="-Awarnings -Ddeprecaref" cargo check
?
cargo publish
deprecated
is in the feature list and pass in --features <dep>/deprecated
"
_
and instead the conversation went in the direction of explicitly marking features as hidden. If a convention isn't acceptable, would a dedicated field to specify the feature should be enabled on upgrade be too specialized?Thoughts?
Effectively, "try a build, and upgrade if and only if it works"?
Upgrade if and only if there are no deprecation messages, yes
Ideally, we'd only care about deprecations for what are candidates for upgrade. If deprecated
feature flags are used, that is easy (selectively enable it). Where deprecated
feature flags aren't used, I do not see a solution within the current tools we have available to meet the ideal behavior.
That sounds like a great additional feature. I don't think it should be a blocker for merging cargo upgrade
, but I'd love to have it in the future.
Sounds like dependabot. I think of cargo upgrade
as a TOML editor, not as a dependency manager. Running compilation with speculative feature flags is more than I expect from it.
I'm not sure if such test is useful. In case of clap I know my projects will fail to build, as they require extensive changes. In some cases I plan to stay on clap v2 or v3 forever, so I wouldn't want other cargo upgrade
runs complicated or slowed down by retrying clap over and over.
Sounds like dependabot. I think of
cargo upgrade
as a TOML editor, not as a dependency manager. Running compilation with speculative feature flags is more than I expect from it.
Personally, I feel that is too limited of a view. Breaking changes aren't something to be done blindly and we should be finding ways to help people through the process, whether that is
cargo semver-check
and to provide an automated report of what changedThinking about the upcoming toml
/ toml_edit
releases and a comment in #1369, one limitation of the pre-upgrade verify step is that a naive solution would only report deprecations for the version the user is currently on and not for the latest version within the current major number. The user could also be upgrading across multiple major versions.
(I say cargo update
throughout, considering the merge path of adding new modes to cargo update
instead of a separation between "update" and "upgrade" functionality. The logic applies equally to a distinct upgrade
, but I currently agree with the line of thought that officially differentiating between the two essentially synonyms isn't a good idea.)
Unless you're also doing a post-upgrade check, you're still going to end up with broken upgrades. Because major version bumps being compatible modulo removing deprecated API is the exception rather than the norm. Doing upgrades for crates individually is also likely to cause failures as well, due to "peer/public dependencies" where the version used by the local crate needs to match the version used upstream.
Absolutely we'd benefit from some sort of cargo fix --edition
like migration tool hookable by crates, and integrating that into cargo upgrade
can make sense, but it's significantly more complicated than just blocking upgrades in the face of deprecation warnings.
If the behavior of cargo update --allow-incompatible --verify
would just be to do a precheck
and bail on deprecation warnings... The user is capable of doing that
step themselves. If you're doing incompatible upgrades, you're taking on the responsibility of knowing what incompatible changes you need to resolve. It's also sort of a false promise if you don't verify the post-upgrade state rather than just try to divine it from the pre-upgrade state.
If it can apply some migrations beyond just whatever cargo fix --deprecated
would, then it becomes meaningful to bundle into cargo update
.
To that "shiny future" vision, I'd say that dependency crates (the lower version) should opt in to providing an update path across semver-incompatible versions. The first step is essentially extending the semver trick; cargo update --persist
can be told to upgrade from autocfg@0.1.8 to autocfg@1, because the former is just a transparent reexport of the latter with no changes.
This then fairly naturally can extend to allowing the crate to provide metadata on how to detect use of deprecated/removed functionality, do automated migrations, etc. It's probably still prudent to still limit cargo update --persist
to the fully compatible upgrades, and have a separate --allow-migrations
to request the more involved process with potentially building and editing the local crate.
Anyone doing cargo update --allow-incompatible
is fully aware that they're potentially getting put into a noncompiling state; they've explicitly requested as such. At a minimum, doing automatic validation (pre or post) of incompatible version upgrades should probably stay a separate opt-in flag from requesting the incompatible upgrades. It should also probably be possible to mark a dependency as "pinned" and ignored by cargo update --allow-incompatible
without renaming it, for cases like deliberately staying on an older clap version despite it (presumably in this future) providing at least partially automated migration assistance.
While I can understand the idea that checking for deprecations isn't complete, this statement makes me feel like you missed the point. From my own experience, I repeatedly see people upgrade clap without checking the migration guide or even the changelog. The process is likely more frustrating and there are enough situations where they pull me in for help.
Some problems I see with the upgrade process include
In contrast to the blind upgrade, someone could do:
cargo upgrade -i
libs.rs/<crate>
(libs.rs
is being used as it removes some steps)Both processes are frustratingly flawed. It'd be great if we could come up with ways to help the user through it where cargo upgrade
could be viewed as a companion like rustc compiler errors are.
How about cargo upgrade
designed to work in stages, choosing what to do depending on the current state of the project:
features = deprecated
test, or upgrade some incompatible deps.That would make cargo upgrade
"just work" without needing extra flags.
It'd be cool if it could tell the difference between semver-incompatible but actually still fine without changes vs really-really incompatible upgrades that require manual code changes, and first upgrade the easy ones.
From a workflow perspective, I'd strongly prefer cargo upgrade
to do something useful without having to add or remove extra flags.
So a workflow where I run cargo upgrade; cargo test; <fix what needs to be fixed>; goto 10
in a loop until everything is fixed and upgraded would be nice.
Over the last 3 months or so I've been re-thinking the command merge. We quickly settled on that in this thread and we've had 6 months or so of runtime on it. While usually those with problems are the loudest, I don't remember seeing any appreciation for the current behavior. I think it was Josh who had a proposed tweak to the flags that I've not had a chance to talk with him further about but I don't think that will fundamentally change things if we found it was better than what we have.
Stated reasons for doing the merge:
From my original post
Separate
cargo update
(for lock file) andcargo upgrade
(for manifest) commands is confusing to some users
From CLI Guidelines
Don’t have ambiguous or similarly-named commands. For example, having two subcommands called “update” and “upgrade” is quite confusing. You might want to use different words, or disambiguate with extra words.
Unfortunately, I do not remember examples of "user confusion" I referenced. I also think people are used to the distinction between "software updates" (patches, safe) and "software upgrades" (here be dragons but new goodies).
As highlighted in kornel's post, the current model is cumbersome. People have requested specialized flags but that doesn't compose well and would be just as cumbersome.
My current thinking
--save
to cargo update
cargo upgrade
to incompatible upgrades with "pinned" dependency detectioncargo upgrade
to only update the lock file in a non-recursive fashion--dev
flags to both cargo upgrade
and cargo update
that only updates dependencies that are not direct build or normal dependencies.
--dev
flag.It'd be cool if it could tell the difference between semver-incompatible but actually still fine without changes vs really-really incompatible upgrades that require manual code changes, and first upgrade the easy ones.
Earlier I mentioned the idea of using cargo-semver-check
to tell you what changed. You bring up a point I forgot,: it'd be even better if it looked at what parts of the API you used and filter the report to just that.
That still leaves behavior changes. We have #[deprecated]
to tell people not to use an API item (possibly in lead up to removal). I've seen people talk about stablizing #[stable]
so we can tell when an API item was added. What if we had another attribute for last behavior change? Granted, all of these are only as useful as people using them but I could see that helping a lot with people upgrading across the upcoming toml
release.
Even more off topic but I wish #[deprecated]
had an "after` attribute rather than a "since" attribute so I didn't have to speculate what my next version is going to be (usually predictable but I don't want to have to think about it and "usually" is the operative word)
I have now skimmed through the 115 comments in this thread so far, and one question still eludes me: what is the motivation for cargo update --save
that isn't solved by checking in Cargo.lock
? The use-cases I've seen so far are:
1 can be done with cargo update --allow-major --save
.
2 can be done with cargo update -p foo --save
If I had to come up with an argument for cargo update --save
that isn't solved by "check in Cargo.lock
", I think it'd be:
I don't know/want to list all the crates that I used new features from, so just bump all of them.
That is a fine position to take for "application" crates (i.e., those at the leaves with no dependents). But I think it's something we want to discourage for anything that may have dependents. And Cargo has (at least in the past) not been opposed to encoding incentives in what is easy/hard to do. Which makes me think that we should not optimize for cargo update --save
. Maybe it should be possible, but that should be the "most annoying to get to" mode, not the easiest one.
Which gets me to the update
/upgrade
discussion. It seems like the main argument (?) for separating them is that upgrade
can imply --save
and possibly --allow-major
, whereas we can't make update
do that. I think there's a lot of merit to that argument. But there is also a lot of merit to the argument that update
and upgrade
are too similar, and people will be confused. Now, granted, confusion here isn't too destructive, but even so it's friction. I wonder if we can mitigate that (as was proposed by at least one past comment) by choosing a better name for either (or both?) commands through a two-level subcommand:
cargo dependencies add # equivalent to cargo add
cargo dependencies upgrade # equivalent to the proposed cargo update --allow-major --save
cargo dependencies upgrade --to-locked # equivalent to the proposed cargo update --save
cargo dependencies upgrade -p foo --to-locked # equivalent to the proposed cargo update -p foo --save
cargo dependencies upgrade -n/--dry-run # equivalent to cargo update --allow-major (no --save)
Crucially, like for other cargo
subcommands this could also be shortened — e.g., cargo b
== cargo build
— so cargo dep up
would work for folks who don't want to type a lot.
If we wanted to go one step further, we'd also deprecate cargo update
and encourage the use of cargo generate-lockfile
(with a flag?) in its place, and in the next edition make cargo update
map to cargo dependencies upgrade
.
I think it's perfectly valid to say "I tested with these versions, so I know these versions work". In the absence of a Cargo change to make "test with minimal versions" usable, together with widespread ecosystem fixes and CI changes, I think "upgrade to latest versions and test those" is a valid strategy for making sure your crate works with the versions it declares.
True, that's a good point. And I guess the argument is that checking in your Cargo.lock
won't help your dependents, and therefore your only way of communicating "I tested with this" is to bump the versions in Cargo.toml
. I still think that's not a state of affairs we want to be in though — we would rather that folks test with the minimum version they specify, and avoid bumping it unnecessarily (again, unless they're an end application). That's supported with what I proposed above, it's just a little cumbersome, which I think is a decent way to balance "we need to do this today" with "we don't generally want this to be the way it's done longer-term".
Tbqh, I also agree somewhat. I don't like moving the functionality behind a subcommand so much, but I do agree that a blanket cargo update --save
is probably something we don't want to directly encourage libraries to be doing.
But on the other hand,
cargo install
's default mode isn't --locked
[1], --minimal-versions
remains nearly useless since most crates aren't "minimal versions correct" (and the way to get correct typically requires requiring the latest version), and we still don't have even an unstable resolver for "minimal just for me." Updating versions is presumed to be desirable and low risk, although it is limited to only be done on user interaction. (Though it should also be noted that cargo update -p
can and does result in updating arbitrary other packages in the tree! If the updated package's dependency constraints aren't satisfied, those get updated to latest as well.) To that end, we're slowly adding features to eliminate minor breaking changes[2].cargo new
defaulting to --bin
) if the two prefer different modes.Given complete carte blanche to redo things, I think my current picture of an ideal world would be roughly:
Choice of flag names was the first naming to come to mind that didn't suffer from undesired ambiguity. Better naming for usability probably exists; this sketch mostly exists as a way to illustrate the modes/switches I would ideally like to exist, not to say that this naming is ideal.
cargo publish
works on "minimal just for me" rules; if the lockfile version of direct dependencies is higher than the version specified in the manifest, verification fails.
--no-verify
now takes an optional argument to allow skipping individual verification steps, of which there are now multiple.cargo update
flags:
--exclude spec...
: the update is conservative w.r.t. the specified package(s), as if --package
was used. Error if the package(s) are also requested to update via --package
or --aggressive
.--transitive
: the update is conservative w.r.t. direct dependencies specified in the workspace, as if all direct dependencies were given to --exclude
.--direct
: the updatd is conservative w.r.t. all transitive dependencies, as if all direct dependencies were given to --package
.--transitive --direct
is not an error; it updates the entire lockfile.--transitive
or --direct
are specified AND the workspace includes at least one library package without publish = false
, --transitive
is implied.--save
: write the resolved versions of direct dependencies into the depending manifest(s). Show note if --transitive
was implied to indicate direct dependencies weren't updated. Preflight verifies nondirty VCS state.--breaking
: only update direct dependencies with semver-major version updates available. Implies --direct --save
; error when combined with --transitive
. Upgrades to direct dependencies without semver-major updates are conservative, as if given to --exclude
.--breaking
unless explicitly requested. Packages are implicitly considered pinned if they've been renamed in the manifest (but not if the "lib" = { package = "name" }
syntax was used without renaming the library crate), but can opt back out of being pinned.This doesn't do the "maintain precision" that cargo-upgrade does. I maintain that underprecise caret bounds (the default bound is caret) are an antipattern; if you support a minimum version of 1.0.0
, specify a dependency constraint of ^1.0.0
, not ^1.0
. Instead, we address a similar (same?) desire by just not updating the direct dependencies of libraries unless requested.
It may be possible to incrementally migrate to something approaching this model, although I don't know; but I do know that deprecating and/or changing the behavior of basic cargo update
commands that work today needs to be done with extreme caution. Editions aren't free reign to make breaking changes, and it's important to note that workspaces don't even have editions, only packages. (This is already kinda painful for the version resolver, which needs to be explicitly set if in a workspace without a package at the root manifest.)
A core guiding principle of editions is that we don't want to make old documentation / forum posts / etc. incorrect, even in the new edition. What edition is in use is a mostly transparent property, with the compiler stepping in to offer help when there's a mismatch. At worst, someone following advice aimed at a different edition than they're in should ideally[3] result in a reasonably helpful compile error that can reference the edition concept. The case where a crate requires the new (or rarely, old) version resolution is an unfortunate case where determining that's likely the issue is difficult[4].
I think this one is slated to change? If an application included a lockfile in the package, we probably should respect it by default. ↩︎
The currently proposed one is still std-only and very restricted, but it lays the precedent groundwork for deprioritizing names added after the library version a package was developed against in name resolution. Alternatively, it's long been discussed to have cargo publish
upload the "fully elaborated" name resolution information and use that for dependencies, to address name resolution breakage that way. (Although how that interacts with upstream macros is unclear.) ↩︎
The edge cases where code compiles in multiple current editions with different meaning are extremely rare. Confusing async
blocks with a struct literal is the simplest one... but that effectively requires not using the resulting value, so the semantics of both versions likely actually matches. ↩︎
I don't actually recall if we print a suggestion that you might want to use the new version resolver when version resolution fails with the old resolver. If we don't, we probably should. Running into this is unfortunately decently common since virtual workspaces still default to the old resolver and wgpu is a notable crate that requires the new version resolver. ↩︎
One should never check in a lockfile. I really gotta find the time to write the full-length essay about how bad a practice this is.
(I might even go as far as to say that lockfiles shouldn't exist. I've seen only one or two legitimate uses and it's not clear to me that these are sufficient to justify how easy it is to screw both yourself and all your downstreams over with the mere existence of a lockfile.)