|
| 1 | +--- |
| 2 | +name: pkg-xcompile-test |
| 3 | +description: > |
| 4 | + Cross-compile test harness for @yao-pkg/pkg. Builds a tiny hello.js for |
| 5 | + every (mode × target) combination and runs what can be executed on a Linux |
| 6 | + host (native x64, arm64 via docker+qemu, win-x64 via docker-wine). Use |
| 7 | + when the user wants to verify pkg cross-compilation claims, reproduce |
| 8 | + issues #87/#181, sanity-check a pkg PR, or compare Standard vs Enhanced |
| 9 | + SEA across Node 20/22/24. Trigger: "test cross-compile", "run pkg |
| 10 | + matrix", "verify xcompile", or /pkg-xcompile-test. |
| 11 | +--- |
| 12 | + |
| 13 | +# pkg cross-compile test harness |
| 14 | + |
| 15 | +Validates `pkg` cross-OS and cross-arch support across Node 20/22/24 and |
| 16 | +Standard vs Enhanced SEA modes. Produces a consistent result table so |
| 17 | +claims in docs and issues can be checked against reality. |
| 18 | + |
| 19 | +## When to use |
| 20 | + |
| 21 | +- User is about to edit docs about cross-compile support and needs ground truth. |
| 22 | +- User is looking at a cross-compile issue (e.g. [#87](https://github.com/yao-pkg/pkg/issues/87), [#181](https://github.com/yao-pkg/pkg/issues/181)) and wants to reproduce it. |
| 23 | +- User bumped `pkg-fetch` or touched bootstrap/prelude and wants a smoke test across targets. |
| 24 | +- User wants to know whether a regression is Node-version specific. |
| 25 | + |
| 26 | +## Host requirements |
| 27 | + |
| 28 | +This harness is designed for a **Linux x86_64** host with: |
| 29 | + |
| 30 | +- `nvm` with node 20, 22, and 24 installed (`nvm install 20 22 24`). |
| 31 | +- `docker` daemon running. |
| 32 | +- `docker` image `scottyhardy/docker-wine` (pulled on first win-x64 run). |
| 33 | +- `tonistiigi/binfmt` installed for cross-arch containers — if you see |
| 34 | + `exec format error` on arm64 runs, install with: |
| 35 | + ```bash |
| 36 | + docker run --privileged --rm tonistiigi/binfmt --install arm64 |
| 37 | + ``` |
| 38 | +- `pkg` built from this repo (`yarn build`) so `lib-es5/bin.js` exists. |
| 39 | + The script resolves the pkg entry from its own location — it lives at |
| 40 | + `.claude/skills/pkg-xcompile-test/run-matrix.sh` and defaults |
| 41 | + `PKG_BIN` to `<repo-root>/lib-es5/bin.js`. |
| 42 | + |
| 43 | +**Not supported:** macOS runtime verification. `docker-osx` needs `/dev/kvm` |
| 44 | +and darling is dead. macOS regressions have to be caught on a real Mac |
| 45 | +or on the GitHub Actions `macos-*` runners — note that in the final report. |
| 46 | + |
| 47 | +## What it runs |
| 48 | + |
| 49 | +For each Node major (20, 22, 24) the harness: |
| 50 | + |
| 51 | +1. Uses `nvm use $major` so the pkg **host** node matches the **target** |
| 52 | + node major. This matters — SEA's blob generator uses host `execPath` |
| 53 | + when host-major == target-major, otherwise tries to run the downloaded |
| 54 | + target-arch node binary (which fails without cross-arch emulation). |
| 55 | +2. Loops over modes × targets: |
| 56 | + - **Modes**: `std`, `std-public` (`--public-packages "*" --public`), `sea` |
| 57 | + - **Targets**: `linux-x64`, `linux-arm64`, `win-x64`, `macos-x64`, `macos-arm64` |
| 58 | +3. Records **build result** (OK / FAIL), then tries to **run** the binary: |
| 59 | + - linux-x64 → native |
| 60 | + - linux-arm64 → `docker run --platform linux/arm64 ubuntu:latest` |
| 61 | + - win-x64 → `docker run scottyhardy/docker-wine` (see gotcha below) |
| 62 | + - macos-\* → skipped (`SKIP-no-mac`) |
| 63 | +4. Prints a summary table. |
| 64 | + |
| 65 | +## Wine gotcha |
| 66 | + |
| 67 | +`wine` inside a non-tty docker container produces invalid stdio file |
| 68 | +descriptors, causing Node to crash with `Error: open EBADF` before any |
| 69 | +user code runs. The workaround is to redirect wine's stdout/stderr to |
| 70 | +files **inside** the container, then `cat` them back: |
| 71 | + |
| 72 | +```bash |
| 73 | +docker run --rm -v "$BINDIR:/mnt" scottyhardy/docker-wine \ |
| 74 | + bash -c "wine '/mnt/app.exe' </dev/null >/tmp/out 2>/tmp/err; cat /tmp/out" |
| 75 | +``` |
| 76 | + |
| 77 | +Without this, every wine run will look like a pkg failure when it isn't. |
| 78 | + |
| 79 | +## Usage |
| 80 | + |
| 81 | +Run from the repo root (paths in the examples are relative to it). |
| 82 | + |
| 83 | +### Quick run (single node version) |
| 84 | + |
| 85 | +```bash |
| 86 | +# Runs the full matrix for node 22 targets with node 22 as host |
| 87 | +./.claude/skills/pkg-xcompile-test/run-matrix.sh 22 |
| 88 | +``` |
| 89 | + |
| 90 | +### Full sweep (20 + 22 + 24) |
| 91 | + |
| 92 | +```bash |
| 93 | +for V in 20 22 24; do |
| 94 | + ./.claude/skills/pkg-xcompile-test/run-matrix.sh $V |
| 95 | +done |
| 96 | +``` |
| 97 | + |
| 98 | +### Custom pkg build |
| 99 | + |
| 100 | +```bash |
| 101 | +./.claude/skills/pkg-xcompile-test/run-matrix.sh 22 /path/to/other/pkg/lib-es5/bin.js |
| 102 | +``` |
| 103 | + |
| 104 | +### Custom work directory |
| 105 | + |
| 106 | +By default build outputs go to `/tmp/pkg-xcompile/bin-node<major>/`. Override with: |
| 107 | + |
| 108 | +```bash |
| 109 | +PKG_XCOMPILE_WORKDIR=/somewhere/else ./.claude/skills/pkg-xcompile-test/run-matrix.sh 22 |
| 110 | +``` |
| 111 | + |
| 112 | +### Reading the output |
| 113 | + |
| 114 | +Each cell is `BUILD / RUN`: |
| 115 | + |
| 116 | +- `OK / OK` — works |
| 117 | +- `OK / FAIL` — built, but runtime error on target |
| 118 | +- `FAIL / n/a` — build failed |
| 119 | +- `OK / SKIP-no-mac` — can't test macOS runtime on this host |
| 120 | + |
| 121 | +## Known results (captured 2026-04-15) |
| 122 | + |
| 123 | +Host: Ubuntu 24.04 x86_64, pkg HEAD of `docs/github-pages-site`. Tests run |
| 124 | +with matching host-node / target-node major. |
| 125 | + |
| 126 | +### Node 20 |
| 127 | + |
| 128 | +| target | std | std-public | sea | |
| 129 | +| ----------- | -------- | ---------- | ------------------------------ | |
| 130 | +| linux-x64 | OK / OK | OK / OK | **FAIL** — SEA needs host ≥ 22 | |
| 131 | +| linux-arm64 | OK / OK | OK / OK | FAIL (same) | |
| 132 | +| win-x64 | OK / OK | OK / OK | FAIL (same) | |
| 133 | +| macos-x64 | OK / n/a | OK / n/a | FAIL (same) | |
| 134 | +| macos-arm64 | OK / n/a | OK / n/a | FAIL (same) | |
| 135 | + |
| 136 | +Standard cross-compile on Node 20 **works without workarounds**. |
| 137 | +SEA mode is unavailable because pkg enforces `host node ≥ 22`. |
| 138 | + |
| 139 | +### Node 22 |
| 140 | + |
| 141 | +| target | std | std-public | sea | |
| 142 | +| ----------- | ------------- | ---------- | -------- | |
| 143 | +| linux-x64 | OK / OK | OK / OK | OK / OK | |
| 144 | +| linux-arm64 | OK / **FAIL** | OK / OK | OK / OK | |
| 145 | +| win-x64 | OK / **FAIL** | OK / OK | OK / OK | |
| 146 | +| macos-x64 | OK / n/a | OK / n/a | OK / n/a | |
| 147 | +| macos-arm64 | OK / n/a | OK / n/a | OK / n/a | |
| 148 | + |
| 149 | +Standard cross-compile on Node 22 is **broken**: |
| 150 | + |
| 151 | +- `linux-arm64` crashes at runtime with `Error: UNEXPECTED-20` in |
| 152 | + `readFileFromSnapshot`. Matches the [#181](https://github.com/yao-pkg/pkg/issues/181) |
| 153 | + failure mode. |
| 154 | +- `win-x64` exits silently with EXIT=4 and no stdout. Matches the [#87](https://github.com/yao-pkg/pkg/issues/87) |
| 155 | + Windows silent-exit bug. |
| 156 | + |
| 157 | +Both are fixed by adding `--public-packages "*" --public` (which skips the |
| 158 | +V8 bytecode step). Enhanced SEA avoids both out of the box. |
| 159 | + |
| 160 | +### Node 24 |
| 161 | + |
| 162 | +| target | std | std-public | sea | |
| 163 | +| ----------- | -------- | ---------- | -------- | |
| 164 | +| linux-x64 | OK / OK | OK / OK | OK / OK | |
| 165 | +| linux-arm64 | OK / OK | OK / OK | OK / OK | |
| 166 | +| win-x64 | OK / OK | OK / OK | OK / OK | |
| 167 | +| macos-x64 | OK / n/a | OK / n/a | OK / n/a | |
| 168 | +| macos-arm64 | OK / n/a | OK / n/a | OK / n/a | |
| 169 | + |
| 170 | +Node 24 **works out of the box** for Standard and SEA, same as Node 20. |
| 171 | +The Node 22 bug is a Node-22-specific `pkg-fetch` patch regression, not |
| 172 | +a permanent Standard-mode limitation. |
| 173 | + |
| 174 | +### macOS runtime |
| 175 | + |
| 176 | +Not verified — see "Host requirements" above. The regressions tracked |
| 177 | +in [#181](https://github.com/yao-pkg/pkg/issues/181) (macOS host, Node 22+) must be confirmed on a real Mac. |
| 178 | +GitHub Actions' `macos-13` / `macos-14` runners are the right place for |
| 179 | +that; a follow-up CI workflow that runs the hello.js matrix there would |
| 180 | +close the last hole. |
| 181 | + |
| 182 | +## Debugging a failing run |
| 183 | + |
| 184 | +Each cell writes its own log; paths are printed on stderr next to any |
| 185 | +`FAIL` and re-summarised at the end of the run. |
| 186 | + |
| 187 | +- **Log location** — `$PKG_XCOMPILE_WORKDIR/logs/node<major>/build-<mode>-<target>.log` |
| 188 | + (and `run-<mode>-<target>.log`). Default workdir is `/tmp/pkg-xcompile`. |
| 189 | + Logs are per-cell so a failing build is not overwritten by the next one. |
| 190 | +- **Re-run one cell** — the fastest way is still to invoke `pkg` directly |
| 191 | + against the same fixture: |
| 192 | + ```bash |
| 193 | + cd /tmp/pkg-xcompile |
| 194 | + node /path/to/pkg/lib-es5/bin.js hello.js -t node22-linux-arm64 \ |
| 195 | + -o /tmp/pkg-xcompile/bin-node22/std-linux-arm64 |
| 196 | + ``` |
| 197 | +- **`exec format error` on arm64 runs** — binfmt is not registered. After |
| 198 | + a reboot `tonistiigi/binfmt` needs to be re-installed: |
| 199 | + ```bash |
| 200 | + docker run --privileged --rm tonistiigi/binfmt --install arm64 |
| 201 | + ``` |
| 202 | +- **Wine cell always FAIL** — pull the image manually once |
| 203 | + (`docker pull scottyhardy/docker-wine`) and inspect |
| 204 | + `logs/node<major>/run-sea-win-x64.log` for the actual crash. The |
| 205 | + EBADF-stdout gotcha is documented above. |
| 206 | +- **SEA FAIL on Node 20** — expected; pkg enforces `host node ≥ 22` for SEA. |
| 207 | +- **`Error: UNEXPECTED-20` / silent exit (EXIT=4)** — these are the |
| 208 | + tracked Node-22 Standard-mode regressions (issues #181 and #87). Not |
| 209 | + environment bugs — see [Node 22 results](#node-22) below. |
| 210 | +- **Stale binary hiding a regression** — pkg does not hash its own source |
| 211 | + into the output. After rebuilding pkg, delete `bin-node<major>/` before |
| 212 | + re-running the matrix. |
| 213 | + |
| 214 | +## The script |
| 215 | + |
| 216 | +The harness lives next to this `SKILL.md` as `run-matrix.sh`. Before |
| 217 | +first run, set `PKG_BIN` to your pkg build and ensure `nvm` is sourced. |
| 218 | + |
| 219 | +## Quick reference of pkg flags touched |
| 220 | + |
| 221 | +| Flag | Effect | |
| 222 | +| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | |
| 223 | +| `-t nodeNN-<os>-<arch>` | Target triple | |
| 224 | +| `--sea` | Enhanced SEA mode | |
| 225 | +| `--public-packages "*" --public` | Disable V8 bytecode, include sources in plaintext — the cross-compile escape hatch for Standard mode | |
| 226 | +| `--no-bytecode` | Same effect for bytecode, but does not imply `--public` — use with `--public-packages` if you want consistent behavior | |
| 227 | +| `--debug` | Inject diagnostic bootstrap; enables `DEBUG_PKG` at runtime | |
| 228 | + |
| 229 | +## Notes for future maintainers |
| 230 | + |
| 231 | +- Do **not** reuse cached binaries between matrix runs — delete |
| 232 | + `bin-node${major}/` first if you rebuilt pkg. pkg does not hash its |
| 233 | + own source into the output, so stale binaries silently hide regressions. |
| 234 | +- If you add a new target to the matrix, also add a runner branch in |
| 235 | + `run_one()`. Unknown targets fall through the `case` and count as pass. |
| 236 | +- If `scottyhardy/docker-wine` becomes unavailable, `tobix/wine` is a |
| 237 | + smaller drop-in replacement — it has the same EBADF stdout gotcha. |
| 238 | +- Don't confuse "builds with warning" with "builds clean". The matrix |
| 239 | + only records `OK` when the binary file exists on disk; a warning like |
| 240 | + `Failed to make bytecode node22-arm64` still produces a binary but it |
| 241 | + will crash at runtime. Runtime is the source of truth. |
0 commit comments