diff --git a/blog/2024-03-25-boostrap-a-macos-machine-with-nix.markdown b/blog/2024-03-25-boostrap-a-macos-machine-with-nix.markdown new file mode 100644 index 0000000..4551363 --- /dev/null +++ b/blog/2024-03-25-boostrap-a-macos-machine-with-nix.markdown @@ -0,0 +1,510 @@ +--- +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"; }; + }; + }; +} +``` +
+ diff --git a/images/nix-install.png b/images/nix-install.png new file mode 100644 index 0000000..2cabdd3 Binary files /dev/null and b/images/nix-install.png differ