Erik's Blog - A Nix Flake-Based System Declaration

Motivation

The problem of keeping multiple machines in sync or easily setting up a new one is a topic often discussed among programmers. There is certainly a part of this that is owed to our obsession with automating everything, but it is also true that I have set up enough machines in my life to appreciate not having to perform certain repetitive steps manually.

A lot of people seem to swear by a combination of stow and ansible. I have not used either, so I don’t feel to confident about speaking on their advantages or disadvantages. For my use-cases, a hacky little bash script that installs software from a package manager and creates symlinks to my dotfiles was usually enough.

At the moment I have two machines, one for work and one for personal stuff, both run macOS. In terms of GUI apps they run wildly different software, except the terminal emulator; at the moment that is Alacritty, though I am curious about Mitchell Hashimoto’s Ghostty, once it becomes publicly available. That said, I can easily install this handful of GUI programs via Homebrew.

However, the list of command line programs is pretty much identical; this is partly thanks to using project-specific Nix Flakes for most of the things I work on, so I don’t have to install a bunch of ecosystems up front. This leaves just general utilities and a text editor as the main software that I use daily.

Utilities

Let’s start with a relatively simple flake; our only input is nixpkgs-unstable. Our output will be a single environment that contains everything we need. We also add a version (the lastModifiedDate time stamp), so that nix profile history outputs something sensible. Finally, we just throw everything we need into the paths.

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  };
  outputs = { self, nixpkgs }:
  let version = self.lastModifiedDate;
  in {
    packages.aarch64-darwin = {
      default = self.packages.aarch64-darwin.personal;
      personal = pkgs.buildEnv {
        name = "personal-" + version;
        paths = with pkgs; [
          direnv
          git
          jq
          fzf
          loc
          neofetch
          ripgrep
          starship
          tmux
          universal-ctags
        ];
        extraOutputsToInstall = [ "man" "doc" ];
        };
    };
  };
}

That’s pretty much the list of utilities that I regularly use. This can be easily extended to accommodate architectures other than aarch64 with something like flake utils.

Text Editor

Now, the only thing we are still missing is a good text editor; and while we’re at it, let’s configure it a little bit, as well.

        paths = with pkgs; [
          # ...
          # configure Neovim, see also
          # https://nixos.org/manual/nixpkgs/stable/#vim
          ( neovim.override {
            configure = {
              # source standard init location for possible local additions and
              # the file where I put the colorscheme
              customRC = ''
                source ~/.config/nvim/init.lua
                source ~/.config/nvim/colorscheme.vim
              '';
              packages.colors.start = with vimPlugins; [
                gruvbox-material
                kanagawa-nvim
                nord-nvim
              ];
              packages.plugins.start = with vimPlugins; [
                (nvim-treesitter.withPlugins (
                  plugins: with plugins; [
                    c cpp go javascript lua nix rust vim zig
                  ]
                ))
                # dependency of telescope-nvim
                plenary-nvim
                telescope-nvim
                # just for syntax highlighting for .typ files
                typst-vim
                vim-commentary
              ];
            };
          })
          # ...

This uses the builtin package manager that comes with Neovim. So, instead of lazy.nvim or packer.nvim we use Nix and functionality built into Neovim. What this will do is build a version of Neovim that contains the listed plugins, as well as the specified customRC.

In the customRC I still source the standard config file location for when I want to play around with something locally before putting it into my dotfiles.

Finally, I run my Neovim configuration as a plugin. Since I don’t plan on publishing this to nixpkgs, we can just fetch the git repo from Codeberg - or your preferred hosting provider - directly and then include it in the list of plugins.

      personal =
        let pkgs = nixpkgs.legacyPackages.aarch64-darwin;
          myConfig = pkgs.vimUtils.buildVimPlugin {
          name = "nvim-config";
          src = fetchGit {
            url = "https://codeberg.org/erikwastaken/nvim-config";
            rev = "e2d0a0ed40dece304a2e965c8c2b4a88981fa8ce";
            ref = "main";
          };
        };
        in pkgs.buildEnv {
        #...
          ( neovim.override {
            configure = {
              #...
              packages.plugins.start = with vimPlugins; [
                # my own configuration packaged in a plugin
                myConfig
                #...
              ];
            };
          })
          #...

Configuration?

The Nix savvy reader may be wondering why I don’t use something like home-manager or even nix-darwin. To be honest, I am not entirely sold on the concept of managing configuration or GUI apps on macOS via Nix.

While my configuration is by now pretty stable, I do still like to change the colorscheme every now and then, and have that be consistent between Alacritty, tmux, and Neovim without needing to rebuild my configuration; for example, when I switch from dark to light theme according to lighting. For this I have a little bash script that sets the colorscheme consistently in the corresponding locations.

I’ve tried to install GUI apps through Nix, as well, but the symlinking to the nix store unfortunately clashes with Spotlight search which I use to launch applications. As of writing this, there does not seem to be any workable solution to this, either. From what I’ve seen of both home-manager and nix-darwin, they use homebrew internally, so I might as well use it directly.

So, for the time being, I still use a bash script to create symlinks to dotfiles and to install nix and homebrew in an automated way. After that, I just need to

nix profile install git+https://codeberg.org/erikwastaken/system-declaration.git

and I’m set. There’s no worries about incompatible versions that suddenly make me migrate my configuration files or anything of the sort. Everything just works. If this peaked your interest, you can find the current state of the flake here.