Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,3 +39,14 @@ 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
# 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
11 changes: 10 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,23 @@ jobs:
- target: linux-arm64
os: ubuntu-24.04-arm
- target: macos-arm64
os: macos-14
os: macos-15
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
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 }}
Expand Down
14 changes: 13 additions & 1 deletion .github/workflows/release-prebuilds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,23 @@ 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
- 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
Comment thread
ThomasK33 marked this conversation as resolved.
Resolved
# 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 }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ vendor/
.zig-cache/
zig-out/
*.log
qemu_*.core
!dogfood/bootstrap/session.log
*.tsbuildinfo
coverage/
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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

## Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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:

Expand Down
4 changes: 4 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 6 additions & 3 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`, `.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 `<prefix>/include`), `LIBGHOSTTY_VT_STATIC_LIB`, then `LIBGHOSTTY_VT_LIB` (default `<prefix>/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] |
Expand All @@ -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. 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 '<cmd>'"` 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 = \"<semver>\";' line in <dir>/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=<lib_version>+<commit> --prefix $PREFIX install`, with `--sysroot <xcrun --show-sdk-path>` on Darwin when `xcrun` returns a path. `<commit>` is the full commit that `git rev-parse --verify HEAD` reports after the checkout. libghostty-vt reports `<lib_version>+<commit>` as its version string and `<commit>` 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=<lib_version>+<commit> --prefix $PREFIX install`, with `--sysroot <xcrun --show-sdk-path>` on Darwin when `xcrun` returns a path. `<commit>` is the full commit that `git rev-parse --verify HEAD` reports after the checkout. libghostty-vt reports `<lib_version>+<commit>` as its version string and `<commit>` 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 <include|library>"` for any other argument [scripts/resolve-libghostty-vt.mjs:command]. It exits 1 when `<includeDir>/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].
Expand All @@ -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 <commit>` and `git checkout --detach <commit>` 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 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 `<source>/prebuilds-*/<platform>` directory into a staging directory next to the target (`<target>.staging-<pid>`, default `prebuilds.staging-<pid>`) with `errorOnExist: true`, and runs every check there. Only then does it move the old target to `<target>.previous-<pid>`, 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<version> <sha>` and `git push origin refs/tags/v<version>` [scripts/create-release-tag.mjs:git].
Expand All @@ -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<version>` tag and pushes it to `origin`.
6 changes: 6 additions & 0 deletions scripts/build-libghostty-vt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -54,6 +58,7 @@ zig_args=(
-Demit-lib-vt=true
"-Dsimd=$SIMD"
"-Doptimize=$OPTIMIZE"
"-Dcpu=$CPU"
Comment thread
ThomasK33 marked this conversation as resolved.
Resolved
"-Dlib-version-string=${lib_version}+${ghostty_commit}"
--prefix "$PREFIX"
install
Expand Down Expand Up @@ -94,6 +99,7 @@ cat > "$PREFIX/ghostty-source.json" <<JSON
"commit": "$ghostty_commit",
"zigVersion": "$zig_version",
"simd": "$SIMD",
"cpu": "$CPU",
"optimize": "$OPTIMIZE"
}
JSON
Expand Down
44 changes: 44 additions & 0 deletions scripts/check-baseline-cpu.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail

# Runs examples/smoke.ts in one Node process under qemu-user with a generic
# x86-64 CPU, so an addon that executes instructions newer than the x86-64
# baseline (such as AVX-512) fails here instead of on users' machines.
# Set PREBUILDS_ONLY=1 to check prebuilds/ instead of build/Release.

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
QEMU_CPU="${QEMU_CPU:-qemu64}"

if [[ "$(uname -s)" != "Linux" || "$(uname -m)" != "x86_64" ]]; then
echo "error: check-baseline-cpu needs Linux on x86_64, found $(uname -s) $(uname -m)" >&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)"

# 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
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
Loading