mirror of
https://github.com/oven-sh/bun
synced 2026-02-28 12:31:00 +01:00
fix(windows): avoid standalone worker dotenv crash (#27434)
### What does this PR do? Fixes #27431. - fixes a Windows standalone executable crash when `compile.autoloadDotenv = false`, a `.env` file exists in the runtime cwd, and the executable spawns a `Worker` - gives worker startup its own cloned `DotEnv.Loader` before `configureDefines()`, so dotenv loading does not mutate env state owned by another thread - aligns worker startup with other Bun runtime paths by wiring `resolver.env_loader = transpiler.env` - extracts standalone runtime flag propagation into `applyStandaloneRuntimeFlags(...)` so main and worker startup share the same env/tsconfig/package.json behavior - adds regression coverage in `test/regression/issue/27431.test.ts` and bundler coverage in `test/bundler/bundler_compile_autoload.test.ts` ### How did you verify your code works? - reproduced the original crash with `bun test regression/issue/27431.test.ts` on stock `1.3.10-canary.104`; the test fails on unpatched Bun - rebuilt `build/debug/bun-debug.exe` with this patch and ran `build/debug/bun-debug.exe test regression/issue/27431.test.ts`; the test passes on the patched build - manually validated the minimal repro from `https://github.com/Hona/bun1310-minimal-repro` against the patched `bun-debug.exe`; the standalone executable no longer crashes and still keeps dotenv disabled (`process.env` does not pick up `.env`)
This commit is contained in:
@@ -3,6 +3,17 @@ pub const webcore = @import("./bun.js/webcore.zig");
|
||||
pub const api = @import("./bun.js/api.zig");
|
||||
pub const bindgen = @import("./bun.js/bindgen.zig");
|
||||
|
||||
pub fn applyStandaloneRuntimeFlags(b: *bun.Transpiler, graph: *const bun.StandaloneModuleGraph) void {
|
||||
b.options.env.disable_default_env_files = graph.flags.disable_default_env_files;
|
||||
b.options.env.behavior = if (graph.flags.disable_default_env_files)
|
||||
.disable
|
||||
else
|
||||
.load_all_without_inlining;
|
||||
|
||||
b.resolver.opts.load_tsconfig_json = !graph.flags.disable_autoload_tsconfig;
|
||||
b.resolver.opts.load_package_json = !graph.flags.disable_autoload_package_json;
|
||||
}
|
||||
|
||||
pub const Run = struct {
|
||||
ctx: Command.Context,
|
||||
vm: *VirtualMachine,
|
||||
@@ -82,18 +93,7 @@ pub const Run = struct {
|
||||
.unspecified => {},
|
||||
}
|
||||
|
||||
// If .env loading is disabled, only load process env vars
|
||||
// Otherwise, load all .env files
|
||||
if (graph_ptr.flags.disable_default_env_files) {
|
||||
b.options.env.behavior = .disable;
|
||||
} else {
|
||||
b.options.env.behavior = .load_all_without_inlining;
|
||||
}
|
||||
|
||||
// Control loading of tsconfig.json and package.json at runtime
|
||||
// By default, these are disabled for standalone executables
|
||||
b.resolver.opts.load_tsconfig_json = !graph_ptr.flags.disable_autoload_tsconfig;
|
||||
b.resolver.opts.load_package_json = !graph_ptr.flags.disable_autoload_package_json;
|
||||
applyStandaloneRuntimeFlags(b, graph_ptr);
|
||||
|
||||
b.configureDefines() catch {
|
||||
failWithBuildError(vm);
|
||||
|
||||
@@ -325,16 +325,30 @@ pub fn start(
|
||||
}
|
||||
|
||||
this.arena = bun.MimallocArena.init();
|
||||
const allocator = this.arena.?.allocator();
|
||||
|
||||
const map = try allocator.create(bun.DotEnv.Map);
|
||||
map.* = try this.parent.transpiler.env.map.cloneWithAllocator(allocator);
|
||||
|
||||
const loader = try allocator.create(bun.DotEnv.Loader);
|
||||
loader.* = bun.DotEnv.Loader.init(map, allocator);
|
||||
|
||||
var vm = try jsc.VirtualMachine.initWorker(this, .{
|
||||
.allocator = this.arena.?.allocator(),
|
||||
.allocator = allocator,
|
||||
.args = transform_options,
|
||||
.env_loader = loader,
|
||||
.store_fd = this.store_fd,
|
||||
.graph = this.parent.standalone_module_graph,
|
||||
});
|
||||
vm.allocator = this.arena.?.allocator();
|
||||
vm.allocator = allocator;
|
||||
vm.arena = &this.arena.?;
|
||||
|
||||
var b = &vm.transpiler;
|
||||
b.resolver.env_loader = b.env;
|
||||
|
||||
if (this.parent.standalone_module_graph) |graph| {
|
||||
bun.bun_js.applyStandaloneRuntimeFlags(b, graph);
|
||||
}
|
||||
|
||||
b.configureDefines() catch {
|
||||
this.flushLogs();
|
||||
@@ -342,16 +356,6 @@ pub fn start(
|
||||
return;
|
||||
};
|
||||
|
||||
// TODO: we may have to clone other parts of vm state. this will be more
|
||||
// important when implementing vm.deinit()
|
||||
const map = try vm.allocator.create(bun.DotEnv.Map);
|
||||
map.* = try vm.transpiler.env.map.cloneWithAllocator(vm.allocator);
|
||||
|
||||
const loader = try vm.allocator.create(bun.DotEnv.Loader);
|
||||
loader.* = bun.DotEnv.Loader.init(map, vm.allocator);
|
||||
|
||||
vm.transpiler.env = loader;
|
||||
|
||||
vm.loadExtraEnvAndSourceCodePrinter();
|
||||
vm.is_main_thread = false;
|
||||
jsc.VirtualMachine.is_main_thread_vm = false;
|
||||
|
||||
@@ -168,6 +168,40 @@ console.log("PRELOAD");
|
||||
},
|
||||
});
|
||||
|
||||
// Regression test: standalone workers must not load .env when autoloadDotenv is disabled
|
||||
itBundled("compile/AutoloadDotenvDisabledWorkerCLI", {
|
||||
compile: {
|
||||
autoloadDotenv: false,
|
||||
},
|
||||
backend: "cli",
|
||||
files: {
|
||||
"/entry.ts": /* js */ `
|
||||
import { rmSync } from "fs";
|
||||
|
||||
rmSync("./worker.ts", { force: true });
|
||||
|
||||
const worker = new Worker("./worker.ts");
|
||||
console.log(await new Promise(resolve => {
|
||||
worker.onmessage = event => resolve(event.data);
|
||||
}));
|
||||
worker.terminate();
|
||||
`,
|
||||
"/worker.ts": /* js */ `
|
||||
postMessage(process.env.TEST_VAR || "not found");
|
||||
`,
|
||||
},
|
||||
entryPointsRaw: ["./entry.ts", "./worker.ts"],
|
||||
outfile: "dist/out",
|
||||
runtimeFiles: {
|
||||
"/.env": `TEST_VAR=from_dotenv`,
|
||||
},
|
||||
run: {
|
||||
stdout: "not found",
|
||||
file: "dist/out",
|
||||
setCwd: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Test CLI backend with autoloadDotenv: true
|
||||
itBundled("compile/AutoloadDotenvEnabledCLI", {
|
||||
compile: {
|
||||
|
||||
50
test/regression/issue/27431.test.ts
Normal file
50
test/regression/issue/27431.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, isWindows, tempDir } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
test.if(isWindows)("standalone worker does not crash when autoloadDotenv is disabled and .env exists", async () => {
|
||||
const target = process.arch === "arm64" ? "bun-windows-aarch64" : "bun-windows-x64";
|
||||
|
||||
using dir = tempDir("issue-27431", {
|
||||
".env": "TEST_VAR=from_dotenv\n",
|
||||
"entry.ts": 'console.log(process.env.TEST_VAR || "not found")\nnew Worker("./worker.ts")\n',
|
||||
"worker.ts": "",
|
||||
"build.ts": `
|
||||
await Bun.build({
|
||||
entrypoints: ["./entry.ts", "./worker.ts"],
|
||||
compile: {
|
||||
autoloadDotenv: false,
|
||||
target: "${target}",
|
||||
outfile: "./app.exe",
|
||||
},
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
await using build = Bun.spawn({
|
||||
cmd: [bunExe(), join(String(dir), "build.ts")],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [, buildStderr, buildExitCode] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]);
|
||||
|
||||
expect(buildExitCode).toBe(0);
|
||||
expect(buildStderr).toBe("");
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [join(String(dir), "app.exe")],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout).toContain("not found");
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stderr).toBe("");
|
||||
});
|
||||
Reference in New Issue
Block a user