Skip to content
Open
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
10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,11 @@ overflow-checks = false
# back to "warn" (their priority 0 beats the group's -1) where a warning level
# is intentional.
warnings = { level = "deny", priority = -1 }
# `bun_asan` / `bun_debug` / `socket_fault_injection` are set via RUSTFLAGS
# (`--cfg=...` + `--check-cfg=cfg(...)`) by scripts/build/rust.ts; register
# them here so a plain `cargo build` / `cargo check` (without those flags)
# doesn't warn.
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(bun_asan)', 'cfg(bun_debug)', 'cfg(socket_fault_injection)'] }
# `bun_asan` / `bun_debug` / `bun_tinycc` / `socket_fault_injection` are set
# via RUSTFLAGS (`--cfg=...` + `--check-cfg=cfg(...)`) by scripts/build/rust.ts;
# register them here so a plain `cargo build` / `cargo check` (without those
# flags) doesn't warn.
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(bun_asan)', 'cfg(bun_debug)', 'cfg(bun_tinycc)', 'cfg(socket_fault_injection)'] }
# link.exe unconditionally prints "Creating library X.dll.lib and object
# X.dll.exp" to stdout when linking each proc-macro DLL on Windows hosts;
# there is no linker flag to suppress it. The lint already exempts itself
Expand Down
25 changes: 12 additions & 13 deletions scripts/build/buildOptionsRs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
* 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.
* `ENABLE_LOGS` / `ENABLE_ASAN` / `ENABLE_TINYCC` are emitted as `cfg!()` of
* the `--cfg` that `rust.ts` derives from the same `Config` field, not as
* literals: each pairs with `#[cfg]`-gated code (the ASAN allocator, the
* `tcc_*` externs in `bun_tcc_sys`) that can only key on the cfg, so reading
* the cfg keeps constant and code in agreement in every invocation, including
* a bare `cargo check` that reads this file with no RUSTFLAGS at all.
*
* Written at configure time alongside `depVersionsHeader.ts` /
* `cargo-config.ts` — it's a constant manifest, not a build edge.
Expand Down Expand Up @@ -59,17 +61,14 @@ export function generateBuildOptionsRs(cfg: Config): string {
`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_debug` ⇔ `cfg.debug`, `--cfg=bun_asan` ⇔",
"// `cfg.asan`, and `cfg.tinycc`'s default (config.ts) is the negation",
"// of this predicate.",
"// Each of these reads the `--cfg` that scripts/build/rust.ts",
"// (cargoBuildInvocation) sets from the matching `Config` field, so it",
"// agrees with the `#[cfg]`-gated code it pairs with; rust.ts sets",
"// `--cfg=bun_debug` ⇔ `cfg.debug`, `--cfg=bun_asan` ⇔ `cfg.asan`,",
"// `--cfg=bun_tinycc` ⇔ `cfg.tinycc`. All false under bare `cargo check`.",
"pub const ENABLE_LOGS: bool = cfg!(bun_debug);",
"pub const ENABLE_ASAN: bool = cfg!(bun_asan);",
"pub const ENABLE_TINYCC: bool = !cfg!(any(",
` target_os = "android",`,
` target_os = "freebsd",`,
"));",
"pub const ENABLE_TINYCC: bool = cfg!(bun_tinycc);",
"",
];

Expand Down
11 changes: 11 additions & 0 deletions scripts/build/rust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,17 @@ export function cargoBuildInvocation(cfg: Config): CargoInvocation {
if (cfg.socketFaultInjection) {
rustflags.push("--cfg=socket_fault_injection");
}
// `bun_tinycc`: libtcc is in the link (deps/tinycc.ts is `enabled` by the
// same `cfg.tinycc`). Same contract as above: `bun_tcc_sys` declares the
// `tcc_*` externs under this cfg and defines link-satisfying stubs without
// it, and `build_options::ENABLE_TINYCC` (the runtime gate that makes
// bun:ffi's cc() throw "not available in this build") is `cfg!(bun_tinycc)`,
// so the option reaches every Rust consumer through this one flag. Bare
// `cargo check` / clippy / miri (no rustflags) get the stubs.
rustflags.push("--check-cfg=cfg(bun_tinycc)");
if (cfg.tinycc) {
rustflags.push("--cfg=bun_tinycc");
}
// Drop `#[track_caller]` source-location capture in release. Every
// `Option::unwrap`/`slice[i]`/`RefCell::borrow` etc. otherwise emits a
// `&'static core::panic::Location` (file/line/col) plus the file-path string
Expand Down
4 changes: 4 additions & 0 deletions src/bun_core/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ pub const BASE_PATH: &[u8] = build_options::BASE_PATH;
pub const ENABLE_LOGS: bool = build_options::ENABLE_LOGS;
pub const ENABLE_ASAN: bool = build_options::ENABLE_ASAN;
pub const ENABLE_FUZZILLI: bool = build_options::ENABLE_FUZZILLI;
/// Whether libtcc is linked into this build: `cfg!(bun_tinycc)`, which
/// `scripts/build/rust.ts` sets from the build's `tinycc` option (off by default
/// on Android and FreeBSD). `bun_tcc_sys` swaps its `tcc_*` externs for stubs on
/// the same cfg, so bun:ffi's cc() must early-return while this is `false`.
pub const ENABLE_TINYCC: bool = build_options::ENABLE_TINYCC;

// TYPE_ONLY: bun_semver::Version moves to bun_core (move-in pass).
Expand Down
33 changes: 15 additions & 18 deletions src/tcc_sys/tcc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,31 @@ pub type TCCErrorFunc = Option<unsafe extern "C" fn(opaque: *mut c_void, msg: *c
/// Typed error callback signature for a given context type.
pub type ErrorFunc<Ctx> = unsafe extern "C" fn(ctx: *mut Ctx, msg: *const c_char);

// `libtcc.a` is only built where `cfg.tinycc` is true (`scripts/build/config.ts`):
// not Android, not FreeBSD (the vendored fork doesn't support those targets).
// On those platforms these `extern "C"` decls would be undefined at link:
// `bun_runtime::ffi::ffi_body::{Source::add,
// CompileC::compile}` are reachable from `extern "C"` JS bindings and the
// monomorphized refs land in `libbun_rust.a` regardless of any
// `if !ENABLE_TINYCC { return }` runtime guard. Swap the `extern` block for
// stub *definitions* on those targets so the link resolves; the gated Rust
// callers never reach them at runtime (they early-return with "not available
// in this build"), and the `unreachable!()` makes any future gate regression
// loud rather than silently UB.
//
// Keep this predicate in sync with `cfg.tinycc` in `scripts/build/config.ts`
// and `ENABLE_TINYCC` in `scripts/build/buildOptionsRs.ts`.
// `cfg(bun_tinycc)` is set by `scripts/build/rust.ts` exactly when the build
// links libtcc (`cfg.tinycc`: the `--tinycc` option, off by default on Android
// and FreeBSD, which the vendored fork doesn't support). Without libtcc these
// `extern "C"` decls would be undefined at link: `bun_runtime::ffi::ffi_body::
// {Source::add, CompileC::compile}` are reachable from `extern "C"` JS
// bindings and the monomorphized refs land in `libbun_rust.a` regardless of
// any `if !ENABLE_TINYCC { return }` runtime guard. So a build without libtcc
// gets stub *definitions* instead; the gated Rust callers never reach them at
// runtime (`ENABLE_TINYCC` is `cfg!(bun_tinycc)`, so they early-return with
// "not available in this build"), and the `unreachable!()` makes any future
// gate regression loud rather than silently UB.
macro_rules! tcc_externs {
($($(#[$attr:meta])* fn $name:ident($($arg:ident: $ty:ty),* $(,)?) $(-> $ret:ty)?;)*) => {
#[cfg(not(any(target_os = "android", target_os = "freebsd")))]
#[cfg(bun_tinycc)]
unsafe extern "C" {
$($(#[$attr])* fn $name($($arg: $ty),*) $(-> $ret)?;)*
}
$(
#[cfg(any(target_os = "android", target_os = "freebsd"))]
#[cfg(not(bun_tinycc))]
#[allow(unused_variables, clippy::missing_safety_doc)]
unsafe extern "C" fn $name($($arg: $ty),*) $(-> $ret)? {
unreachable!(concat!(
stringify!($name),
" called but TinyCC is disabled on this target — keep the ",
"ENABLE_TINYCC early-returns in bun_runtime::ffi in sync with this stub"
" called but this build has no TinyCC; keep the ENABLE_TINYCC ",
"early-returns in bun_runtime::ffi in sync with this stub"
));
}
)*
Expand Down
116 changes: 116 additions & 0 deletions test/internal/source-lints/build-rust.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@
* import), so it is read as text.
* - The rustflags put the Rust half of the binary on the CPU baseline the C++
* half is compiled for (`cpuTargetFlags` in scripts/build/flags.ts).
* - The `tinycc` option decides whether libtcc is linked, so the Rust side
* (the `tcc_*` externs and the `ENABLE_TINYCC` runtime gate) has to follow
* the same option, through a cfg the rustflags carry.
*/
import { describe, expect, test } from "bun:test";
import { tempDir } from "harness";
import { readFileSync } from "node:fs";
import { join } from "node:path";

import { generateBuildOptionsRs } from "../../../scripts/build/buildOptionsRs.ts";
import {
resolveConfig,
type Abi,
Expand Down Expand Up @@ -180,3 +185,114 @@ describe("CPU baseline", () => {
expect(cpuFlags(withAbi(linuxX64, "android"))).toEqual(["-Ctarget-cpu=nehalem"]);
});
});

describe("tinycc option", () => {
// `cfg.tinycc` (`--tinycc=on|off`; config.ts defaults it off on Android and
// FreeBSD) is what puts libtcc into the link (scripts/build/deps/tinycc.ts).
// Two things on the Rust side have to agree with it: src/tcc_sys/tcc.rs
// declares the `tcc_*` symbols as externs (undefined at link time when
// libtcc isn't built) or defines stubs for them, and
// `bun_core::Environment::ENABLE_TINYCC` is the runtime gate behind
// bun:ffi's "cc() is not available in this build". `#[cfg]` can't read a
// const, so the option reaches Rust as a cfg in the rustflags and both of
// them key on it. These tests pin that chain; each link of it used to carry
// its own copy of the platform list instead, which left `--tinycc=off`
// (or any change to the list in config.ts) unknown to the Rust half of the
// build.

const linux: PartialConfig = { os: "linux", arch: "x64", abi: "gnu", linuxSysroot: "/fake" };

/** Resolve a config whose generated files land in `scratch`. */
function configure(partial: PartialConfig, scratch: string): Config {
return resolve({ buildDir: scratch, ...partial });
}

function rustflags(cfg: Config): string[] {
return cargoBuildInvocation(cfg).env.CARGO_ENCODED_RUSTFLAGS!.split("\x1f");
}

/** The cfg that `ENABLE_TINYCC` reads in the build_options.rs generated for `cfg`. */
function enableTinyccCfg(cfg: Config): string {
const source = readFileSync(generateBuildOptionsRs(cfg), "utf8");
const match = /^pub const ENABLE_TINYCC: bool = cfg!\((\w+)\);$/m.exec(source);
if (!match) {
const actual = /^pub const ENABLE_TINYCC: bool = .*$/m.exec(source)?.[0] ?? "(no ENABLE_TINYCC constant)";
throw new Error(`ENABLE_TINYCC in build_options.rs must be one cfg!() that rust.ts sets, got: ${actual}`);
}
return match[1]!;
}

/** The cfg's name, as a default linux configure spells it. */
function cfgName(): string {
using scratch = tempDir("build-rust-tinycc", {});
return enableTinyccCfg(configure(linux, String(scratch)));
}

/** What `ENABLE_TINYCC` evaluates to in the cargo build rust.ts emits for `cfg`. */
function rustEnableTinycc(cfg: Config): boolean {
const name = enableTinyccCfg(cfg);
const flags = rustflags(cfg);
// Declared whether or not it is set: rustc's unexpected_cfgs lint fires
// on the builds that leave an undeclared cfg unset.
expect(flags).toContain(`--check-cfg=cfg(${name})`);
return flags.includes(`--cfg=${name}`);
}

const cases: { name: string; partial: PartialConfig; tinycc: boolean }[] = [
{ name: "linux", partial: linux, tinycc: true },
{ name: "linux --tinycc=off", partial: { ...linux, tinycc: false }, tinycc: false },
{ name: "debug --tinycc=off", partial: { ...linux, buildType: "Debug", tinycc: false }, tinycc: false },
{ name: "windows", partial: { os: "windows", arch: "x64", winsysroot: "/fake" }, tinycc: true },
// config.ts's platform exclusions arrive through the option like everything else.
{ name: "freebsd", partial: { os: "freebsd", arch: "x64", freebsdSysroot: "/fake" }, tinycc: false },
{
name: "freebsd --tinycc=on",
partial: { os: "freebsd", arch: "x64", freebsdSysroot: "/fake", tinycc: true },
tinycc: true,
},
];

for (const { name, partial, tinycc } of cases) {
test(`${name}: the Rust build sees tinycc=${tinycc}`, () => {
using scratch = tempDir("build-rust-tinycc", {});
const cfg = configure(partial, String(scratch));
// The option itself resolves as documented; under test is whether the
// Rust build gets the value the dep graph acts on.
expect(cfg.tinycc).toBe(tinycc);
expect(rustEnableTinycc(cfg)).toBe(tinycc);
});
}

test("the cfg is registered in Cargo.toml for bare cargo", () => {
// `cargo check` / clippy / miri run without rust.ts's rustflags, so the
// `--check-cfg` there doesn't reach them.
const unexpectedCfgs = /^unexpected_cfgs\s*=.*$/m.exec(readFileSync(join(repoRoot, "Cargo.toml"), "utf8"))?.[0];
expect(unexpectedCfgs).toContain(`'cfg(${cfgName()})'`);
});

test("bun_tcc_sys declares the tcc_* externs under the cfg and stubs them without it", () => {
const name = cfgName();
const source = readFileSync(join(repoRoot, "src", "tcc_sys", "tcc.rs"), "utf8").replace(/\/\/[^\n]*/g, "");
const macro = /^macro_rules! tcc_externs \{\n([\s\S]*?)^\}/m.exec(source);
if (!macro) throw new Error("macro_rules! tcc_externs not found in src/tcc_sys/tcc.rs");
const body = macro[1]!;

expect(body).toMatch(new RegExp(String.raw`#\[cfg\(${name}\)\]\s*unsafe extern "C" \{`));
expect(body).toMatch(new RegExp(String.raw`#\[cfg\(not\(${name}\)\)\][^{]*\bunsafe extern "C" fn `));
// A platform predicate here would be a second copy of the list in
// config.ts, which is exactly what the cfg replaces.
expect(body).not.toMatch(/\btarget_os\b|\btarget_arch\b|\btarget_env\b/);

// Every tcc_* symbol the link has to satisfy goes through that macro: a
// `fn tcc_*` declared anywhere else would be outside the cfg.
const strays: string[] = [];
for (const rel of new Bun.Glob("src/**/*.rs").scanSync({ cwd: repoRoot })) {
const file = rel.replaceAll("\\", "/");
if (file === "src/tcc_sys/tcc.rs") continue;
// Searched as bytes: decoding ~1500 files to strings is what makes this
// slow under an ASAN build.
if (readFileSync(join(repoRoot, rel)).includes("fn tcc_")) strays.push(file);
}
expect(strays).toEqual([]);
});
});
Loading