mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
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:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
52
src/bun.zig
52
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
15
src/cli.zig
15
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);
|
||||
|
||||
@@ -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", .{});
|
||||
|
||||
@@ -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)});
|
||||
};
|
||||
|
||||
|
||||
@@ -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)});
|
||||
};
|
||||
|
||||
|
||||
@@ -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/,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user