Do package managers dream of declarative sheep?
The year was 2014ish when I had my first Linux ritual initiation:
"Hm, I will just delete this package, doesn't look like something I use. Yeah, a ton of text, confirm confirm. Shit's easy."
Inevitably, unaware of what the calamity I have just caused, I shut down the PC.
Next time, when I turned it on, it boots into something looking like this: #
Luckily, in those days, I was no stranger to distro hopping, so this was hardly even a speck of dust on my Ubuntu-pink glasses. I would've probably installed a new distro even if I didn't already nuke the previous one (I want it to be noted, I loved them all equally, it was not a them type of problem). Good times.
Anyway, not sure how, presumably after a lot of trial and error, and some more trial-less erroring, I realized you are supposed to read the output when you delete the stuff.
And you are kind of supposed to recognize gnome-* and x something are non-negotiable if you have even a slight inclination to click around. I do love the terminal, but I also love having options (and clicking, sometimes).
Being introduced into dependency hell the hard and righteous way of Linux ("Well, you don't really need a GUI right? Here's vi. Surely that should be enough for any sensible individual? No? How about vim?").
I wanted to be more cautios the next time and learn how to do this properly so that I don't have to learn to love vim (too much).
Motivation
On all the jobs I've worked, there was a requirement of versioning of some sort. Whether it be Ansible, curl, Python, Java or node. Relatively early on, I've been told you're supposed to use virtualenvs, and found similar solutions for other tools I've worked with.
But, you have to keep track of tooling as well as the tools. Some of it installs in /home, some in /opt. All of them have a different update mechanism, commands and interfaces.
Currently, I'm using pyenv, nvm, sdkman, govm, and probably some more that I forgot and would have to reluctantly remember next time I get a new laptop. Once, I deleted govm because it looked suspicious, before remembering "Ah, right, Go. Fuck."
So, Nix and immutable operating systems looked cool to me, and I wanted to explore how do they work and what do they offer. I might be a bit limited on the operating system front, because I need worktool isolation for my job mostly, and I can't choose what OS I run there. For this reason, the focus is primarily on Nix, and this will be written as a reminder and documentation to me, from me.
Motivation (for real this time)
I just love spring cleaning my computer every week.
Files in the recycle bin? Were you raised in a circus? With the monkeys?
Downloads directory with more than 3 files? Why don't you just go ahead and download the Internet?
Anyway, the next couple of sentences should read as an AA meeting and that is completely intentional and something I have full awareness - crucially, not control - of.
My favorite apt command is autoremove.
Unnecessary files are the bane of my existence and I will not tolerate them. If I can keep my development environment clean, easy to transfer and manage - sign me up.
Installation
sh <() yeah, whatever. Sure. Not even close to running out of different hills to die on, so you guys can have this one.
Version management for different projects
Nix can be used as an alternative to virtualenv or conda, or nvm.
I found out there is a tool called Nix Shell. What you do with is is: specify a shell.nix file in your project file, that contains all the dependencies of your project.
Something like:
let
pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/cf8cc1201be8bc71b7cbbbdaf349b22f4f99c7ae.tar.gz") {};
in pkgs.mkShell {
packages = [
(pkgs.python312.withPackages (python-pkgs: with python-pkgs; [
# select Python packages here
boto3
requests
netmiko
]))
];
}
But, my issue with this is that it abstracts from the project dependencies. It doesn't offer native integration with requirements.txt or package.json. There are third party tool with which you can supplement this, but it feels kinda like - needing tools to make my tools work. Perhaps if I had a different use for Nix already, and was used to its syntax and behavior - this would come easier. But in this form, where I consider it as an alternative to pyenv, doesn't really feel like it does the job and gets out of my way, it feels like something I would have to actively interface with.
I guess I'm doing this all wrong
Trying to get Nix working as pyenv felt a bit wrong, and I realized I'm probably misunderstanding its main strengths and trying to shoehorn it into a workflow I'm comfortable with. I decided I would have to dive deeper into Nix on its own.
This was maybe a problem in itself, because its complex and requires you to learn the abstraction and concepts of a bunch of stuff it offers.
Not only that, but as any complex tool, its versatile. I was hoping for a "do-this-to-get-that" but it really isn't how it works. The complexity allows you to get creative and fit any part of Nix ecosystem into your workflow.
My workflow leaned more into using isolated and decoupled tools that do one thing well and offer simple interaction interfaces.
At this point, I kind of decided I wouldn't really try to abuse Nix beyond its imagined utility and then get mad because it gets in my way.
Still, I wanted to learn what was its niche and how to leverage its strengths.
The friends/flakes we made along the way
This will just be a short (and hopefully mostly correct) summary of what I learned.
Nix is a functional package manager that aims to build packages completely independent of each other - no shared binaries, libraries or anything else. (I never got into functional programming and I feel like my time has passed, but I will weather it for now.)
It tries to make managing dependencies easier by way of coupling them in the build process. With deb/rpm, dependencies are other packages that need to be installed and managed. With Nix, if something can be built, it has its dependencies correctly declared and available.
Nix is atomic - updates do not operate by changing existing packages, but using isolation, simply build the newest package on a different location. You have both the old and new version separately available - allowing you clean rollback.
Delete is special in Nix - it won't really delete anything but rather garbage collect - as long as something is used, it is kept.
There is also NixOS, an operating system built on top of Nix. You can specify what a NixOS instance should look like (declaratively) and Nix will reliably produce the same (and I mean this in exactly-the-same-hash way) result. With this, it gets super easy to produce the same environment on multiple machines.
It's what they always say
So, Docker also kind of goes along the same line of thinking - reproducibility - especially in deployment. What is the difference?
Main thing is - Docker is an isolation layer building on top of kernel and filesystem of the machines it runs on. NixOS packs everything in the build - kernel, system packages and configuration files, making it truly independent of what it runs on.
Another thing worth mentioning is reproducibility vs. reusability. Docker makes things re-usable, but not always reproducible. If you install a version of a file in Docker, it is possible that underlying dependencies of that file change and mess up whatever you're trying to do. With Nix, build process is ran in a sandbox, isolated from network, using only declared dependencies - ensuring nothing undeclared gets installed and used, that might not transfer to a different machine.
Conclusion.nix
Based on all that I learned throughout this, Nix seems like a great tool in a sysadmin's toolbox, and perhaps even a next big step in how we think about software and environment isolation. It seems less useful as a development workflow helper, and far more useful as something you have in you deployment pipeline and production.
Docker can get frustrating when building containers for different environments and upgrading something is always a risk, so I can definitely see a giant advantage in Nix.
The downside is however - Nix has its own layers of abstraction and a steeper learning curve. It offers robustness and flexibility, but to leverage it in full, you need to understand its internals.
Now that we cleared this mess up, its back to pyenv, exactly where we started. Remind me why I do this?