Dependency Minimum Versions
One component of the Rust project you are working on is a library. Everything has been going smooth, until someone complains that your library does not work. You are able to reproduce it locally, it seems that there is an issue with an early version of a dependency. Your project depends on it by a range, but in fact the early versions of the range do not work. However, in your CI setup you are always only testing against the latest possible version. How can you make sure these kinds of issues are caught in CI?
To understand this issue, a bit of an explainer for how dependency versioning is performed in Rust is required.
Crate Dependency Versions
In Rust projects, usually Semantic Versioning is used for versioning crates. The semantic part of that name means that versions are not just arbitrary tuples of numbers, but they have a meaning that comes with some stability guarantees that are necessary for writing software that does not implode when you update dependencies.
Semantic versioning, often abbreviated as SemVer, is a versioning scheme for
software that aims to convey meaning about the underlying changes in each
release. It uses a three-part version number format, major.minor.match
(e.g., 2.0.1
), where Major versions introduce breaking changes, Minor
versions add new features without breaking backward compatibility, and Patch
versions include bug fixes that don’t affect the API.
This system helps developers and users understand the impact of updating to a new version, ensuring more predictable and manageable software upgrades.
This allows us to specify dependencies not by their exact versions, but by their version bounds. For example, when you have a dependency bound such as this in your project:
name = "1.2"
This is in fact syntactic sugar for >=1.2.0,<1.3.0
. You are expressing that
you need at least version 1.2.0
, but lower than 1.3.0
. The current crate
version might be at 1.2.77
and it might compile just fine. Since semantic
versioning guarantees that the API remains stable between patch releases, you
can trust that when the dependency receives an update, that the newly released
version should still work with your code.
However, there is one potential issue here: Cargo always tries to use the maximum
possible version. This means that even though version 1.2.0
is within the range
you have specified, Cargo will only ever test it with whatever is the latest
version within those version bounds.
It is possible that your crate depends on some feature or fix that was not
present in 1.2.0
, but only added in 1.2.44
, but since Cargo always tests
against the latest, you will never know.
To detect this issue automatically, Cargo has a feature that allows you to
override the version resolution strategy to always use the minimum possible
version. You can enable this feature using -Z minimum-version
.
cargo-minimial-versions
The cargo-minimal-versions
tool helps to validate
whether your crate works with the minimal versions it advertises.
However, an easier approach is to install cargo-minimal-version
and running
it to check if your code will compile:
cargo install cargo-minimal-version
cargo minimal-version check
Reading
Semantiv Versioning specification which explains the rules of how to apply it.
Chapter 3.1: Specifying Dependencies in The Cargo Book
Explains how Cargo crate dependencies are specified in terms of syntax and semantics.
Chapter 3.14: Dependency Resolution in The Cargo Book
Explains how Cargo resolves crate dependency versions given the version constraints set by the dependencies section of your crate.
Chapter 3.18: Unstable Features in The Cargo Book
Explains the Cargo features miminal-versions
and direct-minimal-versions
which force Cargo to resolve (direct) dependencies to their minimal versions
instead of the latest versions.
Rust minimum versions: SemVer is a lie! by Daniel Wagner-Hall
Article which argues that a lot of crates are broken, because they do not compile with the versions they specify in their manifests. Note that this article is rather old.