--- 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 = ""; 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 = ""; 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/" but we expect "/var/empty"`, make sure you have set `users.users..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, is a better resource. [^2]: For flake build system to find them, git should be aware of the files. ## Final configuration files
flake.nix ```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; }; } ```
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; [ 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"; } ```
home.nix ```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"; }; }; }; } ```