Switching to nix-darwin and Flakes

Since my last post about using Nix to configure my dotfiles, I've since moved to using it for everything.1 (Sort of.)

In my pursuit of treating my computer as cattle not as a pet I have had a series of increasingly complicated systems to help me set up a new computer after a reset2. The earliest I can find is over a decade old… a list of tools I use so that I wouldn't lose it.3 Later on I had a git repo of common config files and a complicated README on how to set it up. As of last year, that had progressed to a combination of brew bundle a Brewfile to install applications and dependencies, macos.sh to set up common settings, and a Makefile to run it all. (You can see that version in git history.)

After moving my dotfiles to nix and learning some more… I wanted to try nix-darwin. nix-darwin is a module system for configuring a Mac system.

Because I had embraced home-manager first, I had to do some refactoring.

  1. I wrote a nix-darwin configuration first… got that working.
  2. I then refactored my home-manager to not be the "entry point" into my nix world.
  3. I then wrote nix-darwin and nixos config to drive home-manager, so that it's a top-down configuration.

It took some mangling, but it feels great. For the first time in years, if I type brew into my mac terminal it doesn't know what brew is. (It's actually still there, but more on that later)

nix-darwin

At first I just used the module in the default location of the configuration file until I got it working. Eventually I rewrote it without host-specific values and used it in the flake, which I'll show in a minute.

{ config, pkgs,  }:

{
  environment.systemPackages =
    [
      pkgs.home-manager
    ];

  # Use a custom configuration.nix location.
  environment.darwinConfig = "$HOME/src/github.com/evantravers/dotfiles/nix-darwin-configuration";

  # Auto upgrade nix package and the daemon service.
  services.nix-daemon.enable = true;
  nix = {
    package = pkgs.nix;
    settings = {
      "extra-experimental-features" = [ "nix-command" "flakes" ];
    };
  };

  # Create /etc/zshrc that loads the nix-darwin environment.
  programs = {
    gnupg.agent.enable = true;
    zsh.enable = true;  # default shell on catalina
  };

  # Used for backwards compatibility, please read the changelog before changing.
  # $ darwin-rebuild changelog
  system.stateVersion = 4;

  # Install fonts
  fonts.fontDir.enable = true;
  fonts.fonts = [
    pkgs.monaspace
  ];

  # Use homebrew to install casks and Mac App Store apps
  homebrew = {
    enable = true;

    casks = [
      "1password"
      "bartender"
      "brave-browser"
      "fantastical"
      "firefox"
      "hammerspoon"
      "karabiner-elements"
      "obsidian"
      "raycast"
      "soundsource"
      "wezterm"
    ];

    masApps = {
      "Drafts" = 1435957248;
      "Reeder" = 1529448980;
      "Things" = 904280696;
      "Timery" = 1425368544;
    };
  };

  # set some OSX preferences that I always end up hunting down and changing.
  system.defaults = {
    # minimal dock
    dock = {
      autohide = true;
      orientation = "left";
      show-process-indicators = false;
      show-recents = false;
      static-only = true;
    };
    # a finder that tells me what I want to know and lets me work
    finder = {
      AppleShowAllExtensions = true;
      ShowPathbar = true;
      FXEnableExtensionChangeWarning = false;
    };
    # Tab between form controls and F-row that behaves as F1-F12
    NSGlobalDomain = {
      AppleKeyboardUIMode = 3;
      "com.apple.keyboard.fnState" = true;
    };
  };
}

(source: darwin.nix)

Pretty soon I could run darwin-rebuild switch and it'd flow right on through!

Flakes

I made my jump to nix-darwin a little bit more complicated by deciding to go all in on nix-flakes. I made this decision because I wanted the reproducability of a flake for configuring a computer. Without flakes the configuration would be at the mercy of the host system's channel configuration… and I want to be a little more careful. (and I want to learn about flakes!)

I used the flake generators from each project, then I modified heavily from examples I found online. (Searching google for "nix-darwin examples" is a lot less helpful than searching github's code search, FYI!)

The final flake (simplified and over-documented) looks something like this:

{
  description = "Evan's darwin system";

  inputs = {
    # I pinned darwin to a particular release
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-23.11-darwin";
    # I then pinned home-manager so that it would not issue the mismatch error
    home-manager.url = "github:nix-community/home-manager/release-23.11";
    home-manager.inputs.nixpkgs.follows = "nixpkgs";
    nix-darwin.url = "github:LnL7/nix-darwin";
    nix-darwin.inputs.nixpkgs.follows = "nixpkgs";
  };

  outputs = inputs@{ self, nix-darwin, home-manager, nixpkgs }: {
    darwinConfigurations = {
      # I have couple computers, but one config per
      "Evans-MacBook-Pro" = nix-darwin.lib.darwinSystem {
        system = "x86_64-darwin"; # alternatively "aarch64-darwin"
        modules = [
          # include the darwin module
          ./darwin.nix
          # setup home-manager
          home-manager.darwinModules.home-manager
          {
            home-manager = {
              # include the home-manager module
              users.evan = import ../home-manager/home.nix;
            };
            users.users.evan.home = "/Users/evan";
          }
        ];
        specialArgs = { inherit inputs; };
      };
    };
  };
}

(source: flake.nix)

I did some folder re-arranging. A year ago I had a series of folders that corresponded to the programs and XDG paths I was configuring. I carried forward that mentality to the first version of nix when I used home-manager, where I symlinked the whole folder to ~/.config/home-manager. Now, I just run the nix command against the folder in my ~/src directory. No symlinks required.

I believe there is a way to run a flake from a git repo with no installation steps. It's possible that I could run something akin to nix run github:evantravers/dotfiles?dir=nix-darwin-configuration… but I need to do some research on that first.4 It's exciting though!

Besides the horde of ; and {}: {} problems (read: my lack of experience with the language forms,) I ran into one issue where I needed to specify users.users.<name>.home because of a home-manager bug.

Homebrew for Apps

I wrestled back and forth with this. @teoljungberg demo'd how to download hammerspoon from the online release, but I opted for what @jonathandion did and used homebrew to install hammerspoon I may write a couple of such expressions to get some of my favorite applications that neither have casks nor are on the Mac App Store, Homerow and MacWhisper for example.

Homebrew is no longer in my path... but it is a very convenient way to get certain packages, and it'll probably stay that way. I will not use it to get things that home-manager can manage though. Mac-specific packages.

I did a very similar process to configure nixos as a flake, and now I have a single home-manager system that is shared equally between the two OS configurations.

At the moment, home-manager isn't standalone anymore. I can't run it without running a darwin-rebuild switch or nixos-rebuild switch command. That's not bugging me, but it does seem kind of weird that the only way to get a new tmux plugin is to rebuild the universe from first principles.

It also feels very weird to no longer structure my repo based on stow/symlink assumptions, but it dodges so many headaches and simplifies rolling back changes.

I also want to mess with home-manager apparently has options for setting up calendars and emails… I want to know all about that… but I may need some extra privacy for those.

I'm sure that the first time I set up a new mac/PC from scratch I'll revisit this and have to change a few things, but I'm excited to have something so robust to lean on next time.

References:


  1. There's a lot of buzz about nix on the orange site and red site… and people have accurately nix a cult. I think it is suffering from "new hammer" problems. I am persuaded that it's the best stab at solving some common problems I have in computing… so I'm drinking the koolaid. 

  2. A reset could be a catastrophic hardware failure, a new device being issued by the company, or me just munging up my paths and brew update so bad that it's faster to start from scratch than undo my foolishness. Ask me how I know. 

  3. I know you are curious… here's my list from many years ago. This was pre-vim, so I'm thinking ~2009? I think Square was new, and I was an early adopter. 

  4. I think I need to make a master flake.nix to route the command to the right place. @nmasur has an example


Changelog
  • 2024-02-06 12:17:34 -0600
    Post

  • 2024-02-06 11:38:21 -0600
    Continuing to write

  • 2024-02-06 11:01:53 -0600
    Draft