Table of Contents

Thoughts on Nix

I usually hop across machines, a few Linux laptops and a Mac, so a while back I decided to begin simplifying a way of replicating my usual setup on them, and keep it all in sync. As most people know, this is essentially what a dotfiles repo is for. So I set about writing one up.

Consistent configs are fairly straightforward, just symlink files/directories to the appropriate location. What was actually difficult was keeping packages consistent across machines (and two operating systems).

Enter Nix.

So, what is it?

Nix brands itself as a declarative package manager, something that was really attractive to me. List your packages and nix will make sure to install them all and what’s more, it promises to never leave your system in a broken state.

The frustrating thing about the Nix world though is that there seems to always be about N different ways to go about accomplishing the same thing, and it’s often not very easy to figure out the pros and cons of each approach.

In the end, it came down to one of two: home-manager or flake.nix with packages.

Which to choose?

home-manager is a way of managing your entire system. This is more than just packages, but application settings, service settings, hardware settings, etc. It’s really quite comprehensive. Think of it as a way of getting a lot of the NixOS features on non-NixOS systems.

The other option is to just have a flake.nix with my packages and use it to keep a list of packages sync’d across systems.

In the end, I went with a simple flake.nix for a few reasons:

  • I already manage my configs just fine in my dotfiles repo
  • I didn’t see anything that justified the additional complexity of home-manager
  • Quite a few packages I need aren’t supported on Mac so I’d still need to add Mac-specific package management
  • home-manager can produce nigh-incomprehensible error messages
  • home-manager doesn’t explicitly support rollbacks so if it messes up your home directory, it’s up to you to manually fix it

My flake

Here’s the flake I use to manage most of my packages on Linux and Mac:

  description = "declarative package setup";

  inputs = {
    nixpkgs.url = github:NixOS/nixpkgs/nixpkgs-unstable;
    roc.url = github:roc-lang/roc;
  };


  outputs = { self, nixpkgs, roc, ... }:
    let
      pkgsFor = system: import nixpkgs {
        inherit system;
      };

      pkgsLinux = pkgsFor "x86_64-linux";
      pkgsDarwin = pkgsFor "x86_64-darwin";

      essentials = sysPkgs: with sysPkgs; [
        bash-completion
        cmake
        curl
        coreutils-full
        fd
        fzf
        gdb
        git
        gnumake
        gnuplot
        golangci-lint
        go-toml
        graphviz
        grpc
        imagemagick
        libssh2
        nasm
        nerdfonts
        openssl_3_3
        podman
        qemu
        ripgrep
        shellcheck
        socat
        tree
        universal-ctags
        wabt
        wasmtime
        wget
      ];

      apps = sysPkgs: with sysPkgs; [
        ansible
        ansible-lint
        asciidoctor
        bun
        devenv
        doctl
        emacs
        ffmpeg
        fish
        gh
        glab
        helix
        htop
        http-server
        httpie
        jq
        kitty
        macchina
        meld
        minio-client
        netcat
        nmap
        nmap-formatter
        neovim
        obsidian
        ranger
        terraform
        thc-hydra
        tmux
        yarn
        yq
      ];

      langs = sysPkgs: with sysPkgs; [
        elixir
        go_1_23
        lua
        luarocks
        maxima
        nodejs
        ocamlPackages.reason
        poetry
        python312
        racket
        ruby
        rustup
        sbcl
        scala
        solc
        zig
      ];

      commonPackages = sysPkgs: apps sysPkgs ++ langs sysPkgs ++ essentials sysPkgs;

      linuxPkgs = with pkgsLinux; [
        autotools-language-server
        checksec
        chromium
        ghdl
        ngspice
        octaveFull
        qucs-s
        remmina
        shfmt
        signal-desktop
        vagrant
      ] ++ commonPackages pkgsLinux;

      darwinPkgs = with pkgsDarwin; [
        alacritty
        rectangle
        skhd
        texliveMedium
        texstudio
      ] ++ commonPackages pkgsDarwin;

    in
    {
      packages.x86_64-linux.default =
        let
          pkgs = pkgsLinux;
          rocPkgs = roc.packages.x86_64-linux;
        in
        pkgs.buildEnv {
          name = "user-linux-packages";
          paths = linuxPkgs ++ [ rocPkgs.cli ];
        };

      packages.x86_64-darwin.default =
        let
          pkgs = pkgsDarwin;
          rocPkgs = roc.packages.x86_64-darwin;
        in
        pkgs.buildEnv {
          name = "user-darwin-packages";
          paths = darwinPkgs ++ [ rocPkgs.cli ];
        };
    };
}

I install it by going to the directory with the flake and running:

nix profile install . --impure 

And after that calling:

nix profile upgrade <name> --impure

to keep the list of installed packages in-sync with the flake.

What about the unsupported Mac packages?

To handle this case, but still declaratively, I use the excellent Brewfile package.

I have a bash alias:

darwin::brew-sync() {
  brew bundle install --cleanup --file "$(realpath "$DOTFILES/darwin/Brewfile")"
}

So calling darwin::brew-sync keeps my installed brew packages in-sync with the Brewfile.

brew 'bash'
brew 'djview4'
brew 'm-cli'
brew 'octave'
brew 'winetricks'

cask 'atom'
cask 'chromium'
cask 'discord'
cask 'gimp'
cask 'ghdl'
cask 'google-chrome'
cask 'julia'
cask 'kicad'
cask 'koodo-reader'
cask 'libreoffice'
cask 'macfuse'
cask 'signal'
cask 'spotify'
cask 'steam'
cask 'surfshark'
cask 'telegram'
cask 'thunderbird'
cask 'transmission'
cask 'vagrant'
cask 'whatsapp'
cask 'wine-stable'
cask 'vlc'

tap 'popcorn-official/popcorn-desktop', 'https://github.com/popcorn-official/popcorn-desktop.git'
cask 'popcorn-time'

tap 'cfergeau/crc'
brew 'vfkit'

My experience so far

I’ve been using this setup for nearly two years and I’m very happy with it! That said, I don’t think I’d ever make the move fully to NixOS or home-manager as I feel I’ve already hit the sweet spot of benefit-complexity with the flake.