From f718f4a3121d2d4a61e71c2b0339c478b24a2583 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 23 Aug 2025 19:49:01 -0700 Subject: [PATCH] Fix argv handling for standalone binaries with compile-exec-argv (#22084) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes an issue where `--compile-exec-argv` options were incorrectly appearing in `process.argv` when no user arguments were provided to a compiled standalone binary. ## Problem When building a standalone binary with `--compile-exec-argv`, the exec argv options would leak into `process.argv` when running the binary without any user arguments: ```bash # Build with exec argv bun build --compile-exec-argv="--user-agent=hello" --compile ./a.js # Run without arguments - BEFORE fix ./a # Output showed --user-agent=hello in both execArgv AND argv (incorrect) { execArgv: [ "--user-agent=hello" ], argv: [ "bun", "/$bunfs/root/a", "--user-agent=hello" ], # <- BUG: exec argv leaked here } # Expected behavior (matches runtime): bun --user-agent=hello a.js { execArgv: [ "--user-agent=hello" ], argv: [ "/path/to/bun", "/path/to/a.js" ], # <- No exec argv in process.argv } ``` ## Solution The issue was in the offset calculation for determining which arguments to pass through to the JavaScript runtime. The offset was being calculated before modifying the argv array with exec argv options, causing it to be incorrect when the original argv only contained the executable name. The fix ensures that: - `process.execArgv` correctly contains the compile-exec-argv options - `process.argv` only contains the executable, script path, and user arguments - exec argv options never leak into `process.argv` ## Test plan Added comprehensive tests to verify: 1. Exec argv options don't leak into process.argv when no user arguments are provided 2. User arguments are properly passed through when exec argv is present 3. Existing behavior continues to work correctly All tests pass: ``` bun test compile-argv.test.ts ✓ 3 tests pass ``` 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- docs/bundler/loaders.md | 2 +- docs/guides/runtime/import-yaml.md | 2 +- src/cli.zig | 11 ++- test/bundler/compile-argv.test.ts | 126 +++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 4 deletions(-) diff --git a/docs/bundler/loaders.md b/docs/bundler/loaders.md index c3e70535cd..72ec911ca2 100644 --- a/docs/bundler/loaders.md +++ b/docs/bundler/loaders.md @@ -141,7 +141,7 @@ During bundling, the parsed YAML is inlined into the bundle as a JavaScript obje var config = { database: { host: "localhost", - port: 5432 + port: 5432, }, // ...other fields }; diff --git a/docs/guides/runtime/import-yaml.md b/docs/guides/runtime/import-yaml.md index 791d6c96a2..c13e1d6cd8 100644 --- a/docs/guides/runtime/import-yaml.md +++ b/docs/guides/runtime/import-yaml.md @@ -73,4 +73,4 @@ console.log(data.hobbies); // => ["reading", "coding"] --- -See [Docs > API > YAML](https://bun.com/docs/api/yaml) for complete documentation on YAML support in Bun. \ No newline at end of file +See [Docs > API > YAML](https://bun.com/docs/api/yaml) for complete documentation on YAML support in Bun. diff --git a/src/cli.zig b/src/cli.zig index c00479ab6d..4640b5277c 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -635,15 +635,18 @@ pub const Command = struct { // bun build --compile entry point if (!bun.getRuntimeFeatureFlag(.BUN_BE_BUN)) { if (try bun.StandaloneModuleGraph.fromExecutable(bun.default_allocator)) |graph| { - var offset_for_passthrough: usize = if (bun.argv.len > 1) 1 else 0; + var offset_for_passthrough: usize = 0; const ctx: *ContextData = brk: { if (graph.compile_exec_argv.len > 0) { + const original_argv_len = bun.argv.len; var argv_list = std.ArrayList([:0]const u8).fromOwnedSlice(bun.default_allocator, bun.argv); try bun.appendOptionsEnv(graph.compile_exec_argv, &argv_list, bun.default_allocator); - offset_for_passthrough += (argv_list.items.len -| bun.argv.len); bun.argv = argv_list.items; + // Calculate offset: skip executable name + all exec argv options + offset_for_passthrough = if (bun.argv.len > 1) 1 + (bun.argv.len -| original_argv_len) else 0; + // Handle actual options to parse. break :brk try Command.init(allocator, log, .AutoCommand); } @@ -655,6 +658,10 @@ pub const Command = struct { .allocator = bun.default_allocator, }; global_cli_ctx = &context_data; + + // If no compile_exec_argv, set offset normally + offset_for_passthrough = if (bun.argv.len > 1) 1 else 0; + break :brk global_cli_ctx; }; diff --git a/test/bundler/compile-argv.test.ts b/test/bundler/compile-argv.test.ts index b1fad2c487..d81df175fe 100644 --- a/test/bundler/compile-argv.test.ts +++ b/test/bundler/compile-argv.test.ts @@ -46,4 +46,130 @@ describe("bundler", () => { stdout: /SUCCESS: process.title and process.execArgv are both set correctly/, }, }); + + // Test that exec argv options don't leak into process.argv when no user arguments are provided + itBundled("compile/CompileExecArgvNoLeak", { + compile: { + execArgv: ["--user-agent=test-agent", "--smol"], + }, + files: { + "/entry.ts": /* js */ ` + // Test that compile-exec-argv options don't appear in process.argv + console.log("execArgv:", JSON.stringify(process.execArgv)); + console.log("argv:", JSON.stringify(process.argv)); + + // Check that execArgv contains the expected options + if (process.execArgv.length !== 2) { + console.error("FAIL: Expected exactly 2 items in execArgv, got", process.execArgv.length); + process.exit(1); + } + + if (process.execArgv[0] !== "--user-agent=test-agent") { + console.error("FAIL: Expected --user-agent=test-agent in execArgv[0], got", process.execArgv[0]); + process.exit(1); + } + + if (process.execArgv[1] !== "--smol") { + console.error("FAIL: Expected --smol in execArgv[1], got", process.execArgv[1]); + process.exit(1); + } + + // Check that argv only contains the executable and script name, NOT the exec argv options + if (process.argv.length !== 2) { + console.error("FAIL: Expected exactly 2 items in argv (executable and script), got", process.argv.length, "items:", process.argv); + process.exit(1); + } + + // argv[0] should be "bun" for standalone executables + if (process.argv[0] !== "bun") { + console.error("FAIL: Expected argv[0] to be 'bun', got", process.argv[0]); + process.exit(1); + } + + // argv[1] should be the script path (contains the bundle path) + if (!process.argv[1].includes("bunfs")) { + console.error("FAIL: Expected argv[1] to contain 'bunfs' path, got", process.argv[1]); + process.exit(1); + } + + // Make sure exec argv options are NOT in process.argv + for (const arg of process.argv) { + if (arg.includes("--user-agent") || arg === "--smol") { + console.error("FAIL: exec argv option leaked into process.argv:", arg); + process.exit(1); + } + } + + console.log("SUCCESS: exec argv options are properly separated from process.argv"); + `, + }, + run: { + // No user arguments provided - this is the key test case + args: [], + stdout: /SUCCESS: exec argv options are properly separated from process.argv/, + }, + }); + + // Test that user arguments are properly passed through when exec argv is present + itBundled("compile/CompileExecArgvWithUserArgs", { + compile: { + execArgv: ["--user-agent=test-agent", "--smol"], + }, + files: { + "/entry.ts": /* js */ ` + // Test that user arguments are properly included when exec argv is present + console.log("execArgv:", JSON.stringify(process.execArgv)); + console.log("argv:", JSON.stringify(process.argv)); + + // Check execArgv + if (process.execArgv.length !== 2) { + console.error("FAIL: Expected exactly 2 items in execArgv, got", process.execArgv.length); + process.exit(1); + } + + if (process.execArgv[0] !== "--user-agent=test-agent" || process.execArgv[1] !== "--smol") { + console.error("FAIL: Unexpected execArgv:", process.execArgv); + process.exit(1); + } + + // Check argv contains executable, script, and user arguments + if (process.argv.length !== 4) { + console.error("FAIL: Expected exactly 4 items in argv, got", process.argv.length, "items:", process.argv); + process.exit(1); + } + + if (process.argv[0] !== "bun") { + console.error("FAIL: Expected argv[0] to be 'bun', got", process.argv[0]); + process.exit(1); + } + + if (!process.argv[1].includes("bunfs")) { + console.error("FAIL: Expected argv[1] to contain 'bunfs' path, got", process.argv[1]); + process.exit(1); + } + + if (process.argv[2] !== "user-arg1") { + console.error("FAIL: Expected argv[2] to be 'user-arg1', got", process.argv[2]); + process.exit(1); + } + + if (process.argv[3] !== "user-arg2") { + console.error("FAIL: Expected argv[3] to be 'user-arg2', got", process.argv[3]); + process.exit(1); + } + + // Make sure exec argv options are NOT mixed with user arguments + if (process.argv.includes("--user-agent=test-agent") || process.argv.includes("--smol")) { + console.error("FAIL: exec argv options leaked into process.argv"); + process.exit(1); + } + + console.log("SUCCESS: user arguments properly passed with exec argv present"); + `, + }, + run: { + args: ["user-arg1", "user-arg2"], + stdout: /SUCCESS: user arguments properly passed with exec argv present/, + }, + }); });