Comment by 0xffff2

Comment by 0xffff2 15 hours ago

5 replies

So there's no difference at all between "0", "0.9" and "0.9.3" in cargo.toml (Since semver says only major version numbers are breaking)? As a decently experienced Rust developer, that's deeply surprising to me.

What if devs don't do a good job of versioning and there is a real incompatibility between 0.9.3 and 0.9.4? Surely there's some way to actually require an exact version?

Diggsey 8 hours ago

They are different:

    "0": ">=0.0.0, <1.0.0"
    "0.9": ">=0.9.0, <1.0.0"
    "0.9.3": ">=0.9.3, <1.0.0"
Notice how the the minimum bound changes while the upper bound is the same for all of them.

The reason for this is that unless otherwise specified, the ^ operator is used, so "0.9" is actually "^0.9", which then gets translated into the kind of range specifier I showed above.

There are other operators you can use, these are the common ones:

    (default) ^ Semver compatible, as described above
    >= Inclusive lower bound only
    < Exclusive upper bound only
    = Exact bound
Note that while an exact bound will force that exact version to be used, it still doesn't allow two semver compatible versions of a crate to exist together. For example. If cargo can't find a single version that satisfies all constraints, it will just error.

For this reason, if you are writing a library, you should in almost all cases stick to regular semver-compatible dependency specifications.

For binaries, it is more common to want exact control over versions and you don't have downstream consumers for whom your exact constraints would be a nightmare.

[removed] 13 hours ago
[deleted]
steveklabnik 13 hours ago

Note that in the output, there is rand 0.9.0, and two instances of rand_core 0.9.3. You may have thought it selected two versions because you missed the _core there.

> So there's no difference at all between "0", "0.9" and "0.9.3" in cargo.toml

No, there is a difference, in particular, they all specify different minimum bounds.

The trick is that these are using the ^ operator to match, which means that the version "0.9.3" will satisfy all of those constraints, and so Cargo will select 0.9.3 (the latest version at the time I write this comment) as the one version to satisfy all of them.

Cargo will only select multiple versions when it's not compatible, that is, if you had something like "1.0.0" and "0.9.0".

> Surely there's some way to actually require an exact version?

Yes, you'd have to use `=`, like `=0.9.3`. This is heavily discouraged because it would lead to a proliferation of duplication in dependency versions, which aren't necessarily unless you are trying to avoid some sort of specific bugfix. This is sometimes done in applications, but basically should never be done in libraries.

  • 0xffff2 13 hours ago

    Sorry, I don't understand the "^ operator" in this context. Do I understand correctly that cargo will basically select the latest release that matches within a major version, so if I have two crates that specify "0.8" and "0.7.1" as dependencies then the compiler will use "0.8.n" for both? And then if I add a new dependency that specifies "0.9.5", all three crates would use "0.9.5"? Assuming I have that right, I'm quite surprised that it works in practice.

    • steveklabnik 13 hours ago

      It’s all good. Let me break it down.

      Semver specifies versions. These are the x.y.z (plus other optional stuff) triples you see. Nothing should be complicated there.

      Tools that use semver to select versions also define syntax for defining which versions are acceptable. npm calls these “ranges”, cargo calls them “version requirements”, I forget what other tools call them. These are what you actually write in your Cargo.toml or equivalent. These are not defined by the semver specification, but instead, by the tools. They are mostly identical across tools, but not always. Anyway, they often use operators to define the ranges (that’s the name I’m going to use in this post because I think it makes the most sense.) So for example, ‘>3.0.0’ means “any version where x >= 3.” “=3.0.0” means “any version where x is 3, y is 0, and z is 0” which 99% of the time means only one version.

      When you write “0.9.3” in a Cargo.toml, you’re writing a range, not a version. When you do not specify an operator, Cargo treats that as if you use the ^ operator. So “0.9.3” is equivalent to “^0.9.3” what does ^ do? It means two things, one if x is 0 and one if x is nonzero. Since “^0.9.3” has x of zero, this range means “any version where x is 0, y is 9, and z is >= 3.” Likewise, “0.9” is equivalent to “^0.9.0” which is “any version where x is 0, y is 9, and z is >=0.”

      Putting these two together:

        0.9.0 satisfies the latter, but not the former
        0.9.1 satisfies the latter, but not the former
        0.9.2 satisfies the latter, but not the former
        0.9.3 satisfies both
      
      Given that 0.9.3 is a version that has been released, if one package depends on “0.9” and another depends on “0.9.3”, version 0.9.3 satisfies both constraints, and so is selected.

      If we had “0.8” and “0.7.1”, no version could satisfy both simultaneously, as “y must be 8” and “y must be 7” would conflict. Cargo would give you both versions in this case, whichever y=8 and y=7 versions have the highest z at the time.