511 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
		
		
			
		
	
	
			511 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
|   | --- | ||
|  | 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 | ||
|  | ``` | ||
|  |  | ||
|  | 
 | ||
|  | 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> | ||
|  | 
 |