Cargo Idea: follows attribute for inheriting versions between dependencies

Proposal for the follows attribute for dependencies in Cargo.toml to match the version of another dependency or vice versa.

Problem

Resolving shared dependencies in Cargo is implicit, but I propose a method for explicit resolution by the user with the ability to inherit versions between them, similar to how it works in nix flakes. This would allow root->A, which depends on B, to use the specified version of root->B so that root->A->B = root->B.

Consideration

Nix Flake Example

For those unfamiliar with how the follows attribute works in nix flakes:

nixpkgs.url = "github:NixOS/nixpkgs/23.11";
rust-overlay = {
  url = "github:oxalica/rust-overlay/master";
  inputs.nixpkgs.follows = "nixpkgs";
};

This means:

  1. The environment uses nixpkgs as a dependency from branch 23.11.
  2. The environment uses rust-overlay as a dependency from branch master.
  3. rust-overlay also has nixpkgs as a dependency, and inputs.nixpkgs.follows = "nixpkgs"; makes rust-overlay follow the same version of nixpkgs as described above.

Cargo Example

Similarly, in Cargo:

[dependencies]
url = "2.5.0"
reqwest = { version = "0.12", dependencies.url.follows = "url" }

Or, preferably the reverse:

[dependencies]
reqwest = { version = "0.12" }
url.follows = "reqwest.dependencies.url"

This way, you don't need to manually find the version of url used by reqwest to ensure they are the same and not automatically resolved by Cargo.

2 Likes

For the compatible semver-major version Cargo will automatically unify versions already, no extra annotations needed.

In other cases I think the best practice is to re-export the dependency. You can use reqwest::Url instead of url::Url.

There has been a related RFC for it already:

1 Like

You mean that I may use something like url = "*" and Cargo will resolve that automaticly?

Yeah that will be a solution to my problem, hope this will go further

No because Cargo eagerly picks the latest version for a version range, so this would pick a higher version than the other dependency. This only works if the version requirements are compatible.

There are a lot of related cases where re-exports don't work

There has been a related RFC for it already:

The original public/private dependency RFC tried to provide a solution for this but it caused problems. In the follow up RFC, I actually point out a future possibility related to the proposal here under Caller-declared relations.

[package]
name = "some-cli"

[dependencies]
clap = { from = ["clap_complete", "clap_mangen"] }
clap_complete = "3.4"
clap_mangen = "3.4"

There the idea is that you can say "for this dependency, select the source for it from these dependencies' public dependencies". Limiting this to public dependencies is an important distinction because dependencies are implementation details and can come and go.

So before we can even consider this, public/private dependencies needs to be finished. For the status on that, see Tracking issue for RFC 1977: public & private dependencies · Issue #44663 · rust-lang/rust · GitHub

In particular, the biggest hurdle at this moment is that the rustc lints for public/private dependencies are not sufficient for stabilization.

4 Likes