Skip to content

build: bun-standalone — reduced-footprint --compile runtime - #32262

Open
Jarred-Sumner wants to merge 12 commits into
mainfrom
claude/bun-standalone
Open

build: bun-standalone — reduced-footprint --compile runtime#32262
Jarred-Sumner wants to merge 12 commits into
mainfrom
claude/bun-standalone

Conversation

@Jarred-Sumner

@Jarred-Sumner Jarred-Sumner commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

What

Adds a second build of the bun executable — bun-standalone — with the toolkit subcommands and the JS APIs that back them compiled out. bun build --compile uses this as its embedded runtime by default so single-file executables ship without the bundler, package manager, or test runner.

Size (linux-x64 release, non-LTO, this branch)

bytes MB
stripped bun 70,389,048 67.13
stripped bun-standalone 62,392,896 59.50
delta −7,996,152 −7.63 (−11.4%)

bloaty section diff: .text −6.80 MB, .rodata −849 KB.

crate full MB standalone MB Δ
bun_runtime 6.45 4.75 −1.70
bun_install 2.03 0.03 −2.00
bun_css 1.77 0 −1.77
bun_bundler 1.61 0.44 −1.17
bun_css_jsc 0.10 0 −0.10
bun_install_jsc 0.05 0 −0.05

The remaining bun_bundler 0.44 MB is the Transpiler half (single-file TS→JS, options/defines/cache, analyze_transpiled_module) which is structurally embedded in VirtualMachine and required by the module loader.

< 35 MB isn't reachable from Rust gating: JSC 22.9 MB + ICU data 23.7 MB + bindings/crypto/codecs ≈ 57 MB floor. The next lever is small-icu (~5 MB instead of 24 MB), a WebKit-prebuilt change.

How

  • Build system: Config.standalone drives a second cargo build -p bun_bin --features standalone (with RUSTFLAGS=--cfg=bun_standalone and a separate rust-target-standalone/ dir) and a second link producing bun-standalone[-profile]. The C++ object set is unchanged — both binaries link the same libbun-profile.a; --gc-sections + .llvm_addrsig drop the C++ functions whose only Rust callers are gated.
  • CI: each release platform gets <target>-build-rust-standalone and <target>-build-bun-standalone steps that reuse the existing <target>-build-cpp archive. binary-size tracks both variants. Release/upload-release.sh/upload-npm publish bun-standalone-<triplet>.zip and @oven/bun-standalone-* packages. Test runners soft-download bun-standalone (runner.node.mjs --standalone-step) and export BUN_STANDALONE_EXE so test/cli/standalone-binary.test.ts exercises it in CI without blocking.
  • Rust gating (cfg(bun_standalone)):
    • CLI: dispatch reduced to run/exec/repl/help; 27 toolkit pub mod declarations cfg'd out; runtime-reachable items (FileSystemTmpdirExt, Bun__githubURL, release-artifact name consts) relocated to cli/shared.rs; run_command/shell_completions bun_install refs cfg-split.
    • bake: pub mod bake replaced by bake_standalone_stub.rs — uninhabited DevServer/HotReloadEvent so Option<Box<DevServer>> is a ZST; throwing JSFrameworkRouter; C-ABI BakeProd*/Bake__* stubs; unreachable __bun_dispatch__DevServerHandle__Bake__* link stubs for debug builds.
    • server: AnyRoute::FrameworkRouter variant + every dev_server/bake field access cfg-gated; HTMLBundle bundle_v2-typed internals gated.
    • api: Bun.build / JSBundlerPlugin__* / __bun_blob_from_build_artifact throw; js_bundle_completion_task/output_file_jsc modules + BundleV2DeferredBatchTask arm + EXTERNAL_FREE_VTABLE registration gated.
    • jsc: hot_reloader BundleV2 impl gated; AsyncModule install-queue machinery gated; VirtualMachine PackageManager log-swap gated.
    • Bun.color / 8× JS2Zig__css_internals_* throw; bun:test C-ABI entry points throw / no-op; __bun_resolver_init_package_manager unreachable; dispatch_js2native install_jsc/patch_jsc hooks stubbed; bun_standalone_graph write side gated.
  • --compile integration: new CompileRuntime { Standalone (default), Full } on CompileTarget; --compile-runtime=<standalone|full> flag; npm URL → @oven/bun-standalone-*; cache key and tarball basename follow the variant; is_default() no longer short-circuits to self_exe_path() for standalone.
  • Version string: --version/--revision/crash-reporter footer report 1.4.0-standalone[-canary.N][+sha] so CI annotations and bun.report distinguish the binaries.

Tested

  • cargo check -p bun_bin and cargo check -p bun_bin --features standalone --cfg=bun_standalone clean across all 10 rust:check-all targets.
  • bun-standalone (debug+release) builds, links, smoke-tests; toolkit subcommands print an actionable error and exit 1; Bun.build/Bun.color throw TypeError; Bun.serve/fetch/-e work.
  • test/cli/standalone-binary.test.ts (6 tests) passes against bun-standalone-debug and fails against full bun-debug.
  • test/cli/bun.test.ts + test/bundler/bun-build-api.test.ts (54 tests) pass on full bun-debug — no regression.
  • .buildkite/ci.mjs generates valid YAML with 11× build-rust-standalone + 11× build-bun-standalone steps.

Adds Config.standalone driving a second cargo build (--features standalone,
--cfg=bun_standalone, separate rust-target-standalone/ dir) and a second
link producing bun-standalone[-profile]. CI gets <target>-build-rust-standalone
and <target>-build-bun-standalone steps that reuse the existing build-cpp
archive. CLI dispatch is reduced to the run path under cfg(bun_standalone);
toolkit subcommands print an actionable error and exit 1.

Both configs cargo-check clean; bun-standalone-debug builds, smoke-tests,
and passes test/cli/standalone-binary.test.ts.
- upload-release.sh: add bun-standalone-*.zip to the artifact list
- bun-release: add standalonePlatforms (derived from platforms minus
  android/freebsd) and publish @oven/bun-standalone-* alongside @oven/bun-*
- ci.mjs: track bun-standalone in binary-size step; release step depends
  on -build-bun-standalone
…r cfg(bun_standalone)

Stubs every #[no_mangle] symbol the shared C++ archive references for the
toolkit subsystems, so the same libbun.a links into both bun and
bun-standalone while gc-sections drops the now-unreferenced Rust impls:

- Bun.build: js_bundler_build adapter throws; JSBundlerPlugin__{addError,
  onLoadAsync,onResolveAsync,onDefer} unreachable!(); HTMLBundle route in
  Bun.serve throws; __bun_blob_from_build_artifact returns None.
- Bun.color: BunObject_callback_color throws; the 8 css_internals js2native
  hooks throw.
- bun:test: Bun__Jest__createTestModuleObject + Expect_* C-ABI helpers
  throw / return false; module stays compiled for codegen Expect* classes.
- bake: Bake__* / BakeProd* / BakeResponseClass__* / DevServer testing hook
  throw / return null.
- install: __bun_resolver_init_package_manager unreachable; PackageManager
  init_with_runtime gated; install-queue enqueue/on_poll severed.
- standalone_graph: write side (to_bytes/inject/download_to_path) gated;
  to_executable stub kept for build_command call sites; dead bun_js_parser
  dep dropped.
- CompileTarget: new CompileRuntime {Standalone (default), Full} field;
  --compile-runtime flag; npm URL @oven/bun-standalone-*; cache key and
  tarball basename follow the runtime variant; is_default() no longer
  short-circuits to self_exe_path() for standalone.
- CI: test runners soft-download bun-standalone via runner.node.mjs and
  export BUN_STANDALONE_EXE; windows-sign covers bun-standalone-windows-*.
The toolkit *_command modules were previously declared with
allow(dead_code) under bun_standalone, which still compiled their bodies
and kept calls into bun_install / bundle_v2 / bun_css alive in the link.
Gate the module declarations themselves so the compiler never sees them.

- New cli::shared module hosts the runtime-reachable items that lived in
  upgrade_command (FileSystemTmpdirExt, Bun__githubURL, release-name
  consts, BUN__GITHUB_BASELINE_URL); jsc_hooks/ffi_body/bun_bin updated.
- upgrade_command and pack_command get cfg(bun_standalone) stubs that
  satisfy the generated_js2native thunk signatures and throw at runtime.
- pm_print_help and its bun_install-backed match arms gated.
- lib.rs crate-root re-export split into always-on vs toolkit halves.

test_command + cli::test stay declared: test_runner depends on
CommandLineReporter and is referenced from jsc_hooks/dispatch/timer/
BunObject/spawn; severing that is a separate change.
Adds STANDALONE_SUFFIX to package_json_version{,_with_canary,_with_sha,
_with_revision} so --version, --revision, the unhandled-error footer,
and the bun.report crash payload distinguish bun-standalone from bun.
@robobun

robobun commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator
Updated 10:05 AM PT - Jun 15th, 2026

@Jarred-Sumner, your commit f1a9925 has 1 failures in Build #62561 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32262

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

bun-32262 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Use a minimal runtime for binary executables #14546 - Requests a minimal runtime for compiled executables, which is exactly what the bun-standalone build variant implements

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

Fixes #14546

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Jarred-Sumner, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 51 minutes and 36 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

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.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8804a150-150b-490c-8192-775d92f12215

📥 Commits

Reviewing files that changed from the base of the PR and between 87dbc85 and f1a9925.

📒 Files selected for processing (20)
  • scripts/build/bun.ts
  • src/codegen/class-definitions.ts
  • src/codegen/generate-classes.ts
  • src/jsc/generated_classes_list.rs
  • src/resolver/node_fallbacks.rs
  • src/runtime/api.rs
  • src/runtime/api/BunObject.rs
  • src/runtime/api/bun/js_bun_spawn_bindings.rs
  • src/runtime/api/standalone_api_stubs.rs
  • src/runtime/cli/Arguments.rs
  • src/runtime/cli/mod.rs
  • src/runtime/dispatch.rs
  • src/runtime/jsc_hooks.rs
  • src/runtime/lib.rs
  • src/runtime/standalone_build.rs
  • src/runtime/test_runner/jest.classes.ts
  • src/runtime/test_runner_standalone_stub.rs
  • src/runtime/timer/mod.rs
  • src/runtime/timer/timer_object_internals.rs
  • test/internal/dead-code-escape-limits.json

Walkthrough

Introduces a bun-standalone second executable variant built with a standalone Cargo feature and --cfg=bun_standalone RUSTFLAG. This variant removes bundler, package manager, test runner, Bake, and CSS CLI subcommands via conditional compilation, and supports single-file executable production via a new --compile-runtime CLI flag. Wires up CI pipeline orchestration with separate Rust target directories, release upload/npm publish paths, test runner integration, and integration tests covering command availability and API stubs.

Changes

bun-standalone build variant

Layer / File(s) Summary
Build config: standalone flag, naming helpers, Cargo features
Cargo.toml, scripts/build/config.ts, scripts/build.ts, scripts/build/buildOptionsRs.ts, src/bun_bin/Cargo.toml, src/runtime/Cargo.toml, package.json
Adds standalone: boolean to Config/PartialConfig, introduces bunStrippedName/bunExeName standalone-aware helpers, registers --standalone as a boolean CLI override, emits STANDALONE_BUILD Rust constant, adds standalone Cargo features to bun_bin and bun_runtime, adds npm build:standalone scripts, and allowlists cfg(bun_standalone) workspace-wide.
Rust build pipeline: target-dir, LTO, link, strip
scripts/build/rust.ts, scripts/build/bun.ts
Routes standalone builds to rust-target-standalone, passes --cfg=bun_standalone and the standalone Cargo feature, renames the LTO object to bun_rust_standalone.lto.o, fixes emitLinkOnly archive path derivation to use standalone: false, and changes emitStrip output to use bunStrippedName.
CI artifact naming, triplet derivation, and Buildkite step download
scripts/build/ci.ts
Changes computeBunTriplet to use bunStrippedName, extends packageAndUpload to use crossFeaturesJson for standalone builds, derives bunPath with proper name replacement for standalone profiles, and extends downloadArtifacts to parse an optional -standalone suffix from BUILDKITE_STEP_KEY.
cli/shared.rs: shared constants, FileSystemTmpdirExt, Bun__githubURL
src/runtime/cli/shared.rs, src/runtime/cli/upgrade_command.rs, src/runtime/ffi/ffi_body.rs, src/runtime/jsc_hooks.rs, src/bun_bin/lib.rs
Creates shared.rs with the release constants module, FileSystemTmpdirExt trait, BUN__GITHUB_BASELINE_URL, and C-ABI Bun__githubURL static. Refactors upgrade_command.rs to re-export from shared and removes the local Bun__githubURL symbol. Updates call sites.
Version strings: STANDALONE_SUFFIX in Global.rs and CompileRuntime enum
src/bun_core/Global.rs, src/options_types/compile_target.rs
Adds STANDALONE_SUFFIX and threads it through all package_json_version* constants. Introduces CompileRuntime enum with npm_prefix(), adds runtime field to CompileTarget, updates equality, default, npm URL construction, and Display.
standalone_build.rs, CLI dispatch split, and --compile-runtime argument
src/runtime/standalone_build.rs, src/runtime/cli/mod.rs, src/runtime/cli/Arguments.rs, src/runtime/cli/build_command.rs, src/runtime/lib.rs
Adds IS_STANDALONE and unavailable_command helpers. Splits Command::start to a reduced tag set under bun_standalone. Gates toolkit subcommand modules, exec helpers, and PM help arms. Adds --compile-runtime CLI option. Updates crate-root re-exports.
Bake stub module: bake_standalone_stub.rs for empty DevServer and routes
src/runtime/bake_standalone_stub.rs
Adds cfg(bun_standalone) replacement for the bake module with uninhabited DevServer, route, and router types. Exports C-ABI functions returning dead values and JS host-call stubs that throw errors. Includes dispatch-stub macros for link-time symbol resolution.
Runtime API stubs: Bun.build, Bun.color, JSBundler plugin callbacks
src/runtime/api/BunObject.rs, src/runtime/api/JSBundler.rs
Bun.build and Bun.color throw TypeErrors. JSBundler stubs the build() function and all four plugin callbacks with unreachable!() or type error returns. __bun_blob_from_build_artifact returns None in standalone.
Bake runtime API stubs: DevServer, production bake, responses
src/runtime/bake/..., src/runtime/webcore/BakeResponse.rs
DevServer.rs throws errors for bundle/route functions. BakeResponse.rs throws errors for SSR construction. production.rs stubs four FFI entrypoints to return dead values. mod.rs suppresses unused warnings in standalone.
Test runner API stubs: bun:test unavailable in standalone
src/runtime/test_runner/...
bun_test.rs, diff_format.rs, expect.rs, and jest.rs stub test runner APIs with UNDEFINED returns, false returns, or "bun:test unavailable" errors.
Server routes: HTML, framework router, directory routes gated
src/runtime/server/...
Conditionally compiles bake field and adds has_bake() helper. Throws errors for HTML/directory routes in standalone. Gates js_string_allocations field and plugin callback DevServer wiring. Gates FrameworkRouter variant and memory cost. Changes State::Building payload and splits on_plugins_resolved logic.
Dispatch: async modules, bundle tasks, hot reloader gated
src/runtime/dispatch*.rs, src/jsc/..., src/runtime/allocators/mod.rs, src/runtime/api.rs
Gates PollPendingModulesTask on_poll logic, splits BundleV2DeferredBatchTask to throw in standalone, stubs install/patch/CSS exports with type errors, gates async module enqueue path, gates html_rewriter/native_promise_context modules, gates hot-reloader implementation.
Package manager and auto-installer gated
src/install/..., src/runtime/cli/run_command.rs, src/runtime/node.rs
Gates init_with_runtime* functions and auto-installer hook. Standalone run-script replacement skips PM delegation. Windows .bunx fast-path disabled. Shell-completions gated. MaybeCssExt trait gated.
AsyncModule queue API gated in standalone
src/jsc/AsyncModule.rs
Gates entire impl Queue and impl AsyncModule method sets behind cfg(not(bun_standalone)).
StandaloneModuleGraph: cfg gating, to_executable stub, CompileRuntime download selection
src/standalone_graph/...
Gates compile/inject/serialize functions. Adds standalone to_executable stub. Changes binary source filename selection to use target.runtime. Removes unused dependency.
Buildkite CI pipeline: standalone Rust/link steps, signing, size tracking, release deps
.buildkite/ci.mjs
Adds standalone step builders and shouldBuildStandalone() gate. Extends getBuildArgs/getBuildCommand with extra.standalone. Wires standalone steps into test, signing, size, and release orchestration.
Release upload and npm publish: standalone artifacts
.buildkite/scripts/upload-release.sh, packages/bun-release/src/platform.ts, packages/bun-release/scripts/upload-npm.ts
Adds standalone_artifacts list and best-effort upload loop. Exports standalonePlatforms. Updates upload-npm.ts to iterate allPlatforms and derive exeBase for standalone zip extraction.
Test runner: standalone-step download and BUN_STANDALONE_EXE injection
scripts/runner.node.mjs
Adds --standalone-step option, downloads standalone zip from Buildkite, and sets BUN_STANDALONE_EXE without blocking the test run on failure.
Integration tests and documentation
test/cli/standalone-binary.test.ts, docs/standalone-binary.md, test/internal/dead-code-escape-limits.json
Adds subprocess tests covering unavailable-command errors, --revision suffix, -e execution, Bun.build/Bun.color errors, Bun.serve end-to-end, and process.isBun. Adds documentation describing the build mode, removed subcommands, size measurements, and follow-up work. Updates dead-code escape limits configuration.

Possibly related PRs

  • oven-sh/bun#31412: Both PRs modify scripts/build/rust.ts and scripts/build/bun.ts around cross-language Rust↔C++ LTO via rustLtoLinkInputs; the main PR further changes its standalone-specific output filename behavior.
  • oven-sh/bun#30749: Both PRs modify scripts/build/buildOptionsRs.ts to extend the generated build_options.rs constants; the main PR adds STANDALONE_BUILD.
  • oven-sh/bun#30880: Both PRs touch __bun_resolver_init_package_manager in src/install/auto_installer.rs; the main PR gates it with #[cfg(not(bun_standalone))] plus a standalone stub.

Suggested reviewers

  • alii
  • dylan-conway

Comment on lines +288 to +296
# bun-standalone-* zips ship alongside the regular zips. Derived from the
# main artifact list so a new platform can't be forgotten here. Best-effort:
# a missing standalone artifact warns but doesn't abort the release
# (download_buildkite_artifact's `exit 1` only kills the subshell).
for artifact in "${artifacts[@]}"; do
local standalone="${artifact/bun-/bun-standalone-}"
( upload_artifact "$standalone" ) || echo "warn: skipping missing standalone artifact: $standalone"
done

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.

🔴 Two mutually-exclusive approaches for uploading standalone artifacts are both active: line 269 appends standalone_artifacts into artifacts (so the first loop already uploads them with hard-fail semantics), and the second loop at 292-295 then iterates the same extended array applying ${artifact/bun-/bun-standalone-}, re-uploading every standalone zip a second time and deriving bogus names like bun-standalone-standalone-darwin-aarch64.zip and bun-standalone-linux-aarch64-android.zip. Either drop artifacts+=(...) (and keep the derivation loop over the original 30 entries) or drop the second loop — as written every release does ~22 redundant uploads plus ~30 failed downloads, and the "best-effort" comment is wrong since the first loop's download_buildkite_artifact will exit 1 the whole script if any standalone zip is missing.

Extended reasoning...

What the bug is

create_release() now contains two conflicting implementations of "upload the standalone zips" that are both executing:

  1. Explicit list, appended to the main array (lines 245-269): a hand-maintained standalone_artifacts=(...) array is appended to artifacts via artifacts+=("${standalone_artifacts[@]}"). After this, artifacts has 52 entries (30 regular + 22 standalone).
  2. Derivation loop (lines 288-295): iterates "${artifacts[@]}" again and computes ${artifact/bun-/bun-standalone-} for each, uploading the result in a subshell.

The comment on the second loop says "Derived from the main artifact list so a new platform can't be forgotten here" — but the main artifact list is no longer just the 30 regular zips; it's the 52-element combined array.

Step-by-step trace

After line 269, artifacts = 30 regular entries + 22 bun-standalone-* entries.

First loop (284-286)for artifact in "${artifacts[@]}"; do upload_artifact "$artifact"; done:

  • Uploads all 30 regular zips (correct).
  • Uploads all 22 bun-standalone-* zips. upload_artifact calls download_buildkite_artifact not in a subshell, and that function does exit 1 on line 134 if the artifact is missing. So if any standalone build failed to produce an artifact, the entire release script aborts here — directly contradicting the "Best-effort: a missing standalone artifact warns but doesn't abort the release" comment on the second loop.

Second loop (292-295) — iterates the same 52 entries and applies ${artifact/bun-/bun-standalone-} (replaces the first bun- occurrence):

  • bun-darwin-aarch64.zipbun-standalone-darwin-aarch64.zipalready uploaded by loop 1; downloaded and uploaded to S3/GitHub a second time. (22 such duplicates.)
  • bun-linux-aarch64-android.zipbun-standalone-linux-aarch64-android.zip — never built (shouldBuildStandalone excludes android/freebsd); download_buildkite_artifact fails, the subshell exits 1, prints warn: skipping.... (8 such bogus android/freebsd derivations.)
  • bun-standalone-darwin-aarch64.zipbun-standalone-standalone-darwin-aarch64.zip — nonsense filename, doesn't exist; warns. (22 such double-prefixed names.)

Net per release: 22 redundant download+upload cycles (~hundreds of MB to S3/GitHub) and 30 spurious "warn: skipping missing standalone artifact" lines.

Why existing code doesn't prevent it

gh release upload --clobber and S3 cp are idempotent, so the duplicate uploads don't corrupt the release — they just waste time and bandwidth. The subshell wrapper on the second loop catches the exit 1 for the bogus names, so the script still completes. But the first loop has no such wrapper, so the "best-effort" intent for standalone artifacts is not actually honored: a single missing bun-standalone-*.zip kills the canary release before it reaches update_github_release.

Impact

  • Every canary release performs ~22 redundant artifact downloads + 66 redundant uploads (S3 ×2 + GitHub) and emits ~30 warning lines for nonexistent files.
  • A missing standalone artifact aborts the entire release (hard exit 1 from the first loop), contrary to the documented best-effort intent.
  • The "derived so a new platform can't be forgotten" comment is misleading — the explicit standalone_artifacts list is now the source of truth.

How to fix

Pick one approach:

Option A (keep the explicit list): delete the second loop (lines 288-295) entirely. The first loop already handles everything. If best-effort semantics are desired for standalone, wrap those entries in a subshell or iterate standalone_artifacts separately with the ( ... ) || warn pattern.

Option B (keep derivation): delete artifacts+=("${standalone_artifacts[@]}") and the standalone_artifacts array; have the second loop iterate the original 30-element artifacts array, and skip android/freebsd entries (e.g. [[ $artifact == *-android* || $artifact == *freebsd* ]] && continue).

Option A is simpler given the explicit list already exists and matches shouldBuildStandalone.

Comment thread src/bun_core/Global.rs
Comment on lines 440 to 446
/// Does not have the canary tag, because it is exposed in `Bun.version`
/// "1.0.0" or "1.0.0-debug"
pub const package_json_version: &str = if cfg!(debug_assertions) {
concatcp!(version_string, "-debug")
concatcp!(version_string, STANDALONE_SUFFIX, "-debug")
} else {
version_string
concatcp!(version_string, STANDALONE_SUFFIX)
};

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.

🔴 Adding STANDALONE_SUFFIX to package_json_version leaks into surfaces the PR description didn't target: in standalone release builds Bun.version / process.versions.bun become "1.4.0-standalone" (so semver.gte(Bun.version, '1.4.0') is false in every --compile output), and Bun__githubURL / BUN__GITHUB_BASELINE_URL in cli/shared.rs become .../releases/download/bun-v1.4.0-standalone/... — a 404, since releases are tagged bun-v1.4.0. Consider keeping package_json_version as the clean version_string and adding STANDALONE_SUFFIX only to the with_canary/with_sha/with_revision variants (and building the GitHub URLs from the raw version_string).

Extended reasoning...

What the bug is

This PR injects STANDALONE_SUFFIX ("-standalone" under cfg(bun_standalone)) into package_json_version in src/bun_core/Global.rs. The doc comment on that constant says "Does not have the canary tag, because it is exposed in Bun.version" — i.e. it's deliberately the clean x.y.z string for JS-API consumption. The PR description and the STANDALONE_SUFFIX doc comment scope the suffix to --version/--revision/crash-reporter, but package_json_version flows to several other places that now break in the default --compile output (since CompileRuntime::Standalone is the new default).

Code paths and concrete effects

1. Bun.version / process.versions.bun change semver ordering. Bun__version (src/runtime/node/node_process.rs:92) is "v" + Global::package_json_version, consumed by Bun.version (BunObject.cpp:288, Bun__version + 1) and process.versions.bun (BunProcess.cpp:216). In a release standalone binary these now return "1.4.0-standalone". Per the semver spec, a prerelease compares less than the bare release: semver.gte('1.4.0-standalone', '1.4.0')false. So user code that feature-gates on Bun.version will believe a compiled app is running a pre-1.4.0 Bun. Naive parsers (Bun.version.split('.').map(Number)) get [1, 4, NaN]. The same string also propagates into user_agent (Global.rs: "Bun/" + package_json_version) → Bun-User-Agent: Bun/1.4.0-standalone on every outbound fetch.

2. GitHub release URLs 404. The newly-created src/runtime/cli/shared.rs (which now compiles under cfg(bun_standalone), unlike the old upgrade_command.rs location) builds:

"https://github.com/oven-sh/bun/releases/download/bun-v" + Global::package_json_version + "/" + ZIP_FILENAME

for both Bun__githubURL and BUN__GITHUB_BASELINE_URL. In a release standalone binary that's .../download/bun-v1.4.0-standalone/bun-linux-x64.zip, but releases are tagged bun-v1.4.0 — so process.release.sourceUrl (BunProcess.cpp:273) is a 404, and the AVX-missing baseline-download hint (bun_bin/lib.rs calls bun_warn_avx_missing(BUN__GITHUB_BASELINE_URL) from main()) points non-AVX users at a dead link.

Why this isn't caught elsewhere

The --version/--revision paths the PR meant to change use package_json_version_with_canary / package_json_version_with_revision, which already get the suffix independently. Nothing in the test suite checks Bun.version inside a --compile'd binary, and standalone-binary.test.ts only asserts on --revision (which correctly uses the with_revision variant). The pre-existing -debug suffix had the same issue but only in debug builds that don't ship; this PR extends it to the release standalone artifact that every bun build --compile user will run.

Step-by-step proof

  1. cfg(bun_standalone) release build → STANDALONE_SUFFIX = "-standalone", package_json_version = concatcp!("1.4.0", "-standalone") = "1.4.0-standalone".
  2. node_process.rs:92Bun__version = "v1.4.0-standalone\0".
  3. User runs bun build --compile app.ts (default CompileRuntime::Standalone); inside the compiled exe, app.ts does if (semver.gte(Bun.version, '1.4.0')) useNewAPI(). Bun.version is "1.4.0-standalone"; semver §11.3 says any prerelease < no-prerelease at equal major.minor.patch, so gte returns false → feature gate fails.
  4. Same binary on a Nehalem-era CPU: bun_bin::main() calls bun_warn_avx_missing("https://github.com/oven-sh/bun/releases/download/bun-v1.4.0-standalone/bun-linux-x64-baseline.zip\0") → user copy-pastes a 404.
  5. process.release.sourceUrl in the compiled app returns the same 404 URL.

There's also an internal inconsistency: compile_target.rs::define_values() (which runs in full bun where the suffix is empty) bakes process.versions.bun = "1.4.0" as a compile-time define into the bundled JS, but dynamic access at runtime returns "1.4.0-standalone" — so process.versions.bun differs depending on whether the bundler constant-folded it.

How to fix

Leave package_json_version as the clean version_string (or version_string + "-debug" in debug, as before), and add STANDALONE_SUFFIX only to package_json_version_with_canary / _with_sha / _with_revision — those already cover --version, --revision, the unhandled-error footer, and the crash reporter. Separately, build Bun__githubURL / BUN__GITHUB_BASELINE_URL in cli/shared.rs from the raw bun_core::env::version_string rather than package_json_version so the release-tag path component never picks up any suffix (this also fixes the latent -debug case).

Comment on lines 76 to +79
if ctx.bundler_options.bake {
#[cfg(bun_standalone)]
crate::standalone_build::unavailable_command(b"build --app");
#[cfg(not(bun_standalone))]

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.

🟡 nit: pub mod build_command is itself gated #[cfg(not(bun_standalone))] in cli/mod.rs, so this module never compiles under cfg(bun_standalone) — the added #[cfg(bun_standalone)] arm (and the now-redundant #[cfg(not(bun_standalone))] on the return) are dead code. Harmless, but worth removing for clarity.

Extended reasoning...

What this is

In src/runtime/cli/build_command.rs the PR adds:

if ctx.bundler_options.bake {
    #[cfg(bun_standalone)]
    crate::standalone_build::unavailable_command(b"build --app");
    #[cfg(not(bun_standalone))]
    return crate::bake::production::build_command(ctx);
}

But in the same PR, src/runtime/cli/mod.rs gates the entire module declaration:

#[cfg(not(bun_standalone))]
#[path = "build_command.rs"]
pub mod build_command;

So build_command.rs is only compiled when cfg(not(bun_standalone)). Under that single configuration, the #[cfg(bun_standalone)] arm is filtered out, and the #[cfg(not(bun_standalone))] attribute on the return is always satisfied (so it's redundant).

Step-by-step proof

  1. Build with --cfg=bun_standalone: cli/mod.rs evaluates #[cfg(not(bun_standalone))] → false → pub mod build_command; is not declared → build_command.rs is never parsed/compiled. The #[cfg(bun_standalone)] line inside it does not exist in the build.
  2. Build without --cfg=bun_standalone: the module compiles, but #[cfg(bun_standalone)] is false, so unavailable_command(b"build --app") is removed by the compiler. The next line's #[cfg(not(bun_standalone))] is true, so return crate::bake::production::build_command(ctx); runs — exactly the pre-PR behavior.

There is no configuration under which the new branch executes.

Why existing code doesn't make this reachable

The standalone binary handles bun build at the dispatch layer (cli/mod.rs): under cfg(bun_standalone), Tag::BuildCommand falls through to crate::standalone_build::unavailable_command(tag_name(other)) in the reduced match tag arm. exec_build() (which would call into build_command::BuildCommand::exec) is itself #[cfg(not(bun_standalone))]. So even if the module were compiled, this function would never be called from standalone.

Impact

Zero functional impact. This is purely dead/redundant code:

  • Under standalone: module doesn't compile, line doesn't exist.
  • Under full bun: line is cfg'd out, behavior unchanged.

The only cost is reader confusion — it suggests build_command.rs is reachable from standalone when it isn't, and the redundant #[cfg(not(bun_standalone))] on the return adds noise.

How to fix

Just revert this hunk to its original form:

if ctx.bundler_options.bake {
    return crate::bake::production::build_command(ctx);
}

(Or, if the intent was belt-and-suspenders in case the module-level gate is ever removed, leave it — but as-is it's unreachable.)

@mintlify

mintlify Bot commented Jun 15, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
bun 🟢 Ready View Preview Jun 15, 2026, 6:51 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@coderabbitai coderabbitai 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.

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.buildkite/ci.mjs:
- Around line 977-982: The standalone artifact prefix is derived multiple times
using triplet.replace(/^bun-/, "bun-standalone-"), violating the single source
of truth principle. Centralize this derivation by creating or using an existing
helper function such as getTargetTriplet(platform, { standalone: true }) and
replace all direct invocations of the triplet.replace() pattern with calls to
this helper throughout the code. This ensures that signing, size metadata, and
packaging all reference the same naming contract from one location, making
future updates atomic and reducing duplication.

In @.buildkite/scripts/upload-release.sh:
- Around line 269-270: The standalone upload flow has a contradiction where line
269 makes standalone artifacts mandatory by appending them to the artifacts
array, while lines 292-295 handle them as best-effort with a separate upload
pass. This causes releases to potentially abort if standalone zips are missing
and creates duplicate uploads with malformed artifact names like
bun-standalone-standalone-*. Remove the line at 269 that appends
standalone_artifacts to the artifacts array (the line containing
artifacts+=("${standalone_artifacts[@]}")) so that standalone artifacts are only
handled through the best-effort upload mechanism at lines 292-295, making them
optional and preventing duplicates.

In `@docs/standalone-binary.md`:
- Around line 61-65: The fenced code block displaying the build target diagram
is missing a language tag, which causes markdownlint to flag it. Add the
language identifier `text` to the opening fence of the code block (change the
opening triple backticks to ```text) to satisfy the linter while keeping the
rendered content unchanged.

In `@scripts/runner.node.mjs`:
- Around line 447-449: In the else block where Bun standalone is logged as
unavailable (the path that outputs "Bun (standalone): <not available>"), add a
statement to clear the BUN_STANDALONE_EXE environment variable by either
deleting it (delete process.env.BUN_STANDALONE_EXE) or setting it to an empty
string to prevent tests from accidentally using a stale binary from a previous
run.
- Around line 2183-2185: The chmodSync call at line 2184 can throw an exception,
breaking the helper function's soft-fail contract and potentially aborting the
runner. Wrap the chmodSync invocation in a try-catch block to catch any
exceptions that might occur during the chmod operation, allowing the function to
continue gracefully without throwing and preserve the expected soft-fail
behavior.

In `@src/bun_core/Global.rs`:
- Around line 471-476: The formatcp! macro at line 471-476 in the Global.rs file
is formatting package_json_version_with_sha without preserving the `-debug`
marker that appears in sibling debug version constants like package_json_version
and package_json_version_with_revision. Modify the format string or ensure that
either version_string or STANDALONE_SUFFIX includes the `-debug` suffix in debug
builds so that the resulting formatted string matches the pattern of other debug
version constants. The format should produce a string like
`<version>-debug<suffix> (<sha>)` in debug builds rather than dropping the
`-debug` marker.

In `@src/runtime/hw_exports.rs`:
- Around line 457-459: The unsafe block at lines 457-459 in
src/runtime/hw_exports.rs is missing a required `// SAFETY:` comment that
explains why the unsafe dereference and write to the `out` pointer is sound. Add
a `// SAFETY:` comment immediately above the `unsafe { ... }` block (before line
457) that documents the safety invariants, such as explaining that `out` is
guaranteed to be a valid, properly aligned, initialized pointer that won't be
accessed concurrently, so dereferencing and writing to it is safe. This will
satisfy the clippy lint that is currently blocking CI.

In `@src/runtime/node.rs`:
- Around line 542-547: Remove the unused MaybeCssExt trait and its impl block
from src/runtime/node.rs. The MaybeCssExt trait definition and the impl block
for MaybeCssExt on Maybe are not referenced anywhere in the codebase and should
be deleted as dead code. Delete both the trait declaration and the corresponding
impl block that follows it.

In `@src/runtime/webcore/BakeResponse.rs`:
- Around line 70-77: The `bake_ssr_has_jsx` out-pointer parameter is not being
initialized in the standalone executable error path within the
`#[cfg(bun_standalone)]` block. Before the early return with
`core::ptr::null_mut()`, dereference and set `bake_ssr_has_jsx` to `0` to ensure
the out-parameter is properly zero-initialized on the error path, preventing
callers from observing stale stack data.

In `@test/cli/standalone-binary.test.ts`:
- Around line 45-47: The subprocess tests in test/cli/standalone-binary.test.ts
are collecting stderr but not asserting it, allowing unexpected warnings to
silently pass. Add an assertion `expect(stderr).toBe("")` immediately before
each `expect(exitCode)` assertion to ensure the success path has clean stderr
output. This fix is needed at four locations: lines 45-47 (add assertion before
the exitCode check), lines 57-59 (add assertion before the exitCode check),
lines 94-97 (add assertion before the exitCode check), and lines 106-109 (add
assertion before the exitCode check). Each location follows the same pattern of
collecting stdout, stderr, and exitCode via Promise.all, and each needs the
stderr assertion inserted before the existing exitCode assertion.
- Around line 99-101: The test named "STANDALONE_BUILD const is true" is
checking process.isBun instead of the STANDALONE_BUILD constant, creating a
mismatch between the test name and what is actually being asserted. Either
rename the test to reflect that it checks process.isBun (such as "process.isBun
is true in standalone binary"), or update the cmd array in the Bun.spawn call to
actually verify that the STANDALONE_BUILD constant is true, whichever aligns
with the intended test purpose for the standalone binary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 46851dde-5bd5-4fce-9dfe-49448a823bf7

📥 Commits

Reviewing files that changed from the base of the PR and between e0acad3 and eb087fc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (48)
  • .buildkite/ci.mjs
  • .buildkite/scripts/upload-release.sh
  • Cargo.toml
  • docs/standalone-binary.md
  • package.json
  • packages/bun-release/scripts/upload-npm.ts
  • packages/bun-release/src/platform.ts
  • scripts/build.ts
  • scripts/build/buildOptionsRs.ts
  • scripts/build/bun.ts
  • scripts/build/ci.ts
  • scripts/build/config.ts
  • scripts/build/rust.ts
  • scripts/runner.node.mjs
  • src/bun_bin/Cargo.toml
  • src/bun_bin/lib.rs
  • src/bun_core/Global.rs
  • src/install/PackageManager.rs
  • src/install/auto_installer.rs
  • src/options_types/compile_target.rs
  • src/runtime/Cargo.toml
  • src/runtime/api/BunObject.rs
  • src/runtime/api/JSBundler.rs
  • src/runtime/bake/DevServer.rs
  • src/runtime/bake/mod.rs
  • src/runtime/bake/production.rs
  • src/runtime/cli/Arguments.rs
  • src/runtime/cli/build_command.rs
  • src/runtime/cli/mod.rs
  • src/runtime/cli/shared.rs
  • src/runtime/cli/upgrade_command.rs
  • src/runtime/dispatch.rs
  • src/runtime/dispatch_js2native.rs
  • src/runtime/ffi/ffi_body.rs
  • src/runtime/hw_exports.rs
  • src/runtime/jsc_hooks.rs
  • src/runtime/lib.rs
  • src/runtime/node.rs
  • src/runtime/server/server_body.rs
  • src/runtime/standalone_build.rs
  • src/runtime/test_runner/bun_test.rs
  • src/runtime/test_runner/diff_format.rs
  • src/runtime/test_runner/expect.rs
  • src/runtime/test_runner/jest.rs
  • src/runtime/webcore/BakeResponse.rs
  • src/standalone_graph/Cargo.toml
  • src/standalone_graph/StandaloneModuleGraph.rs
  • test/cli/standalone-binary.test.ts
💤 Files with no reviewable changes (1)
  • src/standalone_graph/Cargo.toml

Comment thread .buildkite/ci.mjs
Comment on lines +977 to +982
if (shouldBuildStandalone(platform)) {
const standaloneTriplet = triplet.replace(/^bun-/, "bun-standalone-");
const standaloneStepKey = `${getTargetKey(platform)}-build-bun-standalone`;
artifacts.push(`${standaloneTriplet}-profile.zip`, `${standaloneTriplet}.zip`);
buildSteps.push(standaloneStepKey, standaloneStepKey);
}

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.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Centralize standalone triplet derivation.

The standalone artifact prefix is derived twice with triplet.replace(/^bun-/, "bun-standalone-"). Please route both call sites through a helper such as getTargetTriplet(platform, { standalone: true }) so signing, size metadata, and packaging stay on one naming contract. As per coding guidelines, “One source of truth; update every consumer atomically.”

Also applies to: 1020-1027

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.buildkite/ci.mjs around lines 977 - 982, The standalone artifact prefix is
derived multiple times using triplet.replace(/^bun-/, "bun-standalone-"),
violating the single source of truth principle. Centralize this derivation by
creating or using an existing helper function such as getTargetTriplet(platform,
{ standalone: true }) and replace all direct invocations of the
triplet.replace() pattern with calls to this helper throughout the code. This
ensures that signing, size metadata, and packaging all reference the same naming
contract from one location, making future updates atomic and reducing
duplication.

Source: Coding guidelines

Comment on lines +269 to +270
artifacts+=("${standalone_artifacts[@]}")

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix contradictory standalone upload flow (mandatory + duplicate best-effort pass).

Line 269 makes standalone artifacts required by appending them to artifacts, but Lines 292-295 treat them as best-effort. This can abort releases on missing standalone zips and also generates duplicate uploads plus bogus bun-standalone-standalone-* lookups.

Suggested fix
-  artifacts+=("${standalone_artifacts[@]}")
@@
-  for artifact in "${artifacts[@]}"; do
-    local standalone="${artifact/bun-/bun-standalone-}"
-    ( upload_artifact "$standalone" ) || echo "warn: skipping missing standalone artifact: $standalone"
-  done
+  for standalone in "${standalone_artifacts[@]}"; do
+    ( upload_artifact "$standalone" ) || echo "warn: skipping missing standalone artifact: $standalone"
+  done

Also applies to: 292-295

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.buildkite/scripts/upload-release.sh around lines 269 - 270, The standalone
upload flow has a contradiction where line 269 makes standalone artifacts
mandatory by appending them to the artifacts array, while lines 292-295 handle
them as best-effort with a separate upload pass. This causes releases to
potentially abort if standalone zips are missing and creates duplicate uploads
with malformed artifact names like bun-standalone-standalone-*. Remove the line
at 269 that appends standalone_artifacts to the artifacts array (the line
containing artifacts+=("${standalone_artifacts[@]}")) so that standalone
artifacts are only handled through the best-effort upload mechanism at lines
292-295, making them optional and preventing duplicates.

Comment thread docs/standalone-binary.md
Comment on lines +61 to +65
```
<target>-build-cpp (shared)
<target>-build-rust ────────► <target>-build-bun
<target>-build-rust-standalone ────────► <target>-build-bun-standalone
```

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language tag to this fenced block.

Markdownlint flags the bare code fence here; text would keep the docs check green without changing the rendered content.

Suggested fix
-```
+```text
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 61-61: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/standalone-binary.md` around lines 61 - 65, The fenced code block
displaying the build target diagram is missing a language tag, which causes
markdownlint to flag it. Add the language identifier `text` to the opening fence
of the code block (change the opening triple backticks to ```text) to satisfy
the linter while keeping the rendered content unchanged.

Source: Linters/SAST tools

Comment thread scripts/runner.node.mjs
Comment on lines +447 to +449
} else {
!isQuiet && console.log("Bun (standalone): <not available>");
}

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clear BUN_STANDALONE_EXE when standalone artifact is unavailable.

On Line 447–449, the failure path logs unavailable but leaves any preexisting process.env.BUN_STANDALONE_EXE intact, which can make tests accidentally use a stale binary.

Suggested fix
     } else {
+      delete process.env.BUN_STANDALONE_EXE;
       !isQuiet && console.log("Bun (standalone): <not available>");
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/runner.node.mjs` around lines 447 - 449, In the else block where Bun
standalone is logged as unavailable (the path that outputs "Bun (standalone):
<not available>"), add a statement to clear the BUN_STANDALONE_EXE environment
variable by either deleting it (delete process.env.BUN_STANDALONE_EXE) or
setting it to an empty string to prevent tests from accidentally using a stale
binary from a previous run.

Comment thread scripts/runner.node.mjs
Comment on lines +2183 to +2185
if (/bun-standalone(?:-[a-z]+)?(?:\.exe)?$/i.test(entry) && statSync(exe).isFile()) {
if (!isWindows) chmodSync(exe, 0o755);
return exe;

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve the helper’s soft-fail contract by guarding chmodSync.

Line 2184 can throw; that breaks the function’s “never throws” behavior and can abort the whole runner instead of soft-failing.

Suggested fix
     if (/bun-standalone(?:-[a-z]+)?(?:\.exe)?$/i.test(entry) && statSync(exe).isFile()) {
-      if (!isWindows) chmodSync(exe, 0o755);
+      if (!isWindows) {
+        try {
+          chmodSync(exe, 0o755);
+        } catch (cause) {
+          console.warn(`Failed to chmod ${exe}:`, cause);
+          return undefined;
+        }
+      }
       return exe;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/runner.node.mjs` around lines 2183 - 2185, The chmodSync call at line
2184 can throw an exception, breaking the helper function's soft-fail contract
and potentially aborting the runner. Wrap the chmodSync invocation in a
try-catch block to catch any exceptions that might occur during the chmod
operation, allowing the function to continue gracefully without throwing and
preserve the expected soft-fail behavior.

Comment thread src/runtime/hw_exports.rs
Comment on lines +457 to +459
unsafe {
*out = crate::bake::get_deinit_count_for_testing()
};

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a safety comment for the non-standalone unsafe write block.

cargo clippy is failing on Line 457 (unsafe block missing a safety comment), which is currently blocking CI. Add a // SAFETY: comment immediately above the #[cfg(not(bun_standalone))] unsafe { ... } block (or refactor to one documented unsafe write path) so both cfg branches satisfy the lint.

🧰 Tools
🪛 GitHub Actions: Clippy / 0_cargo clippy.txt

[error] 457-457: cargo clippy failed: unsafe block missing a safety comment.

🪛 GitHub Actions: Clippy / cargo clippy

[error] 457-457: cargo clippy reported: unsafe block missing a safety comment. (unsafe block missing a safety comment)

🪛 GitHub Check: cargo clippy

[failure] 457-457:
unsafe block missing a safety comment

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/hw_exports.rs` around lines 457 - 459, The unsafe block at lines
457-459 in src/runtime/hw_exports.rs is missing a required `// SAFETY:` comment
that explains why the unsafe dereference and write to the `out` pointer is
sound. Add a `// SAFETY:` comment immediately above the `unsafe { ... }` block
(before line 457) that documents the safety invariants, such as explaining that
`out` is guaranteed to be a valid, properly aligned, initialized pointer that
won't be accessed concurrently, so dereferencing and writing to it is safe. This
will satisfy the clippy lint that is currently blocking CI.

Sources: Linters/SAST tools, Pipeline failures

Comment thread src/runtime/node.rs
Comment on lines +542 to 547
#[cfg(not(bun_standalone))]
pub trait MaybeCssExt<R>: Sized {
fn to_css_result(self) -> Maybe<R, bun_css::ParseError<bun_css::ParserError>>;
}
#[cfg(not(bun_standalone))]
impl<R> MaybeCssExt<R> for Maybe<R, bun_css::BasicParseError> {

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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Description: Find all MaybeCssExt/to_css_result references and verify they are cfg-gated consistently.
# Expect: matches outside this trait/impl are guarded by #[cfg(not(bun_standalone))] or live only in modules excluded from standalone.
rg -n -C4 '\bMaybeCssExt\b|\.to_css_result\s*\(' src

Repository: oven-sh/bun

Length of output: 838


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for actual calls/uses of to_css_result (method invocation pattern)
# Exclude the definition lines to see only usages
echo "=== Direct .to_css_result() calls ==="
rg '\.to_css_result\(\)' src --type rust -n

echo ""
echo "=== Searching for any reference to MaybeCssExt in code (excluding trait def) ==="
rg 'MaybeCssExt' src --type rust -n | grep -v 'pub trait MaybeCssExt' | grep -v 'impl.*MaybeCssExt'

Repository: oven-sh/bun

Length of output: 93


Delete the unused MaybeCssExt trait and impl.

MaybeCssExt (lines 542–551) and its impl are not called anywhere in the codebase. Remove them as dead code per the style guidelines.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/node.rs` around lines 542 - 547, Remove the unused MaybeCssExt
trait and its impl block from src/runtime/node.rs. The MaybeCssExt trait
definition and the impl block for MaybeCssExt on Maybe are not referenced
anywhere in the codebase and should be deleted as dead code. Delete both the
trait declaration and the corresponding impl block that follows it.

Source: Coding guidelines

Comment on lines +70 to +77
#[cfg(bun_standalone)]
{
let _ = (call_frame, bake_ssr_has_jsx, js_this);
let _ = global_object.throw(format_args!(
"Bake is not available in standalone executables"
));
return core::ptr::null_mut();
}

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Initialize bake_ssr_has_jsx before returning the standalone error.

The new early return leaves this C-ABI out-pointer untouched. Set it to 0 before throwing so callers never observe stale stack data on the error path.

Proposed fix
         #[cfg(bun_standalone)]
         {
-            let _ = (call_frame, bake_ssr_has_jsx, js_this);
+            // SAFETY: C++ guarantees this is a valid out-pointer for the call.
+            unsafe { bake_ssr_has_jsx.write(0) };
+            let _ = (call_frame, js_this);
             let _ = global_object.throw(format_args!(
                 "Bake is not available in standalone executables"
             ));

As per coding guidelines, “Zero-init out-params and every slot a GC visitor or destructor can walk.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/webcore/BakeResponse.rs` around lines 70 - 77, The
`bake_ssr_has_jsx` out-pointer parameter is not being initialized in the
standalone executable error path within the `#[cfg(bun_standalone)]` block.
Before the early return with `core::ptr::null_mut()`, dereference and set
`bake_ssr_has_jsx` to `0` to ensure the out-parameter is properly
zero-initialized on the error path, preventing callers from observing stale
stack data.

Source: Coding guidelines

Comment on lines +45 to +47
const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toMatch(/^\d+\.\d+\.\d+-standalone\b/);
expect(exitCode).toBe(0);

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert stderr on success-path subprocess tests.

These tests collect stderr but don’t assert it, so unexpected runtime warnings can slip through while exit code remains 0. Add expect(stderr).toBe("") before the exit-code assertion.

Suggested patch
@@
-    const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+    const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
     expect(stdout.trim()).toMatch(/^\d+\.\d+\.\d+-standalone\b/);
+    expect(stderr).toBe("");
     expect(exitCode).toBe(0);
@@
-    const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+    const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
     expect(normalizeBunSnapshot(stdout)).toBe("2");
+    expect(stderr).toBe("");
     expect(exitCode).toBe(0);
@@
     const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
     expect(normalizeBunSnapshot(stdout)).toBe("ok true");
+    expect(stderr).toBe("");
     expect(exitCode).toBe(0);
@@
-    const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+    const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
     // Sanity: it's a Bun runtime.
     expect(stdout).toBe("true");
+    expect(stderr).toBe("");
     expect(exitCode).toBe(0);

As per coding guidelines, subprocess tests should surface output assertions before expect(exitCode); based on learnings, empty-stderr assertions are the preferred success-path pattern in Bun tests.

Also applies to: 57-59, 94-97, 106-109

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/cli/standalone-binary.test.ts` around lines 45 - 47, The subprocess
tests in test/cli/standalone-binary.test.ts are collecting stderr but not
asserting it, allowing unexpected warnings to silently pass. Add an assertion
`expect(stderr).toBe("")` immediately before each `expect(exitCode)` assertion
to ensure the success path has clean stderr output. This fix is needed at four
locations: lines 45-47 (add assertion before the exitCode check), lines 57-59
(add assertion before the exitCode check), lines 94-97 (add assertion before the
exitCode check), and lines 106-109 (add assertion before the exitCode check).
Each location follows the same pattern of collecting stdout, stderr, and
exitCode via Promise.all, and each needs the stderr assertion inserted before
the existing exitCode assertion.

Sources: Coding guidelines, Learnings

Comment on lines +99 to +101
test("STANDALONE_BUILD const is true", async () => {
await using proc = Bun.spawn({
cmd: [exe, "-e", "process.stdout.write(String(process.isBun))"],

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Test name does not match what is being asserted.

The title says STANDALONE_BUILD const is true, but the test only checks process.isBun. That makes this assertion non-standalone-specific.

Suggested patch
-  test("STANDALONE_BUILD const is true", async () => {
+  test("process.isBun is true in standalone runtime", async () => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/cli/standalone-binary.test.ts` around lines 99 - 101, The test named
"STANDALONE_BUILD const is true" is checking process.isBun instead of the
STANDALONE_BUILD constant, creating a mismatch between the test name and what is
actually being asserted. Either rename the test to reflect that it checks
process.isBun (such as "process.isBun is true in standalone binary"), or update
the cmd array in the Bun.spawn call to actually verify that the STANDALONE_BUILD
constant is true, whichever aligns with the intended test purpose for the
standalone binary.

Comment thread src/runtime/cli/Arguments.rs Outdated
Comment on lines +32 to +34
/// The slimmed-down `bun-standalone` runtime (no bundler/installer/shell).
#[default]
Standalone,

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.

🟡 nit: the doc comment says "(no bundler/installer/shell)", but the shell is not compiled out of bun-standalonepub mod shell is unconditional in runtime/lib.rs, bun exec / Bun.$ remain available, and both docs/standalone-binary.md and the unavailable_command error text list "bundler, package manager, or test runner" without mentioning shell. Consider "(no bundler/installer/test runner)".

Extended reasoning...

What this is

The doc comment on CompileRuntime::Standalone reads:

/// The slimmed-down `bun-standalone` runtime (no bundler/installer/shell).
#[default]
Standalone,

The parenthetical "(no bundler/installer/shell)" is factually inaccurate on the third item: the shell is not removed in bun-standalone. What's actually removed is the bundler, package manager, and test runner — not the shell.

Evidence that shell is kept

Several places in this same PR confirm the shell stays in:

  1. src/runtime/lib.rs declares pub mod shell; unconditionally — no #[cfg(not(bun_standalone))] gate.
  2. src/runtime/cli/mod.rs keeps Tag::ExecCommand => exec_exec(log) in the cfg(bun_standalone) dispatch arm (bun exec is the CLI front-end for the shell), and pub mod exec_command; is not cfg-gated.
  3. src/runtime/api/BunObject.rs does not gate the Bun.$ parsed-script constructor in static_adapters.
  4. docs/standalone-binary.md (added in this PR) lists what's removed and explicitly says "bun exec … remain"; shell is not in the removed list.
  5. src/runtime/standalone_build.rs's own module doc comment lists "bun install/add/…, bun build, bun test, bun create/init/x/upgrade, Bun.build(), the bake DevServer, and the CSS parser surface" — shell is absent.
  6. unavailable_command() in standalone_build.rs prints "the Bun runtime but not the bundler, package manager, or test runner" — again, no mention of shell.

Step-by-step proof

  1. Build bun-standalone: cfg(bun_standalone) is set.
  2. src/runtime/lib.rs line pub mod shell; has no cfg attribute → compiled in.
  3. src/runtime/cli/mod.rs standalone match arm: Tag::ExecCommand => exec_exec(log)exec_command::ExecCommand::exec(ctx) → invokes crate::shell.
  4. Run bun-standalone exec 'echo hi' → succeeds (shell runs). The doc comment says shell is removed; it isn't.

Why existing code doesn't make this true

There is no gating anywhere that would make "no shell" accurate. Every other authoritative description of what bun-standalone excludes (the user-facing error, the new docs page, the standalone_build.rs module doc, the PR description itself) consistently says "bundler, package manager, test runner" and keeps bun exec working.

Impact

Zero functional impact — this is a one-word inaccuracy in an internal Rust enum doc comment. It could mislead a future reader into thinking the shell is unavailable in --compile output when it is in fact fully available.

How to fix

Change the parenthetical to match reality:

/// The slimmed-down `bun-standalone` runtime (no bundler/installer/test runner).

…dalone

Severs the struct-field references that kept bundle_v2/bun_css/bun_install
alive after entry-point gating:

- pub mod bake replaced under cfg(bun_standalone) by bake_standalone_stub.rs:
  uninhabited DevServer/HotReloadEvent/SourceMapStore so
  Option<Box<DevServer>> is a ZST; throwing JSFrameworkRouter; C-ABI
  BakeProd*/Bake__* stubs; unreachable __bun_dispatch__DevServerHandle__Bake__*
  + __bun_jsc_enable_hot_module_reloading_for_bundler stubs so debug links
  (no gc-sections) resolve.
- server: AnyRoute::FrameworkRouter variant + every dev_server/bake field
  access cfg-gated; HTMLBundle bundle_v2-typed internals gated.
- api: js_bundle_completion_task/output_file_jsc modules + JSBundler::build
  body gated; BundleV2DeferredBatchTask dispatch arm gated;
  EXTERNAL_FREE_VTABLE registration gated.
- jsc: hot_reloader BundleV2 impl gated; AsyncModule install-queue
  machinery gated; VirtualMachine PackageManager log-swap gated.
- cli: run_command/shell_completions bun_install refs cfg-split
  (replace_package_manager_run inlined, BUN_NODE_DIR const inlined,
  create_fake_temporary_node_executable no-op, Windows shim path gated).
- dispatch_js2native: install_jsc/patch_jsc hooks stubbed.

Result (linux-x64 release, non-LTO): bun_css 1.74 MB -> 0, bun_install
1.10 MB -> 25 KB, bun_bundler 1.43 MB -> 0.44 MB (Transpiler half remains).
Stripped bun-standalone: 64.32 MB -> 59.50 MB; vs full bun: -7.63 MB
(-11.4%, .text -6.80 MB, .rodata -849 KB).
Comment on lines +150 to +155
pub unsafe fn Bake__bundleNewRouteJSFunctionImpl(
global: &JSGlobalObject,
_request_ptr: *mut c_void,
_route_kind: u8,
_route_index: u32,
) -> JSValue {

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.

🟡 nit: this stub's signature is (global, *mut c_void, u8, u32) but the C++ caller (BakeAdditionsToGlobalObject.cpp:104) and the real impl in DevServer.rs:6917 both declare (JSGlobalObject*, void*, BunString url) — and the same C++ archive links into both binaries. Practical impact is nil (the path is unreachable under standalone, only global is read, and under jsc_host_abi! it lands in the same register either way), but it violates the "extern declarations diff'd parameter-by-parameter" rule and the file comment claiming "Signatures mirror … DevServer.rs" is wrong for this symbol. One-line fix: change the trailing params to _url: BunString.

Extended reasoning...

What the bug is

The cfg(bun_standalone) stub for Bake__bundleNewRouteJSFunctionImpl at src/runtime/bake_standalone_stub.rs:150-155 declares a 4-parameter signature (global: &JSGlobalObject, _request_ptr: *mut c_void, _route_kind: u8, _route_index: u32), but both the C++ caller and the real Rust implementation use a 3-parameter signature with a BunString as the third argument. The PR's design is explicitly "the C++ object set is unchanged — both binaries link the same libbun-profile.a", so the C++ declaration at BakeAdditionsToGlobalObject.cpp:104 applies to both binaries:

extern "C" SYSV_ABI JSC::EncodedJSValue Bake__bundleNewRouteJSFunctionImpl(JSC::JSGlobalObject*, void* requestPtr, BunString url);

The real implementation at DevServer.rs:6917-6921 matches the C++ side:

pub unsafe fn Bake__bundleNewRouteJSFunctionImpl(
    global: &JSGlobalObject,
    request_ptr: *mut c_void,
    url: BunString,
) -> JSValue

The file's own comment at line 120 ("Signatures mirror the cfg(bun_standalone) stubs that previously lived in bake/production.rs / bake/DevServer.rs") is therefore factually incorrect for this symbol — the DevServer.rs signature has never had (u8, u32) trailing parameters.

Step-by-step proof

  1. C++ sideBakeAdditionsToGlobalObject.cpp:104 declares (JSGlobalObject*, void*, BunString) and line 134 calls it as Bake__bundleNewRouteJSFunctionImpl(globalObject, request->m_ctx, url). This object file is in the shared libbun-profile.a linked by both bun and bun-standalone.
  2. Full-bun Rust sideDevServer.rs:6917 defines (global, request_ptr: *mut c_void, url: BunString) -> JSValue inside jsc_host_abi!. Matches C++ param-for-param.
  3. Standalone Rust sidebake_standalone_stub.rs:150 defines (global, _request_ptr: *mut c_void, _route_kind: u8, _route_index: u32) -> JSValue inside jsc_host_abi!. Does not match: 4 params vs 3, and a 16-byte BunString (passed in rdx:rcx under sysv64) vs a u8 in dl + u32 in ecx.
  4. Under bun-standalone, lib.rs mounts bake_standalone_stub.rs as mod bake (and DevServer.rs is not compiled), so the stub is what links against the C++ caller.

Why this doesn't crash in practice

  • jsc_host_abi! forces sysv64/aapcs on all platforms (including Windows x64), so global is passed in rdi/x0 under both signatures and the return type (JSValueEncodedJSValueu64) goes in rax/x0 with no hidden sret pointer to shift the argument registers.
  • The stub body only reads global — the mismatched trailing parameters are never dereferenced; they're discarded.
  • The path is unreachable under standalone: jsFunctionBakeGetBundleNewRouteJSFunction is wired into m_bakeGetBundleNewRoute (a LazyProperty on the global object) and is only ever invoked from bake DevServer JS, which cannot load when there's no DevServer.

So this is a latent ABI mismatch, not an active crash.

Why it's still worth fixing

CLAUDE.md's cross-platform section explicitly requires "extern declarations diff'd parameter-by-parameter against definitions — they compile cleanly per side and crash only on the platform you didn't build". This stub violates that rule, and the misleading comment ("Signatures mirror … DevServer.rs") will steer the next reader wrong. If a future change makes the C++ side reachable (e.g. someone exposes the lazy property unconditionally) or starts reading the trailing args, this becomes a real bug with no compile-time warning.

How to fix

Change the trailing parameters to match the C++ declaration and DevServer.rs:

pub unsafe fn Bake__bundleNewRouteJSFunctionImpl(
    global: &JSGlobalObject,
    _request_ptr: *mut c_void,
    _url: BunString,
) -> JSValue {

BunString is already imported at the top of the file (line 15).

- packageAndUpload: use crossFeaturesJson for standalone (features.mjs
  imports bun:internal-for-testing whose lazy upgrade/pack/install slots
  throw under cfg(bun_standalone)).
- --compile-runtime defaults to Full until @oven/bun-standalone-* exists
  on npm; the Standalone path is opt-in via --compile-runtime=standalone.
- update dead-code-escapes inventory for the new cfg_attr(allow(dead_code))
  attributes in runtime/lib.rs, server/mod.rs, standalone_build.rs.
Comment on lines +31 to +36
pub enum CompileRuntime {
/// The slimmed-down `bun-standalone` runtime (no bundler/installer/shell).
#[default]
Standalone,
/// The full `bun` binary.
Full,

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.

🟡 nit: commit 87dbc85 reverted the --compile default to Full (CompileTarget::default() sets runtime: Full at line 71, the --compile-runtime help text says '"full" (default)', and the Arguments.rs comment confirms it), but this #[default] derive attribute still points to Standalone — so CompileRuntime::default() returns the wrong variant. No functional impact today since the only construction site overrides it explicitly, but move #[default] to Full so future ..Default::default() callers don't silently get Standalone.

Extended reasoning...

What the bug is

The #[default] attribute on CompileRuntime is stale after the mid-PR revert in commit 87dbc85 ("ci: fix standalone packaging and revert --compile default to Full"). The enum derives Default with #[default] on Standalone:

#[derive(Clone, Copy, PartialEq, Eq, Default, strum::IntoStaticStr)]
pub enum CompileRuntime {
    /// The slimmed-down `bun-standalone` runtime (no bundler/installer/shell).
    #[default]
    Standalone,
    /// The full `bun` binary.
    Full,
}

So CompileRuntime::default() == Standalone. But every other source of truth in the same PR says Full is the default:

  1. CompileTarget::default() (compile_target.rs:71) explicitly sets runtime: CompileRuntime::Full with the comment "The running process is always the full bun binary; is_default() only short-circuits to self_exe_path when the requested runtime is Full."
  2. The --compile-runtime help text (Arguments.rs) reads: 'Which Bun runtime to embed: "full" (default) or "standalone" (smaller)'.
  3. The Arguments.rs comment after the option parser says: "The --compile default stays CompileRuntime::Full until @oven/bun-standalone-* packages exist on npm".
  4. Commit 87dbc85's title is literally "revert --compile default to Full".

The PR originally had Standalone as the default (the PR description still says "new CompileRuntime { Standalone (default), Full }"), then 87dbc85 reverted that — but missed the derive attribute.

Step-by-step proof

  1. CompileRuntime derives Default (line 30); the #[default] attribute on line 33 selects Standalone, so <CompileRuntime as Default>::default() returns CompileRuntime::Standalone.
  2. CompileTarget::default() (line 49-73) does not call CompileRuntime::default() — it hard-codes runtime: CompileRuntime::Full at line 71.
  3. grep -r 'CompileRuntime::default' finds no callers, and no struct other than CompileTarget embeds a CompileRuntime field.
  4. Therefore: CompileRuntime::default() disagrees with the documented default, but no code path currently observes the disagreement.

Why existing code doesn't prevent it

It does prevent it — that's why this is a nit. CompileTarget::default() is the sole construction site and explicitly writes Full. The Default derive on the enum is effectively dead today.

Impact

Zero observable behavior change. The risk is purely latent: if future code adds a struct containing a CompileRuntime field and uses ..Default::default(), or calls CompileRuntime::default() directly, it will silently get Standalone while the help text, the Arguments.rs comment, and CompileTarget::default() all say Full. Since the entire enum is new in this PR, fixing it now avoids leaving a footgun in fresh code.

How to fix

Move the #[default] attribute from Standalone to Full:

pub enum CompileRuntime {
    /// The slimmed-down `bun-standalone` runtime (no bundler/installer/test runner).
    Standalone,
    /// The full `bun` binary.
    #[default]
    Full,
}

(Or drop the Default derive entirely since nothing uses it — but keeping it consistent with the documented default is cheaper than auditing future callers.)

Note that CompileTarget::default()'s explicit Full should stay regardless: it represents "the currently-running process" for the is_default()self_exe_path() shortcut, which is a separate invariant from the user-facing --compile-runtime default. But the #[default] attribute should match the documented default so the two don't silently diverge.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/api/JSBundler.rs`:
- Around line 1293-1298: The build function in the bun_standalone configuration
is using the generic throw method instead of throwing a TypeError as required by
the standalone API contract. Replace the global_this.throw call with
global_this.throw_type_error to align with other standalone stubs and ensure the
correct error type is thrown when Bun.build is accessed in standalone
executables. Keep the error message describing why the API is unavailable.

In `@src/runtime/cli/run_command.rs`:
- Around line 1927-1942: The standalone build branch returns Ok(()) without
creating the node shim, but the caller interprets this success and exports
NODE/npm_node_execpath to the non-existent path. Instead of returning Ok(()) in
the #[cfg(bun_standalone)] block, return a specific error that indicates the
shim was not created. Then in the caller function
configure_path_for_run_with_package_json_dir, catch and handle this specific
error by skipping the env var rewrites instead of panicking, allowing the code
to proceed safely when the shim doesn't exist.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bcd2e265-5fb8-4539-b2fa-bf11ea21fbe8

📥 Commits

Reviewing files that changed from the base of the PR and between eb087fc and 87dbc85.

📒 Files selected for processing (22)
  • docs/standalone-binary.md
  • scripts/build/ci.ts
  • src/jsc/AsyncModule.rs
  • src/jsc/VirtualMachine.rs
  • src/jsc/hot_reloader.rs
  • src/runtime/allocators/mod.rs
  • src/runtime/api.rs
  • src/runtime/api/JSBundler.rs
  • src/runtime/api/js_bundle_completion_task.rs
  • src/runtime/bake_standalone_stub.rs
  • src/runtime/cli/Arguments.rs
  • src/runtime/cli/mod.rs
  • src/runtime/cli/run_command.rs
  • src/runtime/dispatch.rs
  • src/runtime/dispatch_js2native.rs
  • src/runtime/jsc_hooks.rs
  • src/runtime/lib.rs
  • src/runtime/server/HTMLBundle.rs
  • src/runtime/server/ServerConfig.rs
  • src/runtime/server/mod.rs
  • src/runtime/server/server_body.rs
  • test/internal/dead-code-escape-limits.json

Comment on lines +1293 to +1298
#[cfg(bun_standalone)]
fn build(global_this: &JSGlobalObject, _arguments: &[JSValue]) -> JsResult<JSValue> {
Err(global_this.throw(format_args!(
"Bun.build is not available in standalone executables. Install Bun: https://bun.com/get"
)))
}

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Throw TypeError from the standalone Bun.build stub.

This stub currently uses the generic throw(...) path, but the standalone API contract says disabled JS APIs throw TypeError. Keep this aligned with the other standalone stubs that call throw_type_error.

Proposed fix
 #[cfg(bun_standalone)]
 fn build(global_this: &JSGlobalObject, _arguments: &[JSValue]) -> JsResult<JSValue> {
-        Err(global_this.throw(format_args!(
-            "Bun.build is not available in standalone executables. Install Bun: https://bun.com/get"
-        )))
+        let _ = global_this.throw_type_error(format_args!(
+            "Bun.build is not available in standalone executables. Install Bun: https://bun.com/get"
+        ));
+        Err(JsError::Thrown)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/api/JSBundler.rs` around lines 1293 - 1298, The build function in
the bun_standalone configuration is using the generic throw method instead of
throwing a TypeError as required by the standalone API contract. Replace the
global_this.throw call with global_this.throw_type_error to align with other
standalone stubs and ensure the correct error type is thrown when Bun.build is
accessed in standalone executables. Keep the error message describing why the
API is unavailable.

Comment on lines +1927 to +1942
#[cfg(not(bun_standalone))]
return bun_install::RunCommand::create_fake_temporary_node_executable(
path,
optional_bun_path,
);
// Standalone runtime: skip creating the `/tmp/bun-node*/node` shim.
// The bare `bun-standalone` binary's `bun run <script>` path still
// prepends `.bin` dirs to PATH; child `node` invocations resolve to
// the system node (or fail) instead of bun — acceptable for the
// debugging-only bare-binary case, and a compiled exe never reaches
// this path at all.
#[cfg(bun_standalone)]
{
let _ = (path, optional_bun_path);
Ok(())
}

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Standalone no-op reports success but leaves NODE/npm_node_execpath pointing to a non-existent shim.

In standalone builds this branch returns Ok(()) without creating <tmp>/bun-node*/node, but the caller still treats success as “shim created” and exports NODE/npm_node_execpath to that path. That can break spawned script tooling when needs_to_force_bun is true.

Suggested fix
@@
-        #[cfg(bun_standalone)]
-        {
-            let _ = (path, optional_bun_path);
-            Ok(())
-        }
+        #[cfg(bun_standalone)]
+        {
+            let _ = (path, optional_bun_path);
+            // Signal "no shim available" so caller can skip NODE/npm_node_execpath rewrite.
+            Err(bun_core::err!("UnsupportedInStandalone"))
+        }

And in configure_path_for_run_with_package_json_dir, handle this specific error by skipping shim-dependent env var rewrites instead of panicking.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/cli/run_command.rs` around lines 1927 - 1942, The standalone
build branch returns Ok(()) without creating the node shim, but the caller
interprets this success and exports NODE/npm_node_execpath to the non-existent
path. Instead of returning Ok(()) in the #[cfg(bun_standalone)] block, return a
specific error that indicates the shim was not created. Then in the caller
function configure_path_for_run_with_package_json_dir, catch and handle this
specific error by skipping the env var rewrites instead of panicking, allowing
the code to proceed safely when the shim doesn't exist.

…filter_run under bun_standalone

Round 3: -2.48 MB (linux-x64 non-LTO 59.50 -> 57.02 MB; vs full bun -10.1 MB).

- codegen: new per-class standaloneStub knob in generate-classes.ts. When set,
  the emitted Rust thunks become cfg-split: real call under not(bun_standalone),
  ZST type + throwing/zero-value body under bun_standalone. C++ output is
  byte-identical. All 15 jest.classes.ts classes get standaloneStub.
- pub mod test_runner replaced under bun_standalone by
  test_runner_standalone_stub.rs: FakeTimers ZST, Jest::runner()->None, the
  6 hand-written C-ABI symbols (Bun__Jest__createTestModuleObject,
  Expect_readFlagsAndProcessPromise, etc.), CommandLineReporter facade.
- new api/standalone_api_stubs.rs: ZST stubs for JSTranspiler/FileSystemRouter/
  MatchedRoute/Image; throwing markdown_object::create. pub mod image/
  js_transpiler/filesystem_router/markdown_object cfg-gated.
- filter_run/multi_run/filter_arg modules cfg-gated; exec_auto_or_run replaces
  the workspace-scan calls with unavailable_command under standalone;
  parse_build_command_options/parse_test_command_options cfg-gated.
- node_fallbacks: include_bytes! dropped under bun_standalone (browser-target
  polyfills, never reached without the bundler) — ~90 KB rodata.
- standalone_build.rs: link stubs for the BufferedReaderParentLink/ProcessExit
  variants whose link_impl_*! lived in the gated modules
  (FilterRunHandle/MultiRunPipeReader/TestParallelWorker[Pipe]); BunTest__
  shouldGenerateCodeCoverage -> false.
- bun.ts: list shell32/ole32/oleaut32/user32 explicitly for Windows (Rust
  #[link] directives carrying them can be in dead-stripped .o members).
- update dead-code-escapes inventory.
Comment on lines +1395 to +1411
#[cfg(bun_standalone)]
bun_jsc::jsc_host_abi! {
#[unsafe(no_mangle)]
pub unsafe fn Bun__TestScope__Describe2__bunTestThen(
_global: *mut JSGlobalObject,
_frame: *mut CallFrame,
) -> JSValue {
JSValue::UNDEFINED
}
}
#[cfg(bun_standalone)]
bun_jsc::jsc_host_abi! {
#[unsafe(no_mangle)]
pub unsafe fn Bun__TestScope__Describe2__bunTestCatch(
_global: *mut JSGlobalObject,
_frame: *mut CallFrame,
) -> JSValue {

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.

🟡 nit: same pattern as the build_command.rs comment above, but it applies across ~9 more files. lib.rs swaps mod test_runner / mod bake to stub files under cfg(bun_standalone) and api.rs gates mod js_bundle_completion_task out entirely — so the real test_runner/{bun_test,diff_format,expect,jest}.rs, bake/{DevServer,production,mod}.rs, and js_bundle_completion_task.rs never compile under cfg(bun_standalone), and every #[cfg(bun_standalone)] arm added inside them (plus the now-redundant #[cfg(not(bun_standalone))] guards) is dead. The BakeProd* / Bun__TestScope__Describe2__* stubs are duplicated verbatim in bake_standalone_stub.rs / test_runner_standalone_stub.rs (the live copies — bake_standalone_stub.rs's own comment says they "previously lived in bake/production.rs / bake/DevServer.rs"). Same goes for the inner #[cfg(not(bun_standalone))] at dispatch.rs:460 (the enclosing match arm at :454 already carries that gate). ~80 lines of misleading dead code, zero functional impact.

Extended reasoning...

What this is

The PR took two passes at gating the test-runner / bake / bundle-completion code out of bun-standalone:

  1. Per-symbol gating (earlier commits) — added #[cfg(bun_standalone)] stub arms and #[cfg(not(bun_standalone))] guards inside test_runner/*.rs, bake/*.rs, js_bundle_completion_task.rs, and dispatch.rs.
  2. Module-level swap (commit cb8c88c "structural cfg-gating") — replaced the entire mod test_runner / mod bake declarations in lib.rs with stub files, and gated mod js_bundle_completion_task in api.rs.

Step 2 supersedes step 1 for those modules, but the per-symbol cfg attributes from step 1 were never removed. They are now dead in both build configurations. This is the same finding as the existing comment on build_command.rs:79, but covering the rest of the affected files.

Step-by-step proof

src/runtime/lib.rs declares:

#[cfg(not(bun_standalone))]
pub mod test_runner;
#[cfg(bun_standalone)]
#[path = "test_runner_standalone_stub.rs"]
pub mod test_runner;

#[cfg(not(bun_standalone))]
pub mod bake;
#[cfg(bun_standalone)]
#[path = "bake_standalone_stub.rs"]
pub mod bake;

and src/runtime/api.rs declares #[cfg(not(bun_standalone))] pub mod js_bundle_completion_task;.

So under --cfg=bun_standalone, the files test_runner/{bun_test,diff_format,expect,jest}.rs, bake/{DevServer,production,mod}.rs, and api/js_bundle_completion_task.rs are never parsed. Under cfg(not(bun_standalone)) they compile, but every #[cfg(bun_standalone)] block inside them evaluates false and is stripped, while every #[cfg(not(bun_standalone))] attribute evaluates true and is redundant. Either way, none of the added attributes affect the compiled output.

Decisive evidence

The stub files re-declare the same #[no_mangle] symbols the per-file stubs declare:

  • test_runner_standalone_stub.rs defines Bun__TestScope__Describe2__bunTestThen / bunTestCatch — identical to the #[cfg(bun_standalone)] blocks at bun_test.rs:1395-1411.
  • bake_standalone_stub.rs defines BakeToWindowsPath / BakeProdResolve / BakeProdLoad / BakeProdSourceMap — identical to the #[cfg(bun_standalone)] blocks in bake/production.rs.

If both copies compiled in the same build, the linker would fail on duplicate symbols. The fact that it doesn't proves only the stub-file copies are live under cfg(bun_standalone). bake_standalone_stub.rs even documents this: "Signatures mirror the cfg(bun_standalone) stubs that previously lived in bake/production.rs / bake/DevServer.rs" — explicitly calling them leftovers.

Affected locations

  • test_runner/bun_test.rs:1395-1411 — two stub fns (duplicated in test_runner_standalone_stub.rs)
  • test_runner/diff_format.rs:102-107 and the matching #[cfg(not(bun_standalone))] { … } wrapper
  • test_runner/expect.rs — three sites (~507, ~2648, ~2806)
  • test_runner/jest.rs — two sites (~323, ~475)
  • bake/DevServer.rs#![cfg_attr(bun_standalone, allow(...))] at the top, plus the cfg-split blocks at ~6922 and ~7054
  • bake/production.rs — four #[cfg(bun_standalone)] stub fns + the matching #[cfg(not(bun_standalone))] on the real impls
  • bake/mod.rs:22#[cfg_attr(bun_standalone, allow(unused_imports))]
  • api/js_bundle_completion_task.rs:557 — redundant #[cfg(not(bun_standalone))]
  • dispatch.rs:460 — inner #[cfg(not(bun_standalone))] on the call_check_slow statement, redundant because the enclosing BundleV2DeferredBatchTask match arm at :454 already carries #[cfg(not(bun_standalone))] (this file is compiled under both configs, so it's the nested-redundancy flavor rather than the dead-module flavor)

Impact

Zero functional impact — none of this code participates in either build configuration. The cost is purely reader confusion: ~80 lines that suggest these files participate in the standalone build when they don't, plus duplicated #[no_mangle] symbol definitions that look like they'd collide.

How to fix

Delete the #[cfg(bun_standalone)] blocks and drop the #[cfg(not(bun_standalone))] attributes (reverting those hunks to their pre-PR form) in the eight module-swapped files, and remove the inner #[cfg(not(bun_standalone))] at dispatch.rs:460. The stub modules (test_runner_standalone_stub.rs, bake_standalone_stub.rs) are the single source of truth for the standalone build.

These are used by compiled executables. Only Bun.FileSystemRouter remains
stubbed from the round-3 API set. Reverts the cfg-gates on image/
js_transpiler/markdown_object module declarations, the AsyncImageTask/
AsyncTransformTask dispatch arms, and trims standalone_api_stubs.rs to
the FileSystemRouter/MatchedRoute pair only.

linux-x64 non-LTO: 70.39 -> 61.49 MB (-8.49 MB, -12.6%).
Comment on lines +42 to +47
#[cfg(bun_standalone)]
unreachable!(concat!(
"node-fallback polyfill `",
$code_path,
"` is not available in standalone executables"
));

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.

🟡 The justifying comment ("bun-standalone never bundles for --target=browser") only covers the bundler emitting these paths; it misses that the runtime module loader unconditionally passes through any specifier starting with /bun-vfs$$/node_modules/ (VirtualMachine.rs:4013, jsc_hooks.rs:4824) and then calls contents_from_path()(module.code)() (jsc_hooks.rs:2389, RuntimeTranspilerStore.rs:888) — none of which are cfg-gated. So bun-standalone -e 'await import("/bun-vfs$$/node_modules/buffer/index.js")' aborts the process via unreachable!() instead of throwing a JS error, contradicting the "every stub here surfaces a user-facing error" rule in standalone_build.rs. Low exposure (undocumented internal prefix), but trivial to fix: have contents_from_path() return None under cfg(bun_standalone) and keep the ~90 KB .rodata saving.

Extended reasoning...

What the bug is

Under cfg(bun_standalone), the create_source_code_getter! macro body becomes unreachable!("node-fallback polyfill … is not available in standalone executables"). The justifying comment says "bun-standalone never bundles for --target=browser (it has no bundler), so the browser polyfills are unreachable." That rationale is correct for the resolver producing these paths (which is gated on polyfill_node_globals = (target == Browser) in bundler/options.rs), but it misses a second consumer: the runtime module loader has an unconditional pass-through for any specifier with the /bun-vfs$$/node_modules/ prefix, and that path eventually calls (module.code)() — the unreachable!() getter — on the JS thread, aborting the process.

The code path

  1. ResolveVirtualMachine.rs:4013 and jsc_hooks.rs:4824 both do if specifier.starts_with(node_fallbacks::IMPORT_PATH) { ret.path = specifier; return Ok(()); } — passing the specifier straight through as the resolved path. Neither is #[cfg(not(bun_standalone))].
  2. Loadtranspile_source_code_inner (jsc_hooks.rs:2307-2389) and the threaded RuntimeTranspilerStore.rs:772-888 both check is_node_override = path.starts_with(node_fallbacks::IMPORT_PATH) and, when true, call node_fallbacks::contents_from_path(specifier). Neither call site is cfg-gated.
  3. contents_from_path (node_fallbacks.rs:185-197) strips the prefix to "buffer", looks it up in map(), and calls (module.code)() — which under cfg(bun_standalone) is the unreachable!() getter introduced by this PR.

Step-by-step proof

bun-standalone -e 'await import("/bun-vfs$$/node_modules/buffer/index.js")'
  1. import() calls the module resolver with specifier "/bun-vfs$$/node_modules/buffer/index.js".
  2. jsc_hooks.rs:4824 matches starts_with(IMPORT_PATH) (the 24-byte constant at node_fallbacks.rs:5), returns the specifier verbatim as the resolved path.
  3. The transpile hook sees is_node_override == true and calls contents_from_path("/bun-vfs$$/node_modules/buffer/index.js").
  4. contents_from_path strips the prefix → "buffer/index.js", takes the segment up to the first /"buffer", finds it in the static map() (buffer is one of the 21 fallback modules), and calls (module.code)().
  5. Under cfg(bun_standalone), module.code is get as fn() -> &'static str whose body is unreachable!("node-fallback polyfill \node-fallbacks/buffer.js` is not available in standalone executables")`.
  6. unreachable!() panics → the panic handler aborts the process. No JS-level TypeError/ResolveMessage is thrown; the user's try { await import(...) } catch never runs.

The same applies to a --compile'd executable that contains await import(userControlledString) where the string happens to have this prefix.

Why existing code doesn't prevent it

The four call sites listed above (VirtualMachine.rs:4013, jsc_hooks.rs:4824, jsc_hooks.rs:2389, RuntimeTranspilerStore.rs:888) are all in modules that compile under both configurations, and none carry a #[cfg(not(bun_standalone))] gate. The IMPORT_PATH prefix check is a fast 3×u64 compare that runs on every module specifier in the runtime loader, regardless of build flavor or --target. The PR's gating only addresses the bundler-side emitter (which is indeed compiled out), not this runtime-loader pass-through.

Impact

This violates the PR's own design rule in standalone_build.rs: "Every stub here surfaces a user-facing error; nothing is a silent no-op." A panic that aborts the whole process is neither — and is strictly worse than the catchable TypeError every other gated API (Bun.build, Bun.color, Bun.FileSystemRouter, …) throws.

That said, realistic exposure is low: /bun-vfs$$/node_modules/ is an undocumented internal prefix. The resolver only ever emits it for --target=browser bundling, which cannot combine with --compile (and the standalone runtime has no bundler at all). A user would have to hard-code the magic prefix in a dynamic import() to hit this. No realistic library code does so. Hence nit, not blocking.

How to fix

Either of these one-liners preserves the ~90 KB .rodata saving:

  • Gate contents_from_path itself: under cfg(bun_standalone), return None immediately. The transpile hook then falls through to its normal "file not found" / module-resolution-error path, surfacing a catchable JS error.
  • Or have the getter return "" instead of unreachable!() (the empty source then fails downstream as a normal module error).

The first is cleaner — it makes the "browser polyfills don't exist here" intent explicit at the lookup site rather than relying on a panic that asserts unreachability of a path that is, in fact, reachable.

@archiewood

Copy link
Copy Markdown

I just started attempting this and my harness pointed me here! Cool PR!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants