lgrant The classic approach with shared libs (
.so) already allows multiple versions. A library typically has a version number with multiple components, like
libfoo.so.1.2.3. It also has a
SONAME
property in its headers containing only a part of the version number, most commonly just the first component, so here
libfoo.so.1
. The library is then installed with two symlinks:
Code:
libfoo.so.1.2.3
libfoo.so.1 => libfoo.so.1.2.3
libfoo.so => libfoo.so.1
When you upgrade to a newer major version, but need to keep the old one, you'd see something like this:
Code:
libfoo.so.1.2.3
libfoo.so.2.0.0
libfoo.so.1 => libfoo.so.1.2.3
libfoo.so.2 => libfoo.so.2.0.0
libfoo.so => libfoo.so.2
The source of a program needing
libfoo
will
typically just link
libfoo.so and therefore get whatever this symlink points to. This will add a dependency in the binary to the
SONAME
, so e.g.
libfoo.so.2
. Some program compiled with
libfoo.so pointing to the older major version will always request
libfoo.so.1
from the runtime linker.
This approach works perfectly well as long as:
- For every major version, you make sure to have the latest version of the library installed
- The library author carefully versions the library: Additions are always ok, but whenever there is a breaking change, the part of the version that's in
SONAME
must be bumped
It is also very friendly for package management, in theory, you can package every library separately and upgrade your package (for the same
SONAME
) independently of any consumers without ever causing breakage.
In practice, it happens from time to time that a library update breaks stuff without
SONAME
getting bumped. If this happens, it's an individual versioning error on the side of the library.
Now we enter the "wonderful"? world of language-specific package managers (like nuget, cargo, npm, ...). They try to solve the problem by having every program specify the full exact version of its dependencies. Typically, the libraries are just linked statically into the final binary, or they're bundled on installation. If I'm not mistaken, Rust offers a way for dynamically linking, which only works if all components were build with the same rust version, but that doesn't help much, you'd end up with tons of incarnations of the same library installed. To "simplify" things, there's often the possibility to have a "vendor" subtree in your source repository, containing full copies of all your dependencies. This indeed simplifies the packaging work, but doesn't solve the other issues: You clutter the installation with a huge amount of libraries (in case of dynamic linking) or you install many unnecessarily fat binaries (in case of static linking), and you completely lose the ability to upgrade a library independently of its consumers. The latter is very relevant when a library has a security vulnerability. With the classic shared libs, you'd upgrade let's say the package
libfoo2
, replacing
libfoo.so.2.0.0 with
libfoo.so.2.0.1 which fixes the vuln. Every consumer requesting
libfoo.so.2
will automatically use the fixed version. But if the exact library is baked in everywhere, you'll have the maintenance nightmare to analyze the dependency trees of all binaries, and upgrade all of these affected.