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
5 changes: 5 additions & 0 deletions .changeset/x64-baseline-binaries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Build the x64 binaries for CPUs without AVX2, so Hunk no longer dies with an illegal instruction on pre-Haswell machines and conservative VM CPU models.
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,15 @@ jobs:
- name: Build binary
run: bun run build:bin

# Bun's default x64 runtime is compiled for Haswell and raises SIGILL on older CPUs.
# Emulating a pre-AVX CPU here catches a dropped baseline target on main rather than
# after a release ships a binary that cannot start.
- name: Verify the binary starts without AVX2
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends qemu-user-static
qemu-x86_64-static -cpu Nehalem dist/hunk --version

- name: Verify compiled binary watch mode
run: bun test ./test/pty/watch.test.ts
env:
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/release-prebuilt-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ jobs:
bun run build:bin
bun run ./scripts/build-prebuilt-artifact.ts --expect-package "${{ matrix.package_name }}"

# Bun's default x64 runtime is compiled for Haswell and raises SIGILL on older CPUs.
# Run the real artifact under an emulated pre-AVX CPU so a dropped baseline target
# fails the release instead of shipping a binary that cannot start.
- name: Verify the x64 artifact starts without AVX2
if: matrix.package_name == 'hunkdiff-linux-x64'
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends qemu-user-static
qemu-x86_64-static -cpu Nehalem dist/hunk --version

- name: Upload binary artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ mise use -g hunk
Requirements:

- macOS, Linux, or Windows
- On x86-64, a CPU with SSE4.2 (Intel Nehalem 2008+, AMD Bulldozer 2011+); arm64 has no CPU feature floor
- Node.js 18+ for the npm install; the install script, Homebrew, mise, and Nix ship a standalone binary
- Git recommended for most workflows

Expand Down
25 changes: 25 additions & 0 deletions scripts/build-bin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, test } from "bun:test";
import { compileTargetForHost } from "./build-bin";

describe("compileTargetForHost", () => {
test("compiles every x64 platform against Bun's baseline runtime", () => {
Comment thread
elucid marked this conversation as resolved.
// Bun's default x64 runtime needs AVX2/BMI2; these targets only need x86-64-v2.
expect(compileTargetForHost("darwin", "x64")).toBe("bun-darwin-x64-baseline");
expect(compileTargetForHost("win32", "x64")).toBe("bun-windows-x64-baseline");
expect(compileTargetForHost("linux", "x64", () => false)).toBe("bun-linux-x64-baseline");
});

test("keeps the host libc when compiling on a musl x64 host", () => {
expect(compileTargetForHost("linux", "x64", () => true)).toBe("bun-linux-x64-musl-baseline");
});

test("leaves arm64 hosts on Bun's own default runtime", () => {
expect(compileTargetForHost("darwin", "arm64")).toBeNull();
expect(compileTargetForHost("linux", "arm64")).toBeNull();
expect(compileTargetForHost("win32", "arm64")).toBeNull();
});

test("returns no target for platforms Hunk does not publish binaries for", () => {
expect(compileTargetForHost("freebsd", "x64")).toBeNull();
});
});
116 changes: 80 additions & 36 deletions scripts/build-bin.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,87 @@
#!/usr/bin/env bun

import { mkdirSync, rmSync } from "node:fs";
import { existsSync, mkdirSync, rmSync } from "node:fs";
import path from "node:path";

const repoRoot = path.resolve(import.meta.dir, "..");
const distDir = path.join(repoRoot, "dist");
const binaryName = process.platform === "win32" ? "hunk.exe" : "hunk";
const outfile = path.join(distDir, binaryName);
const legacyOutfile = path.join(distDir, process.platform === "win32" ? "otdiff.exe" : "otdiff");

mkdirSync(distDir, { recursive: true });
rmSync(legacyOutfile, { force: true });

const proc = Bun.spawnSync(
[
"bun",
"build",
"--compile",
"--no-compile-autoload-bunfig",
path.join(repoRoot, "src", "main.tsx"),
path.join(repoRoot, "src", "highlightWorkerEntry.ts"),
"--outfile",
outfile,
],
{
cwd: repoRoot,
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
env: {
...process.env,
BUN_TMPDIR: path.join(repoRoot, ".bun-tmp"),
BUN_INSTALL: path.join(repoRoot, ".bun-install"),
},
},
);
/**
* Resolves the Bun compile target for one host, or null to keep Bun's own host default.
*
* x64 hosts compile against Bun's baseline runtime. Bun's default x64 runtime is built for
* Haswell (AVX2/BMI2, 2013+) and dies with SIGILL before any Hunk code runs on older CPUs and
* on VMs that expose a conservative CPU model, so the shipped binary would be unusable there.
* The baseline runtime only asks for x86-64-v2 (SSE4.2/POPCNT). arm64 has no such split and
* keeps whatever runtime the host Bun already carries.
*/
export function compileTargetForHost(
platform: NodeJS.Platform,
arch: string,
isMuslHost = () => existsSync("/lib/ld-musl-x86_64.so.1"),
) {
if (arch !== "x64") {
return null;
}

if (platform === "darwin") {
return "bun-darwin-x64-baseline";
}

if (platform === "win32") {
return "bun-windows-x64-baseline";
}

if (proc.exitCode !== 0) {
throw new Error(`bun build --compile failed with exit ${proc.exitCode}`);
if (platform === "linux") {
// The musl and glibc runtimes are not interchangeable, so keep the host's libc.
return isMuslHost() ? "bun-linux-x64-musl-baseline" : "bun-linux-x64-baseline";
}

return null;
}

console.log(`Built ${outfile}`);
if (import.meta.main) {
const repoRoot = path.resolve(import.meta.dir, "..");
const distDir = path.join(repoRoot, "dist");
const binaryName = process.platform === "win32" ? "hunk.exe" : "hunk";
const outfile = path.join(distDir, binaryName);
const legacyOutfile = path.join(distDir, process.platform === "win32" ? "otdiff.exe" : "otdiff");

mkdirSync(distDir, { recursive: true });
rmSync(legacyOutfile, { force: true });

const target = compileTargetForHost(process.platform, process.arch);

const proc = Bun.spawnSync(
[
"bun",
"build",
"--compile",
"--no-compile-autoload-bunfig",
...(target ? [`--target=${target}`] : []),
path.join(repoRoot, "src", "main.tsx"),
path.join(repoRoot, "src", "highlightWorkerEntry.ts"),
"--outfile",
outfile,
],
{
cwd: repoRoot,
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
env: {
...process.env,
BUN_TMPDIR: path.join(repoRoot, ".bun-tmp"),
BUN_INSTALL: path.join(repoRoot, ".bun-install"),
},
},
);

if (proc.exitCode !== 0) {
// Bun fetches a non-host target runtime instead of reusing the installed one, so the first
// build on a machine needs network access; after that it comes from the repo-local cache.
const offlineHint = target
? ` Building for ${target} downloads that runtime once into .bun-install; rerun with network access if the download failed.`
: "";
throw new Error(`bun build --compile failed with exit ${proc.exitCode}.${offlineHint}`);
}

console.log(`Built ${outfile}${target ? ` for ${target}` : ""}`);
}
Loading