1{ config, lib, pkgs, ... }:
2
3let
4 keysDirectory = "/var/keys";
5
6 user = "builder";
7
8 keyType = "ed25519";
9
10 cfg = config.virtualisation.darwin-builder;
11
12in
13
14{
15 imports = [
16 ../virtualisation/qemu-vm.nix
17
18 # Avoid a dependency on stateVersion
19 {
20 disabledModules = [
21 ../virtualisation/nixos-containers.nix
22 ../services/x11/desktop-managers/xterm.nix
23 ];
24 config = { };
25 options.boot.isContainer = lib.mkOption { default = false; internal = true; };
26 }
27 ];
28
29 options.virtualisation.darwin-builder = with lib; {
30 diskSize = mkOption {
31 default = 20 * 1024;
32 type = types.int;
33 example = 30720;
34 description = "The maximum disk space allocated to the runner in MB";
35 };
36 memorySize = mkOption {
37 default = 3 * 1024;
38 type = types.int;
39 example = 8192;
40 description = "The runner's memory in MB";
41 };
42 min-free = mkOption {
43 default = 1024 * 1024 * 1024;
44 type = types.int;
45 example = 1073741824;
46 description = ''
47 The threshold (in bytes) of free disk space left at which to
48 start garbage collection on the runner
49 '';
50 };
51 max-free = mkOption {
52 default = 3 * 1024 * 1024 * 1024;
53 type = types.int;
54 example = 3221225472;
55 description = ''
56 The threshold (in bytes) of free disk space left at which to
57 stop garbage collection on the runner
58 '';
59 };
60 workingDirectory = mkOption {
61 default = ".";
62 type = types.str;
63 example = "/var/lib/darwin-builder";
64 description = ''
65 The working directory to use to run the script. When running
66 as part of a flake will need to be set to a non read-only filesystem.
67 '';
68 };
69 hostPort = mkOption {
70 default = 22;
71 type = types.int;
72 example = 31022;
73 description = ''
74 The localhost host port to forward TCP to the guest port.
75 '';
76 };
77 };
78
79 config = {
80 # The builder is not intended to be used interactively
81 documentation.enable = false;
82
83 environment.etc = {
84 "ssh/ssh_host_ed25519_key" = {
85 mode = "0600";
86
87 source = ./keys/ssh_host_ed25519_key;
88 };
89
90 "ssh/ssh_host_ed25519_key.pub" = {
91 mode = "0644";
92
93 source = ./keys/ssh_host_ed25519_key.pub;
94 };
95 };
96
97 # DNS fails for QEMU user networking (SLiRP) on macOS. See:
98 #
99 # https://github.com/utmapp/UTM/issues/2353
100 #
101 # This works around that by using a public DNS server other than the DNS
102 # server that QEMU provides (normally 10.0.2.3)
103 networking.nameservers = [ "8.8.8.8" ];
104
105 nix.settings = {
106 auto-optimise-store = true;
107
108 min-free = cfg.min-free;
109
110 max-free = cfg.max-free;
111
112 trusted-users = [ "root" user ];
113 };
114
115 services = {
116 getty.autologinUser = user;
117
118 openssh = {
119 enable = true;
120
121 authorizedKeysFiles = [ "${keysDirectory}/%u_${keyType}.pub" ];
122 };
123 };
124
125 system.build.macos-builder-installer =
126 let
127 privateKey = "/etc/nix/${user}_${keyType}";
128
129 publicKey = "${privateKey}.pub";
130
131 # This installCredentials script is written so that it's as easy as
132 # possible for a user to audit before confirming the `sudo`
133 installCredentials = hostPkgs.writeShellScript "install-credentials" ''
134 KEYS="''${1}"
135 INSTALL=${hostPkgs.coreutils}/bin/install
136 "''${INSTALL}" -g nixbld -m 600 "''${KEYS}/${user}_${keyType}" ${privateKey}
137 "''${INSTALL}" -g nixbld -m 644 "''${KEYS}/${user}_${keyType}.pub" ${publicKey}
138 '';
139
140 hostPkgs = config.virtualisation.host.pkgs;
141
142 script = hostPkgs.writeShellScriptBin "create-builder" (
143 # When running as non-interactively as part of a DarwinConfiguration the working directory
144 # must be set to a writeable directory.
145 (if cfg.workingDirectory != "." then ''
146 ${hostPkgs.coreutils}/bin/mkdir --parent "${cfg.workingDirectory}"
147 cd "${cfg.workingDirectory}"
148 '' else "") + ''
149 KEYS="''${KEYS:-./keys}"
150 ${hostPkgs.coreutils}/bin/mkdir --parent "''${KEYS}"
151 PRIVATE_KEY="''${KEYS}/${user}_${keyType}"
152 PUBLIC_KEY="''${PRIVATE_KEY}.pub"
153 if [ ! -e "''${PRIVATE_KEY}" ] || [ ! -e "''${PUBLIC_KEY}" ]; then
154 ${hostPkgs.coreutils}/bin/rm --force -- "''${PRIVATE_KEY}" "''${PUBLIC_KEY}"
155 ${hostPkgs.openssh}/bin/ssh-keygen -q -f "''${PRIVATE_KEY}" -t ${keyType} -N "" -C 'builder@localhost'
156 fi
157 if ! ${hostPkgs.diffutils}/bin/cmp "''${PUBLIC_KEY}" ${publicKey}; then
158 (set -x; sudo --reset-timestamp ${installCredentials} "''${KEYS}")
159 fi
160 KEYS="$(${hostPkgs.nix}/bin/nix-store --add "$KEYS")" ${config.system.build.vm}/bin/run-nixos-vm
161 '');
162
163 in
164 script.overrideAttrs (old: {
165 meta = (old.meta or { }) // {
166 platforms = lib.platforms.darwin;
167 };
168 });
169
170 system = {
171 # To prevent gratuitous rebuilds on each change to Nixpkgs
172 nixos.revision = null;
173
174 stateVersion = lib.mkDefault (throw ''
175 The macOS linux builder should not need a stateVersion to be set, but a module
176 has accessed stateVersion nonetheless.
177 Please inspect the trace of the following command to figure out which module
178 has a dependency on stateVersion.
179
180 nix-instantiate --attr darwin.builder --show-trace
181 '');
182 };
183
184 users.users."${user}" = {
185 isNormalUser = true;
186 };
187
188 security.polkit.enable = true;
189
190 security.polkit.extraConfig = ''
191 polkit.addRule(function(action, subject) {
192 if (action.id === "org.freedesktop.login1.power-off" && subject.user === "${user}") {
193 return "yes";
194 } else {
195 return "no";
196 }
197 })
198 '';
199
200 virtualisation = {
201 diskSize = cfg.diskSize;
202
203 memorySize = cfg.memorySize;
204
205 forwardPorts = [
206 { from = "host"; guest.port = 22; host.port = cfg.hostPort; }
207 ];
208
209 # Disable graphics for the builder since users will likely want to run it
210 # non-interactively in the background.
211 graphics = false;
212
213 sharedDirectories.keys = {
214 source = "\"$KEYS\"";
215 target = keysDirectory;
216 };
217
218 # If we don't enable this option then the host will fail to delegate builds
219 # to the guest, because:
220 #
221 # - The host will lock the path to build
222 # - The host will delegate the build to the guest
223 # - The guest will attempt to lock the same path and fail because
224 # the lockfile on the host is visible on the guest
225 #
226 # Snapshotting the host's /nix/store as an image isolates the guest VM's
227 # /nix/store from the host's /nix/store, preventing this problem.
228 useNixStoreImage = true;
229
230 # Obviously the /nix/store needs to be writable on the guest in order for it
231 # to perform builds.
232 writableStore = true;
233
234 # This ensures that anything built on the guest isn't lost when the guest is
235 # restarted.
236 writableStoreUseTmpfs = false;
237 };
238 };
239}