cargo update --save never really sat right with me. To help work through this, @josh and I had some further conversations on this.
Based on where this landed, I would ask that if people propose alternative solutions, to frame them in terms of what role the related commands are meant to fill and how the solution fits within that role.
Care Abouts
For reference, our shared priorities are
- Don't break behavior on
cargo update - Don't write out incompatible
Cargo.lockandCargo.toml - Focus is on end-users solving common problems and not on being a general programmatic CLI that is meant to cover every case
- Be predictable and understandable
- Can someone unfamiliar with Rust, reading a blog post, predict what different command invocations will do?
- Preference for not having too similarly named commands
- When higher priorities allow, avoid errors that make users go "if you know exactly what I was asking for then why didn't you just do it?"; those are a sign of issues with the UX.
- Don't be hassle when dealing with intentionally held back dependencies
Our primary use cases we are targeting are:
- Want to have simple workflow for "upgrade incompatible dependencies only".
- Want to have simple workflow for bulk lock to latest compatible (already exists as
cargo update) - (medium priority) Selective modify one dependency's version requirement to latest compatible, latest incompatible
- (lower priority) Want to have simple-ish workflow for bulk upgrade to latest compatible
- (lower priority) bulk upgrade all dependencies (could just be two command invocations)
Our secondary use cases are:
- Selective modify version requirement to user-provided value
- Upgrade explicitly pinned version requirements
Some open questions we had
- How do we tell when a renamed dependency like
tokio_03is pinned or not?- We could just assume all renamed are pinned
- We could add a dependency field but I'm a bit leery of adding that kind of bookkeeping to the manifest
- We could force users to
--excludethese dependencies but that might be a bit of a pain to always remember to do - We could only skip renamed if multiple dependencies exist that point to the same package
Context
Currently, cargo update is focused solely on Cargo.lock editing
- Spans entire dependency tree
- Multiple versions of a package may exist, referenced by
name@version - Deals with exact versions and not version ranges
- Only affects you and not your dependents
Version requirement editing is different in that
- Workspace members only
- May want differences between members
- Supports alternative names for packages
- Affects dependents
And as a reminder of the CLI:
cargo update -p foo # select a non-ambiguous `foo` and update it
cargo update -p foo@ver # selects a specific `foo` to update
cargo update -p foo --aggressive # also update dependencies
cargo update -p foo --precise <ver> # select a specific version
cargo update --locked # fail if the lockfile will change
Note: cargo add --locked will also fail if the manifest will change
Some design tools we can consider include
- Renaming a command, making the old name an alias
- Even if there isn't a culture shift to use the new name,
cargo <cmd> --helpandcargo --listwill point people to the new name
- Even if there isn't a culture shift to use the new name,
- Versions without the build metadata field is a subset of version requirement syntax, we may be able to do some mixing of them
- Precedence: using the same
foo@versyntax for versions and version requirements
- Precedence: using the same
- Minimal-version resolution being the default mode would make
Cargo.lockmostly align withCargo.toml, making it easier to conflate the two commands (whether merging them or keeping separate but de-emphasizingupdate)
Interjection
Through this, I realized that the core of my concern with our previous attempts at a single command is that it feels like we are shoehorning new behaviors into cargo update rather than making the behavior cohesive.
- If I see a
cargo update --incompatibleon a blog post, can I predict what will happen if you docargo update? No, because--saveis needed to match behavior - We were trying to make
--packagebe both for package IDs and dependency names to make some of thecargo upgradeworkflows work - We were trying to overload
--preciseto allow control over version requirements
I also realized that my Windows Updates vs Windows Upgrades analogy for cargo update and cargo upgrade breaks down a little because cargo upgrade can do "upgrades" that are on the level of cargo update (say we call it cargo upgrade --compatible). The difference is in the target audience (yourself vs your dependents)
Proposal: cargo update only changes version requirements as a side effect
The primary role of cargo update has been to update your active dependencies (ie Cargo.lock). We do not plan to change that role but give the user control to force it to update in situations that were previously unsupported, particularly updating the Cargo.toml if need be.
Behavior:
- By default, only "safe" updates are allowed (today's behavior)
cargo update --incompatible/cargo update -iwill force (ie update version reqs) update unpinned, incompatible versions (no other dependencies)- Yes, this could potentially be called
--breakingor something else. The name depends on what expresses the concept clearly especially in light of any pinning behavior we have
- Yes, this could potentially be called
cargo update -p foo --precise verwill force the update to happen, only erroring if we can't (don't own relevant version reqs, is pinned), even if its incompatible but unpinned. Version requirement is only changed on incompatible.- Maybe the error on pinned could be relaxed
Somewhere between deferred and rejected (speaking for myself): Support in cargo update for writing to the manifest for non-breaking changes, like bulk compatible upgrades of version requirements (ie a -save flag) which was one of our lower priority workflows. A --save flag is more about updating versions for your dependents, which while important for having valid lower-bounds on version requirements, doesn't fit with the existing model of cargo update. Maybe in the future we can find a way to express this in cargo update that fits with how it works or maybe another command can take on this role. We just aren't wanting to distract our efforts for handling most of the use cases to handle this one
While this tells a cohesive story, a part of me is somewhat concerned that this goes beyond the name update.
Potential related cargo update improvements
- "Rename"
--aggressiveto--recursive(make--aggressivean alias for the new name) - Specialize the parsing of
foo@x.y.zso thatfoo@xwill work so long as its unambiguous (which it should be), much likecargo install foo@x.y.zsupports auto-selecting theyandzfields (picks latest) - Consider applying some of the output formatting from
cargo upgradeintocargo update - Add a positional
packageargument tocargo update, removing the need for--package- Should be safe because there are no trailing var args for a wrapped child process and no (and little chance of) flags that accept
0..=1values
- Should be safe because there are no trailing var args for a wrapped child process and no (and little chance of) flags that accept
Alternatives
These are alternatives I had considered that help give an idea of what I mean by fitting into cargo.
cargo update always modifies Cargo.toml
- This would be a breaking change
- This would get in the way of people intentionally keeping separate versions from version requirements
We deprecate cargo update and a cargo upgrade always updates both files
- This would get in the way of people intentionally keeping separate versions from version requirements
We migrate to minimal-version resolution by default
cargo updatebecomes less useful and we move it out of the spot light.- A
cargo upgradeis added that is focused on editing version requirements
Separate commands for Cargo.lock (update) and Cargo.toml (upgrade)
- Names don't clarify the role each fills
- Much like Debian has
apt dist-upgrade, maybe it could becargo req-update?
Misc Notes
- I don't see us making a distinction between default operator and
^as we document them as being the same thing and sometimes people use^just because - We are erring on the side of needing
cargo update && cargo update --incompatiblevscargo update --incompatibledoing both as there are people who do want them separated and running two commands, while annoying, allows us to cover more use cases.