In my very large Rust-based project of about 100k lines of code, 100 direct dependencies, and 800 total dependencies, I always require version="*"
for each dependency, except for specific ones that either I cannot or don’t want to upgrade.
Crucially, I also commit my Cargo.lock
, as the Cargo manual recommends.
I normally build with cargo --locked
. Periodically, I will do a cargo update
and run cargo outdated
so that I’m able to see if there are specific packages keeping me at older versions.
To upgrade a specific package, I can just remove its record directly from Cargo.lock
and then do a regular cargo build
.
This works very well!
Advantages
- I minimize my number of dependencies, and therefor build times
- I keep my personal ecosystem from falling too far behind that of crates.io
- I rarely wind up with a situation where I have two dependencies that depend on a different symbol version
- I don’t have to change a version in all of my many
Cargo.toml
s
Disadvantages
- I cannot publish my large repository to a wider audience (but that will never happen for this repository)
- People who see my code start feeling ill and start shifting their eyes nervously.
People who see my code start feeling ill and start shifting their eyes nervously.
To be honest, I had this reaction just reading your description. But, if it’s a only personal project there’s no harm, as long as you’re not selling it as a new best practice!
I once had a project where I added a crate and a completely different part of the application that didn’t use that crate broke.
Turns out that I didn’t include the patch version in a dependency and that new dependency required an earlier patch version that had a critical bug in it.
Your solution is like this, but even more extreme by also allowing a dependency to get your code to link to an old major version, breaking everything.
So, your solution only works if you don’t plan to ever add a new dependency.
In practice, I have about 10% of my dependencies not have the latest version, according to
cargo-outdated
, once I complete acargo update
.For personal projects this is fine, but I’m curious why you feel the need to have every crate be the newest? Once you have it compiling, why upgrade dependencies at all unless you have to? Compiling a new binary is way more work than just running the one that is already compiled. You talk about minimizing build times with this method, but it isn’t clear why recompiling at all with newer dependencies is beneficial.
Theoretically, every update to a crate is better than the last, but sometimes it’s just adding non-breaking features that you weren’t using anyway. You could just check crate updates every once in a while looking for performance gains or features you would like to make use of.
Could also be performance benefits. You could research into updates of every dependency (and some crates don’t publish changelogs, so you have to dive through commit messages), but who has the time for that?
My experience is that keeping up-to-date is beneficial in any language:
- Your risk of having big changes break everything seriously is all but removed
- Those risks are gradually spread out over time into small and manageable parts that won’t cause downtime
- You pull in one new dependency and you’re not likely to need to update a bazillion other things
- You don’t fall so behind that an update is something you start to avoid it entirely because it’s so onerous
I’m not obsessive, though. If a few packages are out-of-date due to some dependency that I want to hold back, it’s fine. And I only do the
cargo update
once or twice a month anyway.
I can somewhat relate. I mostly do something like this (instead of the exact dependency version):
chrono = {version = "0", features = ["serde"]} clap = {version = "4", features = ["derive"]} anyhow = "1"
I do, however, typically write application code instead of library, so it’s probably less critical for me. Occasionally do run into dependency hell here and there, but nothing too bad so far!
I can certainly see the trade-offs. I typically write high performance optimizers, so my dependency list is fairly compact; the big risk I see in general, without knowing anything of you application, is bug fixes or quality of life improvements. Those that manifest as full version bumps are fairly insidious with ‘*’, and can make porting to the future a potential nightmare.
All that said, there’s something nice about using a fixed version of common crates to develop against. One of the big advantages of languages like Python and Go is that robust stdlib which makes many tasks trivial to program assuming a wide enough coverage of libraries.
Yes, it’s true that for each
cargo update
, I have to study the list of updates reasonably closely, and then I may have some compile fixes as well. Those are quite rare, and 9/10 times, there’s nothing to do but the update.