diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 582ee37a6..df0ff36e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,10 +128,11 @@ jobs: bun-version-file: package.json - run: bun install --frozen-lockfile - run: bun run test - # build:bundle skips the postbuild hook, which shells out to bash to - # prepend a shebang and chmod dist/cli.js. Windows needs neither to run - # `node dist/cli.js`. - - run: bun run build:bundle + # `build` (not build:bundle) so the postbuild hook runs here: only a + # Windows runner proves the hook stays executable by bun's script + # runner, which cannot shell out to .sh files (release v1.1.0–v1.4.2 + # Windows binaries all failed on exactly that). + - run: bun run build # Bundling resolves the `~/` alias Node cannot. Only spawn.ts needs it; # the production-wiring check below runs the shipped bundle instead. - name: Bundle defaultSpawn for smoke diff --git a/package.json b/package.json index 857222b8e..81e3c1844 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "knip": "knip", "lint": "oxlint", "lint:fix": "oxlint --fix", - "postbuild": "./scripts/postbuild.sh", + "postbuild": "bun scripts/postbuild.ts", "prepare": "git config core.hooksPath .githooks || true", "test": "bun run generate && bun test", "test:watch": "bun test --watch", diff --git a/scripts/postbuild.sh b/scripts/postbuild.sh deleted file mode 100755 index be404738c..000000000 --- a/scripts/postbuild.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -# Runs after `bun run build` to make dist/cli.js executable as a CLI. -# Bun's bundler doesn't add a shebang, so we prepend one manually. - -set -euo pipefail - -OUTFILE="dist/cli.js" - -# Prepend the Node.js shebang so the OS knows how to execute the file -printf '#!/usr/bin/env node\n' | cat - "$OUTFILE" > "$OUTFILE.tmp" -mv "$OUTFILE.tmp" "$OUTFILE" - -# Set the executable bit so it can run directly (required by npm link / bin) -chmod +x "$OUTFILE" diff --git a/scripts/postbuild.ts b/scripts/postbuild.ts new file mode 100644 index 000000000..218a1deaa --- /dev/null +++ b/scripts/postbuild.ts @@ -0,0 +1,23 @@ +#!/usr/bin/env bun +// Runs after `bun run build` to make dist/cli.js executable as a CLI. +// Bun's bundler doesn't add a shebang, so we prepend one manually. +// TypeScript rather than shell so the hook also runs on Windows, where +// bun's script runner cannot execute .sh files (no shebang mechanism). +import { chmodSync, readFileSync, statSync, writeFileSync } from "node:fs"; + +const outfile = "dist/cli.js"; + +// Prepend the Node.js shebang so the OS knows how to execute the file. +// Skip when one is already present: a repeat run without a rebuild would +// otherwise add a second shebang line, which is a syntax error to Node. +const bundle = readFileSync(outfile); +if (bundle.subarray(0, 2).toString("ascii") !== "#!") { + writeFileSync( + outfile, + Buffer.concat([Buffer.from("#!/usr/bin/env node\n"), bundle]), + ); +} + +// Set the executable bit so it can run directly (required by npm link / bin); +// mode | 0o111 mirrors `chmod +x`. No-op on Windows, same as in bash. +chmodSync(outfile, statSync(outfile).mode | 0o111);