managing my OSX userland with nix & home-manager

During my transition between $old_dayjob and $new_dayjob, I codified the installed tools and my dotfiles using nix, niv, and home-manager in source control. The Nix documentation isn’t easily understandable for new users (there is NixOs the Linux distro, Nix the language, Nix packages, and nix the cross-platform toolset used for managing Nix packages, which makes searching for information a bit difficult until you know what you are looking for), and there are so many patterns for application in an array of use cases that it took me days until I was able to get anything working end-to-end. In the end, the pay-off was worth it: no matter how much I install, change, or re-configure my development setup, any of my OSX machines are one line of bash from being up-to-date: git pull origin && bin/switch.

Because I use niv to pin what version of the NixOs/nixpkgs repository I am installing Nix packages from, my setup is also reproducible. The packages I am installing won’t vary between machines unless I explicitly update them using niv update. I am also using niv to manage some dependencies that aren’t available from NixOs/nixpkgs.

This post is kept up-to-date with the changes I make to booninite/files-i-need.

useful resources

I found the following links extremely useful while getting used to the Nix language and how the various ecosystem components fit together.

home-manager:

niv:

Other resources:

figuring out a workflow

My first challenge was understanding how to resolve my bootstrapping problem. I would install Nix at a fixed version (by using a release URL pinned to a specific version) in bin/init:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

# used when setting up my osx machines for first time, installs nix
# so that the rest of machine management can be done via nix tooling

NIX_VERSION="2.3.7"

echo "installing nix@$NIX_VERSION"

sh <(curl -L https://releases.nixos.org/nix/nix-$NIX_VERSION/install) --darwin-use-unencrypted-nix-store-volume

After that, it wasn’t clear to me how I would be able to use home-manager to build and manage my environments without also having home-manager installed by bin/init, but I wanted to manage home-manager installations via niv so that they are also reproducible and part of the Nix code that defines my environments. Eventually I found mjlbach/nix-dotfiles, which helped me put together that you can use nix-shell to define a shell.nix which creates a shell that has home-manager installed from the niv-managed sources:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let
sources = import ./nix/sources.nix;
# overlay niv managed nixpkgs so that home-manager uses it
pkgs = import sources.nixpkgs {
overlays = [
(_: _: { inherit sources; })
];
};
in
pkgs.mkShell rec {
name = "home-manager-shell";

buildInputs = with pkgs; [
niv
(import sources.home-manager { inherit pkgs; }).home-manager
];

shellHook = ''
export NIX_PATH="nixpkgs=${sources.nixpkgs}:home-manager=${sources.home-manager}"
'';

}

Once I had my shell.nix, I could define a portable bin/switch script that used nix-shell to build & apply my environment on machines that only had nix installed:

1
2
#!/bin/sh
nix-shell --run "home-manager -f machines/$(hostname)/home.nix switch"

The $(hostname) bit will be explained in the section on how my Nix code is structured.

With my bootstrapping problem resolved, I could create home.nix files for each of my machines and let home-manager handle the rest of the heavy lifting associated with building and applying my environments.

home.nix

The home.nix file for my personal laptop (machines/orca/home.nix):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{ config, ... }:
let
# import raw sources to use github sources
sources = import ../../nix/sources.nix;
# import niv-managed pkgs overlay
pkgs = import ../../nix { inherit sources; };
in
{
imports = [
../../tools
../../kitty
../../vscode
../../git
../../nix/config
../../zsh
../../home-manager
];

home.sessionVariables = {
KUBECONFIG = "${config.home.homeDirectory}/dev/homestar/kubeconfig";
};

programs.git.userEmail = "shimmerjs@dpu.sh";
}

Almost all of my environment specification is encapsulated in Nix modules so that I minimize as much copy/pasting between my various machines as possible.

In order to make home-manager use packages from my pinned version of nixpkgs, the pkgs variable is overridden with the version that niv has pinned for us:

1
2
3
4
5
let
sources = import ../../nix/sources.nix;
pkgs = import ../../nix { inherit sources; };
in
[...]

niv creates a nix/ folder containing nix/sources.json and nix/sources.nix (which you see imported at the top of my home.nix file). In order to easily redefine pkgs using those sources, I created nix/default.nix which can be imported directly (../../nix in the Nix snippet above is automatically expanded to ../../nix/default.nix – this is true for all Nix modules):

1
2
3
4
5
6
7
8
{ sources ? import ./sources.nix }:
import sources.nixpkgs {
overlays = [
(_: pkgs: { inherit sources; })
];
config = { };
}

After that, I import my modules and make any machine-specific tweaks. For my personal macbook, that includes configuring my personal GitHub e-mail and pointing KUBECONFIG to my self-hosted K3s cluster’s configuration file. The keys I am setting come directly from the home-manager configuration appendix, and you can see more examples of how they can be used in the section on my modules.

modules

There are modules in the booninite/files-i-need repository that aren’t documented here – that is because they are uninteresting or extremely similar to modules that are documented here.

vscode

kitty

zsh

nix/config

tools/

repository structure