https://github.com/brannondorsey/rust-incompatible-transitive-dependencies
An example demonstrating how Rust and cargo support incompatible transitive dependencies (like Node.js + npm not Python + pip)
https://github.com/brannondorsey/rust-incompatible-transitive-dependencies
Last synced: 4 months ago
JSON representation
An example demonstrating how Rust and cargo support incompatible transitive dependencies (like Node.js + npm not Python + pip)
- Host: GitHub
- URL: https://github.com/brannondorsey/rust-incompatible-transitive-dependencies
- Owner: brannondorsey
- License: mit
- Created: 2024-08-17T14:56:47.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2024-08-20T02:27:35.000Z (about 1 year ago)
- Last Synced: 2025-05-03T18:36:57.270Z (5 months ago)
- Language: Rust
- Homepage:
- Size: 15.6 KB
- Stars: 30
- Watchers: 1
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Can a Rust binary use _incompatible_ versions of the same library?
Yes it can.
## Background
As I've continued learning Rust and the Cargo ecosystem, one question has continued to puzzle me:
> Can a Rust project import multiple SemVer-incompatible library crates, or will the dependency resolver reject this scenario and cause your code to fail to compile?
### Transitive dependency conflicts
This is a scenario which occurs mostly with [transitive dependencies](https://en.wikipedia.org/wiki/Transitive_dependency) and sorting them out can be a real headache, especially if you are using unmaintained libraries. A transitive dependency is a dependency of one of your dependencies, and you often don't get to control this version.
> A (your app) > B (your app's dependency) > C (your dependency's dependency)
In this scenario, C is considered a transitive dependency of A.
### Is Rust like `npm` or is it like `pip`?
Does the `cargo` + Rust ecosystem behave more like [`npm` + Node.js by allowing](#nodejs--npm) incompatible transitive dependency versions, or is it more like [`pip` + Python](#python--pip), which does not?
Spoiler: It behaves like `npm` and thank god for that. 😇
## Prove it
```bash
git clone https://github.com/brannondorsey/rust-incompatible-transitive-dependencies
cd rust-incompatible-transitive-dependencies/cargo run # see example output below
``````plain
Compiling a v0.1.0 (/home/brannon/Documents/code/rust-incompatible-transitive-dependencies/a)
Compiling b v0.1.0 (/home/brannon/Documents/code/rust-incompatible-transitive-dependencies/b)
Compiling dependency-test v0.1.0 (/home/brannon/Documents/code/rust-incompatible-transitive-dependencies)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.74s
Running `target/debug/dependency-test`
2024-08-17T23:02:02.709Z INFO [a] logged using log@0.4.22
2024-08-17T23:02:02.709Z INFO [b] logged using log@0.3.9
```### Show me the code
The example binary crate below uses two library crates, `a` and `b`, each requiring their own incompatible versions of the [log](https://crates.io/crates/log) crate.
`cargo.toml`
```toml
[package]
name = "dependency-test"
version = "0.1.0"
edition = "2021"[dependencies]
a = { version = "0.1.0", path = "a" }
b = { version = "0.1.0", path = "b" }
simple_logger = "5.0.0"
````main.rs`
```rust
use a::log as log_a;
use b::log as log_b;
use simple_logger::SimpleLogger;fn main() {
SimpleLogger::new()
.init()
.expect("Failed to initialize logger");
log_a();
log_b();
}
````a/cargo.toml`
```toml
[package]
name = "a"
version = "0.1.0"
edition = "2021"[dependencies]
log = "0.4.22"
````a/src/lib.rs`
```rust
use log::info;pub fn log() {
info!("logged using log@0.4.22");
}
````b/cargo.toml`
```toml
[package]
name = "b"
version = "0.1.0"
edition = "2021"[dependencies]
# Intentionally using an outdated version
log = "0.3.9"
````b/src/lib.rs`
```rust
// Required for the outdated 0.3.* version of log
#[macro_use]
extern crate log;use log::info;
pub fn log() {
info!("logged using log@0.3.9");
}
```## Can I check for dupes?
It turns out Cargo actually has a nifty method to check your dependencies for duplicate versions.
```plain
cargo tree --duplicates
log v0.3.9
└── b v0.1.0 (/home/brannon/Documents/code/rust-incompatible-transitive-dependencies/b)
└── rust-incompatible-transitive-version-example v0.1.0 (/home/brannon/Documents/code/rust-incompatible-transitive-dependencies)log v0.4.22
├── a v0.1.0 (/home/brannon/Documents/code/rust-incompatible-transitive-dependencies/a)
│ └── rust-incompatible-transitive-version-example v0.1.0 (/home/brannon/Documents/code/rust-incompatible-transitive-dependencies)
├── log v0.3.9 (*)
└── simple_logger v5.0.0
└── rust-incompatible-transitive-version-example v0.1.0 (/home/brannon/Documents/code/rust-incompatible-transitive-dependencies)
```## What's happening under the hood?
```bash
cargo build --releasetree target/debug
target/release
├── build
│  ├── ...
├── deps
│  ├── a-5a849d8d164ba2f5.d
│  ├── b-1132515820191c83.d
│  ├── ...
│  ├── liblog-918cd93f416d7029.rlib
│  ├── liblog-918cd93f416d7029.rmeta
│  ├── liblog-fee03072b48908b6.rlib
│  ├── liblog-fee03072b48908b6.rmeta
│  ├── ...
│  ├── log-918cd93f416d7029.d
│  ├── log-fee03072b48908b6.d
│  ├── ...
├── examples
├── incremental
├── rust-incompatible-transitive-version-example
└── rust-incompatible-transitive-version-example.d
```Notice that Cargo has built two versions of the `.d`, `.rmeta`, and `.rlib` files for each separate version of the log dependency.
> NOTE: Run `cat target/release/deps/log-*.d` to see which source files were used to generate each compiled binary file.
As an exercise, try setting SemVer _compatible_ versions of the log crate in `a/Cargo.toml` and `b/Cargo.toml` and then.
1. Run `cargo clean` to empty `target/*`
1. Run `cargo build --release` again
1. Inspect the `target/release` directory again.You should see only a single collection of intermediate files named `*log*`.
> NOTE: Did you change `b/Cargo.toml` to a `0.4` version of log that is _lower_ than `0.4.22`?
>
> If so, you may be surprised to find only the `0.4.22` version requested by `a/Cargo.toml` was fetched and built. This is because the Cargo dependency resolver takes the liberty to use the highest SemVer compatible crate version required by another dependency. I.e. `0.4.10` can be treated by Cargo as `0.4.x` (unless it is specified like `=0.4.22` which should be avoided in most cases).## How does this behavior relate to other languages?
As you can see below, [Python can't handle](#python--pip) the situation we've just described above. But [Node.js can](#nodejs--npm).
> NOTE: These languages were selected because they are both popular and have canonical package managers.
### Python + `pip`
Unfortunately, you're out of luck if you find yourself using Python and requiring incompatible transitive dependency versions. The dependency resolver will simply reject your install and the path forward may be difficult.
```bash
# Can actually be omitted because the install will fail
# and no modules will be globally anyway
python -m venv venv && source venv/bin/activatecat < requirements.txt
flask==2.0.0
werkzeug==1.0.0
EOFpip install -r requirements.txt
``````plain
Collecting flask==2.0.0 (from -r requirements.txt (line 1))
Using cached Flask-2.0.0-py3-none-any.whl.metadata (3.8 kB)
Collecting werkzeug==1.0.0 (from -r requirements.txt (line 2))
Using cached Werkzeug-1.0.0-py2.py3-none-any.whl.metadata (4.7 kB)
INFO: pip is looking at multiple versions of flask to determine which version is compatible with other requirements. This could take a while.
ERROR: Cannot install -r requirements.txt (line 1) and werkzeug==1.0.0 because these package versions have conflicting dependencies.The conflict is caused by:
The user requested werkzeug==1.0.0
flask 2.0.0 depends on Werkzeug>=2.0To fix this you could try to:
1. loosen the range of package versions you've specified
2. remove package versions to allow pip to attempt to solve the dependency conflictERROR: ResolutionImpossible: for help visit https://pip.pypa.io/en/latest/topics/dependency-resolution/#dealing-with-dependency-conflicts
```As far as I'm aware, this is a fundamental problem with Python packages and this scenario can't be avoided by newer package managers like [Rye](https://rye.astral.sh/) or [Poetry](https://python-poetry.org/).
### Node.js + `npm`
Like Cargo, the Node Package Manager (`npm`) allows multiple incompatible dependency versions to be used in the same project.
```bash
cat < package.json
{
"name": "incompatible-dependencies-example",
"version": "1.0.0",
"description": "Example project demonstrating npm allowing incompatible transitive dependencies",
"main": "index.js",
"dependencies": {
"har-validator": "^4.2.1",
"request": "^2.88.2"
}
}
EOFnpm install # Note the dependency resolver exits just fine
grep har-validator package-lock.json
``````plain
grep har-validator package-lock.json"har-validator": "^4.2.1",
"node_modules/har-validator": {
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz",
"har-validator": "~5.1.3",
"node_modules/request/node_modules/har-validator": {
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",```
Notice how `npm` has resolved separate incompatible versions of the har-validator package? The former is the one we explicitly required and the later is the one `request` needs. Each resides in a separate location in `node_modules/` and can be accessed by the project at runtime.
## References and further reading
* The [Dependency Resolution](https://doc.rust-lang.org/cargo/reference/resolver.html) chapter of the Cargo book, particularly the section on [version incompatibility hazards](https://doc.rust-lang.org/cargo/reference/resolver.html#version-incompatibility-hazards).
* The "Dependency Resolution with multiple versions?" [question](https://users.rust-lang.org/t/dependency-resolution-with-multiple-versions/81936) on the Rust language users forum (which was part of the motivation to create this example repo).
* This older post on [version selection in Cargo](https://web.archive.org/web/20180726081617/http://aturon.github.io/2018/07/25/cargo-version-selection/) (circa 2018).
* A post about this repo on the [r/rust](https://www.reddit.com/r/rust/comments/1evnmh6/can_a_rust_binary_use_incompatible_versions_of/) subreddit with some interesting notes in the comments.