Rethinking my dotfiles setup

I’ve recently had another go at organising my settings files (‘dotfiles’) and the way that I install command line applications and tools. It started out highly sophisticated (Nixpkgs and Home Manager), and then reverted to much simpler but more maintainable system (Homebrew and Stow). It has been an interesting and intermittently frustrating process, but I’ve ended up with a system that I like and understand.

You may remember that last year I played with setting up an old Macbook Air running NixOS. That was a fun learning experience, and I was impressed with NixOS, and the way that you could declaratively define in a setting file both the software to install and the configuration file to go with it. In theory, this should make it easy to keep multiple machines updated with the same system, and to set up a new machine quickly with all the tools you need.

I was getting a new Macbook Air for work, so setting up a new machine cleanly was at the top of my mind. I decided (before getting the new laptop), that I would try configuring my old laptop and my home iMac using Home Manager in combination with the Nix package manager. In theory, this would allow me to store the list of software to install and bundle that together with the respective dotfiles. However, unlike my previous experiment, this system would be running on top of macOS, not within NixOS, which would constrain the range of stuff which could be configured at the operating system level.

To cut a very long and frustrating story short, I got it to work, but it was a very difficult process. Part of the problem was that not all of the software available through the Nix packages channel is set up to build on macOS, and I didn’t understand enough about the Nix language to be able to pull in and build packages outside this system. I hit lots of frustrating barriers, and even situations where something would build on one machine but not on another, which — given that Nix is supposed to provide a reliable and reproducible system — was baffling. Eventually, it became apparent that it was going to be too much work for me to maintain a system like this, and it would incur too high a risk that it would suddenly and inexplicably break. I would need to learn a lot more about Nix before really knowing what I was doing with it, and I think it also needs some of the macOS issues smoothed out as well. My previous experience running NixOS was much less problematic, so I suspect it’s much better if you go all-in with it, and use it to run the whole operating system.

Previously, I had been using dotdrop for my dotfiles together with Homebrew to install software. I decided to stick with Homebrew, but moved to Stow to manage my dotfiles. After my battles with Nix’s complexity, I wanted something really simple and easy to maintain. Stow is designed to organise software installed from source (similarly to Homebrew, but for any Unix-like operating system), but it can also be used to copy a directory of dotfiles in one source location to the proper locations for each installation. This article by Alex Pearce explains the way this works, but essentially you create a single ‘dotfiles’ directory wherever it is convenient (which can be version controlled). Inside this directory, you have top-level folders for each of the ‘packages’ you want to configure (e.g. vim, emacs, zsh and so on). Within each of these, you set up the exact file structure you want to appear in your home directory. To install these files, you move into the dotfiles directory and use the command stow zsh (for example) to move all the files inside the zsh directory to their proper locations in your home folder. You then repeat this for each set of configuration files, or when you add a file and want to update the configuration. This means that you don’t need to install everything on every machine, but can pick and choose what you install. It’s a simple but effective system, with a very easy to understand model because all you need to do is reproduce the desired organisation and naming of files within your dotfiles directories.

The next piece of the puzzle was Homebrew. I cleaned up what I had installed and discovered that you can dump a list of Homebrew-installed software to a Brewfile, and then use that to install the same packages on another machine using the brew bundle commands. You can even use brew bundle cleanup to uninstall anything which isn’t specified in the Brewfile, so you can try out software and then get your system back to a known state using the Brewfile. It was also news to me that you can install quite a few GUI applications (like Atom) via brew cask commands. This has meant that I have been able to organise a lot more of my installed tools via Homebrew, making it much quicker to get started when setting up (or cleaning up) a new machine.

Finally, I switched to using zprezto instead of oh-my-zsh to configure zsh to my liking. It seems a bit lighter, and I like the organisation of the configuration files better, though the end result is very similar. I’m starting to use more of zsh’s more interesting features as a result, like global aliases which allow you to create aliased commands to use anywhere in a command pipeline, not just at the start of the command. These are extremely handy for commonly used tasks like piping a command to | wc -l or something similar. I’m also using direnv to manage per-project configuration (for Python versions and packages, and so on), which integrates very nicely with both zsh and emacs (through emacs-direnv). Many languages (like Ruby and Python) have ways of achieving something similar, but direnv allows you to use one common method to handle them all (plus any shell-specific environment variables you need), which is much cleaner and more maintainable.

I’ve also created a README file for myself in my dotfiles directory which outlines both the steps to install all the necessary stuff and to maintain and update the system, which is something that I typically forget how to do after a while. I may even write a proper bootstrap script at some point which installs and configures everything from scratch, but for now, doing a few steps manually is fine with the instructions in front of me. I have a nice, stable, maintainable system, and it is easy to install or uninstall new stuff cleanly and easily.