About one year ago, I started running NixOS as my daily operating system, and after this one year and much reflection, I’ve concluded that it is time to switch away form NixOS as my primary system again. This blog post will serve as a snapshot of what it was like for myself to be using NixOS, both a reflection of what I found appealing about NixOS and my own learning along the way, as well as the frustrations that ultimately pushed me away.
What NixOS does well
Closeness of package installation and configuration — system replicability
I have two primary machines that I daily drive, and I want the experience between these two machines to be as identical as possible, meaning that I want the same set of programs installed with as-close-to-identical theming and configurations as reasonably possible. This means keeping both the master package list synced, along with a handful configurations files that is relevant to the usage experience. The problem, however, comes when there are configurations that are intrinsically different the 2 machines: slightly different hardware capabilities requiring different driver packages; different screen real estate requiring slightly different configuration fields; different storage sizes means that backup handling is different and such. When you want something to be “nearly the same”, it gets stuck in the awkward middle ground of not being able to stuff everything in a git repository and call it a day, but at the same time much too cumbersome to maintain two separate instances of the configurations files because too many entries needs to be kept in sync.
The Nix way of configuration systems is glorious: the requirement of a package is almost immediately followed by the configuration snippet, and the configuration being part of a programming logic means that logic can be built into that extract configuration: the same package can be installed with slightly different configuration flag based on the differences between the multiple systems that you are maintaining, all the while retaining identical configurations everywhere.
The fact that nix generates the actual configuration files that are placed in the /etc
or other directory during build
time also means that it solves one of my biggest grips managing configuration files on Linux: configuration files can
grow into monstrous multi-thousand line entities, while only three lines are relevant to the configuration that I want
to do. The nix configuration system means that the “configuration” that I interact with (i.e. the .nix
file in
question) needs only to keep those relevant lines massively reducing the amount of stuff I need to keep in my
configuration repository.
Another problem that the nix method of configuration solves is that cross-package configurations can now be done in whatever method makes the most sense for system maintainer: for example, instead of all “network configurations” sitting in a gigantic file, with comment snippets that point to the configuration of other programs that may or may not be out of date, nix then allows the network configurations to sit directly behind each package install, so that it is much easier to keep track of why configurations are enabled, and also cleanly disables them when a package becomes obsolete.
The tools provided in nix is the closest thing I got to system replicability (not reproducibility, that is a
distinction that I will discuss in a little later). The fact that I can run nix
as a local package manager also means
I can replicate all my terminal experience to the many computing clusters that I need to interact with was the biggest
push for me to start investigating how to use nix in the first place.
Exotic package dependencies and explicitly adding dependencies
Despite the best efforts of distribution maintainers, there will be odd cases where the official package manager does not properly list “all” dependencies. Maybe there is some odd format conversion routine of package “A” that requires extra python package “B” and is not listed because it is not considered a “core” feature of the primary package (One example that I ran into was the conversion o DXF files to regular SVG files using inkscape). So you go ahead and explicitly install this package in the command line package “B”. Great, now there is this dangling package “B” that is labeled as being explicitly requested user, but to the package manager, this package is completely unrelated to package “A”; the problem here goes both ways: if you no longer use package “A” and uninstall it, you now have this unnecessary package “B” floating around in your system; or after sometime when performing system maintenance, you see that package “B” is not required by anything else, so you uninstalled it because you think you don’t need it, thereby unwittingly breaking that fix for package “A”.
The nix way of adding packages to the system means that there such dependencies can actually be properly tracked: for example, when you install a packaged with python as a dependency, you can also specify exactly which addition python packages you want to go along with the python environment in question.
Temporary development installs
C and C++ does not have package managers. So when you need to build a C/C++ project, you might be tempted to install the build dependencies with your system package manager. Great! Now you have a whole host of low-level packages that you don’t know if you can or cannot install after you are done with the project, or after you moved the project else where. This environment isolation problem for C/C++ project is probably why all newer languages try to have this function built in.
The shell
function of nix
essentially the functionality of project-level package management to anything
you might be using, regardless what language you might be using (even Fortran
if you still need to work with that for
whatever reason).
This also extends to the use of temporary packages for interactive sessions. If there is a transient issue that you need
to solve just this once, you can quickly jump into a shell with the temporary diagnostic tools, and cleanly jump out,
where it would look like the package never existed in the first place (the actual files are, of course, cleaned up after
the next nix-garbage-collect
call).
Explicitly defining what is exposed to the user — functionality hiding
For certain packages, you might just want some of the functionality to be exposed to the user: An example is that you
might want to have the icat
module around from the kitty
terminal for the fullest implementation of the
image display protocol, but still want to use your favorite terminal emulator instead of kitty
, and/or you
don’t want an extra application entry taking up space in your application manager. In nix
, there are ways to make sure
packages are installed, but only exposed to the user through other functionalities.
I add this mainly as a nicety, not strictly as a “must have” for system management. I like having my environment clean, but not to the extent that all unused entries should be purged at all times.
In this one year experiment of daily driving NixOS, it made me think more carefully about what package-manager systems should try and do, what makes for a “good” system maintenance experience, and where other package managers (system or otherwise) are failing to provide tools that is useful. And just for gaining this experience, I would always be happy that I took this route of trying to daily drive NixOS.
And now for the parts of NixOS that ultimately pushed me away from NixOS in search of another solution.
What makes Nix infuriating/frustrating
Doing everything “the Nix way”
While I do think nix configurations is amazing, at the end of the day it is just another layer of abstraction on top of the existing abstractions found in Linux configuration system.
At some points, it felt redundant: Is having a line vim.opt.relativenumber = true
in a lua file really that much
different than adding, neovim={enable=True; relativenumber=True}
in a nix file? Especially when neovim’s lua
file
will eventually contain additional items anyway?
At other points, it’s frustrating when the abstractions are leaky. If the option flag that you are looking for is not implemented in nix, you either have to implement the functionality in nix yourself (after a year I still don’t properly understand how the think in terms of functional programming languages, more on that later), or resort of having the “file contents” be injected into configuration files, at which point the question becomes why don’t I just managed configurations directly with the configuration files instead of having nix as an “extra layer”?
The worst problem is when nix abstraction bleeds into everything in a way that is incompatible with any other systems.
One of the more egregious examples I ended up having was the configuration files I had for zsh
, where I realized that
the generation of the zsh
is so fragmented that I would be impossible to move away from nix without major refactoring
of how machine-specific configurations are handled. Packages not being able to be ported between Nix
systems and other
FHS
-based systems was already becoming problematic, and it hit me that if my configurations were going this
route as well, how much am I really giving up for the promise of reproducibility?
No proper method of temporarily pinning a package
While there is a promise of reproducibility and rollback on the system level, what I found in practice is that getting
the nix to actually install packages that was not on the HEAD
of the nixpkgs
channels was incredibly involved. Here
is an example that actually happened to myself: there was a regression in a GUI application (KiCad in this case),
where I need to roll back to version 8.0.X
while the leading branch is at 8.0.Y
. To do so in nix, I need to either:
- Split my repository reference
nixpkgs
intonixpkgs-unstable
(rolling release) andnixpkgs-24.11
(latest stable), and point just one specific package to usernixpkgs-24.11
, this is, of course, working on the assumption that version that I wanted was still being used in the stable branch, if not, I would need to point to an even oldernixpkgs
version, where there isn’t an easy way to look up which version of in whichnixpkgs
branch, and pray that that branch is still kept in the official nix cache system. If it is not in the cache system, I’m looking at building the “entire” package compile chain (yes, including the all the compilers of low-level languages and all the high-level language interpreter of that specific branch) on my personal machine. - Maintain a temporary branch of the
<package>.nix
file to always use a custom version. (Which is something that I don’t want to do)
When I was using ArchLinux, rolling back to a version is effectively: going to the Arch time machine to find the
snapshot of the package at a certain date, download the package and install it with pacman -U
. Sure, now there is no
guarantee that there is package will 100% function as with the NixOS forcing you to replicate the entire environment at
some snapshot, but in most cases, I’m not trying to roll back a package to a completely different version, this is just
a sub-sub version change, where I can most likely track the total number of changes that are done, do I really want to
have a complete separate compile environment in my machine just to roll back?
At least for my use cases, the promise of “NixOS being declarative” feel just a bit broken. In language-specific package
manager (think pip
and npm
related tool chains), pinning a package version is as
straightforwards as changing a few lines in the configuration to something like package1==x.y.z
. There are no such
equivalences in nix
, which, at least to myself, defeats quite a bit of the purpose of wanting to claim your entire
system is declarative: You can only declare what is allowed by nixpkgs
. I would much rather nix
have some equivalent
of pip
/npm
’s system, where you can explicitly pin package version, and then complain loudly if something is
obviously in conflict.
nixpkgs
repository
The fragility of the monolithic Despite the mantra of NixOS being of package reproducibility and dependency isolation, I have found that, in practice,
the rolling release of the NixOS package repository to be surprisingly fragile and prone to breaking, especially when
compared to other rolling release versions like Archlinux. During my one-year span of using NixOS, multiple times the
“stable” version of the Linux kernel and “stable” version of the nvidia drivers where simply not compatible, large GUI
packages having esoteric build fails after a repository update (FreeCad and KiCad are particularly prone to this for
some reason); and to top this all off, the update to the rolling release branch simply is not as immediate as other
distributions. While I found this odd that there are so many hiccups in the package distribution process for a Linux
distribution that has been around nearly as long as Arch, I still understand that nix
expects users to be more hands on with package management. All of this would have been tolerable if it was not for the
final issue that I don’t see an easy way of fixing.
The lack of documentation
This was probably the final straw that pushed to start looking for nix alternatives after the whole fiasco with package
pinning and breaking repositories. Nix, being a functional language, is already hard enough to generalize as
is, and when trying to look up how to do something, there isn’t centralized wiki that I can reference (I mean,
there is, but that hardly contains enough information, and the fact there are two wiki’s
also highlights the problem), and I have to resort to scouring Reddit or Discourse for
solution patches that I don’t fully understand, have difficultly generalizing to a more complete solution, and am not
certain will still be a good solution 6 months down the line. Maybe the documentation is fine, and I just too
procedural-oriented to actually understand now to navigate nix documentations, but having to learn all the oddities of a
functional language, the oddities of interacting with nixpkgs
, and having no solid ground to work off in terms of
references is just too much, especially in a pinch when I need to change something “right now”.
What do I actually want in from Nix-like systems
After some long reflections just after the frustrating experience with nix
, I wanted to rethink why it was that I was
attracted to nix
in the first place. Is it actual reproducibility and declarative-ness? To be honest, not really. I
don’t actually care what my system looks like 100 day ago, and I certainly am not going to dedicate hard drive space for
a snapshot of my machine state to trace these steps. If this is not the actual target that I want out of my system
management experience, I don’t think NixOS is actually the distribution for me.
What I really want out of nix
, when tracing the line-of-thought in my initial praise of nix
, can effectively be
boiled down to just 2 points:
- Closeness of installation and configurations.
- Ease of temporary installs and cleanup
What I actually want is replicability: the ability to ensure the same experience across multiple machines; while reproducibility should always guarantee replicability, just wanting replicability should not require the amounts of stringent-ness and tie-in as NixOS demands.
Is there a way to achieve what I want without having to completely dive tied to the nix
ecosystem, outside of
hand-rolling everything myself?
decman
The alternate solution — Turns out, I’m not the only one what has the same frustrations with NixOS, and a solution for Archlinux has already
been developed: decman
!
Breaking it down, decman
takes the listed packages (both standard and AUR) and uses this to generate a list of
install/remove commands after comparing this with the current system state. It has very base-line modules to also handle
the generation of files to fixed paths, as well as listing commands to run after installation/upgrades (such as enabling
services), this effectively covers all the basis on my requirements! But what is my operations is not covered by
existing decman
functionalities? Because decman
is written in Python, it is very easy to patch in
functions. And being python, this is not just limited to the simple stuff like adding machine/user-specific
configurations using if
statements on environment. Need something in incrementally update a cfg
file? Python has a
module for that! Want to use symbolic links instead of explicitly writing file contents for certain
configuration files? Python has a module for that! Want to download something from a fixed URL to use for
your configuration? Python has a module for that! Want to perform the same operation over a list of
files/modules? You can write looping in whatever method you have in mind!
It felt very freeing to be able to patch whatever function one has in mind directly on-the-fly with whatever modules
Python has to offer instead of having to wade through the fragmented information of how nix
/nixpkgs
functions on the
internet. The decman
package serves as a nice base to start off “80% of the way there”, and you the then free to build
up extra extensions that best suit what you want to do with your system. This ethos I feel is much more fitting of how I
want to manage my personal systems. Nix in comparison feels much more robust (less likely to generate something that can
catastrophically break your system), but at the same time much more rigid (not being allowed to do something outside
predefined limits) to the point of feeling suffocating. A similar statement seems to be from the developer of decman.
What about remote package installation?
One of the big benefits that pushed me to working with nix was the ability to install arbitrary packages on any machine,
regardless of if I have root access or not. If I am no longer using nix
, what am I going to do about that? Another
Linux distributions nicely slides into the picture: Gentoo! Their “prefix” project for allowing the
Gentoo build system portage
to be portable on any POSIX-like machine is very simple to set up! You
can find the instructions here, and it is basically [3 lines of commands][prefix-boostrap] (minus the many, many hours
you will need to wait for the package compilation)
Actually, that was a lie, because the CERN AFS file system is an incredibly annoying file system to work with. In this particular case, AFS not allowing pipe files to be created, breaking many of the installation scripts handled by the Gentoo package manager. Luckily, Gentoo has the foresight of isolated all building actions into a dedicated
$EPREFIX/tmp
directory, so as long as you bind that path to a directory that is not under the AFS directory (like the system/tmp
directory), most install scripts would work. Another outlier is during the installation of thesgml
package used for documentation generation, as when it invokes theflock
command, AFS will effectively freeze up. To get around this, you actually need to installsgml
on another machine, then copy the contents in the$EPREFIX/usr/share
directory under AFS. To reiterate, this is not a problem with Gentoo, but a rant of how AFS is a terrible file system to work with from the user side.
Having everything compile from source also has other benefits compared with running nix
. Even though
nix-portable
tries to be as “native” as possible, nix-portable
having to go through proot
still introduces oddities that is unavoidable: users ID needs to be masked, so all other users showing up as nobody
when inspecting system resource usages in htop
is one example. With everything being compiled from source, packages
installed with portage
is about as native as one can get, with basically mirrored functionality from what I have on my
local installations. This is not to say portage
was all smooth sailing: btop
simply refuses to compile is oddity
that I have not solved yet, and getting GUI programs such as kitty
to compile required additional flags to be toggled
in the prefix system (something that was not immediately obvious to me when I first ran the command). The replicability
aspect right now doesn’t expand beyond keeping a list of packages in plain text, but with proper
configuration file management, it is already providing 95% functionality of what I want out of nix running
on remote machines.
nix
?
Will you never be using I think nix still has its place. decman
is effectively a wrapper for vanilla pacman
, and is not designed to work on
per-directory or per-project bases, only for system level package management. So when I am working on a project that
uses a tool chain that does not have a good in-built package management system (think C/C++), then nix
is the perfect
solution for this.
Conclusion
So TL;DR: Got skill-issued by a functional language, so I’m giving up on NixOS for now. The tools that I will now be using be using from now one is:
- Archlinux with
decman
for all my personal machines. - [Gentoo][`portage`]gentoo-portage for machines where I don’t have root access.
- Nix as the package manager for individual project.
Are these tools perfect? Far from it, and there is much configuration to be done (THE RICING NEVER STOPS). But the biggest upside for using this is I can now say that: