mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
## 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>
105 lines
3.7 KiB
TypeScript
105 lines
3.7 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { bunEnv, isWindows, tempDir } from "harness";
|
|
|
|
// This test verifies that the placeholder scripts created during npm package build
|
|
// print an error message and exit with code 1, rather than silently succeeding.
|
|
// See: https://github.com/oven-sh/bun/issues/24329
|
|
|
|
test("bun npm placeholder script should exit with error if postinstall hasn't run", async () => {
|
|
// Skip on Windows as the placeholder is a shell script
|
|
if (isWindows) {
|
|
return;
|
|
}
|
|
|
|
// 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)
|
|
// Note: no shebang — a #!/bin/sh shebang breaks Windows because npm's cmd-shim
|
|
// 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 "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 "" >&2
|
|
echo "To fix this, run the postinstall script manually:" >&2
|
|
echo " cd node_modules/bun && node install.js" >&2
|
|
echo "" >&2
|
|
echo "Or reinstall bun without the --ignore-scripts flag." >&2
|
|
exit 1
|
|
`;
|
|
|
|
using dir = tempDir("issue-24329", {
|
|
"bun-placeholder": placeholderScript,
|
|
});
|
|
|
|
// Make the placeholder executable
|
|
const { exitCode: chmodExitCode } = Bun.spawnSync({
|
|
cmd: ["chmod", "+x", "bun-placeholder"],
|
|
cwd: String(dir),
|
|
env: bunEnv,
|
|
});
|
|
expect(chmodExitCode).toBe(0);
|
|
|
|
// 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({
|
|
cmd: ["sh", "./bun-placeholder"],
|
|
cwd: String(dir),
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
// The placeholder should exit with code 1
|
|
expect(exitCode).toBe(1);
|
|
|
|
// stdout should be empty (all output goes to stderr)
|
|
expect(stdout).toBe("");
|
|
|
|
// stderr should contain the error message
|
|
expect(stderr).toContain("Error: Bun's postinstall script was not run.");
|
|
expect(stderr).toContain("--ignore-scripts");
|
|
expect(stderr).toContain("cd node_modules/bun && node install.js");
|
|
});
|
|
|
|
test("empty shell script exits with code 0 (demonstrating why the fix is needed)", async () => {
|
|
// Skip on Windows
|
|
if (isWindows) {
|
|
return;
|
|
}
|
|
|
|
// This simulates the OLD behavior: an empty shell script (with shebang)
|
|
// Note: A completely empty file can't be executed by Bun.spawn (ENOEXEC),
|
|
// but an empty shell script with a shebang exits with code 0
|
|
using dir = tempDir("issue-24329-old", {
|
|
"bun-placeholder": "#!/bin/sh\n",
|
|
});
|
|
|
|
// Make it executable
|
|
const { exitCode: chmodExitCode } = Bun.spawnSync({
|
|
cmd: ["chmod", "+x", "bun-placeholder"],
|
|
cwd: String(dir),
|
|
env: bunEnv,
|
|
});
|
|
expect(chmodExitCode).toBe(0);
|
|
|
|
// Run the empty shell script
|
|
await using proc = Bun.spawn({
|
|
cmd: ["./bun-placeholder", "--version"],
|
|
cwd: String(dir),
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
// Empty shell script exits with code 0 silently - this is similar to the bug behavior
|
|
// Assert stdout/stderr before exitCode to get more useful error messages on failure
|
|
expect(stdout).toBe("");
|
|
expect(stderr).toBe("");
|
|
expect(exitCode).toBe(0);
|
|
});
|