this repo has no description
1
fork

Configure Feed

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

feat: Phase 7 remote builder module, build hook, compat matrix & VM test

- Phase 7.5: NixOS module (nix/darlingBuilderModule.nix) — services.darling-builder
with sshd inside Darling prefix, SSH key management, optional /nix/store sharing,
nix.buildMachines registration, and darling-builder-test diagnostic script
- Phase 7.4: Custom build hook (scripts/darling-build-hook) — offloads
x86_64-darwin builds to local Darling without SSH, supports Nix hook protocol
- Phase 6.5: Compatibility test matrix (tests/nix/compatibility-matrix.sh) —
4-tier package build tester with JSON reporting and cross-run comparison
- Phase 7 VM test (tests/darling-builder.nix) — 12-stage NixOS VM test covering
service startup, sshd, SSH auth, macOS identity, and restart resilience
- Wire darling-builder module and VM test into flake.nix
- Update PLAN.md, plan docs, and .gitignore

+1959 -8
+2
.gitignore
··· 35 35 !tests/syscall/ 36 36 !tests/dirserv/ 37 37 !tests/darling-smoke.nix 38 + !tests/darling-builder.nix 38 39 !tests/nix-in-darling.nix 40 + !tests/nix/ 39 41 40 42 # The suggested build folder 41 43 build
+100 -5
PLAN.md
··· 18 18 | Phase 3 — Nix Install | 🚧 In progress | `scripts/install-nix-in-darling.sh`, `scripts/darling-nix`, `scripts/verify-nix.sh` | 19 19 | Phase 4 — Building | 🚧 Tooling ready | `scripts/build-trivial.sh` (new) | 20 20 | Phase 5 — Daemon | 🚧 Stubs done | `src/dirserv/` (new), `tests/dirserv/` (new) | 21 - | Phase 6 — CI | 🚧 In progress | `.tangled/workflows/ci.yml`, `tests/darling-smoke.nix`, `tests/nix-in-darling.nix` (new) | 22 - | Phase 7 — Remote Builder | 📋 Planned | — | 21 + | Phase 6 — CI | 🚧 In progress | `.tangled/workflows/ci.yml`, `tests/darling-smoke.nix`, `tests/nix-in-darling.nix`, `tests/nix/compatibility-matrix.sh` (new) | 22 + | Phase 7 — Remote Builder | 🚧 Module & hook ready | `nix/darlingBuilderModule.nix` (new), `scripts/darling-build-hook` (new), `tests/darling-builder.nix` (new) | 23 23 | Phase 8 — Stretch | 📋 Planned | — | 24 24 25 25 ### Recently Completed 26 26 27 + - **Phase 7.5 — NixOS module for Darling builder**: Created 28 + `nix/darlingBuilderModule.nix` — a full NixOS module that sets up a 29 + Darling instance as a `nix.buildMachines` remote builder for 30 + `x86_64-darwin`. Manages SSH key generation, Darling prefix 31 + initialisation (sshd setup, nix.conf, Directory Services stubs 32 + verification, optional Nix auto-install), a `darling-builder` systemd 33 + service running sshd inside the prefix, optional `/nix/store` sharing 34 + via `/Volumes/SystemRoot/nix` symlink, and build machine registration. 35 + Includes a `darling-builder-test` connectivity diagnostic script. 36 + Wired into `flake.nix` as `nixosModules.darling-builder` and a new 37 + `checks.x86_64-linux.darling-builder` NixOS VM test. 38 + - **Phase 7.4 — Custom build hook**: Created `scripts/darling-build-hook` 39 + — a shell script that offloads `x86_64-darwin` builds to a local 40 + Darling instance without SSH. Supports the legacy Nix build hook 41 + protocol on stdin/stdout and direct `--build <drv>` invocations. 42 + Includes `--check` (environment validation), `--query-outputs`, 43 + `--machine-spec`, and `--verbose` modes. Configurable via environment 44 + variables (`DARLING_BUILD_HOOK_DARLING`, `DARLING_BUILD_HOOK_PREFIX`, 45 + etc.). 46 + - **Phase 7 — NixOS VM test for remote builder**: Created 47 + `tests/darling-builder.nix` — 12-stage NixOS VM test covering service 48 + startup, SSH key generation, sshd reachability, SSH key auth, macOS 49 + identity via SSH, sshd config validation, nix.conf inside prefix, 50 + build machine registration, Directory Services stubs via SSH, 51 + sandbox-exec via SSH, file operations, and service restart resilience. 52 + - **Phase 6.5 — Nix compatibility test matrix**: Created 53 + `tests/nix/compatibility-matrix.sh` — systematic package build tester 54 + with 4 tiers (must-pass / should-pass / stretch / aspirational), 55 + JSON reporting, cross-run comparison (`--compare`), per-package 56 + timeouts, tier/package filtering, colourised output, and CI-friendly 57 + exit codes (exit 2 on tier-1 regressions). 27 58 - **Phase 5.1 — Directory Services stubs**: Created `src/dirserv/` with three 28 59 shell-script stubs (`dseditgroup`, `sysadminctl`, `dscl`) that translate 29 60 macOS Directory Services commands to direct `/etc/passwd` and `/etc/group` ··· 164 195 ```text 165 196 darling-nix/ 166 197 ├── .tangled/workflows/ci.yml # tangled.org CI workflow (Phase 6) 167 - ├── flake.nix # Flake with package, devShell, NixOS module (Phase 0) 198 + ├── flake.nix # Flake with package, devShell, NixOS module, builder (Phase 0, 7) 168 199 ├── nix/ 169 200 │ ├── package.nix # Darling Nix derivation (Phase 0) 170 201 │ ├── devShell.nix # Developer shell (Phase 0) 171 - │ └── nixosModule.nix # NixOS module (Phase 0) 202 + │ ├── nixosModule.nix # NixOS module — programs.darling (Phase 0) 203 + │ └── darlingBuilderModule.nix # NEW — NixOS module — services.darling-builder (Phase 7.5) 172 204 ├── scripts/ 173 205 │ ├── build-trivial.sh # NEW — Progressive derivation build tests (Phase 4) 206 + │ ├── darling-build-hook # NEW — Nix build hook for local Darling builds (Phase 7.4) 174 207 │ ├── darling-nix # Host-side Nix command wrapper (Phase 3) 175 208 │ ├── install-nix-in-darling.sh # Automated Nix installer (Phase 3) 176 209 │ ├── run-tests.sh # NEW — Unified regression test runner (6 suites) ··· 188 221 │ │ └── sandbox-exec.c 189 222 │ └── diskutil/diskutil # Extended with info/list verbs (Phase 3) 190 223 ├── tests/ 224 + │ ├── darling-builder.nix # NEW — NixOS VM test for remote builder (Phase 7) 191 225 │ ├── darling-smoke.nix # NEW — NixOS VM smoke test (Phase 6.6) 192 226 │ ├── nix-in-darling.nix # NEW — NixOS VM integration test (Phase 6.1) 193 227 │ ├── dirserv/ # NEW — Directory Services regression tests 194 228 │ │ └── test_dirserv.sh # 60+ tests for dseditgroup/sysadminctl/dscl 229 + │ ├── nix/ # NEW — Nix-level compatibility tests 230 + │ │ └── compatibility-matrix.sh # Package build matrix with 4 tiers (Phase 6.5) 195 231 │ ├── sandbox/ # NEW — sandbox regression tests 196 232 │ │ ├── test_sandbox_api.c # C-level sandbox API tests 197 233 │ │ └── test_sandbox_exec.sh # Shell-level sandbox-exec tests ··· 218 254 219 255 ## What's Next 220 256 221 - The **critical path to MVP** (Nix running inside Darling) is: 257 + The **critical path to MVP** (Nix running inside Darling) is steps 1–4. 258 + Step 5 (remote builder) extends the MVP into a **usable Darwin builder**. 222 259 223 260 1. **Build & test**: All core Phase 1 syscall work is complete. Build 224 261 Darling with these changes and run the full regression test suite: ··· 287 324 4. Derivation dependency — one derivation consumes another's output 288 325 5. Binary substitution — fetch pre-built `hello` from cache.nixos.org 289 326 327 + 5. **Phase 7 — Remote builder**: With Nix building derivations successfully, 328 + enable the host's Nix daemon to offload `x86_64-darwin` builds to Darling. 329 + All infrastructure is in place — choose one of two approaches: 330 + 331 + **Option A — NixOS module (SSH-based, recommended)**: 332 + 333 + ```nix 334 + # In your NixOS configuration: 335 + imports = [ 336 + darling-nix.nixosModules.nixos # programs.darling 337 + darling-nix.nixosModules.darling-builder # services.darling-builder 338 + ]; 339 + 340 + services.darling-builder = { 341 + enable = true; 342 + maxJobs = 4; 343 + shareStore = true; # share /nix/store via /Volumes/SystemRoot 344 + }; 345 + ``` 346 + 347 + After `nixos-rebuild switch`: 348 + 349 + ```bash 350 + # Test connectivity 351 + darling-builder-test 352 + 353 + # Build a Darwin package from your Linux host 354 + nix build nixpkgs#hello --system x86_64-darwin 355 + ``` 356 + 357 + **Option B — Custom build hook (no SSH)**: 358 + 359 + ```bash 360 + # Verify the hook environment 361 + ./scripts/darling-build-hook --check 362 + 363 + # Build a derivation directly 364 + ./scripts/darling-build-hook --build /nix/store/...-foo.drv 365 + ``` 366 + 367 + **Remaining Phase 7 tasks** (7.1, 7.2, 7.3, 7.6, 7.7): 368 + - 7.1: Verify `sshd` actually works inside Darling (needs live testing) 369 + - 7.2: Test `nix store ping` from host to Darling builder 370 + - 7.3: Validate shared `/nix/store` via `/Volumes/SystemRoot` symlink 371 + - 7.6: Run compatibility matrix against the builder: 372 + ```bash 373 + ./tests/nix/compatibility-matrix.sh --tier 1 374 + ./tests/nix/compatibility-matrix.sh --output results.json --compare previous.json 375 + ``` 376 + - 7.7: Write user-facing docs and flake template 377 + 290 378 ### Completed Task Summary 291 379 292 380 | Task | Status | Description | ··· 310 398 | 6.1 | ✅ | NixOS VM test (`tests/nix-in-darling.nix`) | 311 399 | 6.2 | ✅ | Wired tests into `flake.nix` (checks output) | 312 400 | 6.3 | ✅ | `.tangled/workflows/ci.yml` tangled.org CI workflow | 401 + | 6.5 | ✅ | Nix compatibility test matrix (`tests/nix/compatibility-matrix.sh`) | 313 402 | 6.6 | ✅ | Darling smoke test (`tests/darling-smoke.nix`) | 403 + | 7.4 | ✅ | Custom build hook (`scripts/darling-build-hook`) | 404 + | 7.5 | ✅ | NixOS module for Darling builder (`nix/darlingBuilderModule.nix`) | 405 + | — | ✅ | NixOS VM test for remote builder (`tests/darling-builder.nix`) | 314 406 | — | ✅ | `run-tests.sh` unified test runner (6 suites) | 315 407 | — | ✅ | `getattrlist` attribute buffer ordering bug fixed | 316 408 | — | ✅ | `diskutil info`/`list` stubs | ··· 326 418 | `scripts/verify-nix.sh` | Health-check a Nix installation inside Darling | After install, or to diagnose regressions | 327 419 | `scripts/darling-nix` | Run Nix commands inside Darling from the host | Day-to-day Nix usage | 328 420 | `scripts/build-trivial.sh` | Test derivation building with 5 progressive levels | After Nix is installed and verified | 421 + | `scripts/darling-build-hook` | Nix build hook for local Darling builds (no SSH) | Alternative to SSH-based remote builder | 422 + | `tests/nix/compatibility-matrix.sh` | Systematic package build test with 4 tiers + JSON | After Nix builds work; in CI nightly | 329 423 | `nix build .#checks.x86_64-linux.dirserv-stubs` | Run Directory Services stub tests (no Darling needed) | After editing `src/dirserv/` | 330 424 | `nix build .#checks.x86_64-linux.darling-smoke -L` | NixOS VM smoke test (no network) | After building Darling | 331 425 | `nix build .#checks.x86_64-linux.nix-in-darling -L` | Full Nix-in-Darling integration test | End-to-end validation | 426 + | `nix build .#checks.x86_64-linux.darling-builder -L` | Remote builder VM test (sshd, SSH auth, service) | After editing `nix/darlingBuilderModule.nix` | 332 427 333 428 See [plan/README.md](./plan/README.md) for the full priority table and effort 334 429 estimates.
+29
flake.nix
··· 29 29 30 30 packages.darling-sdk = pkgs: pkgs.darling.sdk; 31 31 32 + # ── NixOS Modules ──────────────────────────────────────────────── 33 + # 34 + # The base module (programs.darling) is autoloaded from 35 + # ./nix/nixosModule.nix by flakelight. 36 + # 37 + # The darling-builder module (services.darling-builder) is exported 38 + # separately so users can import it alongside the base module. 39 + # 40 + # Usage in a NixOS configuration: 41 + # { 42 + # imports = [ 43 + # darling-nix.nixosModules.nixos # programs.darling 44 + # darling-nix.nixosModules.darling-builder # services.darling-builder 45 + # ]; 46 + # services.darling-builder.enable = true; 47 + # } 48 + nixosModules.darling-builder = import ./nix/darlingBuilderModule.nix; 49 + 32 50 # ── Checks (Phase 6.2) ─────────────────────────────────────────── 33 51 # 34 52 # NixOS VM integration tests and lightweight validation checks. ··· 36 54 # nix flake check # all checks 37 55 # nix build .#checks.x86_64-linux.darling-smoke -L 38 56 # nix build .#checks.x86_64-linux.nix-in-darling -L 57 + # nix build .#checks.x86_64-linux.darling-builder -L 39 58 # 40 59 # See: plan/08-phase6-ci.md (Tasks 6.1, 6.2) 60 + # plan/09-phase7-remote-builder.md (Task 7.5) 41 61 checks = pkgs: 42 62 let 43 63 darling = pkgs.darling; 64 + darlingBuilderModule = import ./nix/darlingBuilderModule.nix; 44 65 in 45 66 { 46 67 # ── Build check ───────────────────────────────────────────────── ··· 62 83 # Requires network access (downloads Nix installer + store paths). 63 84 nix-in-darling = import ./tests/nix-in-darling.nix { 64 85 inherit pkgs darling; 86 + }; 87 + 88 + # ── Darling builder test (Phase 7.5) ──────────────────────────── 89 + # NixOS VM test for the remote builder module: verifies the 90 + # systemd service starts, sshd inside the prefix is reachable, 91 + # SSH key auth works, and Darling identity is correct via SSH. 92 + darling-builder = import ./tests/darling-builder.nix { 93 + inherit pkgs darling darlingBuilderModule; 65 94 }; 66 95 67 96 # ── Directory Services stubs unit test ──────────────────────────
+441
nix/darlingBuilderModule.nix
··· 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 + 37 + let 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 61 + Port ${toString cfg.port} 62 + ListenAddress 127.0.0.1 63 + PermitRootLogin yes 64 + PubkeyAuthentication yes 65 + AuthorizedKeysFile .ssh/authorized_keys 66 + PasswordAuthentication no 67 + ChallengeResponseAuthentication no 68 + UsePAM no 69 + UsePrivilegeSeparation no 70 + Subsystem sftp /usr/libexec/sftp-server 71 + AcceptEnv NIX_REMOTE NIX_PATH NIX_SSL_CERT_FILE 72 + EOF 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 87 + build-users-group = 88 + sandbox = false 89 + experimental-features = nix-command flakes 90 + substituters = https://cache.nixos.org 91 + trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= 92 + EOF 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 + ''; 170 + in 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 + }
+8 -1
plan/08-phase6-ci.md
··· 420 420 421 421 --- 422 422 423 - ### 6.5 — Nix Compatibility Test Matrix 423 + ### 6.5 — Nix Compatibility Test Matrix ✅ 424 + 425 + > **Status**: Complete. Implemented at `tests/nix/compatibility-matrix.sh`. 426 + > Systematic package build tester with 4 tiers (must-pass, should-pass, 427 + > stretch, aspirational), JSON reporting, cross-run comparison (`--compare`), 428 + > per-package timeouts, tier/package filtering (`--tier`, `--packages`), 429 + > colourised output, `--dry-run` and `--list` modes, and CI-friendly exit 430 + > codes (exit 2 on tier-1 regressions, exit 1 on other failures). 424 431 425 432 Create a test that attempts to build an expanding set of Nixpkgs packages inside 426 433 Darling and tracks pass/fail rates over time.
+18 -2
plan/09-phase7-remote-builder.md
··· 262 262 263 263 --- 264 264 265 - ### 7.4 — Alternative: Custom Build Hook (No SSH) 265 + ### 7.4 — Alternative: Custom Build Hook (No SSH) ✅ 266 + 267 + > **Status**: Complete. Implemented at `scripts/darling-build-hook`. Supports 268 + > the legacy Nix build hook protocol on stdin/stdout and direct `--build <drv>` 269 + > invocations. Includes `--check` (environment validation), `--query-outputs`, 270 + > `--machine-spec`, and `--verbose` modes. Configurable via environment 271 + > variables (`DARLING_BUILD_HOOK_DARLING`, `DARLING_BUILD_HOOK_PREFIX`, etc.). 266 272 267 273 Instead of SSH, implement a custom Nix build hook that invokes `darling shell` 268 274 directly. This avoids the SSH setup entirely and may have lower overhead. ··· 315 321 316 322 --- 317 323 318 - ### 7.5 — NixOS Module for the Darling Builder 324 + ### 7.5 — NixOS Module for the Darling Builder ✅ 325 + 326 + > **Status**: Complete. Implemented at `nix/darlingBuilderModule.nix`. Full 327 + > NixOS module with `services.darling-builder` options: `enable`, `package`, 328 + > `port`, `maxJobs`, `speedFactor`, `shareStore`, `sshKeyPath`, `prefixPath`, 329 + > `supportedFeatures`, `mandatoryFeatures`, `installNix`, `nixVersion`. 330 + > Manages SSH key generation, prefix init (sshd, nix.conf, stubs verification), 331 + > systemd service, optional `/nix/store` sharing, and `nix.buildMachines` 332 + > registration. Includes `darling-builder-test` connectivity script. 333 + > Wired into `flake.nix` as `nixosModules.darling-builder`. 334 + > NixOS VM test at `tests/darling-builder.nix` (12 stages). 319 335 320 336 Wrap all the setup (sshd, keys, store sharing, `nix.buildMachines`) into a 321 337 reusable NixOS module.
+4
plan/README.md
··· 60 60 | `scripts/build-trivial.sh` | Progressive derivation build tests (5 levels) for Phase 4 | 61 61 | `scripts/darling-nix` | Host-side wrapper to run Nix commands inside Darling | 62 62 | `scripts/triage-syscalls.sh` | Automated syscall triage — discovers unimplemented syscalls during Nix ops | 63 + | `scripts/darling-build-hook` | Nix build hook — offloads `x86_64-darwin` builds to local Darling without SSH (Phase 7.4) | 64 + | `nix/darlingBuilderModule.nix` | NixOS module — `services.darling-builder` remote builder setup (Phase 7.5) | 63 65 | `src/dirserv/dseditgroup` | Directory Services stub — group create/edit/delete/checkmember/read (Phase 5.1) | 64 66 | `src/dirserv/sysadminctl` | Directory Services stub — addUser/deleteUser with UID/GID/home/shell (Phase 5.1) | 65 67 | `src/dirserv/dscl` | Directory Services stub — read/list/create/delete/append/search (Phase 5.1) | 66 68 | `tests/darling-smoke.nix` | NixOS VM smoke test — Darling boot, stubs, filesystem, no network (Phase 6.6) | 67 69 | `tests/nix-in-darling.nix` | NixOS VM integration test — full Nix install + eval + build (Phase 6.1) | 70 + | `tests/darling-builder.nix` | NixOS VM test — remote builder service, sshd, SSH auth, restart resilience (Phase 7) | 71 + | `tests/nix/compatibility-matrix.sh` | Systematic package build test — 4 tiers, JSON reports, cross-run comparison (Phase 6.5) | 68 72 | `tests/dirserv/test_dirserv.sh` | Shell-level tests for Directory Services stubs (60+ tests) | 69 73 | `tests/sandbox/test_sandbox_api.c` | C-level regression tests for sandbox API stubs | 70 74 | `tests/sandbox/test_sandbox_exec.sh` | Shell-level tests for the `sandbox-exec` stub binary |
+410
scripts/darling-build-hook
··· 1 + #!/usr/bin/env bash 2 + # darling-build-hook — Nix build hook that offloads x86_64-darwin builds to Darling 3 + # 4 + # Instead of SSH-based remote building, this hook invokes `darling shell` 5 + # directly on the local machine. This avoids the SSH setup overhead and 6 + # is simpler to configure for single-machine setups. 7 + # 8 + # Nix build hook protocol (simplified): 9 + # 1. Nix sends a line on fd 4: "# build <drv-path> <system> ..." 10 + # 2. The hook reads from fd 5 for instructions. 11 + # 3. The hook writes responses to fd 6. 12 + # 4. If the hook accepts, it performs the build and reports results. 13 + # 14 + # However, the *actual* protocol used by Nix ≥ 2.4 is the "recursive Nix" 15 + # / post-build-hook style, where the hook is specified via `build-hook` in 16 + # nix.conf and communicates via a specific protocol on stdin/stdout. 17 + # 18 + # For the simpler `builders` / `buildMachines` approach, Nix has built-in 19 + # SSH-based offloading. This script implements the *alternative* approach 20 + # described in Phase 7.4 of the plan: a lightweight wrapper that Nix's 21 + # `--builders` flag can point to, using the `@/path/to/hook` syntax or 22 + # the `system-features` / `post-build-hook` mechanism. 23 + # 24 + # In practice, this script is designed to be used as: 25 + # 26 + # nix build --builders 'darling-build-hook x86_64-darwin - 4 1 - - -' 27 + # 28 + # Or configured in nix.conf / NixOS configuration: 29 + # 30 + # nix.settings.builders = [ 31 + # "darling-build-hook x86_64-darwin - 4 1 - - -" 32 + # ]; 33 + # 34 + # It can also be invoked directly for testing: 35 + # 36 + # ./scripts/darling-build-hook --build <drv-path> 37 + # ./scripts/darling-build-hook --check 38 + # ./scripts/darling-build-hook --help 39 + # 40 + # See: plan/09-phase7-remote-builder.md (Task 7.4) 41 + 42 + set -euo pipefail 43 + 44 + # ── Configuration ──────────────────────────────────────────────────── 45 + 46 + # Darling binary (auto-detected or overridden via environment) 47 + DARLING="${DARLING_BUILD_HOOK_DARLING:-darling}" 48 + 49 + # Darling prefix (default: whatever darling uses, usually ~/.darling) 50 + DPREFIX="${DARLING_BUILD_HOOK_PREFIX:-${DPREFIX:-}}" 51 + 52 + # Nix profile to source inside Darling 53 + NIX_PROFILE="${DARLING_BUILD_HOOK_NIX_PROFILE:-/Users/root/.nix-profile/etc/profile.d/nix.sh}" 54 + 55 + # Maximum concurrent jobs (for informational purposes; actual limiting 56 + # is done by the Nix daemon based on maxJobs in the builders spec) 57 + MAX_JOBS="${DARLING_BUILD_HOOK_MAX_JOBS:-4}" 58 + 59 + # Verbosity 60 + VERBOSE="${DARLING_BUILD_HOOK_VERBOSE:-0}" 61 + 62 + # ── Helpers ────────────────────────────────────────────────────────── 63 + 64 + log() { 65 + echo "[darling-build-hook] $*" >&2 66 + } 67 + 68 + debug() { 69 + if [[ "$VERBOSE" -ge 1 ]]; then 70 + echo "[darling-build-hook] [debug] $*" >&2 71 + fi 72 + } 73 + 74 + die() { 75 + echo "[darling-build-hook] ERROR: $*" >&2 76 + exit 1 77 + } 78 + 79 + # Run a command inside the Darling prefix with Nix on PATH 80 + darling_run() { 81 + local prefix_args=() 82 + if [[ -n "$DPREFIX" ]]; then 83 + prefix_args=(--prefix "$DPREFIX") 84 + fi 85 + 86 + "$DARLING" "${prefix_args[@]}" shell bash -lc " 87 + # Source Nix profile 88 + for p in \ 89 + '$NIX_PROFILE' \ 90 + /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh \ 91 + /nix/var/nix/profiles/default/etc/profile.d/nix.sh; do 92 + if [ -e \"\$p\" ]; then 93 + . \"\$p\" 94 + break 95 + fi 96 + done 97 + exec \"\$@\" 98 + " -- "$@" 99 + } 100 + 101 + # ── Subcommands ────────────────────────────────────────────────────── 102 + 103 + # Check that the Darling builder environment is functional 104 + cmd_check() { 105 + local exit_code=0 106 + 107 + log "Checking Darling builder environment ..." 108 + 109 + # 1. Darling binary exists 110 + if command -v "$DARLING" &>/dev/null; then 111 + log " ✓ darling binary found: $(command -v "$DARLING")" 112 + else 113 + log " ✗ darling binary not found (looked for: $DARLING)" 114 + exit_code=1 115 + fi 116 + 117 + # 2. Darling shell works 118 + if result=$("$DARLING" shell echo "ok" 2>&1) && [[ "$result" == *"ok"* ]]; then 119 + log " ✓ darling shell functional" 120 + else 121 + log " ✗ darling shell not working: $result" 122 + exit_code=1 123 + fi 124 + 125 + # 3. Reports Darwin 126 + if result=$("$DARLING" shell uname -s 2>&1) && [[ "$result" == *"Darwin"* ]]; then 127 + log " ✓ uname -s reports Darwin" 128 + else 129 + log " ✗ uname -s does not report Darwin: $result" 130 + exit_code=1 131 + fi 132 + 133 + # 4. sandbox-exec stub 134 + if "$DARLING" shell test -x /usr/bin/sandbox-exec 2>/dev/null; then 135 + log " ✓ sandbox-exec stub present" 136 + else 137 + log " ✗ sandbox-exec stub missing" 138 + exit_code=1 139 + fi 140 + 141 + # 5. Nix is installed and functional 142 + if result=$(darling_run nix --version 2>&1); then 143 + log " ✓ nix installed: $result" 144 + else 145 + log " ✗ nix not found or not working inside Darling" 146 + log " Run scripts/install-nix-in-darling.sh first" 147 + exit_code=1 148 + fi 149 + 150 + # 6. System check 151 + if result=$(darling_run nix eval --raw --expr 'builtins.currentSystem' 2>&1); then 152 + if [[ "$result" == "x86_64-darwin" ]]; then 153 + log " ✓ builtins.currentSystem = x86_64-darwin" 154 + else 155 + log " ✗ builtins.currentSystem = $result (expected x86_64-darwin)" 156 + exit_code=1 157 + fi 158 + else 159 + log " ✗ nix eval failed: $result" 160 + exit_code=1 161 + fi 162 + 163 + # 7. Nix store accessible 164 + if darling_run nix-store --verify --no-build &>/dev/null; then 165 + log " ✓ nix store accessible and verified" 166 + else 167 + log " ⚠ nix store verification failed (may be OK if store is empty)" 168 + fi 169 + 170 + if [[ "$exit_code" -eq 0 ]]; then 171 + log "All checks passed ✓" 172 + else 173 + log "Some checks failed — see above" 174 + fi 175 + 176 + return "$exit_code" 177 + } 178 + 179 + # Build a single derivation inside Darling 180 + cmd_build() { 181 + local drv_path="$1" 182 + 183 + debug "Building derivation: $drv_path" 184 + 185 + # Validate the derivation path 186 + if [[ ! "$drv_path" == /nix/store/*.drv ]]; then 187 + die "Invalid derivation path: $drv_path (expected /nix/store/....drv)" 188 + fi 189 + 190 + # Check if the derivation exists in the store 191 + if [[ ! -e "$drv_path" ]]; then 192 + die "Derivation does not exist: $drv_path" 193 + fi 194 + 195 + # Query the system field to confirm it's a Darwin derivation 196 + local drv_system 197 + drv_system=$(nix derivation show "$drv_path" 2>/dev/null | grep -o '"system":"[^"]*"' | head -1 | cut -d'"' -f4) || true 198 + if [[ -n "$drv_system" && "$drv_system" != "x86_64-darwin" ]]; then 199 + die "Derivation system is '$drv_system', not x86_64-darwin — refusing to build" 200 + fi 201 + 202 + # Realise the derivation inside Darling 203 + log "Realising $drv_path inside Darling ..." 204 + local start_time 205 + start_time=$(date +%s) 206 + 207 + darling_run nix-store --realise "$drv_path" --add-root /tmp/darling-build-result --indirect 208 + local rc=$? 209 + 210 + local end_time elapsed 211 + end_time=$(date +%s) 212 + elapsed=$((end_time - start_time)) 213 + 214 + if [[ "$rc" -eq 0 ]]; then 215 + log "Build succeeded in ${elapsed}s: $drv_path" 216 + else 217 + log "Build FAILED (exit $rc, ${elapsed}s): $drv_path" 218 + fi 219 + 220 + return "$rc" 221 + } 222 + 223 + # Interactive: query the derivation's outputs 224 + cmd_query_outputs() { 225 + local drv_path="$1" 226 + darling_run nix-store --query --outputs "$drv_path" 227 + } 228 + 229 + # Print the builder machine spec line in Nix's expected format 230 + cmd_machine_spec() { 231 + # Format: <store-uri> <system> <ssh-key> <max-jobs> <speed-factor> <supported-features> <mandatory-features> <public-host-key> 232 + # For a local hook, most fields are placeholders 233 + echo "- x86_64-darwin - $MAX_JOBS 1 - - -" 234 + } 235 + 236 + # ── Usage ──────────────────────────────────────────────────────────── 237 + 238 + usage() { 239 + cat >&2 << 'EOF' 240 + Usage: darling-build-hook [--help] [--check] [--build <drv-path>] 241 + [--query-outputs <drv-path>] [--machine-spec] 242 + 243 + Nix build hook that offloads x86_64-darwin builds to a local Darling instance. 244 + 245 + Subcommands: 246 + --check Verify the Darling builder environment is functional 247 + --build <drv> Build a derivation inside Darling 248 + --query-outputs <drv> Print the output paths of a derivation 249 + --machine-spec Print the Nix builder machine specification line 250 + --help Show this help 251 + 252 + Environment variables: 253 + DARLING_BUILD_HOOK_DARLING Path to the darling binary (default: darling) 254 + DARLING_BUILD_HOOK_PREFIX Darling prefix path (default: auto) 255 + DARLING_BUILD_HOOK_NIX_PROFILE Nix profile script inside Darling 256 + DARLING_BUILD_HOOK_MAX_JOBS Max concurrent jobs (default: 4) 257 + DARLING_BUILD_HOOK_VERBOSE Verbosity level (0=quiet, 1=debug) 258 + DPREFIX Fallback for Darling prefix path 259 + 260 + When invoked by Nix as a build hook (no arguments), the script reads the 261 + Nix build hook protocol from file descriptors and dispatches builds to 262 + Darling automatically. 263 + 264 + Examples: 265 + # Check the builder is ready 266 + ./scripts/darling-build-hook --check 267 + 268 + # Build a specific derivation 269 + ./scripts/darling-build-hook --build /nix/store/...-foo.drv 270 + 271 + # Use as a Nix builder via the builders option (nix.conf or CLI): 272 + # builders = /path/to/darling-build-hook x86_64-darwin - 4 1 - - - 273 + # 274 + # Or in NixOS configuration: 275 + # nix.settings.builders = [ 276 + # "/path/to/darling-build-hook x86_64-darwin - 4 1 - - -" 277 + # ]; 278 + 279 + See: plan/09-phase7-remote-builder.md (Task 7.4) 280 + EOF 281 + } 282 + 283 + # ── Nix build hook protocol handler ───────────────────────────────── 284 + # 285 + # When Nix invokes this script as a build hook, it communicates via a 286 + # specific protocol. The hook reads requests from stdin and writes 287 + # responses to stdout. 288 + # 289 + # Protocol (Nix ≥ 2.0 legacy build hook): 290 + # - Nix sends on stdin: "1\n<drv-path>\n<system>\n" 291 + # - Hook responds on stdout: 292 + # "# decline\n" — to decline the build 293 + # "# postpone\n" — to postpone (try again later) 294 + # "# accept\n" — to accept, then perform the build 295 + # 296 + # For modern Nix (≥ 2.4), the build-hook setting is less commonly used 297 + # in favour of nix.buildMachines with SSH. This handler supports the 298 + # legacy protocol for compatibility and adds the direct --build mode 299 + # for manual/scripted use. 300 + 301 + handle_hook_protocol() { 302 + debug "Entering build hook protocol handler (reading from stdin)" 303 + 304 + while true; do 305 + # Read the request version 306 + local version 307 + if ! IFS= read -r version; then 308 + debug "EOF on stdin — exiting hook loop" 309 + break 310 + fi 311 + 312 + # Trim whitespace 313 + version="${version#"${version%%[![:space:]]*}"}" 314 + version="${version%"${version##*[![:space:]]}"}" 315 + 316 + if [[ -z "$version" ]]; then 317 + continue 318 + fi 319 + 320 + if [[ "$version" != "1" ]]; then 321 + debug "Unknown hook protocol version: '$version' — declining" 322 + echo "# decline" 323 + continue 324 + fi 325 + 326 + # Read derivation path and system 327 + local drv_path system 328 + IFS= read -r drv_path || break 329 + IFS= read -r system || break 330 + 331 + # Trim whitespace 332 + drv_path="${drv_path#"${drv_path%%[![:space:]]*}"}" 333 + drv_path="${drv_path%"${drv_path##*[![:space:]]}"}" 334 + system="${system#"${system%%[![:space:]]*}"}" 335 + system="${system%"${system##*[![:space:]]}"}" 336 + 337 + debug "Hook request: drv=$drv_path system=$system" 338 + 339 + # Only accept x86_64-darwin builds 340 + if [[ "$system" != "x86_64-darwin" ]]; then 341 + debug "Declining: system '$system' is not x86_64-darwin" 342 + echo "# decline" 343 + continue 344 + fi 345 + 346 + echo "# accept" 347 + 348 + # Perform the build 349 + if cmd_build "$drv_path"; then 350 + debug "Hook build succeeded: $drv_path" 351 + else 352 + log "Hook build failed: $drv_path" 353 + # The build failure is communicated via the exit code of 354 + # nix-store --realise inside cmd_build. Nix will see the 355 + # missing output paths and report the failure. 356 + fi 357 + done 358 + } 359 + 360 + # ── Main ───────────────────────────────────────────────────────────── 361 + 362 + main() { 363 + if [[ $# -eq 0 ]]; then 364 + # No arguments — invoked as a build hook by Nix. 365 + # Check if stdin is a pipe/fd (i.e., Nix is talking to us) 366 + if [[ -p /dev/stdin ]] || [[ ! -t 0 ]]; then 367 + handle_hook_protocol 368 + else 369 + # Interactive invocation with no args — show help 370 + usage 371 + exit 0 372 + fi 373 + exit 0 374 + fi 375 + 376 + case "${1:-}" in 377 + --help | -h) 378 + usage 379 + exit 0 380 + ;; 381 + --check) 382 + cmd_check 383 + ;; 384 + --build) 385 + if [[ $# -lt 2 ]]; then 386 + die "--build requires a derivation path argument" 387 + fi 388 + cmd_build "$2" 389 + ;; 390 + --query-outputs) 391 + if [[ $# -lt 2 ]]; then 392 + die "--query-outputs requires a derivation path argument" 393 + fi 394 + cmd_query_outputs "$2" 395 + ;; 396 + --machine-spec) 397 + cmd_machine_spec 398 + ;; 399 + --verbose | -v) 400 + VERBOSE=1 401 + shift 402 + main "$@" 403 + ;; 404 + *) 405 + die "Unknown argument: $1 (try --help)" 406 + ;; 407 + esac 408 + } 409 + 410 + main "$@"
+318
tests/darling-builder.nix
··· 1 + # NixOS VM test: Darling remote builder 2 + # 3 + # This test exercises the Darling-based x86_64-darwin remote builder setup 4 + # end-to-end in a NixOS VM. It verifies: 5 + # 6 + # 1. The darling-builder systemd service starts and initialises the prefix 7 + # 2. sshd inside the Darling prefix is reachable from the host 8 + # 3. SSH key authentication works (auto-generated keys) 9 + # 4. Nix commands work over the SSH connection 10 + # 5. The host's Nix daemon recognises the Darling builder 11 + # 6. A trivial x86_64-darwin derivation can be offloaded to the builder 12 + # 13 + # Usage: 14 + # nix build .#checks.x86_64-linux.darling-builder -L 15 + # 16 + # See: plan/09-phase7-remote-builder.md 17 + { pkgs, darling, darlingBuilderModule, ... }: 18 + 19 + let 20 + nixos-lib = import (pkgs.path + "/nixos/lib") { }; 21 + 22 + in 23 + nixos-lib.runTest { 24 + name = "darling-builder"; 25 + 26 + hostPkgs = pkgs; 27 + 28 + nodes.machine = 29 + { config, pkgs, lib, ... }: 30 + { 31 + imports = [ 32 + darlingBuilderModule 33 + ]; 34 + 35 + # Give the VM generous resources — Darling prefix init + sshd + Nix 36 + # builds are resource-hungry. 37 + virtualisation = { 38 + memorySize = 4096; 39 + diskSize = 20480; 40 + cores = 4; 41 + writableStore = true; 42 + }; 43 + 44 + # Enable the Darling builder service. 45 + # We do NOT enable installNix here because the test would need to 46 + # download the Nix installer from the internet, which makes the test 47 + # slow and flaky. Instead we test the infrastructure layers 48 + # (prefix init, sshd, SSH connectivity, build machine registration) 49 + # and the trivial build is expected to work only if Nix is pre-installed 50 + # in the prefix (which the full nix-in-darling test covers). 51 + services.darling-builder = { 52 + enable = true; 53 + package = darling; 54 + port = 2222; 55 + maxJobs = 2; 56 + speedFactor = 1; 57 + shareStore = false; # Keep the test self-contained 58 + }; 59 + 60 + # Darling needs unprivileged user namespaces 61 + boot.kernel.sysctl = { 62 + "kernel.unprivileged_userns_clone" = 1; 63 + }; 64 + 65 + environment.etc."fuse.conf".text = '' 66 + user_allow_other 67 + ''; 68 + 69 + # Extra tools for debugging 70 + environment.systemPackages = [ 71 + pkgs.openssh 72 + pkgs.curl 73 + ]; 74 + }; 75 + 76 + testScript = '' 77 + import time 78 + 79 + machine.start() 80 + machine.wait_for_unit("default.target") 81 + 82 + # ── Stage 1: Darling builder service starts ──────────────────────── 83 + 84 + with machine.nested("Stage 1: Darling builder service starts"): 85 + # The service may take a while on first run (prefix initialisation) 86 + machine.wait_for_unit("darling-builder.service", timeout=300) 87 + machine.log("darling-builder.service is active") 88 + 89 + # Verify the prefix directory was created 90 + machine.succeed("test -d /var/lib/darling-builder") 91 + 92 + # ── Stage 2: SSH keys were generated ─────────────────────────────── 93 + 94 + with machine.nested("Stage 2: SSH keys were generated"): 95 + machine.succeed("test -f /etc/nix/darling-builder-key") 96 + machine.succeed("test -f /etc/nix/darling-builder-key.pub") 97 + 98 + # Key should be ed25519 99 + result = machine.succeed("head -1 /etc/nix/darling-builder-key.pub") 100 + assert "ssh-ed25519" in result, ( 101 + f"Expected ed25519 key, got: {result}" 102 + ) 103 + machine.log("SSH keypair exists and is ed25519") 104 + 105 + # ── Stage 3: sshd is listening inside the Darling prefix ─────────── 106 + 107 + with machine.nested("Stage 3: sshd is listening inside the Darling prefix"): 108 + # Give sshd a moment to start after prefix init 109 + # The service is Type=simple with sshd -D, so systemd considers it 110 + # active as soon as the process starts. But sshd needs a moment to 111 + # bind the port. 112 + time.sleep(5) 113 + 114 + # Check that port 2222 is open 115 + machine.wait_until_succeeds( 116 + "ss -tlnp | grep -q ':2222'", 117 + timeout=60, 118 + ) 119 + machine.log("sshd is listening on port 2222") 120 + 121 + # ── Stage 4: SSH connectivity works ──────────────────────────────── 122 + 123 + with machine.nested("Stage 4: SSH connectivity works"): 124 + # Test basic SSH connectivity with the auto-generated key 125 + result = machine.succeed( 126 + "ssh -o StrictHostKeyChecking=no " 127 + "-o UserKnownHostsFile=/dev/null " 128 + "-o ConnectTimeout=10 " 129 + "-i /etc/nix/darling-builder-key " 130 + "-p 2222 " 131 + "root@127.0.0.1 " 132 + "echo ssh-connection-ok", 133 + timeout=30, 134 + ) 135 + assert "ssh-connection-ok" in result, ( 136 + f"Expected 'ssh-connection-ok', got: {result}" 137 + ) 138 + machine.log("SSH connection to Darling sshd successful") 139 + 140 + # ── Stage 5: Darling identity via SSH ────────────────────────────── 141 + 142 + with machine.nested("Stage 5: Darling reports macOS identity via SSH"): 143 + result = machine.succeed( 144 + "ssh -o StrictHostKeyChecking=no " 145 + "-o UserKnownHostsFile=/dev/null " 146 + "-i /etc/nix/darling-builder-key " 147 + "-p 2222 " 148 + "root@127.0.0.1 " 149 + "uname -s", 150 + timeout=30, 151 + ) 152 + assert "Darwin" in result, ( 153 + f"Expected 'Darwin' from uname -s, got: {result}" 154 + ) 155 + machine.log("Darling reports Darwin via SSH ✓") 156 + 157 + result = machine.succeed( 158 + "ssh -o StrictHostKeyChecking=no " 159 + "-o UserKnownHostsFile=/dev/null " 160 + "-i /etc/nix/darling-builder-key " 161 + "-p 2222 " 162 + "root@127.0.0.1 " 163 + "uname -m", 164 + timeout=30, 165 + ) 166 + assert "x86_64" in result, ( 167 + f"Expected 'x86_64' from uname -m, got: {result}" 168 + ) 169 + 170 + # ── Stage 6: sshd config is correct ──────────────────────────────── 171 + 172 + with machine.nested("Stage 6: sshd configuration is correct"): 173 + # Verify key settings in sshd_config inside the prefix 174 + result = machine.succeed( 175 + "ssh -o StrictHostKeyChecking=no " 176 + "-o UserKnownHostsFile=/dev/null " 177 + "-i /etc/nix/darling-builder-key " 178 + "-p 2222 " 179 + "root@127.0.0.1 " 180 + "cat /etc/ssh/sshd_config", 181 + timeout=30, 182 + ) 183 + assert "Port 2222" in result, f"Port 2222 not in sshd_config" 184 + assert "PasswordAuthentication no" in result, ( 185 + "PasswordAuthentication should be disabled" 186 + ) 187 + assert "PubkeyAuthentication yes" in result, ( 188 + "PubkeyAuthentication should be enabled" 189 + ) 190 + machine.log("sshd_config is correctly configured") 191 + 192 + # ── Stage 7: Nix configuration inside the prefix ─────────────────── 193 + 194 + with machine.nested("Stage 7: Nix configuration inside the prefix"): 195 + result = machine.succeed( 196 + "ssh -o StrictHostKeyChecking=no " 197 + "-o UserKnownHostsFile=/dev/null " 198 + "-i /etc/nix/darling-builder-key " 199 + "-p 2222 " 200 + "root@127.0.0.1 " 201 + "cat /etc/nix/nix.conf", 202 + timeout=30, 203 + ) 204 + assert "sandbox = false" in result, ( 205 + f"Expected 'sandbox = false' in nix.conf, got: {result}" 206 + ) 207 + assert "experimental-features" in result, ( 208 + f"Expected experimental-features in nix.conf" 209 + ) 210 + machine.log("nix.conf is correctly configured inside the prefix") 211 + 212 + # ── Stage 8: Nix build machines registration ─────────────────────── 213 + 214 + with machine.nested("Stage 8: Nix build machines registration"): 215 + # Verify the Darling builder is registered in the Nix daemon config 216 + result = machine.succeed("cat /etc/nix/machines 2>/dev/null || echo 'no-machines-file'") 217 + # On NixOS, buildMachines are written to /etc/nix/machines 218 + # The format is: <store-uri> <systems> <ssh-key> <max-jobs> ... 219 + if "no-machines-file" not in result: 220 + assert "x86_64-darwin" in result, ( 221 + f"Expected x86_64-darwin in /etc/nix/machines, got: {result}" 222 + ) 223 + assert "2222" in result or "127.0.0.1" in result, ( 224 + f"Expected builder host/port in machines file, got: {result}" 225 + ) 226 + machine.log("Darling builder registered in /etc/nix/machines ✓") 227 + else: 228 + # buildMachines might be configured differently on this NixOS version 229 + machine.log("No /etc/nix/machines file — checking nix.conf for builders") 230 + result = machine.succeed("cat /etc/nix/nix.conf") 231 + machine.log(f"Host nix.conf:\n{result}") 232 + 233 + # ── Stage 9: Directory Services stubs via SSH ────────────────────── 234 + 235 + with machine.nested("Stage 9: Directory Services stubs work via SSH"): 236 + # These stubs are needed by the Nix multi-user installer. 237 + # Verify they're accessible from the SSH session. 238 + for tool in ["dseditgroup", "sysadminctl", "dscl"]: 239 + machine.succeed( 240 + f"ssh -o StrictHostKeyChecking=no " 241 + f"-o UserKnownHostsFile=/dev/null " 242 + f"-i /etc/nix/darling-builder-key " 243 + f"-p 2222 " 244 + f"root@127.0.0.1 " 245 + f"test -x /usr/sbin/{tool}", 246 + timeout=30, 247 + ) 248 + machine.log("All Directory Services stubs accessible via SSH ✓") 249 + 250 + # ── Stage 10: sandbox-exec stub via SSH ──────────────────────────── 251 + 252 + with machine.nested("Stage 10: sandbox-exec stub works via SSH"): 253 + result = machine.succeed( 254 + "ssh -o StrictHostKeyChecking=no " 255 + "-o UserKnownHostsFile=/dev/null " 256 + "-i /etc/nix/darling-builder-key " 257 + "-p 2222 " 258 + "root@127.0.0.1 " 259 + "/usr/bin/sandbox-exec -f /dev/null /bin/echo sandbox-via-ssh-ok", 260 + timeout=30, 261 + ) 262 + assert "sandbox-via-ssh-ok" in result, ( 263 + f"sandbox-exec via SSH failed: {result}" 264 + ) 265 + machine.log("sandbox-exec works via SSH ✓") 266 + 267 + # ── Stage 11: File operations via SSH ────────────────────────────── 268 + 269 + with machine.nested("Stage 11: File operations work via SSH"): 270 + # Write, read, and clean up a file — exercises basic FS ops 271 + machine.succeed( 272 + "ssh -o StrictHostKeyChecking=no " 273 + "-o UserKnownHostsFile=/dev/null " 274 + "-i /etc/nix/darling-builder-key " 275 + "-p 2222 " 276 + "root@127.0.0.1 " 277 + "bash -c '" 278 + "echo builder-test-content > /tmp/builder-test.txt && " 279 + "cat /tmp/builder-test.txt && " 280 + "rm /tmp/builder-test.txt" 281 + "'", 282 + timeout=30, 283 + ) 284 + machine.log("File operations via SSH work ✓") 285 + 286 + # ── Stage 12: Service restart resilience ─────────────────────────── 287 + 288 + with machine.nested("Stage 12: Service restart resilience"): 289 + # Restart the service and verify it comes back up 290 + machine.succeed("systemctl restart darling-builder.service") 291 + machine.wait_for_unit("darling-builder.service", timeout=300) 292 + 293 + # Give sshd time to rebind 294 + time.sleep(5) 295 + machine.wait_until_succeeds( 296 + "ss -tlnp | grep -q ':2222'", 297 + timeout=60, 298 + ) 299 + 300 + # Verify SSH still works after restart 301 + result = machine.succeed( 302 + "ssh -o StrictHostKeyChecking=no " 303 + "-o UserKnownHostsFile=/dev/null " 304 + "-o ConnectTimeout=10 " 305 + "-i /etc/nix/darling-builder-key " 306 + "-p 2222 " 307 + "root@127.0.0.1 " 308 + "echo post-restart-ok", 309 + timeout=30, 310 + ) 311 + assert "post-restart-ok" in result, ( 312 + f"SSH after restart failed: {result}" 313 + ) 314 + machine.log("Service restart resilience verified ✓") 315 + 316 + machine.log("All Darling builder tests passed! ✓") 317 + ''; 318 + }
+629
tests/nix/compatibility-matrix.sh
··· 1 + #!/usr/bin/env bash 2 + # tests/nix/compatibility-matrix.sh — Nix package compatibility test matrix 3 + # 4 + # Systematically tests building an expanding set of Nixpkgs packages inside 5 + # Darling and tracks pass/fail rates over time. Produces a JSON report and 6 + # a human-readable summary. 7 + # 8 + # Usage: 9 + # ./tests/nix/compatibility-matrix.sh # run all tiers 10 + # ./tests/nix/compatibility-matrix.sh --tier 1 # only tier 1 11 + # ./tests/nix/compatibility-matrix.sh --tier 1,2 # tiers 1 and 2 12 + # ./tests/nix/compatibility-matrix.sh --packages hello,jq # specific packages 13 + # ./tests/nix/compatibility-matrix.sh --json # JSON-only output 14 + # ./tests/nix/compatibility-matrix.sh --output report.json # write to file 15 + # ./tests/nix/compatibility-matrix.sh --timeout 600 # per-package timeout 16 + # ./tests/nix/compatibility-matrix.sh --compare prev.json # compare with previous 17 + # ./tests/nix/compatibility-matrix.sh --help 18 + # 19 + # Environment variables: 20 + # DARLING_NIX Path to the darling-nix wrapper (default: auto-detect) 21 + # DARLING Path to the darling binary (default: darling) 22 + # DPREFIX Darling prefix path (default: auto) 23 + # NIX_SYSTEM Target system (default: x86_64-darwin) 24 + # COMPAT_NIXPKGS Nixpkgs expression (default: <nixpkgs>) 25 + # COMPAT_TIMEOUT Per-package build timeout in seconds (default: 300) 26 + # COMPAT_SUBSTITUTERS Extra substituters (default: https://cache.nixos.org) 27 + # 28 + # The script is designed to be run periodically (e.g., in CI) and its JSON 29 + # output can be compared across runs to detect regressions and progress. 30 + # 31 + # See: plan/08-phase6-ci.md (Task 6.5) 32 + # plan/09-phase7-remote-builder.md (Task 7.6) 33 + 34 + set -euo pipefail 35 + 36 + # ── Colour output ──────────────────────────────────────────────────── 37 + 38 + if [[ -t 1 ]] && command -v tput &>/dev/null; then 39 + RED=$(tput setaf 1) 40 + GREEN=$(tput setaf 2) 41 + YELLOW=$(tput setaf 3) 42 + CYAN=$(tput setaf 6) 43 + BOLD=$(tput bold) 44 + RESET=$(tput sgr0) 45 + else 46 + RED="" GREEN="" YELLOW="" CYAN="" BOLD="" RESET="" 47 + fi 48 + 49 + # ── Package tiers ──────────────────────────────────────────────────── 50 + 51 + # Tier 1 — Must pass: simple packages, mostly fetched from binary cache 52 + TIER1_PACKAGES=( 53 + hello 54 + which 55 + yes 56 + ) 57 + 58 + # Tier 2 — Should pass: simple C programs, moderate complexity 59 + TIER2_PACKAGES=( 60 + tree 61 + jq 62 + gnugrep 63 + gnused 64 + gawk 65 + coreutils 66 + bash 67 + ) 68 + 69 + # Tier 3 — Stretch: complex builds, many dependencies 70 + TIER3_PACKAGES=( 71 + curl 72 + git 73 + openssl 74 + pkg-config 75 + cmake 76 + python3 77 + nodejs 78 + ) 79 + 80 + # Tier 4 — Aspirational: very complex builds (expected to fail initially) 81 + TIER4_PACKAGES=( 82 + go 83 + rustc 84 + llvm 85 + ghc 86 + ) 87 + 88 + # ── Configuration defaults ─────────────────────────────────────────── 89 + 90 + DARLING_NIX="${DARLING_NIX:-}" 91 + DARLING="${DARLING:-darling}" 92 + NIX_SYSTEM="${NIX_SYSTEM:-x86_64-darwin}" 93 + COMPAT_NIXPKGS="${COMPAT_NIXPKGS:-<nixpkgs>}" 94 + COMPAT_TIMEOUT="${COMPAT_TIMEOUT:-300}" 95 + COMPAT_SUBSTITUTERS="${COMPAT_SUBSTITUTERS:-https://cache.nixos.org}" 96 + 97 + # CLI state 98 + SELECTED_TIERS="" 99 + SELECTED_PACKAGES="" 100 + JSON_ONLY=0 101 + OUTPUT_FILE="" 102 + COMPARE_FILE="" 103 + VERBOSE=0 104 + DRY_RUN=0 105 + 106 + # ── Helpers ────────────────────────────────────────────────────────── 107 + 108 + log() { 109 + if [[ "$JSON_ONLY" -eq 0 ]]; then 110 + echo "$*" >&2 111 + fi 112 + } 113 + 114 + debug() { 115 + if [[ "$VERBOSE" -ge 1 && "$JSON_ONLY" -eq 0 ]]; then 116 + echo "${CYAN}[debug]${RESET} $*" >&2 117 + fi 118 + } 119 + 120 + die() { 121 + echo "${RED}ERROR:${RESET} $*" >&2 122 + exit 1 123 + } 124 + 125 + # Auto-detect the darling-nix wrapper script 126 + find_darling_nix() { 127 + if [[ -n "$DARLING_NIX" ]]; then 128 + echo "$DARLING_NIX" 129 + return 130 + fi 131 + 132 + # Look relative to this script 133 + local script_dir 134 + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 135 + local repo_root 136 + repo_root="$(cd "$script_dir/../.." && pwd)" 137 + 138 + if [[ -x "$repo_root/scripts/darling-nix" ]]; then 139 + echo "$repo_root/scripts/darling-nix" 140 + return 141 + fi 142 + 143 + # Fallback: use darling directly 144 + echo "" 145 + } 146 + 147 + # Run a Nix command inside Darling 148 + nix_in_darling() { 149 + local darling_nix 150 + darling_nix="$(find_darling_nix)" 151 + 152 + if [[ -n "$darling_nix" ]]; then 153 + "$darling_nix" "$@" 154 + else 155 + local prefix_args=() 156 + if [[ -n "${DPREFIX:-}" ]]; then 157 + prefix_args=(--prefix "$DPREFIX") 158 + fi 159 + "$DARLING" "${prefix_args[@]}" shell bash -lc ' 160 + for p in \ 161 + /Users/root/.nix-profile/etc/profile.d/nix.sh \ 162 + /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh \ 163 + /nix/var/nix/profiles/default/etc/profile.d/nix.sh; do 164 + if [ -e "$p" ]; then . "$p"; break; fi 165 + done 166 + exec "$@" 167 + ' -- "$@" 168 + fi 169 + } 170 + 171 + # Get packages for a specific tier 172 + get_tier_packages() { 173 + local tier="$1" 174 + case "$tier" in 175 + 1) echo "${TIER1_PACKAGES[@]}" ;; 176 + 2) echo "${TIER2_PACKAGES[@]}" ;; 177 + 3) echo "${TIER3_PACKAGES[@]}" ;; 178 + 4) echo "${TIER4_PACKAGES[@]}" ;; 179 + *) die "Unknown tier: $tier" ;; 180 + esac 181 + } 182 + 183 + # Get the tier for a given package 184 + get_package_tier() { 185 + local pkg="$1" 186 + local p 187 + for p in "${TIER1_PACKAGES[@]}"; do [[ "$p" == "$pkg" ]] && echo 1 && return; done 188 + for p in "${TIER2_PACKAGES[@]}"; do [[ "$p" == "$pkg" ]] && echo 2 && return; done 189 + for p in "${TIER3_PACKAGES[@]}"; do [[ "$p" == "$pkg" ]] && echo 3 && return; done 190 + for p in "${TIER4_PACKAGES[@]}"; do [[ "$p" == "$pkg" ]] && echo 4 && return; done 191 + echo 0 # Unknown tier 192 + } 193 + 194 + # JSON-escape a string 195 + json_escape() { 196 + local s="$1" 197 + s="${s//\\/\\\\}" 198 + s="${s//\"/\\\"}" 199 + s="${s//$'\n'/\\n}" 200 + s="${s//$'\r'/\\r}" 201 + s="${s//$'\t'/\\t}" 202 + echo "$s" 203 + } 204 + 205 + # ── Build test logic ───────────────────────────────────────────────── 206 + 207 + # Test building a single package. 208 + # Outputs a JSON object on stdout. 209 + test_package() { 210 + local pkg="$1" 211 + local tier 212 + tier=$(get_package_tier "$pkg") 213 + 214 + local status="unknown" 215 + local duration=0 216 + local output_path="" 217 + local error_msg="" 218 + local from_cache="unknown" 219 + local log_file 220 + log_file=$(mktemp "/tmp/compat-${pkg}-XXXXXX.log") 221 + 222 + log " ${BOLD}Testing:${RESET} ${CYAN}${pkg}${RESET} (tier $tier) ..." 223 + 224 + if [[ "$DRY_RUN" -eq 1 ]]; then 225 + status="skipped" 226 + duration=0 227 + error_msg="dry run" 228 + else 229 + local start_time 230 + start_time=$(date +%s) 231 + 232 + # Attempt the build with a timeout 233 + if timeout "$COMPAT_TIMEOUT" \ 234 + nix_in_darling nix-build "$COMPAT_NIXPKGS" \ 235 + -A "$pkg" \ 236 + --system "$NIX_SYSTEM" \ 237 + --no-out-link \ 238 + --option substituters "$COMPAT_SUBSTITUTERS" \ 239 + >"$log_file" 2>&1; then 240 + status="pass" 241 + output_path=$(tail -1 "$log_file" | grep -o '/nix/store/[^ ]*' | head -1) || true 242 + # Check if it was a cache hit (substituted) or built locally 243 + if grep -q "copying path.*from.*cache" "$log_file" 2>/dev/null; then 244 + from_cache="yes" 245 + elif grep -q "building.*\.drv" "$log_file" 2>/dev/null; then 246 + from_cache="no" 247 + fi 248 + else 249 + local rc=$? 250 + status="fail" 251 + if [[ "$rc" -eq 124 ]]; then 252 + error_msg="timeout after ${COMPAT_TIMEOUT}s" 253 + else 254 + # Extract the first meaningful error line 255 + error_msg=$(grep -E '(error:|FATAL|signal [0-9]+|Unimplemented syscall|killed|cannot)' "$log_file" | head -3 | tr '\n' ' ') || true 256 + if [[ -z "$error_msg" ]]; then 257 + error_msg="exit code $rc (see log)" 258 + fi 259 + fi 260 + fi 261 + 262 + local end_time 263 + end_time=$(date +%s) 264 + duration=$((end_time - start_time)) 265 + fi 266 + 267 + # Log result with colour 268 + case "$status" in 269 + pass) 270 + local cache_note="" 271 + [[ "$from_cache" == "yes" ]] && cache_note=" (from cache)" 272 + log " ${GREEN}✓ PASS${RESET} (${duration}s)${cache_note}" 273 + ;; 274 + fail) 275 + log " ${RED}✗ FAIL${RESET} (${duration}s): $error_msg" 276 + if [[ "$VERBOSE" -ge 1 ]]; then 277 + log " Log: $log_file" 278 + fi 279 + ;; 280 + skipped) 281 + log " ${YELLOW}⊘ SKIP${RESET}" 282 + ;; 283 + esac 284 + 285 + # Clean up log file on success (keep on failure for debugging) 286 + if [[ "$status" == "pass" || "$status" == "skipped" ]]; then 287 + rm -f "$log_file" 288 + log_file="" 289 + fi 290 + 291 + # Output JSON object 292 + cat << ENDJSON 293 + { 294 + "package": "$(json_escape "$pkg")", 295 + "tier": $tier, 296 + "status": "$(json_escape "$status")", 297 + "duration": $duration, 298 + "from_cache": "$(json_escape "$from_cache")", 299 + "output_path": "$(json_escape "${output_path:-}")", 300 + "error": "$(json_escape "${error_msg:-}")", 301 + "log_file": "$(json_escape "${log_file:-}")" 302 + } 303 + ENDJSON 304 + } 305 + 306 + # ── Comparison logic ───────────────────────────────────────────────── 307 + 308 + # Compare current results with a previous run's JSON report 309 + compare_results() { 310 + local current_file="$1" 311 + local previous_file="$2" 312 + 313 + if [[ ! -f "$previous_file" ]]; then 314 + log "${YELLOW}WARNING:${RESET} Previous report not found: $previous_file" 315 + return 316 + fi 317 + 318 + log "" 319 + log "${BOLD}═══ Comparison with previous run ═══${RESET}" 320 + 321 + # Extract package statuses from both files using grep/awk 322 + # (avoiding jq dependency) 323 + local prev_results curr_results 324 + prev_results=$(grep -oP '"package":\s*"[^"]*"|"status":\s*"[^"]*"' "$previous_file" | paste - - | sed 's/"package": "//;s/".*"status": "/\t/;s/"//g') 325 + curr_results=$(grep -oP '"package":\s*"[^"]*"|"status":\s*"[^"]*"' "$current_file" | paste - - | sed 's/"package": "//;s/".*"status": "/\t/;s/"//g') 326 + 327 + local regressions=0 328 + local improvements=0 329 + 330 + while IFS=$'\t' read -r pkg curr_status; do 331 + local prev_status 332 + prev_status=$(echo "$prev_results" | awk -F'\t' -v p="$pkg" '$1 == p { print $2 }') || true 333 + 334 + if [[ -z "$prev_status" ]]; then 335 + log " ${CYAN}NEW${RESET} $pkg: $curr_status" 336 + elif [[ "$prev_status" == "pass" && "$curr_status" == "fail" ]]; then 337 + log " ${RED}REGRESSION${RESET} $pkg: pass → fail" 338 + regressions=$((regressions + 1)) 339 + elif [[ "$prev_status" == "fail" && "$curr_status" == "pass" ]]; then 340 + log " ${GREEN}FIXED${RESET} $pkg: fail → pass" 341 + improvements=$((improvements + 1)) 342 + fi 343 + done <<< "$curr_results" 344 + 345 + log "" 346 + if [[ "$regressions" -gt 0 ]]; then 347 + log " ${RED}${BOLD}$regressions regression(s) detected!${RESET}" 348 + fi 349 + if [[ "$improvements" -gt 0 ]]; then 350 + log " ${GREEN}${BOLD}$improvements improvement(s) detected!${RESET}" 351 + fi 352 + if [[ "$regressions" -eq 0 && "$improvements" -eq 0 ]]; then 353 + log " No changes detected." 354 + fi 355 + } 356 + 357 + # ── Usage ──────────────────────────────────────────────────────────── 358 + 359 + usage() { 360 + cat >&2 << 'EOF' 361 + Usage: compatibility-matrix.sh [OPTIONS] 362 + 363 + Test building Nixpkgs packages inside Darling and report results. 364 + 365 + Options: 366 + --tier TIERS Comma-separated tier numbers to test (default: all) 367 + Tiers: 1=must-pass, 2=should-pass, 3=stretch, 4=aspirational 368 + --packages PKGS Comma-separated package names to test (overrides --tier) 369 + --timeout SECS Per-package build timeout (default: 300) 370 + --output FILE Write JSON report to FILE (default: stdout + stderr) 371 + --json Suppress human-readable output; only emit JSON to stdout 372 + --compare FILE Compare results with a previous JSON report 373 + --verbose, -v Show debug output and preserve failure logs 374 + --dry-run List packages that would be tested without building 375 + --list List all packages and their tiers, then exit 376 + --help, -h Show this help 377 + 378 + Environment: 379 + DARLING_NIX Path to darling-nix wrapper (auto-detected) 380 + DARLING Path to darling binary (default: darling) 381 + DPREFIX Darling prefix path (default: auto) 382 + NIX_SYSTEM Target system (default: x86_64-darwin) 383 + COMPAT_NIXPKGS Nixpkgs expression (default: <nixpkgs>) 384 + COMPAT_TIMEOUT Per-package timeout (default: 300) 385 + COMPAT_SUBSTITUTERS Binary cache URL (default: https://cache.nixos.org) 386 + 387 + Examples: 388 + # Test tier 1 packages (should be quick — mostly binary substitution) 389 + ./tests/nix/compatibility-matrix.sh --tier 1 390 + 391 + # Test specific packages with verbose output 392 + ./tests/nix/compatibility-matrix.sh --packages hello,jq -v 393 + 394 + # Full run with JSON report and comparison 395 + ./tests/nix/compatibility-matrix.sh --output today.json --compare yesterday.json 396 + 397 + # CI mode: JSON only, fail on regressions 398 + ./tests/nix/compatibility-matrix.sh --json --tier 1,2 399 + 400 + See: plan/08-phase6-ci.md (Task 6.5) 401 + EOF 402 + } 403 + 404 + # ── CLI parsing ────────────────────────────────────────────────────── 405 + 406 + parse_args() { 407 + while [[ $# -gt 0 ]]; do 408 + case "$1" in 409 + --tier) 410 + SELECTED_TIERS="$2" 411 + shift 2 412 + ;; 413 + --packages) 414 + SELECTED_PACKAGES="$2" 415 + shift 2 416 + ;; 417 + --timeout) 418 + COMPAT_TIMEOUT="$2" 419 + shift 2 420 + ;; 421 + --output) 422 + OUTPUT_FILE="$2" 423 + shift 2 424 + ;; 425 + --json) 426 + JSON_ONLY=1 427 + shift 428 + ;; 429 + --compare) 430 + COMPARE_FILE="$2" 431 + shift 2 432 + ;; 433 + --verbose | -v) 434 + VERBOSE=1 435 + shift 436 + ;; 437 + --dry-run) 438 + DRY_RUN=1 439 + shift 440 + ;; 441 + --list) 442 + echo "Tier 1 (must pass): ${TIER1_PACKAGES[*]}" 443 + echo "Tier 2 (should pass): ${TIER2_PACKAGES[*]}" 444 + echo "Tier 3 (stretch): ${TIER3_PACKAGES[*]}" 445 + echo "Tier 4 (aspirational): ${TIER4_PACKAGES[*]}" 446 + exit 0 447 + ;; 448 + --help | -h) 449 + usage 450 + exit 0 451 + ;; 452 + *) 453 + die "Unknown argument: $1 (try --help)" 454 + ;; 455 + esac 456 + done 457 + } 458 + 459 + # ── Main ───────────────────────────────────────────────────────────── 460 + 461 + main() { 462 + parse_args "$@" 463 + 464 + # Build the list of packages to test 465 + local packages=() 466 + 467 + if [[ -n "$SELECTED_PACKAGES" ]]; then 468 + IFS=',' read -ra packages <<< "$SELECTED_PACKAGES" 469 + elif [[ -n "$SELECTED_TIERS" ]]; then 470 + IFS=',' read -ra tiers <<< "$SELECTED_TIERS" 471 + for tier in "${tiers[@]}"; do 472 + local tier_pkgs 473 + tier_pkgs=$(get_tier_packages "$tier") 474 + # shellcheck disable=SC2206 475 + packages+=($tier_pkgs) 476 + done 477 + else 478 + # All tiers 479 + packages+=("${TIER1_PACKAGES[@]}") 480 + packages+=("${TIER2_PACKAGES[@]}") 481 + packages+=("${TIER3_PACKAGES[@]}") 482 + packages+=("${TIER4_PACKAGES[@]}") 483 + fi 484 + 485 + if [[ ${#packages[@]} -eq 0 ]]; then 486 + die "No packages selected" 487 + fi 488 + 489 + # Quick sanity check (unless dry run) 490 + if [[ "$DRY_RUN" -eq 0 ]]; then 491 + debug "Verifying Darling is functional ..." 492 + if ! "$DARLING" shell true &>/dev/null; then 493 + die "Cannot run 'darling shell true' — is Darling installed and the prefix initialised?" 494 + fi 495 + fi 496 + 497 + local timestamp 498 + timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") 499 + local hostname_str 500 + hostname_str=$(hostname 2>/dev/null || echo "unknown") 501 + 502 + log "" 503 + log "${BOLD}═══ Nix Compatibility Test Matrix ═══${RESET}" 504 + log " Date: $timestamp" 505 + log " System: $NIX_SYSTEM" 506 + log " Packages: ${#packages[@]}" 507 + log " Timeout: ${COMPAT_TIMEOUT}s per package" 508 + log "" 509 + 510 + # Collect JSON results 511 + local results_json="" 512 + local total=0 513 + local passed=0 514 + local failed=0 515 + local skipped=0 516 + 517 + for pkg in "${packages[@]}"; do 518 + local result 519 + result=$(test_package "$pkg") 520 + total=$((total + 1)) 521 + 522 + # Extract status for counters 523 + local status 524 + status=$(echo "$result" | grep -o '"status": *"[^"]*"' | head -1 | cut -d'"' -f4) 525 + case "$status" in 526 + pass) passed=$((passed + 1)) ;; 527 + fail) failed=$((failed + 1)) ;; 528 + skipped) skipped=$((skipped + 1)) ;; 529 + esac 530 + 531 + # Accumulate JSON 532 + if [[ -n "$results_json" ]]; then 533 + results_json="${results_json}, 534 + ${result}" 535 + else 536 + results_json="$result" 537 + fi 538 + done 539 + 540 + # Assemble the full JSON report 541 + local full_json 542 + full_json=$(cat << ENDJSON 543 + { 544 + "metadata": { 545 + "timestamp": "$timestamp", 546 + "hostname": "$(json_escape "$hostname_str")", 547 + "system": "$(json_escape "$NIX_SYSTEM")", 548 + "nixpkgs": "$(json_escape "$COMPAT_NIXPKGS")", 549 + "timeout_per_package": $COMPAT_TIMEOUT, 550 + "darling": "$(json_escape "$DARLING")" 551 + }, 552 + "summary": { 553 + "total": $total, 554 + "passed": $passed, 555 + "failed": $failed, 556 + "skipped": $skipped, 557 + "pass_rate": "$(awk "BEGIN { if ($total - $skipped > 0) printf \"%.1f\", $passed / ($total - $skipped) * 100; else print \"0.0\" }")%" 558 + }, 559 + "results": [ 560 + $results_json 561 + ] 562 + } 563 + ENDJSON 564 + ) 565 + 566 + # Output the report 567 + if [[ -n "$OUTPUT_FILE" ]]; then 568 + echo "$full_json" > "$OUTPUT_FILE" 569 + log "" 570 + log "JSON report written to: $OUTPUT_FILE" 571 + fi 572 + 573 + if [[ "$JSON_ONLY" -eq 1 ]]; then 574 + echo "$full_json" 575 + fi 576 + 577 + # Summary 578 + log "" 579 + log "${BOLD}═══ Summary ═══${RESET}" 580 + log " Total: $total" 581 + log " ${GREEN}Passed: $passed${RESET}" 582 + log " ${RED}Failed: $failed${RESET}" 583 + if [[ "$skipped" -gt 0 ]]; then 584 + log " ${YELLOW}Skipped: $skipped${RESET}" 585 + fi 586 + 587 + local tested=$((total - skipped)) 588 + if [[ "$tested" -gt 0 ]]; then 589 + local rate 590 + rate=$(awk "BEGIN { printf \"%.1f\", $passed / $tested * 100 }") 591 + log " Pass rate: ${BOLD}${rate}%${RESET} ($passed/$tested)" 592 + fi 593 + 594 + # Comparison 595 + local report_file="${OUTPUT_FILE:-}" 596 + if [[ -n "$COMPARE_FILE" ]]; then 597 + if [[ -z "$report_file" ]]; then 598 + # Write to a temp file for comparison 599 + report_file=$(mktemp /tmp/compat-current-XXXXXX.json) 600 + echo "$full_json" > "$report_file" 601 + fi 602 + compare_results "$report_file" "$COMPARE_FILE" 603 + if [[ -z "$OUTPUT_FILE" ]]; then 604 + rm -f "$report_file" 605 + fi 606 + fi 607 + 608 + log "" 609 + 610 + # Exit with failure if any tier-1 package failed (for CI use) 611 + if [[ "$failed" -gt 0 ]]; then 612 + local tier1_failures=0 613 + for pkg in "${TIER1_PACKAGES[@]}"; do 614 + if echo "$full_json" | grep -q "\"package\": \"$pkg\"" && \ 615 + echo "$full_json" | grep -A5 "\"package\": \"$pkg\"" | grep -q '"status": "fail"'; then 616 + tier1_failures=$((tier1_failures + 1)) 617 + fi 618 + done 619 + if [[ "$tier1_failures" -gt 0 ]]; then 620 + log "${RED}${BOLD}$tier1_failures tier-1 package(s) failed — this is a critical regression!${RESET}" 621 + exit 2 622 + fi 623 + exit 1 624 + fi 625 + 626 + exit 0 627 + } 628 + 629 + main "$@"