···6161```ShellSession
6262$ sudo launchctl kickstart -k system/org.nixos.nix-daemon
6363```
6464+6565+## Example flake usage
6666+6767+```
6868+{
6969+ inputs = {
7070+ nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-22.11-darwin";
7171+ darwin.url = "github:lnl7/nix-darwin/master";
7272+ darwin.inputs.nixpkgs.follows = "nixpkgs";
7373+ };
7474+7575+ outputs = { self, darwin, nixpkgs, ... }@inputs:
7676+ let
7777+7878+ inherit (darwin.lib) darwinSystem;
7979+ system = "aarch64-darwin";
8080+ pkgs = nixpkgs.legacyPackages."${system}";
8181+ linuxSystem = builtins.replaceStrings [ "darwin" ] [ "linux" ] system;
8282+8383+ darwin-builder = nixpkgs.lib.nixosSystem {
8484+ system = linuxSystem;
8585+ modules = [
8686+ "${nixpkgs}/nixos/modules/profiles/macos-builder.nix"
8787+ { virtualisation.host.pkgs = pkgs; }
8888+ ];
8989+ };
9090+ in {
9191+9292+ darwinConfigurations = {
9393+ machine1 = darwinSystem {
9494+ inherit system;
9595+ modules = [
9696+ {
9797+ nix.distributedBuilds = true;
9898+ nix.buildMachines = [{
9999+ hostName = "ssh://builder@localhost";
100100+ system = linuxSystem;
101101+ maxJobs = 4;
102102+ supportedFeatures = [ "kvm" "benchmark" "big-parallel" ];
103103+ }];
104104+105105+ launchd.daemons.darwin-builder = {
106106+ command = "${darwin-builder.config.system.build.macos-builder-installer}/bin/create-builder";
107107+ serviceConfig = {
108108+ KeepAlive = true;
109109+ RunAtLoad = true;
110110+ StandardOutPath = "/var/log/darwin-builder.log";
111111+ StandardErrorPath = "/var/log/darwin-builder.log";
112112+ };
113113+ };
114114+ }
115115+ ];
116116+ };
117117+ };
118118+119119+ };
120120+}
121121+```
122122+123123+## Reconfiguring the builder
124124+125125+Initially you should not change the builder configuration else you will not be
126126+able to use the binary cache. However, after you have the builder running locally
127127+you may use it to build a modified builder with additional storage or memory.
128128+129129+To do this, you just need to set the `virtualisation.darwin-builder.*` parameters as
130130+in the example below and rebuild.
131131+132132+```
133133+ darwin-builder = nixpkgs.lib.nixosSystem {
134134+ system = linuxSystem;
135135+ modules = [
136136+ "${nixpkgs}/nixos/modules/profiles/macos-builder.nix"
137137+ {
138138+ virtualisation.host.pkgs = pkgs;
139139+ virtualisation.darwin-builder.diskSize = 5120;
140140+ virtualisation.darwin-builder.memorySize = 1024;
141141+ virtualisation.darwin-builder.hostPort = 33022;
142142+ virtualisation.darwin-builder.workingDirectory = "/var/lib/darwin-builder";
143143+ }
144144+ ];
145145+```
146146+147147+You may make any other changes to your VM in this attribute set. For example,
148148+you could enable Docker or X11 forwarding to your Darwin host.
149149+
+177-117
nixos/modules/profiles/macos-builder.nix
···7788 keyType = "ed25519";
991010+ cfg = config.virtualisation.darwin-builder;
1111+1012in
11131214{
···2426 }
2527 ];
26282727- # The builder is not intended to be used interactively
2828- documentation.enable = false;
2929+ options.virtualisation.darwin-builder = with lib; {
3030+ diskSize = mkOption {
3131+ default = 20 * 1024;
3232+ type = types.int;
3333+ example = 30720;
3434+ description = "The maximum disk space allocated to the runner in MB";
3535+ };
3636+ memorySize = mkOption {
3737+ default = 3 * 1024;
3838+ type = types.int;
3939+ example = 8192;
4040+ description = "The runner's memory in MB";
4141+ };
4242+ min-free = mkOption {
4343+ default = 1024 * 1024 * 1024;
4444+ type = types.int;
4545+ example = 1073741824;
4646+ description = ''
4747+ The threshold (in bytes) of free disk space left at which to
4848+ start garbage collection on the runner
4949+ '';
5050+ };
5151+ max-free = mkOption {
5252+ default = 3 * 1024 * 1024 * 1024;
5353+ type = types.int;
5454+ example = 3221225472;
5555+ description = ''
5656+ The threshold (in bytes) of free disk space left at which to
5757+ stop garbage collection on the runner
5858+ '';
5959+ };
6060+ workingDirectory = mkOption {
6161+ default = ".";
6262+ type = types.str;
6363+ example = "/var/lib/darwin-builder";
6464+ description = ''
6565+ The working directory to use to run the script. When running
6666+ as part of a flake will need to be set to a non read-only filesystem.
6767+ '';
6868+ };
6969+ hostPort = mkOption {
7070+ default = 22;
7171+ type = types.int;
7272+ example = 31022;
7373+ description = ''
7474+ The localhost host port to forward TCP to the guest port.
7575+ '';
7676+ };
7777+ };
29783030- environment.etc = {
3131- "ssh/ssh_host_ed25519_key" = {
3232- mode = "0600";
7979+ config = {
8080+ # The builder is not intended to be used interactively
8181+ documentation.enable = false;
33823434- source = ./keys/ssh_host_ed25519_key;
3535- };
8383+ environment.etc = {
8484+ "ssh/ssh_host_ed25519_key" = {
8585+ mode = "0600";
36863737- "ssh/ssh_host_ed25519_key.pub" = {
3838- mode = "0644";
8787+ source = ./keys/ssh_host_ed25519_key;
8888+ };
8989+9090+ "ssh/ssh_host_ed25519_key.pub" = {
9191+ mode = "0644";
39924040- source = ./keys/ssh_host_ed25519_key.pub;
9393+ source = ./keys/ssh_host_ed25519_key.pub;
9494+ };
4195 };
4242- };
43964444- # DNS fails for QEMU user networking (SLiRP) on macOS. See:
4545- #
4646- # https://github.com/utmapp/UTM/issues/2353
4747- #
4848- # This works around that by using a public DNS server other than the DNS
4949- # server that QEMU provides (normally 10.0.2.3)
5050- networking.nameservers = [ "8.8.8.8" ];
9797+ # DNS fails for QEMU user networking (SLiRP) on macOS. See:
9898+ #
9999+ # https://github.com/utmapp/UTM/issues/2353
100100+ #
101101+ # This works around that by using a public DNS server other than the DNS
102102+ # server that QEMU provides (normally 10.0.2.3)
103103+ networking.nameservers = [ "8.8.8.8" ];
511045252- nix.settings = {
5353- auto-optimise-store = true;
105105+ nix.settings = {
106106+ auto-optimise-store = true;
541075555- min-free = 1024 * 1024 * 1024;
108108+ min-free = cfg.min-free;
561095757- max-free = 3 * 1024 * 1024 * 1024;
110110+ max-free = cfg.max-free;
581115959- trusted-users = [ "root" user ];
6060- };
112112+ trusted-users = [ "root" user ];
113113+ };
611146262- services = {
6363- getty.autologinUser = user;
115115+ services = {
116116+ getty.autologinUser = user;
641176565- openssh = {
6666- enable = true;
118118+ openssh = {
119119+ enable = true;
671206868- authorizedKeysFiles = [ "${keysDirectory}/%u_${keyType}.pub" ];
121121+ authorizedKeysFiles = [ "${keysDirectory}/%u_${keyType}.pub" ];
122122+ };
69123 };
7070- };
711247272- system.build.macos-builder-installer =
7373- let
7474- privateKey = "/etc/nix/${user}_${keyType}";
125125+ system.build.macos-builder-installer =
126126+ let
127127+ privateKey = "/etc/nix/${user}_${keyType}";
751287676- publicKey = "${privateKey}.pub";
129129+ publicKey = "${privateKey}.pub";
771307878- # This installCredentials script is written so that it's as easy as
7979- # possible for a user to audit before confirming the `sudo`
8080- installCredentials = hostPkgs.writeShellScript "install-credentials" ''
8181- KEYS="''${1}"
8282- INSTALL=${hostPkgs.coreutils}/bin/install
8383- "''${INSTALL}" -g nixbld -m 600 "''${KEYS}/${user}_${keyType}" ${privateKey}
8484- "''${INSTALL}" -g nixbld -m 644 "''${KEYS}/${user}_${keyType}.pub" ${publicKey}
8585- '';
131131+ # This installCredentials script is written so that it's as easy as
132132+ # possible for a user to audit before confirming the `sudo`
133133+ installCredentials = hostPkgs.writeShellScript "install-credentials" ''
134134+ KEYS="''${1}"
135135+ INSTALL=${hostPkgs.coreutils}/bin/install
136136+ "''${INSTALL}" -g nixbld -m 600 "''${KEYS}/${user}_${keyType}" ${privateKey}
137137+ "''${INSTALL}" -g nixbld -m 644 "''${KEYS}/${user}_${keyType}.pub" ${publicKey}
138138+ '';
861398787- hostPkgs = config.virtualisation.host.pkgs;
140140+ hostPkgs = config.virtualisation.host.pkgs;
881418989- script = hostPkgs.writeShellScriptBin "create-builder" ''
9090- KEYS="''${KEYS:-./keys}"
9191- ${hostPkgs.coreutils}/bin/mkdir --parent "''${KEYS}"
9292- PRIVATE_KEY="''${KEYS}/${user}_${keyType}"
9393- PUBLIC_KEY="''${PRIVATE_KEY}.pub"
9494- if [ ! -e "''${PRIVATE_KEY}" ] || [ ! -e "''${PUBLIC_KEY}" ]; then
9595- ${hostPkgs.coreutils}/bin/rm --force -- "''${PRIVATE_KEY}" "''${PUBLIC_KEY}"
9696- ${hostPkgs.openssh}/bin/ssh-keygen -q -f "''${PRIVATE_KEY}" -t ${keyType} -N "" -C 'builder@localhost'
9797- fi
9898- if ! ${hostPkgs.diffutils}/bin/cmp "''${PUBLIC_KEY}" ${publicKey}; then
9999- (set -x; sudo --reset-timestamp ${installCredentials} "''${KEYS}")
100100- fi
101101- KEYS="$(nix-store --add "$KEYS")" ${config.system.build.vm}/bin/run-nixos-vm
102102- '';
142142+ script = hostPkgs.writeShellScriptBin "create-builder" (
143143+ # When running as non-interactively as part of a DarwinConfiguration the working directory
144144+ # must be set to a writeable directory.
145145+ (if cfg.workingDirectory != "." then ''
146146+ ${hostPkgs.coreutils}/bin/mkdir --parent "${cfg.workingDirectory}"
147147+ cd "${cfg.workingDirectory}"
148148+ '' else "") + ''
149149+ KEYS="''${KEYS:-./keys}"
150150+ ${hostPkgs.coreutils}/bin/mkdir --parent "''${KEYS}"
151151+ PRIVATE_KEY="''${KEYS}/${user}_${keyType}"
152152+ PUBLIC_KEY="''${PRIVATE_KEY}.pub"
153153+ if [ ! -e "''${PRIVATE_KEY}" ] || [ ! -e "''${PUBLIC_KEY}" ]; then
154154+ ${hostPkgs.coreutils}/bin/rm --force -- "''${PRIVATE_KEY}" "''${PUBLIC_KEY}"
155155+ ${hostPkgs.openssh}/bin/ssh-keygen -q -f "''${PRIVATE_KEY}" -t ${keyType} -N "" -C 'builder@localhost'
156156+ fi
157157+ if ! ${hostPkgs.diffutils}/bin/cmp "''${PUBLIC_KEY}" ${publicKey}; then
158158+ (set -x; sudo --reset-timestamp ${installCredentials} "''${KEYS}")
159159+ fi
160160+ KEYS="$(${hostPkgs.nix}/bin/nix-store --add "$KEYS")" ${config.system.build.vm}/bin/run-nixos-vm
161161+ '');
103162104104- in
105105- script.overrideAttrs (old: {
106106- meta = (old.meta or { }) // {
107107- platforms = lib.platforms.darwin;
108108- };
109109- });
163163+ in
164164+ script.overrideAttrs (old: {
165165+ meta = (old.meta or { }) // {
166166+ platforms = lib.platforms.darwin;
167167+ };
168168+ });
110169111111- system = {
112112- # To prevent gratuitous rebuilds on each change to Nixpkgs
113113- nixos.revision = null;
170170+ system = {
171171+ # To prevent gratuitous rebuilds on each change to Nixpkgs
172172+ nixos.revision = null;
114173115115- stateVersion = lib.mkDefault (throw ''
116116- The macOS linux builder should not need a stateVersion to be set, but a module
117117- has accessed stateVersion nonetheless.
118118- Please inspect the trace of the following command to figure out which module
119119- has a dependency on stateVersion.
174174+ stateVersion = lib.mkDefault (throw ''
175175+ The macOS linux builder should not need a stateVersion to be set, but a module
176176+ has accessed stateVersion nonetheless.
177177+ Please inspect the trace of the following command to figure out which module
178178+ has a dependency on stateVersion.
120179121121- nix-instantiate --attr darwin.builder --show-trace
122122- '');
123123- };
180180+ nix-instantiate --attr darwin.builder --show-trace
181181+ '');
182182+ };
124183125125- users.users."${user}" = {
126126- isNormalUser = true;
127127- };
184184+ users.users."${user}" = {
185185+ isNormalUser = true;
186186+ };
128187129129- security.polkit.enable = true;
188188+ security.polkit.enable = true;
130189131131- security.polkit.extraConfig = ''
132132- polkit.addRule(function(action, subject) {
133133- if (action.id === "org.freedesktop.login1.power-off" && subject.user === "${user}") {
134134- return "yes";
135135- } else {
136136- return "no";
137137- }
138138- })
139139- '';
190190+ security.polkit.extraConfig = ''
191191+ polkit.addRule(function(action, subject) {
192192+ if (action.id === "org.freedesktop.login1.power-off" && subject.user === "${user}") {
193193+ return "yes";
194194+ } else {
195195+ return "no";
196196+ }
197197+ })
198198+ '';
140199141141- virtualisation = {
142142- diskSize = 20 * 1024;
200200+ virtualisation = {
201201+ diskSize = cfg.diskSize;
143202144144- memorySize = 3 * 1024;
203203+ memorySize = cfg.memorySize;
145204146146- forwardPorts = [
147147- { from = "host"; guest.port = 22; host.port = 22; }
148148- ];
205205+ forwardPorts = [
206206+ { from = "host"; guest.port = 22; host.port = cfg.hostPort; }
207207+ ];
149208150150- # Disable graphics for the builder since users will likely want to run it
151151- # non-interactively in the background.
152152- graphics = false;
209209+ # Disable graphics for the builder since users will likely want to run it
210210+ # non-interactively in the background.
211211+ graphics = false;
153212154154- sharedDirectories.keys = {
155155- source = "\"$KEYS\"";
156156- target = keysDirectory;
157157- };
213213+ sharedDirectories.keys = {
214214+ source = "\"$KEYS\"";
215215+ target = keysDirectory;
216216+ };
158217159159- # If we don't enable this option then the host will fail to delegate builds
160160- # to the guest, because:
161161- #
162162- # - The host will lock the path to build
163163- # - The host will delegate the build to the guest
164164- # - The guest will attempt to lock the same path and fail because
165165- # the lockfile on the host is visible on the guest
166166- #
167167- # Snapshotting the host's /nix/store as an image isolates the guest VM's
168168- # /nix/store from the host's /nix/store, preventing this problem.
169169- useNixStoreImage = true;
218218+ # If we don't enable this option then the host will fail to delegate builds
219219+ # to the guest, because:
220220+ #
221221+ # - The host will lock the path to build
222222+ # - The host will delegate the build to the guest
223223+ # - The guest will attempt to lock the same path and fail because
224224+ # the lockfile on the host is visible on the guest
225225+ #
226226+ # Snapshotting the host's /nix/store as an image isolates the guest VM's
227227+ # /nix/store from the host's /nix/store, preventing this problem.
228228+ useNixStoreImage = true;
170229171171- # Obviously the /nix/store needs to be writable on the guest in order for it
172172- # to perform builds.
173173- writableStore = true;
230230+ # Obviously the /nix/store needs to be writable on the guest in order for it
231231+ # to perform builds.
232232+ writableStore = true;
174233175175- # This ensures that anything built on the guest isn't lost when the guest is
176176- # restarted.
177177- writableStoreUseTmpfs = false;
234234+ # This ensures that anything built on the guest isn't lost when the guest is
235235+ # restarted.
236236+ writableStoreUseTmpfs = false;
237237+ };
178238 };
179239}