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:

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:

  1. List generations
$ home-manager generations
2023-01-29 16:54 : id 44 -> /nix/store/h2la2lyc5cg0b7ixd4ixrkl9c4x6lznr-home-manager-generation
  1. Activate desired generation with activate script:
$ /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:

and / or

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.