at 25.11-pre 8.6 kB view raw
1/* 2 This profile uses NixOS to create a remote builder VM to build Linux packages, 3 which can be used to build packages for Linux on other operating systems; 4 primarily macOS. 5 6 It contains both the relevant guest settings as well as an installer script 7 that manages it as a QEMU virtual machine on the host. 8*/ 9{ 10 config, 11 lib, 12 options, 13 ... 14}: 15 16let 17 keysDirectory = "/var/keys"; 18 19 user = "builder"; 20 21 keyType = "ed25519"; 22 23 cfg = config.virtualisation.darwin-builder; 24 25in 26 27{ 28 imports = [ 29 ../virtualisation/qemu-vm.nix 30 31 # Avoid a dependency on stateVersion 32 { 33 disabledModules = [ 34 ../virtualisation/nixos-containers.nix 35 ../services/x11/desktop-managers/xterm.nix 36 ]; 37 # swraid's default depends on stateVersion 38 config.boot.swraid.enable = false; 39 options.boot.isContainer = lib.mkOption { 40 default = false; 41 internal = true; 42 }; 43 } 44 ]; 45 46 options.virtualisation.darwin-builder = with lib; { 47 diskSize = mkOption { 48 default = 20 * 1024; 49 type = types.int; 50 example = 30720; 51 description = "The maximum disk space allocated to the runner in MB"; 52 }; 53 memorySize = mkOption { 54 default = 3 * 1024; 55 type = types.int; 56 example = 8192; 57 description = "The runner's memory in MB"; 58 }; 59 min-free = mkOption { 60 default = 1024 * 1024 * 1024; 61 type = types.int; 62 example = 1073741824; 63 description = '' 64 The threshold (in bytes) of free disk space left at which to 65 start garbage collection on the runner 66 ''; 67 }; 68 max-free = mkOption { 69 default = 3 * 1024 * 1024 * 1024; 70 type = types.int; 71 example = 3221225472; 72 description = '' 73 The threshold (in bytes) of free disk space left at which to 74 stop garbage collection on the runner 75 ''; 76 }; 77 workingDirectory = mkOption { 78 default = "."; 79 type = types.str; 80 example = "/var/lib/darwin-builder"; 81 description = '' 82 The working directory to use to run the script. When running 83 as part of a flake will need to be set to a non read-only filesystem. 84 ''; 85 }; 86 hostPort = mkOption { 87 default = 31022; 88 type = types.int; 89 example = 22; 90 description = '' 91 The localhost host port to forward TCP to the guest port. 92 ''; 93 }; 94 }; 95 96 config = { 97 # The builder is not intended to be used interactively 98 documentation.enable = false; 99 100 environment.etc = { 101 "ssh/ssh_host_ed25519_key" = { 102 mode = "0600"; 103 104 source = ./keys/ssh_host_ed25519_key; 105 }; 106 107 "ssh/ssh_host_ed25519_key.pub" = { 108 mode = "0644"; 109 110 source = ./keys/ssh_host_ed25519_key.pub; 111 }; 112 }; 113 114 # DNS fails for QEMU user networking (SLiRP) on macOS. See: 115 # 116 # https://github.com/utmapp/UTM/issues/2353 117 # 118 # This works around that by using a public DNS server other than the DNS 119 # server that QEMU provides (normally 10.0.2.3) 120 networking.nameservers = [ "8.8.8.8" ]; 121 122 # The linux builder is a lightweight VM for remote building; not evaluation. 123 nix.channel.enable = false; 124 125 # Deployment is by image. 126 # TODO system.switch.enable = false;? 127 system.disableInstallerTools = true; 128 129 nix.settings = { 130 min-free = cfg.min-free; 131 132 max-free = cfg.max-free; 133 134 trusted-users = [ user ]; 135 }; 136 137 services = { 138 getty.autologinUser = user; 139 140 openssh = { 141 enable = true; 142 143 authorizedKeysFiles = [ "${keysDirectory}/%u_${keyType}.pub" ]; 144 }; 145 }; 146 147 system.build.macos-builder-installer = 148 let 149 privateKey = "/etc/nix/${user}_${keyType}"; 150 151 publicKey = "${privateKey}.pub"; 152 153 # This installCredentials script is written so that it's as easy as 154 # possible for a user to audit before confirming the `sudo` 155 installCredentials = hostPkgs.writeShellScript "install-credentials" '' 156 set -euo pipefail 157 158 KEYS="''${1}" 159 INSTALL=${hostPkgs.coreutils}/bin/install 160 "''${INSTALL}" -g nixbld -m 600 "''${KEYS}/${user}_${keyType}" ${privateKey} 161 "''${INSTALL}" -g nixbld -m 644 "''${KEYS}/${user}_${keyType}.pub" ${publicKey} 162 ''; 163 164 hostPkgs = config.virtualisation.host.pkgs; 165 166 add-keys = hostPkgs.writeShellScriptBin "add-keys" ( 167 '' 168 set -euo pipefail 169 '' 170 + 171 # When running as non-interactively as part of a DarwinConfiguration the working directory 172 # must be set to a writeable directory. 173 ( 174 if cfg.workingDirectory != "." then 175 '' 176 ${hostPkgs.coreutils}/bin/mkdir --parent "${cfg.workingDirectory}" 177 cd "${cfg.workingDirectory}" 178 '' 179 else 180 "" 181 ) 182 + '' 183 KEYS="''${KEYS:-./keys}" 184 ${hostPkgs.coreutils}/bin/mkdir --parent "''${KEYS}" 185 PRIVATE_KEY="''${KEYS}/${user}_${keyType}" 186 PUBLIC_KEY="''${PRIVATE_KEY}.pub" 187 if [ ! -e "''${PRIVATE_KEY}" ] || [ ! -e "''${PUBLIC_KEY}" ]; then 188 ${hostPkgs.coreutils}/bin/rm --force -- "''${PRIVATE_KEY}" "''${PUBLIC_KEY}" 189 ${hostPkgs.openssh}/bin/ssh-keygen -q -f "''${PRIVATE_KEY}" -t ${keyType} -N "" -C 'builder@localhost' 190 fi 191 if ! ${hostPkgs.diffutils}/bin/cmp "''${PUBLIC_KEY}" ${publicKey}; then 192 (set -x; sudo --reset-timestamp ${installCredentials} "''${KEYS}") 193 fi 194 '' 195 ); 196 197 run-builder = hostPkgs.writeShellScriptBin "run-builder" ('' 198 set -euo pipefail 199 KEYS="''${KEYS:-./keys}" 200 KEYS="$(${hostPkgs.nix}/bin/nix-store --add "$KEYS")" ${lib.getExe config.system.build.vm} 201 ''); 202 203 script = hostPkgs.writeShellScriptBin "create-builder" ('' 204 set -euo pipefail 205 export KEYS="''${KEYS:-./keys}" 206 ${lib.getExe add-keys} 207 ${lib.getExe run-builder} 208 ''); 209 210 in 211 script.overrideAttrs (old: { 212 pos = __curPos; # sets meta.position to point here; see script binding above for package definition 213 meta = (old.meta or { }) // { 214 platforms = lib.platforms.darwin; 215 }; 216 passthru = (old.passthru or { }) // { 217 # Let users in the repl inspect the config 218 nixosConfig = config; 219 nixosOptions = options; 220 221 inherit add-keys run-builder; 222 }; 223 }); 224 225 system = { 226 # To prevent gratuitous rebuilds on each change to Nixpkgs 227 nixos.revision = null; 228 229 # to be updated by module maintainers, see nixpkgs#325610 230 stateVersion = "24.05"; 231 }; 232 233 users.users."${user}" = { 234 isNormalUser = true; 235 }; 236 237 security.polkit.enable = true; 238 239 security.polkit.extraConfig = '' 240 polkit.addRule(function(action, subject) { 241 if (action.id === "org.freedesktop.login1.power-off" && subject.user === "${user}") { 242 return "yes"; 243 } else { 244 return "no"; 245 } 246 }) 247 ''; 248 249 virtualisation = { 250 diskSize = cfg.diskSize; 251 252 memorySize = cfg.memorySize; 253 254 forwardPorts = [ 255 { 256 from = "host"; 257 guest.port = 22; 258 host.port = cfg.hostPort; 259 } 260 ]; 261 262 # Disable graphics for the builder since users will likely want to run it 263 # non-interactively in the background. 264 graphics = false; 265 266 sharedDirectories.keys = { 267 source = "\"$KEYS\""; 268 target = keysDirectory; 269 }; 270 271 # If we don't enable this option then the host will fail to delegate builds 272 # to the guest, because: 273 # 274 # - The host will lock the path to build 275 # - The host will delegate the build to the guest 276 # - The guest will attempt to lock the same path and fail because 277 # the lockfile on the host is visible on the guest 278 # 279 # Snapshotting the host's /nix/store as an image isolates the guest VM's 280 # /nix/store from the host's /nix/store, preventing this problem. 281 useNixStoreImage = true; 282 283 # Obviously the /nix/store needs to be writable on the guest in order for it 284 # to perform builds. 285 writableStore = true; 286 287 # This ensures that anything built on the guest isn't lost when the guest is 288 # restarted. 289 writableStoreUseTmpfs = false; 290 291 # Pass certificates from host to the guest otherwise when custom CA certificates 292 # are required we can't use the cached builder. 293 useHostCerts = true; 294 }; 295 }; 296}