Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const BunBuildOptions = struct {
/// `./build/codegen` or equivalent
codegen_path: []const u8,
no_llvm: bool,
lto: bool,
override_no_export_cpp_apis: bool,

cached_options_module: ?*Module = null,
Expand Down Expand Up @@ -201,6 +202,7 @@ pub fn build(b: *Build) !void {
const obj_format = b.option(ObjectFormat, "obj_format", "Output file for object files") orelse .obj;

const no_llvm = b.option(bool, "no_llvm", "Experiment with Zig self hosted backends. No stability guaranteed") orelse false;
const lto = b.option(bool, "lto", "Emit LLVM bitcode for full LTO instead of a native object") orelse false;
const override_no_export_cpp_apis = b.option(bool, "override-no-export-cpp-apis", "Override the default export_cpp_apis logic to disable exports") orelse false;

var build_options = BunBuildOptions{
Expand All @@ -211,6 +213,7 @@ pub fn build(b: *Build) !void {
.codegen_path = codegen_path,
.codegen_embed = codegen_embed,
.no_llvm = no_llvm,
.lto = lto,
.override_no_export_cpp_apis = override_no_export_cpp_apis,
.version = try Version.parse(bun_version),
.canary_revision = canary: {
Expand Down Expand Up @@ -640,6 +643,7 @@ fn addMultiCheck(
.reported_nodejs_version = root_build_options.reported_nodejs_version,
.codegen_path = root_build_options.codegen_path,
.no_llvm = root_build_options.no_llvm,
.lto = false,
.enable_asan = root_build_options.enable_asan,
.enable_valgrind = root_build_options.enable_valgrind,
.enable_tinycc = root_build_options.enable_tinycc,
Expand Down Expand Up @@ -751,6 +755,10 @@ fn configureObj(b: *Build, opts: *BunBuildOptions, obj: *Compile) void {
// Object options
obj.use_llvm = !opts.no_llvm;
obj.use_lld = if (opts.os == .mac or opts.os == .linux) false else !opts.no_llvm;
if (opts.lto) {
obj.lto = .full;
obj.use_lld = true;
Comment on lines 756 to +760

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Does Zig support obj.lto = .fullwhenuse_llvm = false / the self-hosted backend is enabled?

💡 Result:

No, Zig does not support obj.lto = .full when use_llvm = false / the self-hosted backend is enabled.

Citations:


🏁 Script executed:

# Find the build.zig file and check the relevant sections
fd -t f "build.zig" | head -1 | xargs wc -l

Repository: oven-sh/bun

Length of output: 69


🏁 Script executed:

# Check the BunBuildOptions struct and flag parsing
grep -n "no_llvm\|\.lto" build.zig | head -30

Repository: oven-sh/bun

Length of output: 869


🏁 Script executed:

# Check how opts is initialized and where defaults are set
sed -n '50,70p' build.zig
sed -n '200,220p' build.zig
sed -n '640,660p' build.zig

Repository: oven-sh/bun

Length of output: 2849


🏁 Script executed:

# Check if there are existing validation/guard patterns elsewhere in build.zig
rg "const fail_step|addFail|@panic" build.zig -A 2 -B 2 | head -50

Repository: oven-sh/bun

Length of output: 1783


Reject -Dno_llvm=true together with -Dlto=true.

Full LTO is an LLVM-only path, but the code still allows both flags to be set independently. When both are true, obj.use_llvm becomes false while obj.lto = .full is set, which Zig does not support. Add a guard to fail fast instead of producing confusing Zig errors.

Suggested fix
     obj.use_llvm = !opts.no_llvm;
     obj.use_lld = if (opts.os == .mac or opts.os == .linux) false else !opts.no_llvm;
     if (opts.lto) {
+        if (opts.no_llvm) {
+            const fail_step = b.addFail("LTO requires the LLVM backend");
+            obj.step.dependOn(&fail_step.step);
+            return;
+        }
         obj.lto = .full;
         obj.use_lld = true;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
obj.use_llvm = !opts.no_llvm;
obj.use_lld = if (opts.os == .mac or opts.os == .linux) false else !opts.no_llvm;
if (opts.lto) {
obj.lto = .full;
obj.use_lld = true;
obj.use_llvm = !opts.no_llvm;
obj.use_lld = if (opts.os == .mac or opts.os == .linux) false else !opts.no_llvm;
if (opts.lto) {
if (opts.no_llvm) {
const fail_step = b.addFail("LTO requires the LLVM backend");
obj.step.dependOn(&fail_step.step);
return;
}
obj.lto = .full;
obj.use_lld = true;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@build.zig` around lines 756 - 760, The build currently allows
opts.no_llvm=true with opts.lto=true leading to obj.use_llvm = false while
obj.lto = .full (an unsupported Zig state); add a guard early after evaluating
opts to detect when opts.no_llvm and opts.lto are both true and fail fast with a
clear error message. Modify the logic around obj.use_llvm / obj.lto (the block
setting obj.use_llvm = !opts.no_llvm; obj.lto = .full; obj.use_lld = true) to
check the conflicting flags first and call a build-failure routine (or
std.debug.panic with a descriptive message) if both are set, so we never set
obj.lto when obj.use_llvm is false.

}

if (@hasField(std.meta.Child(@TypeOf(obj)), "llvm_codegen_threads"))
obj.llvm_codegen_threads = opts.llvm_codegen_threads orelse 0;
Expand Down
8 changes: 4 additions & 4 deletions scripts/build/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { WEBKIT_VERSION } from "./deps/webkit.ts";
import { BuildError, assert } from "./error.ts";
import { clangTargetArch } from "./tools.ts";
import { cyan, dim, green } from "./tty.ts";
import { defaultZigCommit } from "./zig.ts";
import { ZIG_COMMIT } from "./zig.ts";

export type OS = "linux" | "darwin" | "windows";
export type Arch = "x64" | "aarch64";
Expand Down Expand Up @@ -51,7 +51,7 @@ export interface Host {
const versionDefaults = {
nodejsVersion: NODEJS_VERSION,
nodejsAbiVersion: NODEJS_ABI_VERSION,
// zigCommit's default varies by host OS — see defaultZigCommit() in zig.ts.
zigCommit: ZIG_COMMIT,
webkitVersion: WEBKIT_VERSION,
};

Expand Down Expand Up @@ -475,7 +475,7 @@ export function resolveConfig(partial: PartialConfig, toolchain: Toolchain): Con
// to test a branch before bumping the pinned default.
const nodejsVersion = partial.nodejsVersion ?? versionDefaults.nodejsVersion;
const nodejsAbiVersion = partial.nodejsAbiVersion ?? versionDefaults.nodejsAbiVersion;
const zigCommit = partial.zigCommit ?? defaultZigCommit(host.os);
const zigCommit = partial.zigCommit ?? versionDefaults.zigCommit;
const webkitVersion = partial.webkitVersion ?? versionDefaults.webkitVersion;

// ─── macOS SDK ───
Expand Down Expand Up @@ -803,7 +803,7 @@ export function formatConfig(cfg: Config, exe: string): string {
// revert my WebKit test branch" before the build goes weird.
if (cfg.webkitVersion !== versionDefaults.webkitVersion)
features.push(`webkit-version:${cfg.webkitVersion.slice(0, 10)}`);
if (cfg.zigCommit !== defaultZigCommit(cfg.host.os)) features.push(`zig-commit:${cfg.zigCommit.slice(0, 10)}`);
if (cfg.zigCommit !== versionDefaults.zigCommit) features.push(`zig-commit:${cfg.zigCommit.slice(0, 10)}`);
if (cfg.nodejsVersion !== versionDefaults.nodejsVersion) features.push(`nodejs:${cfg.nodejsVersion}`);
lines.push(` ${label("features")} ${features.length > 0 ? c.cyan(features.join(", ")) : c.dim("(none)")}`);
return lines.join("\n");
Expand Down
41 changes: 7 additions & 34 deletions scripts/build/zig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import { mkdir, readdir, rename, rm, writeFile } from "node:fs/promises";
import { availableParallelism, homedir } from "node:os";
import { join, resolve } from "node:path";
import type { Config, OS } from "./config.ts";
import type { Config } from "./config.ts";
import { downloadWithRetry, extractZip } from "./download.ts";
import { assert } from "./error.ts";
import { fetchCliPath } from "./fetch-cli.ts";
Expand All @@ -31,38 +31,8 @@
* Zig compiler commit — determines compiler download + bundled stdlib.
* Override via `--zig-commit=<hash>` to test a new compiler.
* From https://github.com/oven-sh/zig releases.
*
* TEMPORARY SPLIT: ZIG_COMMIT is the pre-parallel-sema compiler, kept
* for Windows hosts only (COFF shard emission isn't implemented and
* the build path needs a single object). Everything else — local and
* CI, all targets — uses ZIG_COMMIT_PARALLEL (parallel sema is
* deterministic; codegen-unit count is decided separately by
* codegenThreads()). Once Windows is supported, collapse both back to
* one constant.
*/
export const ZIG_COMMIT = "365343af4fc5a1a632e6b54aadd0b87be30edd81";
export const ZIG_COMMIT_PARALLEL = "0bcf4c3d998133e724d27e9fd783172ffed4c943";

/**
* The one place that picks which compiler to use. The parallel compiler
* is used everywhere except Windows (its sharded-codegen object emission
* for COFF is unimplemented). Parallel SEMA is deterministic and changes
* no output, so CI gets it too — only the codegen-unit count differs by
* config (see codegenThreads()).
*/
export function defaultZigCommit(hostOs: OS): string {
if (hostOs === "windows") return ZIG_COMMIT;
return ZIG_COMMIT_PARALLEL;
}

/**
* True iff `cfg` is using the parallel-sema compiler. Gates
* ZIG_PARALLEL_SEMA and the `llvm_no_merge_shards` build.zig path —
* the stable compiler doesn't understand either.
*/
function usingParallelCompiler(cfg: Config): boolean {
return cfg.zigCommit !== ZIG_COMMIT;
}
export const ZIG_COMMIT = "04e7f6ac1e009525bc00934f20199c68f04e0a24";

/**
* Number of LLVM codegen units. >1 splits the build into N independent
Expand All @@ -73,15 +43,17 @@
* - Non-ASAN CI: shipped releases want full IPO; cg=1 keeps that and
* keeps the upload/download contract a single file.
* - Windows targets: COFF shard emission is unimplemented in oven-sh/zig.
* - LTO: zig_llvm.cpp gates SplitModule on !lto, so cg>1 would emit one
* .o instead of N and the no_merge_shards path would expect missing files.
*
* ASAN CI uses a FIXED count (CI_ASAN_CODEGEN_THREADS) so zig-only and
* link-only — which run on different machines — agree on the artifact
* names. Local builds shard at availableParallelism(); benchmark against
* a non-ASAN CI artifact if cross-unit inlining matters.
*/
function codegenThreads(cfg: Config): number {
if (!usingParallelCompiler(cfg)) return 0;
if (cfg.windows) return 1;
if (cfg.lto) return 1;
if (cfg.ci) {
// ASAN is a test-only build (not shipped), so cross-shard IPO loss is
// fine and the speedup is worth it. The count is FIXED so zig-only and
Expand Down Expand Up @@ -291,7 +263,7 @@
// our fork (upstream added Feb 2026, not backported).
const interleave = false;
const consoleMode = !interleave || hostWin;
const parallelSema = usingParallelCompiler(cfg) ? " --env=ZIG_PARALLEL_SEMA=1" : "";
const parallelSema = " --env=ZIG_PARALLEL_SEMA=1";
n.rule("zig_build", {
command: `${stream} ${consoleMode ? "--console" : "--zig-progress"} --env=ZIG_LOCAL_CACHE_DIR=$zig_local_cache --env=ZIG_GLOBAL_CACHE_DIR=$zig_global_cache${parallelSema} $zig build $step $args`,
description: "zig $step → $out",
Expand Down Expand Up @@ -467,6 +439,7 @@
`-Denable_fuzzilli=${bool(cfg.fuzzilli)}`,
`-Denable_valgrind=${bool(cfg.valgrind)}`,
`-Denable_tinycc=${bool(cfg.tinycc)}`,
`-Dlto=${bool(cfg.lto)}`,

Check warning on line 442 in scripts/build/zig.ts

View check run for this annotation

Claude / Claude Code Review

Stale comment references removed stable-compiler / return-0 logic

The comment at lines 446-448 still says "MUST be 0 on the stable compiler — see codegenThreads()", but this PR removed the stable/parallel compiler split and `codegenThreads()` no longer ever returns 0 (its minimum is now 1). Consider updating this comment to drop the stale "stable compiler" / "0" reference.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The comment at lines 446-448 still says "MUST be 0 on the stable compiler — see codegenThreads()", but this PR removed the stable/parallel compiler split and codegenThreads() no longer ever returns 0 (its minimum is now 1). Consider updating this comment to drop the stale "stable compiler" / "0" reference.

Extended reasoning...

What the issue is

The comment immediately following the new -Dlto=${bool(cfg.lto)} line in zigBuildArgs() reads:

// Sharded LLVM codegen — one shard per host core on the parallel
// compiler. Zig has no "auto" value (0 = single-threaded). MUST be 0
// on the stable compiler — see codegenThreads().
`-Dllvm_codegen_threads=${codegenThreads(cfg)}`,

This comment is now stale documentation that this PR's own changes orphaned.

Why it's stale

This PR collapsed the ZIG_COMMIT / ZIG_COMMIT_PARALLEL split into a single ZIG_COMMIT constant and deleted:

  • usingParallelCompiler()
  • defaultZigCommit()
  • the if (!usingParallelCompiler(cfg)) return 0; branch at the top of codegenThreads()

After these removals, codegenThreads() (lines 54-66) returns at minimum 1 — never 0. There is no longer a "stable compiler" vs "parallel compiler" distinction anywhere in the codebase, and the function the comment tells the reader to consult no longer contains any logic about 0 or a stable compiler.

Step-by-step proof

  1. Before this PR, codegenThreads() started with if (!usingParallelCompiler(cfg)) return 0; — the comment's "MUST be 0 on the stable compiler" pointed at that line.
  2. The diff removes that line: - if (!usingParallelCompiler(cfg)) return 0; and replaces it with nothing (the function now starts with if (cfg.windows) return 1;).
  3. The diff also removes the entire "TEMPORARY SPLIT" doc block, ZIG_COMMIT_PARALLEL, defaultZigCommit(), and usingParallelCompiler() — so "the stable compiler" no longer refers to anything that exists.
  4. The comment at 446-448 was not touched and still tells the reader "MUST be 0 on the stable compiler — see codegenThreads()". A reader following that pointer will find no such logic.

Impact

No behavioral impact — this is purely misleading documentation. A future reader trying to understand why -Dllvm_codegen_threads is set the way it is will be sent looking for a "stable compiler" branch that no longer exists.

How to fix

Update the comment to reflect the current state, e.g.:

// Sharded LLVM codegen — see codegenThreads() for when sharding is
// gated off (Windows, LTO, non-ASAN CI). Zig has no "auto" value.

Or simply drop the second sentence entirely, since codegenThreads()'s own doc comment now fully explains the gating.

// Always ON — bun uses mimalloc as its default allocator. The flag
// exists for experimentation; in practice it's never OFF.
`-Duse_mimalloc=true`,
Expand Down
Loading