You can do this in one crate:<p><pre><code> [dependencies]
foo_v1 = { package = "foo", version = "1" }
foo_v2 = { package = "foo", version = "2" }</code></pre>
This isn’t a magic bullet. Using multiple versions of the same crate can still blow up your project.<p>For example, the compiler error in this example:<p>note: perhaps two different versions of crate `smithay_client_toolkit` are being used?<p><a href="https://github.com/pop-os/launcher/issues/237">https://github.com/pop-os/launcher/issues/237</a>
I thought this was about loading two incompatible versions of a shared object into the same address space at first :-)<p>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 <i>could</i> 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 <i>lot</i> of applications probably - accidentally - break the indirect relationship and directly import their transitive dependencies.<p>(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.)
For fun, you could add this to Python and I think it would it cover a lot of edge cases?<p>You would need:<p>A function <i>v_tree_install(spec)</i> which installs a versioned pypi package like “foo=3.2” and all its dependencies in its own tree, rather than in site-packages.<p>Another pair of functions <i>v_import</i> and <i>v_from_import</i> to wrap importlib with a name, version, and symbols. These functions know how to find the versioned package in its special tree <i>and</i> push that tree to sys.path before starting the import.<p>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 <i>also</i> does the sys.push/pop before/after each call.<p>You then replace third party imports in your code with calls assigning to symbols in your module:<p><pre><code> # 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”,
)
</code></pre>
Finally, provide a function (or CLI tool) to statically scan your code looking for <i>v_import</i> and calling <i>v_tree_install</i> ahead of time. Or just let <i>v_import</i> do it.<p>Edit: …and you’d need to edit the sys.modules cache too, or purge it after each “clever” import?
This is great for avoiding conflicts when you try to get your project running.<p>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.<p>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.
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 <i>each</i>? That means it "works" at compile time but fails at runtime? That's the worst kind of "works!"<p>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?<p>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?
FYI, Python can/did support multiple versions via buildout (<a href="http://www.buildout.org/en/latest/" rel="nofollow">http://www.buildout.org/en/latest/</a>) but it's complicated and wide-scale support has probably bit-rotted away.
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.