Skip to content

← Modules, Visibility, Testing and Docs step 15 of 22

Medium Framework

Version requirements: caret, tilde, wildcard, pre-releases

“Why did cargo update break my build” and “why is 0.x special” are both answered entirely by one table. It is also perfectly shaped for a problem: string in, bool out, and a published specification to check yourself against.

pub fn matches(req: &str, version: &str) -> bool
pub fn select(req: &str, available: Vec<String>) -> Option<String>

Caret — the default nobody types

foo = "1.2.3" in a manifest means ^1.2.3. The caret says: anything that should not break me, which Cargo defines as “does not change the leftmost non-zero field”.

requirement range
^1.2.3 >=1.2.3, <2.0.0
^1.2 >=1.2.0, <2.0.0
^1 >=1.0.0, <2.0.0
^0.2.3 >=0.2.3, **<0.3.0**
^0.2 >=0.2.0, <0.3.0
^0.0.3 >=0.0.3, **<0.0.4**
^0.0 >=0.0.0, <0.1.0
^0 >=0.0.0, <1.0.0

The 0.x rows are where nearly everyone gets it wrong, and they explain the churn in the pre-1.0 ecosystem: below 1.0.0 the minor field is treated as the breaking one, so 0.2 to 0.3 is a major bump in everything but name, and every downstream manifest has to be edited by hand.

Tilde — by component count

~ freezes everything to the left of the last field you actually wrote:

requirement range
~1.2.3 >=1.2.3, <1.3.0
~1.2 >=1.2.0, <1.3.0
~1 >=1.0.0, **<2.0.0**

~1 is the same as ^1, because you did not write a minor field for it to freeze. How many components you typed is load-bearing.

Wildcards and operators

* matches anything (crates.io rejects a bare * in a published manifest — having no bound at all is not a dependency specification). 1.* is >=1.0.0, <2.0.0; 1.2.* is >=1.2.0, <1.3.0. Note that 1.2.* is not ^1.2.

>=, >, <, <= and = do what they look like, with one wrinkle: applied to a partial version they operate on the whole band. =1.2 is >=1.2.0, <1.3.0; >1.2 means “past all of 1.2.x”, i.e. >=1.3.0.

Comma-joined comparators are an AND, not an OR. >=1.2, <1.5 is one requirement with two constraints.

Pre-releases, and the classic implementation bug

Two rules, both worth internalising.

Ordering. A pre-release sorts below the release with the same numbers: 1.0.0-alpha < 1.0.0. Within a tag, fields are compared one at a time, and a field made only of digits is compared numerically:

1.0.0-alpha.2  <  1.0.0-alpha.11

Compare those as strings and you get the wrong answer, which is the single most common bug in hand-rolled semver code.

Admission. A pre-release only satisfies a requirement when some comparator in that requirement names a pre-release on the same major.minor.patch. So ^1.0 never resolves to 1.2.0-beta.1, and you have to opt in explicitly with something like >=1.0.0-alpha.2, <1.0.0. This is why pre-release versions do not ambush you and why depending on one is deliberately awkward.

Contract

  • A version is always a complete major.minor.patch, optionally with -pre.release.tag. Build metadata after + is dropped: it takes no part in ordering or matching.
  • A requirement is one or more comma-separated comparators. Anything you cannot parse means matches returns false.
  • select returns the highest matching version, not the first.