Skip to content

bundler: fail the build when --bytecode cannot be generated - #36708

Open
robobun wants to merge 5 commits into
mainfrom
farm/01628365/bytecode-failure-exit-code
Open

robobun wants to merge 5 commits into
mainfrom
farm/01628365/bytecode-failure-exit-code

Conversation

@robobun

@robobun robobun commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #15528.

Problem

bun build --compile --bytecode prints error: Failed to generate bytecode for ... when JSC rejects the bundled output, but then proceeds to write the executable and exit 0. CI can't detect that the binary shipped without bytecode.

The --outdir (non --compile) path was worse: it swallowed the bytecode failure entirely (no message, no .jsc file, exit 0).

Repro

echo 'console.log(1)' > a.js
bun build a.js --compile --bytecode --banner='this is not valid js !!!' --outfile ./out
# error: Failed to generate bytecode for ./a.js
echo $?   # 0 (before), 1 (after)

The banner is prepended verbatim after bun's own parser runs, so JSC is the first thing to see the bad syntax; any real-world input that trips JSC's bytecode generator (the yargs example in the issue, sloppy-mode constructs in ESM strict mode, etc.) hits the same path.

Fix

Both chunk-generation paths (generateChunksInParallel.rs for in-memory/compile, writeOutputFilesToDisk.rs for --outdir) now return Err(BuildFailed) after logging the bytecode error, so bun build exits 1 and Bun.build() reports success: false.

This also makes a latent debug assertion in OutputFileList::take() unreachable: the output list is pre-sized on the assumption that every JS chunk produces a bytecode entry, and leaving that slot unfilled tripped total_insertions != output_files.len() in debug builds.

Verification

bundler > compile/BytecodeFailureIsAnError              (fails on main, passes here)
bundler > bytecode/OutdirBytecodeFailureIsAnError       (fails on main, passes here)

All 19 existing Bytecode tests in bundler_compile.test.ts and all of bundler_banner.test.ts still pass.


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

When JSC's bytecode generator rejects a bundled chunk, bun build
--bytecode would log 'error: Failed to generate bytecode for ...' but
then proceed to write the executable and exit 0, so CI could not detect
the failure. The --outdir (non --compile) path did not even log the
error.

Return Err(BuildFailed) from both chunk-generation paths after logging.
This also avoids a debug-build assertion in OutputFileList::take() that
fires when the pre-sized bytecode slot is left unfilled.

Fixes #15528
@coderabbitai

coderabbitai Bot commented Aug 1, 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: 10 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: e871efaf-7bda-432d-acd2-49b118886f31

📥 Commits

Reviewing files that changed from the base of the PR and between f91d5c9 and b06839f.

📒 Files selected for processing (3)
  • src/bundler/linker_context/generateChunksInParallel.rs
  • src/bundler/linker_context/writeOutputFilesToDisk.rs
  • test/bundler/bundler_compile.test.ts

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

@github-actions github-actions Bot added the claude label Aug 1, 2026
Comment thread src/bundler/linker_context/generateChunksInParallel.rs Outdated
@robobun

robobun commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 6:27 PM PT - Aug 1st, 2026

❌ @robobun, your commit b06839f has 2 failures in Build #87467 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 36708

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

bun-36708 --bun

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Failed to generate bytecode for ./index.js #15351 - Reports "Failed to generate bytecode for ./index.js" error is printed but the build still produces an executable and exits successfully, which is exactly the bug this PR fixes

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #15351

🤖 Generated with Claude Code

Comment thread src/bundler/linker_context/writeOutputFilesToDisk.rs
A CSS chunk created from a JS entry has a JS-like loader, so the
--outdir bytecode path was attempting (and, after the previous commit,
failing) bytecode generation on CSS text. Match the sibling guard used
by the --compile path and the OutputFileList pre-sizing.

@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 — the CSS-chunk guard I flagged earlier is in place with a regression test, and both bytecode paths now fail the build consistently.

What was reviewed:

  • Both chunk-emit paths (generateChunksInParallel.rs in-memory/compile, writeOutputFilesToDisk.rs --outdir) now log + return Err(BuildFailed) on None from generate_cached_bytecode; verified crate::Error::BuildFailed is the established bundler error variant and add_error_fmt returns () so dropping the let _ = is correct.
  • The new matches!(chunk.content, Content::Javascript(_)) guard on the --outdir path now matches the sibling in-memory path and the OutputFileListBuilder pre-sizing predicate, so CSS/HTML chunks are skipped and the take() insertion-count invariant holds.
  • Checked early-return cleanup: OwnedString RAII derefs source_provider_url, and intermediate_output is already restored to the chunk before the bytecode block in both files.
Extended reasoning...

Overview

Two-line native change in each of the two chunk-emit paths: when JSC bytecode generation returns None, log the existing "Failed to generate bytecode for …" message and return Err(crate::Error::BuildFailed) instead of falling through to a successful build. The --outdir path additionally gains a matches!(chunk.content, Content::Javascript(_)) guard (my prior review finding) so CSS chunks derived from a JS entry are not fed to the bytecode generator. Three tests cover --compile, --outdir, and the CSS-chunk-with-JS-entry case.

Security risks

None. This is bundler control-flow on an internal error path; no untrusted input parsing, no auth/crypto, no memory-safety changes (no new allocations, pointers, or unsafe). The early return happens after intermediate_output has been restored and while source_provider_url is held by an OwnedString whose Drop derefs it, so no new leak path.

Level of scrutiny

Low-to-moderate. The change is a focused bugfix for two user-filed issues (#15528, #15351): a build that prints error: and then exits 0 is unambiguously wrong. Err(BuildFailed) is the established idiom throughout bundle_v2.rs / computeChunks.rs for "logged an error, abort the build". The one non-trivial risk — turning a previously-silent None into a hard error for chunks that legitimately should not be bytecode-compiled — was the CSS-chunk case I raised on the last revision, and it is now guarded and regression-tested.

Other factors

  • All prior inline comments (comment-cop noise, my CSS-chunk finding) are resolved on the current commit.
  • add_error_fmt returns () (src/ast/lib.rs:1781), so removing the let _ = is a correct cleanup, not a discarded Result.
  • Partial --outdir output when bytecode fails mid-loop (earlier chunks already written) matches every other pre-existing error path in write_output_files_to_disk and is not a regression.
  • This is a user-visible behavior change (exit 0 → exit 1), but it is exactly what the linked issues request and the previous behavior was producing silently-broken binaries; not a design decision needing human sign-off.

@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 — the CSS-chunk guard I flagged earlier is in place and covered by bytecode/OutdirBytecodeSkipsCssChunk.

What was reviewed:

  • Both generate_cached_bytecode call sites now log + return Err(BuildFailed) on None; guards match the pre-sizing predicate in OutputFileListBuilder.rs:119, so the take() insertion-count assertion stays sound.
  • Confirmed add_error_fmt returns (), so dropping the let _ = is a no-op; log_disjoint() writes to the same Transpiler.log as log_mut().
  • Verified these are the only two generate_cached_bytecode callers in the bundler; no other silent-swallow siblings.
  • Early-return on the --outdir path leaves partial output (sourcemap already written) — consistent with every other error exit in that function.
Extended reasoning...

Overview

Two-line behavioral change replicated across the two chunk-generation paths: when JSC returns None from generate_cached_bytecode, log the existing "Failed to generate bytecode" error and return Err(crate::Error::BuildFailed) instead of falling through to break 'brk None. The --outdir path also gains the matches!(chunk.content, Content::Javascript(_)) guard (added after my previous review) so a CSS chunk born from a JS entry point does not attempt bytecode generation. Three tests cover the --compile failure path, the --outdir failure path, and the CSS-chunk-skips-bytecode regression.

Security risks

None. This is bundler error-handling flow control; no untrusted-input parsing, allocation sizing, FFI, or memory-lifetime changes. add_error_fmt returns () so removing let _ = is cosmetic.

Level of scrutiny

Moderate — it is a user-visible behavior change (builds that previously exited 0 with a printed error now exit 1), but that is the explicit fix for #15528/#15351 and matches REVIEW.md's "operations the user explicitly requested fail the whole command on any failure — never warn-and-exit-zero." --bytecode is an explicit request; shipping a binary without the requested bytecode is a silent failure. The change is small, mechanical, and mirrors the established add_error_fmt + return Err(BuildFailed) pattern used throughout bundle_v2.rs and LinkerContext.rs.

Other factors

  • My prior review flagged the missing Content::Javascript guard on the --outdir path; the author fixed it in 2d79296 and added a dedicated regression test. All three bytecode sites (generateChunksInParallel.rs, writeOutputFilesToDisk.rs, OutputFileListBuilder.rs:119) now share the same predicate, so the pre-sized total_insertions == output_files.len() invariant holds.
  • Grep confirms only two callers of generate_cached_bytecode in the bundler; both are patched.
  • Tests follow harness conventions: tempDir, bunEnv/bunExe, concurrent pipe drains, stderr asserted before exit code, issue URL comment. The banner-based repro is a clean way to make JSC (not bun's parser) reject the bundle.
  • The comment-cop bot's flag on the redundant comment was addressed in 35056f2; all inline threads are resolved.

@robobun

robobun commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator Author

CI status: the diff itself is green. bundler_compile.test.ts (which carries the three new tests) passed on every lane in build 87467.

The red on that build is unrelated to this change:

  • bun-upgrade.test.ts on windows-aarch64 (Canary builds are not available for this platform yet), reported for main-break triage
  • AWS EC2 agent-creation failure (infra)
  • the rest are tagged [flaky] (CPU-time/memory thresholds, parallel-batch timeouts) and passed when retried or run alone

Ready for review/merge.

@robobun

robobun commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator Author

Verified again on top of current main (1ab272b833, merged cleanly) with a debug build. A new trigger for the same failure surfaced in a fuzz matrix: an entry point that calls import.meta.require(...) or import.meta.resolve(...) under --bytecode with the default cjs format. JSC rejects import.meta in the CommonJS wrapper, so bytecode generation fails for that chunk.

On main, that input panics: the debug build asserts total_insertions (1) != output_files.items.len (2) in OutputFileList::take, and the release build panics with index out of bounds: the len is 8 but the index is 18446744073709551615 once there are 5 entry points and a with { type: "file" } asset (the truncated list loses the asset). With this branch all three forms exit 1 with error: Failed to generate bytecode for ./imr.js and no executable is written:

bun build --target=bun --bytecode ./imr.ts --outdir out
bun build --target=bun --bytecode ./far.ts ./e1.ts ./e2.ts ./e3.ts ./e4.ts --outdir out
bun build --compile --bytecode ./bc.ts ./b.ts ./c.cjs --outfile app

The three tests in this PR and the other 25 Bytecode tests in bundler_compile.test.ts pass on that build. #39715 removes this particular trigger (it prints import.meta as the wrapper argument), but the exit code fix here is still needed for every other input JSC rejects.

@robobun

robobun commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Another trigger for the same failure, from a fuzz matrix. This one is an ESM-goal JSC rejection, so the import.meta work in #39715 cannot remove it: a CommonJS dependency with sloppy-mode-only syntax is bundled into an ESM chunk, and JSC's module-goal parser rejects with.

printf 'var o = { x: "with-x" };\nwith (o) { exports.v = x; }\n' > s.cjs
printf 'import { v } from "./s.cjs";\nconsole.log("v =", v);\n' > entry.ts
bun run entry.ts                  # v = with-x
bun build --compile --bytecode --format=esm ./entry.ts --outfile app; echo rc=$?; ./app

Verified on 1.4.3-canary.1+f42e98025 (release): error: Failed to generate bytecode for ./app, rc 0, the executable is written, and it dies with SyntaxError: 'with' statements are not valid in strict mode.. On a debug build of main (5f554969bc) it asserts total_insertions (2) != output_files.items.len (3) in OutputFileList::take and writes nothing (rc 134). N is 2 against 3 here because the ESM bytecode payload also pre-counts the module-info output.

This branch merged onto main (5f554969bc, one trivial conflict in writeOutputFilesToDisk.rs where main moved to the path-buffer pool) fixes all three forms: the --compile form exits 1 with the bytecode error and no executable, Bun.build({ compile, bytecode, format: "esm" }) returns success: false with the same log and writes nothing, and --outdir --bytecode --format=esm is now rejected earlier by the "ESM bytecode requires --compile" check. All 28 Bytecode tests in bundler_compile.test.ts pass on that build.

An extra case for this PR, if you want ESM coverage next to the two --banner ones. It passes with this branch and fails on the released build:

// A CommonJS dependency with sloppy-mode-only syntax is bundled into an ESM
// chunk, which JSC's module-goal parser rejects at bytecode generation time.
// The build must fail instead of writing an executable without bytecode.
test("compile/BytecodeFailureIsAnErrorESM", async () => {
  using dir = tempDir("bytecode-failure-esm", {
    "entry.ts": `import { v } from "./s.cjs";\nconsole.log("v =", v);\n`,
    "s.cjs": `var o = { x: "with-x" };\nwith (o) { exports.v = x; }\n`,
  });
  await using proc = Bun.spawn({
    cmd: [bunExe(), "build", "./entry.ts", "--compile", "--bytecode", "--format=esm", "--outfile", "./out"],
    env: bunEnv,
    cwd: String(dir),
    stdout: "pipe",
    stderr: "pipe",
  });
  const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
  expect(stderr).toContain("error: Failed to generate bytecode");
  expect(stdout).not.toContain("compile");
  expect(existsSync(join(String(dir), process.platform === "win32" ? "out.exe" : "out"))).toBe(false);
  expect(exitCode).toBe(1);
});

The executable dying without --bytecode as well is a separate issue (sloppy .cjs code bundled as ESM), not this one.

@robobun

robobun commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator Author

Another report of the same failure. This trigger is a published package, not a synthetic input.

@remix-run/react@2.10.3 reads bare import.meta (dist/esm/components.js:784, let isViteClient = import.meta && import.meta.env !== undefined;, and dist/esm/browser.js:46). With --bytecode and the default cjs format, the bundle keeps import.meta inside the CommonJS wrapper, and JSC rejects it.

cd test/node_modules
bun build @remix-run/react/dist/esm/index.js --target=bun --bytecode --outdir /tmp/out
  • Release 1.4.3-canary.1+09bb54630: exit 0, no message. It writes index.js and no index.js.jsc. bun /tmp/out/index.js then throws SyntaxError: import.meta is only valid inside modules.
  • Debug build of 09bb54630: panic: total_insertions (1) != output_files.items.len (2), exit 134.

The writeOutputFilesToDisk.rs hunk in this PR covers this path. It returns Err(BuildFailed) through the ? at generateChunksInParallel.rs:857, before OutputFileList::take() runs at line 1384. #39715 removes this one trigger.

This branch now conflicts with main. The conflict is one hunk in writeOutputFilesToDisk.rs. Main changed PathBuffer::uninit() to bun_paths::path_buffer_pool::get() on the line next to the new matches!(chunk.content, Content::Javascript(_)) guard. Keep the line from main and add the guard.

@robobun

robobun commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator Author

Update, and a correction to my two earlier notes here. #43822 merged (ba1faad337), and it changes what this PR is about.

What #43822 settled

  • The crash is gone. OutputFileList::take now drops the slots that were counted and never claimed, so the total_insertions != output_files.items.len assert and the release index out of bounds panic no longer reproduce with any of the triggers I listed above (import.meta in the cjs wrapper, the sloppy with in a .cjs dependency).
  • The CSS chunk guard in this PR is superseded by LinkerContext::chunk_gets_bytecode, which the count and both writers now share.
  • A chunk without bytecode is now a supported state. The new test in test/bundler/bun-build-compile.test.ts expects build.success === true and an executable whose bad chunk runs from source and throws SyntaxError only when it is imported.

What is left of this PR
Only the return Err(BuildFailed) in the two writers, and that is the opposite answer to the merged test. So this is no longer a rebase problem (GitHub shows the branch as conflicting). It is a decision about #15528. I said before that this PR "is still needed for every other input JSC rejects". That was true for the panic, which is fixed. For the exit code it is a design choice, not a bug fix, and I should not have stated it as settled.

State on main today, for whoever decides

  • --compile --bytecode and Bun.build: the log gets an error-level Failed to generate bytecode for <chunk> (generateChunksInParallel.rs:1162, add_error_fmt), the build reports success, exit code 0. So the CLI prints error: and exits 0.
  • --outdir --bytecode: the disk writer has no else branch (writeOutputFilesToDisk.rs:389). It prints nothing, writes no .jsc, and the .js file still carries the // @bun @bytecode pragma.

The two options

  1. Close this PR. "Runs from source" is the chosen behaviour. If so, a small follow-up could make the message a warning and also print it in the --outdir path, so the CLI does not print error: with exit code 0. That would answer Bun exits successfully when bytecode compilation failed #15528 as "by design, now visible".
  2. Keep the failure semantics of this PR. Then it needs a rebase onto chunk_gets_bytecode, and the new test in bun-build-compile.test.ts has to change from success: true to a failed build.

The ESM test case I posted above asserts option 2. Ignore it if the answer is option 1. I am not changing anything here until a maintainer picks one.

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.

Bun exits successfully when bytecode compilation failed

2 participants