fix(install): handle lifecycle script spawn failures gracefully instead of crashing (#21054)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: jarred-sumner-bot <220441119+jarred-sumner-bot@users.noreply.github.com>
This commit is contained in:
jarred-sumner-bot
2025-07-14 20:50:32 -07:00
committed by GitHub
parent c5f64036a7
commit 7f29446d9b
2 changed files with 107 additions and 1 deletions

View File

@@ -413,7 +413,13 @@ pub const LifecycleScriptSubprocess = struct {
Lockfile.Scripts.names[new_script_index],
@errorName(err),
});
Global.exit(1);
if (this.ctx) |this_ctx| {
this_ctx.installer.store.entries.items(.step)[this_ctx.entry_id.get()].store(.done, .monotonic);
this_ctx.installer.onTaskComplete(this_ctx.entry_id, .fail);
}
_ = this.manager.pending_lifecycle_script_tasks.fetchSub(1, .monotonic);
this.deinit();
return;
};
return;
}
@@ -571,6 +577,12 @@ pub const LifecycleScriptSubprocess = struct {
Lockfile.Scripts.names[list.first_index],
@errorName(err),
});
if (lifecycle_subprocess.ctx) |subprocess_ctx| {
subprocess_ctx.installer.store.entries.items(.step)[subprocess_ctx.entry_id.get()].store(.done, .monotonic);
subprocess_ctx.installer.onTaskComplete(subprocess_ctx.entry_id, .fail);
}
_ = manager.pending_lifecycle_script_tasks.fetchSub(1, .monotonic);
lifecycle_subprocess.deinit();
};
}
};

View File

@@ -0,0 +1,94 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
test("lifecycle script should handle directory deletion gracefully", async () => {
const dir = tempDirWithFiles("lifecycle-crash-test", {
"package.json": JSON.stringify({
name: "test-package",
version: "1.0.0",
scripts: {
preinstall: process.platform === "win32" ? "rmdir /s /q ." : "rm -rf .",
postinstall: "echo hello world",
},
}),
});
// Run bun install and expect it to handle the directory deletion gracefully
// without crashing with assertions
await using proc = Bun.spawn({
cmd: [bunExe(), "install"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
// The process should not crash with assertion failures
// It may fail (non-zero exit code) but should fail gracefully
// and not with internal assertion errors
expect(stderr).not.toContain("assertion");
expect(stderr).not.toContain("atIndex");
expect(stderr).not.toContain("panic");
// Should contain an error message about the script failure
expect(stderr.includes("script") && (stderr.includes("exited with") || stderr.includes("Failed to run script"))).toBe(
true,
);
// The process should exit with a non-zero code due to the script failure
expect(exitCode).not.toBe(0);
});
test("lifecycle script with optional dependency should handle directory deletion", async () => {
const depDir = tempDirWithFiles("optional-dep", {
"package.json": JSON.stringify({
name: "optional-dep",
version: "1.0.0",
scripts: {
preinstall: process.platform === "win32" ? "rmdir /s /q ." : "rm -rf .",
postinstall: "echo hello from optional dep",
},
}),
});
const mainDir = tempDirWithFiles("main-package", {
"package.json": JSON.stringify({
name: "main-package",
version: "1.0.0",
optionalDependencies: {
"optional-dep": `file:${depDir}`,
},
}),
});
// Run bun install and expect it to handle the optional dependency
// directory deletion gracefully
await using proc = Bun.spawn({
cmd: [bunExe(), "install"],
env: bunEnv,
cwd: mainDir,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
// The process should not crash with assertion failures
expect(stderr).not.toContain("assertion");
expect(stderr).not.toContain("atIndex");
expect(stderr).not.toContain("panic");
// For optional dependencies, the install should succeed even if scripts fail
// The process may warn about deleting the optional dependency
expect(exitCode).toBe(0);
});