this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at master 441 lines 17 kB view raw
1# NixOS module: Darling-based x86_64-darwin remote builder 2# 3# This module sets up a Darling instance as a Nix remote builder for 4# x86_64-darwin, allowing a Linux host to build Darwin packages without 5# Apple hardware. It manages: 6# 7# - SSH key generation for the Nix daemon ↔ Darling sshd connection 8# - Darling prefix initialisation (Nix install, sshd setup) 9# - A systemd service running sshd inside the Darling prefix 10# - Registration as a `nix.buildMachines` entry 11# - Optional /nix/store sharing between host and Darling prefix 12# 13# Usage (in a NixOS configuration): 14# 15# { 16# imports = [ ./path/to/darling-nix/nix/darlingBuilderModule.nix ]; 17# 18# services.darling-builder = { 19# enable = true; 20# maxJobs = 4; 21# shareStore = true; 22# }; 23# } 24# 25# After `nixos-rebuild switch`: 26# 27# nix build nixpkgs#hello --system x86_64-darwin 28# 29# See: plan/09-phase7-remote-builder.md 30{ 31 config, 32 lib, 33 pkgs, 34 ... 35}: 36 37let 38 cfg = config.services.darling-builder; 39 40 # Helper: script that initialises the Darling prefix for builder use. 41 # Idempotent — safe to run on every service start. 42 prefixInitScript = pkgs.writeShellScript "darling-builder-init" '' 43 set -euo pipefail 44 export PATH="${lib.makeBinPath [ cfg.package pkgs.coreutils pkgs.openssh ]}:$PATH" 45 46 DPREFIX="${cfg.prefixPath}" 47 export DPREFIX 48 49 echo "[darling-builder] Initialising prefix at $DPREFIX ..." 50 51 # First run creates the prefix (may take a while) 52 darling --prefix "$DPREFIX" shell true 53 54 # SSH setup 55 # Generate host keys inside the prefix if missing 56 darling --prefix "$DPREFIX" shell \ 57 bash -c 'test -f /etc/ssh/ssh_host_ed25519_key || ssh-keygen -A' 58 59 # Write a minimal sshd_config 60 darling --prefix "$DPREFIX" shell bash -c 'cat > /etc/ssh/sshd_config << EOF 61Port ${toString cfg.port} 62ListenAddress 127.0.0.1 63PermitRootLogin yes 64PubkeyAuthentication yes 65AuthorizedKeysFile .ssh/authorized_keys 66PasswordAuthentication no 67ChallengeResponseAuthentication no 68UsePAM no 69UsePrivilegeSeparation no 70Subsystem sftp /usr/libexec/sftp-server 71AcceptEnv NIX_REMOTE NIX_PATH NIX_SSL_CERT_FILE 72EOF 73' 74 75 # Install the public key 76 darling --prefix "$DPREFIX" shell mkdir -p /var/root/.ssh 77 cat "${cfg.sshKeyPath}.pub" | \ 78 darling --prefix "$DPREFIX" shell tee /var/root/.ssh/authorized_keys > /dev/null 79 darling --prefix "$DPREFIX" shell chmod 700 /var/root/.ssh 80 darling --prefix "$DPREFIX" shell chmod 600 /var/root/.ssh/authorized_keys 81 82 # Nix configuration 83 darling --prefix "$DPREFIX" shell mkdir -p /etc/nix 84 85 darling --prefix "$DPREFIX" shell bash -c 'cat > /etc/nix/nix.conf << EOF 86# Managed by NixOS darling-builder module do not edit 87build-users-group = 88sandbox = false 89experimental-features = nix-command flakes 90substituters = https://cache.nixos.org 91trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= 92EOF 93' 94 95 # Shared store (optional) 96 ${lib.optionalString cfg.shareStore '' 97 # Darling exposes the host root at /Volumes/SystemRoot inside the 98 # prefix, so /Volumes/SystemRoot/nix is the host's /nix. 99 # We symlink /nix -> /Volumes/SystemRoot/nix to share the store 100 # and avoid expensive SSH-based copying. 101 darling --prefix "$DPREFIX" shell bash -c ' 102 if [ ! -L /nix ] && [ ! -d /nix/store ]; then 103 rm -rf /nix 2>/dev/null || true 104 ln -sf /Volumes/SystemRoot/nix /nix 105 elif [ -L /nix ]; then 106 # Already a symlink make sure it points to the right place 107 target=$(readlink /nix) 108 if [ "$target" != "/Volumes/SystemRoot/nix" ]; then 109 rm /nix 110 ln -sf /Volumes/SystemRoot/nix /nix 111 fi 112 fi 113 ' 114 ''} 115 116 # Directory Services stubs (for multi-user Nix if needed) 117 # Verify the stubs are installed (they ship with our Darling package) 118 for tool in dseditgroup sysadminctl dscl; do 119 if ! darling --prefix "$DPREFIX" shell test -x "/usr/sbin/$tool"; then 120 echo "[darling-builder] WARNING: /usr/sbin/$tool not found in prefix" 121 fi 122 done 123 124 # Verify sandbox-exec stub 125 if ! darling --prefix "$DPREFIX" shell test -x /usr/bin/sandbox-exec; then 126 echo "[darling-builder] WARNING: /usr/bin/sandbox-exec not found in prefix" 127 fi 128 129 # Ensure /var/empty exists (home dir for build users) 130 darling --prefix "$DPREFIX" shell mkdir -p /var/empty 131 darling --prefix "$DPREFIX" shell chmod 555 /var/empty 132 133 # sshd privilege-separation directory 134 darling --prefix "$DPREFIX" shell mkdir -p /var/empty/sshd 135 darling --prefix "$DPREFIX" shell chmod 755 /var/empty/sshd 136 137 echo "[darling-builder] Prefix initialisation complete." 138 ''; 139 140 # Connectivity test script — useful for manual debugging 141 connectivityTestScript = pkgs.writeShellScript "darling-builder-test" '' 142 set -euo pipefail 143 export PATH="${lib.makeBinPath [ pkgs.openssh pkgs.coreutils ]}:$PATH" 144 145 echo "Testing SSH connectivity to Darling builder ..." 146 ssh -o StrictHostKeyChecking=no \ 147 -o UserKnownHostsFile=/dev/null \ 148 -i "${cfg.sshKeyPath}" \ 149 -p ${toString cfg.port} \ 150 root@127.0.0.1 \ 151 echo "SSH connection OK" 152 153 echo "Testing Nix inside Darling builder ..." 154 ssh -o StrictHostKeyChecking=no \ 155 -o UserKnownHostsFile=/dev/null \ 156 -i "${cfg.sshKeyPath}" \ 157 -p ${toString cfg.port} \ 158 root@127.0.0.1 \ 159 'source /Users/root/.nix-profile/etc/profile.d/nix.sh 2>/dev/null || true; nix --version' 160 161 echo "Testing nix store ping ..." 162 nix store ping --store "ssh://root@127.0.0.1?ssh-key=${cfg.sshKeyPath}" \ 163 --option ssh-port ${toString cfg.port} \ 164 && echo "nix store ping OK" \ 165 || echo "nix store ping FAILED (this may be expected if Nix is not yet installed in the prefix)" 166 167 echo "" 168 echo "Darling builder connectivity test complete." 169 ''; 170in 171{ 172 options.services.darling-builder = { 173 enable = lib.mkEnableOption "Darling-based x86_64-darwin remote Nix builder"; 174 175 package = lib.mkPackageOption pkgs "darling" { }; 176 177 port = lib.mkOption { 178 type = lib.types.port; 179 default = 2222; 180 description = '' 181 SSH port for the sshd instance running inside the Darling prefix. 182 Uses 2222 by default to avoid conflict with the host's sshd on port 22. 183 ''; 184 }; 185 186 maxJobs = lib.mkOption { 187 type = lib.types.ints.positive; 188 default = 4; 189 description = '' 190 Maximum number of concurrent builds the Darling builder will run. 191 Set this based on available CPU cores; Darwin builds inside Darling 192 are slower than native, so a conservative value is recommended. 193 ''; 194 }; 195 196 speedFactor = lib.mkOption { 197 type = lib.types.ints.unsigned; 198 default = 1; 199 description = '' 200 Speed factor for this builder relative to others. Lower values 201 mean Nix will prefer faster (native) builders when available. 202 Use 1 for a Darling-only setup, or a low value (1-5) when real 203 macOS builders are also configured. 204 ''; 205 }; 206 207 shareStore = lib.mkOption { 208 type = lib.types.bool; 209 default = true; 210 description = '' 211 Whether to share the host's /nix/store with the Darling prefix 212 via a symlink through /Volumes/SystemRoot/nix. This avoids the 213 expensive step of copying store paths over SSH and makes build 214 outputs immediately available on the host. 215 216 When disabled, Nix will use SSH-based store path transfer, which 217 is slower but simpler and avoids any store database conflicts. 218 ''; 219 }; 220 221 sshKeyPath = lib.mkOption { 222 type = lib.types.str; 223 default = "/etc/nix/darling-builder-key"; 224 description = '' 225 Path to the SSH ed25519 private key used by the host's Nix daemon 226 to connect to the sshd running inside Darling. The corresponding 227 public key (.pub) is installed into the Darling prefix's 228 authorized_keys. 229 230 The key is auto-generated on first activation if it doesn't exist. 231 ''; 232 }; 233 234 prefixPath = lib.mkOption { 235 type = lib.types.str; 236 default = "/var/lib/darling-builder"; 237 description = '' 238 Path to the Darling prefix used by the builder. This is a 239 separate prefix from the default (~/.darling) to avoid 240 interference with interactive Darling usage. 241 ''; 242 }; 243 244 supportedFeatures = lib.mkOption { 245 type = lib.types.listOf lib.types.str; 246 default = [ ]; 247 description = '' 248 List of supported features to advertise for this builder. 249 Derivations can require specific features (e.g. "big-parallel") 250 and will only be scheduled on builders that support them. 251 ''; 252 }; 253 254 mandatoryFeatures = lib.mkOption { 255 type = lib.types.listOf lib.types.str; 256 default = [ ]; 257 description = '' 258 List of mandatory features for this builder. Only derivations 259 that explicitly require ALL of these features will be scheduled 260 on this builder. Usually left empty. 261 ''; 262 }; 263 264 installNix = lib.mkOption { 265 type = lib.types.bool; 266 default = false; 267 description = '' 268 Whether to automatically install Nix inside the Darling prefix 269 during service startup. Requires network access to download the 270 Nix installer. If false (default), you must install Nix manually 271 using scripts/install-nix-in-darling.sh before enabling the builder. 272 273 NOTE: This is a slow operation on first run (downloads ~50MB) and 274 is best done manually. Set to true only for automated/CI setups. 275 ''; 276 }; 277 278 nixVersion = lib.mkOption { 279 type = lib.types.str; 280 default = "2.24.12"; 281 description = '' 282 Version of Nix to install inside the Darling prefix when 283 `installNix` is true. Must match a release on 284 https://releases.nixos.org/nix/. 285 ''; 286 }; 287 }; 288 289 config = lib.mkIf cfg.enable { 290 # Ensure the base Darling program is available 291 programs.darling.enable = true; 292 programs.darling.package = cfg.package; 293 294 # ── SSH key generation ─────────────────────────────────────────── 295 # Generate the ed25519 keypair on system activation if it doesn't 296 # already exist. The key is owned by root with mode 600. 297 system.activationScripts.darling-builder-keys = lib.stringAfter [ "users" ] '' 298 if [ ! -f "${cfg.sshKeyPath}" ]; then 299 echo "[darling-builder] Generating SSH keypair at ${cfg.sshKeyPath} ..." 300 mkdir -p "$(dirname "${cfg.sshKeyPath}")" 301 ${pkgs.openssh}/bin/ssh-keygen -t ed25519 -N "" -f "${cfg.sshKeyPath}" -C "darling-builder@$(hostname)" 302 chown root:root "${cfg.sshKeyPath}" "${cfg.sshKeyPath}.pub" 303 chmod 600 "${cfg.sshKeyPath}" 304 chmod 644 "${cfg.sshKeyPath}.pub" 305 fi 306 ''; 307 308 # ── Prefix directory ───────────────────────────────────────────── 309 systemd.tmpfiles.rules = [ 310 "d ${cfg.prefixPath} 0755 root root -" 311 ]; 312 313 # ── Darling builder service ────────────────────────────────────── 314 # This service: 315 # 1. Initialises the Darling prefix (idempotent) 316 # 2. Optionally installs Nix 317 # 3. Starts sshd inside the prefix (foreground, Type=simple) 318 # 319 # sshd runs in the foreground (-D) so systemd can manage its lifecycle. 320 systemd.services.darling-builder = { 321 description = "Darling x86_64-darwin Nix remote builder"; 322 wantedBy = [ "multi-user.target" ]; 323 after = [ "network.target" "nss-lookup.target" ]; 324 325 path = [ cfg.package pkgs.coreutils pkgs.openssh pkgs.curl ]; 326 327 environment = { 328 DPREFIX = cfg.prefixPath; 329 HOME = "/root"; 330 }; 331 332 serviceConfig = { 333 Type = "simple"; 334 335 # Initialise the prefix before starting sshd 336 ExecStartPre = [ 337 "+${prefixInitScript}" 338 ] ++ lib.optional cfg.installNix 339 "+${pkgs.writeShellScript "darling-builder-install-nix" '' 340 set -euo pipefail 341 export PATH="${lib.makeBinPath [ cfg.package pkgs.coreutils pkgs.curl pkgs.gnutar pkgs.xz ]}:$PATH" 342 export DPREFIX="${cfg.prefixPath}" 343 344 # Skip if Nix is already installed 345 if darling --prefix "$DPREFIX" shell test -x /nix/var/nix/profiles/default/bin/nix 2>/dev/null; then 346 echo "[darling-builder] Nix is already installed, skipping." 347 exit 0 348 fi 349 350 # Download and install Nix 351 nix_version="${cfg.nixVersion}" 352 installer_name="nix-''${nix_version}-x86_64-darwin" 353 installer_url="https://releases.nixos.org/nix/nix-''${nix_version}/''${installer_name}.tar.xz" 354 355 tmpdir=$(mktemp -d) 356 trap "rm -rf $tmpdir" EXIT 357 358 echo "[darling-builder] Downloading Nix $nix_version ..." 359 curl -fsSL -o "$tmpdir/nix-installer.tar.xz" "$installer_url" 360 361 echo "[darling-builder] Extracting ..." 362 mkdir -p "$tmpdir/extract" 363 tar -xf "$tmpdir/nix-installer.tar.xz" -C "$tmpdir/extract" 364 365 installer_dir=$(find "$tmpdir/extract" -maxdepth 1 -type d -name 'nix-*' | head -1) 366 if [ -z "$installer_dir" ]; then 367 echo "[darling-builder] ERROR: Could not find extracted installer directory" 368 exit 1 369 fi 370 371 # Patch: force single-user / no-daemon mode 372 sed -i 's/INSTALL_MODE=daemon/INSTALL_MODE=no-daemon/g' "$installer_dir/install" 373 374 # Copy into the Darling prefix 375 prefix_tmp="$DPREFIX/private/tmp/nix-installer" 376 mkdir -p "$prefix_tmp" 377 cp -a "$installer_dir/"* "$prefix_tmp/" 378 chmod +x "$prefix_tmp/install" 379 380 echo "[darling-builder] Running Nix installer inside Darling ..." 381 darling --prefix "$DPREFIX" shell env \ 382 NIX_INSTALLER_NO_MODIFY_PROFILE=0 \ 383 bash /tmp/nix-installer/install --no-daemon 384 385 rm -rf "$prefix_tmp" 386 echo "[darling-builder] Nix installation complete." 387 ''}"; 388 389 ExecStart = "${cfg.package}/bin/darling --prefix ${cfg.prefixPath} shell /usr/sbin/sshd -D -f /etc/ssh/sshd_config"; 390 391 # Stop sshd cleanly, then shut down the Darling prefix 392 ExecStopPost = "-${cfg.package}/bin/darling --prefix ${cfg.prefixPath} shutdown"; 393 394 Restart = "on-failure"; 395 RestartSec = 10; 396 397 # Give the prefix init plenty of time (first run is slow) 398 TimeoutStartSec = "10min"; 399 TimeoutStopSec = "30s"; 400 401 # Security hardening (the service runs as root because Darling 402 # needs namespace capabilities, but we restrict what it can do) 403 NoNewPrivileges = false; # Darling needs to create namespaces 404 ProtectSystem = "full"; 405 ProtectHome = "read-only"; 406 ReadWritePaths = [ 407 cfg.prefixPath 408 "/nix" 409 ]; 410 }; 411 }; 412 413 # ── Register as a Nix remote builder ───────────────────────────── 414 nix.buildMachines = [ 415 { 416 hostName = "127.0.0.1"; 417 port = cfg.port; 418 systems = [ "x86_64-darwin" ]; 419 sshUser = "root"; 420 sshKey = cfg.sshKeyPath; 421 maxJobs = cfg.maxJobs; 422 speedFactor = cfg.speedFactor; 423 supportedFeatures = cfg.supportedFeatures; 424 mandatoryFeatures = cfg.mandatoryFeatures; 425 } 426 ]; 427 428 nix.distributedBuilds = true; 429 430 # Trust the builder's SSH host key automatically on first connection 431 # to avoid interactive prompts when the Nix daemon connects. 432 nix.settings.trusted-users = [ "root" ]; 433 434 # ── Convenience: connectivity test script ──────────────────────── 435 environment.systemPackages = [ 436 (pkgs.writeShellScriptBin "darling-builder-test" '' 437 exec ${connectivityTestScript} "$@" 438 '') 439 ]; 440 }; 441}