From 881023486b4d223d415e2e7f01c5b36cc56dc1e1 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Sun, 1 Nov 2026 20:07:32 +0000 Subject: [PATCH 1/8] fix(scripts): build libghostty-vt for the baseline CPU The v0.1.0-beta.4 linux-x64 prebuild crashes with SIGILL on CPUs without AVX-512. build-libghostty-vt.sh passed no -Dcpu, so Zig compiled for the release runner's CPU (14,870 AVX-512 instructions in the addon). Under qemu-user with a Skylake or generic x86-64 CPU, loading it and creating a terminal dies with exit 132. libghostty-vt now builds with -Dcpu=$LIBGHOSTTY_VT_CPU (default baseline) and records the CPU in ghostty-source.json. A baseline build has no AVX-512; its only AVX2 code is wuffs' runtime-dispatched *_x86_avx2 functions. scripts/check-baseline-cpu.sh runs examples/smoke.ts in one Node process under qemu-x86_64 -cpu qemu64. The linux-x64 CI job runs it against build/Release and the linux-x64 release-prebuilds job against prebuilds/. It exits 1 for the beta.4 prebuild and 0 for a baseline build. Change-Id: I21af6fdcb2575f9eededee641c1377a9b96a1c55 Signed-off-by: Thomas Kosiewski --- .github/workflows/ci.yml | 6 +++++ .github/workflows/release-prebuilds.yml | 8 ++++++ CHANGELOG.md | 3 +++ README.md | 2 +- mise.toml | 4 +++ scripts/README.md | 9 ++++--- scripts/build-libghostty-vt.sh | 6 +++++ scripts/check-baseline-cpu.sh | 35 +++++++++++++++++++++++++ 8 files changed, 69 insertions(+), 4 deletions(-) create mode 100755 scripts/check-baseline-cpu.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e798069..9b7222d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,3 +39,9 @@ jobs: - uses: actions/checkout@v6 - uses: jdx/mise-action@v3 - run: mise run ${{ matrix.task }} + # GitHub's x64 runners differ in CPU features, so a passing run proves + # nothing about older CPUs. Run the addon on an emulated baseline CPU. + - if: matrix.target == 'linux-x64' + run: sudo apt-get update && sudo apt-get install -y qemu-user + - if: matrix.target == 'linux-x64' + run: mise run check-baseline-cpu diff --git a/.github/workflows/release-prebuilds.yml b/.github/workflows/release-prebuilds.yml index b0e11ba..15647cf 100644 --- a/.github/workflows/release-prebuilds.yml +++ b/.github/workflows/release-prebuilds.yml @@ -30,6 +30,14 @@ jobs: - uses: actions/checkout@v6 - uses: jdx/mise-action@v3 - run: mise run release-prebuilds + # The runner's CPU decides nothing here: the prebuild must run on a + # generic x86-64 CPU, as users' machines can lack AVX-512 or AVX2. + - if: matrix.target == 'linux-x64' + run: sudo apt-get update && sudo apt-get install -y qemu-user + - if: matrix.target == 'linux-x64' + run: mise run check-baseline-cpu + env: + PREBUILDS_ONLY: "1" - uses: actions/upload-artifact@v4 with: name: prebuilds-${{ matrix.target }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ec50f0..33787b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable user-facing changes to this package will be documented in this file. ## [Unreleased] +## Fixed +- The `linux-x64` prebuild no longer crashes with `Illegal instruction` (SIGILL) on CPUs without AVX-512. `v0.1.0-beta.4` was built for the release runner's CPU, so loading it and creating a terminal failed on many x86-64 machines and CI runners. libghostty-vt now builds with Zig's `-Dcpu=baseline` (override with `LIBGHOSTTY_VT_CPU`), and CI and the release workflow run the smoke example on an emulated generic x86-64 CPU. + ## [v0.1.0-beta.4](https://github.com/coder/libghostty-vt-node/releases/tag/v0.1.0-beta.4) - 2026-10-30 ## Fixed diff --git a/README.md b/README.md index 77ae4f2..8351142 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ npm test npm run smoke ``` -`npm run build:libghostty` fetches the pinned Ghostty commit into `vendor/ghostty` and installs a static `libghostty-vt` into `vendor/libghostty-vt`. The default uses `-Dsimd=false` for a dependency-minimal static library. Set `LIBGHOSTTY_VT_SIMD=true` to build SIMD support when your build environment supports it. +`npm run build:libghostty` fetches the pinned Ghostty commit into `vendor/ghostty` and installs a static `libghostty-vt` into `vendor/libghostty-vt`. The default uses `-Dsimd=false` for a dependency-minimal static library. Set `LIBGHOSTTY_VT_SIMD=true` to build SIMD support when your build environment supports it. The build targets the generic `baseline` CPU of the architecture, so the library runs on older CPUs. Set `LIBGHOSTTY_VT_CPU=native` for a local build tuned to your machine; do not ship it. You can also provide an existing install: diff --git a/mise.toml b/mise.toml index 509444a..f5ac5d5 100644 --- a/mise.toml +++ b/mise.toml @@ -37,6 +37,10 @@ run = [ "npm pack --dry-run", ] +[tasks.check-baseline-cpu] +description = "Run the smoke example under an emulated x86-64 baseline CPU (Linux x86_64, needs qemu-user)." +run = "scripts/check-baseline-cpu.sh" + [tasks.assemble-prebuilds] description = "Assemble per-platform GitHub Actions prebuild artifacts into npm package layout." run = "npm run assemble:prebuilds" diff --git a/scripts/README.md b/scripts/README.md index df16b10..83bb1f2 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -8,7 +8,8 @@ | Script | Invoked by | Inputs | Output | | --- | --- | --- | --- | -| `build-libghostty-vt.sh` | `npm run build:libghostty` (mise tasks `ci` and `release-prebuilds`) | env `GHOSTTY_REPO`, `GHOSTTY_COMMIT`, `GHOSTTY_SOURCE_DIR` (default `vendor/ghostty`), `LIBGHOSTTY_VT_PREFIX` (default `vendor/libghostty-vt`), `LIBGHOSTTY_VT_SIMD` (default `false`), `LIBGHOSTTY_VT_OPTIMIZE` (default `ReleaseFast`) | progress on stdout, errors on stderr. Exit 0 on success. Its own checks exit 1. Under `set -euo pipefail`, a failed `mkdir` or `git` command exits with that command's status [scripts/build-libghostty-vt.sh:need] | +| `build-libghostty-vt.sh` | `npm run build:libghostty` (mise tasks `ci` and `release-prebuilds`) | env `GHOSTTY_REPO`, `GHOSTTY_COMMIT`, `GHOSTTY_SOURCE_DIR` (default `vendor/ghostty`), `LIBGHOSTTY_VT_PREFIX` (default `vendor/libghostty-vt`), `LIBGHOSTTY_VT_SIMD` (default `false`), `LIBGHOSTTY_VT_OPTIMIZE` (default `ReleaseFast`), `LIBGHOSTTY_VT_CPU` (default `baseline`) | progress on stdout, errors on stderr. Exit 0 on success. Its own checks exit 1. Under `set -euo pipefail`, a failed `mkdir` or `git` command exits with that command's status [scripts/build-libghostty-vt.sh:need] | +| `check-baseline-cpu.sh` | mise task `check-baseline-cpu`, run by the `linux-x64` jobs of `.github/workflows/ci.yml` and `.github/workflows/release-prebuilds.yml` (the latter with `PREBUILDS_ONLY=1`) | env `QEMU_CPU` (default `qemu64`), `PREBUILDS_ONLY`. Needs Linux on x86_64 and `qemu-x86_64-static` or `qemu-x86_64` on `PATH` | the smoke output on stdout. Exit 0 when `examples/smoke.ts` passes on the emulated CPU, 1 when it fails, 2 on the wrong platform or without qemu [scripts/check-baseline-cpu.sh:qemu] | | `resolve-libghostty-vt.mjs` | `binding.gyp` `include_dirs` and `libraries` | argv `include` or `library`. Env `LIBGHOSTTY_VT_PREFIX` (default `vendor/libghostty-vt`), `LIBGHOSTTY_VT_INCLUDE_DIR` (default `/include`), `LIBGHOSTTY_VT_STATIC_LIB`, then `LIBGHOSTTY_VT_LIB` (default `/lib/libghostty-vt.a`, `ghostty-vt-static.lib` on win32) | the absolute path on stdout, with no trailing newline [scripts/resolve-libghostty-vt.mjs:libraryPath] | | `assemble-prebuilds.mjs` | `npm run assemble:prebuilds` (mise task `assemble-prebuilds`, run by `publish-dry-run` and `publish`) | argv[2] source (default `.release-artifacts`), argv[3] target (default `prebuilds`), env `EXPECTED_PREBUILD_PLATFORMS` (default `darwin-arm64,linux-arm64,linux-x64`) | JSON `{ sourceRoot, targetRoot, platforms }` on stdout [scripts/assemble-prebuilds.mjs:expectedPlatforms] | | `verify-prebuilds.mjs` | `npm run verify:prebuilds` (mise tasks `release-prebuilds`, `publish-dry-run`, and `publish`) | `prebuilds/` and `dist/index.js` (run `npm run build` first) | JSON `{ platforms, addon, nativeInfo }` on stdout, where `addon` is the file it loaded [scripts/verify-prebuilds.mjs:addon] | @@ -20,10 +21,11 @@ In the `.mjs` scripts, a failed `assert` or a thrown `Error` ends the process wi ## Invariants - The default Ghostty commit is `83fb95624b0439ee660c2679b43b38c8b63769cd`. `GHOSTTY_COMMIT` overrides it [scripts/build-libghostty-vt.sh:GHOSTTY_COMMIT]. +- `build-libghostty-vt.sh` passes `-Dcpu=$LIBGHOSTTY_VT_CPU` (default `baseline`), so the library runs on any CPU of its architecture. Without it, Zig targets the build machine, and the `v0.1.0-beta.4` `linux-x64` prebuild, built on a runner with AVX-512, died with SIGILL on CPUs without AVX-512 [scripts/build-libghostty-vt.sh:CPU]. - `build-libghostty-vt.sh` exits 1 with `"error: missing required command ''"` and `"Install it first, then rerun npm run build:libghostty."` when `git` or `zig` is not on `PATH`. For `zig` it also prints `"This Ghostty commit requires Zig 0.17.0. With mise: mise install"` [scripts/build-libghostty-vt.sh:need]. - `build-libghostty-vt.sh` exits 1 when `zig version` differs from `minimum_zig_version` in Ghostty's `build.zig.zon`. It skips this check when that file names no version [scripts/build-libghostty-vt.sh:required_zig]. - `build-libghostty-vt.sh` exits 1 with `"error: expected exactly one 'const lib_version = \"\";' line in /build.zig, with no build metadata"` when Ghostty's `build.zig` has no such line, has more than one, or the value is not `MAJOR.MINOR.PATCH[-PRE]`. It checks this before it runs `zig` [scripts/build-libghostty-vt.sh:lib_version]. -- `build-libghostty-vt.sh` runs `zig build -Demit-lib-vt=true -Dsimd=$SIMD -Doptimize=$OPTIMIZE -Dlib-version-string=+ --prefix $PREFIX install`, with `--sysroot ` on Darwin when `xcrun` returns a path. `` is the full commit that `git rev-parse --verify HEAD` reports after the checkout. libghostty-vt reports `+` as its version string and `` as its build metadata. A failed build exits 1 [scripts/build-libghostty-vt.sh:zig_args] [scripts/build-libghostty-vt.sh:ghostty_commit]. +- `build-libghostty-vt.sh` runs `zig build -Demit-lib-vt=true -Dsimd=$SIMD -Doptimize=$OPTIMIZE -Dcpu=$CPU -Dlib-version-string=+ --prefix $PREFIX install`, with `--sysroot ` on Darwin when `xcrun` returns a path. `` is the full commit that `git rev-parse --verify HEAD` reports after the checkout. libghostty-vt reports `+` as its version string and `` as its build metadata. A failed build exits 1 [scripts/build-libghostty-vt.sh:zig_args] [scripts/build-libghostty-vt.sh:ghostty_commit]. - After the build, `$PREFIX/include/ghostty/vt.h` and `$PREFIX/lib/libghostty-vt.a` must exist. Otherwise the script exits 1 [scripts/build-libghostty-vt.sh:header]. - `resolve-libghostty-vt.mjs` exits 2 with `"usage: resolve-libghostty-vt.mjs "` for any other argument [scripts/resolve-libghostty-vt.mjs:command]. It exits 1 when `/ghostty/vt.h` or the static library is missing, and prints the setup steps and the paths it checked [scripts/resolve-libghostty-vt.mjs:fail]. - `resolve-libghostty-vt.mjs` uses `??` for its defaults, so an environment variable set to an empty string does not fall back. `resolve("")` gives the working directory. Relative values resolve against the working directory, not the repository root [scripts/resolve-libghostty-vt.mjs:prefix]. @@ -39,7 +41,7 @@ In the `.mjs` scripts, a failed `assert` or a thrown `Error` ends the process wi - `build-libghostty-vt.sh` creates the parent of `GHOSTTY_SOURCE_DIR` and the `PREFIX` directory with `mkdir -p` [scripts/build-libghostty-vt.sh:PREFIX]. - Network: when `$GHOSTTY_SOURCE_DIR/.git` is missing, `build-libghostty-vt.sh` runs `git clone $GHOSTTY_REPO` (default `https://github.com/ghostty-org/ghostty.git`). Every run then runs `git fetch --depth 1 origin ` and `git checkout --detach ` in that directory, so every run needs the network [scripts/build-libghostty-vt.sh:GHOSTTY_REPO]. -- `build-libghostty-vt.sh` runs `zig build ... install` into `PREFIX` [scripts/build-libghostty-vt.sh:zig_args] and writes `$PREFIX/ghostty-source.json` with the repository, commit, Zig version, SIMD, and optimize values. `commit` is the same resolved full commit that the build passed to `zig`, not the raw `GHOSTTY_COMMIT` value [scripts/build-libghostty-vt.sh:ghostty-source.json]. +- `build-libghostty-vt.sh` runs `zig build ... install` into `PREFIX` [scripts/build-libghostty-vt.sh:zig_args] and writes `$PREFIX/ghostty-source.json` with the repository, commit, Zig version, SIMD, optimize, and CPU values. `commit` is the same resolved full commit that the build passed to `zig`, not the raw `GHOSTTY_COMMIT` value [scripts/build-libghostty-vt.sh:ghostty-source.json]. - `assemble-prebuilds.mjs` copies each `/prebuilds-*/` directory into a staging directory next to the target (`.staging-`, default `prebuilds.staging-`) with `errorOnExist: true`, and runs every check there. Only then does it move the old target to `.previous-`, rename the staging directory to the target, and delete the old one. A failed run deletes the staging directory and leaves the target as it was [scripts/assemble-prebuilds.mjs:stagingRoot]. - `verify-prebuilds.mjs` imports `../dist/index.js` and calls `getNativeInfo()`, which loads the native addon [scripts/verify-prebuilds.mjs:info]. - Network and remote state: `create-release-tag.mjs` runs `git ls-remote --tags origin`. When the tag is absent, it runs `git tag v ` and `git push origin refs/tags/v` [scripts/create-release-tag.mjs:git]. @@ -51,6 +53,7 @@ In the `.mjs` scripts, a failed `assert` or a thrown `Error` ends the process wi - `npx vitest run test/scripts.test.ts` runs `create-release-tag.mjs`, `resolve-npm-publish-tag.mjs`, `assemble-prebuilds.mjs`, and `verify-prebuilds.mjs` as black boxes in temporary package roots, with a local bare repository as `origin` and no network. `npm test` includes it. No test covers `build-libghostty-vt.sh` or `resolve-libghostty-vt.mjs`. `mise run release-prebuilds` and `mise run publish-dry-run` start with `npm ci`, which reinstalls `node_modules/`. - Build scripts: `npm run build:libghostty && npm run build:native` - Prebuild check: `mise run release-prebuilds` +- Baseline CPU check: `mise run check-baseline-cpu` after `npm run build:native` (Linux x86_64 with the `qemu-user` package). Against the `v0.1.0-beta.4` `linux-x64` prebuild it exits 1 (SIGILL), against a `baseline` build it exits 0. - Prebuild assembly: `mise run publish-dry-run`. It needs `.release-artifacts/prebuilds-*` directories, which CI downloads in `.github/workflows/publish.yml`. - Dist-tag rule: `npm run resolve:publish-tag` (prints `beta` for version `0.1.0-beta.1`) - CAUTION: Do not run `npm run release:create-tag` to test it. It creates the `v` tag and pushes it to `origin`. diff --git a/scripts/build-libghostty-vt.sh b/scripts/build-libghostty-vt.sh index 12c6b54..d9ed50b 100755 --- a/scripts/build-libghostty-vt.sh +++ b/scripts/build-libghostty-vt.sh @@ -8,6 +8,10 @@ GHOSTTY_SOURCE_DIR="${GHOSTTY_SOURCE_DIR:-$ROOT/vendor/ghostty}" PREFIX="${LIBGHOSTTY_VT_PREFIX:-$ROOT/vendor/libghostty-vt}" SIMD="${LIBGHOSTTY_VT_SIMD:-false}" OPTIMIZE="${LIBGHOSTTY_VT_OPTIMIZE:-ReleaseFast}" +# Prebuilds must run on any CPU of their architecture. Without -Dcpu, Zig +# targets the build machine, and an AVX-512 runner makes a library that dies +# with SIGILL on CPUs without AVX-512. +CPU="${LIBGHOSTTY_VT_CPU:-baseline}" need() { if ! command -v "$1" >/dev/null 2>&1; then @@ -54,6 +58,7 @@ zig_args=( -Demit-lib-vt=true "-Dsimd=$SIMD" "-Doptimize=$OPTIMIZE" + "-Dcpu=$CPU" "-Dlib-version-string=${lib_version}+${ghostty_commit}" --prefix "$PREFIX" install @@ -94,6 +99,7 @@ cat > "$PREFIX/ghostty-source.json" <&2 + exit 2 +fi + +qemu="$(command -v qemu-x86_64-static || command -v qemu-x86_64 || true)" +if [[ -z "$qemu" ]]; then + echo "error: missing qemu-x86_64-static or qemu-x86_64" >&2 + echo "Install it first, for example: sudo apt-get install qemu-user" >&2 + exit 2 +fi + +# qemu-user emulates only the binary it starts. A version-manager shim (mise, +# nvm, volta) would exec the real node natively and the check would pass on +# any addon, so start the real binary. +node_bin="$(node -p process.execPath)" + +cd "$ROOT" +echo "Running examples/smoke.ts with $node_bin under $qemu -cpu $QEMU_CPU" +if ! "$qemu" -cpu "$QEMU_CPU" "$node_bin" --import tsx examples/smoke.ts; then + echo "error: the addon failed on a $QEMU_CPU CPU; it likely uses instructions newer than the x86-64 baseline" >&2 + echo "Build libghostty-vt with LIBGHOSTTY_VT_CPU=baseline (the default) and rebuild the addon." >&2 + exit 1 +fi From fc5c2a8f654b1a34192c2b453e525015d600b339 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Sun, 1 Nov 2026 20:08:05 +0000 Subject: [PATCH 2/8] docs: link the changelog entry to #55 Change-Id: Ie403aa95e2c132f64c7d5d2712e82e2f3f440dc1 Signed-off-by: Thomas Kosiewski --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33787b5..442717f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable user-facing changes to this package will be documented in this file. ## [Unreleased] ## Fixed -- The `linux-x64` prebuild no longer crashes with `Illegal instruction` (SIGILL) on CPUs without AVX-512. `v0.1.0-beta.4` was built for the release runner's CPU, so loading it and creating a terminal failed on many x86-64 machines and CI runners. libghostty-vt now builds with Zig's `-Dcpu=baseline` (override with `LIBGHOSTTY_VT_CPU`), and CI and the release workflow run the smoke example on an emulated generic x86-64 CPU. +- The `linux-x64` prebuild no longer crashes with `Illegal instruction` (SIGILL) on CPUs without AVX-512. `v0.1.0-beta.4` was built for the release runner's CPU, so loading it and creating a terminal failed on many x86-64 machines and CI runners. libghostty-vt now builds with Zig's `-Dcpu=baseline` (override with `LIBGHOSTTY_VT_CPU`), and CI and the release workflow run the smoke example on an emulated generic x86-64 CPU ([#55](https://github.com/coder/libghostty-vt-node/pull/55) by @ThomasK33). ## [v0.1.0-beta.4](https://github.com/coder/libghostty-vt-node/releases/tag/v0.1.0-beta.4) - 2026-10-30 From b46c4b17425bc950538e51496041356c66bcab36 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Sun, 1 Nov 2026 20:13:05 +0000 Subject: [PATCH 3/8] fix(scripts): tell a host that cannot emulate the CPU from a bad addon Change-Id: I4fcd37fee02f3f0d0711a8eb1f8e5185e5aafaea Signed-off-by: Thomas Kosiewski --- scripts/check-baseline-cpu.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts/check-baseline-cpu.sh b/scripts/check-baseline-cpu.sh index 039b20e..41fcc7c 100755 --- a/scripts/check-baseline-cpu.sh +++ b/scripts/check-baseline-cpu.sh @@ -26,6 +26,15 @@ fi # any addon, so start the real binary. node_bin="$(node -p process.execPath)" +# Control: if Node or the system libraries alone need a newer CPU (Ubuntu's +# amd64v3 variant builds them for x86-64-v3), the host cannot emulate +# QEMU_CPU and a failure says nothing about the addon. +if ! "$qemu" -cpu "$QEMU_CPU" "$node_bin" -e 0; then + echo "error: node without the addon fails on an emulated $QEMU_CPU CPU, so this host cannot run the check with that CPU" >&2 + echo "Pick an older-than-host CPU that node runs on, for example QEMU_CPU=Haswell" >&2 + exit 2 +fi + cd "$ROOT" echo "Running examples/smoke.ts with $node_bin under $qemu -cpu $QEMU_CPU" if ! "$qemu" -cpu "$QEMU_CPU" "$node_bin" --import tsx examples/smoke.ts; then From 1abebc8d4e1d4748f032e282f865d96c6cda4a4b Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Sun, 1 Nov 2026 20:18:06 +0000 Subject: [PATCH 4/8] ci: run the baseline CPU check on an emulated Haswell Node without the addon dies with SIGILL under qemu64 on ubuntu-latest, which installs Ubuntu's amd64v3 packages. Haswell has AVX2 and no AVX-512: the baseline build passes there and the beta.4 prebuild exits 1. Change-Id: Ibba3fc73e3f99a33feabf21d5e011e060bfb2790 Signed-off-by: Thomas Kosiewski --- .github/workflows/ci.yml | 5 +++++ .github/workflows/release-prebuilds.yml | 4 ++++ scripts/README.md | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b7222d..ff2e77c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,3 +45,8 @@ jobs: run: sudo apt-get update && sudo apt-get install -y qemu-user - if: matrix.target == 'linux-x64' run: mise run check-baseline-cpu + # The runner's system libraries come from Ubuntu's amd64v3 variant, + # and Node without the addon dies on qemu64 there. Haswell (x86-64-v3) + # has AVX2 and no AVX-512, which still catches the beta.4 crash. + env: + QEMU_CPU: Haswell diff --git a/.github/workflows/release-prebuilds.yml b/.github/workflows/release-prebuilds.yml index 15647cf..534ffca 100644 --- a/.github/workflows/release-prebuilds.yml +++ b/.github/workflows/release-prebuilds.yml @@ -36,8 +36,12 @@ jobs: run: sudo apt-get update && sudo apt-get install -y qemu-user - if: matrix.target == 'linux-x64' run: mise run check-baseline-cpu + # The runner's system libraries come from Ubuntu's amd64v3 variant, + # and Node without the addon dies on qemu64 there. Haswell (x86-64-v3) + # has AVX2 and no AVX-512, which still catches the beta.4 crash. env: PREBUILDS_ONLY: "1" + QEMU_CPU: Haswell - uses: actions/upload-artifact@v4 with: name: prebuilds-${{ matrix.target }} diff --git a/scripts/README.md b/scripts/README.md index 83bb1f2..a1eff9c 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -9,7 +9,7 @@ | Script | Invoked by | Inputs | Output | | --- | --- | --- | --- | | `build-libghostty-vt.sh` | `npm run build:libghostty` (mise tasks `ci` and `release-prebuilds`) | env `GHOSTTY_REPO`, `GHOSTTY_COMMIT`, `GHOSTTY_SOURCE_DIR` (default `vendor/ghostty`), `LIBGHOSTTY_VT_PREFIX` (default `vendor/libghostty-vt`), `LIBGHOSTTY_VT_SIMD` (default `false`), `LIBGHOSTTY_VT_OPTIMIZE` (default `ReleaseFast`), `LIBGHOSTTY_VT_CPU` (default `baseline`) | progress on stdout, errors on stderr. Exit 0 on success. Its own checks exit 1. Under `set -euo pipefail`, a failed `mkdir` or `git` command exits with that command's status [scripts/build-libghostty-vt.sh:need] | -| `check-baseline-cpu.sh` | mise task `check-baseline-cpu`, run by the `linux-x64` jobs of `.github/workflows/ci.yml` and `.github/workflows/release-prebuilds.yml` (the latter with `PREBUILDS_ONLY=1`) | env `QEMU_CPU` (default `qemu64`), `PREBUILDS_ONLY`. Needs Linux on x86_64 and `qemu-x86_64-static` or `qemu-x86_64` on `PATH` | the smoke output on stdout. Exit 0 when `examples/smoke.ts` passes on the emulated CPU, 1 when it fails, 2 on the wrong platform or without qemu [scripts/check-baseline-cpu.sh:qemu] | +| `check-baseline-cpu.sh` | mise task `check-baseline-cpu`, run by the `linux-x64` jobs of `.github/workflows/ci.yml` and `.github/workflows/release-prebuilds.yml` (the latter with `PREBUILDS_ONLY=1`), both with `QEMU_CPU=Haswell` because Node alone dies on `qemu64` on those runners, which install Ubuntu's amd64v3 packages | env `QEMU_CPU` (default `qemu64`), `PREBUILDS_ONLY`. Needs Linux on x86_64 and `qemu-x86_64-static` or `qemu-x86_64` on `PATH` | the smoke output on stdout. Exit 0 when `examples/smoke.ts` passes on the emulated CPU, 1 when it fails, 2 on the wrong platform, without qemu, or when `node -e 0` alone fails on that CPU [scripts/check-baseline-cpu.sh:qemu] | | `resolve-libghostty-vt.mjs` | `binding.gyp` `include_dirs` and `libraries` | argv `include` or `library`. Env `LIBGHOSTTY_VT_PREFIX` (default `vendor/libghostty-vt`), `LIBGHOSTTY_VT_INCLUDE_DIR` (default `/include`), `LIBGHOSTTY_VT_STATIC_LIB`, then `LIBGHOSTTY_VT_LIB` (default `/lib/libghostty-vt.a`, `ghostty-vt-static.lib` on win32) | the absolute path on stdout, with no trailing newline [scripts/resolve-libghostty-vt.mjs:libraryPath] | | `assemble-prebuilds.mjs` | `npm run assemble:prebuilds` (mise task `assemble-prebuilds`, run by `publish-dry-run` and `publish`) | argv[2] source (default `.release-artifacts`), argv[3] target (default `prebuilds`), env `EXPECTED_PREBUILD_PLATFORMS` (default `darwin-arm64,linux-arm64,linux-x64`) | JSON `{ sourceRoot, targetRoot, platforms }` on stdout [scripts/assemble-prebuilds.mjs:expectedPlatforms] | | `verify-prebuilds.mjs` | `npm run verify:prebuilds` (mise tasks `release-prebuilds`, `publish-dry-run`, and `publish`) | `prebuilds/` and `dist/index.js` (run `npm run build` first) | JSON `{ platforms, addon, nativeInfo }` on stdout, where `addon` is the file it loaded [scripts/verify-prebuilds.mjs:addon] | From 035970330adf0cbb68b465112b95ae76bd111dae Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Sun, 1 Nov 2026 21:20:20 +0000 Subject: [PATCH 5/8] ci: build and test on macos-15 runners GitHub retires the macos-14 image on 2026-11-02 and cancels macos-14 jobs during scheduled brownouts (actions/runner-images#13518). Change-Id: I83ce63b89a76cd4d3c2ab35235edf96b6f539acd Signed-off-by: Thomas Kosiewski --- .github/workflows/ci.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/release-prebuilds.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff2e77c..78f5e55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: os: ubuntu-24.04-arm task: ci - target: macos-arm64 - os: macos-14 + os: macos-15 task: ci - target: linux-x64-ubsan os: ubuntu-latest diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cad4f50..3dece09 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -42,7 +42,7 @@ jobs: - target: linux-arm64 os: ubuntu-24.04-arm - target: macos-arm64 - os: macos-14 + os: macos-15 steps: - uses: actions/checkout@v6 with: diff --git a/.github/workflows/release-prebuilds.yml b/.github/workflows/release-prebuilds.yml index 534ffca..d5bb86a 100644 --- a/.github/workflows/release-prebuilds.yml +++ b/.github/workflows/release-prebuilds.yml @@ -25,7 +25,7 @@ jobs: - target: linux-arm64 os: ubuntu-24.04-arm - target: macos-arm64 - os: macos-14 + os: macos-15 steps: - uses: actions/checkout@v6 - uses: jdx/mise-action@v3 From f59913145241a3981a96736c18d6be099ea79b27 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Sun, 1 Nov 2026 21:21:46 +0000 Subject: [PATCH 6/8] docs: name the emulated CPU in the changelog entry Change-Id: I4fca59a13415733dfc6cde9137fa477708786e5e Signed-off-by: Thomas Kosiewski --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 442717f..940e632 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable user-facing changes to this package will be documented in this file. ## [Unreleased] ## Fixed -- The `linux-x64` prebuild no longer crashes with `Illegal instruction` (SIGILL) on CPUs without AVX-512. `v0.1.0-beta.4` was built for the release runner's CPU, so loading it and creating a terminal failed on many x86-64 machines and CI runners. libghostty-vt now builds with Zig's `-Dcpu=baseline` (override with `LIBGHOSTTY_VT_CPU`), and CI and the release workflow run the smoke example on an emulated generic x86-64 CPU ([#55](https://github.com/coder/libghostty-vt-node/pull/55) by @ThomasK33). +- The `linux-x64` prebuild no longer crashes with `Illegal instruction` (SIGILL) on CPUs without AVX-512. `v0.1.0-beta.4` was built for the release runner's CPU, so loading it and creating a terminal failed on many x86-64 machines and CI runners. libghostty-vt now builds with Zig's `-Dcpu=baseline` (override with `LIBGHOSTTY_VT_CPU`), and CI and the release workflow run the smoke example on an emulated Haswell CPU, which has AVX2 but no AVX-512 ([#55](https://github.com/coder/libghostty-vt-node/pull/55) by @ThomasK33). ## [v0.1.0-beta.4](https://github.com/coder/libghostty-vt-node/releases/tag/v0.1.0-beta.4) - 2026-10-30 From 6f2477f756ac9f053fa4672cd33bd524ff572673 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Sun, 1 Nov 2026 21:29:37 +0000 Subject: [PATCH 7/8] ci: run the baseline CPU check on the prebuilds that publish.yml ships release-prebuilds.yml is manual-dispatch only. publish.yml builds the files npm ships, so check them there before upload. Also ignore qemu-user core dumps that a failing check leaves in the repo root. Change-Id: I9829884cc978c609a408bb5c593aa91348be86d7 Signed-off-by: Thomas Kosiewski --- .github/workflows/publish.yml | 9 +++++++++ .gitignore | 1 + 2 files changed, 10 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3dece09..5aef9f8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -50,6 +50,15 @@ jobs: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.merge_commit_sha || github.ref }} - uses: jdx/mise-action@v3 - run: mise run release-prebuilds + # These are the prebuilds that npm ships, so run them on an emulated CPU + # without AVX-512 before upload. Haswell, not qemu64: see release-prebuilds.yml. + - if: matrix.target == 'linux-x64' + run: sudo apt-get update && sudo apt-get install -y qemu-user + - if: matrix.target == 'linux-x64' + run: mise run check-baseline-cpu + env: + PREBUILDS_ONLY: "1" + QEMU_CPU: Haswell - uses: actions/upload-artifact@v4 with: name: prebuilds-${{ matrix.target }} diff --git a/.gitignore b/.gitignore index 1156395..e22ed4e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ vendor/ .zig-cache/ zig-out/ *.log +qemu_*.core !dogfood/bootstrap/session.log *.tsbuildinfo coverage/ From e99e5fd6cb434ee0f82bac648f3752e2a5718dd8 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Sun, 1 Nov 2026 21:34:26 +0000 Subject: [PATCH 8/8] docs: say LIBGHOSTTY_VT_CPU has no effect on macOS, list publish.yml Ghostty's build replaces a native macOS target with a generic one, so the CPU override only changes Linux builds. Change-Id: I9fa9f9db966b53fb26462176e644fa3936310b99 Signed-off-by: Thomas Kosiewski --- README.md | 2 +- scripts/README.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8351142..6863748 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ npm test npm run smoke ``` -`npm run build:libghostty` fetches the pinned Ghostty commit into `vendor/ghostty` and installs a static `libghostty-vt` into `vendor/libghostty-vt`. The default uses `-Dsimd=false` for a dependency-minimal static library. Set `LIBGHOSTTY_VT_SIMD=true` to build SIMD support when your build environment supports it. The build targets the generic `baseline` CPU of the architecture, so the library runs on older CPUs. Set `LIBGHOSTTY_VT_CPU=native` for a local build tuned to your machine; do not ship it. +`npm run build:libghostty` fetches the pinned Ghostty commit into `vendor/ghostty` and installs a static `libghostty-vt` into `vendor/libghostty-vt`. The default uses `-Dsimd=false` for a dependency-minimal static library. Set `LIBGHOSTTY_VT_SIMD=true` to build SIMD support when your build environment supports it. The build targets the generic `baseline` CPU of the architecture, so the library runs on older CPUs. Set `LIBGHOSTTY_VT_CPU=native` for a local Linux build tuned to your machine; do not ship it. On macOS, Ghostty's build always targets a generic macOS CPU and ignores `LIBGHOSTTY_VT_CPU`. You can also provide an existing install: diff --git a/scripts/README.md b/scripts/README.md index a1eff9c..ea4215b 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -9,7 +9,7 @@ | Script | Invoked by | Inputs | Output | | --- | --- | --- | --- | | `build-libghostty-vt.sh` | `npm run build:libghostty` (mise tasks `ci` and `release-prebuilds`) | env `GHOSTTY_REPO`, `GHOSTTY_COMMIT`, `GHOSTTY_SOURCE_DIR` (default `vendor/ghostty`), `LIBGHOSTTY_VT_PREFIX` (default `vendor/libghostty-vt`), `LIBGHOSTTY_VT_SIMD` (default `false`), `LIBGHOSTTY_VT_OPTIMIZE` (default `ReleaseFast`), `LIBGHOSTTY_VT_CPU` (default `baseline`) | progress on stdout, errors on stderr. Exit 0 on success. Its own checks exit 1. Under `set -euo pipefail`, a failed `mkdir` or `git` command exits with that command's status [scripts/build-libghostty-vt.sh:need] | -| `check-baseline-cpu.sh` | mise task `check-baseline-cpu`, run by the `linux-x64` jobs of `.github/workflows/ci.yml` and `.github/workflows/release-prebuilds.yml` (the latter with `PREBUILDS_ONLY=1`), both with `QEMU_CPU=Haswell` because Node alone dies on `qemu64` on those runners, which install Ubuntu's amd64v3 packages | env `QEMU_CPU` (default `qemu64`), `PREBUILDS_ONLY`. Needs Linux on x86_64 and `qemu-x86_64-static` or `qemu-x86_64` on `PATH` | the smoke output on stdout. Exit 0 when `examples/smoke.ts` passes on the emulated CPU, 1 when it fails, 2 on the wrong platform, without qemu, or when `node -e 0` alone fails on that CPU [scripts/check-baseline-cpu.sh:qemu] | +| `check-baseline-cpu.sh` | mise task `check-baseline-cpu`, run by the `linux-x64` jobs of `.github/workflows/ci.yml`, `.github/workflows/publish.yml` and `.github/workflows/release-prebuilds.yml` (the last two with `PREBUILDS_ONLY=1`), all with `QEMU_CPU=Haswell` because Node alone dies on `qemu64` on those runners, which install Ubuntu's amd64v3 packages | env `QEMU_CPU` (default `qemu64`), `PREBUILDS_ONLY`. Needs Linux on x86_64 and `qemu-x86_64-static` or `qemu-x86_64` on `PATH` | the smoke output on stdout. Exit 0 when `examples/smoke.ts` passes on the emulated CPU, 1 when it fails, 2 on the wrong platform, without qemu, or when `node -e 0` alone fails on that CPU [scripts/check-baseline-cpu.sh:qemu] | | `resolve-libghostty-vt.mjs` | `binding.gyp` `include_dirs` and `libraries` | argv `include` or `library`. Env `LIBGHOSTTY_VT_PREFIX` (default `vendor/libghostty-vt`), `LIBGHOSTTY_VT_INCLUDE_DIR` (default `/include`), `LIBGHOSTTY_VT_STATIC_LIB`, then `LIBGHOSTTY_VT_LIB` (default `/lib/libghostty-vt.a`, `ghostty-vt-static.lib` on win32) | the absolute path on stdout, with no trailing newline [scripts/resolve-libghostty-vt.mjs:libraryPath] | | `assemble-prebuilds.mjs` | `npm run assemble:prebuilds` (mise task `assemble-prebuilds`, run by `publish-dry-run` and `publish`) | argv[2] source (default `.release-artifacts`), argv[3] target (default `prebuilds`), env `EXPECTED_PREBUILD_PLATFORMS` (default `darwin-arm64,linux-arm64,linux-x64`) | JSON `{ sourceRoot, targetRoot, platforms }` on stdout [scripts/assemble-prebuilds.mjs:expectedPlatforms] | | `verify-prebuilds.mjs` | `npm run verify:prebuilds` (mise tasks `release-prebuilds`, `publish-dry-run`, and `publish`) | `prebuilds/` and `dist/index.js` (run `npm run build` first) | JSON `{ platforms, addon, nativeInfo }` on stdout, where `addon` is the file it loaded [scripts/verify-prebuilds.mjs:addon] | @@ -21,7 +21,7 @@ In the `.mjs` scripts, a failed `assert` or a thrown `Error` ends the process wi ## Invariants - The default Ghostty commit is `83fb95624b0439ee660c2679b43b38c8b63769cd`. `GHOSTTY_COMMIT` overrides it [scripts/build-libghostty-vt.sh:GHOSTTY_COMMIT]. -- `build-libghostty-vt.sh` passes `-Dcpu=$LIBGHOSTTY_VT_CPU` (default `baseline`), so the library runs on any CPU of its architecture. Without it, Zig targets the build machine, and the `v0.1.0-beta.4` `linux-x64` prebuild, built on a runner with AVX-512, died with SIGILL on CPUs without AVX-512 [scripts/build-libghostty-vt.sh:CPU]. +- `build-libghostty-vt.sh` passes `-Dcpu=$LIBGHOSTTY_VT_CPU` (default `baseline`), so the library runs on any CPU of its architecture. On Darwin, Ghostty's build replaces the target with a generic macOS target, so the value has no effect there. Without it, Zig targets the build machine, and the `v0.1.0-beta.4` `linux-x64` prebuild, built on a runner with AVX-512, died with SIGILL on CPUs without AVX-512 [scripts/build-libghostty-vt.sh:CPU]. - `build-libghostty-vt.sh` exits 1 with `"error: missing required command ''"` and `"Install it first, then rerun npm run build:libghostty."` when `git` or `zig` is not on `PATH`. For `zig` it also prints `"This Ghostty commit requires Zig 0.17.0. With mise: mise install"` [scripts/build-libghostty-vt.sh:need]. - `build-libghostty-vt.sh` exits 1 when `zig version` differs from `minimum_zig_version` in Ghostty's `build.zig.zon`. It skips this check when that file names no version [scripts/build-libghostty-vt.sh:required_zig]. - `build-libghostty-vt.sh` exits 1 with `"error: expected exactly one 'const lib_version = \"\";' line in /build.zig, with no build metadata"` when Ghostty's `build.zig` has no such line, has more than one, or the value is not `MAJOR.MINOR.PATCH[-PRE]`. It checks this before it runs `zig` [scripts/build-libghostty-vt.sh:lib_version]. @@ -41,7 +41,7 @@ In the `.mjs` scripts, a failed `assert` or a thrown `Error` ends the process wi - `build-libghostty-vt.sh` creates the parent of `GHOSTTY_SOURCE_DIR` and the `PREFIX` directory with `mkdir -p` [scripts/build-libghostty-vt.sh:PREFIX]. - Network: when `$GHOSTTY_SOURCE_DIR/.git` is missing, `build-libghostty-vt.sh` runs `git clone $GHOSTTY_REPO` (default `https://github.com/ghostty-org/ghostty.git`). Every run then runs `git fetch --depth 1 origin ` and `git checkout --detach ` in that directory, so every run needs the network [scripts/build-libghostty-vt.sh:GHOSTTY_REPO]. -- `build-libghostty-vt.sh` runs `zig build ... install` into `PREFIX` [scripts/build-libghostty-vt.sh:zig_args] and writes `$PREFIX/ghostty-source.json` with the repository, commit, Zig version, SIMD, optimize, and CPU values. `commit` is the same resolved full commit that the build passed to `zig`, not the raw `GHOSTTY_COMMIT` value [scripts/build-libghostty-vt.sh:ghostty-source.json]. +- `build-libghostty-vt.sh` runs `zig build ... install` into `PREFIX` [scripts/build-libghostty-vt.sh:zig_args] and writes `$PREFIX/ghostty-source.json` with the repository, commit, Zig version, SIMD, optimize, and requested CPU values (on Darwin the build ignores the CPU value). `commit` is the same resolved full commit that the build passed to `zig`, not the raw `GHOSTTY_COMMIT` value [scripts/build-libghostty-vt.sh:ghostty-source.json]. - `assemble-prebuilds.mjs` copies each `/prebuilds-*/` directory into a staging directory next to the target (`.staging-`, default `prebuilds.staging-`) with `errorOnExist: true`, and runs every check there. Only then does it move the old target to `.previous-`, rename the staging directory to the target, and delete the old one. A failed run deletes the staging directory and leaves the target as it was [scripts/assemble-prebuilds.mjs:stagingRoot]. - `verify-prebuilds.mjs` imports `../dist/index.js` and calls `getNativeInfo()`, which loads the native addon [scripts/verify-prebuilds.mjs:info]. - Network and remote state: `create-release-tag.mjs` runs `git ls-remote --tags origin`. When the tag is absent, it runs `git tag v ` and `git push origin refs/tags/v` [scripts/create-release-tag.mjs:git].