From 5b0e92cbd21846599e7798bdec1fca8e51a6801e Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 15 May 2026 01:49:50 +0000 Subject: [PATCH 1/2] build: generate bun_core::build_options from Config instead of option_env handshake --- scripts/build/CLAUDE.md | 1 + scripts/build/buildOptionsRs.ts | 75 ++++++++++++++++++++++++++ scripts/build/codegen.ts | 8 +++ scripts/build/rust.ts | 19 ++----- src/bun_core/build.rs | 37 +++++++++++++ src/bun_core/lib.rs | 95 ++------------------------------- 6 files changed, 129 insertions(+), 106 deletions(-) create mode 100644 scripts/build/buildOptionsRs.ts create mode 100644 src/bun_core/build.rs diff --git a/scripts/build/CLAUDE.md b/scripts/build/CLAUDE.md index dfd1917e21a..655a1abcd31 100644 --- a/scripts/build/CLAUDE.md +++ b/scripts/build/CLAUDE.md @@ -193,6 +193,7 @@ Split CI modes: `rust-only` (lolhtml+codegen+cargo → libbun_rust.a), `cpp-only | `shims.ts` | Platform/toolchain workaround dylibs, `emitShims()` | | `workarounds.ts` | Self-obsoleting workaround registry, `checkWorkarounds()` | | `depVersionsHeader.ts` | Generates `bun_dependency_versions.h` for `process.versions` | +| `buildOptionsRs.ts` | Generates `build_options.rs` (`bun_core::build_options`) from `Config` | | `stream.ts` | Subprocess output wrapper — FD-3 sideband, prefixed line streaming | | `shell.ts` | `quote()`/`slash()` — shell escaping for ninja commands | | `fs.ts` | `writeIfChanged()`, `mkdirAll()` | diff --git a/scripts/build/buildOptionsRs.ts b/scripts/build/buildOptionsRs.ts new file mode 100644 index 00000000000..5e835f9f5f4 --- /dev/null +++ b/scripts/build/buildOptionsRs.ts @@ -0,0 +1,75 @@ +/** + * Generates `${codegenDir}/build_options.rs` — Rust constants derived from + * `Config`, `include!()`'d by `bun_core::build_options`. + * + * Replaces the `option_env!("BUN_*")` handshake (a dozen env vars exported + * by `rust.ts`, read back in `bun_core`). `Config` is now the single source + * of truth: literal `pub const`s land on disk, so a bare `cargo check` / + * rust-analyzer sees the real version/sha/paths instead of placeholder + * defaults, and the env-var name list isn't maintained in two places. + * + * `bun_core/build.rs` emits `rerun-if-changed` on the file; `writeIfChanged` + * keeps the mtime stable so a reconfigure with the same sha doesn't + * recompile `bun_core` and its dependents. + * + * Target-dependent constants (`ENABLE_TINYCC`, `ENABLE_ASAN`, `ENABLE_LOGS`) + * stay as `cfg!()` expressions inside the generated file rather than literals + * so a `cargo check --target ` against the same generated file + * still evaluates them per-target. + * + * Written at configure time alongside `depVersionsHeader.ts` / + * `cargo-config.ts` — it's a constant manifest, not a build edge. + */ + +import { mkdirSync } from "node:fs"; +import { resolve } from "node:path"; +import type { Config } from "./config.ts"; +import { writeIfChanged } from "./fs.ts"; + +/** Rust string literal for `s`. JSON escaping is a strict subset of Rust's. */ +const rstr = (s: string): string => JSON.stringify(s); +/** Rust byte-string literal for `s` (the `&[u8]` constants). */ +const rbstr = (s: string): string => `b${JSON.stringify(s)}`; + +export function generateBuildOptionsRs(cfg: Config): string { + const outPath = resolve(cfg.codegenDir, "build_options.rs"); + const [major, minor, patch] = cfg.version.split("."); + + const lines: string[] = [ + "// Generated by scripts/build/buildOptionsRs.ts from `Config` at configure", + "// time. Do not edit. Regenerate with `bun bd`.", + "", + `pub const SHA: &str = ${rstr(cfg.revision)};`, + `pub const REPORTED_NODEJS_VERSION: &str = ${rstr(cfg.nodejsVersion)};`, + `pub const RELEASE_SAFE: bool = ${cfg.assertions};`, + `pub const BASELINE: bool = ${cfg.baseline};`, + `pub const IS_CANARY: bool = ${cfg.canary};`, + `pub const CANARY_REVISION: &str = ${rstr(cfg.canaryRevision)};`, + `pub const ENABLE_FUZZILLI: bool = ${cfg.fuzzilli};`, + `pub const FALLBACK_HTML_VERSION: &str = "0000000000000000";`, + "pub const VERSION: crate::Version = crate::Version {", + ` major: ${Number(major)},`, + ` minor: ${Number(minor)},`, + ` patch: ${Number(patch)},`, + "};", + `pub const BASE_PATH: &[u8] = ${rbstr(cfg.cwd)};`, + `pub const CODEGEN_PATH: &[u8] = ${rbstr(cfg.codegenDir)};`, + "", + "// Target/profile-derived — kept as `cfg!()` so cross-target", + "// `cargo check` evaluates per-triple. Values agree with `Config`:", + "// rust.ts sets `--cfg=bun_asan` ⇔ `cfg.asan`, and `cfg.tinycc`'s", + "// 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 ENABLE_TINYCC: bool = !cfg!(any(", + ` all(windows, target_arch = "aarch64"),`, + ` target_os = "android",`, + ` target_os = "freebsd",`, + "));", + "", + ]; + + mkdirSync(cfg.codegenDir, { recursive: true }); + writeIfChanged(outPath, lines.join("\n")); + return outPath; +} diff --git a/scripts/build/codegen.ts b/scripts/build/codegen.ts index ece1c8f0c01..4d91934ca35 100644 --- a/scripts/build/codegen.ts +++ b/scripts/build/codegen.ts @@ -38,6 +38,7 @@ import { spawnSync } from "node:child_process"; import { mkdirSync, readFileSync } from "node:fs"; import { basename, dirname, relative, resolve } from "node:path"; import type { Sources } from "../glob-sources.ts"; +import { generateBuildOptionsRs } from "./buildOptionsRs.ts"; import type { Config } from "./config.ts"; import { BuildError, assert } from "./error.ts"; import { writeIfChanged } from "./fs.ts"; @@ -285,6 +286,13 @@ export function emitCodegen(n: Ninja, cfg: Config, sources: Sources): CodegenOut const ctx: Ctx = { n, cfg, sources, o, dirStamp }; + // Configure-time write (not a ninja edge — it's a constant manifest like + // depVersionsHeader). Pushed into rustInputs so the cargo edge implicit-deps + // on it; bun_core/build.rs emits the matching `rerun-if-changed`. + const buildOptionsRs = generateBuildOptionsRs(cfg); + o.all.push(buildOptionsRs); + o.rustInputs.push(buildOptionsRs); + emitBunError(ctx); emitStringMaps(ctx); emitFallbackDecoder(ctx); diff --git a/scripts/build/rust.ts b/scripts/build/rust.ts index e02b5922130..7fb47e357ab 100644 --- a/scripts/build/rust.ts +++ b/scripts/build/rust.ts @@ -522,24 +522,11 @@ export function emitRust(n: Ninja, cfg: Config, inputs: RustBuildInputs): string // `include!(concat!(env!("BUN_CODEGEN_DIR"), "/generated_*.rs"))` and // `include_bytes!` in `bun_js_parser`/`bun_runtime` resolve against this. // Set in cargo's env so it reaches every crate's `rustc` invocation - // (not just those with a `build.rs` re-export). + // (not just those with a `build.rs` re-export). `bun_core::build_options` + // is also `include!()`'d from here — its values come from + // `buildOptionsRs.ts` (written at configure time), not env vars. BUN_CODEGEN_DIR: cfg.codegenDir, - // ── build_options (version / sha / feature flags) ── - // Read at compile time by `bun_core::build_options` via `option_env!`. - // Values come straight from `Config`, so `process.versions.bun` / - // `bun --revision` reflect the configured build. - BUN_GIT_SHA: cfg.revision, - BUN_VERSION_MAJOR: cfg.version.split(".")[0]!, - BUN_VERSION_MINOR: cfg.version.split(".")[1]!, - BUN_VERSION_PATCH: cfg.version.split(".")[2]!, - BUN_REPORTED_NODEJS_VERSION: cfg.nodejsVersion, - BUN_RELEASE_SAFE: String(cfg.assertions), - BUN_BASELINE: String(cfg.baseline), - BUN_IS_CANARY: String(cfg.canary), - BUN_CANARY_REVISION: String(cfg.canaryRevision ?? 0), - BUN_BASE_PATH: cfg.cwd, - // ── toolchain forwarding (cc-rs / build scripts) ── // build.rs of vendored crates (lol-html, anything using `cc`) and rustc's // own linker invocations must use the SAME clang/ar `tools.ts` resolved — diff --git a/src/bun_core/build.rs b/src/bun_core/build.rs new file mode 100644 index 00000000000..bc020b824d0 --- /dev/null +++ b/src/bun_core/build.rs @@ -0,0 +1,37 @@ +//! Export `BUN_CODEGEN_DIR` and fingerprint `build_options.rs` for +//! `include!(concat!(env!("BUN_CODEGEN_DIR"), "/build_options.rs"))`. +//! +//! `build_options.rs` is written at configure time by +//! `scripts/build/buildOptionsRs.ts` from the resolved `Config` (sha, +//! version, baseline, …). This script does NOT run the generator — it just +//! resolves the path and tells cargo to track the file so a sha/version +//! change recompiles `bun_core`. Mirrors `src/{jsc,runtime}/build.rs`. + +use std::env; +use std::path::{Path, PathBuf}; + +fn main() { + // src/bun_core → repo root is two up. + let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let repo = manifest + .parent() + .and_then(Path::parent) + .expect("repo root from CARGO_MANIFEST_DIR") + .to_path_buf(); + + let codegen_dir = env::var("BUN_CODEGEN_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| repo.join("build/debug/codegen")); + + let build_options = codegen_dir.join("build_options.rs"); + if !build_options.exists() { + panic!( + "build_options.rs not found at {} — run `bun bd --configure-only` first", + build_options.display() + ); + } + + println!("cargo:rustc-env=BUN_CODEGEN_DIR={}", codegen_dir.display()); + println!("cargo:rerun-if-changed={}", build_options.display()); + println!("cargo:rerun-if-env-changed=BUN_CODEGEN_DIR"); +} diff --git a/src/bun_core/lib.rs b/src/bun_core/lib.rs index 36afa4f114c..372930ff017 100644 --- a/src/bun_core/lib.rs +++ b/src/bun_core/lib.rs @@ -653,97 +653,12 @@ impl ErrnoNames { /// so `$crate::pretty_fmt!` resolves from the wrapper macros in `output.rs`. pub use bun_core_macros::{EnumTag, pretty_fmt}; -/// Stand-in for Zig's `@import("build_options")`. Real values are emitted by -/// `build.rs` via `env!()` in Phase C (link). Placeholder values let env.rs -/// const-evaluate cleanly. +/// Stand-in for Zig's `@import("build_options")`. Values are written at +/// configure time by `scripts/build/buildOptionsRs.ts` from the resolved +/// `Config` and `include!()`'d here; `build.rs` exports `BUN_CODEGEN_DIR` +/// and fingerprints the file so a sha/version change recompiles this crate. pub mod build_options { - /// `option_env!` with a fallback literal — same shape as Zig's - /// `b.option(...) orelse default` in build.zig. - macro_rules! build_opt { - ($name:literal, $default:expr) => { - match option_env!($name) { - Some(v) => v, - None => $default, - } - }; - } - macro_rules! build_opt_bool { - ($name:literal, $default:expr) => { - match option_env!($name) { - Some(v) => matches!(v.as_bytes(), b"true" | b"1"), - None => $default, - } - }; - } - - /// `true` for the `release-assertions` profile (Zig: ReleaseSafe). - pub const RELEASE_SAFE: bool = build_opt_bool!("BUN_RELEASE_SAFE", false); - pub const REPORTED_NODEJS_VERSION: &str = build_opt!("BUN_REPORTED_NODEJS_VERSION", "24.0.0"); - pub const BASELINE: bool = build_opt_bool!("BUN_BASELINE", false); - pub const SHA: &str = build_opt!("BUN_GIT_SHA", "0000000000000000000000000000000000000000"); - pub const IS_CANARY: bool = build_opt_bool!("BUN_IS_CANARY", false); - pub const CANARY_REVISION: &str = build_opt!("BUN_CANARY_REVISION", "0"); - /// Repo root. Zig's build.zig passes `b.pathFromRoot(".")` (already - /// normalized, native separators) — there is *no* fallback in the spec. - /// `scripts/build/rust.ts` exports `BUN_BASE_PATH` for every build. - /// - /// The POSIX fallback derives it from this crate's manifest dir - /// (`/src/bun_core`) so a bare `cargo check` still works for - /// `runtime_embed_file!` (which goes through `PathBuf`, so the OS resolves - /// `..`). On Windows that fallback is *wrong*: `CARGO_MANIFEST_DIR` is - /// backslash-separated and concatenating `/../..` yields a mixed-separator, - /// unnormalized path that crash_handler's byte-wise `starts_with` (which - /// appends `SEP_STR` and compares against debug-info file paths) can never - /// match — so require the env var there, matching the Zig contract. - pub const BASE_PATH: &[u8] = match option_env!("BUN_BASE_PATH") { - Some(v) => v.as_bytes(), - // The fallback is correct on POSIX. On Windows it is mixed-separator - // + unnormalized and crash_handler's byte-wise `starts_with` will - // never match it — but real Windows builds always go through - // `scripts/build/rust.ts` (which sets the env var). Kept so that bare - // `cargo check --target *-windows-*` from a non-Windows host compiles. - None => concat!(env!("CARGO_MANIFEST_DIR"), "/../..").as_bytes(), - }; - pub const ENABLE_LOGS: bool = cfg!(debug_assertions); - pub const ENABLE_ASAN: bool = cfg!(bun_asan); - pub const ENABLE_FUZZILLI: bool = false; - /// Whether `libtcc.a` is built and linked. Mirrors `cfg.tinycc` in - /// `scripts/build/config.ts`: TinyCC is disabled on Windows/aarch64 - /// (TinyCC has no aarch64-pe-coff backend), Android, and FreeBSD (the - /// vendored fork doesn't support those targets and the dep is skipped). - /// Has to be a *compile-time* `false` on those targets — `ffi_body.rs` - /// gates its `bun_tcc_sys::*` calls behind `if !ENABLE_TINYCC { return }`, - /// and rustc only DCEs the `tcc_*` extern refs when the const folds; a - /// runtime check would still leave undefined symbols at link. - pub const ENABLE_TINYCC: bool = !cfg!(any( - all(windows, target_arch = "aarch64"), - target_os = "android", - target_os = "freebsd", - )); - /// `/codegen`. `scripts/build/rust.ts` exports `BUN_CODEGEN_DIR` to - /// every crate's rustc env. POSIX fallback for bare `cargo check`; on - /// Windows the `/../../` fallback is mixed-separator + unnormalized (see - /// `BASE_PATH` above), so require the env var there. - pub const CODEGEN_PATH: &[u8] = match option_env!("BUN_CODEGEN_DIR") { - Some(v) => v.as_bytes(), - // See BASE_PATH note re: Windows fallback being mixed-separator. Real - // Windows builds set the env var; this only fires for cross-target - // `cargo check`. - None => concat!(env!("CARGO_MANIFEST_DIR"), "/../../build/debug/codegen").as_bytes(), - }; - /// `cfg.version` from package.json, split by `scripts/build/rust.ts`. - pub const VERSION: crate::Version = crate::Version { - major: crate::const_parse_u32(build_opt!("BUN_VERSION_MAJOR", "1").as_bytes()), - minor: crate::const_parse_u32(build_opt!("BUN_VERSION_MINOR", "3").as_bytes()), - patch: crate::const_parse_u32(build_opt!("BUN_VERSION_PATCH", "0").as_bytes()), - }; - /// Zig: `build_options.fallback_html_version` — hex-string hash of the - /// fallback HTML bundle, injected by the build system. Placeholder until - /// Phase C wires the real value via `env!()` in `build.rs`. - pub const FALLBACK_HTML_VERSION: &str = match option_env!("BUN_FALLBACK_HTML_VERSION") { - Some(v) => v, - None => "0000000000000000", - }; + include!(concat!(env!("BUN_CODEGEN_DIR"), "/build_options.rs")); } // ── re-exports (the tier-0 surface downstream crates need) ──────────────── From ca6b0cbeeef1fad22af54c5daa63ad8c6c5796bf Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 15 May 2026 02:45:42 +0000 Subject: [PATCH 2/2] build_options.rs: emit paths via .as_bytes() for non-ASCII; drop orphaned const_parse_u32 --- scripts/build/buildOptionsRs.ts | 9 +++++++-- src/bun_core/util.rs | 14 -------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/scripts/build/buildOptionsRs.ts b/scripts/build/buildOptionsRs.ts index 5e835f9f5f4..a24fc16829b 100644 --- a/scripts/build/buildOptionsRs.ts +++ b/scripts/build/buildOptionsRs.ts @@ -28,8 +28,13 @@ import { writeIfChanged } from "./fs.ts"; /** Rust string literal for `s`. JSON escaping is a strict subset of Rust's. */ const rstr = (s: string): string => JSON.stringify(s); -/** Rust byte-string literal for `s` (the `&[u8]` constants). */ -const rbstr = (s: string): string => `b${JSON.stringify(s)}`; +/** + * Rust `&[u8]` literal for `s`. Goes via a UTF-8 string literal + + * `.as_bytes()` rather than `b"..."` so non-ASCII paths (e.g. a Windows + * checkout under `C:\Users\Müller\`) survive — Rust byte-string literals are + * ASCII-only and `JSON.stringify` would pass the `ü` through verbatim. + */ +const rbstr = (s: string): string => `${JSON.stringify(s)}.as_bytes()`; export function generateBuildOptionsRs(cfg: Config): string { const outPath = resolve(cfg.codegenDir, "build_options.rs"); diff --git a/src/bun_core/util.rs b/src/bun_core/util.rs index 1a3b50477ce..dd8ea584a4e 100644 --- a/src/bun_core/util.rs +++ b/src/bun_core/util.rs @@ -2237,20 +2237,6 @@ impl Version { } } -/// `const`-context decimal `u32` parse of an ASCII byte slice. No sign, no -/// whitespace, wrapping on overflow; non-digits are accumulated as garbage so -/// only feed it digit-only build-time literals (e.g. `env!` version components). -/// `str::parse` isn't `const`, hence this. -pub const fn const_parse_u32(bytes: &[u8]) -> u32 { - let mut i = 0usize; - let mut n: u32 = 0; - while i < bytes.len() { - n = n.wrapping_mul(10).wrapping_add((bytes[i] - b'0') as u32); - i += 1; - } - n -} - // ─── RacyCell ───────────────────────────────────────────────────────────── /// Stable equivalent of `core::cell::SyncUnsafeCell` (nightly-only as of /// 1.79). A `static`-safe interior-mutability cell with **no** synchronization.