August 18th, 2024

Can a Rust binary use incompatible versions of the same library?

The GitHub repository highlights Rust's Cargo package manager, which enables multiple incompatible library versions to coexist without compilation failures, contrasting it with Python's pip and Node.js's npm.

Read original articleLink Icon
CuriositySkepticismConcern
Can a Rust binary use incompatible versions of the same library?

The GitHub repository discusses Rust's capability to handle multiple SemVer-incompatible library crates through its Cargo package manager, allowing for successful compilation without failures. This feature is particularly significant for transitive dependencies, which are dependencies of other dependencies. An example in the repository illustrates how two library crates, `a` and `b`, can depend on different versions of the `log` crate, demonstrating the setup and execution of a project despite version conflicts. The main binary crate, `dependency-test`, utilizes both library crates, each specifying their dependencies in their respective `Cargo.toml` files. Cargo's functionality enables the coexistence of multiple versions of the same dependency, compiling them separately as required. The repository also provides commands for checking duplicate dependencies and inspecting build outputs. In comparison to other programming languages, Rust's approach is more flexible than Python's pip, which does not permit incompatible transitive dependencies, leading to installation issues. Similar to Rust, Node.js's npm allows for the simultaneous use of multiple incompatible versions. Overall, the repository serves as a practical example of Rust's dependency resolution capabilities, particularly in scenarios involving incompatible versions, while contrasting it with other package management systems.

- Rust's Cargo allows multiple incompatible library versions to coexist without compilation failures.

- The repository includes an example demonstrating dependency management with conflicting versions.

- Cargo compiles separate versions of libraries as needed, unlike Python's pip.

- The approach is similar to Node.js's npm, which also supports multiple incompatible versions.

- The repository serves as a practical guide for understanding Rust's dependency resolution.

AI: What people are saying
The discussion around Rust's Cargo package manager and its ability to handle multiple library versions reveals several key points of contention and insight.
  • While Cargo allows multiple versions of libraries, some users express concerns about potential runtime issues and conflicts that can arise from this flexibility.
  • There are suggestions for how Python could implement similar functionality, but concerns about complexity and the potential for breaking existing code are prevalent.
  • Some commenters argue that preventing multiple versions can lead to cleaner dependency management and fewer surprises in the long run.
  • There is a recognition that while Cargo's approach can be beneficial, it may also introduce new challenges that developers need to navigate.
  • Historical references to Python's past support for multiple versions highlight the ongoing debate about the best way to manage dependencies across programming languages.
Link Icon 15 comments
By @richardwhiuk - 8 months
You can do this in one crate:

    [dependencies]
    foo_v1 = { package = "foo", version = "1" }
    foo_v2 = { package = "foo", version = "2" }
By @WD-42 - 8 months
This isn’t a magic bullet. Using multiple versions of the same crate can still blow up your project.

For example, the compiler error in this example:

note: perhaps two different versions of crate `smithay_client_toolkit` are being used?

https://github.com/pop-os/launcher/issues/237

By @woodruffw - 8 months
I thought this was about loading two incompatible versions of a shared object into the same address space at first :-)

The author correctly contrasts Rust (and NPM's) behavior with that of Python/pip, where only one version per package name is allowed. The Python packaging ecosystem could in theory standardize a form of package name mangling wherein multiple versions could be imported simultaneously (akin to what's currently possible with multiple vendored versions), but that would likely be a significant undertaking given that a lot of applications probably - accidentally - break the indirect relationship and directly import their transitive dependencies.

(The more I work in Python, the more I think that Python's approach is actually a good one: preventing multiple versions of the same package prevents dependency graph spaghetti when every subdependency depends on a slightly different version, and provides a strong incentive to keep public API surfaces small and flexible. But I don't think that was the intention, more of an accidental perk of an otherwise informal approach to packaging.)

By @gorgoiler - 8 months
For fun, you could add this to Python and I think it would it cover a lot of edge cases?

You would need:

A function v_tree_install(spec) which installs a versioned pypi package like “foo=3.2” and all its dependencies in its own tree, rather than in site-packages.

Another pair of functions v_import and v_from_import to wrap importlib with a name, version, and symbols. These functions know how to find the versioned package in its special tree and push that tree to sys.path before starting the import.

To cover the case for when the imported code has dynamic imports you could also wrap any callable code (functions, classes) with a wrapper that also does the sys.push/pop before/after each call.

You then replace third party imports in your code with calls assigning to symbols in your module:

  # import foo
  foo = v_import(“foo==3.2”)

  # from foo import bar, baz as q
  bar, q = v_from_import(
    “foo>=3.3”,
    “bar”,
    “baz”,
  )
Finally, provide a function (or CLI tool) to statically scan your code looking for v_import and calling v_tree_install ahead of time. Or just let v_import do it.

Edit: …and you’d need to edit the sys.modules cache too, or purge it after each “clever” import?

By @btilly - 8 months
This is great for avoiding conflicts when you try to get your project running.

It sucks when there is a vulnerability in a particular library, and you're trying to track all of the ways in which that vulnerable code is being pulled into your project.

My preference is to force the conflict up front by saying that you can't import conflicting versions. This creates a constant stream of small problems, but avoids really big ones later. However I absolutely understand why a lot of people prefer it the other way around.

By @alkonaut - 8 months
How does this work? Assume that the log crate in its internal state has a lock it uses for synchronizing writing to some log endpoint. If I have two versions of log in my process then they must have two copies of their internal state. So they both point to the same log endpoint, but they have one mutex each? That means it "works" at compile time but fails at runtime? That's the worst kind of "works!"

Or if I depend transitively on two versions of a library (e.g. a matrix math lib) through A and B and try to read a value from A and send it into B. Then presumably due to type namespacing that will fail at compile time?

So the options when using incompatible dependencies are a) it compiles, but fails at runtime, b) it doesn't compile, or c) it compiles and works at runtime?

By @dboreham - 8 months
Every language will re-create its own version (sic) of DLL-hell.
By @pjmlp - 8 months
And this is why one gets to watch some crates being compiled from scratch multiple times in a single "make world" build.
By @aragilar - 8 months
FYI, Python can/did support multiple versions via buildout (http://www.buildout.org/en/latest/) but it's complicated and wide-scale support has probably bit-rotted away.
By @anonymoushn - 8 months
You can do this, but you can't use two semver-compatible versions of the same library in *different binaries* in the same workspace.
By @hsfzxjy - 8 months
So both versions of log crate manage their own internal states within the same process? Would this lead to surprising results?
By @jcelerier - 8 months
How does that work if you want to export a symbol for dlopen?
By @gmueckl - 8 months
I cannot shake the feeling that this is actually a misfeature that will get people into trouble in new and puzzling ways. The isolated classloaders in Java and the assembly domains in .Net didn't turn out to be very bright ideas and from a software design perspective this is virtually identical.