build: bun-standalone — reduced-footprint --compile runtime - #32262
build: bun-standalone — reduced-footprint --compile runtime#32262Jarred-Sumner wants to merge 12 commits into
Conversation
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.
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
|
Warning Review limit reached
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 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (20)
WalkthroughIntroduces a Changesbun-standalone build variant
Possibly related PRs
Suggested reviewers
|
| # 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 | ||
|
|
There was a problem hiding this comment.
🔴 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:
- Explicit list, appended to the main array (lines 245-269): a hand-maintained
standalone_artifacts=(...)array is appended toartifactsviaartifacts+=("${standalone_artifacts[@]}"). After this,artifactshas 52 entries (30 regular + 22 standalone). - 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_artifactcallsdownload_buildkite_artifactnot in a subshell, and that function doesexit 1on 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.zip→bun-standalone-darwin-aarch64.zip— already uploaded by loop 1; downloaded and uploaded to S3/GitHub a second time. (22 such duplicates.)bun-linux-aarch64-android.zip→bun-standalone-linux-aarch64-android.zip— never built (shouldBuildStandaloneexcludes android/freebsd);download_buildkite_artifactfails, the subshell exits 1, printswarn: skipping.... (8 such bogus android/freebsd derivations.)bun-standalone-darwin-aarch64.zip→bun-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 1from 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_artifactslist 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.
| /// 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) | ||
| }; |
There was a problem hiding this comment.
🔴 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_FILENAMEfor 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
cfg(bun_standalone)release build →STANDALONE_SUFFIX = "-standalone",package_json_version = concatcp!("1.4.0", "-standalone") = "1.4.0-standalone".node_process.rs:92→Bun__version = "v1.4.0-standalone\0".- User runs
bun build --compile app.ts(defaultCompileRuntime::Standalone); inside the compiled exe,app.tsdoesif (semver.gte(Bun.version, '1.4.0')) useNewAPI().Bun.versionis"1.4.0-standalone"; semver §11.3 says any prerelease < no-prerelease at equal major.minor.patch, sogtereturnsfalse→ feature gate fails. - Same binary on a Nehalem-era CPU:
bun_bin::main()callsbun_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. process.release.sourceUrlin 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).
| if ctx.bundler_options.bake { | ||
| #[cfg(bun_standalone)] | ||
| crate::standalone_build::unavailable_command(b"build --app"); | ||
| #[cfg(not(bun_standalone))] |
There was a problem hiding this comment.
🟡 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
- Build with
--cfg=bun_standalone:cli/mod.rsevaluates#[cfg(not(bun_standalone))]→ false →pub mod build_command;is not declared →build_command.rsis never parsed/compiled. The#[cfg(bun_standalone)]line inside it does not exist in the build. - Build without
--cfg=bun_standalone: the module compiles, but#[cfg(bun_standalone)]is false, sounavailable_command(b"build --app")is removed by the compiler. The next line's#[cfg(not(bun_standalone))]is true, soreturn 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.)
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (48)
.buildkite/ci.mjs.buildkite/scripts/upload-release.shCargo.tomldocs/standalone-binary.mdpackage.jsonpackages/bun-release/scripts/upload-npm.tspackages/bun-release/src/platform.tsscripts/build.tsscripts/build/buildOptionsRs.tsscripts/build/bun.tsscripts/build/ci.tsscripts/build/config.tsscripts/build/rust.tsscripts/runner.node.mjssrc/bun_bin/Cargo.tomlsrc/bun_bin/lib.rssrc/bun_core/Global.rssrc/install/PackageManager.rssrc/install/auto_installer.rssrc/options_types/compile_target.rssrc/runtime/Cargo.tomlsrc/runtime/api/BunObject.rssrc/runtime/api/JSBundler.rssrc/runtime/bake/DevServer.rssrc/runtime/bake/mod.rssrc/runtime/bake/production.rssrc/runtime/cli/Arguments.rssrc/runtime/cli/build_command.rssrc/runtime/cli/mod.rssrc/runtime/cli/shared.rssrc/runtime/cli/upgrade_command.rssrc/runtime/dispatch.rssrc/runtime/dispatch_js2native.rssrc/runtime/ffi/ffi_body.rssrc/runtime/hw_exports.rssrc/runtime/jsc_hooks.rssrc/runtime/lib.rssrc/runtime/node.rssrc/runtime/server/server_body.rssrc/runtime/standalone_build.rssrc/runtime/test_runner/bun_test.rssrc/runtime/test_runner/diff_format.rssrc/runtime/test_runner/expect.rssrc/runtime/test_runner/jest.rssrc/runtime/webcore/BakeResponse.rssrc/standalone_graph/Cargo.tomlsrc/standalone_graph/StandaloneModuleGraph.rstest/cli/standalone-binary.test.ts
💤 Files with no reviewable changes (1)
- src/standalone_graph/Cargo.toml
| 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); | ||
| } |
There was a problem hiding this comment.
🧹 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
| artifacts+=("${standalone_artifacts[@]}") | ||
|
|
There was a problem hiding this comment.
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"
+ doneAlso 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.
| ``` | ||
| <target>-build-cpp (shared) | ||
| <target>-build-rust ────────► <target>-build-bun | ||
| <target>-build-rust-standalone ────────► <target>-build-bun-standalone | ||
| ``` |
There was a problem hiding this comment.
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
| } else { | ||
| !isQuiet && console.log("Bun (standalone): <not available>"); | ||
| } |
There was a problem hiding this comment.
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.
| if (/bun-standalone(?:-[a-z]+)?(?:\.exe)?$/i.test(entry) && statSync(exe).isFile()) { | ||
| if (!isWindows) chmodSync(exe, 0o755); | ||
| return exe; |
There was a problem hiding this comment.
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.
| unsafe { | ||
| *out = crate::bake::get_deinit_count_for_testing() | ||
| }; |
There was a problem hiding this comment.
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
| #[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> { |
There was a problem hiding this comment.
🧩 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*\(' srcRepository: 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
| #[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(); | ||
| } |
There was a problem hiding this comment.
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
| 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); |
There was a problem hiding this comment.
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
| test("STANDALONE_BUILD const is true", async () => { | ||
| await using proc = Bun.spawn({ | ||
| cmd: [exe, "-e", "process.stdout.write(String(process.isBun))"], |
There was a problem hiding this comment.
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.
| /// The slimmed-down `bun-standalone` runtime (no bundler/installer/shell). | ||
| #[default] | ||
| Standalone, |
There was a problem hiding this comment.
🟡 nit: the doc comment says "(no bundler/installer/shell)", but the shell is not compiled out of bun-standalone — pub 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:
src/runtime/lib.rsdeclarespub mod shell;unconditionally — no#[cfg(not(bun_standalone))]gate.src/runtime/cli/mod.rskeepsTag::ExecCommand => exec_exec(log)in thecfg(bun_standalone)dispatch arm (bun execis the CLI front-end for the shell), andpub mod exec_command;is not cfg-gated.src/runtime/api/BunObject.rsdoes not gate theBun.$parsed-script constructor instatic_adapters.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.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.unavailable_command()instandalone_build.rsprints "the Bun runtime but not the bundler, package manager, or test runner" — again, no mention of shell.
Step-by-step proof
- Build
bun-standalone:cfg(bun_standalone)is set. src/runtime/lib.rslinepub mod shell;has no cfg attribute → compiled in.src/runtime/cli/mod.rsstandalone match arm:Tag::ExecCommand => exec_exec(log)→exec_command::ExecCommand::exec(ctx)→ invokescrate::shell.- 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).
| pub unsafe fn Bake__bundleNewRouteJSFunctionImpl( | ||
| global: &JSGlobalObject, | ||
| _request_ptr: *mut c_void, | ||
| _route_kind: u8, | ||
| _route_index: u32, | ||
| ) -> JSValue { |
There was a problem hiding this comment.
🟡 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,
) -> JSValueThe 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
- C++ side —
BakeAdditionsToGlobalObject.cpp:104declares(JSGlobalObject*, void*, BunString)and line 134 calls it asBake__bundleNewRouteJSFunctionImpl(globalObject, request->m_ctx, url). This object file is in the sharedlibbun-profile.alinked by bothbunandbun-standalone. - Full-bun Rust side —
DevServer.rs:6917defines(global, request_ptr: *mut c_void, url: BunString) -> JSValueinsidejsc_host_abi!. Matches C++ param-for-param. - Standalone Rust side —
bake_standalone_stub.rs:150defines(global, _request_ptr: *mut c_void, _route_kind: u8, _route_index: u32) -> JSValueinsidejsc_host_abi!. Does not match: 4 params vs 3, and a 16-byteBunString(passed inrdx:rcxunder sysv64) vs au8indl+u32inecx. - Under
bun-standalone,lib.rsmountsbake_standalone_stub.rsasmod bake(andDevServer.rsis 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), soglobalis passed inrdi/x0under both signatures and the return type (JSValue≡EncodedJSValue≡u64) goes inrax/x0with 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:
jsFunctionBakeGetBundleNewRouteJSFunctionis wired intom_bakeGetBundleNewRoute(aLazyPropertyon 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.
| pub enum CompileRuntime { | ||
| /// The slimmed-down `bun-standalone` runtime (no bundler/installer/shell). | ||
| #[default] | ||
| Standalone, | ||
| /// The full `bun` binary. | ||
| Full, |
There was a problem hiding this comment.
🟡 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:
CompileTarget::default()(compile_target.rs:71) explicitly setsruntime: CompileRuntime::Fullwith the comment "The running process is always the fullbunbinary;is_default()only short-circuits to self_exe_path when the requested runtime is Full."- The
--compile-runtimehelp text (Arguments.rs) reads: 'Which Bun runtime to embed: "full" (default) or "standalone" (smaller)'. - The Arguments.rs comment after the option parser says: "The
--compiledefault staysCompileRuntime::Fulluntil@oven/bun-standalone-*packages exist on npm". - 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
CompileRuntimederivesDefault(line 30); the#[default]attribute on line 33 selectsStandalone, so<CompileRuntime as Default>::default()returnsCompileRuntime::Standalone.CompileTarget::default()(line 49-73) does not callCompileRuntime::default()— it hard-codesruntime: CompileRuntime::Fullat line 71.grep -r 'CompileRuntime::default'finds no callers, and no struct other thanCompileTargetembeds aCompileRuntimefield.- 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.
There was a problem hiding this comment.
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
📒 Files selected for processing (22)
docs/standalone-binary.mdscripts/build/ci.tssrc/jsc/AsyncModule.rssrc/jsc/VirtualMachine.rssrc/jsc/hot_reloader.rssrc/runtime/allocators/mod.rssrc/runtime/api.rssrc/runtime/api/JSBundler.rssrc/runtime/api/js_bundle_completion_task.rssrc/runtime/bake_standalone_stub.rssrc/runtime/cli/Arguments.rssrc/runtime/cli/mod.rssrc/runtime/cli/run_command.rssrc/runtime/dispatch.rssrc/runtime/dispatch_js2native.rssrc/runtime/jsc_hooks.rssrc/runtime/lib.rssrc/runtime/server/HTMLBundle.rssrc/runtime/server/ServerConfig.rssrc/runtime/server/mod.rssrc/runtime/server/server_body.rstest/internal/dead-code-escape-limits.json
| #[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" | ||
| ))) | ||
| } |
There was a problem hiding this comment.
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.
| #[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(()) | ||
| } |
There was a problem hiding this comment.
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.
| #[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 { |
There was a problem hiding this comment.
🟡 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:
- Per-symbol gating (earlier commits) — added
#[cfg(bun_standalone)]stub arms and#[cfg(not(bun_standalone))]guards insidetest_runner/*.rs,bake/*.rs,js_bundle_completion_task.rs, anddispatch.rs. - Module-level swap (commit cb8c88c "structural cfg-gating") — replaced the entire
mod test_runner/mod bakedeclarations inlib.rswith stub files, and gatedmod js_bundle_completion_taskinapi.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.rsdefinesBun__TestScope__Describe2__bunTestThen/bunTestCatch— identical to the#[cfg(bun_standalone)]blocks atbun_test.rs:1395-1411.bake_standalone_stub.rsdefinesBakeToWindowsPath/BakeProdResolve/BakeProdLoad/BakeProdSourceMap— identical to the#[cfg(bun_standalone)]blocks inbake/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 intest_runner_standalone_stub.rs)test_runner/diff_format.rs:102-107and the matching#[cfg(not(bun_standalone))] { … }wrappertest_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 ~7054bake/production.rs— four#[cfg(bun_standalone)]stub fns + the matching#[cfg(not(bun_standalone))]on the real implsbake/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 thecall_check_slowstatement, redundant because the enclosingBundleV2DeferredBatchTaskmatch 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%).
| #[cfg(bun_standalone)] | ||
| unreachable!(concat!( | ||
| "node-fallback polyfill `", | ||
| $code_path, | ||
| "` is not available in standalone executables" | ||
| )); |
There was a problem hiding this comment.
🟡 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
- Resolve —
VirtualMachine.rs:4013andjsc_hooks.rs:4824both doif 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))]. - Load —
transpile_source_code_inner(jsc_hooks.rs:2307-2389) and the threadedRuntimeTranspilerStore.rs:772-888both checkis_node_override = path.starts_with(node_fallbacks::IMPORT_PATH)and, when true, callnode_fallbacks::contents_from_path(specifier). Neither call site is cfg-gated. contents_from_path(node_fallbacks.rs:185-197) strips the prefix to"buffer", looks it up inmap(), and calls(module.code)()— which undercfg(bun_standalone)is theunreachable!()getter introduced by this PR.
Step-by-step proof
bun-standalone -e 'await import("/bun-vfs$$/node_modules/buffer/index.js")'import()calls the module resolver with specifier"/bun-vfs$$/node_modules/buffer/index.js".jsc_hooks.rs:4824matchesstarts_with(IMPORT_PATH)(the 24-byte constant atnode_fallbacks.rs:5), returns the specifier verbatim as the resolved path.- The transpile hook sees
is_node_override == trueand callscontents_from_path("/bun-vfs$$/node_modules/buffer/index.js"). contents_from_pathstrips the prefix →"buffer/index.js", takes the segment up to the first/→"buffer", finds it in the staticmap()(bufferis one of the 21 fallback modules), and calls(module.code)().- Under
cfg(bun_standalone),module.codeisget as fn() -> &'static strwhose body isunreachable!("node-fallback polyfill \node-fallbacks/buffer.js` is not available in standalone executables")`. unreachable!()panics → the panic handler aborts the process. No JS-levelTypeError/ResolveMessageis thrown; the user'stry { await import(...) } catchnever 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_pathitself: undercfg(bun_standalone), returnNoneimmediately. 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 ofunreachable!()(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.
|
I just started attempting this and my harness pointed me here! Cool PR! |
What
Adds a second build of the
bunexecutable —bun-standalone— with the toolkit subcommands and the JS APIs that back them compiled out.bun build --compileuses 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)
bunbun-standalonebloatysection diff:.text−6.80 MB,.rodata−849 KB.bun_runtimebun_installbun_cssbun_bundlerbun_css_jscbun_install_jscThe remaining
bun_bundler0.44 MB is theTranspilerhalf (single-file TS→JS, options/defines/cache,analyze_transpiled_module) which is structurally embedded inVirtualMachineand 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
Config.standalonedrives a secondcargo build -p bun_bin --features standalone(withRUSTFLAGS=--cfg=bun_standaloneand a separaterust-target-standalone/dir) and a second link producingbun-standalone[-profile]. The C++ object set is unchanged — both binaries link the samelibbun-profile.a;--gc-sections+.llvm_addrsigdrop the C++ functions whose only Rust callers are gated.<target>-build-rust-standaloneand<target>-build-bun-standalonesteps that reuse the existing<target>-build-cpparchive.binary-sizetracks both variants. Release/upload-release.sh/upload-npm publishbun-standalone-<triplet>.zipand@oven/bun-standalone-*packages. Test runners soft-downloadbun-standalone(runner.node.mjs --standalone-step) and exportBUN_STANDALONE_EXEsotest/cli/standalone-binary.test.tsexercises it in CI without blocking.cfg(bun_standalone)):pub moddeclarations cfg'd out; runtime-reachable items (FileSystemTmpdirExt,Bun__githubURL, release-artifact name consts) relocated tocli/shared.rs;run_command/shell_completionsbun_installrefs cfg-split.pub mod bakereplaced bybake_standalone_stub.rs— uninhabitedDevServer/HotReloadEventsoOption<Box<DevServer>>is a ZST; throwingJSFrameworkRouter; C-ABIBakeProd*/Bake__*stubs; unreachable__bun_dispatch__DevServerHandle__Bake__*link stubs for debug builds.AnyRoute::FrameworkRoutervariant + everydev_server/bake field access cfg-gated;HTMLBundlebundle_v2-typed internals gated.Bun.build/JSBundlerPlugin__*/__bun_blob_from_build_artifactthrow;js_bundle_completion_task/output_file_jscmodules +BundleV2DeferredBatchTaskarm +EXTERNAL_FREE_VTABLEregistration gated.hot_reloaderBundleV2impl gated;AsyncModuleinstall-queue machinery gated;VirtualMachinePackageManager log-swap gated.Bun.color/ 8×JS2Zig__css_internals_*throw;bun:testC-ABI entry points throw / no-op;__bun_resolver_init_package_managerunreachable;dispatch_js2nativeinstall_jsc/patch_jsc hooks stubbed;bun_standalone_graphwrite side gated.--compileintegration: newCompileRuntime { Standalone (default), Full }onCompileTarget;--compile-runtime=<standalone|full>flag; npm URL →@oven/bun-standalone-*; cache key and tarball basename follow the variant;is_default()no longer short-circuits toself_exe_path()for standalone.--version/--revision/crash-reporter footer report1.4.0-standalone[-canary.N][+sha]so CI annotations and bun.report distinguish the binaries.Tested
cargo check -p bun_binandcargo check -p bun_bin --features standalone --cfg=bun_standaloneclean across all 10rust:check-alltargets.bun-standalone(debug+release) builds, links, smoke-tests; toolkit subcommands print an actionable error and exit 1;Bun.build/Bun.colorthrowTypeError;Bun.serve/fetch/-ework.test/cli/standalone-binary.test.ts(6 tests) passes againstbun-standalone-debugand fails against fullbun-debug.test/cli/bun.test.ts+test/bundler/bun-build-api.test.ts(54 tests) pass on fullbun-debug— no regression..buildkite/ci.mjsgenerates valid YAML with 11×build-rust-standalone+ 11×build-bun-standalonesteps.