Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
75 changes: 75 additions & 0 deletions scripts/build/buildOptionsRs.ts
Original file line number Diff line number Diff line change
@@ -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 <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 byte-string literal for `s` (the `&[u8]` constants). */
const rbstr = (s: string): string => `b${JSON.stringify(s)}`;

Check failure on line 32 in scripts/build/buildOptionsRs.ts

View check run for this annotation

Claude / Claude Code Review

rbstr() generates invalid Rust for non-ASCII paths

`rbstr()` emits Rust byte-string literals via `b${JSON.stringify(s)}`, but Rust `b"..."` literals are ASCII-only and `JSON.stringify` passes non-ASCII characters through literally — so if `cfg.cwd` or `cfg.codegenDir` contain non-ASCII characters (e.g. a Windows checkout under `C:\Users\Müller\` or a CJK home dir), the generated `build_options.rs` fails to compile with `non-ASCII character in byte string literal`. This is a regression: the old `option_env!("BUN_BASE_PATH") → v.as_bytes()` path h
Comment thread
claude[bot] marked this conversation as resolved.
Outdated

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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,98 +653,13 @@
/// 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"));
}

Check warning on line 662 in src/bun_core/lib.rs

View check run for this annotation

Claude / Claude Code Review

const_parse_u32 is now dead code

Nit: `const_parse_u32` (src/bun_core/util.rs:2244) is now dead code — its only callers were the three `crate::const_parse_u32(build_opt!("BUN_VERSION_*", ...).as_bytes())` lines this PR removed. The generated file emits integer literals directly, so the helper is orphaned; since it's `pub` (re-exported via `pub use util::*`) the `dead_code` lint won't catch it. Worth deleting alongside the env-var handshake it served.
Comment thread
claude[bot] marked this conversation as resolved.

// ── re-exports (the tier-0 surface downstream crates need) ────────────────
pub use bun_alloc::oom_from_alloc;
Expand Down
Loading