---
layout: post
title: "Bootstrapping a Mac with Nix"
date: 2024-03-25
comments: true
tags: nix, macos, apple, nix-darwin, brew, homebrew
---

With [nix-darwin](https://github.com/LnL7/nix-darwin) and
[home-manager](https://github.com/nix-community/home-manager) it is possible to
manage almost all of a mac configuration declaratively. So when I got my new
Macbook I was pretty sure this is the way to go. Unfortunately, the bootstrap
is a bit involved. These are my notes from the process, which hopefully
serves as a tutorial.

## Installing dependencies

We need to install some software manually before we can go full steam with
configurations stored as nix files, the primary one being `nix` itself.

Install nix with the [determinate systems nix installer](https://install.determinate.systems/), it comes with sensible defaults and a nicer uninstaller.

Unfortunately, the GUI installer installs `x86_64` version of nix, so I had to use `curl |sh` for my `aarch64` Macbook.

```sh
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
```
![](/images/nix-install.png)

If you haven't already, also install `Xcode tools` with `xcode-select --install`

## Generating initial configurations

We are going to use `nix-darwin` to keep a system wide configuration, which
would represent packages that are installed, configuration for packages and
configuration like shell aliases, files etc.

nix-darwin has support for both classic `configuration.nix` tied to a nix-channel
as well as flakes, I chose the latter as it allows more finer control over dependency versions[^1].

```sh
% nix flake init -t nix-darwin
wrote: /Users/db/code/private/config/flake.nix
# replace the hostname
% sed -i '' "s/simple/$(scutil --get LocalHostName)/" flake.nix

# Add to revision control
git init
git add flake.nix
```

This generates an example flake file, with some boilerplate code to get started.

With some light editing:

 1. Moved the configuration to its on own file `configuration.nix`, and added it to git tree[^2].
 2. The appropriate `hostPlatform` for your mac. `nixpkgs.hostPlatform = "aarch64-darwin";`
 3. Set the home directory path `users.users.dj.home = "/Users/dj";`

We end up with

### flake.nix

```nix
{
  description = "System flake configuration file";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    nix-darwin.url = "github:LnL7/nix-darwin";
    nix-darwin.inputs.nixpkgs.follows = "nixpkgs";
  };

  outputs = inputs@{ self, nix-darwin, nixpkgs }: {
    # Build darwin flake using:
    # $ darwin-rebuild build --flake .#MacBook-Pro
    darwinConfigurations."MacBook-Pro" =
      nix-darwin.lib.darwinSystem {
        system = "aarch64-darwin";
        modules = [ ./configuration.nix ];
      };

    # Expose the package set, including overlays, for convenience.
    darwinPackages = self.darwinConfigurations."MacBook-Pro".pkgs;
  };
}
```

### configuration.nix

```nix
{ config, lib, pkgs, ... }:

{
  # List packages installed in system profile. To search by name, run:
  # $ nix-env -qaP | grep wget
  environment.systemPackages = with pkgs; [
  ];

  users.users.dj.home = "/Users/dj";
  # Auto upgrade nix package and the daemon service.
  services.nix-daemon.enable = true;
  # nix.package = pkgs.nix;

  # Necessary for using flakes on this system.
  nix.settings.experimental-features = "nix-command flakes";

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

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

  nix.configureBuildUsers = true;

  # The platform the configuration will be used on.
  nixpkgs.hostPlatform = "aarch64-darwin";
}

```

Its time to bootstrap the system with `nix-darwin`!


```sh
% nix run nix-darwin -- switch --flake ~/.config/nix-darwin
building the system configuration...
[1/38/42 built, 227 copied (1406.7/1407.6 MiB), 237.3 MiB DL] building darwin-uninstaller (fixupPhase): str
Password:
setting up /run via /etc/synthetic.conf...
user defaults...
setting up user launchd services...
setting up /Applications/Nix Apps...
setting up pam...
applying patches...
setting up /etc...
system defaults...
setting up launchd services...
creating service org.nixos.activate-system
reloading service org.nixos.nix-daemon
reloading nix-daemon...
waiting for nix-daemon
waiting for nix-daemon
configuring networking...
setting nvram variables...
```
During the bootstrap, `nix-darwin` installs the command `darwin-rebuild`, subsequent rebuilds should use `darwin-rebuild`.

Both `nix-darwin` and `darwin-rebuild` follows same semantics as `nixos-rebuild`, `test` for test activation, `build` for only building the configuration, `switch` for commit and activate etc.

## Install some packages

I have a set of packages that I like to have available system-wide (for all users). Add those to
`environment.systemPackages` in `configuration.nix`, which gives us:

```nix
{ config, lib, pkgs, ... }:

{
  # List packages installed in system profile. To search by name, run:
  # $ nix-env -qaP | grep wget
  environment.systemPackages = with pkgs; [
    vim
    curl
    gitAndTools.gitFull
    mg
    mosh
  ];
...
```

Activate with `darwin-rebuild switch --flake ~/path-to-config-directory` 

## Home Manager

[home-manager](https://github.com/nix-community/home-manager) is a nix community project for managing user environments, it comes with a [tone of module for configuring more day-to-day user facing programs](https://home-manager-options.extranix.com/), for e.g the git module for configuring, well git.

```nix
programs.git = {
  enable = true;
  extraConfig = {
    github.user = "<user>";
    init = { defaultBranch = "trunk"; };
    diff = { external = "${pkgs.difftastic}/bin/difft"; };
  };
};
```

Install `home-manager` with flakes,

1. Add a flake input in the inputs section
```nix
home-manager = {
    url = "github:nix-community/home-manager";
    inputs.nixpkgs.follows = "nixpkgs-unstable";
};
```

2. And add the module to the `modules` section

```nix
home-manager.darwinModules.home-manager {
  # `home-manager` config
  home-manager.useGlobalPkgs = true;
  home-manager.useUserPackages = true;
  home-manager.users.db = import ./home.nix;
};
```

I choose to keep the home configuration in a separate file, `home.nix`.

```nix
{ config, lib, pkgs, ... }:

{
  home.stateVersion = "23.11";

  programs.git = {
    enable = true;
    userName = "name";
    userEmail = "mail@example.org";
    extraConfig = {
      github.user = "<user>";
      init = { defaultBranch = "trunk"; };
      diff = { external = "${pkgs.difftastic}/bin/difft"; };
    };
  };
}
```

Also the updated `flake.nix` is now

```nix
{
  description = "System flake configuration file";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    nix-darwin.url = "github:LnL7/nix-darwin";
    nix-darwin.inputs.nixpkgs.follows = "nixpkgs";

    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };

  };

  outputs = inputs@{ self, nix-darwin, nixpkgs, home-manager, nix-homebrew
    , homebrew-core, homebrew-cask, homebrew-bundle, ... }:
    {
      # Build darwin flake using:
      # $ darwin-rebuild build --flake .#MacBook-Pro
      darwinConfigurations."MacBook-Pro" =
        nix-darwin.lib.darwinSystem {
          system = "aarch64-darwin";
          modules = [
            ./configuration.nix

            home-manager.darwinModules.home-manager
            {
              # `home-manager` config
              home-manager.useGlobalPkgs = true;
              home-manager.useUserPackages = true;
              home-manager.users.db = import ./home.nix;
            }

          ];
        };

      # Expose the package set, including overlays, for convenience.
      darwinPackages =
        self.darwinConfigurations."MacBook-Pro".pkgs;
    };
}

```

If you get an error `Error: HOME is set to "/Users/<username>" but we expect "/var/empty"`, make sure you have set `users.users.<username>.home` in configuration.nix.


## Manage homebrew applications

Sadly, nix still has some catching up to do with mac compatibility, biggest gripe for me was [accessing GUI apps with spotlight seems to need some workarounds](https://github.com/LnL7/nix-darwin/issues/214). Luckily brew solves this and we can just install applications with brew, still managed by nix.

`nix-darwin` can declaratively manage brew packages, however we need [nix-homebrew](https://github.com/zhaofengli/nix-homebrew) to install brew itself and manage the taps declaratively.

Grab nix-homebrew using flakes, and also add the taps itself as inputs, this maybe the most underrated flakes feature. We can pin the taps to a specific version!

```nix
inputs = {
    nix-homebrew = {
      url = "github:zhaofengli-wip/nix-homebrew";
      inputs.nixpkgs.follows = "nixpkgs-unstable";
    };

    homebrew-core = {
      url = "github:homebrew/homebrew-core";
      flake = false;
    };
    homebrew-cask = {
      url = "github:homebrew/homebrew-cask";
      flake = false;
    };
    homebrew-bundle = {
      url = "github:homebrew/homebrew-bundle";
      flake = false;
    };

```

.. and import the module into the system configuration.

```nix
nix-homebrew.darwinModules.nix-homebrew
{
  nix-homebrew = {
    enable = true;
    # Apple Silicon Only: Also install Homebrew under the default Intel prefix for Rosetta 2
    enableRosetta = true;
    user = "username";

    taps = {
      "homebrew/homebrew-core" = homebrew-core;
      "homebrew/homebrew-cask" = homebrew-cask;
      "homebrew/homebrew-bundle" = homebrew-bundle;
    };
    mutableTaps = false;
  };
}
```

The package installations are itself managed by nix-darwin, using `homebrew.*` options.
```nix
homebrew = {
  enable = true;
  global.autoIpdate = false;

  casks = [ "kitty" ];
};
```

## Fin!

If you have followed through all of the above, like me, you should have a mac with almost everything configured declaratively, using nix. 

Further customizations options can be found in 

 - [nix-darwin options search](https://daiderd.com/nix-darwin/manual/index.html), you could also use `man configuration.nix`
 - [Home manager option search](https://home-manager-options.extranix.com/)
 

This setup helps me share configuration with my other machines; they are just an `import` away! However this bootstrapping is neither simple nor short, that's definitly something to improve.

[^1]: Explaining flakes or nix nuanceses are out of scope and probably out of my reach, <https://nixos-and-flakes.thiscute.world/> is a better resource.
[^2]: For flake build system to find them, git should be aware of the files.

## Final configuration files

<details>
  <summary>flake.nix</summary>
```nix
{
  description = "System flake configuration file";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    nix-darwin.url = "github:LnL7/nix-darwin";
    nix-darwin.inputs.nixpkgs.follows = "nixpkgs";

    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    nix-homebrew = { url = "github:zhaofengli-wip/nix-homebrew"; };

    homebrew-core = {
      url = "github:homebrew/homebrew-core";
      flake = false;
    };
    homebrew-cask = {
      url = "github:homebrew/homebrew-cask";
      flake = false;
    };
    homebrew-bundle = {
      url = "github:homebrew/homebrew-bundle";
      flake = false;
    };

  };

  outputs = inputs@{ self, nix-darwin, nixpkgs, home-manager, nix-homebrew
    , homebrew-core, homebrew-cask, homebrew-bundle, ... }: {
      # Build darwin flake using:
      # $ darwin-rebuild build --flake .#MacBook-Pro
      darwinConfigurations."MacBook-Pro" =
        nix-darwin.lib.darwinSystem {
          system = "aarch64-darwin";
          modules = [
            ./configuration.nix

            home-manager.darwinModules.home-manager
            {
              # `home-manager` config
              home-manager.useGlobalPkgs = true;
              home-manager.useUserPackages = true;
              home-manager.users.db = import ./home.nix;
            }

            nix-homebrew.darwinModules.nix-homebrew
            {
              nix-homebrew = {
                enable = true;
                # Apple Silicon Only: Also install Homebrew under the default Intel prefix for Rosetta 2
                enableRosetta = true;
                user = "db";

                taps = {
                  "homebrew/homebrew-core" = homebrew-core;
                  "homebrew/homebrew-cask" = homebrew-cask;
                  "homebrew/homebrew-bundle" = homebrew-bundle;
                };
                mutableTaps = false;
              };
            }

          ];
        };

      # Expose the package set, including overlays, for convenience.
      darwinPackages =
        self.darwinConfigurations."MacBook-Pro".pkgs;
    };
}
```
</details>

<details>
  <summary>configuration.nix</summary>
```nix
{ config, lib, pkgs, ... }:

{
  # List packages installed in system profile. To search by name, run:
  # $ nix-env -qaP | grep wget
  environment.systemPackages = with pkgs; [
    vim
    curl
    gitAndTools.gitFull
    mg
    mosh
  ];

  homebrew = {
    enable = true;
    global.autoUpdate = false;

    casks = [ "kitty" ];
  };

  users.users.db.home = "/Users/db";

  # Auto upgrade nix package and the daemon service.
  services.nix-daemon.enable = true;
  # nix.package = pkgs.nix;

  # Necessary for using flakes on this system.
  nix.settings.experimental-features = "nix-command flakes";

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

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

  nix.configureBuildUsers = true;

  # The platform the configuration will be used on.
  nixpkgs.hostPlatform = "aarch64-darwin";
}

```
</details>
<details>
  <summary>home.nix</summary>
```nix
{ config, pkgs, ... }:

{
  home.stateVersion = "23.11";
  programs.git = {
    enable = true;
    userName = "user name";
    userEmail = "email";
    extraConfig = {
      github.user = "gh_user";
      init = { defaultBranch = "trunk"; };
      diff = { external = "${pkgs.difftastic}/bin/difft"; };
    };
  };
}
```
</details>