Skip to content

compile: reject --target version tokens that are not exactly vX.Y.Z - #37389

Open
robobun wants to merge 3 commits into
mainfrom
farm/cea4c58d/compile-target-reject-malformed-version
Open

robobun wants to merge 3 commits into
mainfrom
farm/cea4c58d/compile-target-reject-malformed-version

Conversation

@robobun

@robobun robobun commented Aug 11, 2026 •

Copy link
Copy Markdown
Collaborator

Repro

$ echo 'console.log(1)' > entry.js
$ bun build --compile --target=bun-linux-x64-v1.2.3.4.5 entry.js --outfile=out
   [5ms]  bundle  1 modules
 [204ms] compile  out
$ echo $?
0

The version token is not a version, but the build succeeds and out is built from the running bun (1.4.0 here), with no download. Bun.build({ compile: { target: "bun-linux-x64-v1.2.3.4.5" } }) does the same. -v1.2.3., -v1..2 and -v0.a take the same path. Two more shapes get through differently: -v1.2.3rc1 downloads 1.2.3 and -v1.99999999999999999999.7 downloads 1.0.7. By contrast -v1.2 and -v1. are already rejected ("Please pass a complete version number to --target"). Reproduced with bun 1.4.0.

Cause

CompileTarget::try_from (src/options_types/compile_target.rs) walks the - separated tokens; every arm either records the token or returns an error, except the v1. / v0. arm. It calls Version::parse and only handles version.valid: when Semver rejects the token there is no else branch, so the loop moves on to the next token and this.version keeps its default, the running version. The Zig original had the same shape.

Version::parse is also lenient by design (it is the package manager's parser): trailing text becomes a tag and a component that overflows is read as 0 (parse_version_number does unwrap_or(ZERO)), with valid still set. So checking valid alone would still let -v1.2.3rc1 and the overflowing token select a version the user did not write.

Fix

The arm now requires the token to print back as itself: after taking major/minor/patch out of the parse result (a missing component is rejected as before), format!("{major}.{minor}.{patch}") has to equal the text after the v. Anything else is ParseError::InvalidVersion. That covers the dropped tokens, trailing text and overflow with one rule, and it is the rule that matters for this token: the download URL and cache name are built from exactly those three numbers, so the token has to name that version and nothing else.

Why reject rather than keep the lenient readings: this token exists to pin which bun the executable is built from. Building from a different one (the running bun, 1.2.3 instead of 1.2.3rc1, 1.0.7 instead of the written version) while exiting 0 is the one outcome --target is there to prevent. -v1.2.3rc1-style tokens were never meaningful for this flag: pre-release bun builds are not published under these versions, and anything with a - in it is split into separate tokens before this arm sees it.

InvalidVersion is its own variant because CompileTarget::from used to pick the CLI message by substring matching the whole target string, checking for musl / android before v; with the version arm now rejecting more inputs, bun-musl-v1.2.3.4.5 would have been reported as "musl libc only exists on linux". from() prints the existing version message for the new variant and no longer needs the v heuristic; the musl / android / wasm messages for the remaining InvalidTarget producers are unchanged. Bun.build (compile_target_from_slice) ignores the variant and keeps throwing Unknown compile target: ....

The UnsupportedTarget token scan in from() still treats v1./v0. tokens as known, which stays correct: the version arm now always consumes them, either as a version or as InvalidVersion.

Verification

New tests in test/bundler/bun-build-compile.test.ts ("version token in the target"). Both entry points run in a child process whose download is pointed at a local Bun.serve that returns 404 (BUN_COMPILE_TARGET_TARBALL_URL, plus BUN_INSTALL_CACHE_DIR set to an empty directory), so every case runs offline and a target that gets past the parser fails immediately with an error naming the parsed target.

  • Control: bun-<host platform>-v1.2.3 reaches the download step as v1.2.3 on both entry points ("Target platform 'bun-linux-x64-v1.2.3' is not available for download", Bun.build with throw: false returns success: false with that log), which checks that the version actually lands in the target rather than just that the token is accepted.
  • Rejected, on both entry points: the four dropped shapes above plus bare bun-v1.2.3.4.5, the trailing-text and overflow tokens, bun-musl-v1.2.3.4.5 (CLI reports the version message, not the musl one), and the two incomplete shapes that were already rejected. The CLI cases check the message, an empty stdout, exit code 1 and that nothing was written; the Bun.build cases check the exact thrown message.

With src/options_types/compile_target.rs at main, the 8 newly rejected targets fail on both entry points (16 failures; the five dropped shapes build successfully, the other three hit the local server); the control and the two incomplete shapes pass either way. With the change, the whole file passes under bun bd test.

#37385 (a glibc token) edits the same token loop and the same test file; whichever lands second has a small textual rebase, the two changes do not interact otherwise.


no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/bundler/bun-build-compile.test.ts

@coderabbitai

coderabbitai Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fa081cf8-fe2f-4976-bab0-0b900578730e

📥 Commits

Reviewing files that changed from the base of the PR and between 54d6d16 and 9bc500a.

📒 Files selected for processing (2)
  • src/options_types/compile_target.rs
  • test/bundler/bun-build-compile.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 11, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:33 AM PT - Aug 11th, 2026

✅ @robobun, your commit 9bc500a8a3b57eb8ced0242064f8679b3261ef00 passed in Build #92084! 🎉


🧪   To try this PR locally:

bunx bun-pr 37389

That installs a local version of the PR into your bun-37389 executable, so you can run:

bun-37389 --bun

@robobun

robobun commented Aug 11, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: reproduced with bun 1.4.0 on both entry points. bun build --compile --target=bun-linux-x64-v1.2.3.4.5 (also -v1.2.3., -v1..2, -v0.a) exits 0 and builds from the running bun, and -v1.2.3rc1 / -v1.99999999999999999999.7 select 1.2.3 / 1.0.7; Bun.build({ compile: { target } }) behaves the same. With this branch all of them are rejected at parse time, and -v1.2.3 still reaches the download step as v1.2.3.

Fix and tests are in this PR. With src/options_types/compile_target.rs at main, the 8 newly rejected targets fail on both entry points in test/bundler/bun-build-compile.test.ts (16 failures, all offline: the builds are pointed at a local 404 server); with the change the whole file passes under bun bd test. Rebased onto main after #37417 landed, so this PR no longer touches the outfile test.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — small, well-scoped fix that turns a silent fallthrough into the same InvalidTarget error the incomplete-version case already returns.

What was reviewed:

  • The v1./v0. arm restructure: !parsed.valid now returns InvalidTarget instead of falling out of the arm with this.version left at its default; the let ... else replaces the three unwrap()s with no behavior change on the valid path.
  • Error routing: CompileTarget::from maps InvalidTarget on any input containing v to the "complete version number" message, and compile_target_from_slice throws Unknown compile target: <full string> — both match what the new tests assert.
  • Tests cover both entry points (Bun.build and CLI) across the malformed shapes plus a control that names the running version so no lane downloads; the readdirSync check confirms nothing is written on the CLI reject path.
Extended reasoning...

Overview

The PR fixes a silent-failure bug in CompileTarget::try_from (src/options_types/compile_target.rs): when a --target token starts with v1./v0. but Version::parse returns valid = false (e.g. v1.2.3.4.5, v1..2, v0.a), the old code had no else branch and fell out of the arm without a continue, so the token was dropped and this.version kept its default (the running bun's version). The build then succeeded with the wrong version and exit code 0. The fix returns ParseError::InvalidTarget on !parsed.valid, matching what the incomplete-version case (major/minor/patch is None) already did. The arm is also restructured to use let (Some, Some, Some) = ... else in place of three unwrap()s, which is a pure refactor on the valid path.

Tests are added in test/bundler/bun-build-compile.test.ts: a test.each over 8 malformed/incomplete targets against both Bun.build (throws Unknown compile target: ...) and the CLI (exits 1 with the "complete version number" message, writes nothing), plus a control that compiles for bun-<host>-v<running version> to confirm the happy path still works without a download. An unrelated pre-existing test that compiles three binaries gets a 60s timeout to match its siblings under debug+ASAN.

Security risks

None. This is input validation on a CLI/API option string; the change strictly tightens validation (previously-ignored malformed tokens now error). No untrusted-data parsing, no memory management, no network or filesystem effects beyond what the existing code already did.

Level of scrutiny

Low-to-medium. The Rust change is ~20 lines with a net +2; the only behavioral delta is one new early-return on a path that was previously a silent no-op. I traced the error through both consumers: CompileTarget::from (CLI) hits the contains(input, b"v") branch and prints the version hint; compile_target_from_slice (Bun.build) throws with the full input string including the bun- prefix, which matches the test's .toThrow(Unknown compile target: ${target}). The UnsupportedTarget re-scan in from() still skips v1./v0. tokens, which is consistent because those now always resolve in try_from (either as a version or as InvalidTarget).

Other factors

  • The tests follow harness conventions: tempDir with using, bunEnv/bunExe, concurrent pipe drain via Promise.all, exit-code asserted last, test.each for the matrix.
  • The control test uses the host platform + running version so is_default() is true and self_exe_path() is used — no network on any CI lane.
  • The currentPlatformTarget computation was hoisted from an existing test to module scope; the original test now reuses it, no behavior change there.
  • The PR description explicitly verified the new tests fail without the src change and pass with it.
  • No prior human reviews or outstanding comments on the PR.

@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Heads up on the 60_000 added to "compile with relative outfile paths": #37417 splits that test into one compile per case instead, so if it lands first this hunk goes away on rebase (and if this PR lands first, #37417 rebases over it). No action needed either way, just cross-linking so the two do not surprise whoever merges second.

Jarred-Sumner pushed a commit that referenced this pull request Aug 11, 2026
### What

`test/bundler/bun-build-compile.test.ts` "compile with relative outfile
paths" ran three `Bun.build({ compile: { outfile } })` calls inside one
test. Every compile copies and rewrites the whole bun binary, which is
about 1 GB with a debug+ASAN build and takes roughly 2.5s there (every
single-compile test in the file reports 2.4s to 2.9s), so the test
needed 7s or more of a 5s default per-test budget. On the unmodified
file, `bun bd test test/bundler/bun-build-compile.test.ts` at
9fcdea8:

```
(fail) Bun.build compile > compile with relative outfile paths [5523.70ms]
  ^ this test timed out after 5000ms.
(pass) Bun.build compile > compile with embedded resources uses correct module prefix [5227.91ms]
```

The failure is intermittent (the session that reported it saw it in
about 1 run in 3, with the passing runs taking 7.0s to 7.2s). The
compile step runs on the JS thread inside the bundle completion task
(`do_compilation` is called from `JSBundleCompletionTask::on_complete`),
so the timeout can only be observed between compiles: when the first two
compiles add up to more than 5s the test fails after the second one,
otherwise the third compile starts and the test finishes late but
passes. The third compile of a timed-out run also keeps going and slows
down the next test, which is why "embedded resources" shows 5.2s above
and 2.5s once this is fixed.

This only affects the 5s default used by a plain `bun test` / `bun bd
test` run; the CI runner passes a much larger `--timeout`, and with a
release binary each compile takes about 0.13s.

### Fix

Turn the three cases into a `test.each`, so every test does exactly one
compile, the same shape as the rest of the file. The per-test timeout is
left at the default rather than raised: the workload per test is what
was out of proportion.

While rewriting the assertion, the three `toContain` / `toEndWith`
checks on the reported path become an exact comparison against the
outfile that was passed in (`.exe` appended on Windows; with no `outdir`
the artifact path is the outfile verbatim), and each case also checks
that the executable exists at that path, which is what the nested
`output/nested/` and `a/b/c/d/` cases are there to prove. Nothing else
in the file changes.

### Verification

`bun bd test test/bundler/bun-build-compile.test.ts` (debug+ASAN), two
runs, 14 pass / 1 skip each, about 40s per run:

```
(pass) Bun.build compile > compile writes the executable to outfile output/nested/app1 [2567.03ms]
(pass) Bun.build compile > compile writes the executable to outfile app2 [2576.66ms]
(pass) Bun.build compile > compile writes the executable to outfile a/b/c/d/app3 [2590.81ms]
(pass) Bun.build compile > compile with embedded resources uses correct module prefix [2501.16ms]
```

The same file also passes with a release bun on Linux
(`USE_SYSTEM_BUN=1`) and on Windows Server 2019 x64 (release bun 1.4.0,
where the exact path assertion exercises the `.exe` branch).

Related: #37389 raises this test's timeout to 60s as a side change of an
unrelated fix; whichever lands second drops or rebases a one-line hunk.
#37397 fixes the neighbouring "embedded resources" test in the same file
and does not overlap with this hunk.
CompileTarget::try_from only handled the case where the v1.x / v0.x
token parsed as a valid version. When Semver rejected the token
(bun-linux-x64-v1.2.3.4.5, -v1..2, -v0.a, -v1.2.3.) the branch fell
through without setting anything or returning an error, so the token
was dropped and the build compiled for the running version of bun.

Version::parse is also lenient about what it accepts: it turns
trailing text into a tag and reads an overflowing component as 0, so
-v1.2.3rc1 selected 1.2.3 and -v1.99999999999999999999.7 selected
1.0.7. Compare the parsed version against the token instead, so the
token has to name exactly the version that will be downloaded.

The version failure gets its own ParseError variant so the CLI message
no longer depends on substring matching of the whole target string,
which reported "musl libc only exists on linux" for bun-musl-v1.2.3.4.5.
Bun.build keeps throwing "Unknown compile target".

Tests run both entry points in child processes with the download
pointed at a local 404 server, so a token that gets past the parser
fails offline with a message naming the parsed target. That also
covers the well formed case: -v1.2.3 reaches the download step as
v1.2.3.
@robobun
robobun force-pushed the farm/cea4c58d/compile-target-reject-malformed-version branch from d40ddc3 to 89a441f Compare August 11, 2026 08:05
Comment thread src/options_types/compile_target.rs Outdated
@robobun robobun changed the title compile: reject --target version tokens that do not parse instead of building for the running version compile: reject --target version tokens that are not exactly vX.Y.Z Aug 11, 2026
Comment thread src/options_types/compile_target.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — focused parser tightening with a clear root cause and thorough offline tests.

What was reviewed:

  • parse_exact_version round-trip check covers all the malformed shapes (extra components, trailing text, overflow, unparseable) with one rule; valid vX.Y.Z still passes.
  • Removing the contains(input, b"v") heuristic from the InvalidTarget arm is safe: the version arm now always consumes v1./v0. tokens, and the remaining InvalidTarget producers (musl/android on non-linux, wasm) are still matched by their own substring checks.
  • compile_target_from_slice in options_jsc.rs ignores the error variant, so the Bun.build message is unchanged ("Unknown compile target: ...") as the tests assert.
  • Tests are hermetic (local 404 server + isolated cache dir), cover CLI and API, include a positive control, and follow harness conventions.
Extended reasoning...

Overview

Two files: src/options_types/compile_target.rs tightens the --target version-token parser and adds a ParseError::InvalidVersion variant with its own CLI message; test/bundler/bun-build-compile.test.ts adds a describe("version token in the target") block with 21 test cases across CLI and Bun.build entry points. The existing "compile with current platform target string" test is refactored to share the hoisted currentPlatformTarget constant.

Security risks

None. This is CLI/API argument validation for bun build --compile. The change strictly rejects more inputs than before; nothing new is accepted. No memory management, no untrusted parsing beyond what Version::parse already did, no network or filesystem changes.

Level of scrutiny

Low-to-medium. The parser is a one-shot CLI-arg path (not hot), the format! round-trip is a simple and robust way to enforce exact X.Y.Z, and the change is a strict tightening — the failure mode is a loud error where there was previously silent wrong behavior. I traced the two other consumers of ParseError: CompileTarget::from (CLI, updated in this PR) and compile_target_from_slice in bundler_jsc/options_jsc.rs (JS API, uses let Ok(...) else so the new variant maps to the existing "Unknown compile target" message without changes). I also confirmed the removed contains(input, b"v") heuristic is dead: after this change, InvalidTarget is only returned for libc-on-non-linux (input contains "musl"/"android") or wasm (input contains "wasm"), each of which has its own message branch, so nothing falls through to the generic fallback that previously needed the v check.

Other factors

Tests follow the repo's conventions closely: tempDir/bunEnv/bunExe, await using on spawns, concurrent stdout/stderr/exited drain, test.each for the matrix, port: 0 local server with afterAll cleanup, and a positive control that proves a valid v1.2.3 still reaches the download step with the version intact. The comment-cop bot flagged verbose comments twice; both were addressed in b1a14eb and 9bc500a (helper extracted, doc comment cut to one line), and those threads are resolved. The PR description documents that the new tests fail on main and pass with the fix.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant