Bootstrapping a Mac with Nix
With nix-darwin and 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, 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.
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 versions1.
% 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:
- Moved the configuration to its on own file
configuration.nix
, and added it to git tree2. - The appropriate
hostPlatform
for your mac.nixpkgs.hostPlatform = "aarch64-darwin";
- Set the home directory path
users.users.dj.home = "/Users/dj";
We end up with
flake.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" =
-darwin.lib.darwinSystem {
nixmodules = [ ./configuration.nix ];
};
# Expose the package set, including overlays, for convenience.
darwinPackages = self.darwinConfigurations."MacBook-Pro".pkgs;
};
}
configuration.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
!
% 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:
{ 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 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, for e.g the git module for configuring, well git.
{
programs.git = enable = true;
extraConfig = {
github.user = "<user>";
init = { defaultBranch = "trunk"; };
diff = { external = "${pkgs.difftastic}/bin/difft"; };
};
};
Install home-manager
with flakes,
- Add a flake input in the inputs section
-manager = {
homeurl = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs-unstable";
};
- And add the module to the
modules
section
-manager.darwinModules.home-manager {
home# `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
.
{ 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
{
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" =
-darwin.lib.darwinSystem {
nixmodules = [
./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 =
"MacBook-Pro".pkgs;
self.darwinConfigurations.};
}
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. 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 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!
{
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.
-homebrew.darwinModules.nix-homebrew
nix{
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.
{
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, you could also use
man configuration.nix
- Home manager option search
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.
Final configuration files
flake.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" =
-darwin.lib.darwinSystem {
nixsystem = "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 =
"MacBook-Pro".pkgs;
self.darwinConfigurations.};
}
configuration.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
{ 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"; };
};
};
}
Revisions
- 28-03-2024: Removed redundant
system
attribute – Thx @roberth@functional.cafe
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.↩︎
For flake build system to find them, git should be aware of the files.↩︎