Fix argv handling for standalone binaries with compile-exec-argv (#22084)

## 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 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Jarred Sumner
2025-08-23 19:49:01 -07:00
committed by GitHub
parent c0eebd7523
commit f718f4a312
4 changed files with 137 additions and 4 deletions

View File

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

View File

@@ -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.
See [Docs > API > YAML](https://bun.com/docs/api/yaml) for complete documentation on YAML support in Bun.

View File

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

View File

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