How to not update dependencies in Rust
Mon Dec 30 '19
Rust, uses a program called Cargo to do things like run tests, invoke a compiler, and manage dependencies.
A program or library in Rust is referred to by Cargo as a “crate”. Cargo can be configured to do things for your crate if you give it a TOML document called Cargo.toml. That file specifies your crate’s dependencies. Cargo will fetch the dependencies over the information superhighway order to build your crate. It will also fetch your dependencies’ dependencies.
Suppose your program depends on A version 0.1 and B v0.1. Cargo will download and use A v0.1 and B v0.1 so it can build your program. Horray!
Now, it has been a while since you picked those versions and you’ve noticed that B v0.2 was released at some point. So just adjust your dependencies in the Cargo.toml file.
-B = "0.1"
+B = "0.2"
But, let’s say A’s API uses B’s API somehow. For example, A has a function
a::do_a_stuff(_: b::B)
that takes an object from B as an argument.
Since using B v0.2, you can no longer build invocations of that function in your crate
and the compiler will tell you that it expected you to pass a b::B
but instead you
passed a b::B
.
Previously, this was a profoundly confusing experience.
Recently, the error message was improved such that it may cause less confusion than before.
It reads:
expected struct `b::B`, found a different struct `b::B`
|
= note: perhaps two different versions of crate `b` are being used?
At this point, you may displace your frustrations onto your computer by exclaiming: “I don’t know, computer. Why are you asking me? You resolved the dependencies. You did the stupid thing that you’re asking me about.”
It is the case, in this example, that B is a dependency of both our crate and of A. However, A still depends on B v0.1 while we started using B v0.2.
The sane thing to do would be to use the same versions everywhere, but we don’t live in that world anymore because too many people on hacker news complained about setuptools in Python. Instead, Cargo will install both versions of B and use the appropriate one when building each crate.
A lot of the time, this works out okay. You never notice that A required B and the two Bs never know about each other and the story ends with them living out their lives in blissful ignorance.[1]
But it’s not uncommon to use a library that accepts types, like UUIDs, dates and times, URIs, or futures, from other libraries that both you and they then depend on. In these cases, using different versions of the same crate seems to not work.
Cargo records its dependency resolutions in a file called Cargo.lock, located adjacent to your Cargo.toml file.
To demonstrate, I made a new Cargo.toml file with a single dependency, rand = "0.6.5"
,
a crate used for random number generation.
[dependencies]
rand = "0.6.5"
After running cargo build
to resolve the dependencies, we should have a Cargo.lock
file.
Each TOML section in that file looks like a package it downloaded. We can even see that
it downloaded two different versions of rand_core for some reason.
[[package]]
name = "rand_core"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
dependencies = [
"rand_core 0.4.2",
]
[[package]]
name = "rand_core"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
Perusing the rest of the file, you should see similar sections for other crates. They may have an item named “dependencies” and, at some point, you should notice that “rand_core 0.3.1” is listed as a dependency somewhere. Something like this:
[[package]]
name = "rand_xorshift"
version = "0.1.1"
dependencies = [
"rand_core 0.3.1",
]
Back to our example. By reading/grep-ing our Cargo.lock file, we can figure out what versions of each library are being pulled in, and who requires them. In our case, we might see something like
Sometimes, entries in the dependencies list will not include a version. My guess is that this is done when the package referred to only appears once in the Cargo.lock file, so the version is not required to disambiguate the dependency.
[[package]]
name = "A"
version = "0.1.0"
dependencies = [
"B 0.1.0",
]
[[package]]
name = "B"
version = "0.1.0"
[[package]]
name = "B"
version = "0.2.0"
[[package]]
name = "MyExampleCrate"
version = "0.2.0"
dependencies = [
"A",
"B 0.2.0",
]
From this, we can confirm that the B package is used twice with two different versions. And that A depends on a different version of B than what we (MyExampleCrate) depend on.
bonus meme: Use cargo-tree instead of wading through the Cargo.lock file yourself.
To solve this, we could use a version of A that uses the version of B that we would like to use. But this isn’t possible because the author of A hasn’t released one. Instead, we’ll use the version of B we were using previously:
-B = "0.2"
+B = "0.1"
In conjunction with the patch from earlier, where we switched from using B v0.1 to B v0.2, the resulting diff is the following:
And that’s how you do not upgrade the dependencies for your Rust crate at two thirty in the morning on a Monday when you should be in bed trying to fall asleep but instead wondering if quitting your job was worth the happiness it afforded you and how you can even compare those things and if things have any value other than how they affect your future or any potential you might ever have which is probably the most important thing because it is the thing that you will be later and there’s increasingly less of it so there’s less of you and it’s sooner than you think and it’s affected most the earlier that things happen in it and the most meaningful way to change it is by doing meaningful things in the present but in spite of all of that you’re always just fucking with dependencies in Rust.