fix(compile): apply BUN_OPTIONS env var to standalone executables (#26346)

## Summary
- Fixed `BUN_OPTIONS` environment variable not being applied as runtime
options for standalone executables (`bun build --compile`). Previously,
args from `BUN_OPTIONS` were incorrectly passed through to
`process.argv` instead of being parsed as Bun runtime options
(`process.execArgv`).
- Removed `BUN_CPU_PROFILE`, `BUN_CPU_PROFILE_DIR`, and
`BUN_CPU_PROFILE_NAME` env vars since `BUN_OPTIONS="--cpu-prof
--cpu-prof-dir=... --cpu-prof-name=..."` now works correctly with
standalone executables.
- Made `cpu_prof.name` and `cpu_prof.dir` non-optional with empty string
defaults.

fixes #21496

## Test plan
- [x] Added tests for `BUN_OPTIONS` with standalone executables (no
`compile-exec-argv`)
- [x] Added tests for `BUN_OPTIONS` combined with `--compile-exec-argv`
- [x] Added tests for `BUN_OPTIONS` with user passthrough args
- [x] Verified existing `compile-argv` tests still pass
- [x] Verified existing `bun-options` tests still pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Claude Bot <claude-bot@bun.sh>
This commit is contained in:
Dylan Conway
2026-01-23 00:24:18 -08:00
committed by GitHub
parent e8b2455f11
commit d8d8182e8e
8 changed files with 181 additions and 34 deletions

View File

@@ -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,
};

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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", .{});

View File

@@ -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)});
};

View File

@@ -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)});
};

View File

@@ -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/,
},
});
});