From 4e7fed426ff9cf52d3ea219dfe2e76ca1ddbd449 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 02:53:20 +0000 Subject: [PATCH 1/6] compile: accept "glibc" as an explicit libc token in --target Bun.Build.Libc has advertised "glibc" | "musl" since compile targets were added to Bun.build, but CompileTarget::try_from only knew the "musl" and "android" tokens, so "bun-linux-x64-glibc" failed in both the CLI (Unsupported target "glibc") and Bun.build (Unknown compile target). "glibc" now parses to Libc::Default, so it names the same build as the plain Linux targets and never downloads anything on a glibc host. Like "musl" it is only valid together with linux; the OS check keys off the explicit token instead of the libc value so "bun-windows-x64-glibc" is still rejected, with its own message on the CLI. --- docs/bundler/executables.mdx | 2 +- src/options_types/compile_target.rs | 11 ++- test/bundler/bun-build-compile.test.ts | 95 +++++++++++++++++++++++++- 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/docs/bundler/executables.mdx b/docs/bundler/executables.mdx index 77cb6a6ba3c4..48b97dbfdd4b 100644 --- a/docs/bundler/executables.mdx +++ b/docs/bundler/executables.mdx @@ -226,7 +226,7 @@ To build for macOS x64: ### Supported targets -The segments of the `--target` value can appear in any order, as long as they're delimited by `-`. +The segments of the `--target` value can appear in any order, as long as they're delimited by `-`. Linux targets also accept a libc segment: `-musl` selects the musl build, and `-glibc` (for example `bun-linux-x64-glibc`) explicitly selects the glibc build that the plain `bun-linux-*` targets below use. | --target | Operating System | Architecture | Modern | Baseline | Libc | | -------------------- | ---------------- | ------------ | ------ | -------- | ----- | diff --git a/src/options_types/compile_target.rs b/src/options_types/compile_target.rs index 1875cab88df8..4b35762cf3f2 100644 --- a/src/options_types/compile_target.rs +++ b/src/options_types/compile_target.rs @@ -297,6 +297,10 @@ impl CompileTarget { _found_version = true; continue; } + } else if token == b"glibc" { + this.libc = Libc::Default; + found_libc = true; + continue; } else if token == b"musl" { this.libc = Libc::Musl; found_libc = true; @@ -328,7 +332,8 @@ impl CompileTarget { this.baseline = false; } - if this.libc != Libc::Default && this.os != OperatingSystem::Linux { + // Not `libc != Default`: an explicit "glibc" parses to `Default` and is just as invalid off linux. + if found_libc && this.os != OperatingSystem::Linux { return Err(ParseError::InvalidTarget); } @@ -354,6 +359,7 @@ impl CompileTarget { && OPERATING_SYSTEM_NAMES.get(token).is_none() && token != b"modern" && token != b"baseline" + && token != b"glibc" && token != b"musl" && token != b"android" && !(strings::has_prefix(token, b"v1.") @@ -381,6 +387,9 @@ impl CompileTarget { let input = strings::trim(input_, b" \t\r"); if strings::contains(input, b"musl") && !strings::contains(input, b"linux") { bun_core::err_generic!("invalid target, musl libc only exists on linux"); + } else if strings::contains(input, b"glibc") && !strings::contains(input, b"linux") + { + bun_core::err_generic!("invalid target, glibc only exists on linux"); } else if strings::contains(input, b"android") && !strings::contains(input, b"linux") { diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index 6d6c2055012f..7d5da450ecf1 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, isArm64, isLinux, isMacOS, isMusl, isPosix, isWindows, tempDir } from "harness"; +import { bunEnv, bunExe, isArm64, isGlibc, isLinux, isMacOS, isMusl, isPosix, isWindows, tempDir } from "harness"; import { chmodSync, closeSync, cpSync, existsSync, openSync, readSync } from "node:fs"; import { join } from "path"; @@ -122,6 +122,99 @@ describe("Bun.build compile", () => { }); }); +// `Bun.Build.Libc` is "glibc" | "musl". "-glibc" explicitly selects the default Linux build, which on +// a glibc host is the running bun itself, so the accepting tests below never download anything. +describe("compile target -glibc token", () => { + const arch = isArm64 ? "arm64" : "x64"; + + test.concurrent.skipIf(!isGlibc)("Bun.build accepts a bun-linux--glibc target", async () => { + using dir = tempDir("build-compile-glibc-api", { + "app.js": `console.log("glibc-target-ok");`, + }); + + const result = await Bun.build({ + entrypoints: [join(String(dir), "app.js")], + compile: { + target: `bun-linux-${arch}-glibc`, + outfile: join(String(dir), "app"), + }, + }); + expect(result.success).toBe(true); + + await using proc = Bun.spawn({ + cmd: [result.outputs[0].path], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ stdout: "glibc-target-ok\n", stderr: "", exitCode: 0 }); + }); + + test.concurrent.skipIf(!isGlibc)("bun build --compile accepts --target=bun-linux--glibc", async () => { + using dir = tempDir("build-compile-glibc-cli", { + "app.js": `console.log("glibc-target-ok");`, + }); + const outfile = join(String(dir), "app"); + + await using build = Bun.spawn({ + cmd: [bunExe(), "build", "--compile", `--target=bun-linux-${arch}-glibc`, "app.js", "--outfile", outfile], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [, buildStderr, buildExitCode] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]); + expect({ stderr: buildStderr, exitCode: buildExitCode }).toEqual({ stderr: "", exitCode: 0 }); + + await using proc = Bun.spawn({ + cmd: [outfile], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ stdout: "glibc-target-ok\n", stderr: "", exitCode: 0 }); + }); + + // libc is a Linux-only axis. These are rejected while parsing the target, on every host. + test.concurrent("bun build --compile rejects -glibc combined with a non-linux OS", async () => { + using dir = tempDir("build-compile-glibc-cli-invalid", { + "app.js": `console.log("unreachable");`, + }); + + await using build = Bun.spawn({ + cmd: [bunExe(), "build", "--compile", "--target=bun-windows-x64-glibc", "app.js", "--outfile", "app"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ + stdout: "", + stderr: "error: invalid target, glibc only exists on linux\n", + exitCode: 1, + }); + }); + + test("Bun.build rejects -glibc combined with a non-linux OS", () => { + using dir = tempDir("build-compile-glibc-api-invalid", { + "app.js": `console.log("unreachable");`, + }); + + expect(() => + Bun.build({ + entrypoints: [join(String(dir), "app.js")], + compile: { + target: "bun-darwin-arm64-glibc" as Bun.Build.CompileTarget, + outfile: join(String(dir), "app"), + }, + }), + ).toThrowErrorMatchingInlineSnapshot(`"Unknown compile target: bun-darwin-arm64-glibc"`); + }); +}); + describe("compiled binary validity", () => { test("output binary has valid executable header", async () => { using dir = tempDir("build-compile-valid-header", { From 4e95696d204b142ea2442fed90f16eb13ee2f7ae Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:03:06 +0000 Subject: [PATCH 2/6] test: cover a glibc target with an unknown segment on both entry points --- test/bundler/bun-build-compile.test.ts | 28 +++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index 7d5da450ecf1..8344701b34cf 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { bunEnv, bunExe, isArm64, isGlibc, isLinux, isMacOS, isMusl, isPosix, isWindows, tempDir } from "harness"; -import { chmodSync, closeSync, cpSync, existsSync, openSync, readSync } from "node:fs"; +import { chmodSync, closeSync, cpSync, existsSync, openSync, readdirSync, readSync } from "node:fs"; import { join } from "path"; describe("Bun.build compile", () => { @@ -177,28 +177,38 @@ describe("compile target -glibc token", () => { expect({ stdout, stderr, exitCode }).toEqual({ stdout: "glibc-target-ok\n", stderr: "", exitCode: 0 }); }); - // libc is a Linux-only axis. These are rejected while parsing the target, on every host. - test.concurrent("bun build --compile rejects -glibc combined with a non-linux OS", async () => { + // Rejected while parsing the target, on every host: libc is a Linux-only axis, and a recognized + // "glibc" must not hide which segment of an otherwise broken target is the unknown one. + test.concurrent.each([ + ["bun-windows-x64-glibc", "error: invalid target, glibc only exists on linux\n"], + [ + "bun-linux-x64-glibc-invalid", + 'error: Unsupported target "invalid" in "bun-linux-x64-glibc-invalid"\n' + + "To see the supported targets:\n" + + " https://bun.com/docs/bundler/executables\n", + ], + ])("bun build --compile rejects --target=%s", async (target, expectedStderr) => { using dir = tempDir("build-compile-glibc-cli-invalid", { "app.js": `console.log("unreachable");`, }); await using build = Bun.spawn({ - cmd: [bunExe(), "build", "--compile", "--target=bun-windows-x64-glibc", "app.js", "--outfile", "app"], + cmd: [bunExe(), "build", "--compile", `--target=${target}`, "app.js", "--outfile", "app"], env: bunEnv, cwd: String(dir), stdout: "pipe", stderr: "pipe", }); const [stdout, stderr, exitCode] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]); - expect({ stdout, stderr, exitCode }).toEqual({ + expect({ stdout, stderr, exitCode, files: readdirSync(String(dir)) }).toEqual({ stdout: "", - stderr: "error: invalid target, glibc only exists on linux\n", + stderr: expectedStderr, exitCode: 1, + files: ["app.js"], }); }); - test("Bun.build rejects -glibc combined with a non-linux OS", () => { + test.each(["bun-darwin-arm64-glibc", "bun-linux-x64-glibc-invalid"])("Bun.build rejects target %s", target => { using dir = tempDir("build-compile-glibc-api-invalid", { "app.js": `console.log("unreachable");`, }); @@ -207,11 +217,11 @@ describe("compile target -glibc token", () => { Bun.build({ entrypoints: [join(String(dir), "app.js")], compile: { - target: "bun-darwin-arm64-glibc" as Bun.Build.CompileTarget, + target: target as Bun.Build.CompileTarget, outfile: join(String(dir), "app"), }, }), - ).toThrowErrorMatchingInlineSnapshot(`"Unknown compile target: bun-darwin-arm64-glibc"`); + ).toThrow(new TypeError(`Unknown compile target: ${target}`)); }); }); From e1bfee3d41f27e06f2b1bc3d642797cb22f9da88 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:27:45 +0000 Subject: [PATCH 3/6] compile_target: report the parse failure itself instead of re-deriving it try_from knew which segment it rejected and why, but returned unit variants, so from() re-tokenized the input against a second copy of the token list to name the bad segment and picked the InvalidTarget message by substring-matching the input. Adding the glibc token meant teaching all three places, and the substring chain misattributed inputs such as bun-x64-glibc-v1.2 (an incomplete version) to the libc message. ParseError now carries the segment or the reason and Display renders the same messages; from() only prints it. The libc tokens move into a single LIBC_NAMES map like the arch and OS tables. Tests pin every message, add the musl/glibc selection check that is only observable from a bun built against the other libc (the fetch goes to a local 404 server, so the build fails naming the bun it resolved to), and the docs sentence now describes that a Linux target without a libc segment follows the running bun's libc. --- docs/bundler/executables.mdx | 4 +- src/options_types/compile_target.rs | 147 +++++++++---------- test/bundler/bun-build-compile.test.ts | 192 ++++++++++++++++++------- 3 files changed, 209 insertions(+), 134 deletions(-) diff --git a/docs/bundler/executables.mdx b/docs/bundler/executables.mdx index 48b97dbfdd4b..ee33b8af8b1c 100644 --- a/docs/bundler/executables.mdx +++ b/docs/bundler/executables.mdx @@ -226,7 +226,9 @@ To build for macOS x64: ### Supported targets -The segments of the `--target` value can appear in any order, as long as they're delimited by `-`. Linux targets also accept a libc segment: `-musl` selects the musl build, and `-glibc` (for example `bun-linux-x64-glibc`) explicitly selects the glibc build that the plain `bun-linux-*` targets below use. +The segments of the `--target` value can appear in any order, as long as they're delimited by `-`. + +Linux targets also accept a libc segment, `-glibc` or `-musl` (for example `bun-linux-x64-glibc`), which selects that libc's build of Bun no matter which build is running. Without one, a Linux target uses the libc of the `bun` running the build: the table below describes a glibc build of Bun, while a musl build of Bun turns `bun-linux-x64` into a musl executable. | --target | Operating System | Architecture | Modern | Baseline | Libc | | -------------------- | ---------------- | ------------ | ------ | -------- | ----- | diff --git a/src/options_types/compile_target.rs b/src/options_types/compile_target.rs index 4b35762cf3f2..f2b5f523e5ea 100644 --- a/src/options_types/compile_target.rs +++ b/src/options_types/compile_target.rs @@ -49,7 +49,7 @@ impl Default for CompileTarget { } #[repr(u8)] -#[derive(Clone, Copy, PartialEq, Eq, strum::IntoStaticStr)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::IntoStaticStr)] pub enum Libc { /// The default libc for the target /// "glibc" for linux, unspecified for other OSes @@ -77,6 +77,15 @@ impl fmt::Display for Libc { } } +bun_core::comptime_string_map! { + /// `--target` segments that pick a libc; only meaningful together with linux. + static LIBC_NAMES: Libc = { + b"glibc" => Libc::Default, + b"musl" => Libc::Musl, + b"android" => Libc::Android, + }; +} + struct BaselineFormatter { baseline: bool, } @@ -90,12 +99,50 @@ impl fmt::Display for BaselineFormatter { } } -#[derive(thiserror::Error, Debug, strum::IntoStaticStr)] -pub enum ParseError { - #[error("UnsupportedTarget")] - UnsupportedTarget, - #[error("InvalidTarget")] - InvalidTarget, +/// Why a `--target` string was rejected. `Display` is the CLI error message. +#[derive(thiserror::Error, Debug, Clone, Copy)] +pub enum ParseError<'a> { + /// `segment` is not an architecture, OS, CPU tier, libc or version. `input` is the string + /// that was parsed (everything after `bun`), echoed back in the message. + UnsupportedSegment { + segment: &'a [u8], + input: &'a [u8], + }, + /// A `-vX.Y.Z` segment missing one of its three components. + IncompleteVersion, + /// A libc segment was combined with a non-linux OS. + LibcRequiresLinux(Libc), + Wasm, +} + +impl fmt::Display for ParseError<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + ParseError::UnsupportedSegment { segment, input } => write!( + f, + "Unsupported target {} in \"bun{}\"\n\ + To see the supported targets:\n \ + https://bun.com/docs/bundler/executables", + bun_fmt::quote(segment), + bstr::BStr::new(input), + ), + ParseError::IncompleteVersion => write!( + f, + "Please pass a complete version number to --target. For example, --target=bun-v{}", + Environment::VERSION_STRING, + ), + ParseError::LibcRequiresLinux(Libc::Default) => { + f.write_str("invalid target, glibc only exists on linux") + } + ParseError::LibcRequiresLinux(Libc::Musl) => { + f.write_str("invalid target, musl libc only exists on linux") + } + ParseError::LibcRequiresLinux(Libc::Android) => f.write_str( + "invalid target, android only exists with linux (use bun-linux-arm64-android)", + ), + ParseError::Wasm => f.write_str("invalid target, WebAssembly is not supported. Sorry!"), + } + } } impl CompileTarget { @@ -238,7 +285,7 @@ impl CompileTarget { } } - pub fn try_from(input_: &[u8]) -> Result { + pub fn try_from(input_: &[u8]) -> Result> { let mut this = CompileTarget::default(); let input = strings::trim(input_, b" \t\r"); if input.is_empty() { @@ -284,7 +331,7 @@ impl CompileTarget { || version.version.minor.is_none() || version.version.patch.is_none() { - return Err(ParseError::InvalidTarget); + return Err(ParseError::IncompleteVersion); } this.version = Version { @@ -297,20 +344,15 @@ impl CompileTarget { _found_version = true; continue; } - } else if token == b"glibc" { - this.libc = Libc::Default; - found_libc = true; - continue; - } else if token == b"musl" { - this.libc = Libc::Musl; - found_libc = true; - continue; - } else if token == b"android" { - this.libc = Libc::Android; + } else if let Some(libc) = LIBC_NAMES.get(token) { + this.libc = *libc; found_libc = true; continue; } else { - return Err(ParseError::UnsupportedTarget); + return Err(ParseError::UnsupportedSegment { + segment: token, + input: input_, + }); } } @@ -334,11 +376,11 @@ impl CompileTarget { // Not `libc != Default`: an explicit "glibc" parses to `Default` and is just as invalid off linux. if found_libc && this.os != OperatingSystem::Linux { - return Err(ParseError::InvalidTarget); + return Err(ParseError::LibcRequiresLinux(this.libc)); } if this.arch == Architecture::Wasm || this.os == OperatingSystem::Wasm { - return Err(ParseError::InvalidTarget); + return Err(ParseError::Wasm); } Ok(this) @@ -347,65 +389,8 @@ impl CompileTarget { pub fn from(input_: &[u8]) -> CompileTarget { match Self::try_from(input_) { Ok(t) => t, - Err(ParseError::UnsupportedTarget) => { - let input = strings::trim(input_, b" \t\r"); - let mut splitter = strings::split(input, b"-"); - let mut unsupported_token: Option<&[u8]> = None; - while let Some(token) = splitter.next() { - if token.is_empty() { - continue; - } - if ARCHITECTURE_NAMES.get(token).is_none() - && OPERATING_SYSTEM_NAMES.get(token).is_none() - && token != b"modern" - && token != b"baseline" - && token != b"glibc" - && token != b"musl" - && token != b"android" - && !(strings::has_prefix(token, b"v1.") - || strings::has_prefix(token, b"v0.")) - { - unsupported_token = Some(token); - break; - } - } - - if let Some(token) = unsupported_token { - bun_core::err_generic!( - "Unsupported target {} in \"bun{}\"\n\ - To see the supported targets:\n \ - https://bun.com/docs/bundler/executables", - bun_fmt::quote(token), - bstr::BStr::new(input_), - ); - } else { - bun_core::err_generic!("Unsupported target: {}", bstr::BStr::new(input_)); - } - Global::exit(1); - } - Err(ParseError::InvalidTarget) => { - let input = strings::trim(input_, b" \t\r"); - if strings::contains(input, b"musl") && !strings::contains(input, b"linux") { - bun_core::err_generic!("invalid target, musl libc only exists on linux"); - } else if strings::contains(input, b"glibc") && !strings::contains(input, b"linux") - { - bun_core::err_generic!("invalid target, glibc only exists on linux"); - } else if strings::contains(input, b"android") - && !strings::contains(input, b"linux") - { - bun_core::err_generic!( - "invalid target, android only exists with linux (use bun-linux-arm64-android)" - ); - } else if strings::contains(input, b"wasm") { - bun_core::err_generic!("invalid target, WebAssembly is not supported. Sorry!"); - } else if strings::contains(input, b"v") { - bun_core::err_generic!( - "Please pass a complete version number to --target. For example, --target=bun-v{}", - Environment::VERSION_STRING, - ); - } else { - bun_core::err_generic!("Invalid target: {}", bstr::BStr::new(input_)); - } + Err(err) => { + bun_core::err_generic!("{}", err); Global::exit(1); } } diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index 8344701b34cf..7b915dc2e521 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -122,73 +122,100 @@ describe("Bun.build compile", () => { }); }); -// `Bun.Build.Libc` is "glibc" | "musl". "-glibc" explicitly selects the default Linux build, which on -// a glibc host is the running bun itself, so the accepting tests below never download anything. -describe("compile target -glibc token", () => { +// `Bun.Build.Libc` is "glibc" | "musl". "-glibc" selects the glibc build of bun, which on a glibc host +// is the running binary itself, so the accepting tests below never download anything. +describe("compile target libc segments", () => { const arch = isArm64 ? "arm64" : "x64"; + // Targets are named `-vX.Y.Z`; Bun.version additionally carries `-debug` / `-canary.N` suffixes. + const version = Bun.version.split("-")[0]; + // Tests that actually compile copy and rewrite the whole bun binary (~1GB under debug+ASAN), which + // blows the 5s default; the parse-only tests below stay on the default. + const compileTimeout = 60_000; + + test.skipIf(!isGlibc)( + "Bun.build accepts a bun-linux--glibc target", + async () => { + using dir = tempDir("build-compile-glibc-api", { + "app.js": `console.log("glibc-target-ok");`, + }); - test.concurrent.skipIf(!isGlibc)("Bun.build accepts a bun-linux--glibc target", async () => { - using dir = tempDir("build-compile-glibc-api", { - "app.js": `console.log("glibc-target-ok");`, - }); - - const result = await Bun.build({ - entrypoints: [join(String(dir), "app.js")], - compile: { - target: `bun-linux-${arch}-glibc`, - outfile: join(String(dir), "app"), - }, - }); - expect(result.success).toBe(true); + const result = await Bun.build({ + entrypoints: [join(String(dir), "app.js")], + compile: { + target: `bun-linux-${arch}-glibc`, + outfile: join(String(dir), "app"), + }, + }); + expect(result.success).toBe(true); - await using proc = Bun.spawn({ - cmd: [result.outputs[0].path], - env: bunEnv, - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect({ stdout, stderr, exitCode }).toEqual({ stdout: "glibc-target-ok\n", stderr: "", exitCode: 0 }); - }); + await using proc = Bun.spawn({ + cmd: [result.outputs[0].path], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ stdout: "glibc-target-ok\n", stderr: "", exitCode: 0 }); + }, + compileTimeout, + ); - test.concurrent.skipIf(!isGlibc)("bun build --compile accepts --target=bun-linux--glibc", async () => { - using dir = tempDir("build-compile-glibc-cli", { - "app.js": `console.log("glibc-target-ok");`, - }); - const outfile = join(String(dir), "app"); + test.skipIf(!isGlibc)( + "bun build --compile accepts --target=bun-linux--glibc", + async () => { + using dir = tempDir("build-compile-glibc-cli", { + "app.js": `console.log("glibc-target-ok");`, + }); + const outfile = join(String(dir), "app"); - await using build = Bun.spawn({ - cmd: [bunExe(), "build", "--compile", `--target=bun-linux-${arch}-glibc`, "app.js", "--outfile", outfile], - env: bunEnv, - cwd: String(dir), - stdout: "pipe", - stderr: "pipe", - }); - const [, buildStderr, buildExitCode] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]); - expect({ stderr: buildStderr, exitCode: buildExitCode }).toEqual({ stderr: "", exitCode: 0 }); + await using build = Bun.spawn({ + cmd: [bunExe(), "build", "--compile", `--target=bun-linux-${arch}-glibc`, "app.js", "--outfile", outfile], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [, buildStderr, buildExitCode] = await Promise.all([ + build.stdout.text(), + build.stderr.text(), + build.exited, + ]); + expect({ stderr: buildStderr, exitCode: buildExitCode }).toEqual({ stderr: "", exitCode: 0 }); - await using proc = Bun.spawn({ - cmd: [outfile], - env: bunEnv, - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect({ stdout, stderr, exitCode }).toEqual({ stdout: "glibc-target-ok\n", stderr: "", exitCode: 0 }); - }); + await using proc = Bun.spawn({ + cmd: [outfile], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ stdout: "glibc-target-ok\n", stderr: "", exitCode: 0 }); + }, + compileTimeout, + ); - // Rejected while parsing the target, on every host: libc is a Linux-only axis, and a recognized - // "glibc" must not hide which segment of an otherwise broken target is the unknown one. + // Rejected while parsing the target, on every host. The message has to describe what the parser + // actually tripped on, not whichever libc word happens to appear somewhere in the target. test.concurrent.each([ ["bun-windows-x64-glibc", "error: invalid target, glibc only exists on linux\n"], + ["bun-windows-x64-musl", "error: invalid target, musl libc only exists on linux\n"], + [ + "bun-darwin-arm64-android", + "error: invalid target, android only exists with linux (use bun-linux-arm64-android)\n", + ], [ "bun-linux-x64-glibc-invalid", 'error: Unsupported target "invalid" in "bun-linux-x64-glibc-invalid"\n' + "To see the supported targets:\n" + " https://bun.com/docs/bundler/executables\n", ], + [ + "bun-x64-glibc-v1.2", + `error: Please pass a complete version number to --target. For example, --target=bun-v${version}\n`, + ], + ["bun-wasm", "error: invalid target, WebAssembly is not supported. Sorry!\n"], ])("bun build --compile rejects --target=%s", async (target, expectedStderr) => { - using dir = tempDir("build-compile-glibc-cli-invalid", { + using dir = tempDir("build-compile-target-invalid", { "app.js": `console.log("unreachable");`, }); @@ -209,7 +236,7 @@ describe("compile target -glibc token", () => { }); test.each(["bun-darwin-arm64-glibc", "bun-linux-x64-glibc-invalid"])("Bun.build rejects target %s", target => { - using dir = tempDir("build-compile-glibc-api-invalid", { + using dir = tempDir("build-compile-target-invalid-api", { "app.js": `console.log("unreachable");`, }); @@ -223,6 +250,67 @@ describe("compile target -glibc token", () => { }), ).toThrow(new TypeError(`Unknown compile target: ${target}`)); }); + + // Which libc a target resolved to is only visible from a bun built against the other one: the + // running bun's own libc is satisfied by the running binary, anything else has to be fetched. + // The fetch is pointed at a local 404 server, so instead of downloading, the build fails naming + // the bun it resolved to. + describe.skipIf(!isLinux)("libc selection", () => { + const npmArch = isArm64 ? "aarch64" : "x64"; + const [otherLibc, otherSuffix] = isMusl ? ["glibc", ""] : ["musl", "-musl"]; + + async function compileFor(target: string) { + using dir = tempDir("build-compile-libc-selection", { + "app.js": `console.log("libc-selection");`, + }); + let fetches = 0; + await using server = Bun.serve({ + port: 0, + hostname: "127.0.0.1", + fetch() { + fetches++; + return new Response(null, { status: 404 }); + }, + }); + + await using build = Bun.spawn({ + cmd: [bunExe(), "build", "--compile", `--target=${target}`, "app.js", "--outfile", "app"], + env: { + ...bunEnv, + BUN_COMPILE_TARGET_TARBALL_URL: server.url.href, + BUN_INSTALL_CACHE_DIR: join(String(dir), "cache"), + // A CI proxy must not sit between the build and the local server. + HTTP_PROXY: undefined, + http_proxy: undefined, + HTTPS_PROXY: undefined, + https_proxy: undefined, + }, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [, stderr, exitCode] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]); + return { stderr, exitCode, fetches }; + } + + test( + "a Linux target without a libc segment is the running bun's libc", + async () => { + expect(await compileFor(`bun-linux-${arch}`)).toEqual({ stderr: "", exitCode: 0, fetches: 0 }); + }, + compileTimeout, + ); + + test("a libc segment for the other libc selects that build instead of the running bun", async () => { + expect(await compileFor(`bun-linux-${arch}-${otherLibc}`)).toEqual({ + stderr: + `Target platform 'bun-linux-${npmArch}${otherSuffix}-v${version}' is not available for download. ` + + "Check if this version of Bun supports this target.\n", + exitCode: 1, + fetches: 1, + }); + }); + }); }); describe("compiled binary validity", () => { From b0ae4fde392850c93853be58528de1685d4af6ce Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:28:51 +0000 Subject: [PATCH 4/6] compile_target: shorten the UnsupportedSegment doc comment --- src/options_types/compile_target.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/options_types/compile_target.rs b/src/options_types/compile_target.rs index f2b5f523e5ea..09b2d4815d96 100644 --- a/src/options_types/compile_target.rs +++ b/src/options_types/compile_target.rs @@ -102,8 +102,7 @@ impl fmt::Display for BaselineFormatter { /// Why a `--target` string was rejected. `Display` is the CLI error message. #[derive(thiserror::Error, Debug, Clone, Copy)] pub enum ParseError<'a> { - /// `segment` is not an architecture, OS, CPU tier, libc or version. `input` is the string - /// that was parsed (everything after `bun`), echoed back in the message. + /// `segment` of `input` is not an architecture, OS, CPU tier, libc or version. UnsupportedSegment { segment: &'a [u8], input: &'a [u8], From ccdfd626f17cfdad6aba4e632186f46a15e328ec Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:38:29 +0000 Subject: [PATCH 5/6] test: assert the build stage summary of successful compiles; docs: libc segment list is not exhaustive --- docs/bundler/executables.mdx | 2 +- test/bundler/bun-build-compile.test.ts | 30 +++++++++++++++++++------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/docs/bundler/executables.mdx b/docs/bundler/executables.mdx index ee33b8af8b1c..3a2f5ddcb59e 100644 --- a/docs/bundler/executables.mdx +++ b/docs/bundler/executables.mdx @@ -228,7 +228,7 @@ To build for macOS x64: The segments of the `--target` value can appear in any order, as long as they're delimited by `-`. -Linux targets also accept a libc segment, `-glibc` or `-musl` (for example `bun-linux-x64-glibc`), which selects that libc's build of Bun no matter which build is running. Without one, a Linux target uses the libc of the `bun` running the build: the table below describes a glibc build of Bun, while a musl build of Bun turns `bun-linux-x64` into a musl executable. +Linux targets also accept a libc segment such as `-glibc` or `-musl` (for example `bun-linux-x64-glibc`), which selects that libc's build of Bun no matter which build is running. Without one, a Linux target uses the libc of the `bun` running the build: the table below describes a glibc build of Bun, while a musl build of Bun turns `bun-linux-x64` into a musl executable. | --target | Operating System | Architecture | Modern | Baseline | Libc | | -------------------- | ---------------- | ------------ | ------ | -------- | ----- | diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index 7b915dc2e521..7d8a359b781b 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -131,6 +131,11 @@ describe("compile target libc segments", () => { // Tests that actually compile copy and rewrite the whole bun binary (~1GB under debug+ASAN), which // blows the 5s default; the parse-only tests below stay on the default. const compileTimeout = 60_000; + // `bun build --compile` prints one timed line per stage; only the timings vary. A cross-compile + // additionally appends the resolved target to the compile line, so these summaries also assert + // that the target was the running binary. + const stages = (stdout: string) => stdout.replace(/^\s*\[[\d.]+m?s\] +/gm, ""); + const bundledAndCompiled = "bundle 1 modules\ncompile app\n"; test.skipIf(!isGlibc)( "Bun.build accepts a bun-linux--glibc target", @@ -166,24 +171,27 @@ describe("compile target libc segments", () => { using dir = tempDir("build-compile-glibc-cli", { "app.js": `console.log("glibc-target-ok");`, }); - const outfile = join(String(dir), "app"); await using build = Bun.spawn({ - cmd: [bunExe(), "build", "--compile", `--target=bun-linux-${arch}-glibc`, "app.js", "--outfile", outfile], + cmd: [bunExe(), "build", "--compile", `--target=bun-linux-${arch}-glibc`, "app.js", "--outfile", "app"], env: bunEnv, cwd: String(dir), stdout: "pipe", stderr: "pipe", }); - const [, buildStderr, buildExitCode] = await Promise.all([ + const [buildStdout, buildStderr, buildExitCode] = await Promise.all([ build.stdout.text(), build.stderr.text(), build.exited, ]); - expect({ stderr: buildStderr, exitCode: buildExitCode }).toEqual({ stderr: "", exitCode: 0 }); + expect({ stdout: stages(buildStdout), stderr: buildStderr, exitCode: buildExitCode }).toEqual({ + stdout: bundledAndCompiled, + stderr: "", + exitCode: 0, + }); await using proc = Bun.spawn({ - cmd: [outfile], + cmd: [join(String(dir), "app")], env: bunEnv, stdout: "pipe", stderr: "pipe", @@ -289,20 +297,26 @@ describe("compile target libc segments", () => { stdout: "pipe", stderr: "pipe", }); - const [, stderr, exitCode] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]); - return { stderr, exitCode, fetches }; + const [stdout, stderr, exitCode] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]); + return { stdout: stages(stdout), stderr, exitCode, fetches }; } test( "a Linux target without a libc segment is the running bun's libc", async () => { - expect(await compileFor(`bun-linux-${arch}`)).toEqual({ stderr: "", exitCode: 0, fetches: 0 }); + expect(await compileFor(`bun-linux-${arch}`)).toEqual({ + stdout: bundledAndCompiled, + stderr: "", + exitCode: 0, + fetches: 0, + }); }, compileTimeout, ); test("a libc segment for the other libc selects that build instead of the running bun", async () => { expect(await compileFor(`bun-linux-${arch}-${otherLibc}`)).toEqual({ + stdout: "bundle 1 modules\n", stderr: `Target platform 'bun-linux-${npmArch}${otherSuffix}-v${version}' is not available for download. ` + "Check if this version of Bun supports this target.\n", From fda5879f359e92218bee661631d45280264397b9 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:57:22 +0000 Subject: [PATCH 6/6] docs: list the glibc compile targets next to the musl ones --- docs/bundler/executables.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/bundler/executables.mdx b/docs/bundler/executables.mdx index 3a2f5ddcb59e..224a54763490 100644 --- a/docs/bundler/executables.mdx +++ b/docs/bundler/executables.mdx @@ -1284,6 +1284,8 @@ type CompileTarget = | "bun-linux-x64-baseline" | "bun-linux-x64-modern" | "bun-linux-arm64" + | "bun-linux-x64-glibc" + | "bun-linux-arm64-glibc" | "bun-linux-x64-musl" | "bun-linux-arm64-musl" | "bun-windows-x64"