Today I had a few problems again with brew, which is getting on in years. So I looked for an alternative for brew and came across nixos. At first I didn't realize how nixos could help me here. But a quick look at the documentation and I realized what nix is capable of.
There are multiple reasons:
In addition to that, we’re looking at nix-darwin
in this article, which adds the following reasons on top:
This Blogpost is about two steps and provides additional info for each:
On every new Mac, it’s just two command-line invocations if your nix-darwin configuration is already prepared.
There are multiple ways to install i chose to use Determinate System’s shell installer, which is a one-liner as described in their GitHub repository:
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
The installation just takes a minute or two. After running the command, the installer asks for the sudo password and then prints a nice explanation about what it will do with our system, which we can accept or deny:
It’s advisable to check if there have been any errors during the installation and if there are none, close the shell and start a new one. We don’t need to restart the system.
To test if Nix generally works, just run GNU hello or any other package:
nix run "nixpkgs#hello"
Hello, world!
Please note that this command works with or without quotes, depending on your ZSH configuration.
If you’re new to Nix, make sure you get a copy of the Nix cheat sheet. It’ll give you the best possible overview of the commands available!
nix-darwin
With Nix installed, we have all the Nix shell magic (using nix develop
, nix shell
) at our disposal and can build and run (using nix build
and nix run
) random projects/packages from the internet, which is great.
However, if we imagine having to install packages using nix profile install
... on every other Mac, we’re not much better off than with classical package management. Also, this doesn’t manage our configurations and services, which is exactly what we’re used to from NixOS.
The general idea is that we want to have one big configuration file (possibly scattered over multiple files for better structure and composability) that sets our system up as we want it with one single command.
This is where nix-darwin enters the scene: As the project description says, this project aims to bring the convenience of a declarative system approach to macOS.
Starting from zero, we can initialize a new nix-darwin configuration file in some configuration folder:
mkdir nix-darwin-config
cd nix-darwin-config
nix flake init -t nix-darwin
This creates a flake.nix file like this:
{
description = "Example Darwin 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
];
# 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;
# The platform the configuration will be used on.
nixpkgs.hostPlatform = "x86_64-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;
};
}
At the beginning, we generally want to change two things here:
environment.systemPackages
aarch64-darwin
on Macs with Apple Silicon CPUs. On Intel-based Macs it can be left as x86_64-darwin
.darwinConfigurations."simple"
attribute can be renamed to our hostname. This way we don’t need to provide the name explicitly when building or rebuilding the system configuration.This is enough to start. Bootstrapping this new configuration can be done even without installing any nix-darwin-related packages with a single command:
nix run nix-darwin -- switch --flake .
The last parameter still needs to be --flake .#simple if we didn’t rename the configuration attribute at the bottom of the file.
The installation process might warn us of files that could destructively be overwritten. We need to back up or remove them (on a new Mac, I typically just delete) first.
After the remove of the existing files rerun the nix run command with some more parameters:
nix --extra-experimental-features nix-command --extra-experimental-features flakes run nix-darwin -- switch --flake .
nix-darwin
is now bootstrapped on our system, which gives us the darwin-rebuild
command that is similar to nixos-rebuild
on NixOS hosts. We can now run darwin-rebuild switch --flake .
anytime.
nix-darwin
goodiesThe new nix-darwin
config did not do much to our system. It’s just a starting point. What now?
Have a look at the nix-darwin
configuration options Documentation which lists and describes all the available options. This overview is a goldmine - there is something for everyone.
Let’s have a look at a few nice examples:
sudo
via fingerprintIf we have a Mac with Touch ID, we can unlock sudo
commands with our fingerprint instead of typing the password. This is of course not exclusive to nix-darwin
users, but these have it particularly easy to enable it.
Simply add the following line to the new config:
security.pam.enableSudoTouchIdAuth = true;
Rebuild and apply the config using
darwin-rebuild switch --flake .
Generally, reboots aren’t necessary, but this specific setting needs a reboot.
Voila, this is how it looks in action:
nix-darwin
provides configuration lines for many different macOS default settings. These can typically be altered using UI application setting dialogues or with the defaults
terminal command. However, nix-darwin
manages them all for us:
system.defaults = {
dock.autohide = true;
dock.mru-spaces = false;
finder.AppleShowAllExtensions = true;
finder.FXPreferredViewStyle = "clmv";
loginwindow.LoginwindowText = "Found? Call XXX";
screencapture.location = "~/Pictures/screenshots";
screensaver.askForPasswordDelay = 10;
};
This example configuration snippet sets:
Apple Silicon Macs can install Rosetta, which enables the system to run binaries for Intel CPUs transparently.
The installation still needs to be done manually in the terminal with this command:
softwareupdate --install-rosetta --agree-to-license
After that, we can add this line to our nix-darwin configuration and rebuild again:
nix.extraOptions = ''
extra-platforms = x86_64-darwin aarch64-darwin
'';
Now, we can build and run binaries for both CPUs:
$ nix run "nixpkgs#legacyPackages.aarch64-darwin.hello"
Hello, world!
$ nix run "nixpkgs#legacyPackages.x86_64-darwin.hello"
Hello, world!
Although this feature is a relatively specific developer use case, it’s nice to see how easy it is to configure.
If we want to build binaries or even full system images for GNU/Linux systems, we typically end up delegating builds to remote builders.
nix-darwin
provides a neat Linux builder that runs a NixOS VM as a service in the background. It can simply be activated with one additional configuration line:
nix.linux-builder.enable = true;
It works on both Apple Silicon and Intel-based Macs.
The VM itself is bootstrapped by downloading it from the official NixOS cache. It comes with pre-installed SSH keys, which nix-darwin
also handles elegantly for us on the host side.
After rebuilding the system, we can test it. With a quick dummy derivation that simply writes the output of the command uname -a
into its output path, we can check that it is executed in fact on our new Linux builder:
$ nix build \
--impure \
--expr '(with import <nixpkgs> { system = "aarch64-linux"; }; runCommand "foo" {} "uname -a > $out")'
$ cat result
Linux localhost 6.1.72 #1-NixOS SMP Wed Jan 10 16:10:37 UTC 2024 aarch64 GNU/Linux
Wow - how does it work?
org.nixos.linux-builder
running on our system, which keeps SSH keys and disk image in /var/lib/darwin-builder
/etc/ssh/ssh_config.d/100-linux-builder.conf
creates an SSH host-alias linux-builder
/etc/nix/machines
contains a remote builder entryThis specific VM is also documented in the nixpkgs documentation.
Updating the system involves two steps:
nix flake update
darwin-rebuild switch --flake .
If the configuration resides in a git repository, nix flake update --commit-lock-file
can automatically commit the lock file changes.
Some Apple fans might like setting up a new system each time, but most of us want things to be simple and in sync.
Nix in combination with nix-darwin
is an unbeatable combination - on a new Mac, we can simply perform two steps:
…and we’re done. From Finder etc. UI settings, over preinstalled packages, to additional daemons, it’s all in there!