diff --git a/src/bun.js.zig b/src/bun.js.zig index fdf33b3445..fe61c8151a 100644 --- a/src/bun.js.zig +++ b/src/bun.js.zig @@ -277,12 +277,12 @@ pub const Run = struct { vm.onUnhandledRejection = &onUnhandledRejectionBeforeClose; // Start CPU profiler if enabled - if (this.ctx.runtime_options.cpu_prof.enabled or bun.env_var.BUN_CPU_PROFILE.get()) { + if (this.ctx.runtime_options.cpu_prof.enabled) { const cpu_prof_opts = this.ctx.runtime_options.cpu_prof; vm.cpu_profiler_config = CPUProfiler.CPUProfilerConfig{ - .name = if (cpu_prof_opts.name.len > 0) cpu_prof_opts.name else bun.env_var.BUN_CPU_PROFILE_NAME.get() orelse "", - .dir = if (cpu_prof_opts.dir.len > 0) cpu_prof_opts.dir else bun.env_var.BUN_CPU_PROFILE_DIR.get() orelse "", + .name = cpu_prof_opts.name, + .dir = cpu_prof_opts.dir, .md_format = cpu_prof_opts.md_format, .json_format = cpu_prof_opts.json_format, }; diff --git a/src/bun.js/node/node_process.zig b/src/bun.js/node/node_process.zig index 0674c5f901..007631c050 100644 --- a/src/bun.js/node/node_process.zig +++ b/src/bun.js/node/node_process.zig @@ -65,17 +65,29 @@ fn createExecArgv(globalObject: *jsc.JSGlobalObject) bun.JSError!jsc.JSValue { } } - // For compiled/standalone executables, execArgv should contain compile_exec_argv + // For compiled/standalone executables, execArgv should contain compile_exec_argv and BUN_OPTIONS. + // Use appendOptionsEnv for BUN_OPTIONS to correctly handle quoted values. if (vm.standalone_module_graph) |graph| { - if (graph.compile_exec_argv.len > 0) { - // Use tokenize to split the compile_exec_argv string by whitespace + if (graph.compile_exec_argv.len > 0 or bun.bun_options_argc > 0) { var args = std.array_list.Managed(bun.String).init(temp_alloc); defer args.deinit(); defer for (args.items) |*arg| arg.deref(); - var tokenizer = std.mem.tokenizeAny(u8, graph.compile_exec_argv, " \t\n\r"); - while (tokenizer.next()) |token| { - try args.append(bun.String.cloneUTF8(token)); + // Process BUN_OPTIONS first using appendOptionsEnv for proper quote handling. + // appendOptionsEnv inserts starting at index 1, so we need a placeholder. + if (bun.bun_options_argc > 0) { + if (bun.env_var.BUN_OPTIONS.get()) |opts| { + try args.append(bun.String.empty); // placeholder for insert-at-1 + try bun.appendOptionsEnv(opts, bun.String, &args); + _ = args.orderedRemove(0); // remove placeholder + } + } + + if (graph.compile_exec_argv.len > 0) { + var tokenizer = std.mem.tokenizeAny(u8, graph.compile_exec_argv, " \t\n\r"); + while (tokenizer.next()) |token| { + try args.append(bun.String.cloneUTF8(token)); + } } const array = try jsc.JSValue.createEmptyArray(globalObject, args.items.len); diff --git a/src/bun.zig b/src/bun.zig index ca572ae8af..d21adf2df9 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -1924,8 +1924,11 @@ pub const StatFS = switch (Environment.os) { }; pub var argv: [][:0]const u8 = &[_][:0]const u8{}; +/// Number of arguments injected by BUN_OPTIONS environment variable. +/// Used by standalone executables to include these in the parsed options window. +pub var bun_options_argc: usize = 0; -pub fn appendOptionsEnv(env: []const u8, args: *std.array_list.Managed([:0]const u8), allocator: std.mem.Allocator) !void { +pub fn appendOptionsEnv(env: []const u8, comptime ArgType: type, args: *std.array_list.Managed(ArgType)) !void { var i: usize = 0; var offset_in_args: usize = 1; while (i < env.len) { @@ -1971,8 +1974,17 @@ pub fn appendOptionsEnv(env: []const u8, args: *std.array_list.Managed([:0]const // Copy the entire argument including quotes const arg_len = j - start; - const arg = try allocator.allocSentinel(u8, arg_len, 0); - @memcpy(arg, env[start..j]); + + const arg = switch (ArgType) { + bun.String => bun.String.cloneUTF8(env[start..j]), + [:0]const u8 => arg: { + const arg = try bun.default_allocator.allocSentinel(u8, arg_len, 0); + @memcpy(arg, env[start..j]); + break :arg arg; + }, + else => @compileError("unexpected arg type"), + }; + try args.insert(offset_in_args, arg); offset_in_args += 1; @@ -1981,7 +1993,7 @@ pub fn appendOptionsEnv(env: []const u8, args: *std.array_list.Managed([:0]const } // Non-option arguments or standalone values - var buf = std.array_list.Managed(u8).init(allocator); + var buf = std.array_list.Managed(u8).init(bun.default_allocator); var in_single = false; var in_double = false; @@ -2028,16 +2040,26 @@ pub fn appendOptionsEnv(env: []const u8, args: *std.array_list.Managed([:0]const } } - try buf.append(0); - const owned = try buf.toOwnedSlice(); - try args.insert(offset_in_args, owned[0 .. owned.len - 1 :0]); + switch (ArgType) { + bun.String => { + defer buf.deinit(); + try args.insert(offset_in_args, bun.String.cloneUTF8(buf.items)); + }, + [:0]const u8 => { + try buf.append(0); + const owned = try buf.toOwnedSlice(); + try args.insert(offset_in_args, owned[0 .. owned.len - 1 :0]); + }, + else => @compileError("unexpected arg type"), + } + offset_in_args += 1; } } -pub fn initArgv(allocator: std.mem.Allocator) !void { +pub fn initArgv() !void { if (comptime Environment.isPosix) { - argv = try allocator.alloc([:0]const u8, std.os.argv.len); + argv = try bun.default_allocator.alloc([:0]const u8, std.os.argv.len); for (0..argv.len) |i| { argv[i] = std.mem.sliceTo(std.os.argv[i], 0); } @@ -2072,7 +2094,7 @@ pub fn initArgv(allocator: std.mem.Allocator) !void { }; const argvu16 = argvu16_ptr[0..@intCast(length)]; - const out_argv = try allocator.alloc([:0]const u8, @intCast(length)); + const out_argv = try bun.default_allocator.alloc([:0]const u8, @intCast(length)); var string_builder = StringBuilder{}; for (argvu16) |argraw| { @@ -2080,7 +2102,7 @@ pub fn initArgv(allocator: std.mem.Allocator) !void { string_builder.count16Z(arg); } - try string_builder.allocate(allocator); + try string_builder.allocate(bun.default_allocator); for (argvu16, out_argv) |argraw, *out| { const arg = std.mem.span(argraw); @@ -2092,13 +2114,15 @@ pub fn initArgv(allocator: std.mem.Allocator) !void { argv = out_argv; } else { - argv = try std.process.argsAlloc(allocator); + argv = try std.process.argsAlloc(bun.default_allocator); } if (bun.env_var.BUN_OPTIONS.get()) |opts| { - var argv_list = std.array_list.Managed([:0]const u8).fromOwnedSlice(allocator, argv); - try appendOptionsEnv(opts, &argv_list, allocator); + const original_len = argv.len; + var argv_list = std.array_list.Managed([:0]const u8).fromOwnedSlice(bun.default_allocator, argv); + try appendOptionsEnv(opts, [:0]const u8, &argv_list); argv = argv_list.items; + bun_options_argc = argv.len - original_len; } } diff --git a/src/cli.zig b/src/cli.zig index 68eac17b15..cef6c6ee19 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -694,22 +694,25 @@ pub const Command = struct { var offset_for_passthrough: usize = 0; const ctx: *ContextData = brk: { - if (graph.compile_exec_argv.len > 0) { + if (graph.compile_exec_argv.len > 0 or bun.bun_options_argc > 0) { const original_argv_len = bun.argv.len; var argv_list = std.array_list.Managed([:0]const u8).fromOwnedSlice(bun.default_allocator, bun.argv); - try bun.appendOptionsEnv(graph.compile_exec_argv, &argv_list, bun.default_allocator); + if (graph.compile_exec_argv.len > 0) { + try bun.appendOptionsEnv(graph.compile_exec_argv, [:0]const u8, &argv_list); + } // Store the full argv including user arguments const full_argv = argv_list.items; const num_exec_argv_options = full_argv.len -| original_argv_len; - // Calculate offset: skip executable name + all exec argv options - offset_for_passthrough = if (full_argv.len > 1) 1 + num_exec_argv_options else 0; + // Calculate offset: skip executable name + all exec argv options + BUN_OPTIONS args + const num_parsed_options = num_exec_argv_options + bun.bun_options_argc; + offset_for_passthrough = if (full_argv.len > 1) 1 + num_parsed_options else 0; - // Temporarily set bun.argv to only include executable name + exec_argv options. + // Temporarily set bun.argv to only include executable name + exec_argv options + BUN_OPTIONS args. // This prevents user arguments like --version/--help from being intercepted // by Bun's argument parser (they should be passed through to user code). - bun.argv = full_argv[0..@min(1 + num_exec_argv_options, full_argv.len)]; + bun.argv = full_argv[0..@min(1 + num_parsed_options, full_argv.len)]; // Handle actual options to parse. const result = try Command.init(allocator, log, .AutoCommand); diff --git a/src/env_var.zig b/src/env_var.zig index ddae10786c..45a16bc62e 100644 --- a/src/env_var.zig +++ b/src/env_var.zig @@ -37,9 +37,6 @@ pub const BUN_CONFIG_DISABLE_ioctl_ficlonerange = New(kind.boolean, "BUN_CONFIG_ /// /// It's unclear why this was done. pub const BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS = New(kind.unsigned, "BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS", .{ .default = 30 }); -pub const BUN_CPU_PROFILE = New(kind.boolean, "BUN_CPU_PROFILE", .{ .default = false }); -pub const BUN_CPU_PROFILE_DIR = New(kind.string, "BUN_CPU_PROFILE_DIR", .{}); -pub const BUN_CPU_PROFILE_NAME = New(kind.string, "BUN_CPU_PROFILE_NAME", .{}); pub const BUN_CRASH_REPORT_URL = New(kind.string, "BUN_CRASH_REPORT_URL", .{}); pub const BUN_DEBUG = New(kind.string, "BUN_DEBUG", .{}); pub const BUN_DEBUG_ALL = New(kind.boolean, "BUN_DEBUG_ALL", .{}); diff --git a/src/main.zig b/src/main.zig index d486d76c13..6860b08b19 100644 --- a/src/main.zig +++ b/src/main.zig @@ -46,7 +46,7 @@ pub fn main() void { } _bun.start_time = std.time.nanoTimestamp(); - _bun.initArgv(_bun.default_allocator) catch |err| { + _bun.initArgv() catch |err| { Output.panic("Failed to initialize argv: {s}\n", .{@errorName(err)}); }; diff --git a/src/main_test.zig b/src/main_test.zig index e6edd180bf..a5cf6bf637 100644 --- a/src/main_test.zig +++ b/src/main_test.zig @@ -32,7 +32,7 @@ pub fn main() void { _environ = @ptrCast(std.os.environ.ptr); } - bun.initArgv(bun.default_allocator) catch |err| { + bun.initArgv() catch |err| { Output.panic("Failed to initialize argv: {s}\n", .{@errorName(err)}); }; diff --git a/test/bundler/compile-argv.test.ts b/test/bundler/compile-argv.test.ts index 50b32fa5fb..c40245f395 100644 --- a/test/bundler/compile-argv.test.ts +++ b/test/bundler/compile-argv.test.ts @@ -276,4 +276,115 @@ describe("bundler", () => { stdout: /APP_HELP:my custom help/, }, }); + + // Test that BUN_OPTIONS env var is applied to standalone executables + itBundled("compile/BunOptionsEnvApplied", { + compile: true, + backend: "cli", + files: { + "/entry.ts": /* js */ ` + console.log("execArgv:", JSON.stringify(process.execArgv)); + console.log("argv:", JSON.stringify(process.argv)); + + if (process.execArgv.findIndex(arg => arg === "--smol") === -1) { + console.error("FAIL: --smol not found in execArgv:", process.execArgv); + process.exit(1); + } + + // BUN_OPTIONS args should NOT appear in process.argv + for (const arg of process.argv) { + if (arg === "--smol") { + console.error("FAIL: --smol leaked into process.argv:", process.argv); + process.exit(1); + } + } + + console.log("SUCCESS: BUN_OPTIONS applied to standalone executable"); + `, + }, + run: { + env: { BUN_OPTIONS: "--smol" }, + stdout: /SUCCESS: BUN_OPTIONS applied to standalone executable/, + }, + }); + + // Test BUN_OPTIONS combined with compile-exec-argv + itBundled("compile/BunOptionsEnvWithCompileExecArgv", { + compile: { + execArgv: ["--conditions=production"], + }, + backend: "cli", + files: { + "/entry.ts": /* js */ ` + console.log("execArgv:", JSON.stringify(process.execArgv)); + console.log("argv:", JSON.stringify(process.argv)); + + if (process.execArgv.findIndex(arg => arg === "--conditions=production") === -1) { + console.error("FAIL: --conditions=production not found in execArgv:", process.execArgv); + process.exit(1); + } + + if (process.execArgv.findIndex(arg => arg === "--smol") === -1) { + console.error("FAIL: --smol not found in execArgv:", process.execArgv); + process.exit(1); + } + + // Neither BUN_OPTIONS nor compile-exec-argv args should be in process.argv + for (const arg of process.argv) { + if (arg === "--smol" || arg === "--conditions=production") { + console.error("FAIL: exec option leaked into process.argv:", arg); + process.exit(1); + } + } + + console.log("SUCCESS: BUN_OPTIONS and compile-exec-argv both applied"); + `, + }, + run: { + env: { BUN_OPTIONS: "--smol" }, + stdout: /SUCCESS: BUN_OPTIONS and compile-exec-argv both applied/, + }, + }); + + // Test BUN_OPTIONS with user passthrough args + itBundled("compile/BunOptionsEnvWithPassthroughArgs", { + compile: true, + backend: "cli", + files: { + "/entry.ts": /* js */ ` + console.log("execArgv:", JSON.stringify(process.execArgv)); + console.log("argv:", JSON.stringify(process.argv)); + + if (process.execArgv.findIndex(arg => arg === "--smol") === -1) { + console.error("FAIL: --smol not found in execArgv:", process.execArgv); + process.exit(1); + } + + if (process.argv.findIndex(arg => arg === "user-arg1") === -1) { + console.error("FAIL: user-arg1 not found in argv:", process.argv); + process.exit(1); + } + + if (process.argv.findIndex(arg => arg === "user-arg2") === -1) { + console.error("FAIL: user-arg2 not found in argv:", process.argv); + process.exit(1); + } + + // BUN_OPTIONS args should NOT be in process.argv + for (const arg of process.argv) { + if (arg === "--smol") { + console.error("FAIL: --smol leaked into process.argv:", process.argv); + process.exit(1); + } + } + + console.log("SUCCESS: BUN_OPTIONS separated from passthrough args"); + `, + }, + run: { + env: { BUN_OPTIONS: "--smol" }, + args: ["user-arg1", "user-arg2"], + stdout: /SUCCESS: BUN_OPTIONS separated from passthrough args/, + }, + }); });