<import "base.kuht" as "base" />
<head>
<title>What Other Distros Can Learn from NixOS</title>
<meta name="description" content="Traditional distros will either have delayed updates or unstable updates. NixOS allows us to have both." />
</head>
<body>
<article>
<h1>What Other Distros Can Learn from NixOS</h1>
<p>
As a warning, this blog post contains the ramblings of an Arch user who
barely knows what a flatpak is and has like, three of them on their system.
Their background is in web deployment pipelines and they're trying to apply
their personal beliefs to Linux distributions, like an idiot, without even
making a proof-of-concept. Reader discretion is advised.
</p>
<p>
One debate that often comes up is static versus dynamic linking. Most Linux
distributions try to manage dependencies by dynamically linking them. This can
be good, because if a previous version of a library contains a bug, then it's
easy to distribute the bug fix and have it apply to all applications
simultaneously. The downside is that if a newer version of a library contains
a bug, or a backwards-incompatible change, then that will apply to all
installed applications. Dynamic linking is a double-edged sword that makes it
both easier to fix bugs and easier to create new ones.
</p>
<p>
But NixOS offers strict control over both the release and use of libraries,
which allows applications to get both the ease of rollout that comes with
dynamic linking, and the control that comes from static linking.
</p>
<h2>Background</h2>
<p>
When an application is installed on NixOS, it is not put in the
<code>/bin</code> folder. In fact, this folder is almost entirely empty.
Instead, it goes to <code>/nix/store</code>, in a directory with an
unreadable hash. Then, when you boot the system, the files for your system
applications are symlinked into <code>/run/current-system/sw</code>, which is
in the <code>PATH</code>, so these applications can run in your terminal.
</p>
<p>
But that's only system applications. For libraries, the application sets its
own <code>LD_LIBRARY_PATH</code> variable. This tells the dynamic linker
where to fetch the required libraries from. This isn't exactly what the file
looks like, but you can imagine it looking something like this:
</p>
<pre>
#!/path/to/bash
LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/nix/store/naioghuraopjfi4738-libgit2-2.23"
export LD_LIBRARY_PATH
exec -a git "$@"
</pre>
<p>
The benefit of this system is that you can multiple versions of the same
library installed on your system, and different applications can depend on
different versions, depending on what they need.
</p>
<p>
I want to note that there's nothing special about NixOS that would make this
impossible on other systems. Flatpak already allows applications to bundle
their own versions of libraries, and Snap has a similar system. So I think
other distros can still benefit from this system.
</p>
<h2>Testing</h2>
<p>
Before I go into the benefits of the system described above, I'll touch on
something that distros can do already, but that specifying dependency
versions might make easier.
</p>
<p>
Before you can even update the repository, you'll have to test the library.
Hopefully, the maintainer of the library has already done this for you, by
creating a suite of unit tests that they ran before creating the release. I
suppose it wouldn't hurt to double check.
</p>
<p>
The more interesting test you'll need to run is similar to what
<a href="https://github.com/rust-lang/crater">crater</a> does for the Rust
ecosystem. Whenever a change is made to the language, Rust runs crater to
determine if the change would cause a breaking change for any packages on
crates.io. It's a very brute-force way to test for new bugs.
</p>
<p>
In order for this to work, not only does the library need a unit test suite,
but so does every application that depends on that library. You'll build
every single dependent against the newer version of the library, and then
test all of them to see if anything breaks.
</p>
<p>
Inevitably, some applications will fail under a new version of a library.
This doesn't necessarily need to be because of a problem in the library
itself, but it could be due to a transient failure in the application. In
such a scenario, the application's dependency would be updated to say
<code>3.13</code> instead of <code>3</code>, so it doesn't block other
applications from using the newer version.
</p>
<p>
It would also be beneficial to store somewhere in the package which version
of each dependency an application was last tested with. Then, if you want to
be extra safe, you can delay upgrading the dependency until you know that a
test has been run on the latest dependencies.
</p>
<h2>Beta Channel</h2>
<p>
The delayed release of packages is what separates Arch Linux from Manajaro.
Some users are more interested in being on the bleeding edge than others. I
don't see any reason why two separate package repositories are needed for
this. We can allow users to opt-in to the bleeding edge for specific packages
faster. These people can submit bug reports before the new features make
their way to regular users.
</p>
<p>
NixOS has this in the form of the <code>unstable</code> channel. Users can
opt-in to using the unstable channel to get an experience more similar to a
rolling release distro. And judging from some Reddit threads, there are
people using the unstable channel regularly. This makes sense to me, since
not everyone who uses Nix is there for stability. Some people just like
having a central config file. NixOS also lets people go one step further and
use <code>unstable</code> for specific packages, but leave the rest of their
entire system stable.
</p>
<p>
Other distros do have this. Most commonly this comes in the form of an LTS
variant of the distro, which most users are expected to stay on. The non-LTS
variant is often treated as a beta.
</p>
<h2>Rollout</h2>
<p>
The rest of the ideas presented in this post are ideas that can be done as
long as you have a way to pin dependency versions, but they're not done by
NixOS. How important these are is debatable, since NixOS already tests their
stable channels before releasing them.
</p>
<p>
Not every package is of equal importance. A change to a library affecting
pacman is far more critical to a library that only affects GIMP. But thanks
to the system we have here, it doesn't need to affect every package at the
same time. We can rollout a library change to less important packages first
before we apply it to every library.
</p>
<p>
To give an example of how this would work, let's say a new library version
releases, and it affects both pacman and GIMP. For this to work, we'd need to
assign a priority level to every package. Pacman would probably be a level 2
package, meaning it's very important. There should only be a few level 1
packages. GIMP would be level 4, so not very important. As a level 4 package,
GIMP would be one of the first packages to use the new version of the
library. After a couple days, pacman would also start using the new library.
</p>
<h2>Rollback</h2>
<p>
So with all that in mind, what happens when a library actually is bad? Well,
marking the library version as bad is luckily pretty simple. We just update
the version resolver to say that if a package asks for version
<code>3</code>, don't use <code>3.14</code>. Prefer <code>3.13</code>
instead. Pacman already caches older versions of packages, so you might not
even need to re-install <code>3.13</code>.
</p>
<p>
Of course, rolling backwards isn't always safe. Some newer packaages might
already be using features of <code>3.14</code> that don't exist in
<code>3.13</code>. To partially resolve this, we can run the test suite
against each package before downgrading to <code>3.13</code>. Hopefully
everything will pass, but if a particular package does not, then we can set
that package specifically to require a minimum of <code>3.14</code>. We can
also mark this downstream package as bad, and prevent new installations of it
until a patch is made.
</p>
<h2>Distribution</h2>
<p>
There's one thing here that we can't control: the distribution. Meaning, we
don't know when the user is going to run an upgrade. Unlike in Windows land
or web land, we don't run the upgrade automatically. Some systems will at
least automatically check for updates and notify the user when there is one,
but there's usually no distinction between critical bug fixes and feature
updates.
</p>
<p>
I would recommend having a daemon that at least automatically downgrades in
the case of bad library versions. Since we can cache older versions of the
library, this wouldn't require too much network activity. And we can always
give the user an option to disable it.
</p>
<p>
NixOS actually does support full automatic upgrades in the background. Some
NixOS users report that because NixOS is so stable, they feel comfortable to
just letting everything update in the background without intervention. The
NixOS wiki provides this example on how to do so.
</p>
<pre>
system.autoUpgrade = {
enable = true;
flags = [
"--print-build-logs"
];
dates = "02:00";
randomizedDelaySec = "45min";
allowReboot = false; # Set to true if you want automatic reboots
};
</pre>
<h2>Conclusion</h2>
<p>
I don't want to say none of these solutions come with caveats. NixOS has
trouble running arbitrary executables because of how it goes about
accomplishing its goals. The way things are set up on NixOS can sometimes be
confusing to newcomers, and I understand why a Linux distro wouldn't want to
copy everything from NixOS. But I think even a best-effort attempt at some of
the ideas that NixOS provides could provide major benefits for stability.
It's just about choosing the right compromises.
</p>
|