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
1 change: 1 addition & 0 deletions scripts/build/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()` |
Expand Down
80 changes: 80 additions & 0 deletions scripts/build/buildOptionsRs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* 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 <other-triple>` 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 `&[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");
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;
}
8 changes: 8 additions & 0 deletions scripts/build/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down
19 changes: 3 additions & 16 deletions scripts/build/rust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 —
Expand Down
37 changes: 37 additions & 0 deletions src/bun_core/build.rs
Original file line number Diff line number Diff line change
@@ -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");
}
95 changes: 5 additions & 90 deletions src/bun_core/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
/// (`<repo>/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",
));
/// `<build>/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"));
}
Comment thread
claude[bot] marked this conversation as resolved.

// ── re-exports (the tier-0 surface downstream crates need) ────────────────
Expand Down
14 changes: 0 additions & 14 deletions src/bun_core/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` (nightly-only as of
/// 1.79). A `static`-safe interior-mutability cell with **no** synchronization.
Expand Down
Loading