Hey there - first off, massive thanks for all the work you folks have been doing!
I’ve been using nix for a short while and I’m now moving to nixOS which, for the most part, has been a great experience! A key issue I’ve had while using nix however has been package versioning.
I’m not sure how to best describe this so I’ll just provide a comparison to how package versions are managed in nodejs.
Node package managment (npm)
A list of dependencies are declared as follows (psudeosyntax)
{
"package1": "latest",
"package2": "latest",
"package3": "latest"
}
In the above example, because the dependency version is not pinned, the latest version from the registry will be used. This resolved “latest” version is then pinned to a lockfile and can only change once the lockfile is rebuilt.
If a user wants to explicitly use a particular version of a package, they can specify the version of the package they want - this will have no impact on other packages.
{
"package1": "latest",
"package2": "latest",
"package3": "1.0.0"
}
Here’s the important bit - no matter how many new releases of a package are pushed to the registry, the previous releases are still available.
All packages have access to all versions of current and previous packages. If we consider transitive dependencies, updates of packages happen in isolation and do not introduce restrictions on their peers. Packages are not coupled to their peers - only their own version and dependencies
{
"package1": "latest"
}
// package1 trans deps
{
"package3": "1.0.0"
}
// package1 trans deps
{
"package3": "2.0.0"
}
In this sense, packages are very much contained.
Note: Sometimes some magic takes place to deduplicate transitive dependencies for code-reduction reasons but that’s outside the scope of this discussion.
Nix package management
A list of dependencies are declared as follows (psudeosyntax)
pkgs.package1
pkgs.package2
In this case, versioning is pinned to the version of the registry, not the version of the package. This is where the problems start.
Updating package1
requires an update of the registry (i.e. nixpkgs) which will in turn update package2
and package3
. This means, while nix packages are designed to be used in complete isolation, they are inherently coupled to a monolithic registry.
Overlays can solve this problem for the most part (e.g. pulling different releases of registries and overlaying the top-level package) but in the case where different versions of transitive dependencies are required - you’re kind of stuck.
I came across this issue when trying to use a previous version of Gnome3 packages, while also using a recent registry release.
error: builder for '/nix/store/yf1j7wq8jq6kdfw32x9gq5whxg1mgqpv-yelp-tools-40.0.drv' failed with exit code 1;
last 10 log lines:
> Program python3 found: YES (/nix/store/66fbv9mmx1j4hrn9y06kcp73c3yb196r-python3-3.8.9/bin/python3)
> Found pkg-config: /nix/store/b8mzg1b1s3lh2zvmz1yichqprzhi0f2d-pkg-config-wrapper-0.29.2/bin/pkg-config (0.29.2)
> Dependency yelp-xsl found: NO found 3.36.0 but need: '>= 3.38.0'
> Did not find CMake 'cmake'
> Found CMake: NO
> Run-time dependency yelp-xsl found: NO
>
> meson.build:30:0: ERROR: Invalid version of dependency, need 'yelp-xsl' ['>= 3.38.0'] found '3.36.0'.
>
> A full log can be found at /build/yelp-tools-40.0/build/meson-logs/meson-log.txt
For full logs, run 'nix log /nix/store/yf1j7wq8jq6kdfw32x9gq5whxg1mgqpv-yelp-tools-40.0.drv'.
error: 1 dependencies of derivation '/nix/store/qc5wv1fkkmyja9cvlj3axy5khwp74crm-gnome-user-docs-40.1.drv' failed to build
error: 1 dependencies of derivation '/nix/store/8cfi7c29p519rc9ygpwgf7imkgfzv7sz-system-path.drv' failed to build
error: 1 dependencies of derivation '/nix/store/z146mqk8rxqvl4qpb4zyp7v317r4vgs7-nixos-system-nixos-21.11.20210603.95222ea.drv' failed to build
I was now stuck with the options of:
- Rolling back the whole registry (and all system package versions)
- Overlaying the gnome packages, and every single one of it’s transitive dependencies (which would impact all packages)
Solutions
There’s a good chance I’m may be missing something but as far as I can tell, there isn’t really a solution for version management currently.
While the NPM approach isn’t perfect, I think there are some key takeaways that nix could massively benefit from:
- New releases to the registry (or of the registry in nix’s case) don’t need to break previous releases
- Packages can be updated independently (not a monolithic system)
- Redundancy when working with transitive dependencies is a good thing
The most primitive way I can see this working in nix would be by keeping release versions of packages. In the case where an unpinned version is used, an update to the registry would then cause the unpinned version to be updated.
{
mypackage = mypackage@1; # manually updated on major releases
mypackage@1 = mypackage@1.3; # manually updated on minor releases
mypackage@1.3 = mypackage@1.3.0; # manually updated on patch releases
mypackage@1.3.0 = pkgs.stdenv.stdenv.mkDerivation # ...
mypackage@1.2 = mypackage@1.2.1; # manually updated on patch releases
mypackage@1.2.1 = pkgs.stdenv.stdenv.mkDerivation # ...
mypackage@1.2.0 = pkgs.stdenv.stdenv.mkDerivation # ...
}
In this example, the following dependency would work indefinitely, regardless of how many future releases of it’s dependent packages (or the registry) are published:
{
somepackage = somepackage@1;
somepackage@1 = somepackage@1.1;
somepackage@1.1 = somepackage@1.1.0;
somepackage@1.1.0 = (pkgs.stdenv.mkDerivation {
buildInputs = [mypackage@1];
});
}
It would also automatically have transitive dependencies updated as new registry versions are pulled.
I’ve spent way too long on this so i’m going to stop rambling but it would be great to get some thoughts on this!