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