on
How I came to use Nix
It all started while I was reasoning about my inefficient ways of maintaining my working environments.
A mess
I am using ZSH and Neovim editor, development tools for Rust, Python or Node.js and a lot of linux command line tools like bat, ripgrep or exa. If I installed those programs using the package manager of my linux distribution (Ubuntu), I would get rather outdated versions. So I used to install them individually, following the respective instructions.
I only got to feel the pain later:
- my program versions were not regularly updated, because I didn't want to afford the effort of updating each of them individually.
- my different environments (at home and a few VMs at work) where never in sync, but rather had different tool sets in different versions: because the maintenance effort was multiplied by the number of environments.
A promising idea
In this situation I got a good nudge by reading about the Nix package manager: it promises reproducible environments and a vast collection of packages to choose from. It is based on the idea of pure functions that transform inputs to outputs without side effects, a principle I was already applying successfully in programming. Naturally I was intrigued.
I started by installing Nix in my systems (one time effort, instructions here) and then I installed my tool set using a shared shell script containing basically:
nix-env --install \
neovim \
bat \
ripgrep \
exa
Great achievement, thank you Nix!
Now I could update all those packages very easy:
$ nix-channel --update && nix-env -u '*'
What about configuration?
But it appeared rather soon, that there is yet more to environment maintenance: tool configuration is contained in dotfiles, e.g. ~/.config/nvim/init.vim for Neovim, and must be kept in sync across environments as well.
To this end I have been using dotbot for some time. But configuration tends to have a strong dependency on the program version: e.g. when I update Neovim or one of its plugins, my configuration more often than not also needs adaptation.
Moreover I had to deal with yet another maintenance tool. Could I maybe have all in one management tool: programs and configuration?
When I read about Nix Home Manager, it appeared to be the answer.
Adopting Home Manager
In order to make Home Manager work, I needed to install it as standalone (again a one time effort on each linux system) and then create a file home.nix in Nix Language containing my desired tools and their respective configuration.
Nix Language is a functional language: lazy, pure, dynamically typed, that I didn't find difficult to approach. For my use I didn't need to know the syntax in detail, indeed I could already get quite far by copying and adapting examples.
I included the whole list of tools from my former shell script, and configured the ones that needed configuration according to the documentation.
This was an iterative process: whenever I found time, I configured one of the tools in home.nix and with a
$ home-manager switch
Home Manager activated the changes, creating a dotfile as needed (optionally even backing up my previous configuration file).
Then I could use this piece of configuration in my real work, which would reveal possible flaws quite soon!
If any change turned out catastrophic, I could always go back to my last known good state of version / configuration (kept as a 'generation' in Nix) in two steps:
- List generations
$ home-manager generations
2023-01-29 16:54 : id 44 -> /nix/store/h2la2lyc5cg0b7ixd4ixrkl9c4x6lznr-home-manager-generation
- Activate desired generation with
activatescript:
$ /nix/store/h2la2lyc5cg0b7ixd4ixrkl9c4x6lznr-home-manager-generation/activate
I found this process very straightforward and a very smooth transition. In most of the cases it simply just worked.
If I had a configuration option, that was not (yet) included in Home Manager's options, I could always resort to specify that part literally in home.nix, to be copied into the generated configuration file.
Simplified example of home.nix:
{ config, pkgs, ... }:
{
home.username = "dev";
home.homeDirectory = "/home/dev";
home.packages = [
pkgs.bat
pkgs.ripgrep
pkgs.exa
];
home.stateVersion = "22.05";
programs.neovim = {
enable = true;
plugins = with pkgs.vimPlugins; [
neoformat
{
plugin = nvim-tree-lua;
config = ''
lua <<EOF
require("nvim-tree").setup()
EOF
'';
}
];
extraConfig = ''
" this extraConfig will appear literally in init.vim
let mapleader=','
'';
};
}
After some time I ended up with a 'big' home.nix containing all my configurations. I shared my home.nix over a Git repository that I cloned on each linux system and created a symlink to the location ~/.config/nixpkgs/home.nix where home manager expects its configuration.
Done!
...almost...
One size doesn't fit all
Publishing my home.nix from the system where I had iteratively created it to other linux systems, it hit me immediately that one configuration wouldn't suffice: different systems require:
- different configurations: username, home directory, e-mail address and many more
and / or
- different features: e.g. on a desktop system I wanted X Session and a Window Manager activated, while on a headless, SSH-accessed server it wouldn't make any sense to have such packages installed.
Instead of duplicating/adapting the configuration file or creating specialized downstream versions of a common upstream, the flexible Nix configuration language allowed for factoring out common configurations - with ease!
As it stands now, I have split configurations, e.g. per feature like xsession, in sub-directories and several home-*.nix configuration files that import from there and add the specialized part.
Example home-desktop.nix:
{ config, pkgs, ... }:
{
home.username = "dev";
home.homeDirectory = "/home/dev";
imports = [
./common/default.nix
./xsession/default.nix
./programs/alacritty.nix
./programs/neovim.nix
];
}
Example home-headless.nix:
{ config, pkgs, ... }:
{
home.username = "wrk";
home.homeDirectory = "/home/wrk";
imports = [
./common/default.nix
./programs/neovim.nix
];
}
On each linux system I have a symlink from the respective home-*.nix with its specialized configuration to ~/.config/nixpkgs/home.nix.
Conclusion
Nix is a comprehensive and efficient solution for managing my working environments. It features a vast, continuously updated package repository and a flexible, functional configuration language.
In addition, Nix Home Manager integrates tool configuration under one consistent interface, independent of your linux distribution.
I found it welcoming easy to adopt it gradually, under my unimpaired workload.
Based on my experience so far, I feel encouraged to explore more of Nix's apparently much bigger potential, which may lead to a follow-up some time in the future!
My current, working configuration can be found here, this repo is a snapshot and will not get updates.