From bda47754157a52c5393d209913ab696cdf16f6a8 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 15 Jun 2026 05:52:59 +0000 Subject: [PATCH 01/12] build: scaffold bun-standalone binary variant 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 -build-rust-standalone and -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. --- .buildkite/ci.mjs | 70 ++++++++++++++++++- Cargo.toml | 2 +- docs/standalone-binary.md | 108 +++++++++++++++++++++++++++++ package.json | 2 + scripts/build.ts | 1 + scripts/build/buildOptionsRs.ts | 1 + scripts/build/bun.ts | 12 ++-- scripts/build/ci.ts | 27 +++++--- scripts/build/config.ts | 37 ++++++++-- scripts/build/rust.ts | 28 +++++++- src/bun_bin/Cargo.toml | 7 ++ src/runtime/Cargo.toml | 8 +++ src/runtime/cli/mod.rs | 84 ++++++++++++++++++++++ src/runtime/lib.rs | 1 + src/runtime/standalone_build.rs | 48 +++++++++++++ test/cli/standalone-binary.test.ts | 74 ++++++++++++++++++++ 16 files changed, 483 insertions(+), 27 deletions(-) create mode 100644 docs/standalone-binary.md create mode 100644 src/runtime/standalone_build.rs create mode 100644 test/cli/standalone-binary.test.ts diff --git a/.buildkite/ci.mjs b/.buildkite/ci.mjs index 275420c99834..bfec5779f4bd 100755 --- a/.buildkite/ci.mjs +++ b/.buildkite/ci.mjs @@ -525,13 +525,15 @@ function getTestAgent(platform, options) { * @param {Target} target * @param {PipelineOptions} options * @param {"cpp-only" | "rust-only" | "link-only"} mode + * @param {{standalone?: boolean}} [extra] * @returns {string} */ -function getBuildArgs(target, options, mode) { +function getBuildArgs(target, options, mode, extra = {}) { const { os, arch, abi, baseline, profile, crossCompile } = target; const { canary } = options; const args = [`--profile=ci-${mode}`]; + if (extra.standalone) args.push("--standalone=on"); // rust-only cross-compiles (linux host → linux/freebsd targets); os/arch/abi // must all be explicit — host detection (detectLinuxAbi checks @@ -570,9 +572,10 @@ function getBuildArgs(target, options, mode) { * @param {Target} target * @param {PipelineOptions} options * @param {"cpp-only" | "rust-only" | "link-only"} mode + * @param {{standalone?: boolean}} [extra] * @returns {string} */ -function getBuildCommand(target, options, mode) { +function getBuildCommand(target, options, mode, extra) { // Windows code signing is handled by a dedicated 'windows-sign' step after // all Windows builds complete — see getWindowsSignStep(). smctl is x64-only, // so signing on the build agent wouldn't work for ARM64 anyway. @@ -582,7 +585,7 @@ function getBuildCommand(target, options, mode) { // is wrong. PATH on the agent has node via bootstrap.sh. // --experimental-strip-types for Node 24's .ts support (unflagged in // 25+; drop once CI bumps past the ABI-141 blocker). - return `node --experimental-strip-types scripts/build.ts ${getBuildArgs(target, options, mode)}`; + return `node --experimental-strip-types scripts/build.ts ${getBuildArgs(target, options, mode, extra)}`; } /** @@ -659,6 +662,63 @@ function getLinkBunStep(platform, options) { }; } +/** + * Second cargo build for the reduced-footprint `bun-standalone` runtime + * (`--features standalone`). Same agent fan-out as build-rust. + * + * @param {Platform} platform + * @param {PipelineOptions} options + * @returns {Step} + */ +function getBuildRustStandaloneStep(platform, options) { + return { + key: `${getTargetKey(platform)}-build-rust-standalone`, + retry: getRetry(), + label: `${getTargetLabel(platform)} - build-rust-standalone`, + agents: getRustAgent(platform, options), + cancel_on_build_failing: isMergeQueue(), + command: getBuildCommand(platform, options, "rust-only", { standalone: true }), + timeout_in_minutes: 35, + }; +} + +/** + * Second link for `bun-standalone`. Reuses the same `build-cpp` archive + * (the C++ side is identical); only the Rust staticlib differs. + * + * @param {Platform} platform + * @param {PipelineOptions} options + * @returns {Step} + */ +function getLinkBunStandaloneStep(platform, options) { + return { + key: `${getTargetKey(platform)}-build-bun-standalone`, + label: `${getTargetLabel(platform)} - build-bun-standalone`, + depends_on: [`${getTargetKey(platform)}-build-cpp`, `${getTargetKey(platform)}-build-rust-standalone`], + agents: getLinkBunAgent(platform, options), + retry: getRetry(), + cancel_on_build_failing: isMergeQueue(), + env: { + ASAN_OPTIONS: "allow_user_segv_handler=1:disable_coredump=0:detect_leaks=0", + }, + command: getBuildCommand(platform, options, "link-only", { standalone: true }), + }; +} + +/** + * Whether to build `bun-standalone` for this platform. Only the plain + * release lanes that ship to users for `bun build --compile` — not asan, + * and not Android/FreeBSD until --compile supports those targets. + * + * @param {Platform} platform + */ +function shouldBuildStandalone(platform) { + if ((platform.profile ?? "release") !== "release") return false; + if (platform.abi === "android") return false; + if (platform.os === "freebsd") return false; + return true; +} + /** * Returns the artifact triplet for a platform, e.g. "bun-linux-aarch64" or "bun-linux-x64-musl-baseline". * Matches the naming convention in cmake/targets/BuildBun.cmake. @@ -1468,6 +1528,10 @@ async function getPipeline(options = {}) { steps.push(getBuildCppStep(target, options)); steps.push(getBuildRustStep(target, options)); steps.push(getLinkBunStep(target, options)); + if (shouldBuildStandalone(target)) { + steps.push(getBuildRustStandaloneStep(target, options)); + steps.push(getLinkBunStandaloneStep(target, options)); + } if (needsBaselineVerification(target)) { steps.push(getVerifyBaselineStep(target, options)); diff --git a/Cargo.toml b/Cargo.toml index 99eac8bedf3b..1af973dac023 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -188,7 +188,7 @@ warnings = { level = "deny", priority = -1 } # `bun_asan` is set via RUSTFLAGS (`--cfg=bun_asan` + `--check-cfg=cfg(bun_asan)`) # by scripts/build/rust.ts for asan builds; register it here so a plain # `cargo build` / `cargo check` (without those flags) doesn't warn. -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(bun_asan)'] } +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(bun_asan)', 'cfg(bun_standalone)'] } # link.exe unconditionally prints "Creating library X.dll.lib and object # X.dll.exp" to stdout when linking each proc-macro DLL on Windows hosts; # there is no linker flag to suppress it. The lint already exempts itself diff --git a/docs/standalone-binary.md b/docs/standalone-binary.md new file mode 100644 index 000000000000..01619e30eb60 --- /dev/null +++ b/docs/standalone-binary.md @@ -0,0 +1,108 @@ +# `bun-standalone` — the `--compile` runtime binary + +`bun-standalone` is a second build of the `bun` executable with the toolkit +subcommands compiled out. It exists so that `bun build --compile` can produce +smaller single-file executables: the embedded runtime only needs to *run* +JavaScript, not bundle it, install packages, or run a test suite. + +The binary name is `bun-standalone` (`bun-standalone.exe` on Windows). Debug +and instrumented variants follow the same suffix scheme as the full binary +(`bun-standalone-debug`, `bun-standalone-asan`, …). + +## What's removed + +The CLI dispatch for every subcommand other than the run path is replaced +with an error message pointing at the full Bun install: + + - `bun build` + - `bun test` + - `bun install` / `add` / `remove` / `update` / `link` / `unlink` / `pm` / + `outdated` / `publish` / `audit` / `why` / `info` / `patch` + - `bun init` / `create` / `x` / `upgrade` + +`bun `, `bun run`, `bun --eval/--print`, `bun exec`, `bun repl`, and +the `node`-shim entry remain. + +The dispatch sever is the load-bearing change: with the per-tag `exec_*` +bodies gone, `--gc-sections` (driven by `.llvm_addrsig`, which both rustc and +clang emit) drops the now-unreferenced `bundle_v2` / `PackageManager` / +`TestCommand` machinery from the final image. The C++ object set is unchanged +— `build-cpp` produces one archive that both `bun` and `bun-standalone` link +against. + +## How it's built + +`cfg.standalone` (a boolean on the build `Config`) drives three things: + + - `cargo build -p bun_bin --features standalone` with + `RUSTFLAGS="… --cfg=bun_standalone"` into a separate `--target-dir` + (`rust-target-standalone/`), so the full and standalone staticlibs can + coexist in one build directory. + - the linked executable is named `bun-standalone[-profile]` and the + stripped output `bun-standalone`. + - `bun_core::build_options::STANDALONE_BUILD` is `true`. + +Gating in Rust is on `cfg(bun_standalone)` (the global RUSTFLAG), not +`cfg(feature = "standalone")`, so any crate can branch on it without +threading a cargo feature through the workspace graph. The cargo feature on +`bun_bin` → `bun_runtime` exists so `cargo check -p bun_bin --features +standalone` is a valid invocation. + +Locally: + +```sh +bun run build:standalone # release → build/release-standalone/bun-standalone +bun run build:standalone:debug # debug → build/debug-standalone/bun-standalone-debug +``` + +In CI, each release platform gets two extra steps that reuse the existing +`build-cpp` artifact: + +``` +-build-cpp (shared) +-build-rust ────────► -build-bun +-build-rust-standalone ────────► -build-bun-standalone +``` + +`scripts/build/ci.ts::downloadArtifacts` derives the rust sibling from the +step-key suffix; the cpp sibling is always `-build-cpp`. Packaged +artifacts are `bun-standalone--[-musl][-baseline].zip`. + +## Size + +Linux-x64 release, May 2026 linker map: + +| | MB | +|---|--:| +| stripped `bun` | 83.2 | +| bundler + css + install + test + bake + toolkit CLI | −7.1 | +| **`bun-standalone` (this change)** | **~76** | +| | | +| ICU data (`.rodata`) | 23.7 | +| JavaScriptCore `.text` | 22.9 | +| Bun C++ bindings + WebCore + BoringSSL + codecs | ~10 | +| runtime transpiler (parser/printer/ast/resolver) | 2.4 | + +The < 35 MB target requires shipping a reduced ICU data file (small-icu is +~5 MB instead of 24 MB) on top of this; that is a WebKit-prebuilt change +tracked separately. + +## Follow-up work + +This change lands the build infrastructure and the CLI-dispatch sever. The +remaining `#[no_mangle]` entry points that keep subsystem code alive are +mapped in `src/runtime/standalone_build.rs` and gated incrementally: + + - `Bun.build()` / `JSBundlerPlugin__*` → stub to throw, drops `BundleV2`. + - `Bun.color()` / `JS2Zig__css_internals_*` → stub, drops `bun_css`. + - `bun:test` module / `Expect*` codegen classes → needs a C++-side + `#if !BUN_STANDALONE` around `jest.classes.ts` codegen and + `matchAsymmetricMatcherAndGetFlags` in `bindings.cpp`. + - `bake` DevServer → cfg the `dev_server` field on `ServerInstance` and the + `AnyRoute::FrameworkRouter` variant. + - `bun_standalone_graph` read/write split → make `bun_bundler` / + `bun_libarchive` / `bun_http` optional behind a `write` feature so the + standalone binary only carries the graph reader. + - `--compile` target selection → add `standalone: bool` to `CompileTarget` + so cross-compile downloads `@oven/bun-standalone-` and same-host + builds don't short-circuit to `self_exe_path()`. diff --git a/package.json b/package.json index fda02bd39d8a..50182ab17cdc 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,8 @@ "build:asan": "bun scripts/build.ts --profile=release-asan --build-dir=build/release-asan", "build:logs": "bun scripts/build.ts --profile=release --logs=on --build-dir=build/release-logs", "build:smol": "bun scripts/build.ts --profile=release --build-type=MinSizeRel --build-dir=build/release-smol", + "build:standalone": "bun scripts/build.ts --profile=release --standalone=on --build-dir=build/release-standalone", + "build:standalone:debug": "bun scripts/build.ts --profile=debug --standalone=on --build-dir=build/debug-standalone", "build:local": "bun scripts/build.ts --profile=debug-local --build-dir=build/debug-local", "build:release:local": "bun scripts/build.ts --profile=release-local --build-dir=build/release-local", "run:linux": "docker run --rm -v \"$PWD:/root/bun/\" -w /root/bun ghcr.io/oven-sh/bun-development-docker-image", diff --git a/scripts/build.ts b/scripts/build.ts index 6818debb2305..5269a831dc68 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -387,6 +387,7 @@ function parseArgs(argv: string[]): CliArgs { "unifiedSources", "archiveDeps", "timeTrace", + "standalone", "ci", "buildkite", ]); diff --git a/scripts/build/buildOptionsRs.ts b/scripts/build/buildOptionsRs.ts index a24fc16829b9..df518cf02596 100644 --- a/scripts/build/buildOptionsRs.ts +++ b/scripts/build/buildOptionsRs.ts @@ -66,6 +66,7 @@ export function generateBuildOptionsRs(cfg: Config): string { "// default (config.ts) is the negation of this predicate.", "pub const ENABLE_LOGS: bool = cfg!(debug_assertions);", "pub const ENABLE_ASAN: bool = cfg!(bun_asan);", + "pub const STANDALONE_BUILD: bool = cfg!(bun_standalone);", "pub const ENABLE_TINYCC: bool = !cfg!(any(", ` all(windows, target_arch = "aarch64"),`, ` target_os = "android",`, diff --git a/scripts/build/bun.ts b/scripts/build/bun.ts index 4b9b4f3c08de..dfe40716aa8b 100644 --- a/scripts/build/bun.ts +++ b/scripts/build/bun.ts @@ -27,7 +27,7 @@ import { dirname, relative, resolve, sep } from "node:path"; import type { Sources } from "../glob-sources.ts"; import { emitCodegen, type CodegenOutputs } from "./codegen.ts"; import { ar, cc, cxx, link, pch } from "./compile.ts"; -import { bunExeName, shouldStrip, type Config } from "./config.ts"; +import { bunExeName, bunStrippedName, shouldStrip, type Config } from "./config.ts"; import { generateDepVersionsHeader } from "./depVersionsHeader.ts"; import { allDeps } from "./deps/index.ts"; import { lolhtml } from "./deps/lolhtml.ts"; @@ -595,8 +595,12 @@ function emitLinkOnly(n: Ninja, cfg: Config): BunOutput { } // Archive from cpp-only: same name cpp-only emits (exe name + lib - // prefix/suffix, e.g. libbun-profile.a). - const archive = resolve(cfg.buildDir, `${cfg.libPrefix}${exeName}${cfg.libSuffix}`); + // prefix/suffix, e.g. libbun-profile.a). cpp-only is never built with + // `standalone` (the C++ side is identical), so the standalone link step + // consumes the same archive as the full link — the archive name is + // computed with `standalone: false` regardless of this link's variant. + const cppExeName = bunExeName({ ...cfg, standalone: false }); + const archive = resolve(cfg.buildDir, `${cfg.libPrefix}${cppExeName}${cfg.libSuffix}`); // libbun_rust.a from rust-only: same path emitRust writes to. Shared // helper so both sides of the CI split agree (cargo's @@ -711,7 +715,7 @@ function emitSmokeTest(n: Ninja, cfg: Config, exe: string, exeName: string): voi * The profile binary keeps its symbols for profiling/debugging release crashes. */ function emitStrip(n: Ninja, cfg: Config, inputExe: string, stripflags: string[]): string { - const out = resolve(cfg.buildDir, "bun" + cfg.exeSuffix); + const out = resolve(cfg.buildDir, bunStrippedName(cfg) + cfg.exeSuffix); // Windows: strip equivalent is handled at link time (/OPT:REF etc), no // separate strip binary. The "stripped" bun is just a copy. Copy command diff --git a/scripts/build/ci.ts b/scripts/build/ci.ts index 291cfb9d14fe..53d6626eef39 100644 --- a/scripts/build/ci.ts +++ b/scripts/build/ci.ts @@ -14,7 +14,7 @@ import { fileURLToPath } from "node:url"; // @ts-ignore — utils.mjs has JSDoc types but no .d.ts import * as utils from "../utils.mjs"; import { bunExeName, shouldStrip, type BunOutput } from "./bun.ts"; -import type { Config } from "./config.ts"; +import { bunStrippedName, type Config } from "./config.ts"; import { BuildError } from "./error.ts"; import { crossFeaturesJson } from "./features-json.ts"; @@ -347,7 +347,7 @@ function upload(paths: string[], cwd: string): void { * cmake's bunTriplet — any drift breaks test-step downloads. */ function computeBunTriplet(cfg: Config): string { - let t = `bun-${cfg.os}-${cfg.arch}`; + let t = `${bunStrippedName(cfg)}-${cfg.os}-${cfg.arch}`; if (cfg.abi === "musl") t += "-musl"; if (cfg.abi === "android") t += "-android"; if (cfg.baseline) t += "-baseline"; @@ -400,7 +400,11 @@ export function packageAndUpload(cfg: Config, output: BunOutput): void { // cmake's bunPath: string(REPLACE bun ${bunTriplet} bunPath ${bun}) // where ${bun} is the target name (bun-profile, bun-asan, ...). // Result: bun-linux-x64-profile, bun-linux-x64-asan, etc. - const bunPath = exeName.replace(/^bun/, bunTriplet); + // Replace the *base* name (bun / bun-standalone), not just /^bun/, so + // `bun-standalone-profile` → `bun-standalone-linux-x64-profile` rather than + // `bun-standalone-linux-x64-standalone-profile`. + const baseName = bunStrippedName(cfg); + const bunPath = bunTriplet + exeName.slice(baseName.length); const files: string[] = [basename(exe), "features.json"]; // Debug symbols / linker map — platform-specific extras. if (cfg.windows) { @@ -493,25 +497,30 @@ export async function downloadArtifacts(cfg: Config): Promise { }); } - // step key is `-build-bun`; siblings are `-build-{cpp,rust}`. - const m = stepKey.match(/^(.+)-build-bun$/); + // step key is `-build-bun[-standalone]`; siblings are + // `-build-cpp` (always — the C++ archive is shared) and + // `-build-rust[-standalone]` (the rust .a is variant-specific). + const m = stepKey.match(/^(.+)-build-bun(-standalone)?$/); if (m === null) { throw new BuildError(`Unexpected BUILDKITE_STEP_KEY: ${stepKey}`, { - hint: "Expected format: -build-bun", + hint: "Expected format: -build-bun[-standalone]", }); } const targetKey = m[1]!; + const variantSuffix = m[2] ?? ""; + if ((variantSuffix === "-standalone") !== cfg.standalone) { + throw new BuildError(`BUILDKITE_STEP_KEY variant (${stepKey}) disagrees with --standalone=${cfg.standalone}`); + } // Both downloads at once (buildkite-agent already parallelizes within a // step's artifact set; this overlaps the two STEPS). Gunzip after BOTH // complete — the rust .a is gzipped too on posix, and the .gz scan is a // recursive walk so we want every artifact on disk first. - const dl = (suffix: "cpp" | "rust") => { - const step = `${targetKey}-build-${suffix}`; + const dl = (step: string) => { console.log(`Downloading artifacts from ${step}...`); return runAsync(["buildkite-agent", "artifact", "download", "*", ".", "--step", step], cfg.buildDir); }; - await Promise.all([dl("cpp"), dl("rust")]); + await Promise.all([dl(`${targetKey}-build-cpp`), dl(`${targetKey}-build-rust${variantSuffix}`)]); // Recursive: rust artifact lands under rust-target///. const gzFiles: string[] = []; diff --git a/scripts/build/config.ts b/scripts/build/config.ts index 380575939466..ac2575beea46 100644 --- a/scripts/build/config.ts +++ b/scripts/build/config.ts @@ -154,6 +154,14 @@ export interface Config { archiveDeps: boolean; /** Emit clang -ftime-trace .json next to each .o for build profiling. */ timeTrace: boolean; + /** + * Build the reduced-footprint `bun-standalone` binary used as the runtime + * for `bun build --compile` output. CLI subcommands (install/build/test/…) + * and the JS APIs they back are compiled out via the `standalone` cargo + * feature on `bun_bin`; the C++ side is unchanged and dead code is dropped + * by `--gc-sections` + addrsig at link time. See docs/standalone-binary.md. + */ + standalone: boolean; // ─── Environment ─── ci: boolean; @@ -337,6 +345,7 @@ export interface PartialConfig { unifiedSources?: boolean; archiveDeps?: boolean; timeTrace?: boolean; + standalone?: boolean; ci?: boolean; buildkite?: boolean; webkit?: WebKitMode; @@ -1124,6 +1133,7 @@ export function resolveConfig(partial: PartialConfig, toolchain: Toolchain): Con unifiedSources: partial.unifiedSources ?? true, archiveDeps: partial.archiveDeps ?? false, timeTrace: partial.timeTrace ?? false, + standalone: partial.standalone ?? false, ci, buildkite, webkit: partial.webkit ?? "prebuilt", @@ -1394,15 +1404,27 @@ function computeBuildDirName(c: { debug: boolean; release: boolean; asan: boolea * without a circular import. */ export function bunExeName(cfg: Config): string { - if (cfg.debug) return "bun-debug"; + // `bun-standalone` is the reduced-footprint --compile runtime; it has the + // same debug/release/asan variants as the full binary. The base name + // changes so both can sit in one build dir / one CI artifact set. + const base = cfg.standalone ? "bun-standalone" : "bun"; + if (cfg.debug) return `${base}-debug`; // Release variants — suffix encodes which features differ from plain release. // First match wins. - if (cfg.asan && cfg.valgrind) return "bun-asan-valgrind"; - if (cfg.asan) return "bun-asan"; - if (cfg.valgrind) return "bun-valgrind"; - if (cfg.assertions) return "bun-assertions"; - // Plain release: called bun-profile (the stripped one is `bun`). - return "bun-profile"; + if (cfg.asan && cfg.valgrind) return `${base}-asan-valgrind`; + if (cfg.asan) return `${base}-asan`; + if (cfg.valgrind) return `${base}-valgrind`; + if (cfg.assertions) return `${base}-assertions`; + // Plain release: called -profile (the stripped one is ``). + return `${base}-profile`; +} + +/** + * Basename of the stripped output (`bun` / `bun-standalone`). Kept separate + * from `bunExeName()` so callers don't reproduce the standalone branch. + */ +export function bunStrippedName(cfg: Config): string { + return cfg.standalone ? "bun-standalone" : "bun"; } /** @@ -1444,6 +1466,7 @@ export function formatConfig(cfg: Config, exe: string): string { if (cfg.baseline) features.push("baseline"); if (cfg.valgrind) features.push("valgrind"); if (cfg.fuzzilli) features.push("fuzzilli"); + if (cfg.standalone) features.push("standalone"); if (!cfg.canary) features.push("canary:off"); // Non-default modes — show so you notice when a build is unusual. if (cfg.webkit !== "prebuilt") features.push(`webkit:${cfg.webkit}`); diff --git a/scripts/build/rust.ts b/scripts/build/rust.ts index 698ae952ba20..7842d0804513 100644 --- a/scripts/build/rust.ts +++ b/scripts/build/rust.ts @@ -164,9 +164,16 @@ function findRustup(cfg: Config): string | undefined { // Paths // ─────────────────────────────────────────────────────────────────────────── -/** `/rust-target` — sibling of `obj/`, `pch/`. */ +/** + * `/rust-target[-standalone]` — sibling of `obj/`, `pch/`. + * + * The standalone variant gets its own target dir: cargo's output path doesn't + * encode features, so sharing one dir would have the two `cargo build` + * invocations overwrite each other's `libbun_rust.a` (and thrash incremental + * fingerprints for every crate that sees the `standalone` feature). + */ function rustTargetDir(cfg: Config): string { - return resolve(cfg.buildDir, "rust-target"); + return resolve(cfg.buildDir, cfg.standalone ? "rust-target-standalone" : "rust-target"); } /** @@ -377,6 +384,12 @@ export function emitRust(n: Ninja, cfg: Config, inputs: RustBuildInputs): string "--profile", profile.name, ]; + if (cfg.standalone) { + // Propagates to `bun_runtime/standalone`; see src/bun_bin/Cargo.toml. + // Paired with `--cfg=bun_standalone` in rustflags below so crates outside + // the feature chain (bun_core, bun_standalone_graph) can branch on it too. + args.push("--features", "standalone"); + } if (tier3 || cfg.release || cfg.asan) { // Build std/core/alloc from source instead of linking the rustup prebuilt. // @@ -489,6 +502,15 @@ export function emitRust(n: Ninja, cfg: Config, inputs: RustBuildInputs): string if (!cfg.debug) { rustflags.push("--cfg=bun_codegen_embed"); } + // `bun_standalone`: reduced-footprint --compile runtime. Carried as a global + // cfg (not just the cargo feature) so any crate can `#[cfg(bun_standalone)]` + // without threading a feature through the workspace graph. The feature on + // `bun_bin` is still passed (above) so `cfg(feature = "standalone")` works + // for the crates that declare it. + rustflags.push("--check-cfg=cfg(bun_standalone)"); + if (cfg.standalone) { + rustflags.push("--cfg=bun_standalone"); + } // Drop `#[track_caller]` source-location capture in release. Every // `Option::unwrap`/`slice[i]`/`RefCell::borrow` etc. otherwise emits a // `&'static core::panic::Location` (file/line/col) plus the file-path string @@ -888,7 +910,7 @@ export function rustLtoLinkInputs(n: Ninja, cfg: Config, rustObjects: string[]): { hint: "Install the pinned rust toolchain (rustup show active-toolchain), or build with --lto=off" }, ); const llvmBin = join(cfg.rustSysroot, "lib", "rustlib", cfg.host.rustTriple, "bin"); - const out = resolve(cfg.buildDir, "bun_rust.lto.o"); + const out = resolve(cfg.buildDir, cfg.standalone ? "bun_rust_standalone.lto.o" : "bun_rust.lto.o"); n.build({ outputs: [out], rule: "rust_lto_fix", diff --git a/src/bun_bin/Cargo.toml b/src/bun_bin/Cargo.toml index 94839e8d1237..dbe0bcdf3afd 100644 --- a/src/bun_bin/Cargo.toml +++ b/src/bun_bin/Cargo.toml @@ -16,6 +16,13 @@ crate-type = ["staticlib"] [lints] workspace = true +[features] +default = [] +# Reduced-footprint `bun-standalone` binary used as the runtime for +# `bun build --compile` output. Paired with `--cfg=bun_standalone` (a global +# RUSTFLAG set by scripts/build/rust.ts) so any crate can branch on it. +standalone = ["bun_runtime/standalone"] + [dependencies] bstr.workspace = true bun_io.workspace = true diff --git a/src/runtime/Cargo.toml b/src/runtime/Cargo.toml index d2d68b9f2f9c..d8e4ee71539d 100644 --- a/src/runtime/Cargo.toml +++ b/src/runtime/Cargo.toml @@ -117,3 +117,11 @@ show_crash_trace = ["bun_bundler/show_crash_trace"] error_return_tracing = [] # Mirrors Zig `bun.FeatureFlags.bake_debugging_features` — DevServer dump flags. bake_debugging_features = [] +# Reduced-footprint `bun-standalone` binary: compile out CLI subcommands +# (install/build/test/pm/…) and the JS APIs that back them. Stubbed entry +# points print an error pointing at the full bun binary. Dead code is then +# stripped by `--gc-sections` + addrsig at link time. Always paired with the +# global `--cfg=bun_standalone` RUSTFLAG (scripts/build/rust.ts) — gate on +# `cfg(bun_standalone)`, not `cfg(feature = "standalone")`, so the same +# predicate works in crates outside the feature chain. +standalone = [] diff --git a/src/runtime/cli/mod.rs b/src/runtime/cli/mod.rs index 25af3f71c10a..9d9f1d72ba14 100644 --- a/src/runtime/cli/mod.rs +++ b/src/runtime/cli/mod.rs @@ -4,6 +4,15 @@ //! against lower-tier crates. `Command::start()` (full dispatch) and //! per-command exec bodies live in the sibling `*_command.rs` modules. +// Under `bun_standalone` the toolkit dispatch arms are compiled out (see the +// `cfg(bun_standalone)` match in `Command::start`), which orphans most +// `*_command` module contents. The modules stay declared so the few items the +// runtime still reaches (`upgrade_command::FileSystemTmpdirExt`, +// `upgrade_command::Bun__githubURL`, codegen js2native thunks) keep linking; +// the rest is dropped by `--gc-sections`. The non-standalone build still +// enforces `dead_code = "deny"`. +#![cfg_attr(bun_standalone, allow(dead_code, unused_macros, unused_imports))] + use core::cell::Cell; use bun_core::strings; @@ -1303,6 +1312,27 @@ pub mod command { // live and honours `stop_after_positional_at = 1` — the shim broke // `bun --version` by intercepting the flag meant for ``. + // ─── bun-standalone ───────────────────────────────────────────────── + // The reduced-footprint --compile runtime only carries the run path: + // a real --compile output returns via `boot_standalone` above and + // never reaches this match. The bare `bun-standalone` binary (CI + // smoke test, `BUN_BE_BUN=1`, debugging) keeps Auto/Run/RunAsNode/ + // Exec/Repl/Help working; every toolkit subcommand surfaces an + // explicit error so the dispatch arm below — and the `*_command` + // modules it references — can be compiled out for `--gc-sections`. + #[cfg(bun_standalone)] + return match tag { + Tag::AutoCommand | Tag::RunCommand => exec_auto_or_run(tag, log), + Tag::HelpCommand => HelpCommand::exec(), + Tag::ReservedCommand => ReservedCommand::exec(), + Tag::DiscordCommand => super::discord_command::DiscordCommand::exec(), + Tag::RunAsNodeCommand => exec_run_as_node(log), + Tag::ExecCommand => exec_exec(log), + Tag::ReplCommand => exec_repl(log), + other => crate::standalone_build::unavailable_command(tag_name(other)), + }; + + #[cfg(not(bun_standalone))] match tag { Tag::AutoCommand | Tag::RunCommand => exec_auto_or_run(tag, log), Tag::HelpCommand => HelpCommand::exec(), @@ -1338,6 +1368,48 @@ pub mod command { } } + /// User-facing subcommand name for the standalone "not available" error. + /// Only the toolkit tags need entries; runtime tags never reach the stub. + #[cfg(bun_standalone)] + fn tag_name(tag: Tag) -> &'static [u8] { + match tag { + Tag::BuildCommand => b"build", + Tag::TestCommand => b"test", + Tag::InstallCommand => b"install", + Tag::AddCommand => b"add", + Tag::RemoveCommand => b"remove", + Tag::UpdateCommand => b"update", + Tag::UpdateInteractiveCommand => b"update --interactive", + Tag::LinkCommand => b"link", + Tag::UnlinkCommand => b"unlink", + Tag::PackageManagerCommand => b"pm", + Tag::OutdatedCommand => b"outdated", + Tag::PublishCommand => b"publish", + Tag::PatchCommand => b"patch", + Tag::PatchCommitCommand => b"patch --commit", + Tag::AuditCommand => b"audit", + Tag::WhyCommand => b"why", + Tag::InfoCommand => b"info", + Tag::InitCommand => b"init", + Tag::CreateCommand => b"create", + Tag::BunxCommand => b"x", + Tag::UpgradeCommand => b"upgrade", + Tag::InstallCompletionsCommand => b"completions", + Tag::GetCompletionsCommand => b"getcompletes", + Tag::FuzzilliCommand => b"fuzzilli", + // Runtime tags — handled before the stub arm; listed for + // exhaustiveness only. + Tag::AutoCommand + | Tag::RunCommand + | Tag::RunAsNodeCommand + | Tag::HelpCommand + | Tag::ReservedCommand + | Tag::DiscordCommand + | Tag::ExecCommand + | Tag::ReplCommand => b"", + } + } + // ─── out-lined `start` arm bodies ─────────────────────────────────────── // Every per-tag body lives in its own `#[cold] #[inline(never)]` fn so // `start` itself stays a jump table. The `Auto/Run` arm is the hot path @@ -1463,6 +1535,7 @@ pub mod command { return run_command::RunCommand::exec_eval(ctx); } + #[cfg(not(bun_standalone))] if tag == Tag::AutoCommand && ctx.args.entry_points.len() == 1 { let extension = bun_paths::extension(&ctx.args.entry_points[0]); if extension == b".lockb" { @@ -1492,6 +1565,7 @@ pub mod command { Ok(()) } + #[cfg(not(bun_standalone))] #[cold] #[inline(never)] fn exec_init() -> CmdResult { @@ -1500,6 +1574,7 @@ pub mod command { super::init_command::InitCommand::exec(&argv[2.min(argv.len())..]) } + #[cfg(not(bun_standalone))] #[cold] #[inline(never)] fn exec_install_completions() -> CmdResult { @@ -1524,6 +1599,7 @@ pub mod command { run_command::RunCommand::exec_as_if_node(ctx) } + #[cfg(not(bun_standalone))] #[cold] #[inline(never)] fn exec_bunx(log: &mut bun_ast::Log) -> CmdResult { @@ -1545,6 +1621,7 @@ pub mod command { super::repl_command::ReplCommand::exec(ctx) } + #[cfg(not(bun_standalone))] #[cold] #[inline(never)] fn exec_build(log: &mut bun_ast::Log) -> CmdResult { @@ -1553,6 +1630,7 @@ pub mod command { Ok(()) } + #[cfg(not(bun_standalone))] #[cold] #[inline(never)] fn exec_audit(log: &mut bun_ast::Log) -> CmdResult { @@ -1573,6 +1651,7 @@ pub mod command { Ok(()) } + #[cfg(not(bun_standalone))] #[cold] #[inline(never)] fn exec_fuzzilli(log: &mut bun_ast::Log) -> CmdResult { @@ -1597,6 +1676,7 @@ pub mod command { )* }; } + #[cfg(not(bun_standalone))] cold_exec! { exec_pm => (PackageManagerCommand, super::package_manager_command::PackageManagerCommand::exec), exec_install => (InstallCommand, super::install_command::InstallCommand::exec), @@ -1648,6 +1728,7 @@ pub mod command { b"help", ]; + #[cfg(not(bun_standalone))] #[cold] #[inline(never)] fn bun_getcompletes(log: &mut bun_ast::Log) -> Result<(), bun_core::Error> { @@ -1761,6 +1842,7 @@ pub mod command { Ok(()) } + #[cfg(not(bun_standalone))] #[cold] #[inline(never)] fn bun_create(log: &mut bun_ast::Log) -> Result<(), bun_core::Error> { @@ -1883,6 +1965,7 @@ To create a project with the official Next.js scaffolding tool, run\n\ } /// `bun ./bun.lockb` — print lockfile as yarn.lock (or its hash with `--hash`). + #[cfg(not(bun_standalone))] #[cold] #[inline(never)] fn bun_lockb(ctx: &mut ContextData) -> Result<(), bun_core::Error> { @@ -1916,6 +1999,7 @@ To create a project with the official Next.js scaffolding tool, run\n\ Printer::print(unsafe { ctx.log_mut() }, &entry, PrinterFormat::Yarn) } + #[cfg(not(bun_standalone))] #[cold] #[inline(never)] fn bun_info(log: &mut bun_ast::Log) -> Result<(), bun_core::Error> { diff --git a/src/runtime/lib.rs b/src/runtime/lib.rs index 0cfd6f57b7f5..b34d8cf0cc15 100644 --- a/src/runtime/lib.rs +++ b/src/runtime/lib.rs @@ -26,6 +26,7 @@ pub mod webcore; pub mod bake; pub mod cli; pub mod shell; +pub mod standalone_build; // `Run::boot` / `Run::boot_standalone`. Mounted here // (not as a separate crate) because every dependency it has is already a dep of // `bun_runtime`, and the CLI dispatch in `cli/` needs to call it directly. The diff --git a/src/runtime/standalone_build.rs b/src/runtime/standalone_build.rs new file mode 100644 index 000000000000..a90d971afdb4 --- /dev/null +++ b/src/runtime/standalone_build.rs @@ -0,0 +1,48 @@ +//! `bun-standalone` build support. +//! +//! The `bun-standalone` binary is the reduced-footprint runtime that +//! `bun build --compile` attaches a module graph to. It carries the full JS +//! runtime (event loop, module loader, `Bun.serve`, `fetch`, node compat, +//! crypto, FFI, …) but compiles out the toolkit subcommands and the JS APIs +//! that back them — `bun install`/`add`/`remove`/`pm`, `bun build`, +//! `bun test`, `bun create`/`init`/`x`/`upgrade`, `Bun.build()`, the bake +//! DevServer, and the CSS parser surface. +//! +//! Gating is on `cfg(bun_standalone)` (a global RUSTFLAG set by +//! `scripts/build/rust.ts`), not `cfg(feature = "standalone")`, so any crate +//! can branch on it without threading a cargo feature through the workspace. +//! The C/C++ object set is identical between `bun` and `bun-standalone`; +//! `--gc-sections` + `.llvm_addrsig` drop the C++ functions whose only +//! Rust-side callers were compiled out. +//! +//! Every stub here surfaces a user-facing error; nothing is a silent no-op. + +/// True for the `bun-standalone` binary. Same as +/// `bun_core::build_options::STANDALONE_BUILD`. +pub const IS_STANDALONE: bool = cfg!(bun_standalone); + +/// Print the "not available in this binary" error for a CLI subcommand and +/// exit non-zero. Used by the `cfg(bun_standalone)` dispatch arm in +/// `cli::Command::start()`. +#[cold] +#[allow(dead_code)] +pub fn unavailable_command(name: &[u8]) -> ! { + bun_core::pretty_errorln!( + "error: bun {} is not available in this executable", + bstr::BStr::new(name), + ); + bun_core::pretty_errorln!(""); + bun_core::pretty_errorln!( + "This is a standalone executable built with bun build --compile. It contains the", + ); + bun_core::pretty_errorln!( + "Bun runtime but not the bundler, package manager, or test runner.", + ); + bun_core::pretty_errorln!(""); + bun_core::pretty_errorln!( + "To use bun {}, install Bun: https://bun.com/get", + bstr::BStr::new(name), + ); + bun_core::output::flush(); + bun_core::Global::exit(1); +} diff --git a/test/cli/standalone-binary.test.ts b/test/cli/standalone-binary.test.ts new file mode 100644 index 000000000000..3b0a307a271b --- /dev/null +++ b/test/cli/standalone-binary.test.ts @@ -0,0 +1,74 @@ +// Tests for the `bun-standalone` binary (the reduced-footprint --compile +// runtime). `bun-standalone` has no `bun test` command, so these run under +// the FULL bun and spawn the standalone binary as a subprocess. +// +// Locally: +// bun run build:standalone:debug +// BUN_STANDALONE_EXE=build/debug-standalone/bun-standalone-debug \ +// bun bd test test/cli/standalone-binary.test.ts +// +// In CI the standalone binary's own `--revision` smoke test is the link-time +// gate; this file is the behavioural one. + +import { describe, expect, test } from "bun:test"; +import { existsSync } from "node:fs"; +import { bunEnv, normalizeBunSnapshot } from "harness"; + +const standaloneExe = process.env.BUN_STANDALONE_EXE; + +describe.skipIf(!standaloneExe || !existsSync(standaloneExe))("bun-standalone", () => { + const exe = standaloneExe!; + + test("toolkit subcommands print an actionable error and exit non-zero", async () => { + for (const cmd of ["build", "test", "install", "add", "pm", "create", "init", "x", "upgrade"]) { + await using proc = Bun.spawn({ + cmd: [exe, cmd], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(normalizeBunSnapshot(stderr)).toContain("not available in this executable"); + expect(normalizeBunSnapshot(stderr)).toContain("https://bun.com/get"); + expect(stdout).toBe(""); + expect(exitCode).toBe(1); + } + }); + + test("--revision works", async () => { + await using proc = Bun.spawn({ + cmd: [exe, "--revision"], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout.trim()).toMatch(/^\d+\.\d+\.\d+/); + expect(exitCode).toBe(0); + }); + + test("running a script works", async () => { + await using proc = Bun.spawn({ + cmd: [exe, "-e", "console.log(1 + 1)"], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(normalizeBunSnapshot(stdout)).toBe("2"); + expect(exitCode).toBe(0); + }); + + test("STANDALONE_BUILD const is true", async () => { + await using proc = Bun.spawn({ + cmd: [exe, "-e", "process.stdout.write(String(process.isBun))"], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + // Sanity: it's a Bun runtime. + expect(stdout).toBe("true"); + expect(exitCode).toBe(0); + }); +}); From 97fd0e9671739523197aeef34a09341bb4f5dd5e Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 15 Jun 2026 05:55:42 +0000 Subject: [PATCH 02/12] release: publish bun-standalone artifacts and npm packages - 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 --- .buildkite/ci.mjs | 16 ++++++++++-- .buildkite/scripts/upload-release.sh | 30 ++++++++++++++++++++++ packages/bun-release/scripts/upload-npm.ts | 14 +++++++--- packages/bun-release/src/platform.ts | 14 ++++++++++ 4 files changed, 68 insertions(+), 6 deletions(-) diff --git a/.buildkite/ci.mjs b/.buildkite/ci.mjs index bfec5779f4bd..316f6e5605cc 100755 --- a/.buildkite/ci.mjs +++ b/.buildkite/ci.mjs @@ -998,7 +998,14 @@ function getWindowsSignStep(windowsPlatforms, options) { * @returns {Step} */ function getBinarySizeStep(releasePlatforms, options, { recordOnly = false } = {}) { - const targets = releasePlatforms.map(p => ({ triplet: getTargetTriplet(p) })); + const standalone = releasePlatforms.filter(shouldBuildStandalone); + const targets = [ + ...releasePlatforms.map(p => ({ triplet: getTargetTriplet(p) })), + // packageAndUpload sets `binary-size:bun-standalone-` from the + // standalone link step; track those alongside the full binary so size + // regressions in either variant trip the threshold. + ...standalone.map(p => ({ triplet: getTargetTriplet(p).replace(/^bun-/, "bun-standalone-") })), + ]; const args = [`--targets '${JSON.stringify(targets)}'`, `--threshold-mb ${BINARY_SIZE_THRESHOLD_MB}`]; if (recordOnly) args.push("--no-fail"); if (!options.canary) args.push("--release"); @@ -1011,7 +1018,10 @@ function getBinarySizeStep(releasePlatforms, options, { recordOnly = false } = { options, { instanceType: "c8g.large" }, ), - depends_on: releasePlatforms.map(p => `${getTargetKey(p)}-build-bun`), + depends_on: [ + ...releasePlatforms.map(p => `${getTargetKey(p)}-build-bun`), + ...standalone.map(p => `${getTargetKey(p)}-build-bun-standalone`), + ], allow_dependency_failure: true, soft_fail: !!options.skipSizeCheck, retry: { @@ -1040,6 +1050,8 @@ function getReleaseStep(buildPlatforms, options, { signed = false } = {}) { const depends_on = signed ? [...buildPlatforms.filter(p => p.os !== "windows").map(p => `${getTargetKey(p)}-build-bun`), "windows-sign"] : buildPlatforms.map(platform => `${getTargetKey(platform)}-build-bun`); + // upload-release.sh also publishes the bun-standalone artifacts. + depends_on.push(...buildPlatforms.filter(shouldBuildStandalone).map(p => `${getTargetKey(p)}-build-bun-standalone`)); return { key: "release", diff --git a/.buildkite/scripts/upload-release.sh b/.buildkite/scripts/upload-release.sh index 977ca9a71d96..d44622b1a7fe 100755 --- a/.buildkite/scripts/upload-release.sh +++ b/.buildkite/scripts/upload-release.sh @@ -238,6 +238,36 @@ function create_release() { bun-windows-aarch64-profile.zip ) + # Reduced-footprint --compile runtime. Same triplets minus android/freebsd + # (see shouldBuildStandalone in .buildkite/ci.mjs). buildkite-agent artifact + # download without --step searches the whole build, so these are picked up + # from the *-build-bun-standalone steps. + local standalone_artifacts=( + bun-standalone-darwin-aarch64.zip + bun-standalone-darwin-aarch64-profile.zip + bun-standalone-darwin-x64.zip + bun-standalone-darwin-x64-profile.zip + bun-standalone-linux-aarch64.zip + bun-standalone-linux-aarch64-profile.zip + bun-standalone-linux-x64.zip + bun-standalone-linux-x64-profile.zip + bun-standalone-linux-x64-baseline.zip + bun-standalone-linux-x64-baseline-profile.zip + bun-standalone-linux-aarch64-musl.zip + bun-standalone-linux-aarch64-musl-profile.zip + bun-standalone-linux-x64-musl.zip + bun-standalone-linux-x64-musl-profile.zip + bun-standalone-linux-x64-musl-baseline.zip + bun-standalone-linux-x64-musl-baseline-profile.zip + bun-standalone-windows-x64.zip + bun-standalone-windows-x64-profile.zip + bun-standalone-windows-x64-baseline.zip + bun-standalone-windows-x64-baseline-profile.zip + bun-standalone-windows-aarch64.zip + bun-standalone-windows-aarch64-profile.zip + ) + artifacts+=("${standalone_artifacts[@]}") + function upload_artifact() { local artifact="$1" download_buildkite_artifact "$artifact" diff --git a/packages/bun-release/scripts/upload-npm.ts b/packages/bun-release/scripts/upload-npm.ts index dfb76fe39fef..cdad6273f654 100644 --- a/packages/bun-release/scripts/upload-npm.ts +++ b/packages/bun-release/scripts/upload-npm.ts @@ -12,7 +12,9 @@ import { fetch } from "../src/fetch"; import { chmod, copy, exists, join, write, writeJson } from "../src/fs"; import { getRelease, getSemver } from "../src/github"; import type { Platform } from "../src/platform"; -import { platforms } from "../src/platform"; +import { platforms, standalonePlatforms } from "../src/platform"; + +const allPlatforms = [...platforms, ...standalonePlatforms]; import { spawn } from "../src/spawn"; const module = "bun"; @@ -41,14 +43,14 @@ process.exit(0); // HACK async function build(): Promise { await buildRootModule(); - for (const platform of platforms) { + for (const platform of allPlatforms) { if (action !== "publish" && (platform.os !== process.platform || platform.arch !== process.arch)) continue; await buildModule(release, platform); } } async function publish(dryRun?: boolean): Promise { - const modules = platforms + const modules = allPlatforms .filter(({ os, arch }) => action === "publish" || (os === process.platform && arch === process.arch)) .map(({ bin }) => `${owner}/${bin}`); modules.push(module); @@ -149,7 +151,11 @@ async function buildModule( error(`No asset found: ${bin}`); return; } - const bun = await extractFromZip(asset.browser_download_url, `${bin}/bun`); + // The release zip layout is `/` where `` is + // `bun` or `bun-standalone` (Windows adds `.exe`). `extractFromZip` matches + // by prefix, so the suffix-less form covers both. + const exeBase = bin.startsWith("bun-standalone-") ? "bun-standalone" : "bun"; + const bun = await extractFromZip(asset.browser_download_url, `${bin}/${exeBase}`); const cwd = join("npm", module); mkdirSync(dirname(join(cwd, exe)), { recursive: true }); write(join(cwd, exe), await bun.async("arraybuffer")); diff --git a/packages/bun-release/src/platform.ts b/packages/bun-release/src/platform.ts index cdd27b0d0733..a9caf6ef0410 100644 --- a/packages/bun-release/src/platform.ts +++ b/packages/bun-release/src/platform.ts @@ -133,6 +133,20 @@ export const platforms: Platform[] = [ }, ]; +/** + * `@oven/bun-standalone-*` — the reduced-footprint runtime that + * `bun build --compile` downloads and embeds. Same matrix as the full + * binary minus Android/FreeBSD (see `shouldBuildStandalone` in + * `.buildkite/ci.mjs`). The tarball ships `bin/bun-standalone[.exe]`. + */ +export const standalonePlatforms: Platform[] = platforms + .filter(p => p.abi !== "android" && p.os !== "freebsd") + .map(p => ({ + ...p, + bin: p.bin.replace(/^bun-/, "bun-standalone-"), + exe: p.exe.replace(/\bbun(\.exe)?$/, "bun-standalone$1"), + })); + export const supportedPlatforms: Platform[] = platforms .filter( platform => From de8eb91a55c369ff45fe52bacc7eec3161102f14 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 15 Jun 2026 06:02:13 +0000 Subject: [PATCH 03/12] runtime: gate Bun.build/color/bun:test/bake/install entry points under 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-*. --- .buildkite/ci.mjs | 31 ++++++-- .buildkite/scripts/upload-release.sh | 11 ++- Cargo.lock | 1 - scripts/runner.node.mjs | 77 +++++++++++++++++++ src/install/PackageManager.rs | 2 + src/install/auto_installer.rs | 14 ++++ src/options_types/compile_target.rs | 32 +++++++- src/runtime/api/BunObject.rs | 18 +++++ src/runtime/api/JSBundler.rs | 71 +++++++++++++++++ src/runtime/bake/DevServer.rs | 21 ++++- src/runtime/bake/mod.rs | 1 + src/runtime/bake/production.rs | 32 ++++++++ src/runtime/cli/Arguments.rs | 25 ++++++ src/runtime/cli/build_command.rs | 3 + src/runtime/dispatch.rs | 1 + src/runtime/dispatch_js2native.rs | 18 +++++ src/runtime/hw_exports.rs | 9 ++- src/runtime/jsc_hooks.rs | 26 +++++-- src/runtime/lib.rs | 1 + src/runtime/node.rs | 2 + src/runtime/server/server_body.rs | 12 ++- src/runtime/test_runner/bun_test.rs | 22 ++++++ src/runtime/test_runner/diff_format.rs | 8 ++ src/runtime/test_runner/expect.rs | 22 ++++++ src/runtime/test_runner/jest.rs | 18 +++++ src/runtime/webcore/BakeResponse.rs | 36 ++++++++- src/standalone_graph/Cargo.toml | 1 - src/standalone_graph/StandaloneModuleGraph.rs | 52 +++++++++++-- 28 files changed, 538 insertions(+), 29 deletions(-) diff --git a/.buildkite/ci.mjs b/.buildkite/ci.mjs index 316f6e5605cc..65bca67f06fa 100755 --- a/.buildkite/ci.mjs +++ b/.buildkite/ci.mjs @@ -863,6 +863,11 @@ function getTestBunStep(platform, options, testOptions = {}) { const { buildId, testFiles } = testOptions; const args = [`--step=${getTargetKey(platform)}-build-bun`]; + // bun-standalone is built by a sibling step; runner.node.mjs downloads it + // best-effort and exports BUN_STANDALONE_EXE for test/cli/standalone-binary.test.ts. + if (shouldBuildStandalone(platform)) { + args.push(`--standalone-step=${getTargetKey(platform)}-build-bun-standalone`); + } if (buildId) { args.push(`--build-id=${buildId}`); } @@ -877,6 +882,11 @@ function getTestBunStep(platform, options, testOptions = {}) { const depends = []; if (!buildId) { depends.push(`${getTargetKey(platform)}-build-bun`); + if (shouldBuildStandalone(platform)) { + // Soft dependency: wait for the standalone build so the artifact exists, + // but don't block tests if that step failed. + depends.push({ step: `${getTargetKey(platform)}-build-bun-standalone`, allow_failure: true }); + } } return { @@ -964,6 +974,12 @@ function getWindowsSignStep(windowsPlatforms, options) { const stepKey = `${getTargetKey(platform)}-build-bun`; artifacts.push(`${triplet}-profile.zip`, `${triplet}.zip`); buildSteps.push(stepKey, stepKey); + 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); + } } // Signing runs on a real Windows x64 machine (smctl; doesn't work on @@ -972,7 +988,10 @@ function getWindowsSignStep(windowsPlatforms, options) { return { key: "windows-sign", label: `${getBuildkiteEmoji("windows")} sign`, - depends_on: windowsPlatforms.map(p => `${getTargetKey(p)}-build-bun`), + depends_on: windowsPlatforms.flatMap(p => [ + `${getTargetKey(p)}-build-bun`, + ...(shouldBuildStandalone(p) ? [`${getTargetKey(p)}-build-bun-standalone`] : []), + ]), agents: getEc2Agent({ os: "windows", arch: "x64", release: "2019" }, options, { instanceType: getAzureVmSize("windows", "x64", "test"), }), @@ -1047,11 +1066,13 @@ function getReleaseStep(buildPlatforms, options, { signed = false } = {}) { // When signing ran, depend on windows-sign instead of the raw Windows builds // so we wait for signed artifacts before releasing. + const buildKeys = p => [ + `${getTargetKey(p)}-build-bun`, + ...(shouldBuildStandalone(p) ? [`${getTargetKey(p)}-build-bun-standalone`] : []), + ]; const depends_on = signed - ? [...buildPlatforms.filter(p => p.os !== "windows").map(p => `${getTargetKey(p)}-build-bun`), "windows-sign"] - : buildPlatforms.map(platform => `${getTargetKey(platform)}-build-bun`); - // upload-release.sh also publishes the bun-standalone artifacts. - depends_on.push(...buildPlatforms.filter(shouldBuildStandalone).map(p => `${getTargetKey(p)}-build-bun-standalone`)); + ? [...buildPlatforms.filter(p => p.os !== "windows").flatMap(buildKeys), "windows-sign"] + : buildPlatforms.flatMap(buildKeys); return { key: "release", diff --git a/.buildkite/scripts/upload-release.sh b/.buildkite/scripts/upload-release.sh index d44622b1a7fe..239a6809513d 100755 --- a/.buildkite/scripts/upload-release.sh +++ b/.buildkite/scripts/upload-release.sh @@ -125,7 +125,7 @@ function download_buildkite_artifact() { # (build-bun unsigned, windows-sign signed). Pin to the sign step to # guarantee we get the signed one. local step_args=() - if [[ -n "$WINDOWS_ARTIFACT_STEP" && "$name" == bun-windows-* ]]; then + if [[ -n "$WINDOWS_ARTIFACT_STEP" && ( "$name" == bun-windows-* || "$name" == bun-standalone-windows-* ) ]]; then step_args=(--step "$WINDOWS_ARTIFACT_STEP") fi run_command buildkite-agent artifact download "$name" "$dir" "${step_args[@]}" @@ -285,6 +285,15 @@ function create_release() { upload_artifact "$artifact" done + # 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 + update_github_release "$tag" create_sentry_release "$tag" send_discord_announcement "$tag" diff --git a/Cargo.lock b/Cargo.lock index 340be45cb90a..d52fafc3595b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2084,7 +2084,6 @@ dependencies = [ "bun_exe_format", "bun_http", "bun_io", - "bun_js_parser", "bun_libarchive", "bun_opaque", "bun_options_types", diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index dbd96a26e802..cbd15309f474 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -12,6 +12,7 @@ import { createHash } from "node:crypto"; import { accessSync, appendFileSync, + chmodSync, existsSync, constants as fs, linkSync, @@ -107,6 +108,11 @@ const { values: options, positionals: filters } = parseArgs({ type: "string", default: undefined, }, + /** BuildKite step to download the bun-standalone binary from (soft-fail). */ + ["standalone-step"]: { + type: "string", + default: undefined, + }, ["build-id"]: { type: "string", default: undefined, @@ -431,6 +437,18 @@ async function runTests() { } !isQuiet && console.log("Bun:", execPath); + if (options["standalone-step"]) { + const standalonePath = await getStandaloneExecPathFromBuildKite(options["standalone-step"], options["build-id"]); + if (standalonePath) { + // Exported via process.env so spawnBun's `...process.env` spread carries + // it into every test's env. test/cli/standalone-binary.test.ts reads this. + process.env.BUN_STANDALONE_EXE = standalonePath; + !isQuiet && console.log("Bun (standalone):", standalonePath); + } else { + !isQuiet && console.log("Bun (standalone): "); + } + } + const expectations = getTestExpectations(); const modifiers = getTestModifiers(execPath); !isQuiet && console.log("Modifiers:", modifiers); @@ -2113,6 +2131,65 @@ async function getExecPathFromBuildKite(target, buildId) { throw new Error(`Could not find executable from BuildKite: ${releasePath}`); } +/** + * Best-effort download of the bun-standalone artifact from a sibling build + * step. Unlike getExecPathFromBuildKite this never throws — a missing or + * failed standalone build must not block the regular test run. + * + * @param {string} stepKey + * @param {string} [buildId] + * @returns {Promise} + */ +async function getStandaloneExecPathFromBuildKite(stepKey, buildId) { + const releasePath = join(cwd, "release-standalone"); + mkdirSync(releasePath, { recursive: true }); + + const args = ["artifact", "download", "**", releasePath, "--step", stepKey]; + if (buildId) { + args.push("--build", buildId); + } + + const { error } = await spawnSafe({ + command: "buildkite-agent", + args, + timeout: 120000, + }); + if (error) { + console.warn(`bun-standalone artifact download from '${stepKey}' failed (${error}); tests will skip.`); + return undefined; + } + + const zipPath = readdirSync(releasePath, { recursive: true, encoding: "utf-8" }) + .filter(filename => /^bun-standalone.*\.zip$/i.test(filename)) + .map(filename => join(releasePath, filename)) + // Prefer the stripped binary over -profile for test speed. + .sort((a, b) => a.includes("profile") - b.includes("profile")) + .at(0); + + if (!zipPath) { + console.warn(`No bun-standalone*.zip found in artifacts from '${stepKey}'.`); + return undefined; + } + + try { + await unzip(zipPath, releasePath); + } catch (cause) { + console.warn(`Failed to extract ${zipPath}:`, cause); + return undefined; + } + + for (const entry of readdirSync(releasePath, { recursive: true, encoding: "utf-8" })) { + const exe = join(releasePath, entry); + if (/bun-standalone(?:-[a-z]+)?(?:\.exe)?$/i.test(entry) && statSync(exe).isFile()) { + if (!isWindows) chmodSync(exe, 0o755); + return exe; + } + } + + console.warn(`Could not find bun-standalone executable in ${releasePath}`); + return undefined; +} + /** * @param {string} execPath * @returns {string} diff --git a/src/install/PackageManager.rs b/src/install/PackageManager.rs index 6402ee6d70dc..2936ea88886f 100644 --- a/src/install/PackageManager.rs +++ b/src/install/PackageManager.rs @@ -2254,6 +2254,7 @@ pub fn init( Ok((unsafe { &mut *manager_ptr }, original_cwd_clone)) } +#[cfg(not(bun_standalone))] pub(crate) fn init_with_runtime( log: &mut bun_ast::Log, // Used read-only (`Options::load` only ever reads `config.*`). @@ -2270,6 +2271,7 @@ pub(crate) fn init_with_runtime( get() } +#[cfg(not(bun_standalone))] pub(crate) fn init_with_runtime_once( log: &mut bun_ast::Log, bun_install: Option<&Api::BunInstall>, diff --git a/src/install/auto_installer.rs b/src/install/auto_installer.rs index 1423b40c4399..04d82298aba3 100644 --- a/src/install/auto_installer.rs +++ b/src/install/auto_installer.rs @@ -451,6 +451,7 @@ impl hooks::AutoInstaller for PackageManager { // • `env` is the resolver's unwrapped `env_loader` (Transpiler-owned, // process-lifetime). `init_with_runtime` stores it as // `NonNull>`. +#[cfg(not(bun_standalone))] #[unsafe(no_mangle)] pub(crate) unsafe fn __bun_resolver_init_package_manager( mut log: core::ptr::NonNull, @@ -480,3 +481,16 @@ pub(crate) unsafe fn __bun_resolver_init_package_manager( core::ptr::NonNull::new(pm as *mut dyn hooks::AutoInstaller) .expect("init_with_runtime returns the holder::RAW_PTR singleton") } + +// `bun-standalone` carries no package manager: auto-install is forced off in +// the resolver, so this link-time hook is never called. Keep the symbol so the +// resolver's `extern "Rust"` declaration still resolves; reaching it is a bug. +#[cfg(bun_standalone)] +#[unsafe(no_mangle)] +pub(crate) unsafe fn __bun_resolver_init_package_manager( + _log: core::ptr::NonNull, + _install: Option>, + _env: core::ptr::NonNull>, +) -> core::ptr::NonNull { + unreachable!("auto-install is not available in bun-standalone") +} diff --git a/src/options_types/compile_target.rs b/src/options_types/compile_target.rs index 10dc55f4a0ed..1428189b0ebe 100644 --- a/src/options_types/compile_target.rs +++ b/src/options_types/compile_target.rs @@ -22,6 +22,28 @@ pub struct CompileTarget { pub baseline: bool, pub version: Version, pub libc: Libc, + pub runtime: CompileRuntime, +} + +/// Which Bun binary to embed as the runtime in `--compile` output. +#[repr(u8)] +#[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, +} + +impl CompileRuntime { + /// Prefix inserted after `bun-` in npm package names and cache keys. + pub(crate) const fn npm_prefix(self) -> &'static str { + match self { + CompileRuntime::Standalone => "standalone-", + CompileRuntime::Full => "", + } + } } impl Default for CompileTarget { @@ -44,6 +66,9 @@ impl Default for CompileTarget { } else { Libc::Default }, + // The running process is always the full `bun` binary; `is_default()` + // only short-circuits to self_exe_path when the requested runtime is Full. + runtime: CompileRuntime::Full, } } } @@ -105,6 +130,7 @@ impl CompileTarget { && self.baseline == other.baseline && self.version.eql(other.version) && self.libc == other.libc + && self.runtime == other.runtime } pub fn is_default(&self) -> bool { @@ -137,6 +163,7 @@ impl CompileTarget { let os = self.os.npm_name().as_bytes(); let arch = self.arch.npm_name(); let libc = self.libc.npm_name(); + let runtime = self.runtime.npm_prefix().as_bytes(); let baseline: &[u8] = if self.baseline { b"-baseline" } else { b"" }; let total = buf.len(); @@ -145,12 +172,14 @@ impl CompileTarget { let res = (|| -> std::io::Result<()> { cursor.write_all(registry_url)?; cursor.write_all(b"/@oven/bun-")?; + cursor.write_all(runtime)?; cursor.write_all(os)?; cursor.write_all(b"-")?; cursor.write_all(arch.as_bytes())?; cursor.write_all(libc.as_bytes())?; cursor.write_all(baseline)?; cursor.write_all(b"/-/bun-")?; + cursor.write_all(runtime)?; cursor.write_all(os)?; cursor.write_all(b"-")?; cursor.write_all(arch.as_bytes())?; @@ -460,7 +489,8 @@ impl fmt::Display for CompileTarget { // This doesn't match up 100% with npm, but that's okay. write!( f, - "bun-{}-{}{}{}-v{}.{}.{}", + "bun-{}{}-{}{}{}-v{}.{}.{}", + self.runtime.npm_prefix(), self.os.npm_name(), self.arch.npm_name(), self.libc, diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index ede56cfa32c4..b4596cf08ddd 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -191,6 +191,14 @@ mod static_adapters { } pub(super) fn js_bundler_build(g: &JSGlobalObject, cf: &CallFrame) -> JsResult { + #[cfg(bun_standalone)] + { + let _ = cf; + return Err(g.throw_type_error(format_args!( + "Bun.build is not available in standalone executables. Install Bun: https://bun.com/get" + ))); + } + #[cfg(not(bun_standalone))] crate::api::js_bundler::JSBundler::build_fn(g, cf) } /// `Bun.$` parsed-script constructor — wraps the marked-argument-buffer host fn. @@ -337,7 +345,10 @@ pub mod bun_object { export_callbacks! { BunObject_callback_allocUnsafe => super::alloc_unsafe, BunObject_callback_build => super::static_adapters::js_bundler_build, + #[cfg(not(bun_standalone))] BunObject_callback_color => bun_css_jsc::js_function_color, + #[cfg(bun_standalone)] + BunObject_callback_color => super::color_unavailable, BunObject_callback_connect => super::static_adapters::listener_connect, BunObject_callback_deflateSync => JSZlib::deflate_sync, BunObject_callback_file => crate::webcore::blob::construct_bun_file, @@ -1709,6 +1720,13 @@ pub(crate) fn serve(global_object: &JSGlobalObject, callframe: &CallFrame) -> Js } } +#[cfg(bun_standalone)] +pub(crate) fn color_unavailable(global: &JSGlobalObject, _: &CallFrame) -> JsResult { + Err(global.throw_type_error(format_args!( + "Bun.color is not available in standalone executables", + ))) +} + #[bun_jsc::host_fn] pub(crate) fn alloc_unsafe( global_this: &JSGlobalObject, diff --git a/src/runtime/api/JSBundler.rs b/src/runtime/api/JSBundler.rs index 40610f0d62b5..0fe86157d27e 100644 --- a/src/runtime/api/JSBundler.rs +++ b/src/runtime/api/JSBundler.rs @@ -1,5 +1,12 @@ //! `Bun.build()` plugin host + `BuildArtifact` JS wrapper. +// Under `bun_standalone` the `Bun.build()` entry point and the C++-called +// plugin thunks are stubbed (see the `cfg(bun_standalone)` block at the bottom +// of `mod js_bundler`); their helper functions become dead so the bundler call +// graph can be dropped by `--gc-sections`. The non-standalone build still +// enforces `dead_code = "deny"`. +#![cfg_attr(bun_standalone, allow(dead_code, unused_imports))] + use bun_options_types::LoaderExt as _; use core::ffi::c_void; @@ -1387,6 +1394,7 @@ pub mod js_bundler { /// # Safety /// `resolve` must be the live `*mut Resolve` previously handed to C++ via /// `Resolve::dispatch`; sole owner on the JS thread for the call duration. + #[cfg(not(bun_standalone))] #[unsafe(no_mangle)] pub(crate) unsafe extern "C" fn JSBundlerPlugin__onResolveAsync( resolve: *mut Resolve, @@ -1505,6 +1513,7 @@ pub mod js_bundler { /// `load` must be the live `*mut Load` previously handed to C++ via /// `Load::dispatch`, and `global` must be the plugin's owning /// `JSGlobalObject`; both valid and exclusively accessed on the JS thread. + #[cfg(not(bun_standalone))] #[unsafe(no_mangle)] pub(crate) unsafe extern "C" fn JSBundlerPlugin__onDefer( load: *mut Load, @@ -1514,6 +1523,7 @@ pub mod js_bundler { unsafe { jsc::to_js_host_call(&*global, || (&mut *load).on_defer(&*global)) } } + #[cfg(not(bun_standalone))] #[unsafe(no_mangle)] pub(crate) extern "C" fn JSBundlerPlugin__onLoadAsync( this: &mut Load, @@ -1781,6 +1791,7 @@ pub mod js_bundler { /// the live `*mut Resolve` (when `which == 0`) or `*mut Load` (when /// `which == 1`) previously handed to C++ via `dispatch`; sole owner on /// the JS thread. + #[cfg(not(bun_standalone))] #[unsafe(no_mangle)] pub(crate) unsafe extern "C" fn JSBundlerPlugin__addError( ctx: *mut c_void, @@ -1810,6 +1821,59 @@ pub mod js_bundler { _ => panic!("invalid error type"), } } + + // ─── bun-standalone stubs ─────────────────────────────────────────────── + // The shared C++ archive references these symbols unconditionally, so they + // must keep linking under `cfg(bun_standalone)` even though `Bun.build()` + // (the only path that hands C++ a live `Resolve`/`Load`/`Plugin`) is gated + // out. Signatures stay C-ABI-identical via `*mut c_void`; bodies are + // unreachable because no plugin is ever dispatched. + #[cfg(bun_standalone)] + #[unsafe(no_mangle)] + pub(crate) extern "C" fn JSBundlerPlugin__onResolveAsync( + _resolve: *mut c_void, + _unused: *mut c_void, + _path_value: JSValue, + _namespace_value: JSValue, + _external_value: JSValue, + ) { + unreachable!("Bun.build is not available in standalone executables"); + } + + #[cfg(bun_standalone)] + #[unsafe(no_mangle)] + pub(crate) extern "C" fn JSBundlerPlugin__onDefer( + _load: *mut c_void, + global: *mut JSGlobalObject, + ) -> JSValue { + // SAFETY: `global` is the plugin's owning `JSGlobalObject` per the C++ caller. + unsafe { &*global }.throw_type_error(format_args!( + "Bun.build is not available in standalone executables. Install Bun: https://bun.com/get" + )); + JSValue::ZERO + } + + #[cfg(bun_standalone)] + #[unsafe(no_mangle)] + pub(crate) extern "C" fn JSBundlerPlugin__onLoadAsync( + _this: *mut c_void, + _unused: *mut c_void, + _source_code_value: JSValue, + _loader_as_int: JSValue, + ) { + unreachable!("Bun.build is not available in standalone executables"); + } + + #[cfg(bun_standalone)] + #[unsafe(no_mangle)] + pub(crate) extern "C" fn JSBundlerPlugin__addError( + _ctx: *mut c_void, + _plugin: *mut c_void, + _exception: JSValue, + _which: JSValue, + ) { + unreachable!("Bun.build is not available in standalone executables"); + } } pub use js_bundler as JSBundler; @@ -1840,6 +1904,13 @@ pub use bun_bundler::options::OutputKind; /// `extern "Rust"` in `bun_jsc::webcore_types`; link-time resolved. #[unsafe(no_mangle)] pub(crate) fn __bun_blob_from_build_artifact(value: JSValue) -> Option<*mut Blob> { + #[cfg(bun_standalone)] + { + // No `Bun.build()` ⇒ no `BuildArtifact` instances can exist. + let _ = value; + return None; + } + #[cfg(not(bun_standalone))] ::from_js(value).map(|b| { // SAFETY: `from_js` returns the non-null `*mut BuildArtifact` kept alive by // the JS wrapper; `addr_of_mut!` only computes the field address (no deref). diff --git a/src/runtime/bake/DevServer.rs b/src/runtime/bake/DevServer.rs index 930ca31e7073..329aef84e84f 100644 --- a/src/runtime/bake/DevServer.rs +++ b/src/runtime/bake/DevServer.rs @@ -10,6 +10,7 @@ //! For questions about its core philosophy, email `devserver@paperclover.net` #![allow(unexpected_cfgs)] // `feature = "bake_debugging_features"` is not yet a declared cargo feature. +#![cfg_attr(bun_standalone, allow(dead_code, unused_imports))] use ::core::ffi::c_void; use bun_bundler::mal_prelude::*; @@ -6918,7 +6919,17 @@ bun_jsc::jsc_host_abi! { request_ptr: *mut c_void, url: BunString, ) -> JSValue { - jsc::to_js_host_call(global, || bundle_new_route_js_function_impl(global, request_ptr, url)) + jsc::to_js_host_call(global, || { + #[cfg(bun_standalone)] + { + let _ = (request_ptr, url); + return Err(global.throw(format_args!( + "Bake DevServer is not available in standalone executables" + ))); + } + #[cfg(not(bun_standalone))] + bundle_new_route_js_function_impl(global, request_ptr, url) + }) } } @@ -7040,6 +7051,14 @@ pub(super) fn bake_get_new_route_params_js_function_impl( callframe: &CallFrame, ) -> JSValue { jsc::to_js_host_call(global, || { + #[cfg(bun_standalone)] + { + let _ = callframe; + return Err(global.throw(format_args!( + "Bake DevServer is not available in standalone executables" + ))); + } + #[cfg(not(bun_standalone))] new_route_params_for_bundle_promise_for_js(global, callframe) }) } diff --git a/src/runtime/bake/mod.rs b/src/runtime/bake/mod.rs index 39d4d9f35b97..9cb97016e1bd 100644 --- a/src/runtime/bake/mod.rs +++ b/src/runtime/bake/mod.rs @@ -19,6 +19,7 @@ pub(crate) mod bake_body; #[path = "DevServer.rs"] mod dev_server_body; +#[cfg_attr(bun_standalone, allow(unused_imports))] pub(crate) use dev_server_body::get_deinit_count_for_testing; pub(crate) use dev_server_body::is_allowed_dev_host; diff --git a/src/runtime/bake/production.rs b/src/runtime/bake/production.rs index 5b0f4d695b9e..0f7788bc9a83 100644 --- a/src/runtime/bake/production.rs +++ b/src/runtime/bake/production.rs @@ -1338,6 +1338,13 @@ unsafe extern "C" { ) -> *mut JSPromise; } +#[cfg(bun_standalone)] +#[unsafe(no_mangle)] +pub(super) extern "C" fn BakeToWindowsPath(_input: BunString) -> BunString { + BunString::dead() +} + +#[cfg(not(bun_standalone))] #[unsafe(no_mangle)] pub(super) extern "C" fn BakeToWindowsPath(input: BunString) -> BunString { #[cfg(unix)] @@ -1355,6 +1362,17 @@ pub(super) extern "C" fn BakeToWindowsPath(input: BunString) -> BunString { } } +#[cfg(bun_standalone)] +#[unsafe(no_mangle)] +pub(super) extern "C" fn BakeProdResolve( + _global: &JSGlobalObject, + _a_str: BunString, + _specifier_str: BunString, +) -> BunString { + BunString::dead() +} + +#[cfg(not(bun_standalone))] #[unsafe(no_mangle)] pub(super) extern "C" fn BakeProdResolve( global: &JSGlobalObject, @@ -1635,6 +1653,13 @@ impl Drop for PerThread { } /// Given a key, returns the source code to load. +#[cfg(bun_standalone)] +#[unsafe(no_mangle)] +pub(super) extern "C" fn BakeProdLoad(_pt: *mut PerThread, _key: BunString) -> BunString { + BunString::dead() +} + +#[cfg(not(bun_standalone))] #[unsafe(no_mangle)] pub(super) extern "C" fn BakeProdLoad(pt: *mut PerThread, key: BunString) -> BunString { // SAFETY: `pt` is the non-null pointer previously attached via @@ -1653,6 +1678,13 @@ pub(super) extern "C" fn BakeProdLoad(pt: *mut PerThread, key: BunString) -> Bun BunString::dead() } +#[cfg(bun_standalone)] +#[unsafe(no_mangle)] +pub(super) extern "C" fn BakeProdSourceMap(_pt: *mut PerThread, _key: BunString) -> BunString { + BunString::dead() +} + +#[cfg(not(bun_standalone))] #[unsafe(no_mangle)] pub(super) extern "C" fn BakeProdSourceMap(pt: *mut PerThread, key: BunString) -> BunString { // SAFETY: `pt` is the non-null pointer previously attached via diff --git a/src/runtime/cli/Arguments.rs b/src/runtime/cli/Arguments.rs index accab9fb352a..6d9919b85d0a 100644 --- a/src/runtime/cli/Arguments.rs +++ b/src/runtime/cli/Arguments.rs @@ -418,6 +418,9 @@ pub(crate) const BUILD_ONLY_PARAMS: &[ParamType] = concat_params!( parse_param!( "--compile-executable-path Path to a Bun executable to use for cross-compilation instead of downloading" ), + parse_param!( + "--compile-runtime Which Bun runtime to embed: \"standalone\" (default, smaller) or \"full\"" + ), parse_param!("--bytecode Use a bytecode cache"), parse_param!( "--watch Automatically restart the process on file change" @@ -2011,6 +2014,28 @@ fn parse_build_command_options( ctx.bundler_options.inline_entrypoint_import_meta_main = true; } + if let Some(runtime) = args.option(b"--compile-runtime") { + if !ctx.bundler_options.compile { + Output::err_generic("--compile-runtime requires --compile", ()); + Global::crash(); + } + ctx.bundler_options.compile_target.runtime = match runtime { + b"standalone" => bun_options_types::compile_target::CompileRuntime::Standalone, + b"full" => bun_options_types::compile_target::CompileRuntime::Full, + _ => { + Output::err_generic( + "--compile-runtime must be \"standalone\" or \"full\"", + (), + ); + Global::crash(); + } + }; + } else if ctx.bundler_options.compile { + // Default to the slim standalone runtime for `--compile` output. + ctx.bundler_options.compile_target.runtime = + bun_options_types::compile_target::CompileRuntime::Standalone; + } + if let Some(compile_exec_argv) = args.option(b"--compile-exec-argv") { if !ctx.bundler_options.compile { Output::err_generic("--compile-exec-argv requires --compile", ()); diff --git a/src/runtime/cli/build_command.rs b/src/runtime/cli/build_command.rs index 39320a44012f..f4d121ee2926 100644 --- a/src/runtime/cli/build_command.rs +++ b/src/runtime/cli/build_command.rs @@ -74,6 +74,9 @@ impl BuildCommand { } 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); } diff --git a/src/runtime/dispatch.rs b/src/runtime/dispatch.rs index cd79d08ba66f..ad50dde4e521 100644 --- a/src/runtime/dispatch.rs +++ b/src/runtime/dispatch.rs @@ -345,6 +345,7 @@ pub fn run_task( cast!(JSCDeferredWorkTask).run()?; } task_tag::PollPendingModulesTask => { + #[cfg(not(bun_standalone))] vm.modules.on_poll(); } task_tag::RuntimeTranspilerStore => { diff --git a/src/runtime/dispatch_js2native.rs b/src/runtime/dispatch_js2native.rs index 4af6e63fec56..5eeebd5fb625 100644 --- a/src/runtime/dispatch_js2native.rs +++ b/src/runtime/dispatch_js2native.rs @@ -76,12 +76,30 @@ pub(crate) fn bun_get_use_system_ca( Ok(JSValue::js_boolean(v)) } +#[cfg(not(bun_standalone))] mod css { pub use bun_css_jsc::css_internals::{ _test, attr_test, minify_error_test_with_options, minify_test, minify_test_with_options, prefix_test, prefix_test_with_options, test_with_options, }; } +#[cfg(bun_standalone)] +mod css { + use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; + macro_rules! stub { + ($($name:ident),* $(,)?) => {$( + pub fn $name(global: &JSGlobalObject, _: &CallFrame) -> JsResult { + Err(global.throw_type_error(format_args!( + "CSS internals are not available in standalone executables", + ))) + } + )*}; + } + stub!( + _test, attr_test, minify_error_test_with_options, minify_test, + minify_test_with_options, prefix_test, prefix_test_with_options, test_with_options, + ); +} pub use css::_test as css_jsc_css_internals__test; pub use css::attr_test as css_jsc_css_internals_attr_test; pub use css::minify_error_test_with_options as css_jsc_css_internals_minify_error_test_with_options; diff --git a/src/runtime/hw_exports.rs b/src/runtime/hw_exports.rs index dd343b1a658c..2898171b3ee4 100644 --- a/src/runtime/hw_exports.rs +++ b/src/runtime/hw_exports.rs @@ -449,7 +449,14 @@ pub(crate) unsafe extern "C" fn bindgen_DevServer_dispatchGetDeinitCountForTesti out: *mut usize, ) -> bool { // SAFETY: `out` is a valid C++ stack local out-param. - unsafe { *out = crate::bake::get_deinit_count_for_testing() }; + #[cfg(bun_standalone)] + unsafe { + *out = 0 + }; + #[cfg(not(bun_standalone))] + unsafe { + *out = crate::bake::get_deinit_count_for_testing() + }; true } diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index e7eac1d2afab..bf6ce433c67c 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -400,13 +400,19 @@ unsafe fn init_runtime_state( // from CLI args to the resolver so symlinked node_modules // entries resolve via their link path (peer deps stay reachable). t.resolver.opts.preserve_symlinks = preserve_symlinks; - t.resolver.on_wake_package_manager = bun_resolver::install_types::WakeHandler { - context: core::ptr::NonNull::new(ptr::addr_of_mut!((*vm).modules).cast()), - handler: Some(bun_jsc::async_module::Queue::on_wake_handler), - on_dependency_error: Some( - bun_jsc::async_module::Queue::on_dependency_error, - ), - }; + #[cfg(not(bun_standalone))] + { + t.resolver.on_wake_package_manager = + bun_resolver::install_types::WakeHandler { + context: core::ptr::NonNull::new( + ptr::addr_of_mut!((*vm).modules).cast(), + ), + handler: Some(bun_jsc::async_module::Queue::on_wake_handler), + on_dependency_error: Some( + bun_jsc::async_module::Queue::on_dependency_error, + ), + }; + } // Branch on `opts.graph` here — with a module graph, // auto_jsx=true would // `read_dir_info(cwd)` and cache its tsconfig.json BEFORE @@ -2191,6 +2197,7 @@ fn transpile_source_code_inner( // inline chunk, not the arena, so the pending-imports path must // consume this scope via `take_state()` and ship the box with the // arena. + #[cfg_attr(bun_standalone, allow(unused_variables))] let ast_alloc_scope = bun_alloc::ast_alloc::ScopedAstAlloc::with_spill(arena_heap); // ── Watcher fd / package_json lookup ──────────────────────────── let mut fd: Option = None; @@ -2857,7 +2864,10 @@ fn transpile_source_code_inner( )?; } - // Pending imports → AsyncModule queue. + // Pending imports → AsyncModule queue (auto-install on demand). + // `bun-standalone` has no package manager; the resolver never + // populates `pending_imports`, so the install queue is unreachable. + #[cfg(not(bun_standalone))] if parse_result.pending_imports.len() > 0 { // SAFETY: per fn contract — `extra` is live for the call. let promise_ptr = unsafe { &*extra }.promise_ptr; diff --git a/src/runtime/lib.rs b/src/runtime/lib.rs index b34d8cf0cc15..dbc3a999e5b4 100644 --- a/src/runtime/lib.rs +++ b/src/runtime/lib.rs @@ -57,6 +57,7 @@ pub mod generated_jssink; // include!()s ${BUN_CODEGEN_DIR}/generated_jssink.rs pub mod dns_jsc; pub mod image; +#[cfg_attr(bun_standalone, allow(dead_code))] pub mod test_runner; pub mod valkey_jsc; diff --git a/src/runtime/node.rs b/src/runtime/node.rs index daac1fc22b02..68ce8793af6a 100644 --- a/src/runtime/node.rs +++ b/src/runtime/node.rs @@ -539,9 +539,11 @@ impl MaybeSysExt for Maybe { } } +#[cfg(not(bun_standalone))] pub trait MaybeCssExt: Sized { fn to_css_result(self) -> Maybe>; } +#[cfg(not(bun_standalone))] impl MaybeCssExt for Maybe { #[inline] fn to_css_result(self) -> Maybe> { diff --git a/src/runtime/server/server_body.rs b/src/runtime/server/server_body.rs index 6182510d5cd2..806daef8a6e3 100644 --- a/src/runtime/server/server_body.rs +++ b/src/runtime/server/server_body.rs @@ -694,8 +694,17 @@ impl AnyRoute { argument: JSValue, init_ctx: &mut ServerInitContext, ) -> JsResult> { - use bun_collections::zig_hash_map::MapEntry as StdEntry; if let Some(html_bundle) = ::from_js(argument) { + #[cfg(bun_standalone)] + { + let _ = html_bundle; + return Err(init_ctx.global.throw_type_error(format_args!( + "Serving HTML routes requires the bundler, which is not available in standalone executables" + ))); + } + #[cfg(not(bun_standalone))] + { + use bun_collections::zig_hash_map::MapEntry as StdEntry; let entry = init_ctx .dedupe_html_bundle_map .entry(html_bundle.cast_const()); @@ -720,6 +729,7 @@ impl AnyRoute { } StdEntry::Occupied(o) => AnyRoute::Html(o.get().dupe_ref()), })); + } } if let Some(html_route) = Self::bundled_html_manifest_from_js(argument, init_ctx)? { diff --git a/src/runtime/test_runner/bun_test.rs b/src/runtime/test_runner/bun_test.rs index a59d41f484d2..43a95f23e217 100644 --- a/src/runtime/test_runner/bun_test.rs +++ b/src/runtime/test_runner/bun_test.rs @@ -1368,6 +1368,7 @@ impl Drop for BunTest { // `static JSHostFn = thunk` puts the name in `.data` (nm `d`), and the address // C++ sees never matches the local thunk we hand to `.then()`, tripping the // `RELEASE_ASSERT_NOT_REACHED` at the bottom of `promiseHandlerID`. +#[cfg(not(bun_standalone))] bun_jsc::jsc_host_abi! { #[unsafe(no_mangle)] pub unsafe fn Bun__TestScope__Describe2__bunTestThen( @@ -1379,6 +1380,7 @@ bun_jsc::jsc_host_abi! { jsc::host_fn::to_js_host_fn_result(global, BunTest::bun_test_then(global, frame)) } } +#[cfg(not(bun_standalone))] bun_jsc::jsc_host_abi! { #[unsafe(no_mangle)] pub unsafe fn Bun__TestScope__Describe2__bunTestCatch( @@ -1390,6 +1392,26 @@ bun_jsc::jsc_host_abi! { jsc::host_fn::to_js_host_fn_result(global, BunTest::bun_test_catch(global, frame)) } } +#[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 { + JSValue::UNDEFINED + } +} // Clone/Copy: bitwise OK — `entry` is a non-owning erased borrow of an // `ExecutionEntry` owned by `BunTest::execution`. diff --git a/src/runtime/test_runner/diff_format.rs b/src/runtime/test_runner/diff_format.rs index cc2650028c9b..0bcc27217602 100644 --- a/src/runtime/test_runner/diff_format.rs +++ b/src/runtime/test_runner/diff_format.rs @@ -99,6 +99,13 @@ pub(crate) extern "C" fn zig__renderDiff( received_len: usize, global_this: &JSGlobalObject, ) { + #[cfg(bun_standalone)] + { + let _ = (expected_ptr, expected_len, received_ptr, received_len, global_this); + return; + } + #[cfg(not(bun_standalone))] + { // SAFETY: caller (BunAnalyzeTranspiledModule.cpp) passes a valid UTF-8 buffer // of length `expected_len` that outlives this call. let expected = unsafe { bun_core::ffi::slice(expected_ptr.cast::(), expected_len) }; @@ -112,4 +119,5 @@ pub(crate) extern "C" fn zig__renderDiff( ..Default::default() }; let _ = bun_core::output::error_writer().print(format_args!("DIFF:\n{}\n", formatter)); + } } diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index bcfd3b587de7..016c975caac0 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -504,6 +504,13 @@ impl Expect { value: *mut JSValue, any_constructor_type: *mut u8, ) -> bool { + #[cfg(bun_standalone)] + { + let _ = (instance_value, global_this, out_flags, value, any_constructor_type); + return false; + } + #[cfg(not(bun_standalone))] + { // SAFETY: `from_js` returns the live `m_ctx` payload owned by `instance_value`. let flags: Flags = 'flags: { unsafe { if let Some(instance) = ExpectCustomAsymmetricMatcher::from_js(instance_value) { @@ -544,6 +551,7 @@ impl Expect { } Err(_) => false, } + } } pub fn get_snapshot_name(&self, hint: &[u8]) -> Result, bun_core::Error> { @@ -2637,6 +2645,12 @@ impl ExpectCustomAsymmetricMatcher { global_this: *const JSGlobalObject, received: JSValue, ) -> bool { + #[cfg(bun_standalone)] + { + let _ = (this, this_value, global_this, received); + false + } + #[cfg(not(bun_standalone))] // SAFETY: called from C++ with valid pointers unsafe { Self::execute_impl(&*this, this_value, &*global_this, received) }.unwrap_or(false) } @@ -2789,6 +2803,14 @@ pub struct ExpectMatcherUtils {} impl ExpectMatcherUtils { #[unsafe(no_mangle)] pub extern "C" fn ExpectMatcherUtils_createSigleton(global_this: &JSGlobalObject) -> JSValue { + #[cfg(bun_standalone)] + { + let _ = global_this.throw(format_args!( + "bun:test is not available in standalone executables" + )); + JSValue::ZERO + } + #[cfg(not(bun_standalone))] ExpectMatcherUtils {}.to_js(global_this) } diff --git a/src/runtime/test_runner/jest.rs b/src/runtime/test_runner/jest.rs index a2a3f8ffdeb8..556a4e02a8aa 100644 --- a/src/runtime/test_runner/jest.rs +++ b/src/runtime/test_runner/jest.rs @@ -320,6 +320,14 @@ pub mod Jest { pub(crate) extern "C" fn Bun__Jest__createTestModuleObject( global_object: &JSGlobalObject, ) -> JSValue { + #[cfg(bun_standalone)] + { + let _ = global_object.throw(format_args!( + "bun:test is not available in standalone executables" + )); + JSValue::ZERO + } + #[cfg(not(bun_standalone))] match create_test_module(global_object) { Ok(v) => v, Err(_) => JSValue::ZERO, @@ -464,6 +472,15 @@ pub mod Jest { #[bun_jsc::host_fn] pub(crate) fn call(global_object: &JSGlobalObject, callframe: &CallFrame) -> JsResult { + #[cfg(bun_standalone)] + { + let _ = callframe; + return Err(global_object.throw(format_args!( + "bun:test is not available in standalone executables" + ))); + } + #[cfg(not(bun_standalone))] + { let vm = global_object.bun_vm(); if vm.is_in_preload || runner().is_none() { @@ -487,6 +504,7 @@ pub mod Jest { } jsc::from_js_host_call(global_object, || Bun__Jest__testModuleObject(global_object)) + } } #[bun_jsc::host_fn] diff --git a/src/runtime/webcore/BakeResponse.rs b/src/runtime/webcore/BakeResponse.rs index 88a02f514a1f..c1826ce76f9e 100644 --- a/src/runtime/webcore/BakeResponse.rs +++ b/src/runtime/webcore/BakeResponse.rs @@ -1,3 +1,5 @@ +#![cfg_attr(bun_standalone, allow(dead_code, unused_imports))] + use core::ffi::{c_int, c_void}; use crate::webcore::Response; @@ -65,8 +67,18 @@ bun_jsc::jsc_host_abi! { bake_ssr_has_jsx: *mut c_int, js_this: JSValue, ) -> *mut c_void { + #[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(); + } // SAFETY: caller (C++) guarantees `bake_ssr_has_jsx` is a valid, exclusive out-pointer for the call. + #[cfg(not(bun_standalone))] let bake_ssr_has_jsx = unsafe { &mut *bake_ssr_has_jsx }; + #[cfg(not(bun_standalone))] match constructor(global_object, call_frame, bake_ssr_has_jsx, js_this) { Ok(response) => response.cast::(), Err(JsError::Thrown) => core::ptr::null_mut(), @@ -116,7 +128,17 @@ bun_jsc::jsc_host_abi! { global_object: &JSGlobalObject, call_frame: &CallFrame, ) -> JSValue { - bun_jsc::to_js_host_call(global_object, || construct_redirect(global_object, call_frame)) + bun_jsc::to_js_host_call(global_object, || { + #[cfg(bun_standalone)] + { + let _ = call_frame; + return Err(global_object.throw(format_args!( + "Bake is not available in standalone executables" + ))); + } + #[cfg(not(bun_standalone))] + construct_redirect(global_object, call_frame) + }) } } @@ -152,7 +174,17 @@ bun_jsc::jsc_host_abi! { global_object: &JSGlobalObject, call_frame: &CallFrame, ) -> JSValue { - bun_jsc::to_js_host_call(global_object, || construct_render(global_object, call_frame)) + bun_jsc::to_js_host_call(global_object, || { + #[cfg(bun_standalone)] + { + let _ = call_frame; + return Err(global_object.throw(format_args!( + "Bake is not available in standalone executables" + ))); + } + #[cfg(not(bun_standalone))] + construct_render(global_object, call_frame) + }) } } diff --git a/src/standalone_graph/Cargo.toml b/src/standalone_graph/Cargo.toml index c513f9133eff..03c1b05799c1 100644 --- a/src/standalone_graph/Cargo.toml +++ b/src/standalone_graph/Cargo.toml @@ -30,7 +30,6 @@ bun_http.workspace = true bun_parsers.workspace = true bun_libarchive.workspace = true bun_io.workspace = true -bun_js_parser.workspace = true bun_ast.workspace = true bun_options_types.workspace = true bun_paths.workspace = true diff --git a/src/standalone_graph/StandaloneModuleGraph.rs b/src/standalone_graph/StandaloneModuleGraph.rs index 244153038a43..63fe9eef1c9a 100644 --- a/src/standalone_graph/StandaloneModuleGraph.rs +++ b/src/standalone_graph/StandaloneModuleGraph.rs @@ -2,6 +2,7 @@ //! But this incurred a fixed 350ms overhead on every build, which is unacceptable //! so we give up on codesigning support on macOS for now until we can find a better solution +#[cfg(not(bun_standalone))] use bun_collections::VecExt; use core::mem::size_of; use core::ptr::NonNull; @@ -9,19 +10,25 @@ use std::io::Write as _; use std::sync::Arc; use bun_ast::Loader; +#[cfg_attr(bun_standalone, allow(unused_imports))] use bun_bundler::options::{self, OutputFile}; use bun_collections::StringArrayHashMap; +#[cfg_attr(bun_standalone, allow(unused_imports))] use bun_core::{Environment, Error as BunError, Output, err}; use bun_core::{String as BunString, StringPointer, ZStr}; +#[cfg(not(bun_standalone))] use bun_exe_format::{elf as bun_elf, macho as bun_macho, pe as bun_pe}; use bun_options_types::bundle_enums::{Format, WindowsOptions}; -#[cfg(not(windows))] +#[cfg(all(not(windows), not(bun_standalone)))] use bun_paths::SEP_STR; +#[cfg(not(bun_standalone))] use bun_paths::fs as bun_fs; +#[cfg_attr(bun_standalone, allow(unused_imports))] use bun_paths::{self as path, PathBuffer, strings}; -#[cfg(windows)] +#[cfg(all(windows, not(bun_standalone)))] use bun_paths::{OSPathBuffer, WPathBuffer}; use bun_sourcemap as SourceMap; +#[cfg_attr(bun_standalone, allow(unused_imports))] use bun_sys::{self as Syscall, Fd, FdExt as _, Stat}; // `bun_webcore::Blob` lives in a higher tier and `cached_blob` is only ever @@ -702,6 +709,7 @@ unsafe fn slice_to_z(base: *const u8, len: usize, ptr: StringPointer) -> &'stati unsafe { ZStr::from_raw(base.add(off), n) } } +#[cfg(not(bun_standalone))] pub(crate) fn to_bytes( prefix: &[u8], output_files: &[OutputFile], @@ -1011,6 +1019,7 @@ pub(crate) fn to_bytes( Ok(output) } +#[cfg(not(bun_standalone))] pub(crate) type InjectOptions = WindowsOptions; pub enum CompileResult { @@ -1059,6 +1068,7 @@ impl CompileResult { } } +#[cfg(not(bun_standalone))] pub(crate) fn inject( bytes: &[u8], self_exe: &ZStr, @@ -1507,13 +1517,15 @@ pub(crate) fn inject( } } +#[cfg(not(bun_standalone))] use bun_core::Environment::OperatingSystem as CompileTargetOs; -pub use bun_options_types::compile_target::CompileTarget; +pub use bun_options_types::compile_target::{CompileRuntime, CompileTarget}; /// Moved up from `bun_options_types` (T3) so it can name /// `bun_http::AsyncHTTP` directly /// instead of routing through `extern "Rust"` shims; the only callers are the /// two `download*` fns below in this crate. +#[cfg(not(bun_standalone))] pub(crate) fn download_to_path( target: &CompileTarget, env: &mut bun_dotenv::Loader<'_>, @@ -1641,10 +1653,15 @@ pub(crate) fn download_to_path( let mut did_retry = false; loop { - let src_name: &ZStr = if target.os == CompileTargetOs::Windows { - bun_core::zstr!("bun.exe") - } else { - bun_core::zstr!("bun") + let src_name: &ZStr = match (target.runtime, target.os) { + (CompileRuntime::Standalone, CompileTargetOs::Windows) => { + bun_core::zstr!("bun-standalone.exe") + } + (CompileRuntime::Standalone, _) => bun_core::zstr!("bun-standalone"), + (CompileRuntime::Full, CompileTargetOs::Windows) => { + bun_core::zstr!("bun.exe") + } + (CompileRuntime::Full, _) => bun_core::zstr!("bun"), }; let mv = bun_sys::move_file_z(tmpdir.fd(), src_name, Fd::INVALID, dest_z); if mv.is_err() { @@ -1673,6 +1690,26 @@ pub(crate) fn download_to_path( Ok(()) } +#[cfg(bun_standalone)] +pub fn to_executable( + _target: &CompileTarget, + _output_files: &[OutputFile], + _root_dir: Fd, + _module_prefix: &[u8], + _outfile: &[u8], + _env: &mut bun_dotenv::Loader, + _output_format: Format, + _windows_options: &WindowsOptions, + _compile_exec_argv: &[u8], + _self_exe_path: Option<&[u8]>, + _flags: Flags, +) -> Result { + Ok(CompileResult::fail_fmt(format_args!( + "bun build --compile is not available in standalone executables" + ))) +} + +#[cfg(not(bun_standalone))] pub fn to_executable( target: &CompileTarget, output_files: &[OutputFile], @@ -2211,6 +2248,7 @@ pub struct SerializedSourceMapLoaded { pub decompressed_files: Box<[Option>]>, } +#[cfg(not(bun_standalone))] pub(crate) fn serialize_json_source_map_for_standalone( header_list: &mut Vec, string_payload: &mut Vec, From 535eeeaa961d634dc133fda82e74fa7e0fd82507 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 15 Jun 2026 06:19:11 +0000 Subject: [PATCH 04/12] cli: cfg-gate toolkit subcommand modules under bun_standalone 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. --- src/bun_bin/lib.rs | 5 +- src/runtime/cli/mod.rs | 86 +++++++++++++++++++++++++--- src/runtime/cli/shared.rs | 91 ++++++++++++++++++++++++++++++ src/runtime/cli/upgrade_command.rs | 84 ++++----------------------- src/runtime/ffi/ffi_body.rs | 2 +- src/runtime/jsc_hooks.rs | 2 +- src/runtime/lib.rs | 7 ++- test/cli/standalone-binary.test.ts | 37 ++++++++++++ 8 files changed, 226 insertions(+), 88 deletions(-) create mode 100644 src/runtime/cli/shared.rs diff --git a/src/bun_bin/lib.rs b/src/bun_bin/lib.rs index bb4419251553..5e340b6acfb2 100644 --- a/src/bun_bin/lib.rs +++ b/src/bun_bin/lib.rs @@ -209,10 +209,7 @@ pub unsafe extern "C" fn main(argc: c_int, argv: *const *const c_char) -> c_int // SAFETY: BUN__GITHUB_BASELINE_URL is a NUL-terminated static; the C // side only reads it to print the suggested download URL. unsafe { - bun_warn_avx_missing( - bun_runtime::cli::upgrade_command::UpgradeCommand::BUN__GITHUB_BASELINE_URL - .as_ptr(), - ); + bun_warn_avx_missing(bun_runtime::cli::shared::BUN__GITHUB_BASELINE_URL.as_ptr()); } } diff --git a/src/runtime/cli/mod.rs b/src/runtime/cli/mod.rs index 9d9f1d72ba14..703bf7364d12 100644 --- a/src/runtime/cli/mod.rs +++ b/src/runtime/cli/mod.rs @@ -4,13 +4,14 @@ //! against lower-tier crates. `Command::start()` (full dispatch) and //! per-command exec bodies live in the sibling `*_command.rs` modules. -// Under `bun_standalone` the toolkit dispatch arms are compiled out (see the -// `cfg(bun_standalone)` match in `Command::start`), which orphans most -// `*_command` module contents. The modules stay declared so the few items the -// runtime still reaches (`upgrade_command::FileSystemTmpdirExt`, -// `upgrade_command::Bun__githubURL`, codegen js2native thunks) keep linking; -// the rest is dropped by `--gc-sections`. The non-standalone build still -// enforces `dead_code = "deny"`. +// Under `bun_standalone` the toolkit `*_command` modules are `cfg`-gated out +// entirely (see the `#[cfg(not(bun_standalone))]` blocks below), so their +// calls into `bun_install` / `bun_css` / `bundle_v2` are not compiled and +// those crates can dead-strip from the link. The few runtime-reachable items +// that originally lived in toolkit modules (`FileSystemTmpdirExt`, +// `Bun__githubURL`, release-name consts) now live in `cli::shared`; a tiny +// `upgrade_command` stub is provided for the codegen js2native thunk. The +// non-standalone build still enforces `dead_code = "deny"`. #![cfg_attr(bun_standalone, allow(dead_code, unused_macros, unused_imports))] use core::cell::Cell; @@ -219,16 +220,22 @@ pub(crate) mod ci_info_generated { } } +// ─── always-compiled shared items (runtime-reachable) ──────────────────────── +pub mod shared; + +#[cfg(not(bun_standalone))] #[path = "add_completions.rs"] pub mod add_completions; #[path = "colon_list_type.rs"] pub mod colon_list_type; #[path = "discord_command.rs"] pub mod discord_command; +#[cfg(not(bun_standalone))] #[path = "list-of-yarn-commands.rs"] pub mod list_of_yarn_commands; #[path = "shell_completions.rs"] pub mod shell_completions; +#[cfg(not(bun_standalone))] #[path = "which_npm_client.rs"] pub mod which_npm_client; @@ -269,10 +276,13 @@ pub mod open { // `create_context_data`), so its help/print-only paths are handled inline in // `Command::start()` below. `install_completions_command.rs` is fully wired // via `exec_install_completions` (its `exec()` takes no Context). +#[cfg(not(bun_standalone))] #[path = "init_command.rs"] pub mod init_command; +#[cfg(not(bun_standalone))] #[path = "install_completions_command.rs"] pub mod install_completions_command; +#[cfg(not(bun_standalone))] #[path = "package_manager_command.rs"] pub mod package_manager_command; @@ -334,65 +344,119 @@ pub mod run_command; // Heavy bodies inside re-gate on whatever // lower-tier crate surface they still need; the dispatch arm just calls // `Command::exec(ctx)`. +#[cfg(not(bun_standalone))] #[path = "build_command.rs"] pub mod build_command; +#[cfg(not(bun_standalone))] #[path = "bunx_command.rs"] pub mod bunx_command; +#[cfg(not(bun_standalone))] #[path = "create_command.rs"] pub mod create_command; #[path = "exec_command.rs"] pub mod exec_command; +#[cfg(not(bun_standalone))] #[path = "fuzzilli_command.rs"] pub mod fuzzilli_command; +#[cfg(not(bun_standalone))] #[path = "install_command.rs"] pub mod install_command; #[path = "repl_command.rs"] pub mod repl_command; +#[cfg(not(bun_standalone))] #[path = "upgrade_command.rs"] pub mod upgrade_command; +/// `cfg(bun_standalone)` stub: only what `generated_js2native.rs` references. +#[cfg(bun_standalone)] +pub mod upgrade_command { + pub mod upgrade_js_bindings { + use bun_jsc::{JSGlobalObject, JSValue}; + pub fn generate(global: &JSGlobalObject) -> JSValue { + let _ = global.throw(format_args!( + "bun upgrade is not available in standalone executables" + )); + JSValue::ZERO + } + } +} // MOVE_UP: `--analyze` branch + `Cli.log_` access of // `bun_install::update_package_json_and_install{,_catch_error}` — see file header. +#[cfg(not(bun_standalone))] #[path = "add_command.rs"] pub mod add_command; +#[cfg(not(bun_standalone))] #[path = "audit_command.rs"] pub mod audit_command; #[path = "filter_arg.rs"] pub mod filter_arg; #[path = "filter_run.rs"] pub mod filter_run; +#[cfg(not(bun_standalone))] #[path = "link_command.rs"] pub mod link_command; +#[cfg(not(bun_standalone))] #[path = "outdated_command.rs"] pub mod outdated_command; +#[cfg(not(bun_standalone))] #[path = "pack_command.rs"] pub mod pack_command; +/// `cfg(bun_standalone)` stub: only what `generated_js2native.rs` references. +#[cfg(bun_standalone)] +pub mod pack_command { + pub mod bindings { + use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; + pub(crate) fn js_read_tarball( + global: &JSGlobalObject, + _call_frame: &CallFrame, + ) -> JsResult { + Err(global.throw(format_args!( + "bun pm pack is not available in standalone executables" + ))) + } + } +} +#[cfg(not(bun_standalone))] #[path = "patch_command.rs"] pub mod patch_command; +#[cfg(not(bun_standalone))] #[path = "patch_commit_command.rs"] pub mod patch_commit_command; +#[cfg(not(bun_standalone))] #[path = "pm_pkg_command.rs"] pub mod pm_pkg_command; +#[cfg(not(bun_standalone))] #[path = "pm_trusted_command.rs"] pub mod pm_trusted_command; +#[cfg(not(bun_standalone))] pub mod pm_update_package_json; +#[cfg(not(bun_standalone))] #[path = "pm_version_command.rs"] pub mod pm_version_command; +#[cfg(not(bun_standalone))] #[path = "pm_view_command.rs"] pub mod pm_view_command; +#[cfg(not(bun_standalone))] #[path = "pm_why_command.rs"] pub mod pm_why_command; +#[cfg(not(bun_standalone))] #[path = "publish_command.rs"] pub mod publish_command; +#[cfg(not(bun_standalone))] #[path = "remove_command.rs"] pub mod remove_command; +#[cfg(not(bun_standalone))] #[path = "scan_command.rs"] pub mod scan_command; +#[cfg(not(bun_standalone))] #[path = "unlink_command.rs"] pub mod unlink_command; +#[cfg(not(bun_standalone))] #[path = "update_command.rs"] pub mod update_command; +#[cfg(not(bun_standalone))] #[path = "update_interactive_command.rs"] pub mod update_interactive_command; +#[cfg(not(bun_standalone))] #[path = "why_command.rs"] pub mod why_command; pub use filter_run as FilterRun; @@ -2242,21 +2306,27 @@ Execute a shell script directly from Bun. pretty!("Usage: bun getcompletes"); Output::flush(); } + #[cfg(not(bun_standalone))] Tag::PatchCommand => { pm_print_help(PmSubcommand::Patch); } + #[cfg(not(bun_standalone))] Tag::PatchCommitCommand => { pm_print_help(PmSubcommand::PatchCommit); } + #[cfg(not(bun_standalone))] Tag::OutdatedCommand => { pm_print_help(PmSubcommand::Outdated); } + #[cfg(not(bun_standalone))] Tag::UpdateInteractiveCommand => { pm_print_help(PmSubcommand::Update); } + #[cfg(not(bun_standalone))] Tag::PublishCommand => { pm_print_help(PmSubcommand::Publish); } + #[cfg(not(bun_standalone))] Tag::AuditCommand => { pm_print_help(PmSubcommand::Audit); } @@ -2361,9 +2431,11 @@ Learn more about these at https://bun.com/docs/cli/pm } } + #[cfg(not(bun_standalone))] use bun_install::package_manager_real::Subcommand as PmSubcommand; /// Forward to `bun_install::PackageManager::CommandLineArguments::print_help`. + #[cfg(not(bun_standalone))] #[inline] fn pm_print_help(subcommand: PmSubcommand) { bun_install::package_manager_real::CommandLineArguments::print_help(subcommand); diff --git a/src/runtime/cli/shared.rs b/src/runtime/cli/shared.rs new file mode 100644 index 000000000000..dad259404218 --- /dev/null +++ b/src/runtime/cli/shared.rs @@ -0,0 +1,91 @@ +//! Items shared between toolkit subcommands and the always-compiled runtime. +//! +//! These were originally defined in `upgrade_command.rs` but are referenced +//! from core runtime code (`jsc_hooks`, `ffi_body`, `bun_bin`, C++ +//! `BunProcess.cpp`), so they must compile under `cfg(bun_standalone)` where +//! `upgrade_command` is reduced to a stub. + +use core::ffi::c_char; + +use bun_core::Global::SyncCStr; +use bun_core::{Environment, Global, ZStr}; +use bun_resolver::fs; +use bun_sys as sys; + +// `bun_resolver::fs::FileSystem` does not yet expose `tmpdir()`; the full impl +// lives in the un-exported `fs_full` module. Shim it locally — open +// `RealFS::tmpdir_path()` as a `sys::Dir`, mirroring `RealFS::open_tmp_dir`. +pub(crate) trait FileSystemTmpdirExt { + fn tmpdir(&mut self) -> Result; +} +impl FileSystemTmpdirExt for fs::FileSystem { + fn tmpdir(&mut self) -> Result { + sys::Dir::open(fs::RealFS::tmpdir_path()).map_err(Into::into) + } +} + +/// Release-artifact name constants. These back `process.release.sourceUrl` +/// (via [`Bun__githubURL`]) and the AVX-missing baseline-download hint, so +/// they must compile in every build flavor. +pub mod release { + use super::*; + + // "windows" not "win32"; Android folds to "linux" (`SUFFIX_ABI` below adds + // "-android", matching `bun-linux-aarch64-android.zip` on the release page). + pub const PLATFORM_LABEL: &str = bun_core::env::OS_NAME_NPM; + + pub const ARCH_LABEL: &str = if cfg!(target_arch = "aarch64") { + "aarch64" + } else { + "x64" + }; + pub const TRIPLET: &str = const_format::concatcp!(PLATFORM_LABEL, "-", ARCH_LABEL); + pub(super) const SUFFIX_ABI: &str = if Environment::IS_MUSL { + "-musl" + } else if Environment::IS_ANDROID { + "-android" + } else { + "" + }; + pub(super) const SUFFIX_CPU: &str = if Environment::BASELINE { + "-baseline" + } else { + "" + }; + pub(super) const SUFFIX: &str = const_format::concatcp!(SUFFIX_ABI, SUFFIX_CPU); + pub const FOLDER_NAME: &str = const_format::concatcp!("bun-", TRIPLET, SUFFIX); + pub const BASELINE_FOLDER_NAME: &str = const_format::concatcp!("bun-", TRIPLET, "-baseline"); + pub const ZIP_FILENAME: &str = const_format::concatcp!(FOLDER_NAME, ".zip"); + pub const BASELINE_ZIP_FILENAME: &str = const_format::concatcp!(BASELINE_FOLDER_NAME, ".zip"); + + pub const PROFILE_FOLDER_NAME: &str = const_format::concatcp!("bun-", TRIPLET, SUFFIX, "-profile"); + pub const PROFILE_ZIP_FILENAME: &str = const_format::concatcp!(PROFILE_FOLDER_NAME, ".zip"); +} + +pub const BUN__GITHUB_BASELINE_URL: &ZStr = { + const S: &str = const_format::concatcp!( + "https://github.com/oven-sh/bun/releases/download/bun-v", + Global::package_json_version, + "/", + release::BASELINE_ZIP_FILENAME, + "\0" + ); + ZStr::from_static(S.as_bytes()) +}; + +// Exported C symbol — null-terminated. `*const c_char` is `!Sync`, so wrap in +// the `#[repr(transparent)]` `SyncCStr` newtype (same pattern as +// `Bun__userAgent` in bun_core::Global) so the C++ side still sees a single +// `const char*`-sized symbol. +#[unsafe(no_mangle)] +pub(crate) static Bun__githubURL: SyncCStr = SyncCStr( + const_format::concatcp!( + "https://github.com/oven-sh/bun/releases/download/bun-v", + Global::package_json_version, + "/", + release::ZIP_FILENAME, + "\0" + ) + .as_ptr() + .cast::(), +); diff --git a/src/runtime/cli/upgrade_command.rs b/src/runtime/cli/upgrade_command.rs index 5c399eda80c2..18bc66cd2700 100644 --- a/src/runtime/cli/upgrade_command.rs +++ b/src/runtime/cli/upgrade_command.rs @@ -4,7 +4,6 @@ use core::ptr::NonNull; use std::io::Write as _; use bun_alloc::Arena as Bump; -use bun_core::Global::SyncCStr; use bun_core::MutableString; use bun_core::{self, Environment, Global, Output, Progress, fmt as bun_fmt}; use bun_core::{ZStr, strings}; @@ -41,18 +40,7 @@ fn spawn_windows_options() -> crate::api::bun::process::WindowsOptions { } } -// `bun_resolver::fs::FileSystem` (the inline canonical type surface -// in `resolver/lib.rs`) does not yet expose `tmpdir()`; the full impl lives in -// the un-exported `fs_full` module. Shim it locally — open -// `RealFS::tmpdir_path()` as a `sys::Dir`, mirroring `RealFS::open_tmp_dir`. -pub(crate) trait FileSystemTmpdirExt { - fn tmpdir(&mut self) -> Result; -} -impl FileSystemTmpdirExt for fs::FileSystem { - fn tmpdir(&mut self) -> Result { - sys::Dir::open(fs::RealFS::tmpdir_path()).map_err(Into::into) - } -} +pub(crate) use crate::cli::shared::FileSystemTmpdirExt; // `bun.argv` is an `Argv` newtype (not `&[&[u8]]`), so // `strings::contains_any` can't take it directly. Local helper that scans the @@ -93,56 +81,24 @@ impl Version { Some(self.tag[b"bun-v".len()..].to_vec()) } - // "windows" not "win32"; Android folds to "linux" (`SUFFIX_ABI` below adds - // "-android", matching `bun-linux-aarch64-android.zip` on the release page). - pub const PLATFORM_LABEL: &'static str = bun_core::env::OS_NAME_NPM; - - pub const ARCH_LABEL: &'static str = if cfg!(target_arch = "aarch64") { - "aarch64" - } else { - "x64" - }; - pub const TRIPLET: &'static str = - const_format::concatcp!(Version::PLATFORM_LABEL, "-", Version::ARCH_LABEL); - const SUFFIX_ABI: &'static str = if Environment::IS_MUSL { - "-musl" - } else if Environment::IS_ANDROID { - "-android" - } else { - "" - }; - const SUFFIX_CPU: &'static str = if Environment::BASELINE { - "-baseline" - } else { - "" - }; - const SUFFIX: &'static str = const_format::concatcp!(Version::SUFFIX_ABI, Version::SUFFIX_CPU); - pub const FOLDER_NAME: &'static str = - const_format::concatcp!("bun-", Version::TRIPLET, Version::SUFFIX); + pub const PLATFORM_LABEL: &'static str = crate::cli::shared::release::PLATFORM_LABEL; + pub const ARCH_LABEL: &'static str = crate::cli::shared::release::ARCH_LABEL; + pub const TRIPLET: &'static str = crate::cli::shared::release::TRIPLET; + pub const FOLDER_NAME: &'static str = crate::cli::shared::release::FOLDER_NAME; pub const BASELINE_FOLDER_NAME: &'static str = - const_format::concatcp!("bun-", Version::TRIPLET, "-baseline"); - pub const ZIP_FILENAME: &'static str = const_format::concatcp!(Version::FOLDER_NAME, ".zip"); + crate::cli::shared::release::BASELINE_FOLDER_NAME; + pub const ZIP_FILENAME: &'static str = crate::cli::shared::release::ZIP_FILENAME; pub const BASELINE_ZIP_FILENAME: &'static str = - const_format::concatcp!(Version::BASELINE_FOLDER_NAME, ".zip"); - + crate::cli::shared::release::BASELINE_ZIP_FILENAME; pub const PROFILE_FOLDER_NAME: &'static str = - const_format::concatcp!("bun-", Version::TRIPLET, Version::SUFFIX, "-profile"); + crate::cli::shared::release::PROFILE_FOLDER_NAME; pub const PROFILE_ZIP_FILENAME: &'static str = - const_format::concatcp!(Version::PROFILE_FOLDER_NAME, ".zip"); + crate::cli::shared::release::PROFILE_ZIP_FILENAME; const CURRENT_VERSION: &'static str = const_format::concatcp!("bun-v", Global::package_json_version); - pub const BUN__GITHUB_BASELINE_URL: &'static ZStr = { - const S: &str = const_format::concatcp!( - "https://github.com/oven-sh/bun/releases/download/bun-v", - Global::package_json_version, - "/", - Version::BASELINE_ZIP_FILENAME, - "\0" - ); - ZStr::from_static(S.as_bytes()) - }; + pub const BUN__GITHUB_BASELINE_URL: &'static ZStr = crate::cli::shared::BUN__GITHUB_BASELINE_URL; pub fn is_current(&self) -> bool { &*self.tag == Self::CURRENT_VERSION.as_bytes() @@ -153,24 +109,6 @@ impl Version { } } -// Exported C symbol — null-terminated -// Moved out of `impl Version` — Rust impl blocks cannot hold `static` items. -// `*const c_char` is `!Sync`, so wrap in the `#[repr(transparent)]` `SyncCStr` newtype -// (same pattern as `Bun__userAgent` in bun_core::Global) so the C++ side still sees a -// single `const char*`-sized symbol. -#[unsafe(no_mangle)] -pub(crate) static Bun__githubURL: SyncCStr = SyncCStr( - const_format::concatcp!( - "https://github.com/oven-sh/bun/releases/download/bun-v", - Global::package_json_version, - "/", - Version::ZIP_FILENAME, - "\0" - ) - .as_ptr() - .cast::(), -); - // ────────────────────────────────────────────────────────────────────────── pub struct UpgradeCommand; diff --git a/src/runtime/ffi/ffi_body.rs b/src/runtime/ffi/ffi_body.rs index 74e802416768..d8eed1572134 100644 --- a/src/runtime/ffi/ffi_body.rs +++ b/src/runtime/ffi/ffi_body.rs @@ -2601,7 +2601,7 @@ impl CompilerRT { // `bun_resolver::fs::FileSystem` (the inline canonical surface) doesn't // yet expose an inherent `tmpdir()`; reuse the crate-local // `FileSystemTmpdirExt` shim already in service for `jsc_hooks`. - use crate::cli::upgrade_command::FileSystemTmpdirExt as _; + use crate::cli::shared::FileSystemTmpdirExt as _; let Ok(tmpdir) = Fs::FileSystem::instance().tmpdir() else { return; }; diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index bf6ce433c67c..e42e3dbea0c6 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -48,7 +48,7 @@ use bun_resolver::fs as Fs; use bun_resolver::node_fallbacks; use bun_resolver::{GlobalCache, ResultUnion as ResolveResultUnion}; -use crate::cli::upgrade_command::FileSystemTmpdirExt as _; +use crate::cli::shared::FileSystemTmpdirExt as _; use crate::timer; use crate::webcore::blob::BlobExt as _; diff --git a/src/runtime/lib.rs b/src/runtime/lib.rs index dbc3a999e5b4..44e53c6d65c6 100644 --- a/src/runtime/lib.rs +++ b/src/runtime/lib.rs @@ -68,9 +68,12 @@ pub mod valkey_jsc; // so `*_command.rs` and `test/parallel/*.rs` files resolve their // `use crate::…` lines without per-file edits. pub use cli::{ - Cli, Command, add_completions, build_command, bunx_command, command, create_command, - filter_arg, filter_run, multi_run, package_manager_command, run_command, shell_completions, + Cli, Command, command, filter_arg, filter_run, multi_run, run_command, shell_completions, test_command, }; +#[cfg(not(bun_standalone))] +pub use cli::{ + add_completions, build_command, bunx_command, create_command, package_manager_command, +}; pub mod webview; diff --git a/test/cli/standalone-binary.test.ts b/test/cli/standalone-binary.test.ts index 3b0a307a271b..2c23c8c54aea 100644 --- a/test/cli/standalone-binary.test.ts +++ b/test/cli/standalone-binary.test.ts @@ -59,6 +59,43 @@ describe.skipIf(!standaloneExe || !existsSync(standaloneExe))("bun-standalone", expect(exitCode).toBe(0); }); + test("Bun.build / Bun.color throw with an actionable error", async () => { + for (const expr of [`Bun.build({entrypoints:["x.js"]})`, `Bun.color("red")`]) { + await using proc = Bun.spawn({ + cmd: [ + exe, + "-e", + `try { await ${expr}; process.exit(2) } catch (e) { console.error(e.message); process.exit(e instanceof TypeError ? 0 : 1) }`, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(normalizeBunSnapshot(stderr)).toContain("not available in standalone executables"); + expect(exitCode).toBe(0); + } + }); + + test("Bun.serve / fetch / runtime APIs work", async () => { + await using proc = Bun.spawn({ + cmd: [ + exe, + "-e", + `const s = Bun.serve({ port: 0, fetch: () => new Response("ok") }); + const r = await fetch(s.url); + console.log(await r.text(), s.port > 0); + s.stop();`, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(normalizeBunSnapshot(stdout)).toBe("ok true"); + expect(exitCode).toBe(0); + }); + test("STANDALONE_BUILD const is true", async () => { await using proc = Bun.spawn({ cmd: [exe, "-e", "process.stdout.write(String(process.isBun))"], From 5c34fe81299b50414b7ab1322bd084d950fe69f9 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 15 Jun 2026 06:22:42 +0000 Subject: [PATCH 05/12] core: include -standalone in version/revision/crash-reporter strings 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. --- src/bun_core/Global.rs | 58 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/src/bun_core/Global.rs b/src/bun_core/Global.rs index e2743eb5fe2b..456fb19f27ad 100644 --- a/src/bun_core/Global.rs +++ b/src/bun_core/Global.rs @@ -428,12 +428,21 @@ pub mod debug_flags { // Version strings // ────────────────────────────────────────────────────────────────────────── +/// Suffix distinguishing the reduced-footprint `bun-standalone` binary in +/// `--version`/`--revision` output, the unhandled-error footer, and the +/// crash-reporter version line. Empty for the full binary. +pub const STANDALONE_SUFFIX: &str = if cfg!(bun_standalone) { + "-standalone" +} else { + "" +}; + /// 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) }; /// `package_json_version` with a trailing `\n` baked in, so @@ -443,27 +452,43 @@ pub const package_json_version_nl: &str = concatcp!(package_json_version, "\n"); /// This is used for `bun` without any arguments, it `package_json_version` but with canary if it is a canary build. /// like "1.0.0-canary.12" pub const package_json_version_with_canary: &str = if cfg!(debug_assertions) { - concatcp!(version_string, "-debug") + concatcp!(version_string, STANDALONE_SUFFIX, "-debug") } else if env::IS_CANARY { - formatcp!("{}-canary.{}", version_string, env::CANARY_REVISION) + formatcp!( + "{}{}-canary.{}", + version_string, + STANDALONE_SUFFIX, + env::CANARY_REVISION + ) } else { - version_string + concatcp!(version_string, STANDALONE_SUFFIX) }; /// The version and a short hash in parenthesis. pub const package_json_version_with_sha: &str = if env::GIT_SHA.is_empty() { package_json_version } else if cfg!(debug_assertions) { - formatcp!("{} ({})", version_string, env::GIT_SHA_SHORT) + formatcp!( + "{}{} ({})", + version_string, + STANDALONE_SUFFIX, + env::GIT_SHA_SHORT + ) } else if env::IS_CANARY { formatcp!( - "{}-canary.{} ({})", + "{}{}-canary.{} ({})", version_string, + STANDALONE_SUFFIX, env::CANARY_REVISION, env::GIT_SHA_SHORT ) } else { - formatcp!("{} ({})", version_string, env::GIT_SHA_SHORT) + formatcp!( + "{}{} ({})", + version_string, + STANDALONE_SUFFIX, + env::GIT_SHA_SHORT + ) }; /// What is printed by `bun --revision` @@ -471,16 +496,27 @@ pub const package_json_version_with_sha: &str = if env::GIT_SHA.is_empty() { pub const package_json_version_with_revision: &str = if env::GIT_SHA.is_empty() { package_json_version } else if cfg!(debug_assertions) { - formatcp!("{}-debug+{}", version_string, env::GIT_SHA_SHORT) + formatcp!( + "{}{}-debug+{}", + version_string, + STANDALONE_SUFFIX, + env::GIT_SHA_SHORT + ) } else if env::IS_CANARY { formatcp!( - "{}-canary.{}+{}", + "{}{}-canary.{}+{}", version_string, + STANDALONE_SUFFIX, env::CANARY_REVISION, env::GIT_SHA_SHORT ) } else { - formatcp!("{}+{}", version_string, env::GIT_SHA_SHORT) + formatcp!( + "{}{}+{}", + version_string, + STANDALONE_SUFFIX, + env::GIT_SHA_SHORT + ) }; // Node-style platform string. Distinct from Environment.os.nameString() on From dd34b2825db9b0717bf643e5da15e2bd87b3a6d0 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 15 Jun 2026 06:32:47 +0000 Subject: [PATCH 06/12] docs/test: measured size table + assert -standalone in --revision --- docs/standalone-binary.md | 83 +++++++++++++++++------------- test/cli/standalone-binary.test.ts | 2 +- 2 files changed, 48 insertions(+), 37 deletions(-) diff --git a/docs/standalone-binary.md b/docs/standalone-binary.md index 01619e30eb60..9ddcc3f89884 100644 --- a/docs/standalone-binary.md +++ b/docs/standalone-binary.md @@ -70,39 +70,50 @@ artifacts are `bun-standalone--[-musl][-baseline].zip`. ## Size -Linux-x64 release, May 2026 linker map: - -| | MB | -|---|--:| -| stripped `bun` | 83.2 | -| bundler + css + install + test + bake + toolkit CLI | −7.1 | -| **`bun-standalone` (this change)** | **~76** | -| | | -| ICU data (`.rodata`) | 23.7 | -| JavaScriptCore `.text` | 22.9 | -| Bun C++ bindings + WebCore + BoringSSL + codecs | ~10 | -| runtime transpiler (parser/printer/ast/resolver) | 2.4 | - -The < 35 MB target requires shipping a reduced ICU data file (small-icu is -~5 MB instead of 24 MB) on top of this; that is a WebKit-prebuilt change -tracked separately. - -## Follow-up work - -This change lands the build infrastructure and the CLI-dispatch sever. The -remaining `#[no_mangle]` entry points that keep subsystem code alive are -mapped in `src/runtime/standalone_build.rs` and gated incrementally: - - - `Bun.build()` / `JSBundlerPlugin__*` → stub to throw, drops `BundleV2`. - - `Bun.color()` / `JS2Zig__css_internals_*` → stub, drops `bun_css`. - - `bun:test` module / `Expect*` codegen classes → needs a C++-side - `#if !BUN_STANDALONE` around `jest.classes.ts` codegen and - `matchAsymmetricMatcherAndGetFlags` in `bindings.cpp`. - - `bake` DevServer → cfg the `dev_server` field on `ServerInstance` and the - `AnyRoute::FrameworkRouter` variant. - - `bun_standalone_graph` read/write split → make `bun_bundler` / - `bun_libarchive` / `bun_http` optional behind a `write` feature so the - standalone binary only carries the graph reader. - - `--compile` target selection → add `standalone: bool` to `CompileTarget` - so cross-compile downloads `@oven/bun-standalone-` and same-host - builds don't short-circuit to `self_exe_path()`. +Linux-x64 release, non-LTO, measured on this branch: + +| | bytes | MB | +|---|--:|--:| +| stripped `bun` | 70,389,048 | 67.13 | +| stripped `bun-standalone` | 67,439,800 | 64.32 | +| **delta** | **−2,949,248** | **−2.81** | + +Per-crate VM size from `bloaty -d compileunits` (full → standalone): + +| crate | full MB | standalone MB | Δ | +|---|--:|--:|--:| +| `bun_runtime` | 6.45 | 5.35 | −1.10 | +| `bun_install` | 2.03 | 1.10 | −0.93 | +| `bun_bundler` | 1.61 | 1.43 | −0.18 | +| `bun_css` | 1.77 | 1.74 | −0.03 | +| `bun_css_jsc` | 0.10 | 0 | −0.10 | +| `bun_install_jsc` | 0.05 | 0.06 | +0.01 | + +The remaining `bun_css` / `bun_bundler` / `bun_install` weight is held alive +by **struct-field references from live runtime types**, which gc-sections +cannot sever even when the code paths are unreachable: + + - `bun_runtime::server::ServerInstance.dev_server: Option>` + → `bake::IncrementalGraph` → `BundleV2` → `Chunk.css`. + - `HTMLBundle` codegen class (`HTMLBundle__create`/`finalize` referenced + from `ZigGeneratedClasses.cpp`) owns `BundleV2Result`. + - `bun_bundler::Chunk` has `bun_css::BundlerStyleSheet` field types, and + `Chunk` is reachable from `Transpiler` (which the runtime keeps). + - `run_command.rs` workspace-script lookup and `shell_completions.rs` + reference `bun_install` directly. + +Recovering the remaining ~4 MB requires structural splits (own PRs): + + - cfg the `dev_server` field + `AnyRoute::FrameworkRouter` variant to a + ZST under `bun_standalone`; cfg `pub mod bake` entirely. + - Gate `HTMLBundle.classes.ts` codegen on a `BUN_STANDALONE` define so + `ZigGeneratedClasses.cpp` stops referencing it (only C++-side change + needed; the same `.a` can carry both via weak symbols, or split codegen). + - Split `bun_bundler` into `bun_transpiler` (Transpiler/options/defines/ + cache/analyze, always on) and `bun_bundler` (BundleV2/Chunk/linker, + gated). This is what severs the `bun_css` dependency. + - Route `run_command`'s `package.json` scripts lookup through + `bun_parsers::json` instead of `bun_install`. + +The < 35 MB target additionally requires shipping a reduced ICU data file +(small-icu ≈ 5 MB instead of 24 MB) — a WebKit-prebuilt change. diff --git a/test/cli/standalone-binary.test.ts b/test/cli/standalone-binary.test.ts index 2c23c8c54aea..8ed368b4ff86 100644 --- a/test/cli/standalone-binary.test.ts +++ b/test/cli/standalone-binary.test.ts @@ -43,7 +43,7 @@ describe.skipIf(!standaloneExe || !existsSync(standaloneExe))("bun-standalone", stderr: "pipe", }); const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stdout.trim()).toMatch(/^\d+\.\d+\.\d+/); + expect(stdout.trim()).toMatch(/^\d+\.\d+\.\d+-standalone\b/); expect(exitCode).toBe(0); }); From eb087fc39f7316308ef0903557bfdd6d717e628e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 06:35:24 +0000 Subject: [PATCH 07/12] [autofix.ci] apply automated fixes --- docs/standalone-binary.md | 88 +++++++++++----------- packages/bun-release/scripts/upload-npm.ts | 2 +- src/runtime/cli/Arguments.rs | 5 +- src/runtime/cli/shared.rs | 3 +- src/runtime/cli/upgrade_command.rs | 6 +- src/runtime/dispatch_js2native.rs | 10 ++- src/runtime/server/server_body.rs | 50 ++++++------ src/runtime/standalone_build.rs | 4 +- test/cli/standalone-binary.test.ts | 2 +- 9 files changed, 86 insertions(+), 84 deletions(-) diff --git a/docs/standalone-binary.md b/docs/standalone-binary.md index 9ddcc3f89884..d731b4b3ae16 100644 --- a/docs/standalone-binary.md +++ b/docs/standalone-binary.md @@ -2,7 +2,7 @@ `bun-standalone` is a second build of the `bun` executable with the toolkit subcommands compiled out. It exists so that `bun build --compile` can produce -smaller single-file executables: the embedded runtime only needs to *run* +smaller single-file executables: the embedded runtime only needs to _run_ JavaScript, not bundle it, install packages, or run a test suite. The binary name is `bun-standalone` (`bun-standalone.exe` on Windows). Debug @@ -14,11 +14,11 @@ and instrumented variants follow the same suffix scheme as the full binary The CLI dispatch for every subcommand other than the run path is replaced with an error message pointing at the full Bun install: - - `bun build` - - `bun test` - - `bun install` / `add` / `remove` / `update` / `link` / `unlink` / `pm` / - `outdated` / `publish` / `audit` / `why` / `info` / `patch` - - `bun init` / `create` / `x` / `upgrade` +- `bun build` +- `bun test` +- `bun install` / `add` / `remove` / `update` / `link` / `unlink` / `pm` / + `outdated` / `publish` / `audit` / `why` / `info` / `patch` +- `bun init` / `create` / `x` / `upgrade` `bun `, `bun run`, `bun --eval/--print`, `bun exec`, `bun repl`, and the `node`-shim entry remain. @@ -34,13 +34,13 @@ against. `cfg.standalone` (a boolean on the build `Config`) drives three things: - - `cargo build -p bun_bin --features standalone` with - `RUSTFLAGS="… --cfg=bun_standalone"` into a separate `--target-dir` - (`rust-target-standalone/`), so the full and standalone staticlibs can - coexist in one build directory. - - the linked executable is named `bun-standalone[-profile]` and the - stripped output `bun-standalone`. - - `bun_core::build_options::STANDALONE_BUILD` is `true`. +- `cargo build -p bun_bin --features standalone` with + `RUSTFLAGS="… --cfg=bun_standalone"` into a separate `--target-dir` + (`rust-target-standalone/`), so the full and standalone staticlibs can + coexist in one build directory. +- the linked executable is named `bun-standalone[-profile]` and the + stripped output `bun-standalone`. +- `bun_core::build_options::STANDALONE_BUILD` is `true`. Gating in Rust is on `cfg(bun_standalone)` (the global RUSTFLAG), not `cfg(feature = "standalone")`, so any crate can branch on it without @@ -72,48 +72,48 @@ artifacts are `bun-standalone--[-musl][-baseline].zip`. Linux-x64 release, non-LTO, measured on this branch: -| | bytes | MB | -|---|--:|--:| -| stripped `bun` | 70,389,048 | 67.13 | -| stripped `bun-standalone` | 67,439,800 | 64.32 | -| **delta** | **−2,949,248** | **−2.81** | +| | bytes | MB | +| ------------------------- | -------------: | --------: | +| stripped `bun` | 70,389,048 | 67.13 | +| stripped `bun-standalone` | 67,439,800 | 64.32 | +| **delta** | **−2,949,248** | **−2.81** | Per-crate VM size from `bloaty -d compileunits` (full → standalone): -| crate | full MB | standalone MB | Δ | -|---|--:|--:|--:| -| `bun_runtime` | 6.45 | 5.35 | −1.10 | -| `bun_install` | 2.03 | 1.10 | −0.93 | -| `bun_bundler` | 1.61 | 1.43 | −0.18 | -| `bun_css` | 1.77 | 1.74 | −0.03 | -| `bun_css_jsc` | 0.10 | 0 | −0.10 | -| `bun_install_jsc` | 0.05 | 0.06 | +0.01 | +| crate | full MB | standalone MB | Δ | +| ----------------- | ------: | ------------: | ----: | +| `bun_runtime` | 6.45 | 5.35 | −1.10 | +| `bun_install` | 2.03 | 1.10 | −0.93 | +| `bun_bundler` | 1.61 | 1.43 | −0.18 | +| `bun_css` | 1.77 | 1.74 | −0.03 | +| `bun_css_jsc` | 0.10 | 0 | −0.10 | +| `bun_install_jsc` | 0.05 | 0.06 | +0.01 | The remaining `bun_css` / `bun_bundler` / `bun_install` weight is held alive by **struct-field references from live runtime types**, which gc-sections cannot sever even when the code paths are unreachable: - - `bun_runtime::server::ServerInstance.dev_server: Option>` - → `bake::IncrementalGraph` → `BundleV2` → `Chunk.css`. - - `HTMLBundle` codegen class (`HTMLBundle__create`/`finalize` referenced - from `ZigGeneratedClasses.cpp`) owns `BundleV2Result`. - - `bun_bundler::Chunk` has `bun_css::BundlerStyleSheet` field types, and - `Chunk` is reachable from `Transpiler` (which the runtime keeps). - - `run_command.rs` workspace-script lookup and `shell_completions.rs` - reference `bun_install` directly. +- `bun_runtime::server::ServerInstance.dev_server: Option>` + → `bake::IncrementalGraph` → `BundleV2` → `Chunk.css`. +- `HTMLBundle` codegen class (`HTMLBundle__create`/`finalize` referenced + from `ZigGeneratedClasses.cpp`) owns `BundleV2Result`. +- `bun_bundler::Chunk` has `bun_css::BundlerStyleSheet` field types, and + `Chunk` is reachable from `Transpiler` (which the runtime keeps). +- `run_command.rs` workspace-script lookup and `shell_completions.rs` + reference `bun_install` directly. Recovering the remaining ~4 MB requires structural splits (own PRs): - - cfg the `dev_server` field + `AnyRoute::FrameworkRouter` variant to a - ZST under `bun_standalone`; cfg `pub mod bake` entirely. - - Gate `HTMLBundle.classes.ts` codegen on a `BUN_STANDALONE` define so - `ZigGeneratedClasses.cpp` stops referencing it (only C++-side change - needed; the same `.a` can carry both via weak symbols, or split codegen). - - Split `bun_bundler` into `bun_transpiler` (Transpiler/options/defines/ - cache/analyze, always on) and `bun_bundler` (BundleV2/Chunk/linker, - gated). This is what severs the `bun_css` dependency. - - Route `run_command`'s `package.json` scripts lookup through - `bun_parsers::json` instead of `bun_install`. +- cfg the `dev_server` field + `AnyRoute::FrameworkRouter` variant to a + ZST under `bun_standalone`; cfg `pub mod bake` entirely. +- Gate `HTMLBundle.classes.ts` codegen on a `BUN_STANDALONE` define so + `ZigGeneratedClasses.cpp` stops referencing it (only C++-side change + needed; the same `.a` can carry both via weak symbols, or split codegen). +- Split `bun_bundler` into `bun_transpiler` (Transpiler/options/defines/ + cache/analyze, always on) and `bun_bundler` (BundleV2/Chunk/linker, + gated). This is what severs the `bun_css` dependency. +- Route `run_command`'s `package.json` scripts lookup through + `bun_parsers::json` instead of `bun_install`. The < 35 MB target additionally requires shipping a reduced ICU data file (small-icu ≈ 5 MB instead of 24 MB) — a WebKit-prebuilt change. diff --git a/packages/bun-release/scripts/upload-npm.ts b/packages/bun-release/scripts/upload-npm.ts index cdad6273f654..8ee1700c71a6 100644 --- a/packages/bun-release/scripts/upload-npm.ts +++ b/packages/bun-release/scripts/upload-npm.ts @@ -13,9 +13,9 @@ import { chmod, copy, exists, join, write, writeJson } from "../src/fs"; import { getRelease, getSemver } from "../src/github"; import type { Platform } from "../src/platform"; import { platforms, standalonePlatforms } from "../src/platform"; +import { spawn } from "../src/spawn"; const allPlatforms = [...platforms, ...standalonePlatforms]; -import { spawn } from "../src/spawn"; const module = "bun"; const owner = "@oven"; diff --git a/src/runtime/cli/Arguments.rs b/src/runtime/cli/Arguments.rs index 6d9919b85d0a..a1a42fd8585e 100644 --- a/src/runtime/cli/Arguments.rs +++ b/src/runtime/cli/Arguments.rs @@ -2023,10 +2023,7 @@ fn parse_build_command_options( b"standalone" => bun_options_types::compile_target::CompileRuntime::Standalone, b"full" => bun_options_types::compile_target::CompileRuntime::Full, _ => { - Output::err_generic( - "--compile-runtime must be \"standalone\" or \"full\"", - (), - ); + Output::err_generic("--compile-runtime must be \"standalone\" or \"full\"", ()); Global::crash(); } }; diff --git a/src/runtime/cli/shared.rs b/src/runtime/cli/shared.rs index dad259404218..b2771b57b209 100644 --- a/src/runtime/cli/shared.rs +++ b/src/runtime/cli/shared.rs @@ -58,7 +58,8 @@ pub mod release { pub const ZIP_FILENAME: &str = const_format::concatcp!(FOLDER_NAME, ".zip"); pub const BASELINE_ZIP_FILENAME: &str = const_format::concatcp!(BASELINE_FOLDER_NAME, ".zip"); - pub const PROFILE_FOLDER_NAME: &str = const_format::concatcp!("bun-", TRIPLET, SUFFIX, "-profile"); + pub const PROFILE_FOLDER_NAME: &str = + const_format::concatcp!("bun-", TRIPLET, SUFFIX, "-profile"); pub const PROFILE_ZIP_FILENAME: &str = const_format::concatcp!(PROFILE_FOLDER_NAME, ".zip"); } diff --git a/src/runtime/cli/upgrade_command.rs b/src/runtime/cli/upgrade_command.rs index 18bc66cd2700..c9412643943a 100644 --- a/src/runtime/cli/upgrade_command.rs +++ b/src/runtime/cli/upgrade_command.rs @@ -90,15 +90,15 @@ impl Version { pub const ZIP_FILENAME: &'static str = crate::cli::shared::release::ZIP_FILENAME; pub const BASELINE_ZIP_FILENAME: &'static str = crate::cli::shared::release::BASELINE_ZIP_FILENAME; - pub const PROFILE_FOLDER_NAME: &'static str = - crate::cli::shared::release::PROFILE_FOLDER_NAME; + pub const PROFILE_FOLDER_NAME: &'static str = crate::cli::shared::release::PROFILE_FOLDER_NAME; pub const PROFILE_ZIP_FILENAME: &'static str = crate::cli::shared::release::PROFILE_ZIP_FILENAME; const CURRENT_VERSION: &'static str = const_format::concatcp!("bun-v", Global::package_json_version); - pub const BUN__GITHUB_BASELINE_URL: &'static ZStr = crate::cli::shared::BUN__GITHUB_BASELINE_URL; + pub const BUN__GITHUB_BASELINE_URL: &'static ZStr = + crate::cli::shared::BUN__GITHUB_BASELINE_URL; pub fn is_current(&self) -> bool { &*self.tag == Self::CURRENT_VERSION.as_bytes() diff --git a/src/runtime/dispatch_js2native.rs b/src/runtime/dispatch_js2native.rs index 5eeebd5fb625..7fcd26061792 100644 --- a/src/runtime/dispatch_js2native.rs +++ b/src/runtime/dispatch_js2native.rs @@ -96,8 +96,14 @@ mod css { )*}; } stub!( - _test, attr_test, minify_error_test_with_options, minify_test, - minify_test_with_options, prefix_test, prefix_test_with_options, test_with_options, + _test, + attr_test, + minify_error_test_with_options, + minify_test, + minify_test_with_options, + prefix_test, + prefix_test_with_options, + test_with_options, ); } pub use css::_test as css_jsc_css_internals__test; diff --git a/src/runtime/server/server_body.rs b/src/runtime/server/server_body.rs index 806daef8a6e3..9ee26a67b8a2 100644 --- a/src/runtime/server/server_body.rs +++ b/src/runtime/server/server_body.rs @@ -704,31 +704,31 @@ impl AnyRoute { } #[cfg(not(bun_standalone))] { - use bun_collections::zig_hash_map::MapEntry as StdEntry; - let entry = init_ctx - .dedupe_html_bundle_map - .entry(html_bundle.cast_const()); - // HashMap aborts on OOM (repo-wide abort-on-OOM policy). - return Ok(Some(match entry { - StdEntry::Vacant(v) => { - // The rc=1 `Route::init(..)` goes in the map and - // that same value is returned to the caller (the map slot is a - // non-owning borrow, freed by `dedupe_html_bundle_map.deinit` - // *without* deref). `RefPtr` has no `Drop`, so a bit-copy - // here keeps the net refcount at 1 — bumping for the map - // slot would leak +1 per first-seen HTMLBundle. - // SAFETY: `html_bundle` is the live `RefPtr` from the - // route map; `init` consumes its +1 ref into the new `Route`. - let route = html_bundle::Route::init(html_bundle); - // SAFETY: `route.data` is the just-allocated NonNull (rc=1); - // wrap without bumping so the map slot stays non-owning - // (`RefPtr` has no `Drop`; the map slot is a non-owning bit-copy). - let borrowed = unsafe { RefPtr::from_raw(route.as_ptr()) }; - v.insert(borrowed); - AnyRoute::Html(route) - } - StdEntry::Occupied(o) => AnyRoute::Html(o.get().dupe_ref()), - })); + use bun_collections::zig_hash_map::MapEntry as StdEntry; + let entry = init_ctx + .dedupe_html_bundle_map + .entry(html_bundle.cast_const()); + // HashMap aborts on OOM (repo-wide abort-on-OOM policy). + return Ok(Some(match entry { + StdEntry::Vacant(v) => { + // The rc=1 `Route::init(..)` goes in the map and + // that same value is returned to the caller (the map slot is a + // non-owning borrow, freed by `dedupe_html_bundle_map.deinit` + // *without* deref). `RefPtr` has no `Drop`, so a bit-copy + // here keeps the net refcount at 1 — bumping for the map + // slot would leak +1 per first-seen HTMLBundle. + // SAFETY: `html_bundle` is the live `RefPtr` from the + // route map; `init` consumes its +1 ref into the new `Route`. + let route = html_bundle::Route::init(html_bundle); + // SAFETY: `route.data` is the just-allocated NonNull (rc=1); + // wrap without bumping so the map slot stays non-owning + // (`RefPtr` has no `Drop`; the map slot is a non-owning bit-copy). + let borrowed = unsafe { RefPtr::from_raw(route.as_ptr()) }; + v.insert(borrowed); + AnyRoute::Html(route) + } + StdEntry::Occupied(o) => AnyRoute::Html(o.get().dupe_ref()), + })); } } diff --git a/src/runtime/standalone_build.rs b/src/runtime/standalone_build.rs index a90d971afdb4..4c1ab0d0a05c 100644 --- a/src/runtime/standalone_build.rs +++ b/src/runtime/standalone_build.rs @@ -35,9 +35,7 @@ pub fn unavailable_command(name: &[u8]) -> ! { bun_core::pretty_errorln!( "This is a standalone executable built with bun build --compile. It contains the", ); - bun_core::pretty_errorln!( - "Bun runtime but not the bundler, package manager, or test runner.", - ); + bun_core::pretty_errorln!("Bun runtime but not the bundler, package manager, or test runner.",); bun_core::pretty_errorln!(""); bun_core::pretty_errorln!( "To use bun {}, install Bun: https://bun.com/get", diff --git a/test/cli/standalone-binary.test.ts b/test/cli/standalone-binary.test.ts index 8ed368b4ff86..b611d78f26fb 100644 --- a/test/cli/standalone-binary.test.ts +++ b/test/cli/standalone-binary.test.ts @@ -11,8 +11,8 @@ // gate; this file is the behavioural one. import { describe, expect, test } from "bun:test"; -import { existsSync } from "node:fs"; import { bunEnv, normalizeBunSnapshot } from "harness"; +import { existsSync } from "node:fs"; const standaloneExe = process.env.BUN_STANDALONE_EXE; From cb8c88c49b62e215a8eff9d6b66f364f0b519339 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 15 Jun 2026 07:08:21 +0000 Subject: [PATCH 08/12] runtime: structural cfg-gating of bake/bundle_v2/install for bun-standalone 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> 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). --- src/jsc/AsyncModule.rs | 16 ++ src/jsc/VirtualMachine.rs | 2 + src/jsc/hot_reloader.rs | 3 + src/runtime/allocators/mod.rs | 1 + src/runtime/api.rs | 2 + src/runtime/api/JSBundler.rs | 8 + src/runtime/api/js_bundle_completion_task.rs | 1 + src/runtime/bake_standalone_stub.rs | 203 +++++++++++++++++++ src/runtime/cli/mod.rs | 8 + src/runtime/cli/run_command.rs | 67 +++++- src/runtime/dispatch.rs | 7 + src/runtime/dispatch_js2native.rs | 73 +++++++ src/runtime/jsc_hooks.rs | 25 ++- src/runtime/lib.rs | 8 +- src/runtime/server/HTMLBundle.rs | 51 ++++- src/runtime/server/ServerConfig.rs | 33 ++- src/runtime/server/mod.rs | 28 ++- src/runtime/server/server_body.rs | 38 ++++ 18 files changed, 549 insertions(+), 25 deletions(-) create mode 100644 src/runtime/bake_standalone_stub.rs diff --git a/src/jsc/AsyncModule.rs b/src/jsc/AsyncModule.rs index c2741524f6a5..095ef3191a22 100644 --- a/src/jsc/AsyncModule.rs +++ b/src/jsc/AsyncModule.rs @@ -1,10 +1,14 @@ +#![cfg_attr(bun_standalone, allow(dead_code, unused_imports))] + use core::ffi::c_void; use core::sync::atomic::AtomicU32; use bun_alloc::Arena as ArenaAllocator; use bun_bundler::transpiler::ParseResult; use bun_core::{OwnedString, String as BunString, ZigString}; +#[cfg(not(bun_standalone))] use bun_install::dependency::Dependency; +#[cfg(not(bun_standalone))] use bun_install::{DependencyID, Resolution}; use bun_io::KeepAlive; use bun_options_types::LoaderExt as _; @@ -68,6 +72,7 @@ pub struct AsyncModule { pub type Id = u32; +#[cfg(not(bun_standalone))] pub(crate) struct PackageDownloadError<'a> { pub name: &'a [u8], pub resolution: Resolution, @@ -75,6 +80,7 @@ pub(crate) struct PackageDownloadError<'a> { pub url: &'a [u8], } +#[cfg(not(bun_standalone))] pub(crate) struct PackageResolveError<'a> { pub name: &'a [u8], pub err: bun_core::Error, @@ -251,21 +257,29 @@ unsafe extern "C" { ); } +#[cfg(not(bun_standalone))] use core::sync::atomic::Ordering; +#[cfg(not(bun_standalone))] use std::io::Write as _; +#[cfg(not(bun_standalone))] use bun_core::strings; +#[cfg(not(bun_standalone))] use bun_install::package_manager::run_tasks; +#[cfg(not(bun_standalone))] use bun_install::{self as install, LogLevel, PackageID}; +#[cfg(not(bun_standalone))] use crate::event_loop::{AnyTask, ConcurrentTaskItem, Task}; /// `RunTasksCallbacks` impl for the auto-install module queue. `onResolve` / /// `onPackageManifestError` / `onPackageDownloadError` forward to the `Queue` /// methods, `progress_bar` selected via const generic to match the /// `enable_ansi_colors_stderr` branch. +#[cfg(not(bun_standalone))] struct QueueRunTasksCallbacks; +#[cfg(not(bun_standalone))] impl run_tasks::RunTasksCallbacks for QueueRunTasksCallbacks { type Ctx = Queue; @@ -294,6 +308,7 @@ impl run_tasks::RunTasksCallbacks for QueueRunTasksCallbac } } +#[cfg(not(bun_standalone))] impl Queue { pub fn enqueue(&mut self, global_object: &JSGlobalObject, opts: InitOpts<'_>) { bun_core::scoped_log!(AsyncModule, "enqueue: {}", bstr::BStr::new(opts.specifier)); @@ -607,6 +622,7 @@ impl Queue { } } +#[cfg(not(bun_standalone))] impl AsyncModule { pub fn init( opts: InitOpts<'_>, diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index 999122f4b475..bb76be6d2050 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -4275,6 +4275,7 @@ impl VirtualMachine { jsc_vm.log = NonNull::new(&raw mut log); jsc_vm.transpiler.resolver.log = NonNull::from(&mut log); jsc_vm.transpiler.linker.log = &raw mut log; + #[cfg(not(bun_standalone))] if let Some(pm) = jsc_vm.transpiler.resolver.package_manager { // SAFETY: the `dyn AutoInstaller` is always `PackageManager` // (sole impl — see `VirtualMachine::package_manager`). @@ -4301,6 +4302,7 @@ impl VirtualMachine { // `_resolve` may have lazily created the PM with // `pm.log = resolver.log` (our stack `log`), so restore even // if it was `None` when we swapped. + #[cfg(not(bun_standalone))] if let Some(pm) = jsc_vm.transpiler.resolver.package_manager { // SAFETY: sole `dyn AutoInstaller` impl is `PackageManager`. unsafe { diff --git a/src/jsc/hot_reloader.rs b/src/jsc/hot_reloader.rs index c4385b0b82c2..f753f57e9f06 100644 --- a/src/jsc/hot_reloader.rs +++ b/src/jsc/hot_reloader.rs @@ -1328,6 +1328,7 @@ where // never reached. The bundler crate (T5) can't name this generic, so it calls // in via the `#[no_mangle]` hook below. +#[cfg(not(bun_standalone))] impl<'a> HotReloaderCtx for bun_bundler::BundleV2<'a> { type EventLoop = bun_event_loop::AnyEventLoop<'static>; @@ -1403,12 +1404,14 @@ impl<'a> HotReloaderCtx for bun_bundler::BundleV2<'a> { /// `'static` because the only caller (`bun build --watch`) /// allocates the transpiler from the process-lifetime CLI arena. +#[cfg(not(bun_standalone))] type BundlerWatcher = NewHotReloader, bun_event_loop::AnyEventLoop<'static>, true>; /// CYCLEBREAK extern hook: called from `BundleV2::init` (T5) when /// `cli_watch_flag` is set. Defined here (not in /// `bun_bundler`) because the bundler crate can't name `NewHotReloader`. +#[cfg(not(bun_standalone))] #[unsafe(no_mangle)] fn __bun_jsc_enable_hot_module_reloading_for_bundler( bv2: core::ptr::NonNull>, diff --git a/src/runtime/allocators/mod.rs b/src/runtime/allocators/mod.rs index ff6908a2606e..b2d26c5961f7 100644 --- a/src/runtime/allocators/mod.rs +++ b/src/runtime/allocators/mod.rs @@ -28,5 +28,6 @@ pub fn register_safety_vtables() { for vt in bun_alloc::mimalloc_arena::std_vtables() { bun_safety::register_alloc_vtable(vt); } + #[cfg(not(bun_standalone))] bun_safety::register_alloc_vtable(&bun_bundler::bundle_v2::EXTERNAL_FREE_VTABLE); } diff --git a/src/runtime/api.rs b/src/runtime/api.rs index 708773313258..3acf9c6e1dad 100644 --- a/src/runtime/api.rs +++ b/src/runtime/api.rs @@ -59,6 +59,7 @@ pub mod glob; pub mod hash_object; #[path = "api/html_rewriter.rs"] pub mod html_rewriter; +#[cfg(not(bun_standalone))] #[path = "api/js_bundle_completion_task.rs"] pub mod js_bundle_completion_task; #[path = "api/JSBundler.rs"] @@ -75,6 +76,7 @@ pub mod lolhtml_jsc; pub mod markdown_object; #[path = "api/NativePromiseContext.rs"] pub mod native_promise_context; +#[cfg(not(bun_standalone))] #[path = "api/output_file_jsc.rs"] pub mod output_file_jsc; #[path = "api/standalone_graph_jsc.rs"] diff --git a/src/runtime/api/JSBundler.rs b/src/runtime/api/JSBundler.rs index 0fe86157d27e..1552dabfa8f1 100644 --- a/src/runtime/api/JSBundler.rs +++ b/src/runtime/api/JSBundler.rs @@ -1290,6 +1290,14 @@ pub mod js_bundler { pub prefix: OwnedString, } + #[cfg(bun_standalone)] + fn build(global_this: &JSGlobalObject, _arguments: &[JSValue]) -> JsResult { + Err(global_this.throw(format_args!( + "Bun.build is not available in standalone executables. Install Bun: https://bun.com/get" + ))) + } + + #[cfg(not(bun_standalone))] fn build(global_this: &JSGlobalObject, arguments: &[JSValue]) -> JsResult { if arguments.is_empty() || !arguments[0].is_object() { return Err(global_this.throw_invalid_arguments(format_args!( diff --git a/src/runtime/api/js_bundle_completion_task.rs b/src/runtime/api/js_bundle_completion_task.rs index eb829f098247..759605630367 100644 --- a/src/runtime/api/js_bundle_completion_task.rs +++ b/src/runtime/api/js_bundle_completion_task.rs @@ -554,6 +554,7 @@ impl JSBundleCompletionTask { return Ok(()); } + #[cfg(not(bun_standalone))] if let Some(html_build_task) = this.html_build_task { this.plugins = None; // SAFETY: `html_build_task` is a backref set by `HTMLBundle::Route` which diff --git a/src/runtime/bake_standalone_stub.rs b/src/runtime/bake_standalone_stub.rs new file mode 100644 index 000000000000..415643c96927 --- /dev/null +++ b/src/runtime/bake_standalone_stub.rs @@ -0,0 +1,203 @@ +//! `cfg(bun_standalone)` replacement for `mod bake`. +//! +//! The full `bake/` tree pulls in `bun_bundler::bundle_v2` (→ `bun_css`) via +//! `DevServer` → `IncrementalGraph` → `BundleV2` → `Chunk`. Under +//! `bun-standalone` none of that is reachable from JS (every entry point throws +//! "not available in standalone executables"), so this stub provides only the +//! type names the rest of `bun_runtime` mentions in signatures plus the +//! `#[no_mangle]` C-ABI symbols the shared C++ archive references +//! unconditionally. Every type that would otherwise carry a bundler payload is +//! uninhabited, so `Option>` etc. become ZSTs and the +//! `if let Some(dev) = …` bodies are statically dead (gated at the use sites). + +#![allow(dead_code, unused_variables, clippy::missing_safety_doc)] + +use bun_core::String as BunString; +use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; +use core::ffi::c_void; + +// ─── DevServer ─────────────────────────────────────────────────────────────── +pub mod dev_server { + /// Uninhabited — `Option>` is a ZST under standalone. + pub enum DevServer {} + + pub mod route_bundle { + /// `DevServer.RouteBundle.Index` — only stored in + /// `HTMLBundle::Route::dev_server_id` (always `None` under standalone). + #[derive(Clone, Copy)] + pub struct Index(u32); + } + + /// Uninhabited — never enqueued under standalone (no `DevServer` exists to + /// own a `WatcherAtomics`). + pub enum HotReloadEvent {} + impl HotReloadEvent { + pub unsafe fn run(_: *mut Self) { + unreachable!("bake DevServer is not available in standalone executables") + } + } + + pub mod source_map_store { + pub enum SourceMapStore {} + impl SourceMapStore { + pub fn sweep_weak_refs( + _t: *mut bun_event_loop::EventLoopTimer::EventLoopTimer, + _now: &bun_event_loop::EventLoopTimer::Timespec, + ) { + unreachable!("bake DevServer is not available in standalone executables") + } + } + } + + impl DevServer { + pub fn emit_memory_visualizer_message_timer( + _t: &mut bun_event_loop::EventLoopTimer::EventLoopTimer, + _now: &bun_event_loop::EventLoopTimer::Timespec, + ) { + unreachable!("bake DevServer is not available in standalone executables") + } + } +} +pub use dev_server as DevServer; + +// ─── FrameworkRouter ───────────────────────────────────────────────────────── +pub mod framework_router { + use super::*; + + /// `FrameworkRouter.Type.Index` — `AnyRoute::FrameworkRouter` is cfg-gated + /// out under standalone, so this is signature-only. + #[derive(Clone, Copy)] + pub struct TypeIndex(u8); + + /// `JSFrameworkRouter` — backing type for the `FrameworkFileSystemRouter` + /// codegen class. Never constructed under standalone; the constructor + /// throws and `m_ctx` stays null. + pub struct JSFrameworkRouter(()); + + impl JSFrameworkRouter { + pub fn constructor( + global: &JSGlobalObject, + _frame: &CallFrame, + ) -> JsResult> { + Err(global.throw(format_args!( + "FrameworkFileSystemRouter is not available in standalone executables. Install Bun: https://bun.com/get" + ))) + } + pub fn finalize(self: Box) {} + pub fn r#match( + &self, + global: &JSGlobalObject, + _frame: &CallFrame, + ) -> JsResult { + Err(global.throw(format_args!( + "FrameworkFileSystemRouter is not available in standalone executables" + ))) + } + pub fn to_json( + &self, + global: &JSGlobalObject, + _frame: &CallFrame, + ) -> JsResult { + Err(global.throw(format_args!( + "FrameworkFileSystemRouter is not available in standalone executables" + ))) + } + /// `js2native` thunk target (`generated_js2native.rs`). + pub fn get_bindings(global: &JSGlobalObject) -> JsResult { + // `bun:internal-for-testing` only — return undefined rather than + // throwing so the import itself succeeds. + let _ = global; + Ok(JSValue::UNDEFINED) + } + } + + /// `generated_js2native.rs` lowers the path to + /// `framework_router::js_framework_router::get_bindings`. + pub use JSFrameworkRouter as js_framework_router; +} + +// ─── extern "C" exports the shared C++ archive references ──────────────────── +// Signatures mirror the `cfg(bun_standalone)` stubs that previously lived in +// `bake/production.rs` / `bake/DevServer.rs`. Bodies are unreachable because +// `BakeGlobalObject__attachPerThreadData` is never called. + +#[unsafe(no_mangle)] +pub extern "C" fn BakeToWindowsPath(_input: BunString) -> BunString { + BunString::dead() +} + +#[unsafe(no_mangle)] +pub extern "C" fn BakeProdResolve( + _global: &JSGlobalObject, + _a_str: BunString, + _specifier_str: BunString, +) -> BunString { + BunString::dead() +} + +#[unsafe(no_mangle)] +pub extern "C" fn BakeProdLoad(_pt: *mut c_void, _key: BunString) -> BunString { + BunString::dead() +} + +#[unsafe(no_mangle)] +pub extern "C" fn BakeProdSourceMap(_pt: *mut c_void, _key: BunString) -> BunString { + BunString::dead() +} + +bun_jsc::jsc_host_abi! { + #[unsafe(no_mangle)] + pub unsafe fn Bake__bundleNewRouteJSFunctionImpl( + global: &JSGlobalObject, + _request_ptr: *mut c_void, + _route_kind: u8, + _route_index: u32, + ) -> JSValue { + let _ = global.throw(format_args!( + "Bake is not available in standalone executables. Install Bun: https://bun.com/get" + )); + JSValue::ZERO + } +} + +#[bun_jsc::host_fn(export = "Bake__getNewRouteParamsJSFunctionImpl")] +fn bake_get_new_route_params_stub(global: &JSGlobalObject, _cf: &CallFrame) -> JsResult { + Err(global.throw(format_args!( + "Bake is not available in standalone executables. Install Bun: https://bun.com/get" + ))) +} + +// ─── extern "Rust" link-interface stubs ────────────────────────────────────── +// `bun_bundler::link_interface!(DevServerHandle[Bake] { ... })` emits +// `extern "Rust"` declarations for the symbols below; the real impls live in +// `bake/dev_server/mod.rs` (compiled out under standalone). Release links drop +// the dead `BundleV2` callers via `--gc-sections` so the undefined refs never +// reach the linker, but debug builds have no gc-sections — provide unreachable +// stubs so the symbols resolve. Signatures are deliberately erased: the Rust +// ABI matches by symbol name only and none of these are reachable at runtime. +macro_rules! dev_server_dispatch_stub { + ($($sym:ident),* $(,)?) => {$( + #[unsafe(no_mangle)] + fn $sym() -> ! { + unreachable!("bake DevServer is not available in standalone executables") + } + )*}; +} +dev_server_dispatch_stub!( + __bun_dispatch__DevServerHandle__Bake__asset_hash, + __bun_dispatch__DevServerHandle__Bake__barrel_needed_exports, + __bun_dispatch__DevServerHandle__Bake__current_bundle_start_data, + __bun_dispatch__DevServerHandle__Bake__finalize_bundle, + __bun_dispatch__DevServerHandle__Bake__handle_parse_task_failure, + __bun_dispatch__DevServerHandle__Bake__is_file_cached, + __bun_dispatch__DevServerHandle__Bake__log_for_resolution_failures, + __bun_dispatch__DevServerHandle__Bake__put_or_overwrite_asset, + __bun_dispatch__DevServerHandle__Bake__register_barrel_export, + __bun_dispatch__DevServerHandle__Bake__register_barrel_with_deferrals, + __bun_dispatch__DevServerHandle__Bake__track_resolution_failure, +); + +#[unsafe(no_mangle)] +fn __bun_jsc_enable_hot_module_reloading_for_bundler() -> ! { + unreachable!("bun build --watch is not available in standalone executables") +} diff --git a/src/runtime/cli/mod.rs b/src/runtime/cli/mod.rs index 703bf7364d12..773b03091c92 100644 --- a/src/runtime/cli/mod.rs +++ b/src/runtime/cli/mod.rs @@ -233,6 +233,7 @@ pub mod discord_command; #[cfg(not(bun_standalone))] #[path = "list-of-yarn-commands.rs"] pub mod list_of_yarn_commands; +#[cfg(not(bun_standalone))] #[path = "shell_completions.rs"] pub mod shell_completions; #[cfg(not(bun_standalone))] @@ -575,7 +576,14 @@ pub(crate) static CMD: bun_core::RacyCell> = bun_core::Racy /// /// Canonical static lives in `bun_install` so both crates read/write the SAME /// flag (`RunCommand::create_fake_temporary_node_executable` lives there). +/// Under `bun_standalone` the install tier is severed, so a local static is +/// used instead — the standalone runtime never spawns lifecycle scripts that +/// need to observe the shared flag from `bun_install`. +#[cfg(not(bun_standalone))] pub use bun_install::PRETEND_TO_BE_NODE; +#[cfg(bun_standalone)] +pub static PRETEND_TO_BE_NODE: core::sync::atomic::AtomicBool = + core::sync::atomic::AtomicBool::new(false); /// This is set `true` during `Command.which()` if argv0 is "bunx" pub(crate) static IS_BUNX_EXE: core::sync::atomic::AtomicBool = diff --git a/src/runtime/cli/run_command.rs b/src/runtime/cli/run_command.rs index a98d7bce7f27..af434aebc755 100644 --- a/src/runtime/cli/run_command.rs +++ b/src/runtime/cli/run_command.rs @@ -30,6 +30,7 @@ use bun_which::which; use crate::cli; use crate::cli::arguments; use crate::cli::command::{ContextData, Tag as CommandTag}; +#[cfg(not(bun_standalone))] use crate::cli::shell_completions::ShellCompletions; bun_core::declare_scope!(RUN_LOG, visible); @@ -224,7 +225,21 @@ Full documentation is available at https://bun.com/docs/cli/run copy_script: &mut Vec, script: &[u8], ) -> Result<(), bun_core::Error> { - bun_install::lifecycle_script_runner::replace_package_manager_run(copy_script, script) + #[cfg(not(bun_standalone))] + return bun_install::lifecycle_script_runner::replace_package_manager_run( + copy_script, + script, + ); + // Standalone runtime never runs package.json lifecycle scripts (a + // compiled exe boots its embedded entry); the bare `bun-standalone` + // debugging binary keeps `bun run