this repo has no description
1
fork

Configure Feed

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

Implement Phase 2 (sandbox), Phase 3 scaffolding, and CI

Phase 2 — Sandbox & Build Isolation:
- Fix sandbox API stubs: sandbox_init, sandbox_init_with_parameters,
sandbox_init_with_extensions, and sandbox_wakeup_daemon now set
*errorbuf = NULL on success instead of strdup("Not implemented"),
and guard against NULL errorbuf pointers.
- Create sandbox-exec stub (src/sandbox-exec/): small C program that
parses and ignores all sandbox flags (-f, -p, -n, -D) then exec's
the remaining command. Installs to libexec/darling/usr/bin/sandbox-exec.
This unblocks Nix builder invocations which wrap everything in
sandbox-exec.

Phase 3 — Nix Installation Support:
- Extend diskutil with info and list verb stubs so the Nix installer's
filesystem-type check (diskutil info /) succeeds.
- Create scripts/install-nix-in-darling.sh: automated installer that
downloads, patches, and runs the Nix Darwin installer inside a
Darling prefix in single-user mode.
- Create scripts/darling-nix: host-side wrapper for running Nix
commands inside Darling without manual darling shell bash -lc
boilerplate.

Phase 6 — CI:
- Create .github/workflows/nix.yml with flake-check, build, devshell,
and smoke-test jobs. Includes Cachix integration and path filtering.

Testing & Docs:
- Create tests/sandbox/ with C-level API tests and shell-level
sandbox-exec integration tests.
- Create plan/syscall-triage.md tracking table for unimplemented
syscalls.
- Update PLAN.md with progress summary and What's Next section.
- Update plan/README.md with new files and key scripts table.
- Fix .gitignore to allow tests/sandbox/ while ignoring other test dirs.

+1791 -9
+207
.github/workflows/nix.yml
··· 1 + # Nix CI for darling-nix 2 + # 3 + # This workflow builds the Darling package with Nix, runs flake checks, 4 + # and executes integration tests. It uses Cachix to cache build artifacts 5 + # so that subsequent runs (and contributor builds) are fast. 6 + # 7 + # See: plan/08-phase6-ci.md (Task 6.3) 8 + 9 + name: Nix CI 10 + 11 + on: 12 + push: 13 + branches: [master, main] 14 + paths-ignore: 15 + - "**.md" 16 + - "plan/**" 17 + - "LICENSE" 18 + - ".github/ISSUE_TEMPLATE/**" 19 + - ".github/FUNDING.yml" 20 + pull_request: 21 + paths-ignore: 22 + - "**.md" 23 + - "plan/**" 24 + - "LICENSE" 25 + - ".github/ISSUE_TEMPLATE/**" 26 + - ".github/FUNDING.yml" 27 + 28 + concurrency: 29 + group: nix-${{ github.ref }} 30 + cancel-in-progress: true 31 + 32 + jobs: 33 + # ── Flake check ─────────────────────────────────────────────────────────── 34 + # Fast: evaluates the flake, checks formatting, runs lightweight checks. 35 + flake-check: 36 + runs-on: ubuntu-latest 37 + steps: 38 + - name: Checkout 39 + uses: actions/checkout@v4 40 + with: 41 + submodules: recursive 42 + 43 + - name: Install Nix 44 + uses: cachix/install-nix-action@v27 45 + with: 46 + extra_nix_config: | 47 + accept-flake-config = true 48 + experimental-features = nix-command flakes 49 + 50 + - name: Set up Cachix 51 + uses: cachix/cachix-action@v15 52 + with: 53 + name: darling-nix 54 + authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 55 + continue-on-error: true # Don't fail if Cachix isn't configured 56 + 57 + - name: nix flake check (eval only) 58 + run: nix flake check --no-build --all-systems 2>&1 59 + 60 + # ── Build packages ──────────────────────────────────────────────────────── 61 + # Builds the main Darling package and the SDK output. 62 + build: 63 + runs-on: ubuntu-latest 64 + needs: flake-check 65 + steps: 66 + - name: Checkout 67 + uses: actions/checkout@v4 68 + with: 69 + submodules: recursive 70 + 71 + - name: Free up disk space 72 + run: | 73 + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc 74 + df -h / 75 + 76 + - name: Install Nix 77 + uses: cachix/install-nix-action@v27 78 + with: 79 + extra_nix_config: | 80 + accept-flake-config = true 81 + experimental-features = nix-command flakes 82 + 83 + - name: Set up Cachix 84 + uses: cachix/cachix-action@v15 85 + with: 86 + name: darling-nix 87 + authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 88 + continue-on-error: true 89 + 90 + - name: Build Darling 91 + run: nix build .#darling -L --no-link --print-out-paths 92 + 93 + - name: Build Darling SDK 94 + run: nix build .#darling-sdk -L --no-link --print-out-paths 95 + 96 + # ── DevShell evaluation ─────────────────────────────────────────────────── 97 + # Ensures the devShell evaluates without error so contributors can always 98 + # `nix develop`. This is cheap (eval-only, no build). 99 + devshell: 100 + runs-on: ubuntu-latest 101 + needs: flake-check 102 + steps: 103 + - name: Checkout 104 + uses: actions/checkout@v4 105 + with: 106 + submodules: recursive 107 + 108 + - name: Install Nix 109 + uses: cachix/install-nix-action@v27 110 + with: 111 + extra_nix_config: | 112 + accept-flake-config = true 113 + experimental-features = nix-command flakes 114 + 115 + - name: Set up Cachix 116 + uses: cachix/cachix-action@v15 117 + with: 118 + name: darling-nix 119 + authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 120 + continue-on-error: true 121 + 122 + - name: Evaluate devShell 123 + run: nix eval .#devShells.x86_64-linux.default.name 2>&1 124 + 125 + - name: Build devShell 126 + run: nix build .#devShells.x86_64-linux.default -L --no-link --print-out-paths 127 + 128 + # ── Smoke test ──────────────────────────────────────────────────────────── 129 + # If the build succeeds, run a quick smoke test to verify Darling starts 130 + # and the sandbox-exec stub is present. 131 + smoke-test: 132 + runs-on: ubuntu-latest 133 + needs: build 134 + steps: 135 + - name: Checkout 136 + uses: actions/checkout@v4 137 + with: 138 + submodules: recursive 139 + 140 + - name: Free up disk space 141 + run: | 142 + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc 143 + df -h / 144 + 145 + - name: Install Nix 146 + uses: cachix/install-nix-action@v27 147 + with: 148 + extra_nix_config: | 149 + accept-flake-config = true 150 + experimental-features = nix-command flakes 151 + 152 + - name: Set up Cachix 153 + uses: cachix/cachix-action@v15 154 + with: 155 + name: darling-nix 156 + authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 157 + continue-on-error: true 158 + 159 + - name: Build Darling 160 + id: build 161 + run: | 162 + out=$(nix build .#darling -L --no-link --print-out-paths) 163 + echo "darling=$out" >> "$GITHUB_OUTPUT" 164 + 165 + - name: Verify Darling binary exists 166 + run: | 167 + test -x "${{ steps.build.outputs.darling }}/bin/darling" 168 + echo "✓ darling binary found" 169 + 170 + - name: Check sandbox-exec stub is installed 171 + run: | 172 + test -f "${{ steps.build.outputs.darling }}/libexec/darling/usr/bin/sandbox-exec" 173 + echo "✓ sandbox-exec stub found" 174 + 175 + - name: Check diskutil supports info verb 176 + run: | 177 + grep -q 'info' "${{ steps.build.outputs.darling }}/libexec/darling/usr/sbin/diskutil" 178 + echo "✓ diskutil info verb found" 179 + 180 + # The following tests require actually running Darling, which needs 181 + # user namespaces and overlayfs — these may not be available in all 182 + # GitHub Actions runners. We attempt them but allow failure. 183 + - name: Test darling shell (may need user namespaces) 184 + run: | 185 + timeout 30 "${{ steps.build.outputs.darling }}/bin/darling" shell echo "Hello from Darling" || { 186 + echo "::warning::darling shell failed — runner may lack user namespace support" 187 + exit 0 188 + } 189 + continue-on-error: true 190 + 191 + - name: Test sandbox-exec stub inside Darling 192 + run: | 193 + timeout 30 "${{ steps.build.outputs.darling }}/bin/darling" shell \ 194 + /usr/bin/sandbox-exec -f /dev/null -D _GLOBAL_TMP_DIR=/tmp /bin/echo "sandbox-exec works" || { 195 + echo "::warning::sandbox-exec test failed — darling shell may not be functional on this runner" 196 + exit 0 197 + } 198 + continue-on-error: true 199 + 200 + - name: Test diskutil info inside Darling 201 + run: | 202 + timeout 30 "${{ steps.build.outputs.darling }}/bin/darling" shell \ 203 + /usr/sbin/diskutil info / || { 204 + echo "::warning::diskutil info test failed — darling shell may not be functional on this runner" 205 + exit 0 206 + } 207 + continue-on-error: true
+2 -1
.gitignore
··· 30 30 Module.symvers 31 31 32 32 # Directories where too much temp stuff may lay around 33 - tests 33 + tests/* 34 + !tests/sandbox/ 34 35 35 36 # The suggested build folder 36 37 build
+104 -1
PLAN.md
··· 8 8 The full plan has been split into focused documents to keep context manageable. 9 9 See the **[plan/](./plan/)** directory for all details. 10 10 11 + ## Progress Summary 12 + 13 + | Phase | Status | Key Files | 14 + |-------|--------|-----------| 15 + | Phase 0 — Packaging | ✅ Done | `flake.nix`, `nix/package.nix`, `nix/devShell.nix`, `nix/nixosModule.nix`, `.envrc` | 16 + | Phase 1 — Syscalls | 📋 Planned | [Triage table](./plan/syscall-triage.md) | 17 + | Phase 2 — Sandbox | 🚧 In progress | `src/sandbox/sandbox.c` (fixed), `src/sandbox-exec/` (new), `tests/sandbox/` (new) | 18 + | Phase 3 — Nix Install | 🚧 In progress | `scripts/install-nix-in-darling.sh` (new), `scripts/darling-nix` (new) | 19 + | Phase 4 — Building | 📋 Planned | — | 20 + | Phase 5 — Daemon | 📋 Planned | — | 21 + | Phase 6 — CI | 🚧 In progress | `.github/workflows/nix.yml` (new) | 22 + | Phase 7 — Remote Builder | 📋 Planned | — | 23 + | Phase 8 — Stretch | 📋 Planned | — | 24 + 25 + ### Recently Completed 26 + 27 + - **Phase 2.2**: Fixed `sandbox_init`, `sandbox_init_with_parameters`, 28 + `sandbox_init_with_extensions`, and `sandbox_wakeup_daemon` — they now set 29 + `*errorbuf = NULL` on success instead of `strdup("Not implemented")`, and 30 + guard against NULL `errorbuf` pointers. 31 + - **Phase 2.1**: Created `sandbox-exec` stub at `src/sandbox-exec/sandbox-exec.c` 32 + — a small C program that parses and ignores all sandbox flags (`-f`, `-p`, 33 + `-n`, `-D`) then `exec`s the remaining command. Wired into the CMake build 34 + via `src/sandbox-exec/CMakeLists.txt`; installs to 35 + `libexec/darling/usr/bin/sandbox-exec`. 36 + - **Blocker mitigation**: Extended `src/diskutil/diskutil` with `info` and 37 + `list` verb stubs so the Nix installer's filesystem-type check succeeds. 38 + - **Phase 3.1**: Created `scripts/install-nix-in-darling.sh` — automated 39 + installer that downloads, patches, and runs the Nix Darwin installer inside 40 + a Darling prefix in single-user mode. 41 + - **Phase 3.4**: Created `scripts/darling-nix` — host-side wrapper for running 42 + Nix commands inside Darling without manual `darling shell bash -lc` boilerplate. 43 + - **Phase 6.3**: Created `.github/workflows/nix.yml` — Nix CI workflow with 44 + flake check, package build, devShell evaluation, and smoke tests. 45 + - **Phase 1.7**: Created `plan/syscall-triage.md` — tracking table for 46 + unimplemented syscalls with categories, impact levels, and discovery log. 47 + - **Testing**: Created `tests/sandbox/test_sandbox_api.c` (C-level sandbox API 48 + tests) and `tests/sandbox/test_sandbox_exec.sh` (shell-level sandbox-exec 49 + integration tests). 50 + 11 51 ## Quick Navigation 12 52 13 53 | Document | Description | ··· 24 64 | [plan/08-phase6-ci.md](./plan/08-phase6-ci.md) | NixOS VM tests, regression suite, GitHub Actions | 25 65 | [plan/09-phase7-remote-builder.md](./plan/09-phase7-remote-builder.md) | Darling as a `nix.buildMachines` target | 26 66 | [plan/10-phase8-stretch.md](./plan/10-phase8-stretch.md) | `aarch64-darwin`, GUI testing, Hydra builder | 27 - | [plan/11-architecture.md](./plan/11-architecture.md) | System diagram, key technical decisions, glossary | 67 + | [plan/11-architecture.md](./plan/11-architecture.md) | System diagram, key technical decisions, glossary | 68 + | [plan/syscall-triage.md](./plan/syscall-triage.md) | Tracking table for unimplemented syscalls | 69 + 70 + ## New File Map 71 + 72 + Files created or modified as part of this plan: 73 + 74 + ```text 75 + darling-nix/ 76 + ├── .github/workflows/nix.yml # Nix CI workflow (Phase 6) 77 + ├── flake.nix # Flake with package, devShell, NixOS module (Phase 0) 78 + ├── nix/ 79 + │ ├── package.nix # Darling Nix derivation (Phase 0) 80 + │ ├── devShell.nix # Developer shell (Phase 0) 81 + │ └── nixosModule.nix # NixOS module (Phase 0) 82 + ├── scripts/ 83 + │ ├── install-nix-in-darling.sh # Automated Nix installer (Phase 3) 84 + │ └── darling-nix # Host-side Nix command wrapper (Phase 3) 85 + ├── src/ 86 + │ ├── sandbox/sandbox.c # Fixed sandbox API stubs (Phase 2) 87 + │ ├── sandbox-exec/ # NEW — sandbox-exec stub (Phase 2) 88 + │ │ ├── CMakeLists.txt 89 + │ │ └── sandbox-exec.c 90 + │ └── diskutil/diskutil # Extended with info/list verbs (Phase 3) 91 + ├── tests/ 92 + │ └── sandbox/ # NEW — sandbox regression tests 93 + │ ├── test_sandbox_api.c # C-level sandbox API tests 94 + │ └── test_sandbox_exec.sh # Shell-level sandbox-exec tests 95 + └── plan/ 96 + ├── README.md # Index + priority table 97 + ├── 00-background.md # Motivation & current state 98 + ├── 01-blockers.md # Blocking issues analysis 99 + ├── 02-phase0-packaging.md # Phase 0 details 100 + ├── 03-phase1-syscalls.md # Phase 1 details 101 + ├── 04-phase2-sandbox.md # Phase 2 details 102 + ├── 05-phase3-nix-install.md # Phase 3 details 103 + ├── 06-phase4-building.md # Phase 4 details 104 + ├── 07-phase5-daemon.md # Phase 5 details 105 + ├── 08-phase6-ci.md # Phase 6 details 106 + ├── 09-phase7-remote-builder.md # Phase 7 details 107 + ├── 10-phase8-stretch.md # Phase 8 details 108 + ├── 11-architecture.md # Architecture & decisions 109 + └── syscall-triage.md # NEW — syscall tracking table 110 + ``` 111 + 112 + ## What's Next 113 + 114 + The **critical path to MVP** (Nix running inside Darling) is: 115 + 116 + 1. **Phase 1 — Syscall fixes** (P0, not started): This is the biggest remaining 117 + blocker. The `setattrlist`/`renameatx_np`/`utimensat` implementations in 118 + darlingserver are required before Nix binaries can run without crashing. 119 + Start with task 1.3 (`renameatx_np` → `renameat2` mapping) as it's the 120 + quickest win, then 1.1 (`setattrlist`) for the biggest impact. 121 + 122 + 2. **Phase 2 — Verification**: The sandbox-exec stub and API fixes are 123 + implemented but need testing inside a real Darling build. Run the tests in 124 + `tests/sandbox/` to verify. 125 + 126 + 3. **Phase 3 — Nix installation**: Once Phase 1 syscalls are in place, run 127 + `scripts/install-nix-in-darling.sh` and iterate on any remaining issues. 128 + 129 + See [plan/README.md](./plan/README.md) for the full priority table and effort 130 + estimates.
+11 -1
plan/README.md
··· 21 21 | [Phase 7 — Remote Builder](./09-phase7-remote-builder.md) | Darling as a `nix.buildMachines` target | 22 22 | [Phase 8 — Stretch Goals](./10-phase8-stretch.md) | `aarch64-darwin`, GUI testing, Hydra builder | 23 23 | [Architecture](./11-architecture.md) | System diagram, key technical decisions | 24 + | [Syscall Triage](./syscall-triage.md) | Tracking table for unimplemented/buggy syscalls | 24 25 25 26 ## Priority & Effort Estimates 26 27 ··· 46 47 2. **Check upstream** [Darling issues](https://github.com/darlinghq/darling/issues) for existing work. 47 48 3. **Write a minimal reproducer** — a small C program or shell command that demonstrates the bug inside `darling shell`. 48 49 4. **Fix it** in the appropriate subsystem (`darlingserver` for syscalls, `src/external/libc` for wrappers, `src/sandbox` for sandbox, etc.). 49 - 5. **Add a test** to the regression suite (see [Phase 6](./08-phase6-ci.md)). 50 + 5. **Add a test** to the regression suite (see [Phase 6](./08-phase6-ci.md) and `tests/`). 50 51 6. **Submit a PR** to this repo, and consider upstreaming to `darlinghq/darling`. 52 + 53 + ### Key Scripts & Tools 54 + 55 + | File | Description | 56 + |---|---| 57 + | `scripts/install-nix-in-darling.sh` | Automated Nix installer for Darling prefixes | 58 + | `scripts/darling-nix` | Host-side wrapper to run Nix commands inside Darling | 59 + | `tests/sandbox/test_sandbox_api.c` | C-level regression tests for sandbox API stubs | 60 + | `tests/sandbox/test_sandbox_exec.sh` | Shell-level tests for the `sandbox-exec` stub binary | 51 61 52 62 ## References 53 63
+95
plan/syscall-triage.md
··· 1 + # Syscall Triage 2 + 3 + Tracking document for unimplemented or buggy macOS syscalls encountered while 4 + running Nix inside Darling. This table is populated by running Nix operations 5 + with syscall tracing enabled and collecting "Unimplemented syscall" messages. 6 + 7 + See: [Phase 1, Task 1.7](./03-phase1-syscalls.md#17--triage-unimplemented-syscalls) 8 + 9 + --- 10 + 11 + ## How to Collect Data 12 + 13 + ```bash 14 + # Method 1: Watch for "Unimplemented syscall" messages during a Nix operation 15 + darling shell /nix/store/.../bin/nix --version 2>&1 | grep -i "unimplemented" 16 + 17 + # Method 2: Use DARLING_XTRACE for detailed Darwin-side tracing 18 + DARLING_XTRACE=1 darling shell /nix/store/.../bin/nix-env --version 2>&1 | head -500 19 + 20 + # Method 3: Trace darlingserver from the Linux host 21 + strace -f -p $(pidof darlingserver) -e trace=all 2>&1 | grep -i "ENOSYS\|unimpl" 22 + 23 + # Method 4: Run the full Nix install + build sequence and capture all output 24 + ./scripts/install-nix-in-darling.sh 2>&1 | tee nix-install-trace.log 25 + grep -i "unimplemented\|STUB\|not.implemented\|ENOSYS" nix-install-trace.log 26 + ``` 27 + 28 + ## Syscall Number Reference 29 + 30 + macOS (XNU) BSD syscall numbers can be found in: 31 + - `src/external/xnu/bsd/kern/syscalls.master` (this repo, if submodule is checked out) 32 + - [Apple's open-source XNU syscall table](https://github.com/apple-oss-distributions/xnu/blob/main/bsd/kern/syscalls.master) 33 + 34 + --- 35 + 36 + ## Triage Table 37 + 38 + | Syscall # | Name | Caller | Operation | Impact | Category | Status | Notes | 39 + |-----------|------|--------|-----------|--------|----------|--------|-------| 40 + | 488 | `renameatx_np` | `mv` (coreutils) | `nix-build` (file moves) | **Crash** — `mv` aborts | Must fix | 🔧 Planned ([1.3](./03-phase1-syscalls.md#13--implement-renameatx_np-syscall-488)) | Map to Linux `renameat2` | 41 + | 462 | `clonefile` | Nix store optimiser | Store copy-on-write | Slow fallback | Should stub | 🔧 Planned ([1.5](./03-phase1-syscalls.md#15--implement-or-stub-clonefile--fclonefileat-syscall-462)) | Return `ENOTSUP`; Nix handles gracefully | 42 + | 463 | `fclonefileat` | Nix store optimiser | Store copy-on-write | Slow fallback | Should stub | 🔧 Planned ([1.5](./03-phase1-syscalls.md#15--implement-or-stub-clonefile--fclonefileat-syscall-462)) | Same as `clonefile` | 43 + | 220 | `getattrlist` | Various (stat-like) | File metadata reads | **Crash** or wrong results | Must fix | 🔧 Planned ([1.1](./03-phase1-syscalls.md#11--implement-setattrlist--fsetattrlist--getattrlist)) | Minimum: `ATTR_CMN_FLAGS` | 44 + | 221 | `setattrlist` | `lchflags` / `chflags` | `nix-env` profile install | **Blocker** — EINVAL | Must fix | 🔧 Planned ([1.1](./03-phase1-syscalls.md#11--implement-setattrlist--fsetattrlist--getattrlist)) | Core blocker B1 | 45 + | — | `fsetattrlist` | `lchflags` variant | `nix-env` profile install | **Blocker** | Must fix | 🔧 Planned ([1.1](./03-phase1-syscalls.md#11--implement-setattrlist--fsetattrlist--getattrlist)) | Same root cause as B1 | 46 + | 547 | `setattrlistat` | `touch` / timestamps | Build scripts | **Crash** — segfault | Must fix | 🔧 Planned ([1.4](./03-phase1-syscalls.md#14--audit-and-fix-utimensat--futimens)) | May share root cause with B1 | 47 + | — | `getentropy` | Crypto / hashing | Nix eval, signing | Potential crash | Should verify | 📋 Planned ([1.6](./03-phase1-syscalls.md#16--implement-getentropy--ccrandomgeneratebytes)) | May already work | 48 + | | | | | | | | | 49 + <!-- Add new entries above this line as they are discovered --> 50 + 51 + --- 52 + 53 + ## Category Definitions 54 + 55 + | Category | Description | Action Required | 56 + |----------|-------------|-----------------| 57 + | **Must fix** | Causes Nix to crash, abort, or produce incorrect results | Implement the syscall or a correct translation to Linux | 58 + | **Should stub** | Called but return value isn't critical for correctness | Return `0`, `ENOTSUP`, or a sensible default | 59 + | **Can ignore** | Informational / tracing call; does not affect execution | Suppress the warning message; no code change needed | 60 + 61 + ## Impact Levels 62 + 63 + | Impact | Description | 64 + |--------|-------------| 65 + | **Crash** | Process aborts with signal (SIGILL, SIGSEGV, SIGABRT) or "Unimplemented syscall" | 66 + | **Blocker** | Operation returns an error that Nix treats as fatal | 67 + | **Wrong results** | Syscall returns unexpected data leading to subtle bugs | 68 + | **Slow fallback** | Nix handles the error gracefully but uses a slower code path | 69 + | **Cosmetic** | Warning message printed but no functional impact | 70 + 71 + ## Status Key 72 + 73 + | Status | Meaning | 74 + |--------|---------| 75 + | 🔧 Planned | Fix designed, not yet implemented | 76 + | 🚧 In progress | Implementation underway | 77 + | ✅ Fixed | Implemented and verified | 78 + | ⏭️ Stubbed | Returns a harmless value; full implementation deferred | 79 + | ❌ Won't fix | Not needed for Nix; documented and closed | 80 + | 📋 Needs triage | Discovered but not yet categorised | 81 + 82 + --- 83 + 84 + ## Discovery Log 85 + 86 + Record each triage session here so we know what's been tested. 87 + 88 + | Date | Tester | Nix Version | Operation Tested | New Syscalls Found | 89 + |------|--------|-------------|------------------|--------------------| 90 + | | | | | | 91 + <!-- Add rows as triage sessions are performed --> 92 + 93 + --- 94 + 95 + *[← Phase 1 — Syscall Fixes](./03-phase1-syscalls.md) | [Phase 2 — Sandbox →](./04-phase2-sandbox.md)*
+158
scripts/darling-nix
··· 1 + #!/usr/bin/env bash 2 + # darling-nix — run Nix commands inside a Darling prefix from the Linux host 3 + # 4 + # This is a convenience wrapper that sources the Nix profile inside a 5 + # Darling shell and then exec's the given command. It saves users from 6 + # having to remember the long `darling shell bash -lc '...'` incantation 7 + # every time. 8 + # 9 + # Usage: 10 + # darling-nix <command> [arguments ...] 11 + # 12 + # Examples: 13 + # darling-nix nix --version 14 + # darling-nix nix-instantiate --eval -E '1 + 1' 15 + # darling-nix nix eval --expr 'builtins.currentSystem' 16 + # darling-nix nix-build --expr 'derivation { name = "test"; builder = "/bin/bash"; args = ["-c" "echo ok > $out"]; system = "x86_64-darwin"; }' 17 + # darling-nix nix-env -qa hello 18 + # darling-nix nix repl 19 + # 20 + # Options: 21 + # --prefix <path> Use a non-default Darling prefix 22 + # --help Show this help message 23 + # 24 + # Environment: 25 + # DPREFIX Override the default Darling prefix (~/.darling) 26 + # DARLING_NIX_VERBOSE Set to 1 for debug output 27 + # 28 + # See: plan/05-phase3-nix-install.md (Task 3.4) 29 + 30 + set -euo pipefail 31 + 32 + # ── Defaults ──────────────────────────────────────────────────────────────── 33 + 34 + DARLING_PREFIX="${DPREFIX:-}" 35 + VERBOSE="${DARLING_NIX_VERBOSE:-0}" 36 + 37 + # ── Helpers ───────────────────────────────────────────────────────────────── 38 + 39 + die() { 40 + echo "darling-nix: error: $*" >&2 41 + exit 1 42 + } 43 + 44 + debug() { 45 + if [ "$VERBOSE" = "1" ]; then 46 + echo "darling-nix: debug: $*" >&2 47 + fi 48 + } 49 + 50 + usage() { 51 + cat <<'EOF' 52 + Usage: darling-nix [--prefix <path>] <command> [arguments ...] 53 + 54 + Run Nix commands inside a Darling (macOS compatibility) prefix. 55 + 56 + Options: 57 + --prefix <path> Use a non-default Darling prefix (default: ~/.darling) 58 + --help Show this help message 59 + 60 + Environment: 61 + DPREFIX Override the default Darling prefix 62 + DARLING_NIX_VERBOSE Set to 1 for debug output 63 + 64 + Examples: 65 + darling-nix nix --version 66 + darling-nix nix-instantiate --eval -E '1 + 1' 67 + darling-nix nix eval --expr 'builtins.currentSystem' 68 + darling-nix nix-env -qa hello 69 + darling-nix nix repl 70 + EOF 71 + exit 0 72 + } 73 + 74 + # ── Argument Parsing ──────────────────────────────────────────────────────── 75 + 76 + while [ $# -gt 0 ]; do 77 + case "$1" in 78 + --prefix) 79 + [ $# -ge 2 ] || die "--prefix requires an argument" 80 + DARLING_PREFIX="$2" 81 + shift 2 82 + ;; 83 + --help|-h) 84 + usage 85 + ;; 86 + --) 87 + shift 88 + break 89 + ;; 90 + -*) 91 + # Could be a flag for the inner command (e.g. `darling-nix nix --version`) 92 + # so stop parsing our flags and treat everything as the command. 93 + break 94 + ;; 95 + *) 96 + break 97 + ;; 98 + esac 99 + done 100 + 101 + if [ $# -eq 0 ]; then 102 + die "no command specified (try --help)" 103 + fi 104 + 105 + # ── Preflight ─────────────────────────────────────────────────────────────── 106 + 107 + if ! command -v darling &>/dev/null; then 108 + die "darling is not installed or not in PATH" 109 + fi 110 + 111 + # Build the darling command with optional --prefix 112 + DARLING_CMD=(darling) 113 + if [ -n "$DARLING_PREFIX" ]; then 114 + DARLING_CMD+=(--prefix "$DARLING_PREFIX") 115 + debug "using prefix: $DARLING_PREFIX" 116 + fi 117 + 118 + # ── Build the inner command ───────────────────────────────────────────────── 119 + 120 + # We need to carefully quote the user's arguments so they survive the 121 + # bash -lc layer. We use printf %q to produce shell-escaped versions 122 + # of each argument. 123 + ESCAPED_ARGS="" 124 + for arg in "$@"; do 125 + ESCAPED_ARGS+=" $(printf '%q' "$arg")" 126 + done 127 + 128 + debug "inner command:${ESCAPED_ARGS}" 129 + debug "darling cmd: ${DARLING_CMD[*]}" 130 + 131 + # The inner script: 132 + # 1. Sources the Nix profile so nix/nix-env/etc. are on PATH. 133 + # 2. exec's the user's command (preserving exit code). 134 + INNER_SCRIPT=" 135 + # Source Nix profile 136 + if [ -e '/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' ]; then 137 + . '/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' 138 + elif [ -e \"\\\$HOME/.nix-profile/etc/profile.d/nix.sh\" ]; then 139 + . \"\\\$HOME/.nix-profile/etc/profile.d/nix.sh\" 140 + elif [ -e '/etc/profile.d/nix-darling.sh' ]; then 141 + . '/etc/profile.d/nix-darling.sh' 142 + fi 143 + 144 + # Verify nix is available 145 + if ! command -v nix &>/dev/null && ! command -v nix-env &>/dev/null; then 146 + echo 'darling-nix: Nix does not appear to be installed in this prefix.' >&2 147 + echo 'darling-nix: Run ./scripts/install-nix-in-darling.sh first.' >&2 148 + exit 127 149 + fi 150 + 151 + exec${ESCAPED_ARGS} 152 + " 153 + 154 + # ── Execute ───────────────────────────────────────────────────────────────── 155 + 156 + # Run the command inside Darling, forwarding the exit code. 157 + # We use `bash -lc` to get a login shell (which sources /etc/profile.d/*). 158 + exec "${DARLING_CMD[@]}" shell bash -lc "$INNER_SCRIPT"
+495
scripts/install-nix-in-darling.sh
··· 1 + #!/usr/bin/env bash 2 + # install-nix-in-darling.sh — Automated Nix installer for Darling prefixes 3 + # 4 + # This script installs the Nix package manager inside a Darling (macOS 5 + # compatibility layer) prefix, working around the various incompatibilities 6 + # between Darling's environment and the official Nix Darwin installer. 7 + # 8 + # Usage: 9 + # ./scripts/install-nix-in-darling.sh [OPTIONS] 10 + # 11 + # Options: 12 + # --prefix <path> Darling prefix path (default: ~/.darling) 13 + # --nix-version <ver> Nix version to install (default: latest stable) 14 + # --no-channel Skip channel setup after installation 15 + # --no-verify Skip post-install verification 16 + # --offline Don't fetch from binary cache (set substitute = false) 17 + # --help Show this help message 18 + # 19 + # Prerequisites: 20 + # - Darling must be installed and `darling shell echo ok` must work 21 + # - The sandbox-exec stub must be in place (Phase 2) 22 + # - Syscall fixes from Phase 1 should be applied for full functionality 23 + # 24 + # See: plan/05-phase3-nix-install.md (Task 3.1) 25 + 26 + set -euo pipefail 27 + 28 + # ── Defaults ──────────────────────────────────────────────────────────────── 29 + 30 + DARLING_PREFIX="${DPREFIX:-$HOME/.darling}" 31 + NIX_VERSION="" 32 + SETUP_CHANNEL=1 33 + RUN_VERIFY=1 34 + OFFLINE=0 35 + NIX_INSTALL_URL_BASE="https://releases.nixos.org/nix" 36 + 37 + # Colors (disabled if not a terminal) 38 + if [ -t 1 ]; then 39 + RED='\033[0;31m' 40 + GREEN='\033[0;32m' 41 + YELLOW='\033[0;33m' 42 + BLUE='\033[0;34m' 43 + BOLD='\033[1m' 44 + RESET='\033[0m' 45 + else 46 + RED='' GREEN='' YELLOW='' BLUE='' BOLD='' RESET='' 47 + fi 48 + 49 + # ── Helpers ───────────────────────────────────────────────────────────────── 50 + 51 + log() { echo -e "${GREEN}[darling-nix]${RESET} $*"; } 52 + warn() { echo -e "${YELLOW}[darling-nix] WARNING:${RESET} $*" >&2; } 53 + err() { echo -e "${RED}[darling-nix] ERROR:${RESET} $*" >&2; } 54 + fatal() { err "$@"; exit 1; } 55 + 56 + # Run a command inside the Darling prefix 57 + dsh() { 58 + darling shell "$@" 59 + } 60 + 61 + # Run a command inside the Darling prefix with bash -lc (for Nix profile) 62 + dsh_bash() { 63 + darling shell bash -lc "$*" 64 + } 65 + 66 + usage() { 67 + sed -n '/^# Usage:/,/^# See:/p' "$0" | sed 's/^# \?//' 68 + exit 0 69 + } 70 + 71 + # ── Argument Parsing ──────────────────────────────────────────────────────── 72 + 73 + while [ $# -gt 0 ]; do 74 + case "$1" in 75 + --prefix) 76 + DARLING_PREFIX="$2" 77 + shift 2 78 + ;; 79 + --nix-version) 80 + NIX_VERSION="$2" 81 + shift 2 82 + ;; 83 + --no-channel) 84 + SETUP_CHANNEL=0 85 + shift 86 + ;; 87 + --no-verify) 88 + RUN_VERIFY=0 89 + shift 90 + ;; 91 + --offline) 92 + OFFLINE=1 93 + shift 94 + ;; 95 + --help|-h) 96 + usage 97 + ;; 98 + *) 99 + fatal "Unknown option: $1 (try --help)" 100 + ;; 101 + esac 102 + done 103 + 104 + # ── Step 0: Prerequisite Checks ──────────────────────────────────────────── 105 + 106 + log "${BOLD}Step 0: Checking prerequisites...${RESET}" 107 + 108 + # Check that darling is installed 109 + if ! command -v darling &>/dev/null; then 110 + fatal "darling is not installed or not in PATH.\n" \ 111 + " Install it with: nix build .#darling" 112 + fi 113 + 114 + # Check that the prefix exists / can be initialised 115 + if [ ! -d "$DARLING_PREFIX" ]; then 116 + log "Prefix $DARLING_PREFIX does not exist; initialising..." 117 + darling shell true 2>/dev/null || true 118 + sleep 2 119 + fi 120 + 121 + if [ ! -d "$DARLING_PREFIX" ]; then 122 + fatal "Failed to initialise Darling prefix at $DARLING_PREFIX" 123 + fi 124 + 125 + # Verify darling shell works 126 + if ! dsh echo ok &>/dev/null; then 127 + fatal "darling shell is not functional.\n" \ 128 + " Try: darling shell echo ok" 129 + fi 130 + 131 + log " Darling prefix: $DARLING_PREFIX" 132 + log " darling shell: $(dsh sw_vers -productVersion 2>/dev/null || echo 'unknown')" 133 + 134 + # Check for sandbox-exec stub (Phase 2) 135 + if ! dsh test -x /usr/bin/sandbox-exec 2>/dev/null; then 136 + warn "/usr/bin/sandbox-exec not found in Darling prefix." 137 + warn "Nix builds will fail without it. Install Phase 2 fixes first." 138 + warn "Continuing anyway (Nix installation may still work)..." 139 + fi 140 + 141 + # Check if Nix is already installed 142 + if dsh test -e /nix/store 2>/dev/null; then 143 + warn "/nix/store already exists in the prefix." 144 + read -rp " Reinstall? [y/N] " answer 145 + if [[ ! "$answer" =~ ^[Yy] ]]; then 146 + log "Aborted." 147 + exit 0 148 + fi 149 + fi 150 + 151 + # ── Step 1: Pre-configure Nix ────────────────────────────────────────────── 152 + 153 + log "${BOLD}Step 1: Pre-configuring Nix...${RESET}" 154 + 155 + dsh mkdir -p /etc/nix 156 + 157 + NIX_CONF="# Nix configuration for Darling 158 + # Generated by install-nix-in-darling.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ) 159 + 160 + # Single-user mode: no build users group needed 161 + build-users-group = 162 + 163 + # Disable the macOS sandbox — Darling provides Linux-level isolation 164 + # via namespaces and darlingserver instead. 165 + sandbox = false 166 + 167 + # Accept flake commands and the nix3 CLI 168 + experimental-features = nix-command flakes 169 + " 170 + 171 + if [ "$OFFLINE" -eq 1 ]; then 172 + NIX_CONF+=" 173 + # Offline mode — no binary substitution 174 + substitute = false 175 + " 176 + else 177 + NIX_CONF+=" 178 + # Binary cache 179 + substituters = https://cache.nixos.org 180 + trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= 181 + " 182 + fi 183 + 184 + # Write via the prefix filesystem directly (more reliable than piping into dsh) 185 + echo "$NIX_CONF" > "$DARLING_PREFIX/etc/nix/nix.conf" 186 + 187 + log " Wrote /etc/nix/nix.conf" 188 + 189 + # ── Step 2: Download the Nix installer ───────────────────────────────────── 190 + 191 + log "${BOLD}Step 2: Downloading Nix installer...${RESET}" 192 + 193 + # Determine the Nix version to install 194 + if [ -z "$NIX_VERSION" ]; then 195 + # Fetch the latest stable version from the releases page 196 + log " Detecting latest Nix version..." 197 + NIX_VERSION=$(curl -fsSL https://releases.nixos.org/nix/latest/install 2>/dev/null \ 198 + | grep -oP 'nix-\K[0-9]+\.[0-9]+\.[0-9]+' \ 199 + | head -1 || true) 200 + 201 + if [ -z "$NIX_VERSION" ]; then 202 + # Fallback: a known-good version 203 + NIX_VERSION="2.24.12" 204 + warn "Could not detect latest version, falling back to $NIX_VERSION" 205 + fi 206 + fi 207 + 208 + log " Nix version: $NIX_VERSION" 209 + 210 + INSTALLER_NAME="nix-${NIX_VERSION}-x86_64-darwin" 211 + INSTALLER_URL="${NIX_INSTALL_URL_BASE}/nix-${NIX_VERSION}/${INSTALLER_NAME}.tar.xz" 212 + INSTALLER_HOST_TMP=$(mktemp -d "${TMPDIR:-/tmp}/nix-darling-install.XXXXXX") 213 + 214 + cleanup() { 215 + rm -rf "$INSTALLER_HOST_TMP" 216 + } 217 + trap cleanup EXIT 218 + 219 + log " Downloading $INSTALLER_URL ..." 220 + if ! curl -fSL -o "$INSTALLER_HOST_TMP/installer.tar.xz" "$INSTALLER_URL"; then 221 + fatal "Failed to download Nix installer.\n" \ 222 + " URL: $INSTALLER_URL\n" \ 223 + " Check that the version $NIX_VERSION exists for x86_64-darwin." 224 + fi 225 + 226 + log " Extracting..." 227 + tar -xf "$INSTALLER_HOST_TMP/installer.tar.xz" -C "$INSTALLER_HOST_TMP" 228 + 229 + # Find the extracted directory (nix-<version>-x86_64-darwin/) 230 + INSTALLER_DIR=$(find "$INSTALLER_HOST_TMP" -maxdepth 1 -type d -name 'nix-*' | head -1) 231 + if [ -z "$INSTALLER_DIR" ] || [ ! -f "$INSTALLER_DIR/install" ]; then 232 + fatal "Installer extraction failed — cannot find install script." 233 + fi 234 + 235 + log " Installer extracted to: $INSTALLER_DIR" 236 + 237 + # ── Step 3: Patch the installer ──────────────────────────────────────────── 238 + 239 + log "${BOLD}Step 3: Patching installer for Darling compatibility...${RESET}" 240 + 241 + INSTALL_SCRIPT="$INSTALLER_DIR/install" 242 + 243 + # Count patches applied for reporting 244 + PATCHES=0 245 + 246 + # Patch 1: Remove the multi-user enforcement on Darwin. 247 + # The installer detects `uname -s` == Darwin and forces multi-user mode. 248 + # We need single-user (--no-daemon) mode since Directory Services aren't 249 + # available yet. 250 + if grep -q 'case "$(uname -s)"' "$INSTALL_SCRIPT" 2>/dev/null || \ 251 + grep -q 'Darwin' "$INSTALL_SCRIPT" 2>/dev/null; then 252 + 253 + # The typical pattern is: 254 + # if [ "$(uname -s)" = "Darwin" ]; then 255 + # INSTALL_MODE=daemon 256 + # We replace forced daemon mode with no-daemon. 257 + sed -i.bak \ 258 + -e 's/INSTALL_MODE=daemon/INSTALL_MODE=no-daemon/g' \ 259 + "$INSTALL_SCRIPT" 260 + PATCHES=$((PATCHES + 1)) 261 + fi 262 + 263 + # Patch 2: Remove diskutil dependency. 264 + # The installer calls `diskutil info /` or `/usr/sbin/diskutil info -plist /` 265 + # to detect APFS. Our diskutil stub now handles this, but if the installer 266 + # does something more complex, this sed catches it. 267 + if grep -q 'diskutil' "$INSTALL_SCRIPT" 2>/dev/null; then 268 + # Don't actually remove diskutil calls — our stub handles them. 269 + # But if the installer does `type diskutil` or `command -v diskutil` 270 + # as a gate, make sure it passes. 271 + log " diskutil references found — our stub should handle them" 272 + fi 273 + 274 + # Patch 3: Remove xmllint dependency checks. 275 + # If the installer hard-requires xmllint, stub the check. 276 + if grep -q 'xmllint' "$INSTALL_SCRIPT" 2>/dev/null; then 277 + sed -i.bak \ 278 + -e '/xmllint/d' \ 279 + "$INSTALL_SCRIPT" 280 + PATCHES=$((PATCHES + 1)) 281 + fi 282 + 283 + # Patch 4: Remove or relax the root-user check. 284 + # On Darwin the installer may refuse to run as root in certain modes. 285 + # Inside Darling we always run as root. 286 + if grep -q 'running as root is not supported' "$INSTALL_SCRIPT" 2>/dev/null || \ 287 + grep -q 'do not run this script as root' "$INSTALL_SCRIPT" 2>/dev/null; then 288 + sed -i.bak \ 289 + -e '/running as root is not supported/d' \ 290 + -e '/do not run this script as root/d' \ 291 + "$INSTALL_SCRIPT" 292 + PATCHES=$((PATCHES + 1)) 293 + fi 294 + 295 + # Patch 5: Remove dseditgroup / sysadminctl calls (multi-user user creation). 296 + # These should already be unreachable with INSTALL_MODE=no-daemon, but 297 + # belt-and-suspenders. 298 + if grep -q 'dseditgroup\|sysadminctl' "$INSTALL_SCRIPT" 2>/dev/null; then 299 + log " Directory Services references found — should be skipped in no-daemon mode" 300 + fi 301 + 302 + # Patch 6: Ensure --no-daemon is respected even if the script tries to 303 + # override it for Darwin. 304 + if grep -q 'NIX_INSTALLER_NO_MODIFY_PROFILE' "$INSTALL_SCRIPT" 2>/dev/null; then 305 + log " Standard Nix installer detected" 306 + fi 307 + 308 + log " Applied $PATCHES patches to installer script" 309 + 310 + # ── Step 4: Copy installer into the Darling prefix and run it ────────────── 311 + 312 + log "${BOLD}Step 4: Installing Nix inside Darling...${RESET}" 313 + 314 + # Copy the installer into the prefix's /tmp 315 + DARLING_TMP="$DARLING_PREFIX/private/tmp" 316 + mkdir -p "$DARLING_TMP" 317 + DARLING_INSTALLER_DIR="$DARLING_TMP/nix-installer" 318 + rm -rf "$DARLING_INSTALLER_DIR" 319 + cp -a "$INSTALLER_DIR" "$DARLING_INSTALLER_DIR" 320 + 321 + # Create /nix in the prefix if it doesn't exist 322 + dsh mkdir -p /nix 323 + 324 + # Ensure the install script is executable 325 + chmod +x "$DARLING_INSTALLER_DIR/install" 326 + 327 + # Run the patched installer inside Darling 328 + # We use --no-daemon explicitly and set NIX_INSTALLER_NO_MODIFY_PROFILE=1 329 + # to prevent it from modifying shell profiles (we'll handle that ourselves). 330 + log " Running Nix installer (this may take a few minutes)..." 331 + if ! dsh env \ 332 + NIX_INSTALLER_NO_MODIFY_PROFILE=0 \ 333 + bash -ex /tmp/nix-installer/install --no-daemon 2>&1 | \ 334 + tee "$INSTALLER_HOST_TMP/install.log"; then 335 + 336 + err "Nix installer failed. Log saved to: $INSTALLER_HOST_TMP/install.log" 337 + err "" 338 + err "Common causes:" 339 + err " - Missing syscall fixes (Phase 1): check for 'Unimplemented syscall' in the log" 340 + err " - Missing sandbox-exec (Phase 2): check for 'Bad file descriptor'" 341 + err " - Installer script incompatibility: the patches may need updating" 342 + err "" 343 + err "Debug tips:" 344 + err " strace -f -p \$(pidof darlingserver) -e trace=openat,stat 2>&1 | head -200" 345 + err " DARLING_XTRACE=1 darling shell ..." 346 + 347 + # Don't clean up the temp dir on failure so the user can inspect 348 + trap - EXIT 349 + fatal "Installation failed. Temp files preserved at: $INSTALLER_HOST_TMP" 350 + fi 351 + 352 + log " ✓ Nix installer completed successfully" 353 + 354 + # ── Step 5: Post-install configuration ───────────────────────────────────── 355 + 356 + log "${BOLD}Step 5: Post-install configuration...${RESET}" 357 + 358 + # Ensure the Nix profile script is sourceable 359 + NIX_PROFILE_SCRIPT="/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh" 360 + NIX_PROFILE_SCRIPT_ALT="/root/.nix-profile/etc/profile.d/nix.sh" 361 + 362 + if dsh test -f "$NIX_PROFILE_SCRIPT" 2>/dev/null; then 363 + log " Nix profile script: $NIX_PROFILE_SCRIPT" 364 + elif dsh test -f "$NIX_PROFILE_SCRIPT_ALT" 2>/dev/null; then 365 + NIX_PROFILE_SCRIPT="$NIX_PROFILE_SCRIPT_ALT" 366 + log " Nix profile script: $NIX_PROFILE_SCRIPT" 367 + else 368 + warn "Could not find Nix profile script — Nix may not be on PATH in new shells" 369 + fi 370 + 371 + # Ensure /etc/nix/nix.conf wasn't overwritten by the installer 372 + if [ -f "$DARLING_PREFIX/etc/nix/nix.conf" ]; then 373 + if ! grep -q 'build-users-group =' "$DARLING_PREFIX/etc/nix/nix.conf"; then 374 + log " Restoring nix.conf (installer overwrote it)..." 375 + echo "$NIX_CONF" > "$DARLING_PREFIX/etc/nix/nix.conf" 376 + fi 377 + fi 378 + 379 + # Write a helper profile that sources Nix on login 380 + DARLING_PROFILE_D="$DARLING_PREFIX/etc/profile.d" 381 + mkdir -p "$DARLING_PROFILE_D" 382 + cat > "$DARLING_PROFILE_D/nix-darling.sh" <<'PROFILE' 383 + # Source Nix profile if available 384 + if [ -e '/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' ]; then 385 + . '/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' 386 + elif [ -e "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then 387 + . "$HOME/.nix-profile/etc/profile.d/nix.sh" 388 + fi 389 + PROFILE 390 + log " Wrote /etc/profile.d/nix-darling.sh" 391 + 392 + # ── Step 6: Channel / Registry Setup ─────────────────────────────────────── 393 + 394 + if [ "$SETUP_CHANNEL" -eq 1 ] && [ "$OFFLINE" -eq 0 ]; then 395 + log "${BOLD}Step 6: Setting up Nix channels...${RESET}" 396 + 397 + if dsh_bash "nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs" 2>/dev/null; then 398 + log " Added nixpkgs-unstable channel" 399 + 400 + log " Updating channels (this downloads ~20 MB)..." 401 + if dsh_bash "nix-channel --update" 2>/dev/null; then 402 + log " ✓ Channels updated" 403 + else 404 + warn "Channel update failed — this exercises curl/TLS, which may have issues." 405 + warn "You can retry later with: darling shell nix-channel --update" 406 + fi 407 + else 408 + warn "Failed to add channel — Nix may not be on PATH yet." 409 + warn "Try: darling shell bash -lc 'nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs'" 410 + fi 411 + else 412 + log "${BOLD}Step 6: Skipping channel setup${RESET} (--no-channel or --offline)" 413 + fi 414 + 415 + # ── Step 7: Verification ─────────────────────────────────────────────────── 416 + 417 + if [ "$RUN_VERIFY" -eq 1 ]; then 418 + log "${BOLD}Step 7: Verifying installation...${RESET}" 419 + 420 + PASS=0 421 + FAIL=0 422 + 423 + check() { 424 + local desc="$1" 425 + shift 426 + if "$@" &>/dev/null; then 427 + log " ${GREEN}✓${RESET} $desc" 428 + PASS=$((PASS + 1)) 429 + else 430 + err " ✗ $desc" 431 + FAIL=$((FAIL + 1)) 432 + fi 433 + } 434 + 435 + check "nix --version" \ 436 + dsh_bash "nix --version" 437 + 438 + check "nix-env --version" \ 439 + dsh_bash "nix-env --version" 440 + 441 + check "nix-store --verify (may take a moment)" \ 442 + dsh_bash "nix-store --verify --no-build 2>/dev/null || nix-store --verify" 443 + 444 + check "nix-instantiate --eval -E '1 + 1'" \ 445 + dsh_bash "nix-instantiate --eval -E '1 + 1'" 446 + 447 + check "nix eval --expr '1 + 1'" \ 448 + dsh_bash "nix eval --expr '1 + 1'" 449 + 450 + check "builtins.currentSystem == x86_64-darwin" \ 451 + dsh_bash "test \"\$(nix eval --expr 'builtins.currentSystem' --raw)\" = 'x86_64-darwin'" 452 + 453 + if [ "$OFFLINE" -eq 0 ]; then 454 + check "curl to cache.nixos.org" \ 455 + dsh bash -c "curl -sfI https://cache.nixos.org/nix-cache-info >/dev/null 2>&1" 456 + fi 457 + 458 + echo "" 459 + log "Verification: ${GREEN}$PASS passed${RESET}, ${RED}$FAIL failed${RESET}" 460 + 461 + if [ "$FAIL" -gt 0 ]; then 462 + warn "Some checks failed. Nix may be partially functional." 463 + warn "See plan/05-phase3-nix-install.md for debugging tips." 464 + fi 465 + else 466 + log "${BOLD}Step 7: Skipping verification${RESET} (--no-verify)" 467 + fi 468 + 469 + # ── Step 8: Clean up ─────────────────────────────────────────────────────── 470 + 471 + log "${BOLD}Step 8: Cleaning up...${RESET}" 472 + 473 + # Remove the installer from the prefix 474 + rm -rf "$DARLING_INSTALLER_DIR" 475 + log " Removed installer files from prefix" 476 + 477 + # ── Done ──────────────────────────────────────────────────────────────────── 478 + 479 + echo "" 480 + log "${BOLD}${GREEN}═══════════════════════════════════════════════════════${RESET}" 481 + log "${BOLD}${GREEN} Nix installation inside Darling is complete!${RESET}" 482 + log "${BOLD}${GREEN}═══════════════════════════════════════════════════════${RESET}" 483 + echo "" 484 + log "Nix version: $(dsh_bash 'nix --version' 2>/dev/null || echo 'unknown')" 485 + log "Prefix: $DARLING_PREFIX" 486 + log "Store: $DARLING_PREFIX/nix/store" 487 + echo "" 488 + log "Quick start:" 489 + log " ${BLUE}darling shell bash -lc 'nix --version'${RESET}" 490 + log " ${BLUE}darling shell bash -lc 'nix eval --expr 1+1'${RESET}" 491 + log " ${BLUE}darling shell bash -lc 'nix-build --expr \"derivation { name = \\\"test\\\"; builder = \\\"/bin/bash\\\"; args = [\\\"-c\\\" \\\"echo ok > \\\\\\\$out\\\"]; system = \\\"x86_64-darwin\\\"; }\"'${RESET}" 492 + echo "" 493 + log "Or use the wrapper script:" 494 + log " ${BLUE}./scripts/darling-nix nix --version${RESET}" 495 + log " ${BLUE}./scripts/darling-nix nix eval --expr '1 + 1'${RESET}"
+1
src/CMakeLists.txt
··· 292 292 add_subdirectory(external/lzfse) 293 293 add_subdirectory(clt) 294 294 add_subdirectory(diskutil) 295 + add_subdirectory(sandbox-exec) 295 296 add_subdirectory(ditto) 296 297 297 298 # these aren't used by anything we build (they're just included because they're also present in macOS)
+77 -1
src/diskutil/diskutil
··· 9 9 10 10 # subcommands 11 11 12 + info() { 13 + # The Nix installer calls `diskutil info /` to determine the root 14 + # filesystem type (APFS vs HFS+). Inside Darling the root is an 15 + # overlayfs backed by the Linux host, but we report APFS so that 16 + # the installer takes the modern (no-volume) code path. 17 + 18 + PLIST=0 19 + if [ "$#" -gt 0 ] && [ "$1" = "-plist" ]; then 20 + PLIST=1 21 + shift 22 + fi 23 + 24 + DISK_PATH="${1:-/}" 25 + 26 + if [ "$PLIST" -eq 1 ]; then 27 + cat <<- EOF 28 + <?xml version="1.0" encoding="UTF-8"?> 29 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 30 + <plist version="1.0"> 31 + <dict> 32 + <key>MountPoint</key> 33 + <string>$DISK_PATH</string> 34 + <key>FilesystemType</key> 35 + <string>apfs</string> 36 + <key>FilesystemName</key> 37 + <string>APFS</string> 38 + <key>DeviceNode</key> 39 + <string>disk1s1</string> 40 + <key>VolumeName</key> 41 + <string>Macintosh HD</string> 42 + <key>Writable</key> 43 + <true/> 44 + </dict> 45 + </plist> 46 + EOF 47 + else 48 + cat <<- EOF 49 + Device Identifier: disk1s1 50 + Device Node: /dev/disk1s1 51 + Whole: No 52 + Part of Whole: disk1 53 + Volume Name: Macintosh HD 54 + Mounted: Yes 55 + Mount Point: $DISK_PATH 56 + File System Personality: APFS 57 + Type (Bundle): apfs 58 + Writable: Yes 59 + EOF 60 + fi 61 + } 62 + 63 + list() { 64 + # Minimal stub — reports a single synthetic disk so that callers 65 + # that enumerate disks don't fail outright. 66 + cat <<- 'EOF' 67 + /dev/disk0 (internal): 68 + #: TYPE NAME SIZE IDENTIFIER 69 + 0: GUID_partition_scheme *64.0 GB disk0 70 + 1: EFI EFI 209.7 MB disk0s1 71 + 2: Apple_APFS Container disk1 63.8 GB disk0s2 72 + 73 + /dev/disk1 (synthesized): 74 + #: TYPE NAME SIZE IDENTIFIER 75 + 0: APFS Container Scheme - +63.8 GB disk1 76 + 1: APFS Volume Macintosh HD 15.2 GB disk1s1 77 + EOF 78 + } 79 + 12 80 eject() { 13 81 if [ "$#" -lt 1 ]; then 14 82 errcho "Usage: diskutil eject MountPoint|DiskIdentifier|DeviceNode" ··· 36 104 Disk Utility Tool 37 105 Utility to manage local disks and volumes 38 106 Most commands require an administrator or root user 39 - 107 + 40 108 WARNING: Most destructive operations are not prompted 41 109 110 + info (Get information about a disk or volume) 111 + list (List disks) 42 112 eject (Eject a disk) 43 113 EOF 44 114 exit 1 ··· 48 118 shift; 49 119 50 120 case "$VERB" in 121 + info) 122 + info "$@" 123 + ;; 124 + list) 125 + list "$@" 126 + ;; 51 127 eject) 52 128 eject "$@" 53 129 ;;
+9
src/sandbox-exec/CMakeLists.txt
··· 1 + project(sandbox-exec) 2 + 3 + add_darling_executable(sandbox-exec sandbox-exec.c) 4 + 5 + # sandbox-exec is a simple tool that runs inside the Darling prefix. 6 + # It only needs libSystem for execvp, fprintf, strcmp, etc. 7 + target_link_libraries(sandbox-exec system) 8 + 9 + install(TARGETS sandbox-exec DESTINATION libexec/darling/usr/bin)
+78
src/sandbox-exec/sandbox-exec.c
··· 1 + /* 2 + * sandbox-exec stub for Darling 3 + * 4 + * This is a minimal replacement for macOS's /usr/bin/sandbox-exec. 5 + * It parses (and ignores) all sandbox-related arguments, then exec's 6 + * the remaining command. 7 + * 8 + * Darling already provides Linux-level isolation via namespaces and 9 + * darlingserver, so skipping the macOS sandbox is safe for build 10 + * isolation purposes. 11 + * 12 + * Usage (matches real sandbox-exec): 13 + * sandbox-exec [-f <profile-path>] [-p <profile-string>] 14 + * [-n <profile-name>] [-D <key>=<value>]... 15 + * <command> [args...] 16 + * 17 + * See: plan/04-phase2-sandbox.md (Task 2.1) 18 + */ 19 + 20 + #include <unistd.h> 21 + #include <string.h> 22 + #include <stdio.h> 23 + #include <errno.h> 24 + 25 + static void usage(const char *progname) 26 + { 27 + fprintf(stderr, 28 + "Usage: %s [-f profile_path] [-p profile_string] [-n profile_name]\n" 29 + " %*s [-D key=value]... command [arguments ...]\n", 30 + progname, (int)strlen(progname) + 7, ""); 31 + } 32 + 33 + int main(int argc, char *argv[]) 34 + { 35 + int i = 1; 36 + 37 + while (i < argc) { 38 + /* 39 + * -f <profile> : sandbox profile file path 40 + * -p <string> : inline sandbox profile string 41 + * -n <name> : predefined profile name 42 + * -D <key=value> : parameter definition for the profile 43 + * 44 + * All of these take one argument after the flag (with a space), 45 + * except -D which may also appear as -Dkey=value (no space). 46 + */ 47 + if ((strcmp(argv[i], "-f") == 0 || 48 + strcmp(argv[i], "-p") == 0 || 49 + strcmp(argv[i], "-n") == 0 || 50 + strcmp(argv[i], "-D") == 0) && i + 1 < argc) { 51 + i += 2; /* skip flag + its argument */ 52 + } else if (strncmp(argv[i], "-D", 2) == 0 && argv[i][2] != '\0') { 53 + i += 1; /* skip -Dkey=value (no space) */ 54 + } else if (strcmp(argv[i], "--") == 0) { 55 + i += 1; /* skip -- separator */ 56 + break; 57 + } else if (argv[i][0] == '-') { 58 + fprintf(stderr, "sandbox-exec: unknown option '%s'\n", argv[i]); 59 + usage(argv[0]); 60 + return 1; 61 + } else { 62 + break; /* first non-option argument is the command */ 63 + } 64 + } 65 + 66 + if (i >= argc) { 67 + fprintf(stderr, "sandbox-exec: no command specified\n"); 68 + usage(argv[0]); 69 + return 1; 70 + } 71 + 72 + execvp(argv[i], &argv[i]); 73 + 74 + /* If we get here, exec failed */ 75 + fprintf(stderr, "sandbox-exec: exec '%s': %s\n", 76 + argv[i], strerror(errno)); 77 + return 127; 78 + }
+9 -5
src/sandbox/sandbox.c
··· 10 10 11 11 int sandbox_init(const char *profile, uint64_t flags, char **errorbuf) 12 12 { 13 - *errorbuf = strdup("Not implemented"); 13 + if (errorbuf) 14 + *errorbuf = NULL; 14 15 return 0; 15 16 } 16 17 ··· 51 52 52 53 int sandbox_init_with_parameters(const char *profile, uint64_t flags, const char *const parameters[], char **errorbuf) 53 54 { 54 - *errorbuf = strdup("Not implemented"); 55 + if (errorbuf) 56 + *errorbuf = NULL; 55 57 return 0; 56 58 } 57 59 58 60 int sandbox_init_with_extensions(const char *profile, uint64_t flags, const char *const extensions[], char **errorbuf) 59 61 { 60 - *errorbuf = strdup("Not implemented"); 62 + if (errorbuf) 63 + *errorbuf = NULL; 61 64 return 0; 62 65 } 63 66 ··· 129 132 130 133 int sandbox_wakeup_daemon(char **errorbuf) 131 134 { 132 - *errorbuf = strdup("Not implemented"); 133 - return -1; 135 + if (errorbuf) 136 + *errorbuf = NULL; 137 + return 0; 134 138 } 135 139 136 140 const char *_amkrtemp(const char *path)
+290
tests/sandbox/test_sandbox_api.c
··· 1 + /* 2 + * test_sandbox_api.c — Regression tests for Darling's sandbox API stubs 3 + * 4 + * Build (inside darling shell): 5 + * cc -o test_sandbox_api test_sandbox_api.c -lsystem_sandbox 6 + * 7 + * Or with the system sandbox library: 8 + * cc -o test_sandbox_api test_sandbox_api.c -lsandbox 9 + * 10 + * Run: 11 + * ./test_sandbox_api 12 + * 13 + * Expected: all tests pass (exit 0). 14 + * 15 + * See: plan/04-phase2-sandbox.md (Tasks 2.2, 2.3) 16 + */ 17 + 18 + #include <stdio.h> 19 + #include <stdlib.h> 20 + #include <string.h> 21 + #include <unistd.h> 22 + #include <signal.h> 23 + #include <sys/types.h> 24 + 25 + /* sandbox API declarations — these match the real macOS headers */ 26 + extern int sandbox_init(const char *profile, uint64_t flags, char **errorbuf); 27 + extern int sandbox_init_with_parameters(const char *profile, uint64_t flags, 28 + const char *const parameters[], 29 + char **errorbuf); 30 + extern int sandbox_init_with_extensions(const char *profile, uint64_t flags, 31 + const char *const extensions[], 32 + char **errorbuf); 33 + extern void sandbox_free_error(char *errorbuf); 34 + extern int sandbox_check(pid_t pid, const char *operation, int type, ...); 35 + extern int sandbox_wakeup_daemon(char **errorbuf); 36 + 37 + static int tests_run = 0; 38 + static int tests_passed = 0; 39 + static int tests_failed = 0; 40 + 41 + #define TEST(name) \ 42 + do { \ 43 + tests_run++; \ 44 + printf(" TEST %2d: %-50s ", tests_run, (name)); \ 45 + } while (0) 46 + 47 + #define PASS() \ 48 + do { \ 49 + tests_passed++; \ 50 + printf("\033[32mPASS\033[0m\n"); \ 51 + } while (0) 52 + 53 + #define FAIL(reason) \ 54 + do { \ 55 + tests_failed++; \ 56 + printf("\033[31mFAIL\033[0m — %s\n", (reason)); \ 57 + } while (0) 58 + 59 + /* ── Tests ────────────────────────────────────────────────────────────────── */ 60 + 61 + static void test_sandbox_init_returns_zero(void) 62 + { 63 + TEST("sandbox_init returns 0 (success)"); 64 + char *err = (char *)0xDEADBEEF; /* sentinel */ 65 + int ret = sandbox_init("no_network", 0, &err); 66 + if (ret != 0) { 67 + FAIL("returned non-zero"); 68 + } else { 69 + PASS(); 70 + } 71 + } 72 + 73 + static void test_sandbox_init_errorbuf_is_null(void) 74 + { 75 + TEST("sandbox_init sets errorbuf to NULL"); 76 + char *err = (char *)0xDEADBEEF; 77 + sandbox_init("no_network", 0, &err); 78 + if (err != NULL) { 79 + FAIL("errorbuf is not NULL — old bug where it was set to \"Not implemented\""); 80 + } else { 81 + PASS(); 82 + } 83 + } 84 + 85 + static void test_sandbox_init_with_null_errorbuf(void) 86 + { 87 + TEST("sandbox_init with NULL errorbuf does not crash"); 88 + /* Some callers may pass NULL for errorbuf if they don't care about 89 + * the error message. The stub must not dereference NULL. */ 90 + int ret = sandbox_init("no_network", 0, NULL); 91 + if (ret != 0) { 92 + FAIL("returned non-zero"); 93 + } else { 94 + PASS(); 95 + } 96 + } 97 + 98 + static void test_sandbox_init_with_parameters_returns_zero(void) 99 + { 100 + TEST("sandbox_init_with_parameters returns 0"); 101 + char *err = (char *)0xDEADBEEF; 102 + const char *params[] = { "key", "value", NULL }; 103 + int ret = sandbox_init_with_parameters("no_network", 0, params, &err); 104 + if (ret != 0) { 105 + FAIL("returned non-zero"); 106 + } else { 107 + PASS(); 108 + } 109 + } 110 + 111 + static void test_sandbox_init_with_parameters_errorbuf_is_null(void) 112 + { 113 + TEST("sandbox_init_with_parameters sets errorbuf to NULL"); 114 + char *err = (char *)0xDEADBEEF; 115 + const char *params[] = { NULL }; 116 + sandbox_init_with_parameters("no_network", 0, params, &err); 117 + if (err != NULL) { 118 + FAIL("errorbuf is not NULL"); 119 + } else { 120 + PASS(); 121 + } 122 + } 123 + 124 + static void test_sandbox_init_with_extensions_returns_zero(void) 125 + { 126 + TEST("sandbox_init_with_extensions returns 0"); 127 + char *err = (char *)0xDEADBEEF; 128 + const char *exts[] = { NULL }; 129 + int ret = sandbox_init_with_extensions("no_network", 0, exts, &err); 130 + if (ret != 0) { 131 + FAIL("returned non-zero"); 132 + } else { 133 + PASS(); 134 + } 135 + } 136 + 137 + static void test_sandbox_init_with_extensions_errorbuf_is_null(void) 138 + { 139 + TEST("sandbox_init_with_extensions sets errorbuf to NULL"); 140 + char *err = (char *)0xDEADBEEF; 141 + const char *exts[] = { NULL }; 142 + sandbox_init_with_extensions("no_network", 0, exts, &err); 143 + if (err != NULL) { 144 + FAIL("errorbuf is not NULL"); 145 + } else { 146 + PASS(); 147 + } 148 + } 149 + 150 + static void test_sandbox_check_allows_all(void) 151 + { 152 + TEST("sandbox_check returns 0 (allowed) for any operation"); 153 + /* sandbox_check returning 0 means the operation is permitted. 154 + * Our stub should always permit. */ 155 + int ret = sandbox_check(getpid(), "file-read-data", 0); 156 + if (ret != 0) { 157 + FAIL("returned non-zero (operation denied)"); 158 + } else { 159 + PASS(); 160 + } 161 + } 162 + 163 + static void test_sandbox_check_network(void) 164 + { 165 + TEST("sandbox_check allows network operations"); 166 + int ret = sandbox_check(getpid(), "network-outbound", 0); 167 + if (ret != 0) { 168 + FAIL("returned non-zero"); 169 + } else { 170 + PASS(); 171 + } 172 + } 173 + 174 + static void test_sandbox_check_process_exec(void) 175 + { 176 + TEST("sandbox_check allows process-exec"); 177 + int ret = sandbox_check(getpid(), "process-exec", 0); 178 + if (ret != 0) { 179 + FAIL("returned non-zero"); 180 + } else { 181 + PASS(); 182 + } 183 + } 184 + 185 + static void test_sandbox_wakeup_daemon_returns_zero(void) 186 + { 187 + TEST("sandbox_wakeup_daemon returns 0"); 188 + char *err = (char *)0xDEADBEEF; 189 + int ret = sandbox_wakeup_daemon(&err); 190 + if (ret != 0) { 191 + FAIL("returned non-zero"); 192 + } else { 193 + PASS(); 194 + } 195 + } 196 + 197 + static void test_sandbox_wakeup_daemon_errorbuf_is_null(void) 198 + { 199 + TEST("sandbox_wakeup_daemon sets errorbuf to NULL"); 200 + char *err = (char *)0xDEADBEEF; 201 + sandbox_wakeup_daemon(&err); 202 + if (err != NULL) { 203 + FAIL("errorbuf is not NULL"); 204 + } else { 205 + PASS(); 206 + } 207 + } 208 + 209 + static void test_sandbox_free_error_null(void) 210 + { 211 + TEST("sandbox_free_error(NULL) does not crash"); 212 + /* sandbox_free_error calls free(), which should handle NULL. */ 213 + sandbox_free_error(NULL); 214 + PASS(); 215 + } 216 + 217 + static void test_sandbox_free_error_allocated(void) 218 + { 219 + TEST("sandbox_free_error(strdup'd) does not crash"); 220 + char *err = strdup("test error"); 221 + sandbox_free_error(err); 222 + PASS(); 223 + } 224 + 225 + static void test_sandbox_init_all_profiles(void) 226 + { 227 + TEST("sandbox_init succeeds for all predefined profiles"); 228 + const char *profiles[] = { 229 + "no_internet", 230 + "no_network", 231 + "no_write", 232 + "no_write_except_temporary", 233 + "pure_computation", 234 + NULL 235 + }; 236 + 237 + int all_ok = 1; 238 + for (int i = 0; profiles[i] != NULL; i++) { 239 + char *err = NULL; 240 + int ret = sandbox_init(profiles[i], 0, &err); 241 + if (ret != 0 || err != NULL) { 242 + all_ok = 0; 243 + break; 244 + } 245 + } 246 + 247 + if (all_ok) { 248 + PASS(); 249 + } else { 250 + FAIL("one or more predefined profiles failed"); 251 + } 252 + } 253 + 254 + /* ── Main ─────────────────────────────────────────────────────────────────── */ 255 + 256 + int main(int argc, char *argv[]) 257 + { 258 + printf("\n"); 259 + printf("═══════════════════════════════════════════════════════════════\n"); 260 + printf(" Sandbox API regression tests (Phase 2)\n"); 261 + printf("═══════════════════════════════════════════════════════════════\n"); 262 + printf("\n"); 263 + 264 + test_sandbox_init_returns_zero(); 265 + test_sandbox_init_errorbuf_is_null(); 266 + test_sandbox_init_with_null_errorbuf(); 267 + test_sandbox_init_with_parameters_returns_zero(); 268 + test_sandbox_init_with_parameters_errorbuf_is_null(); 269 + test_sandbox_init_with_extensions_returns_zero(); 270 + test_sandbox_init_with_extensions_errorbuf_is_null(); 271 + test_sandbox_check_allows_all(); 272 + test_sandbox_check_network(); 273 + test_sandbox_check_process_exec(); 274 + test_sandbox_wakeup_daemon_returns_zero(); 275 + test_sandbox_wakeup_daemon_errorbuf_is_null(); 276 + test_sandbox_free_error_null(); 277 + test_sandbox_free_error_allocated(); 278 + test_sandbox_init_all_profiles(); 279 + 280 + printf("\n"); 281 + printf("───────────────────────────────────────────────────────────────\n"); 282 + printf(" Results: %d run, \033[32m%d passed\033[0m, \033[%sm%d failed\033[0m\n", 283 + tests_run, tests_passed, 284 + tests_failed > 0 ? "31" : "32", 285 + tests_failed); 286 + printf("───────────────────────────────────────────────────────────────\n"); 287 + printf("\n"); 288 + 289 + return tests_failed > 0 ? 1 : 0; 290 + }
+255
tests/sandbox/test_sandbox_exec.sh
··· 1 + #!/bin/sh 2 + # test_sandbox_exec.sh — Regression tests for the sandbox-exec stub binary 3 + # 4 + # Run inside darling shell: 5 + # sh /path/to/test_sandbox_exec.sh 6 + # 7 + # Expected: all tests pass (exit 0). 8 + # 9 + # See: plan/04-phase2-sandbox.md (Task 2.1) 10 + 11 + set -u 12 + 13 + PASS=0 14 + FAIL=0 15 + TOTAL=0 16 + 17 + pass() { 18 + PASS=$((PASS + 1)) 19 + TOTAL=$((TOTAL + 1)) 20 + printf " TEST %2d: %-55s \033[32mPASS\033[0m\n" "$TOTAL" "$1" 21 + } 22 + 23 + fail() { 24 + FAIL=$((FAIL + 1)) 25 + TOTAL=$((TOTAL + 1)) 26 + printf " TEST %2d: %-55s \033[31mFAIL\033[0m — %s\n" "$TOTAL" "$1" "$2" 27 + } 28 + 29 + # ── Locate sandbox-exec ───────────────────────────────────────────────────── 30 + 31 + SANDBOX_EXEC="/usr/bin/sandbox-exec" 32 + 33 + if [ ! -x "$SANDBOX_EXEC" ]; then 34 + echo "FATAL: $SANDBOX_EXEC not found or not executable" >&2 35 + echo " Install the sandbox-exec stub first (Phase 2, Task 2.1)" >&2 36 + exit 2 37 + fi 38 + 39 + # ── Tests ──────────────────────────────────────────────────────────────────── 40 + 41 + printf "\n" 42 + printf "═══════════════════════════════════════════════════════════════\n" 43 + printf " sandbox-exec stub regression tests (Phase 2)\n" 44 + printf "═══════════════════════════════════════════════════════════════\n" 45 + printf "\n" 46 + 47 + # --- Test: sandbox-exec exists and is executable --- 48 + if [ -x "$SANDBOX_EXEC" ]; then 49 + pass "sandbox-exec exists and is executable" 50 + else 51 + fail "sandbox-exec exists and is executable" "not found at $SANDBOX_EXEC" 52 + fi 53 + 54 + # --- Test: basic command execution --- 55 + OUTPUT=$($SANDBOX_EXEC /bin/echo hello 2>&1) 56 + if [ "$OUTPUT" = "hello" ]; then 57 + pass "basic command: sandbox-exec /bin/echo hello" 58 + else 59 + fail "basic command: sandbox-exec /bin/echo hello" "got: '$OUTPUT'" 60 + fi 61 + 62 + # --- Test: exit code is forwarded --- 63 + $SANDBOX_EXEC /bin/sh -c "exit 0" 2>/dev/null 64 + RET=$? 65 + if [ "$RET" -eq 0 ]; then 66 + pass "exit code 0 forwarded" 67 + else 68 + fail "exit code 0 forwarded" "got exit code $RET" 69 + fi 70 + 71 + $SANDBOX_EXEC /bin/sh -c "exit 42" 2>/dev/null 72 + RET=$? 73 + if [ "$RET" -eq 42 ]; then 74 + pass "exit code 42 forwarded" 75 + else 76 + fail "exit code 42 forwarded" "got exit code $RET" 77 + fi 78 + 79 + # --- Test: -f <profile> flag is ignored --- 80 + OUTPUT=$($SANDBOX_EXEC -f /nonexistent/profile.sb /bin/echo ok 2>&1) 81 + if [ "$OUTPUT" = "ok" ]; then 82 + pass "-f <profile> ignored, command runs" 83 + else 84 + fail "-f <profile> ignored, command runs" "got: '$OUTPUT'" 85 + fi 86 + 87 + # --- Test: -f /dev/null (like Nix uses) --- 88 + OUTPUT=$($SANDBOX_EXEC -f /dev/null /bin/echo "from-dev-null" 2>&1) 89 + if [ "$OUTPUT" = "from-dev-null" ]; then 90 + pass "-f /dev/null works (Nix-style invocation)" 91 + else 92 + fail "-f /dev/null works (Nix-style invocation)" "got: '$OUTPUT'" 93 + fi 94 + 95 + # --- Test: -p <profile-string> flag is ignored --- 96 + OUTPUT=$($SANDBOX_EXEC -p '(version 1)(allow default)' /bin/echo inline-ok 2>&1) 97 + if [ "$OUTPUT" = "inline-ok" ]; then 98 + pass "-p <profile-string> ignored, command runs" 99 + else 100 + fail "-p <profile-string> ignored, command runs" "got: '$OUTPUT'" 101 + fi 102 + 103 + # --- Test: -n <name> flag is ignored --- 104 + OUTPUT=$($SANDBOX_EXEC -n no_network /bin/echo named-ok 2>&1) 105 + if [ "$OUTPUT" = "named-ok" ]; then 106 + pass "-n <name> ignored, command runs" 107 + else 108 + fail "-n <name> ignored, command runs" "got: '$OUTPUT'" 109 + fi 110 + 111 + # --- Test: -D key=value (with space) --- 112 + OUTPUT=$($SANDBOX_EXEC -D _GLOBAL_TMP_DIR=/tmp /bin/echo def-ok 2>&1) 113 + if [ "$OUTPUT" = "def-ok" ]; then 114 + pass "-D key=value (with space) ignored, command runs" 115 + else 116 + fail "-D key=value (with space) ignored, command runs" "got: '$OUTPUT'" 117 + fi 118 + 119 + # --- Test: -Dkey=value (no space) --- 120 + OUTPUT=$($SANDBOX_EXEC -D_GLOBAL_TMP_DIR=/tmp /bin/echo def-nospace-ok 2>&1) 121 + if [ "$OUTPUT" = "def-nospace-ok" ]; then 122 + pass "-Dkey=value (no space) ignored, command runs" 123 + else 124 + fail "-Dkey=value (no space) ignored, command runs" "got: '$OUTPUT'" 125 + fi 126 + 127 + # --- Test: multiple flags combined (Nix-style full invocation) --- 128 + OUTPUT=$($SANDBOX_EXEC \ 129 + -f /dev/null \ 130 + -D _GLOBAL_TMP_DIR=/tmp \ 131 + -D TMPDIR=/tmp \ 132 + /bin/echo "nix-style-ok" 2>&1) 133 + if [ "$OUTPUT" = "nix-style-ok" ]; then 134 + pass "multiple flags combined (Nix-style full invocation)" 135 + else 136 + fail "multiple flags combined (Nix-style full invocation)" "got: '$OUTPUT'" 137 + fi 138 + 139 + # --- Test: all flag types combined --- 140 + OUTPUT=$($SANDBOX_EXEC \ 141 + -f /dev/null \ 142 + -p '(version 1)' \ 143 + -n no_network \ 144 + -D FOO=bar \ 145 + -DBAZ=quux \ 146 + /bin/echo "all-flags-ok" 2>&1) 147 + if [ "$OUTPUT" = "all-flags-ok" ]; then 148 + pass "all flag types combined" 149 + else 150 + fail "all flag types combined" "got: '$OUTPUT'" 151 + fi 152 + 153 + # --- Test: arguments are passed through to the command --- 154 + OUTPUT=$($SANDBOX_EXEC -f /dev/null /bin/echo arg1 arg2 arg3 2>&1) 155 + if [ "$OUTPUT" = "arg1 arg2 arg3" ]; then 156 + pass "arguments passed through to command" 157 + else 158 + fail "arguments passed through to command" "got: '$OUTPUT'" 159 + fi 160 + 161 + # --- Test: command with flags that look like sandbox-exec flags --- 162 + OUTPUT=$($SANDBOX_EXEC -f /dev/null /bin/echo -f -D -n -p 2>&1) 163 + if [ "$OUTPUT" = "-f -D -n -p" ]; then 164 + pass "command args that look like sandbox flags are preserved" 165 + else 166 + fail "command args that look like sandbox flags are preserved" "got: '$OUTPUT'" 167 + fi 168 + 169 + # --- Test: no command specified → error + non-zero exit --- 170 + $SANDBOX_EXEC 2>/dev/null 171 + RET=$? 172 + if [ "$RET" -ne 0 ]; then 173 + pass "no command → non-zero exit" 174 + else 175 + fail "no command → non-zero exit" "got exit code $RET" 176 + fi 177 + 178 + # --- Test: only flags, no command → error + non-zero exit --- 179 + $SANDBOX_EXEC -f /dev/null -D FOO=bar 2>/dev/null 180 + RET=$? 181 + if [ "$RET" -ne 0 ]; then 182 + pass "only flags, no command → non-zero exit" 183 + else 184 + fail "only flags, no command → non-zero exit" "got exit code $RET" 185 + fi 186 + 187 + # --- Test: error message on no command --- 188 + STDERR=$($SANDBOX_EXEC 2>&1 >/dev/null || true) 189 + if echo "$STDERR" | grep -qi "no command\|usage\|sandbox-exec"; then 190 + pass "helpful error message when no command given" 191 + else 192 + fail "helpful error message when no command given" "stderr: '$STDERR'" 193 + fi 194 + 195 + # --- Test: nonexistent command → exit 127 --- 196 + $SANDBOX_EXEC /nonexistent/binary 2>/dev/null 197 + RET=$? 198 + if [ "$RET" -eq 127 ]; then 199 + pass "nonexistent command → exit 127" 200 + else 201 + fail "nonexistent command → exit 127" "got exit code $RET" 202 + fi 203 + 204 + # --- Test: environment variables are inherited --- 205 + OUTPUT=$(FOO_TEST_VAR=hello123 $SANDBOX_EXEC /bin/sh -c 'echo $FOO_TEST_VAR' 2>&1) 206 + if [ "$OUTPUT" = "hello123" ]; then 207 + pass "environment variables inherited through sandbox-exec" 208 + else 209 + fail "environment variables inherited through sandbox-exec" "got: '$OUTPUT'" 210 + fi 211 + 212 + # --- Test: stdin is passed through --- 213 + OUTPUT=$(echo "stdin-data" | $SANDBOX_EXEC /bin/cat 2>&1) 214 + if [ "$OUTPUT" = "stdin-data" ]; then 215 + pass "stdin passed through to command" 216 + else 217 + fail "stdin passed through to command" "got: '$OUTPUT'" 218 + fi 219 + 220 + # --- Test: working directory is preserved --- 221 + EXPECTED_DIR=$(pwd) 222 + OUTPUT=$($SANDBOX_EXEC /bin/pwd 2>&1) 223 + if [ "$OUTPUT" = "$EXPECTED_DIR" ]; then 224 + pass "working directory preserved" 225 + else 226 + fail "working directory preserved" "expected '$EXPECTED_DIR', got '$OUTPUT'" 227 + fi 228 + 229 + # --- Test: sandbox-exec with /bin/bash -e (Nix builder pattern) --- 230 + TMPFILE=$(mktemp /tmp/sandbox-test.XXXXXX) 231 + $SANDBOX_EXEC -f /dev/null -D _GLOBAL_TMP_DIR=/tmp \ 232 + /bin/bash -e -c "echo builder-ok > $TMPFILE" 2>&1 233 + if [ -f "$TMPFILE" ] && [ "$(cat "$TMPFILE")" = "builder-ok" ]; then 234 + pass "Nix builder pattern: sandbox-exec -f ... /bin/bash -e -c ..." 235 + else 236 + fail "Nix builder pattern: sandbox-exec -f ... /bin/bash -e -c ..." \ 237 + "file content: '$(cat "$TMPFILE" 2>/dev/null || echo MISSING)'" 238 + fi 239 + rm -f "$TMPFILE" 240 + 241 + # ── Summary ────────────────────────────────────────────────────────────────── 242 + 243 + printf "\n" 244 + printf "───────────────────────────────────────────────────────────────\n" 245 + if [ "$FAIL" -eq 0 ]; then 246 + printf " Results: %d run, \033[32m%d passed\033[0m, \033[32m0 failed\033[0m\n" \ 247 + "$TOTAL" "$PASS" 248 + else 249 + printf " Results: %d run, \033[32m%d passed\033[0m, \033[31m%d failed\033[0m\n" \ 250 + "$TOTAL" "$PASS" "$FAIL" 251 + fi 252 + printf "───────────────────────────────────────────────────────────────\n" 253 + printf "\n" 254 + 255 + exit "$FAIL"