Best practices for bumping versions in Cargo.toml?

Given a Rust project foo that depends (directly or indirectly) on bar, whenever bar releases a new version, my understanding is that it's recommended to always update bar in Cargo.lock (after ensuring that foo still works with the new bar, of course). However, if bar is a direct dependency of foo listed in its Cargo.toml, when should the Cargo.toml file be updated to make the new bar version the minimum version requirement? Always? Only when the new version of bar is incompatible with the currently-specified version? Some other condition? Does the answer differ for binaries vs. libraries vs. packages with both a binary & a library?

My motivation for asking comes from dissatisfaction with Dependabot on GitHub; if a Rust project has a Cargo.lock in version control, then any new release of a direct dependency will result in both Cargo.lock and Cargo.toml being updated, even if the new version is compatible with the version specifier already in Cargo.toml. While I can tolerate this for binary-only packages, I also have a couple packages that contain both a binary and a library, for which I feel that the bumping of the minimum versions in Cargo.toml is completely unnecessary and arguably undesirable (and I'd be even more strongly against this behavior for pure-library packages with Cargo.lock under version control if I had any). Before I go about trying to either reconfigure or replace Dependabot, I thought I'd get some input on what's considered best practice when it comes to auto-updating Cargo.toml.

1 Like

In principle, for compatible (minor/patch) updates, Cargo.toml should be updated if and only if foo requires new functionality in the new version of bar (so, not as part of any automated dependency updating). However, it's then easy to make a mistake by writing code that uses newer bar functionality without remembering to update the Cargo.toml. You can avoid these mistakes by testing with cargo update -Zdirect-minimal-versions (in your CI, say, to avoid disrupting local development workflow with rebuilds), but if you don't, having strong minimum requirements is a way to avoid making that mistake and, broadly, to have fewer possible configurations that your code needs to work in. So, it comes down to how much effort you want to put into making foo maximally flexible.

For incompatible (major) version updates, there is no choice: Cargo.toml and Cargo.lock always agree on which major version is being used (unless you use a version requirement that spans multiple major versions, which is possible but unusual and requires even more care and testing).

4 Likes

Cargo.lock of libraries is always ignored, so it matters what is in your Cargo.toml.

The easy and the most reliable solution is to always upgrade all your Cargo.toml version requirements to the latest. This ensures that your crate will certainly work with the versions it specifies, and won't break with somebody else's Cargo.lock that has older versions.

The Rust ecosystem tends to chase latest versions of the compiler and dependencies anyway, so there's little value in staying on old versions. If someone really needs older deps, they can downgrade your library to an older version.

If you want to be more conservative, then upgrade all deps to latest immediately after a release, so that your next release cycle will be developed and tested with these versions.

Does this include always updating for non-breaking dependency updates as well (e.g., 1.2.3 → 1.2.4?) If so, how do you respond to @kpreid's answer? (and, @kpreid, how do you respond to @kornel's?).

In theory you shouldn't need to update patch versions, but there are some very popular crates in the Rust ecosystem that intentionally disregard the semver spec about usage of semver-minor, and regularly introduce new features in patch versions.

And even if patch versions were correctly used only for patching bugs, who would want more bugs? At that point you'd need to start imagining very special edge cases where downstream users need specific bug-compatibility, and that is not a sustainable situation to be in.

One realistic scenario is when you require the very latest patch version of a dependency, which turns out to be broken, and gets yanked. But in such situations the dependency should release a fixed higher patch version. Such yanking happens usually very soon after a release, so unless you bump to versions released only days ago, you should be fine. This is why I recommend bumping versions at the beginning of your release cycle. Your development will take some time, so by the time you're ready to release these latest versions of dependencies will be old enough that the chance of them getting yanked will be slim.

A complication here is that of security or soundness updates. You would presumably not want to wait with upgrading those.

Then there is also the case of projects that are effectively done, and the only reason to release new versions would be occasional bug fixes, updating to newer dependency versions and any changes needed to prevent bitrotting as the computing landscape around the software changes. Here the very concept of a release cycle isn't usually well defined. I have a few command line tools like that.

So in conclusion, your advise is good for certain situations, but is by no means universal.

No, there's no problem with that. Your versions in Cargo.toml are only suggested minimums, but don't stop anybody from using later versions.

You can yourself use tools like cargo-audit that will alert you about security updates, but (edit) in case of libraries it's not your responsibility to bump these versions in Cargo.toml for others.

It will affect any cargo install --locked commands (and I was thinking about binary crates mostly in this case). And also at least the Arch Linux rust packaging guidelines recommend using the lock file for building the packages. So yes, there is a problem with that, where you want at least your lock file fairly up to date.

EDIT: And of course there are your pre-built binaries on github (or gitlab etc) to think about. They are presumably built with the lock file for reproducibility.

We aren't disagreeing entirely. What @kornel called “The easy and the most reliable solution” is the one that I called “a way to avoid making that mistake and, broadly, to have fewer possible configurations”. I think I see more value in not eagerly bumping (in general, having less churn), and @kornel thinks it is better to stick to latest versions all the time, but we were saying the same things about the consequences of that choice.

Regarding minor updates vs. patch updates, I don't think it's very important to discriminate between them when choosing dependency version requirements, because “added a feature” and “fixed a bug in that feature” are both reasons you might want to make sure to require a newer version; the first is just more obvious and unconditional because getting it wrong causes compilation failures, whereas a bug might not even affect your use cases for that library.

So, the actually important categories are “compatible update” (in which you might or might not choose to edit Cargo.toml, as already discussed) and “incompatible update” (in which you must edit Cargo.toml).

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.