diff --git a/.changeset/baseline-cpu-fallback.md b/.changeset/baseline-cpu-fallback.md new file mode 100644 index 0000000..a461691 --- /dev/null +++ b/.changeset/baseline-cpu-fallback.md @@ -0,0 +1,5 @@ +--- +"@bunny.net/cli": patch +--- + +fix(install): ship a baseline (non-AVX2) build for older x64 CPUs that crashed with "Illegal instruction" — the installer auto-selects it, and the npm wrapper falls back to it on SIGILL diff --git a/.changeset/bsql-baseline-fallback.md b/.changeset/bsql-baseline-fallback.md new file mode 100644 index 0000000..d4e0a94 --- /dev/null +++ b/.changeset/bsql-baseline-fallback.md @@ -0,0 +1,5 @@ +--- +"@bunny.net/database-shell": patch +--- + +fix(bsql): fall back to a baseline (non-AVX2) binary on older x64 CPUs that crashed with "Illegal instruction" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6bb519d..ba04cde 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -128,6 +128,43 @@ jobs: path: packages/${{ matrix.npm-pkg }}/ if-no-files-found: error + build-cli-baseline: + name: Build ${{ matrix.binary }} + needs: version + if: needs.version.outputs.cli-version + strategy: + matrix: + include: + - target: bun-linux-x64-baseline + os: ubuntu-latest + binary: bunny-linux-x64-baseline + - target: bun-darwin-x64-baseline + os: macos-latest + binary: bunny-darwin-x64-baseline + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v5 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3.11" + - run: bun install + + - name: Build database studio + run: bun run --filter @bunny.net/database-studio build:client + + # Baseline targets avoid AVX2, so the binary runs on pre-Haswell x64 CPUs that crash the default build with SIGILL. + - name: Build binary + run: bun build packages/cli/src/index.ts --compile --minify --sourcemap --target=${{ matrix.target }} --outfile ${{ matrix.binary }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.binary }} + path: ${{ matrix.binary }} + if-no-files-found: error + build-database-shell-binaries: name: Build ${{ matrix.npm-pkg }} needs: version @@ -178,12 +215,47 @@ jobs: path: packages/${{ matrix.npm-pkg }}/ if-no-files-found: error + build-database-shell-baseline: + name: Build ${{ matrix.binary }} + needs: version + if: needs.version.outputs.database-shell-version + strategy: + matrix: + include: + - target: bun-linux-x64-baseline + os: ubuntu-latest + binary: bsql-linux-x64-baseline + - target: bun-darwin-x64-baseline + os: macos-latest + binary: bsql-darwin-x64-baseline + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v5 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3.11" + - run: bun install + + # Baseline targets avoid AVX2, so the binary runs on pre-Haswell x64 CPUs that crash the default build with SIGILL. + - name: Build binary + run: bun build packages/database-shell/src/cli.ts --compile --minify --sourcemap --target=${{ matrix.target }} --outfile ${{ matrix.binary }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.binary }} + path: ${{ matrix.binary }} + if-no-files-found: error + publish-cli: name: Publish CLI runs-on: ubuntu-latest needs: - version - build-cli-binaries + - build-cli-baseline steps: - uses: actions/checkout@v5 @@ -203,12 +275,20 @@ jobs: cp -r npm-artifacts/$pkg/* packages/$pkg/ done + # x64 packages also ship a baseline binary the wrapper falls back to on AVX2-less CPUs. + - name: Copy baseline binaries into x64 npm packages + run: | + cp npm-artifacts/bunny-linux-x64-baseline/bunny-linux-x64-baseline packages/cli-linux-x64/bunny-baseline + cp npm-artifacts/bunny-darwin-x64-baseline/bunny-darwin-x64-baseline packages/cli-darwin-x64/bunny-baseline + - name: Set execute permissions run: | chmod +x packages/cli-linux-x64/bunny chmod +x packages/cli-linux-arm64/bunny chmod +x packages/cli-darwin-x64/bunny chmod +x packages/cli-darwin-arm64/bunny + chmod +x packages/cli-linux-x64/bunny-baseline + chmod +x packages/cli-darwin-x64/bunny-baseline - name: Publish platform packages run: | @@ -247,6 +327,8 @@ jobs: npm-artifacts/cli-darwin-x64/bunny-darwin-x64 npm-artifacts/cli-darwin-arm64/bunny-darwin-arm64 npm-artifacts/cli-windows-x64/bunny-windows-x64.exe + npm-artifacts/bunny-linux-x64-baseline/bunny-linux-x64-baseline + npm-artifacts/bunny-darwin-x64-baseline/bunny-darwin-x64-baseline fail_on_unmatched_files: true publish-database-shell: @@ -255,6 +337,7 @@ jobs: needs: - version - build-database-shell-binaries + - build-database-shell-baseline steps: - uses: actions/checkout@v5 @@ -274,12 +357,20 @@ jobs: cp -r npm-artifacts/$pkg/* packages/$pkg/ done + # x64 packages also ship a baseline binary the wrapper falls back to on AVX2-less CPUs. + - name: Copy baseline binaries into x64 npm packages + run: | + cp npm-artifacts/bsql-linux-x64-baseline/bsql-linux-x64-baseline packages/database-shell-linux-x64/bsql-baseline + cp npm-artifacts/bsql-darwin-x64-baseline/bsql-darwin-x64-baseline packages/database-shell-darwin-x64/bsql-baseline + - name: Set execute permissions run: | chmod +x packages/database-shell-linux-x64/bsql chmod +x packages/database-shell-linux-arm64/bsql chmod +x packages/database-shell-darwin-x64/bsql chmod +x packages/database-shell-darwin-arm64/bsql + chmod +x packages/database-shell-linux-x64/bsql-baseline + chmod +x packages/database-shell-darwin-x64/bsql-baseline - name: Publish platform packages run: | diff --git a/install.sh b/install.sh index 3f41ebe..033d64d 100755 --- a/install.sh +++ b/install.sh @@ -21,6 +21,14 @@ get_arch() { esac } +has_avx2() { + case "$OS" in + linux) grep -qw avx2 /proc/cpuinfo 2>/dev/null ;; + darwin) sysctl -n machdep.cpu.leaf7_features 2>/dev/null | grep -qiw avx2 ;; + *) return 1 ;; + esac +} + OS=$(get_os) ARCH=$(get_arch) @@ -30,7 +38,14 @@ if [ "$OS" = "unsupported" ] || [ "$ARCH" = "unsupported" ]; then exit 1 fi -BINARY="bunny-${OS}-${ARCH}" +# The default Bun-compiled binary uses AVX2; pre-Haswell (~2013) x64 CPUs lack it and crash with "Illegal instruction", so fall back to the baseline build. +VARIANT="" +if [ "$ARCH" = "x64" ] && ! has_avx2; then + VARIANT="-baseline" + echo "AVX2 not detected; using baseline build." +fi + +BINARY="bunny-${OS}-${ARCH}${VARIANT}" # Pinned version uses the tagged release URL; otherwise use the `latest` # redirect so we don't hit api.github.com (rate-limited to 60 req/hr). diff --git a/packages/cli-darwin-x64/package.json b/packages/cli-darwin-x64/package.json index aceba7d..8a65b3f 100644 --- a/packages/cli-darwin-x64/package.json +++ b/packages/cli-darwin-x64/package.json @@ -9,7 +9,8 @@ "x64" ], "files": [ - "bunny" + "bunny", + "bunny-baseline" ], "publishConfig": { "access": "public" diff --git a/packages/cli-linux-x64/package.json b/packages/cli-linux-x64/package.json index 04d7f67..cb0b10b 100644 --- a/packages/cli-linux-x64/package.json +++ b/packages/cli-linux-x64/package.json @@ -9,7 +9,8 @@ "x64" ], "files": [ - "bunny" + "bunny", + "bunny-baseline" ], "publishConfig": { "access": "public" diff --git a/packages/cli/bin/bunny.cjs b/packages/cli/bin/bunny.cjs index 6e53b3a..3b70a86 100755 --- a/packages/cli/bin/bunny.cjs +++ b/packages/cli/bin/bunny.cjs @@ -1,9 +1,30 @@ #!/usr/bin/env node const { execFileSync } = require("node:child_process"); -const { existsSync } = require("node:fs"); +const { existsSync, readFileSync } = require("node:fs"); const path = require("node:path"); +// Only x64 binaries use AVX2; assume it's present on other arches or when detection fails, letting the SIGILL fallback catch any mistake. +function hasAvx2() { + if (process.arch !== "x64") return true; + try { + if (process.platform === "linux") { + return readFileSync("/proc/cpuinfo", "utf8").includes("avx2"); + } + if (process.platform === "darwin") { + const features = execFileSync( + "sysctl", + ["-n", "machdep.cpu.leaf7_features"], + { encoding: "utf8" }, + ); + return /avx2/i.test(features); + } + } catch { + return true; + } + return true; +} + const PLATFORMS = { "darwin-arm64": "@bunny.net/cli-darwin-arm64", "darwin-x64": "@bunny.net/cli-darwin-x64", @@ -45,12 +66,35 @@ if (!existsSync(binPath)) { process.exit(1); } -try { - execFileSync(binPath, process.argv.slice(2), { stdio: "inherit" }); -} catch (err) { - if (err.status != null) { - process.exit(err.status); +// The default binary uses AVX2; on pre-Haswell x64 CPUs it dies with SIGILL, so fall back to the baseline build shipped alongside it. +const baselinePath = path.join(path.dirname(binPath), "bunny-baseline"); +const candidates = [binPath]; +if (existsSync(baselinePath)) { + candidates.push(baselinePath); +} +// Run the baseline first on x64 CPUs without AVX2, so we don't spawn the default binary just to catch its crash on every invocation. +if (candidates.length > 1 && !hasAvx2()) { + candidates.reverse(); +} + +for (let i = 0; i < candidates.length; i++) { + try { + execFileSync(candidates[i], process.argv.slice(2), { stdio: "inherit" }); + process.exit(0); + } catch (err) { + if (err.status != null) { + process.exit(err.status); + } + if (err.signal === "SIGILL" && i < candidates.length - 1) { + continue; + } + if (err.signal === "SIGILL") { + console.error( + "bunny crashed with an illegal instruction; this CPU is not supported.", + ); + process.exit(1); + } + console.error(`Failed to execute bunny binary: ${err.message}`); + process.exit(1); } - console.error(`Failed to execute bunny binary: ${err.message}`); - process.exit(1); } diff --git a/packages/database-shell-darwin-x64/package.json b/packages/database-shell-darwin-x64/package.json index d9cc641..7c2f799 100644 --- a/packages/database-shell-darwin-x64/package.json +++ b/packages/database-shell-darwin-x64/package.json @@ -9,7 +9,8 @@ "x64" ], "files": [ - "bsql" + "bsql", + "bsql-baseline" ], "publishConfig": { "access": "public" diff --git a/packages/database-shell-linux-x64/package.json b/packages/database-shell-linux-x64/package.json index 5a7a06b..6b2be30 100644 --- a/packages/database-shell-linux-x64/package.json +++ b/packages/database-shell-linux-x64/package.json @@ -9,7 +9,8 @@ "x64" ], "files": [ - "bsql" + "bsql", + "bsql-baseline" ], "publishConfig": { "access": "public" diff --git a/packages/database-shell/bin/bsql.cjs b/packages/database-shell/bin/bsql.cjs index 93c8db8..cddd724 100755 --- a/packages/database-shell/bin/bsql.cjs +++ b/packages/database-shell/bin/bsql.cjs @@ -1,9 +1,30 @@ #!/usr/bin/env node const { execFileSync } = require("node:child_process"); -const { existsSync } = require("node:fs"); +const { existsSync, readFileSync } = require("node:fs"); const path = require("node:path"); +// Only x64 binaries use AVX2; assume it's present on other arches or when detection fails, letting the SIGILL fallback catch any mistake. +function hasAvx2() { + if (process.arch !== "x64") return true; + try { + if (process.platform === "linux") { + return readFileSync("/proc/cpuinfo", "utf8").includes("avx2"); + } + if (process.platform === "darwin") { + const features = execFileSync( + "sysctl", + ["-n", "machdep.cpu.leaf7_features"], + { encoding: "utf8" }, + ); + return /avx2/i.test(features); + } + } catch { + return true; + } + return true; +} + const PLATFORMS = { "darwin-arm64": "@bunny.net/database-shell-darwin-arm64", "darwin-x64": "@bunny.net/database-shell-darwin-x64", @@ -45,12 +66,35 @@ if (!existsSync(binPath)) { process.exit(1); } -try { - execFileSync(binPath, process.argv.slice(2), { stdio: "inherit" }); -} catch (err) { - if (err.status != null) { - process.exit(err.status); +// The default binary uses AVX2; on pre-Haswell x64 CPUs it dies with SIGILL, so fall back to the baseline build shipped alongside it. +const baselinePath = path.join(path.dirname(binPath), "bsql-baseline"); +const candidates = [binPath]; +if (existsSync(baselinePath)) { + candidates.push(baselinePath); +} +// Run the baseline first on x64 CPUs without AVX2, so we don't spawn the default binary just to catch its crash on every invocation. +if (candidates.length > 1 && !hasAvx2()) { + candidates.reverse(); +} + +for (let i = 0; i < candidates.length; i++) { + try { + execFileSync(candidates[i], process.argv.slice(2), { stdio: "inherit" }); + process.exit(0); + } catch (err) { + if (err.status != null) { + process.exit(err.status); + } + if (err.signal === "SIGILL" && i < candidates.length - 1) { + continue; + } + if (err.signal === "SIGILL") { + console.error( + "bsql crashed with an illegal instruction; this CPU is not supported.", + ); + process.exit(1); + } + console.error(`Failed to execute bsql binary: ${err.message}`); + process.exit(1); } - console.error(`Failed to execute bsql binary: ${err.message}`); - process.exit(1); }