Search code examples
rustdependenciesrust-cargorust-crates

How do you avoid dependency hell in Rust projects?


I am developing a project that depends on many 3rd party crates. As it often happens, the 3rd party crates depend on others.

I constantly find myself in a situation where two or more crates require different versions of the same crate. As a result, cargo cannot choose a version. Often, the problematic crates are those that I do not add explicitly.

Meanwhile, the only solution was to check out a 3rd party and update the version there, but it often requires updating the whole chain of 3rd parties. It takes time and not fun. When I try to publish my changes to the upstream, it takes ages to merge them.

Is there a better way to handle this situation in Rust?

P.S. I come from a C++ background, where dependency management is even worse. So, I do not have much experience with fancy modern eco systems and might miss something.

To give more context, I am adding a partial (the full version is very long) cargo tree output.

This is the tree before I add a new dependency:

    │   ├── near-chain-configs v0.0.0 (https://github.com/f-squirrel/nearcore.git?rev=76424c004de84f6b5823751f132f62a0da9aa656#0a8e7ac7)
    │   │   ├── anyhow v1.0.75 (*)
    │   │   ├── chrono v0.4.26 (*)
    │   │   ├── derive_more v0.99.17 (proc-macro) (*)
    │   │   ├── near-config-utils v0.0.0 (https://github.com/f-squirrel/nearcore.git?rev=76424c004de84f6b5823751f132f62a0da9aa656#0a8e7ac7)
    │   │   │   ├── anyhow v1.0.75 (*)
    │   │   │   ├── json_comments v0.2.1
    │   │   │   ├── thiserror v1.0.47 (*)
    │   │   │   └── tracing v0.1.37 (*)
    │   │   ├── near-crypto v0.0.0 (https://github.com/f-squirrel/nearcore.git?rev=76424c004de84f6b5823751f132f62a0da9aa656#0a8e7ac7)
    │   │   │   ├── blake2 v0.9.2 (*)
    │   │   │   ├── borsh v0.10.3 (*)
    │   │   │   ├── bs58 v0.4.0
    │   │   │   ├── c2-chacha v0.3.3
    │   │   │   │   ├── cipher v0.2.5
    │   │   │   │   │   └── generic-array v0.14.7 (*)
    │   │   │   │   └── ppv-lite86 v0.2.17
    │   │   │   ├── curve25519-dalek v3.2.0 (*)
    │   │   │   ├── derive_more v0.99.17 (proc-macro) (*)
    │   │   │   ├── ed25519-dalek v1.0.1
    │   │   │   │   ├── curve25519-dalek v3.2.0 (*)
    │   │   │   │   ├── ed25519 v1.5.3
    │   │   │   │   │   └── signature v1.6.4

This is the dependency I add:

[dependencies]
iota-sdk = "1.1.0"

The error I receive:

error: failed to select a version for `signature`.
    ... required by package `ecdsa v0.16.0`
    ... which satisfies dependency `ecdsa-core = "^0.16"` of package `k256 v0.13.1`
    ... which satisfies dependency `k256 = "^0.13"` of package `iota-crypto v0.21.2`
    ... which satisfies dependency `iota-crypto = "^0.21.2"` of package `stronghold_engine v2.0.0-rc.0`
    ... which satisfies dependency `engine = "^2.0.0-rc.0"` of package `iota_stronghold v2.0.0`
    ... which satisfies dependency `iota_stronghold = "^2.0.0"` of package `iota-sdk v1.1.0`
    ... which satisfies dependency `iota-sdk = "^1.1.0"` of package `iota_client v0.1.0 (/my_lib/iota_client)`
    ... which satisfies path dependency `iota_client` (locked to 0.1.0) of package `lib_my_lib v0.1.0 (/my_lib/lib_my_lib)`
    ... which satisfies path dependency `lib_my_lib` (locked to 0.1.0) of package `my_lib v0.1.0 (/my_lib/my_lib)`
versions that meet the requirements `^2.0, <2.1` are: 2.0.0

all possible versions conflict with previously selected packages.

  previously selected package `signature v2.1.0`
    ... which satisfies dependency `signature = "^2"` of package `ed25519 v2.2.2`
    ... which satisfies dependency `ed25519 = "^2.2.0"` of package `ed25519-zebra v4.0.3`
    ... which satisfies dependency `ed25519-zebra = "^4.0.1"` of package `iota-crypto v0.23.0`
    ... which satisfies dependency `iota-crypto = "^0.23.0"` of package `iota-sdk v1.1.0`
    ... which satisfies dependency `iota-sdk = "^1.1.0"` of package `iota_client v0.1.0 (/my_lib/iota_client)`
    ... which satisfies path dependency `iota_client` (locked to 0.1.0) of package `lib_my_lib v0.1.0 (/my_lib/lib_my_lib)`
    ... which satisfies path dependency `lib_my_lib` (locked to 0.1.0) of package `my_lib v0.1.0 (/my_lib/my_lib)`

failed to select a version for `signature` which could resolve this conflict

Update:

I have tried to run cargo update before adding new dependencies to update the version of signature (suggested by Kevin Reid), but it did not update the version.


Solution

  • This is not a typical experience; the blame in this case should be placed on ecdsa for having a < version requirement on one of its dependencies. < or = requirements cause a package to potentially be not compilable with other packages in the same build, and thus are not good practice in published packages.

    < and = requirements should only be used when:

    • there is a bug in later versions of the dependency package that needs to be avoided,
    • the dependency is an “internal” library (e.g. a macro library) that no other package should be depending on anyway, or
    • you're writing non-public libraries within an organization where you can manage conflicts across the entire dependency graph.

    ecdsa does not seem to document why it has this requirement, which I consider an omission in their documentation. However, this commit indicates that versions of ecdsa of v0.16.5 or later (which is a semver-compatible update) should allow using signature v2.1.0 and resolve this conflict.

    One thing you can try to achieve this state is to run cargo update before adding new dependencies, in case the problem is prompted by dependency versions already written in your Cargo.lock file.