diff --git a/.buildkite/ci.mjs b/.buildkite/ci.mjs index 275420c99834..65bca67f06fa 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. @@ -803,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}`); } @@ -817,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 { @@ -904,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 @@ -912,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"), }), @@ -938,7 +1017,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"); @@ -951,7 +1037,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: { @@ -977,9 +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`); + ? [...buildPlatforms.filter(p => p.os !== "windows").flatMap(buildKeys), "windows-sign"] + : buildPlatforms.flatMap(buildKeys); return { key: "release", @@ -1468,6 +1561,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/.buildkite/scripts/upload-release.sh b/.buildkite/scripts/upload-release.sh index 977ca9a71d96..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[@]}" @@ -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" @@ -255,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/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..4516ff307ee5 --- /dev/null +++ b/docs/standalone-binary.md @@ -0,0 +1,101 @@ +# `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, non-LTO, measured on this branch: + +| | bytes | MB | +| ------------------------- | -------------: | --------: | +| stripped `bun` | 70,389,048 | 67.13 | +| stripped `bun-standalone` | 62,392,896 | 59.50 | +| **delta** | **−7,996,152** | **−7.63** | + +`bloaty` section diff: `.text` −6.80 MB, `.rodata` −849 KB. + +Per-crate VM size from `bloaty -d compileunits` (full → standalone): + +| crate | full MB | standalone MB | Δ | +| ----------------- | ------: | ------------: | ----: | +| `bun_runtime` | 6.45 | 4.75 | −1.70 | +| `bun_install` | 2.03 | 0.03 | −2.00 | +| `bun_css` | 1.77 | 0 | −1.77 | +| `bun_bundler` | 1.61 | 0.44 | −1.17 | +| `bun_css_jsc` | 0.10 | 0 | −0.10 | +| `bun_install_jsc` | 0.05 | 0 | −0.05 | + +The remaining `bun_bundler` 0.44 MB is the `Transpiler` half (single-file +TS→JS, options/defines/cache, `analyze_transpiled_module`) which is +structurally embedded in `VirtualMachine` and required by the module loader. + +The < 35 MB target additionally requires shipping a reduced ICU data file +(small-icu ≈ 5 MB instead of 24 MB) — a WebKit-prebuilt change. The hard +floor with full ICU is JSC 22.9 MB + ICU 23.7 MB + bindings/crypto/codecs +≈ 57 MB. 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/packages/bun-release/scripts/upload-npm.ts b/packages/bun-release/scripts/upload-npm.ts index dfb76fe39fef..8ee1700c71a6 100644 --- a/packages/bun-release/scripts/upload-npm.ts +++ b/packages/bun-release/scripts/upload-npm.ts @@ -12,9 +12,11 @@ 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"; import { spawn } from "../src/spawn"; +const allPlatforms = [...platforms, ...standalonePlatforms]; + const module = "bun"; const owner = "@oven"; @@ -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 => 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..4312376eca39 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"; @@ -108,6 +108,15 @@ function systemLibs(cfg: Config): string[] { "wsock32.lib", // ws2_32 + wsock32 — wsock32 has TransmitFile (sendfile equiv) "ws2_32.lib", "delayimp.lib", // required for /delayload: in release + // The full build picks these up via Rust `#[link(name = "...")]` attrs + // (image/backend_wic.rs, windows_sys, install). Under bun_standalone the + // .o members carrying those `.drectve` link directives can be + // dead-stripped while C++ (rescle, WIC bindings) still references the + // symbols, so list them explicitly. All are system DLL import libs. + "shell32.lib", + "ole32.lib", + "oleaut32.lib", + "user32.lib", ); } @@ -595,8 +604,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 +724,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..8a204073933b 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"; @@ -382,8 +382,13 @@ export function packageAndUpload(cfg: Config, output: BunOutput): void { // build-time constant, so generate the same payload host-side instead // (the feature list is parsed out of src/analytics/lib.rs; see // features-json.ts). - if (cfg.crossTarget !== undefined) { - console.log("Generating features.json (host-side; cross-compiled binary cannot run here)..."); + // Standalone binaries can't run features.mjs either: it imports + // bun:internal-for-testing whose module body eagerly evaluates the + // upgrade/pack/install lazy-slot getters that throw under + // cfg(bun_standalone). Every field is a build-time constant, so the + // host-side generator is equivalent. + if (cfg.crossTarget !== undefined || cfg.standalone) { + console.log("Generating features.json (host-side)..."); writeFileSync(resolve(buildDir, "features.json"), crossFeaturesJson(cfg)); } else { console.log("Generating features.json..."); @@ -400,7 +405,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 +502,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/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/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/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/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 diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index 86827d34c65b..e0c2104d77c2 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -130,6 +130,19 @@ export class ClassDefinition { * @default false */ sharedThis?: boolean; + /** + * When set, the generated Rust thunks become cfg-split: under + * `cfg(not(bun_standalone))` they call the real inherent methods as usual; + * under `cfg(bun_standalone)` the backing type is a ZST stub and every thunk + * returns its zero value (throwing a TypeError first for any thunk that has + * a `global` parameter and a JSValue / pointer return). The real Rust module + * can then be `#[cfg(not(bun_standalone))]`-gated out entirely while the C++ + * `extern` symbols still link. + * + * Pass a string to customise the error message; `true` uses a generic + * " is not available in standalone executables" message. + */ + standaloneStub?: boolean | string; /** * Class constructor is newable. Called before the JSValue corresponding to * the object is created. Throwing an exception prevents the object from being diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index b659bb334bf0..1afcd792100c 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -2829,6 +2829,7 @@ function generateRust( getInternalProperties = false, rustPath, sharedThis = true, + standaloneStub = false, } = {} as ClassDefinition, ) { proto = { @@ -2847,8 +2848,33 @@ function generateRust( // placeholder, no `unimplemented!()` — a missing method is a compile error. const thunks: string[] = []; const symbols: string[] = []; + // standaloneStub: under `cfg(bun_standalone)` every thunk returns its zero + // value (throwing a TypeError first for JSValue/ptr returns that have a + // `global` in scope). The real `${T}::method` calls live behind + // `cfg(not(bun_standalone))` so the backing module can be cfg-gated out + // entirely while the C++ `extern` symbols still link. + const stubMsg = + typeof standaloneStub === "string" ? standaloneStub : `${typeName} is not available in standalone executables`; + function stubBody(sig: string): string { + const ret = /-> (.+)$/.exec(sig)?.[1]?.trim() ?? "()"; + const hasGlobal = sig.includes("global: &JSGlobalObject"); + const throwStmt = hasGlobal ? `let _ = global.throw_type_error(format_args!(${JSON.stringify(stubMsg)})); ` : ``; + if (ret === "JSValue") return throwStmt + "JSValue::ZERO"; + if (ret === "*mut c_void") return throwStmt + "core::ptr::null_mut()"; + if (ret === "bool") return throwStmt + "false"; + if (ret === "usize") return "0"; + if (ret === "()") return ""; + return `unreachable!()`; + } function thunk(sym: string, sig: string, body: string) { symbols.push(sym); + if (standaloneStub) { + body = + `#[cfg(not(bun_standalone))]\n` + + ` { ${body.trim()} }\n` + + ` #[cfg(bun_standalone)]\n` + + ` { ${stubBody(sig)} }`; + } // Safe-body thunks: every pointer param is typed as `&`/`&mut` directly // (ABI-identical to `*const`/`*mut` for non-null inputs, which the C++ // caller guarantees) and routed through a safe `host_fn::*` helper. The @@ -3143,6 +3169,19 @@ ${cachedExterns} ${gcAccessors} }`; + const typeDecl = standaloneStub + ? `/// Native backing type for \`JS${typeName}.m_ctx\`. Re-export of the real +/// struct under \`cfg(not(bun_standalone))\`; ZST stub under \`cfg(bun_standalone)\` +/// so every thunk below still links while \`${rustPath}\` is cfg-gated out. +#[cfg(not(bun_standalone))] +pub use ${rustPath} as ${typeName}; +#[cfg(bun_standalone)] +pub struct ${typeName};` + : `/// Native backing type for \`JS${typeName}.m_ctx\`. Re-export of the real +/// struct so the thunks below call its inherent methods directly. A missing +/// method is a compile error — fix it in \`${rustPath}\`, not here. +pub use ${rustPath} as ${typeName};`; + return { symbols, rustPath, @@ -3151,10 +3190,7 @@ ${gcAccessors} // ${typeName} // ════════════════════════════════════════════════════════════════════════════ -/// Native backing type for \`JS${typeName}.m_ctx\`. Re-export of the real -/// struct so the thunks below call its inherent methods directly. A missing -/// method is a compile error — fix it in \`${rustPath}\`, not here. -pub use ${rustPath} as ${typeName}; +${typeDecl} ${thunks.join("\n\n")} 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/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/generated_classes_list.rs b/src/jsc/generated_classes_list.rs index 53eccf23d622..c4ba70d44fcd 100644 --- a/src/jsc/generated_classes_list.rs +++ b/src/jsc/generated_classes_list.rs @@ -33,20 +33,19 @@ pub mod Classes { pub use crate::crypto::CryptoHasher; pub use crate::image as Image; pub use crate::shell::Interpreter as ShellInterpreter; + // Under `cfg(bun_standalone)` `crate::test_runner` is the ZST stub module + // and these paths don't resolve; the codegen-emitted ZST stubs in + // `generated_classes.rs` are the canonical types there instead. + #[cfg(not(bun_standalone))] pub use crate::test_runner::done_callback::DoneCallback; - pub use crate::test_runner::expect::Expect; - pub use crate::test_runner::expect::ExpectAny; - pub use crate::test_runner::expect::ExpectAnything; - pub use crate::test_runner::expect::ExpectArrayContaining; - pub use crate::test_runner::expect::ExpectCloseTo; - pub use crate::test_runner::expect::ExpectCustomAsymmetricMatcher; - pub use crate::test_runner::expect::ExpectMatcherContext; - pub use crate::test_runner::expect::ExpectMatcherUtils; - pub use crate::test_runner::expect::ExpectObjectContaining; - pub use crate::test_runner::expect::ExpectStatic; - pub use crate::test_runner::expect::ExpectStringContaining; - pub use crate::test_runner::expect::ExpectStringMatching; - pub use crate::test_runner::expect::ExpectTypeOf; + #[cfg(not(bun_standalone))] + pub use crate::test_runner::expect::{ + Expect, ExpectAny, ExpectAnything, ExpectArrayContaining, ExpectCloseTo, + ExpectCustomAsymmetricMatcher, ExpectMatcherContext, ExpectMatcherUtils, + ExpectObjectContaining, ExpectStatic, ExpectStringContaining, ExpectStringMatching, + ExpectTypeOf, + }; + #[cfg(not(bun_standalone))] pub use crate::test_runner::scope_functions::ScopeFunctions; pub use crate::webcore::Blob; // `crate::shell::ParsedShellScript` is a `(())` placeholder; the real struct 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/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/resolver/node_fallbacks.rs b/src/resolver/node_fallbacks.rs index b7c4831436d9..5ee995ae3882 100644 --- a/src/resolver/node_fallbacks.rs +++ b/src/resolver/node_fallbacks.rs @@ -36,8 +36,18 @@ macro_rules! create_source_code_getter { // `$code_path` is relative to `BUN_CODEGEN_DIR` (codegen output, not // the source tree). fn get() -> &'static str { + // bun-standalone never bundles for `--target=browser` (it has no + // bundler), so the browser polyfills are unreachable. Dropping + // the `include_bytes!` saves ~90 KB of `.rodata`. + #[cfg(bun_standalone)] + unreachable!(concat!( + "node-fallback polyfill `", + $code_path, + "` is not available in standalone executables" + )); // `bun_codegen_embed` is set via RUSTFLAGS by scripts/build/rust.ts; // plain `cargo check` doesn't pass `--check-cfg` for it. + #[cfg(not(bun_standalone))] #[allow(unexpected_cfgs)] let source: &'static str = { #[cfg(bun_codegen_embed)] @@ -64,6 +74,7 @@ macro_rules! create_source_code_getter { ::bun_core::runtime_embed_file!(Codegen, $code_path) } }; + #[cfg(not(bun_standalone))] source } get as fn() -> &'static str 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/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..8d99c46fcb16 100644 --- a/src/runtime/api.rs +++ b/src/runtime/api.rs @@ -51,14 +51,18 @@ pub mod cron; pub mod cron_parser; #[path = "api/csrf_jsc.rs"] pub mod csrf_jsc; +#[cfg(not(bun_standalone))] #[path = "api/filesystem_router.rs"] pub mod filesystem_router; +#[cfg(bun_standalone)] +pub use standalone_api_stubs::filesystem_router; #[path = "api/glob.rs"] pub mod glob; #[path = "api/HashObject.rs"] 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 +79,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"] @@ -86,6 +91,14 @@ pub mod unsafe_object; #[path = "api/YAMLObject.rs"] pub mod yaml_object; +// ─── cfg(bun_standalone) stub for `Bun.FileSystemRouter` ───────────────────── +// `bun_router` + the directory-walk machinery are not useful inside a compiled +// executable; the stub provides the type names + method signatures the codegen +// references, with a constructor that throws. +#[cfg(bun_standalone)] +#[path = "api/standalone_api_stubs.rs"] +pub mod standalone_api_stubs; + // ─── api/bun/ core (process / spawn / pty / h2) ────────────────────────────── // `#[path]` is relative to the dir containing this file (`src/runtime/`); the // inline `mod bun { }` below is a re-export façade only — module bodies are diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index ede56cfa32c4..12e941ae0a2d 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -72,6 +72,7 @@ pub(crate) fn get_public_path_with_asset_prefix( /// `Bun.getPublicPath` — wrapper over [`get_public_path_with_asset_prefix`] /// using the VM's top-level dir, no asset prefix, and loose path platform. +#[cfg_attr(bun_standalone, allow(dead_code))] pub(crate) fn get_public_path( to: &[u8], origin: &bun_url::URL, @@ -191,6 +192,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 +346,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 +1721,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..1552dabfa8f1 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; @@ -1283,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!( @@ -1387,6 +1402,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 +1521,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 +1531,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 +1799,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 +1829,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 +1912,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/api/bun/js_bun_spawn_bindings.rs b/src/runtime/api/bun/js_bun_spawn_bindings.rs index 55c2fa8b841d..556cadc2047e 100644 --- a/src/runtime/api/bun/js_bun_spawn_bindings.rs +++ b/src/runtime/api/bun/js_bun_spawn_bindings.rs @@ -1777,12 +1777,15 @@ pub(crate) fn spawn_maybe_sync( while subprocess.compute_has_pending_activity() { // Re-evaluate this at each iteration of the loop since it may change between iterations. + #[cfg(not(bun_standalone))] let bun_test_timeout: Timespec = if let Some(runner) = crate::test_runner::jest::Jest::runner() { runner.get_active_timeout() } else { Timespec::EPOCH }; + #[cfg(bun_standalone)] + let bun_test_timeout: Timespec = Timespec::EPOCH; let has_bun_test_timeout = !bun_test_timeout.eql(&Timespec::EPOCH); if has_bun_test_timeout { @@ -1832,6 +1835,7 @@ pub(crate) fn spawn_maybe_sync( // Support bun:test timeouts AND spawnSync() timeout. // There is a scenario where inside of spawnSync() a totally // different test fails, and that SHOULD be okay. + #[cfg(not(bun_standalone))] if has_bun_test_timeout { if bun_test_timeout.order(&now) == core::cmp::Ordering::Less { let mut active_file_strong = crate::test_runner::jest::Jest::runner() 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/api/standalone_api_stubs.rs b/src/runtime/api/standalone_api_stubs.rs new file mode 100644 index 000000000000..728fe9e642a6 --- /dev/null +++ b/src/runtime/api/standalone_api_stubs.rs @@ -0,0 +1,71 @@ +//! `cfg(bun_standalone)` replacement for `Bun.FileSystemRouter`. +//! +//! `bun_router` + the directory-walk machinery are not useful inside a compiled +//! executable (the route table was decided at build time). The codegen-emitted +//! `#[no_mangle]` thunks in `generated_classes.rs` reference these types by +//! path and call their inherent methods, so the stub provides a unit struct +//! with the exact method set the codegen calls — `constructor` throws, every +//! other method is unreachable (constructor failure means `m_ctx` stays null +//! and prototype methods never receive a live `&Self`). This keeps every +//! C++-referenced symbol linkable without compiling the real implementation. + +#![allow(dead_code, unused_variables, clippy::missing_safety_doc)] + +use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; + +#[cold] +fn unavailable(global: &JSGlobalObject, name: &str) -> bun_jsc::JsError { + global.throw(format_args!( + "{name} is not available in standalone executables. Install Bun: https://bun.com/get" + )) +} + +/// Body for prototype methods / getters on a never-constructed stub: the +/// codegen thunk passes `&Self` from a non-null `m_ctx`, but `constructor` +/// always throws, so no `m_ctx` is ever populated. +macro_rules! never { + () => { + unreachable!("constructor throws under bun_standalone; m_ctx is never set") + }; +} + +// ─── Bun.FileSystemRouter / MatchedRoute ───────────────────────────────────── +pub mod filesystem_router { + use super::*; + + // `FrameworkFileSystemRouter` is declared in `filesystem_router.classes.ts`, + // so codegen resolves it via this module. The real backing type already has + // a standalone stub in `bake_standalone_stub.rs`. + pub use crate::bake::framework_router::JSFrameworkRouter as FrameworkFileSystemRouter; + + pub struct FileSystemRouter(()); + + bun_jsc::impl_js_class_via_generated!( + FileSystemRouter => crate::generated_classes::js_FileSystemRouter + ); + + impl FileSystemRouter { + pub fn constructor(g: &JSGlobalObject, _: &CallFrame) -> JsResult> { + Err(unavailable(g, "Bun.FileSystemRouter")) + } + pub fn finalize(self: Box) {} + pub fn r#match(&self, _: &JSGlobalObject, _: &CallFrame) -> JsResult { never!() } + pub fn reload(&self, _: &JSGlobalObject, _: &CallFrame) -> JsResult { never!() } + pub fn get_origin(&self, _: &JSGlobalObject) -> JsResult { never!() } + pub fn get_routes(&self, _: &JSGlobalObject) -> JsResult { never!() } + pub fn get_style(&self, _: &JSGlobalObject) -> JsResult { never!() } + } + + pub struct MatchedRoute(()); + + impl MatchedRoute { + pub fn finalize(self: Box) {} + pub fn get_file_path(&self, _: &JSGlobalObject) -> JsResult { never!() } + pub fn get_kind(&self, _: &JSGlobalObject) -> JsResult { never!() } + pub fn get_name(&self, _: &JSGlobalObject) -> JsResult { never!() } + pub fn get_params(&self, _: &JSGlobalObject) -> JsResult { never!() } + pub fn get_pathname(&self, _: &JSGlobalObject) -> JsResult { never!() } + pub fn get_query(&self, _: &JSGlobalObject) -> JsResult { never!() } + pub fn get_script_src(&self, _: &JSGlobalObject) -> JsResult { never!() } + } +} 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/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/Arguments.rs b/src/runtime/cli/Arguments.rs index accab9fb352a..a008eb1cfa76 100644 --- a/src/runtime/cli/Arguments.rs +++ b/src/runtime/cli/Arguments.rs @@ -380,6 +380,7 @@ macro_rules! maybe_bake_debug_params { }; } +#[cfg(not(bun_standalone))] pub(crate) const BUILD_ONLY_PARAMS: &[ParamType] = concat_params!( &[ parse_param!( @@ -418,6 +419,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: \"full\" (default) or \"standalone\" (smaller)" + ), parse_param!("--bytecode Use a bytecode cache"), parse_param!( "--watch Automatically restart the process on file change" @@ -526,10 +530,12 @@ pub(crate) const BUILD_ONLY_PARAMS: &[ParamType] = concat_params!( ], maybe_bake_debug_params!(), ); +#[cfg(not(bun_standalone))] pub(crate) const BUILD_PARAMS: &[ParamType] = concat_params!(BUILD_ONLY_PARAMS, TRANSPILER_PARAMS_, BASE_PARAMS_); // TODO: update test completions +#[cfg(not(bun_standalone))] pub(crate) const TEST_ONLY_PARAMS: &[ParamType] = &[ parse_param!( "--no-orphans Exit when the parent process dies, and on exit SIGKILL every descendant. Linux/macOS only." @@ -602,6 +608,7 @@ pub(crate) const TEST_ONLY_PARAMS: &[ParamType] = &[ "--shard Run a subset of test files, e.g. '--shard=1/3' runs the first of three shards. Useful for splitting tests across multiple CI jobs." ), ]; +#[cfg(not(bun_standalone))] pub(crate) const TEST_PARAMS: &[ParamType] = concat_params!( TEST_ONLY_PARAMS, RUNTIME_PARAMS_, @@ -643,7 +650,9 @@ pub(crate) const BASE_RUNTIME_TRANSPILER_PARAMS: &[ParamType] = )] pub static AUTO_TABLE: &clap::ConvertedTable = clap::comptime_table!(AUTO_PARAMS); pub static RUN_TABLE: &clap::ConvertedTable = clap::comptime_table!(RUN_PARAMS, cold); +#[cfg(not(bun_standalone))] pub static BUILD_TABLE: &clap::ConvertedTable = clap::comptime_table!(BUILD_PARAMS, cold); +#[cfg(not(bun_standalone))] pub static TEST_TABLE: &clap::ConvertedTable = clap::comptime_table!(TEST_PARAMS, cold); pub(crate) static BASE_RUNTIME_TRANSPILER_TABLE: &clap::ConvertedTable = clap::comptime_table!(BASE_RUNTIME_TRANSPILER_PARAMS, cold); @@ -656,7 +665,9 @@ pub(crate) fn tag_table(cmd: CommandTag) -> &'static clap::ConvertedTable { match cmd { CommandTag::AutoCommand => AUTO_TABLE, CommandTag::RunCommand | CommandTag::RunAsNodeCommand => RUN_TABLE, + #[cfg(not(bun_standalone))] CommandTag::BuildCommand => BUILD_TABLE, + #[cfg(not(bun_standalone))] CommandTag::TestCommand => TEST_TABLE, CommandTag::BunxCommand => RUN_TABLE, _ => BASE_RUNTIME_TRANSPILER_TABLE, @@ -809,6 +820,7 @@ pub fn parse(cmd: CommandTag, ctx: Context<'_>) -> Result) -> Result) -> Result` /// and bare-`bun ` hot path (`USES_GLOBAL_OPTIONS` ⇒ `parse` runs on every /// invocation) doesn't carry the test-runner flag handling in its instruction pages. +#[cfg(not(bun_standalone))] #[cold] #[inline(never)] fn parse_test_command_options(args: &clap::Args, ctx: Context<'_>) { @@ -1815,6 +1829,7 @@ fn parse_test_command_options(args: &clap::Args, ctx: Context<'_>) { /// `--compile` / `CompileTarget`, sourcemap / format / minify, Windows executable /// metadata, etc. Split out of [`parse`] for the same reason as /// [`parse_test_command_options`]. +#[cfg(not(bun_standalone))] #[cold] #[inline(never)] fn parse_build_command_options( @@ -2011,6 +2026,24 @@ 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(); + } + }; + } + // The `--compile` default stays `CompileRuntime::Full` until + // `@oven/bun-standalone-*` packages exist on npm (first canary after this + // change). Flip to `Standalone` once the download path resolves. + 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/cli/mod.rs b/src/runtime/cli/mod.rs index 25af3f71c10a..1d7564dd1dfe 100644 --- a/src/runtime/cli/mod.rs +++ b/src/runtime/cli/mod.rs @@ -4,6 +4,16 @@ //! 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 `*_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; use bun_core::strings; @@ -210,16 +220,23 @@ 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; +#[cfg(not(bun_standalone))] #[path = "shell_completions.rs"] pub mod shell_completions; +#[cfg(not(bun_standalone))] #[path = "which_npm_client.rs"] pub mod which_npm_client; @@ -260,19 +277,24 @@ 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; // Surfaced for `crate::test_runner::{bun_test,jest,Execution}` which // need `CommandLineReporter`. This is the sole live mount of the file. +#[cfg(not(bun_standalone))] #[path = "test_command.rs"] pub mod test_command; /// `bun test` support modules (Scanner / ChangedFilesFilter / ParallelRunner). /// Mounted here so `test_command.rs` can `use crate::cli::test::scanner` etc. +#[cfg(not(bun_standalone))] pub mod test { #[path = "Scanner.rs"] pub mod scanner; @@ -325,70 +347,129 @@ 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; +#[cfg(not(bun_standalone))] #[path = "filter_arg.rs"] pub mod filter_arg; +#[cfg(not(bun_standalone))] #[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; +#[cfg(not(bun_standalone))] pub use filter_run as FilterRun; +#[cfg(not(bun_standalone))] #[path = "multi_run.rs"] pub mod multi_run; +#[cfg(not(bun_standalone))] pub use multi_run as MultiRun; // ─── crate-local helper for param-table concatenation ──────────────────────── @@ -502,7 +583,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 = @@ -1303,6 +1391,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 +1447,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 @@ -1445,6 +1596,7 @@ pub mod command { }; ctx.args.target = Some(bun_options_types::schema::api::Target::Bun); + #[cfg(not(bun_standalone))] if ctx.parallel || ctx.sequential { // Result: if this returns at all, it's Err. let Err(err) = super::multi_run::run(ctx); @@ -1452,6 +1604,7 @@ pub mod command { Global::exit(1); } + #[cfg(not(bun_standalone))] if !ctx.filters.is_empty() || ctx.workspaces { // Result: if this returns at all, it's Err. let Err(err) = super::filter_run::run_scripts_with_filter(ctx); @@ -1459,10 +1612,16 @@ pub mod command { Global::exit(1); } + #[cfg(bun_standalone)] + if ctx.parallel || ctx.sequential || !ctx.filters.is_empty() || ctx.workspaces { + crate::standalone_build::unavailable_command(b"run --filter"); + } + if tag == Tag::AutoCommand && !ctx.runtime_options.eval.script.is_empty() { 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 +1651,7 @@ pub mod command { Ok(()) } + #[cfg(not(bun_standalone))] #[cold] #[inline(never)] fn exec_init() -> CmdResult { @@ -1500,6 +1660,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 +1685,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 +1707,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 +1716,7 @@ pub mod command { Ok(()) } + #[cfg(not(bun_standalone))] #[cold] #[inline(never)] fn exec_audit(log: &mut bun_ast::Log) -> CmdResult { @@ -1573,6 +1737,7 @@ pub mod command { Ok(()) } + #[cfg(not(bun_standalone))] #[cold] #[inline(never)] fn exec_fuzzilli(log: &mut bun_ast::Log) -> CmdResult { @@ -1597,6 +1762,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 +1814,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 +1928,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 +2051,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 +2085,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> { @@ -1958,7 +2128,9 @@ To create a project with the official Next.js scaffolding tool, run\n\ match cmd { Tag::AutoCommand => arguments::AUTO_PARAMS, Tag::RunCommand | Tag::RunAsNodeCommand => arguments::RUN_PARAMS, + #[cfg(not(bun_standalone))] Tag::BuildCommand => arguments::BUILD_PARAMS, + #[cfg(not(bun_standalone))] Tag::TestCommand => arguments::TEST_PARAMS, Tag::BunxCommand => arguments::RUN_PARAMS, _ => arguments::BASE_RUNTIME_TRANSPILER_PARAMS, @@ -2004,6 +2176,7 @@ Examples: ); Output::flush(); } + #[cfg(not(bun_standalone))] Tag::BuildCommand => { pretty!( "\ @@ -2035,6 +2208,7 @@ A full list of flags is available at https://bun.com/docs/bundler ); Output::flush(); } + #[cfg(not(bun_standalone))] Tag::TestCommand => { pretty!( "\ @@ -2158,21 +2332,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); } @@ -2277,9 +2457,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/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