mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
fix(npm): show helpful error when postinstall script hasn't run (#26259)
## Summary - Replaces empty placeholder executables with shell scripts that print helpful error messages - The scripts exit with code 1 instead of silently succeeding with code 0 - Helps users diagnose issues when installing with `--ignore-scripts` or using pnpm ## Problem When installing the `bun` npm package with `--ignore-scripts` or using pnpm (which skips postinstall by default), the placeholder `bun.exe` and `bunx.exe` files were empty, causing them to silently exit with code 0 and produce no output. This made it very difficult for users to understand why bun wasn't working. ## Solution The placeholder files are now shell scripts that: 1. Print a clear error message explaining the issue 2. Provide instructions on how to fix it (manually running postinstall or reinstalling without `--ignore-scripts`) 3. Exit with code 1 to indicate failure Example output when running the placeholder: ``` Error: Bun's postinstall script was not run. This occurs when using --ignore-scripts during installation, or when using a package manager like pnpm that does not run postinstall scripts by default. To fix this, run the postinstall script manually: cd node_modules/bun && node install.js Or reinstall bun without the --ignore-scripts flag. ``` ## Test plan - [x] Added regression test that verifies the placeholder script behavior - [x] Test passes with `bun bd test test/regression/issue/24329.test.ts` Fixes #24329 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -71,8 +71,23 @@ async function buildRootModule(dryRun?: boolean) {
|
||||
js: "// Source code: https://github.com/oven-sh/bun/blob/main/packages/bun-release/scripts/npm-postinstall.ts",
|
||||
},
|
||||
});
|
||||
write(join(cwd, "bin", "bun.exe"), "");
|
||||
write(join(cwd, "bin", "bunx.exe"), "");
|
||||
// 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 Windows, npm creates .cmd wrappers that would fail anyway if the binary isn't valid.
|
||||
const placeholderScript = `#!/bin/sh
|
||||
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
|
||||
`;
|
||||
write(join(cwd, "bin", "bun.exe"), placeholderScript);
|
||||
write(join(cwd, "bin", "bunx.exe"), placeholderScript);
|
||||
write(
|
||||
join(cwd, "bin", "README.txt"),
|
||||
`The 'bun.exe' file is a placeholder for the binary file, which
|
||||
|
||||
101
test/regression/issue/24329.test.ts
Normal file
101
test/regression/issue/24329.test.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
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)
|
||||
const placeholderScript = `#!/bin/sh
|
||||
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 the placeholder 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]);
|
||||
|
||||
// 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);
|
||||
});
|
||||
Reference in New Issue
Block a user