diff --git a/src/install/lifecycle_script_runner.zig b/src/install/lifecycle_script_runner.zig index 2b249771b8..325d59ae38 100644 --- a/src/install/lifecycle_script_runner.zig +++ b/src/install/lifecycle_script_runner.zig @@ -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(); }; } }; diff --git a/test/regression/issue/14945-lifecycle-script-crash.test.ts b/test/regression/issue/14945-lifecycle-script-crash.test.ts new file mode 100644 index 0000000000..00ad75391d --- /dev/null +++ b/test/regression/issue/14945-lifecycle-script-crash.test.ts @@ -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); +});