Reproducible macOS Configurations with Nix

Reproducible macOS Configurations with Nix

July 9, 2024

The Nix logo

What is Nix?

Nix is a powerful package manager 1 for Linux/Unix systems that offers a declarative approach to system configuration. With Nix, you define your system’s state in a configuration file (known as a flake), ensuring that your setup is reproducible across different machines.

A key component of the Nix ecosystem is Nixpkgs, the default repository of Nix packages, which contains thousands of searcheable packages that you can easily integrate into your configuration.

Why Use Nix on macOS?

If you use macOS, you are likely familiar with Homebrew. Homebrew is a great package manager, and it does this one thing very well. Nix, on the other hand, is a comprehensive system configuration solution.

It offers many advanced features, such as binary caching, package pinning, package overrides, automatic runtime dependencies, and custom package sources.

One particularly interesting part of the Nix ecosystem for macOS users is nix-darwin, a set of tools and configurations that allows you to manage your macOS system configuration using Nix. This is what we will go over in this post.

Installing Nix

There are multiple ways to install Nix on your macOS, but I found that the most reliable way is to use the DeterminateSystems/nix-installer script by running 2

curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install

Setting up nix-darwin

Once you have Nix installed, you can set up nix-darwin by running the following command:

# Create and navigate to a new directory for your nix-darwin configuration
cd $(mkdir -p ~/.config/nix-darwin-config && echo $_)

# initialize a new nix flake with the nix-darwin template
nix flake init -t nix-darwin

This last command will create a flakes.nix file in your nix-darwin-config directory. This file is where you will define your system configuration. You will want to change the following configuration options:

  • nixpkgs.hostPlatform to aarch64-darwin (for Apple Silicon) or x86_64-darwin (for Intel Macs).
  • nixpkgs.config.allowUnfree to true if you want to allow unfree packages (e.g. steam).

you can also add any system package you need in the environment.systemPackages array, for example:

environment.systemPackages =
    [ pkgs.vim
        pkgs.pet
        pkgs.lazygit
        pkgs.lazydocker
        pkgs.fzf
    ];

This is a minimal configuration to get you started. You can add more packages and customizations as needed. A full list of available options can be found in the nix-darwin manual.

I like to keep my configuration in a git repository, along with my dotfiles, so I can easily share it across machines.

Your configuration file will probably look something like this:

{
  description = "Victor's MacOS Nix System flake";

  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 }:
  let
    configuration = { pkgs, ... }: {
      # List packages installed in system profile. To search by name, run:
      # $ nix-env -qaP | grep wget
      environment.systemPackages =
        [ pkgs.vim
        pkgs.fzf # Fuzzy finder
        pkgs.pet # Snippet manager
        pkgs.lazydocker # Docker TUI
        pkgs.lazygit # Git TUI
        pkgs.ripgrep # Faster grep
        pkgs.zoxide # Directory jump tool (z)
        pkgs.pyenv # Python version manager
        pkgs.gh # GitHub CLI
        pkgs.go # Go programming language
        pkgs.kubectl # Kubernetes CLI
        pkgs.kubectx # Kubernetes context switcher
        pkgs.tenv # OpenTofu, Terraform, Terragrunt and Atmos version manager
        pkgs.delta # Terminal git diff viewer with syntax highlighting
        pkgs.curl # Command line tool for transferring data with URL syntax
        pkgs.jq # Command line JSON processor
        pkgs.yq # Command line YAML processor
        pkgs.just # Command runner, similar to make
        pkgs.vscode # Visual Studio Code
        pkgs.neovim # Vim-fork focused on extensibility and usability
        pkgs.helix # Vim-like modal text editor
        pkgs.k9s # Kubernetes CLI to manage your clusters in styles
        pkgs.pandoc # Universal document converter
        pkgs.python3 # Python 3 programming language
        pkgs.rustc # Rust programming language
        pkgs.rustup # Rust toolchain installer
        pkgs.lorri # Nix shell manager
        pkgs.htop # Interactive process viewer
        pkgs.tree # Display directories as trees
        pkgs.jetbrains-mono # JetBrains Mono font
        pkgs.ffmpeg # Multimedia framework
        pkgs.alacritty # GPU-accelerated terminal emulator
      ];

      # allowUnfree is required to install some packages that are not "free" software.
      nixpkgs.config.allowUnfree = true;

      # 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;

      # Set Git commit hash for darwin-version.
      system.configurationRevision = self.rev or self.dirtyRev or null;

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

      system.defaults = {
        dock.autohide = true;
        dock.mru-spaces = false; # Most Recently Used spaces.
        finder.AppleShowAllExtensions = true;
        finder.FXPreferredViewStyle = "icnv"; # icon view. Other options are: Nlsv (list), clmv (column), Flwv (cover flow)
        screencapture.location = "~/Pictures/screenshots";
        screensaver.askForPasswordDelay = 10; # in seconds
      };

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

      nix.extraOptions = ''
        extra-platforms = x86_64-darwin aarch64-darwin
      '';
    };
  in
  {
    # Build darwin flake using:
    # $ darwin-rebuild build --flake .#simple
    darwinConfigurations."simple" = nix-darwin.lib.darwinSystem {
      modules = [ configuration ];
    };

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

Applying the Configuration

To apply your configuration, run the following command:

nix run nix-darwin -- switch --flake .

to update your configuration, you can run:

nix flake update
darwin-rebuild switch --flake .

Applying the same configuration on another machine

To apply the same configuration on another machine, you will need to:

  1. Install nix, and nix-darwin as described above.
  2. nix run nix-darwin -- --flake github:your-github-username/your-repository#your-config 3

  1. Actually, Nix is more than just a package manager. It is a complete system configuration solution that can be used to manage your entire system configuration, including user environments, system services, and more. Nix is a functional language, a package manager, a Linux distribution, and more. This can be confusing at first. Here we will focus on 3 aspects only: the package manager and the system configuration (with nix-darwin), and the programming language used to define the configuration. ↩︎

  2. check the repository for a more up-to-date installation instruction. ↩︎

  3. replace your-github-username/your-repository#your-config with your git repository and branch. ↩︎