fix(npm): remove shebang from placeholder scripts to fix npm i -g bun on Windows (#26517)

## Summary
- Removes the `#!/bin/sh` shebang from placeholder `bin/bun.exe` and
`bin/bunx.exe` scripts in the npm package
- Fixes `npm i -g bun` being completely broken on Windows since v1.3.7

## Problem
PR #26259 added a `#!/bin/sh` shebang to the placeholder scripts to show
a helpful error when postinstall hasn't run. However, npm's `cmd-shim`
reads shebangs to generate `.ps1`/`.cmd` wrappers **before** postinstall
runs, and bakes the interpreter path into them. On Windows, the wrappers
referenced `/bin/sh` which doesn't exist, causing:

```
& "/bin/sh$exe"  "$basedir/node_modules/bun/bin/bun.exe" $args
   ~~~~~~~~~~~~~
The term '/bin/sh.exe' is not recognized...
```

Even after postinstall successfully replaced the placeholder with the
real binary, the stale wrappers still tried to invoke `/bin/sh`.

## Fix
Remove the shebang. Without it, `cmd-shim` generates a direct invocation
wrapper that works after postinstall replaces the placeholder. On Unix,
bash/zsh still execute shebang-less files as shell scripts via ENOEXEC
fallback, so the helpful error message is preserved.

## Test plan
- [x] `bun bd test test/regression/issue/24329.test.ts` passes (2/2
tests)
- Manually verify `npm i -g bun` works on Windows

Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Dylan Conway
2026-01-28 00:00:50 -08:00
committed by GitHub
parent 4cd3b241bc
commit 7ebfdf97a8
2 changed files with 12 additions and 7 deletions

View File

@@ -73,9 +73,11 @@ async function buildRootModule(dryRun?: boolean) {
}); });
// Create placeholder scripts that print an error message if postinstall hasn't run. // Create placeholder scripts that print an error message if postinstall hasn't run.
// On Unix, these are executed as shell scripts despite the .exe extension. // On Unix, these are executed as shell scripts despite the .exe extension.
// On Windows, npm creates .cmd wrappers that would fail anyway if the binary isn't valid. // Do NOT add a shebang (#!/bin/sh) here — npm's cmd-shim reads shebangs to generate
const placeholderScript = `#!/bin/sh // .ps1/.cmd wrappers BEFORE postinstall runs, and bakes the interpreter path in.
echo "Error: Bun's postinstall script was not run." >&2 // A #!/bin/sh shebang breaks Windows because the wrappers reference /bin/sh which
// doesn't exist, even after postinstall replaces the placeholder with the real binary.
const placeholderScript = `echo "Error: Bun's postinstall script was not run." >&2
echo "" >&2 echo "" >&2
echo "This occurs when using --ignore-scripts during installation, or when using a" >&2 echo "This occurs when using --ignore-scripts during installation, or when using a" >&2
echo "package manager like pnpm that does not run postinstall scripts by default." >&2 echo "package manager like pnpm that does not run postinstall scripts by default." >&2

View File

@@ -13,8 +13,9 @@ test("bun npm placeholder script should exit with error if postinstall hasn't ru
// This is the placeholder script content that gets written to bin/bun.exe // This is the placeholder script content that gets written to bin/bun.exe
// during npm package build (see packages/bun-release/scripts/upload-npm.ts) // during npm package build (see packages/bun-release/scripts/upload-npm.ts)
const placeholderScript = `#!/bin/sh // Note: no shebang — a #!/bin/sh shebang breaks Windows because npm's cmd-shim
echo "Error: Bun's postinstall script was not run." >&2 // bakes the interpreter path into .ps1/.cmd wrappers before postinstall runs.
const placeholderScript = `echo "Error: Bun's postinstall script was not run." >&2
echo "" >&2 echo "" >&2
echo "This occurs when using --ignore-scripts during installation, or when using a" >&2 echo "This occurs when using --ignore-scripts during installation, or when using a" >&2
echo "package manager like pnpm that does not run postinstall scripts by default." >&2 echo "package manager like pnpm that does not run postinstall scripts by default." >&2
@@ -38,9 +39,11 @@ exit 1
}); });
expect(chmodExitCode).toBe(0); expect(chmodExitCode).toBe(0);
// Run the placeholder script // Run via sh explicitly — in real usage, bash/zsh automatically fall back to sh
// interpretation when execve returns ENOEXEC for a shebang-less executable file.
// Bun.spawn doesn't have that fallback, so we invoke sh directly here.
await using proc = Bun.spawn({ await using proc = Bun.spawn({
cmd: ["./bun-placeholder", "--version"], cmd: ["sh", "./bun-placeholder"],
cwd: String(dir), cwd: String(dir),
env: bunEnv, env: bunEnv,
stdout: "pipe", stdout: "pipe",