From 5ee93e678891d2542936fa530b02e3f74d63814c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 06:30:14 +0000 Subject: [PATCH 1/3] build: make the logs option reach Rust (--cfg=bun_logs) The build's `logs` option (on by default in debug, set by the release-assertions profile and `bun run build:logs`, overridable with --logs=on|off) did nothing for Rust: buildOptionsRs.ts emitted `ENABLE_LOGS = cfg!(bun_debug)` and rust.ts only passes --cfg=bun_debug for Debug builds, so a release build configured with logs compiled every scoped logger out and a debug build configured without still logged. scoped_log!, syslog!, mark_binding! and the two mark_binding fns also gated on IS_DEBUG directly, so fixing the constant alone would not have been enough. rust.ts now passes --cfg=bun_logs exactly when cfg.logs is set (plus the matching --check-cfg, and the cfg is registered in Cargo.toml for bare cargo), build_options.rs reads ENABLE_LOGS from it, and the loggers gate on ENABLE_LOGS. Debug, release and release-asan resolve to the same values as before; release-assertions and --logs=on|off now take effect. A cfg rather than a literal in build_options.rs keeps a bare `cargo check` / `cargo miri test` (which read build/debug's generated file without any RUSTFLAGS) on logs-off semantics, as with bun_debug. --- Cargo.toml | 10 +- scripts/build/buildOptionsRs.ts | 12 +- scripts/build/rust.ts | 18 +- src/bun_core/Global.rs | 8 +- src/bun_core/env.rs | 4 + src/bun_core/output.rs | 21 +- src/bundler/bundle_v2.rs | 2 +- src/jsc/lib.rs | 4 +- src/output/lib.rs | 4 +- src/sys/lib.rs | 6 +- src/sys/windows/mod.rs | 2 +- .../source-lints/build-logs-option.test.ts | 205 ++++++++++++++++++ 12 files changed, 262 insertions(+), 34 deletions(-) create mode 100644 test/internal/source-lints/build-logs-option.test.ts diff --git a/Cargo.toml b/Cargo.toml index c7dcfe86bbb9..a350ccf82cfa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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_logs` / `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_logs)', '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 diff --git a/scripts/build/buildOptionsRs.ts b/scripts/build/buildOptionsRs.ts index 0ea2c440a5be..393a3667d8e8 100644 --- a/scripts/build/buildOptionsRs.ts +++ b/scripts/build/buildOptionsRs.ts @@ -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`) + * Target-dependent constants (`ENABLE_TINYCC`) and the ones mirroring a + * `--cfg` that `rust.ts` passes in RUSTFLAGS (`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. + * so a `cargo check --target `, or a bare `cargo check` / `cargo + * miri test` with no RUSTFLAGS at all, against the same generated file still + * evaluates them per invocation. * * Written at configure time alongside `depVersionsHeader.ts` / * `cargo-config.ts` — it's a constant manifest, not a build edge. @@ -61,10 +63,10 @@ export function generateBuildOptionsRs(cfg: Config): string { "", "// 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` ⇔", + "// rust.ts sets `--cfg=bun_logs` ⇔ `cfg.logs`, `--cfg=bun_asan` ⇔", "// `cfg.asan`, and `cfg.tinycc`'s default (config.ts) is the negation", "// of this predicate.", - "pub const ENABLE_LOGS: bool = cfg!(bun_debug);", + "pub const ENABLE_LOGS: bool = cfg!(bun_logs);", "pub const ENABLE_ASAN: bool = cfg!(bun_asan);", "pub const ENABLE_TINYCC: bool = !cfg!(any(", ` target_os = "android",`, diff --git a/scripts/build/rust.ts b/scripts/build/rust.ts index ce1d728d6d67..10286557f79f 100644 --- a/scripts/build/rust.ts +++ b/scripts/build/rust.ts @@ -454,8 +454,8 @@ export function cargoBuildInvocation(cfg: Config): CargoInvocation { rustflags.push("--cfg=bun_asan"); } // `bun_debug`: the cargo profile is `dev` (a Debug-buildtype build). - // `bun_core::env::IS_DEBUG` and `build_options::ENABLE_LOGS` key on this - // instead of `cfg!(debug_assertions)` so that release-asan / + // `bun_core::env::IS_DEBUG` keys on this instead of + // `cfg!(debug_assertions)` so that release-asan / // release-assertions (which enable `debug-assertions` below for // `debug_assert!()` coverage) don't also flip on Debug-only conveniences: // `DUMP_SOURCE` (per-module writes to /tmp/bun-debug-src/), `debug_warn!` @@ -467,6 +467,20 @@ export function cargoBuildInvocation(cfg: Config): CargoInvocation { if (cfg.debug) { rustflags.push("--cfg=bun_debug"); } + // `bun_logs`: `build_options::ENABLE_LOGS`, the compile-time gate on + // `scoped_log!` (`BUN_DEBUG_=1`). Follows `cfg.logs`, which defaults + // to `cfg.debug` but diverges from it under `release-assertions` / + // `--logs=on` (release build with logs) and `--logs=off` (debug build + // without), hence a cfg of its own instead of `bun_debug`. A cfg rather + // than a literal in build_options.rs so that a bare `cargo check` / + // `cargo miri test` (which reads build/debug's build_options.rs but gets no + // RUSTFLAGS) keeps the log bodies dead like `bun_debug` does; with logs live + // there, `ScopedLogger::is_visible()` would scan the environment through + // the Highway FFI, which Miri can't call. + rustflags.push("--check-cfg=cfg(bun_logs)"); + if (cfg.logs) { + rustflags.push("--cfg=bun_logs"); + } // `bun_codegen_embed`: embed codegen-output `.js` (`include_bytes!`) instead // of reading them from `BUN_CODEGEN_DIR` at runtime. Mirrors Zig // `BunBuildOptions.shouldEmbedCode() = optimize != .Debug or codegen_embed`. diff --git a/src/bun_core/Global.rs b/src/bun_core/Global.rs index 56a912aec7f0..2e8057bab01c 100644 --- a/src/bun_core/Global.rs +++ b/src/bun_core/Global.rs @@ -436,10 +436,10 @@ macro_rules! mark_binding { }; ($fn_name:expr) => { // Opt-in via BUN_DEBUG_JSC=1. The `JSC` scope is owned by bun_core. Gate on - // `env::IS_DEBUG` (== `Environment::ENABLE_LOGS`) — never on a Cargo - // feature, since `cfg!(feature = ..)` is resolved against the *calling* - // crate and would warn (or silently no-op) in crates without it. - if $crate::env::IS_DEBUG && $crate::Global::JSC_SCOPE.is_visible() { + // `env::ENABLE_LOGS` like `scoped_log!` does, never on a Cargo feature, + // since `cfg!(feature = ..)` is resolved against the *calling* crate and + // would warn (or silently no-op) in crates without it. + if $crate::env::ENABLE_LOGS && $crate::Global::JSC_SCOPE.is_visible() { $crate::Global::JSC_SCOPE.log(::core::format_args!( "[JSC] {} ({}:{})\n", $fn_name, diff --git a/src/bun_core/env.rs b/src/bun_core/env.rs index 9f4c52ef8c2f..bf4aafc6440a 100644 --- a/src/bun_core/env.rs +++ b/src/bun_core/env.rs @@ -65,6 +65,10 @@ pub(crate) const CANARY_REVISION: &str = if IS_CANARY { }; pub const DUMP_SOURCE: bool = IS_DEBUG && !IS_TEST; pub const BASE_PATH: &[u8] = build_options::BASE_PATH; +/// The build's `logs` option (`--cfg=bun_logs`, set by `scripts/build/rust.ts` +/// from `cfg.logs`): on by default in Debug builds and in `release-assertions`, +/// off in plain release, `--logs=on|off` overrides. Independent of `IS_DEBUG`. +/// Compile-time gate for `scoped_log!` and the other `BUN_DEBUG_*` loggers. 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; diff --git a/src/bun_core/output.rs b/src/bun_core/output.rs index e0f1aa363edc..24773ea8d03d 100644 --- a/src/bun_core/output.rs +++ b/src/bun_core/output.rs @@ -1540,14 +1540,17 @@ macro_rules! declare_scope { /// `bun.Output.scoped(.X, vis)("fmt", .{args})` → `scoped_log!(X, "fmt", args...)` /// -/// MUST gate arg evaluation: expands to a dead branch in release builds. +/// MUST gate arg evaluation: expands to a dead branch in builds without logs. #[macro_export] macro_rules! scoped_log { ($scope:path, $fmt:expr $(, $arg:expr)* $(,)?) => { - // Gate on `env::IS_DEBUG` (== `Environment::ENABLE_LOGS`) so release - // builds dead-strip the body. Do NOT gate on a Cargo feature — there - // is no `debug_logs` feature and §Forbidden bans silent no-ops. - if $crate::env::IS_DEBUG && $scope.is_visible() { + // Gate on `env::ENABLE_LOGS` (the build's `logs` option, passed as + // `--cfg=bun_logs` by scripts/build/rust.ts) so builds without it + // dead-strip the body. Not `IS_DEBUG`: `release-assertions` and + // `--logs=on` carry logs in a non-Debug build, `--logs=off` drops them + // from a Debug one. Do NOT gate on a Cargo feature: there is no + // `debug_logs` feature and §Forbidden bans silent no-ops. + if $crate::env::ENABLE_LOGS && $scope.is_visible() { const __NL: &str = $crate::output::_needs_nl($crate::pretty_fmt!($fmt, false)); // Branch on ANSI *before* `format_args!` so each `$arg` evaluates // exactly once. @@ -2592,12 +2595,12 @@ fn init_scoped_debug_writer_at_startup() { fn scoped_writer() -> QuietWriter { // All callers are already gated on `Environment::ENABLE_LOGS`; this is a - // Debug-build self-check (release-asan/release-assertions enable - // `debug_assertions` with `ENABLE_LOGS == false`, so keying on - // `debug_assertions` would turn it into a guaranteed abort there). + // Debug-build self-check (release-asan enables `debug_assertions` with + // `ENABLE_LOGS == false`, so keying on `debug_assertions` would turn it + // into a guaranteed abort there). #[cfg(bun_debug)] if !Environment::ENABLE_LOGS { - unreachable!("scopedWriter() should only be called in debug mode"); + unreachable!("scopedWriter() should only be called when logs are enabled"); } // SAFETY: initialized in init_scoped_debug_writer_at_startup; QuietWriter is Copy POD. unsafe { scoped_debug_writer::SCOPED_FILE_WRITER.read() } diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 24999fd82b68..01383906a2b7 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -1978,7 +1978,7 @@ pub mod bv2_impl { } } - if bun_core::env::IS_DEBUG && ReachableFiles.is_visible() { + if bun_core::env::ENABLE_LOGS && ReachableFiles.is_visible() { bun_core::scoped_log!( ReachableFiles, "Reachable count: {} / {}", diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index 605f0c9e9e16..4f1a4d92547e 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -1381,7 +1381,7 @@ pub use self::Node as node; #[track_caller] #[inline] pub fn mark_binding() { - if bun_core::env::IS_DEBUG && bun_core::Global::JSC_SCOPE.is_visible() { + if bun_core::env::ENABLE_LOGS && bun_core::Global::JSC_SCOPE.is_visible() { let loc = core::panic::Location::caller(); bun_core::Global::JSC_SCOPE.log(format_args!("[jsc] ({}:{})\n", loc.file(), loc.line())); } @@ -1390,7 +1390,7 @@ pub fn mark_binding() { /// Like [`mark_binding`], with a class-name prefix. #[inline] pub(crate) fn mark_member_binding(class: &'static str, src: &core::panic::Location<'static>) { - if bun_core::env::IS_DEBUG && bun_core::Global::JSC_SCOPE.is_visible() { + if bun_core::env::ENABLE_LOGS && bun_core::Global::JSC_SCOPE.is_visible() { bun_core::Global::JSC_SCOPE.log(format_args!( "[jsc] {} ({}:{})\n", class, diff --git a/src/output/lib.rs b/src/output/lib.rs index cded6c31c9e5..71cea98cf7de 100644 --- a/src/output/lib.rs +++ b/src/output/lib.rs @@ -17,8 +17,8 @@ // bun_output::scoped_log!(X, "fmt {} {}", a, b); // // `declare_scope!` expands to a `pub static X: ScopedLogger`; `scoped_log!` -// gates arg evaluation behind `env::IS_DEBUG` so release builds pay zero -// cost (see PORTING.md — args MUST sit inside the dead branch). +// gates arg evaluation behind `env::ENABLE_LOGS` so builds without logs pay +// zero cost (see PORTING.md: args MUST sit inside the dead branch). pub use bun_core::declare_scope; pub use bun_core::define_scoped_log; pub use bun_core::scoped_log; diff --git a/src/sys/lib.rs b/src/sys/lib.rs index e563f9bb54b8..116d97d17576 100644 --- a/src/sys/lib.rs +++ b/src/sys/lib.rs @@ -4925,9 +4925,9 @@ pub type EnvMap = std::collections::HashMap; #[macro_export] macro_rules! syslog { ($fmt:literal $(, $arg:expr)* $(,)?) => { - // Gate on `env::IS_DEBUG` (== `Environment::ENABLE_LOGS`) — matches - // bun_core::scoped_log!; there is no `debug_logs` Cargo feature. - if ::bun_core::env::IS_DEBUG && $crate::fd::SYS.is_visible() { + // Gate on `env::ENABLE_LOGS`, matching bun_core::scoped_log!; there is + // no `debug_logs` Cargo feature. + if ::bun_core::env::ENABLE_LOGS && $crate::fd::SYS.is_visible() { const __NL: &str = ::bun_core::output::_needs_nl(::bun_core::pretty_fmt!($fmt, false)); // Branch on ANSI *before* `format_args!` so each `$arg` evaluates diff --git a/src/sys/windows/mod.rs b/src/sys/windows/mod.rs index 13427b1c5445..94fb008be6cb 100644 --- a/src/sys/windows/mod.rs +++ b/src/sys/windows/mod.rs @@ -397,7 +397,7 @@ unsafe extern "system" { pub fn GetFileType(hFile: HANDLE) -> DWORD { let rc = GetFileType_raw(hFile); - // `syslog!` self-gates on `env::IS_DEBUG` (see lib.rs); no extra feature + // `syslog!` self-gates on `env::ENABLE_LOGS` (see lib.rs); no extra feature // flag needed (there is no `debug_logs` feature in bun_sys). bun_sys::syslog!("GetFileType({}) = {}", Fd::from_system(hFile), rc); rc diff --git a/test/internal/source-lints/build-logs-option.test.ts b/test/internal/source-lints/build-logs-option.test.ts new file mode 100644 index 000000000000..80d247e3267d --- /dev/null +++ b/test/internal/source-lints/build-logs-option.test.ts @@ -0,0 +1,205 @@ +/** + * The build's `logs` option (scripts/build/config.ts: on by default in debug + * builds, set by the `release-assertions` profile and `bun run build:logs`, + * overridable with `--logs=on|off`) decides whether `scoped_log!` / + * `BUN_DEBUG_=1` logging is compiled into the Rust side. It reaches + * Rust as `bun_core::Environment::ENABLE_LOGS`, through pieces that have to + * agree with each other, pinned here: + * + * - scripts/build/buildOptionsRs.ts emits `ENABLE_LOGS` into build_options.rs + * as a `cfg!(...)`, and scripts/build/rust.ts passes that `--cfg` exactly + * when `cfg.logs` is set (it used to be `cfg!(bun_debug)`, which made the + * option dead: a release build configured with logs had none, a debug + * build configured without still logged); + * - Cargo.toml registers the cfg so a bare `cargo check` doesn't warn; + * - the loggers in src gate on `ENABLE_LOGS`, not on `IS_DEBUG`, otherwise + * a non-debug build configured with logs still compiles them out. + * + * Pure config evaluation plus a source scan: no compiler is involved. + */ +import { describe, expect, test } from "bun:test"; +import { tempDir } from "harness"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { generateBuildOptionsRs } from "../../../scripts/build/buildOptionsRs.ts"; +import { resolveConfig, type Config, type PartialConfig, type Toolchain } from "../../../scripts/build/config.ts"; +import { getProfile } from "../../../scripts/build/profiles.ts"; +import { cargoBuildInvocation } from "../../../scripts/build/rust.ts"; + +const repoRoot = resolve(import.meta.dir, "..", "..", ".."); + +/** A fully-populated fake toolchain; nothing here is ever executed. */ +function mockToolchain(): Toolchain { + return { + cc: "/fake/llvm/bin/clang", + cxx: "/fake/llvm/bin/clang++", + hostCc: undefined, + hostCxx: undefined, + clangVersion: "21.1.8", + clangResourceDir: "/fake/llvm/lib/clang/21", + ar: "/fake/llvm/bin/llvm-ar", + ranlib: "/fake/llvm/bin/llvm-ranlib", + ld: "/fake/llvm/bin/ld.lld", + ld64Lld: undefined, + rustLld: undefined, + rustLlvmVersion: "22.1.4", + rustSysroot: undefined, + rustHostTriple: undefined, + strip: "/fake/llvm/bin/llvm-strip", + llvmStrip: "/fake/llvm/bin/llvm-strip", + dsymutil: undefined, + bun: "/fake/bin/bun", + jsRuntime: "/fake/bin/bun", + esbuild: "/fake/bin/esbuild", + ccache: undefined, + cmake: "/fake/bin/cmake", + cargo: undefined, + cargoHome: undefined, + rustupHome: undefined, + msvcLinker: undefined, + rc: undefined, + mt: undefined, + nasm: undefined, + }; +} + +/** + * A linux-x64 target resolves on every host once it is told where its sysroot + * is (the path is only recorded, never opened). + */ +function linuxConfig(partial: PartialConfig, buildDir: string): Config { + return resolveConfig( + { os: "linux", arch: "x64", abi: "gnu", buildDir, linuxSysroot: buildDir, ...partial }, + mockToolchain(), + ); +} + +function rustflags(cfg: Config): string[] { + return cargoBuildInvocation(cfg).env.CARGO_ENCODED_RUSTFLAGS?.split("\x1f") ?? []; +} + +/** The right-hand side of `pub const ENABLE_LOGS: bool = ...;` in the build_options.rs generated for `cfg`. */ +function generatedEnableLogs(cfg: Config): string { + const source = readFileSync(generateBuildOptionsRs(cfg), "utf8"); + const match = /^pub const ENABLE_LOGS: bool = (.+);$/m.exec(source); + if (!match) throw new Error(`build_options.rs has no ENABLE_LOGS constant:\n${source}`); + return match[1]; +} + +/** The name inside `cfg!(...)`, or undefined when the constant is a plain literal. */ +function cfgName(expr: string): string | undefined { + return /^cfg!\((\w+)\)$/.exec(expr)?.[1]; +} + +/** + * What `bun_core::Environment::ENABLE_LOGS` evaluates to in the cargo build + * rust.ts emits for `cfg`: the generated constant, resolved against the + * rustflags of that same build. + */ +function rustEnableLogs(cfg: Config): boolean { + const expr = generatedEnableLogs(cfg); + if (expr === "true" || expr === "false") return expr === "true"; + const name = cfgName(expr); + if (name === undefined) throw new Error(`unexpected ENABLE_LOGS initializer: ${expr}`); + const flags = rustflags(cfg); + // rustc's unexpected_cfgs lint needs the cfg declared in the same build + // that may set it, whether or not this configuration sets it. + expect(flags).toContain(`--check-cfg=cfg(${name})`); + return flags.includes(`--cfg=${name}`); +} + +describe("ENABLE_LOGS follows the logs option", () => { + const cases: { name: string; partial: PartialConfig; logs: boolean }[] = [ + // The profile's doc comment promises "Release + assertions + logs". + { name: "release-assertions profile", partial: getProfile("release-assertions"), logs: true }, + // `bun run build:logs`. + { name: "release --logs=on", partial: { buildType: "Release", logs: true }, logs: true }, + { name: "debug --logs=off", partial: { buildType: "Debug", logs: false }, logs: false }, + { name: "debug (default on)", partial: { buildType: "Debug" }, logs: true }, + { name: "release (default off)", partial: { buildType: "Release" }, logs: false }, + // release-asan / the CI asan lane: assertions on, logs stay off. + { + name: "release-asan (default off)", + partial: { buildType: "Release", asan: true, assertions: true }, + logs: false, + }, + ]; + + for (const { name, partial, logs } of cases) { + test(`${name}: rust ENABLE_LOGS is ${logs}`, () => { + using dir = tempDir("build-logs-option", {}); + const cfg = linuxConfig(partial, String(dir)); + // The option itself resolves as documented; what is under test is + // whether the Rust build sees that value. + expect(cfg.logs).toBe(logs); + expect(rustEnableLogs(cfg)).toBe(logs); + }); + } + + test("logs is independent of the Debug-build cfg", () => { + using dir = tempDir("build-logs-option", {}); + const releaseWithLogs = rustflags(linuxConfig({ buildType: "Release", logs: true }, String(dir))); + expect(releaseWithLogs).not.toContain("--cfg=bun_debug"); + const debugWithoutLogs = rustflags(linuxConfig({ buildType: "Debug", logs: false }, String(dir))); + expect(debugWithoutLogs).toContain("--cfg=bun_debug"); + }); + + test("the cfg build_options.rs reads is registered for bare cargo in Cargo.toml", () => { + using dir = tempDir("build-logs-option", {}); + const name = cfgName(generatedEnableLogs(linuxConfig({ buildType: "Debug" }, String(dir)))); + if (name === undefined) return; // a literal needs no registration + const cargoToml = readFileSync(resolve(repoRoot, "Cargo.toml"), "utf8"); + const unexpectedCfgs = /^unexpected_cfgs\s*=.*$/m.exec(cargoToml)?.[0]; + expect(unexpectedCfgs).toBeDefined(); + expect(unexpectedCfgs).toContain(`'cfg(${name})'`); + }); +}); + +describe("the loggers in src gate on ENABLE_LOGS", () => { + /** `//` comments blanked out (newlines kept, so line numbers survive). */ + const stripComments = (source: string) => source.replace(/\/\/[^\n]*/g, ""); + + /** Body of `macro_rules! { ... }` in `source`, up to the first line that is just `}`. */ + function macroBody(source: string, name: string): string { + const match = new RegExp(String.raw`^macro_rules! ${name} \{\n([\s\S]*?)^\}`, "m").exec(source); + if (!match) throw new Error(`macro_rules! ${name} not found`); + return match[1]; + } + + // ` && scope.is_visible()`: one `if` condition, however rustfmt + // wraps it, since `;` and `{` cannot occur inside it. + const guardOn = (constant: string, flags = "") => + new RegExp(String.raw`\b${constant}\b[^;{]*\.is_visible\(\)`, flags); + + test("scoped_log!, syslog! and mark_binding! check ENABLE_LOGS before the scope", () => { + const macros = [ + { file: "src/bun_core/output.rs", name: "scoped_log" }, + { file: "src/sys/lib.rs", name: "syslog" }, + { file: "src/bun_core/Global.rs", name: "mark_binding" }, + ]; + for (const { file, name } of macros) { + const body = macroBody(stripComments(readFileSync(resolve(repoRoot, file), "utf8")), name); + expect(body, `${name}! in ${file}`).toMatch(guardOn("ENABLE_LOGS")); + expect(body, `${name}! in ${file}`).not.toMatch(/\bIS_DEBUG\b/); + } + }); + + test("no logger guard keys on IS_DEBUG", () => { + // `IS_DEBUG && scope.is_visible()` compiles the log out of every non-debug + // build, including the ones configured with logs. Gate on ENABLE_LOGS + // instead, or just call scoped_log!, which does. + const offenders: string[] = []; + const guard = guardOn("IS_DEBUG", "g"); + for (const rel of new Bun.Glob("src/**/*.rs").scanSync({ cwd: repoRoot })) { + const raw = readFileSync(resolve(repoRoot, rel), "utf8"); + if (!raw.includes(".is_visible()")) continue; + const source = stripComments(raw); + for (const match of source.matchAll(guard)) { + const line = source.slice(0, match.index).split("\n").length; + offenders.push(`${rel.replaceAll("\\", "/")}:${line}`); + } + } + expect(offenders.sort()).toEqual([]); + }); +}); From ff57be0de3435b4e0c3805c6b4f5826191a8888f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:43:20 +0000 Subject: [PATCH 2/3] build: keep #[track_caller] locations in release builds with logs -Zlocation-detail=none was passed for every non-assertions release build, which now includes `release --logs=on` (bun run build:logs). The mark_binding() and test-runner group::begin() loggers that build turns on print Location::caller(), so they logged `:0`. Gate the flag on !cfg.logs as well; the shipped profiles have logs off and are unchanged. --- scripts/build/rust.ts | 9 ++++++--- .../source-lints/build-logs-option.test.ts | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/scripts/build/rust.ts b/scripts/build/rust.ts index 10286557f79f..99fc3013ca5a 100644 --- a/scripts/build/rust.ts +++ b/scripts/build/rust.ts @@ -511,9 +511,12 @@ export function cargoBuildInvocation(cfg: Config): CargoInvocation { // file:line server-side, so the panic call site is recoverable from the trace // without embedding the location in the binary — same as the Zig build, which // had ~0 embedded source paths. Kept off for debug and `release-assertions` - // where panic messages are read locally. Nightly-only; the pinned toolchain - // is nightly. - if (cfg.release && !cfg.assertions) { + // where panic messages are read locally, and for `--logs=on` builds: the + // `bun_jsc::mark_binding()` / test-runner `group::begin()` loggers print + // `Location::caller()`, which this flag turns into `:0`, and a + // build that embeds every `scoped_log!` format string is not the one the + // size matters for. Nightly-only; the pinned toolchain is nightly. + if (cfg.release && !cfg.assertions && !cfg.logs) { rustflags.push("-Zlocation-detail=none"); } // Path remapping (CI reproducibility) — rustc equivalent of the C/C++ diff --git a/test/internal/source-lints/build-logs-option.test.ts b/test/internal/source-lints/build-logs-option.test.ts index 80d247e3267d..c0f6fd75cd60 100644 --- a/test/internal/source-lints/build-logs-option.test.ts +++ b/test/internal/source-lints/build-logs-option.test.ts @@ -12,6 +12,9 @@ * option dead: a release build configured with logs had none, a debug * build configured without still logged); * - Cargo.toml registers the cfg so a bare `cargo check` doesn't warn; + * - a release build with logs keeps `#[track_caller]` locations, which the + * `mark_binding()` style of logger prints (`-Zlocation-detail=none` would + * turn them into `:0`); * - the loggers in src gate on `ENABLE_LOGS`, not on `IS_DEBUG`, otherwise * a non-debug build configured with logs still compiles them out. * @@ -145,6 +148,21 @@ describe("ENABLE_LOGS follows the logs option", () => { expect(debugWithoutLogs).toContain("--cfg=bun_debug"); }); + test("a release build with logs keeps the call-site locations its loggers print", () => { + using dir = tempDir("build-logs-option", {}); + const locationDetail = (partial: PartialConfig) => + rustflags(linuxConfig(partial, String(dir))).includes("-Zlocation-detail=none"); + expect({ + release: locationDetail({ buildType: "Release" }), + "release --logs=on": locationDetail({ buildType: "Release", logs: true }), + "release-assertions": locationDetail(getProfile("release-assertions")), + }).toEqual({ + release: true, + "release --logs=on": false, + "release-assertions": false, + }); + }); + test("the cfg build_options.rs reads is registered for bare cargo in Cargo.toml", () => { using dir = tempDir("build-logs-option", {}); const name = cfgName(generatedEnableLogs(linuxConfig({ buildType: "Debug" }, String(dir)))); From 63181fb285c095d6be25284ca628875da4c8e405 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:48:50 +0000 Subject: [PATCH 3/3] build: shorten the ENABLE_LOGS comments --- scripts/build/rust.ts | 23 +++++++++-------------- src/bun_core/Global.rs | 6 ++---- src/bun_core/env.rs | 6 ++---- src/bun_core/output.rs | 16 ++++++---------- 4 files changed, 19 insertions(+), 32 deletions(-) diff --git a/scripts/build/rust.ts b/scripts/build/rust.ts index 99fc3013ca5a..8812996e8daf 100644 --- a/scripts/build/rust.ts +++ b/scripts/build/rust.ts @@ -468,15 +468,12 @@ export function cargoBuildInvocation(cfg: Config): CargoInvocation { rustflags.push("--cfg=bun_debug"); } // `bun_logs`: `build_options::ENABLE_LOGS`, the compile-time gate on - // `scoped_log!` (`BUN_DEBUG_=1`). Follows `cfg.logs`, which defaults - // to `cfg.debug` but diverges from it under `release-assertions` / - // `--logs=on` (release build with logs) and `--logs=off` (debug build - // without), hence a cfg of its own instead of `bun_debug`. A cfg rather - // than a literal in build_options.rs so that a bare `cargo check` / - // `cargo miri test` (which reads build/debug's build_options.rs but gets no - // RUSTFLAGS) keeps the log bodies dead like `bun_debug` does; with logs live - // there, `ScopedLogger::is_visible()` would scan the environment through - // the Highway FFI, which Miri can't call. + // `scoped_log!`. Follows `cfg.logs`, which `release-assertions` / `--logs` + // set independently of `cfg.debug`. A cfg rather than a literal in + // build_options.rs so that bare `cargo check` / `cargo miri test` (no + // RUSTFLAGS, reading build/debug's file) keep the log bodies dead like + // `bun_debug` does; live, `ScopedLogger::is_visible()` would scan the + // environment through the Highway FFI, which Miri can't call. rustflags.push("--check-cfg=cfg(bun_logs)"); if (cfg.logs) { rustflags.push("--cfg=bun_logs"); @@ -511,11 +508,9 @@ export function cargoBuildInvocation(cfg: Config): CargoInvocation { // file:line server-side, so the panic call site is recoverable from the trace // without embedding the location in the binary — same as the Zig build, which // had ~0 embedded source paths. Kept off for debug and `release-assertions` - // where panic messages are read locally, and for `--logs=on` builds: the - // `bun_jsc::mark_binding()` / test-runner `group::begin()` loggers print - // `Location::caller()`, which this flag turns into `:0`, and a - // build that embeds every `scoped_log!` format string is not the one the - // size matters for. Nightly-only; the pinned toolchain is nightly. + // where panic messages are read locally, and for logs builds, whose + // `mark_binding()`-style loggers print `Location::caller()` (`:0` + // under this flag). Nightly-only; the pinned toolchain is nightly. if (cfg.release && !cfg.assertions && !cfg.logs) { rustflags.push("-Zlocation-detail=none"); } diff --git a/src/bun_core/Global.rs b/src/bun_core/Global.rs index 2e8057bab01c..0bfc33f65c4c 100644 --- a/src/bun_core/Global.rs +++ b/src/bun_core/Global.rs @@ -435,10 +435,8 @@ macro_rules! mark_binding { $crate::mark_binding!(::core::panic::Location::caller().file()) }; ($fn_name:expr) => { - // Opt-in via BUN_DEBUG_JSC=1. The `JSC` scope is owned by bun_core. Gate on - // `env::ENABLE_LOGS` like `scoped_log!` does, never on a Cargo feature, - // since `cfg!(feature = ..)` is resolved against the *calling* crate and - // would warn (or silently no-op) in crates without it. + // Opt-in via BUN_DEBUG_JSC=1. Same gate as `scoped_log!`; not a Cargo + // feature, which `cfg!` would resolve against the *calling* crate. if $crate::env::ENABLE_LOGS && $crate::Global::JSC_SCOPE.is_visible() { $crate::Global::JSC_SCOPE.log(::core::format_args!( "[JSC] {} ({}:{})\n", diff --git a/src/bun_core/env.rs b/src/bun_core/env.rs index bf4aafc6440a..112191304229 100644 --- a/src/bun_core/env.rs +++ b/src/bun_core/env.rs @@ -65,10 +65,8 @@ pub(crate) const CANARY_REVISION: &str = if IS_CANARY { }; pub const DUMP_SOURCE: bool = IS_DEBUG && !IS_TEST; pub const BASE_PATH: &[u8] = build_options::BASE_PATH; -/// The build's `logs` option (`--cfg=bun_logs`, set by `scripts/build/rust.ts` -/// from `cfg.logs`): on by default in Debug builds and in `release-assertions`, -/// off in plain release, `--logs=on|off` overrides. Independent of `IS_DEBUG`. -/// Compile-time gate for `scoped_log!` and the other `BUN_DEBUG_*` loggers. +/// The build's `logs` option (`--cfg=bun_logs` from scripts/build/rust.ts; defaults +/// to `IS_DEBUG`, `release-assertions` and `--logs=on|off` override). Gates `scoped_log!`. 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; diff --git a/src/bun_core/output.rs b/src/bun_core/output.rs index 24773ea8d03d..438aa57a4059 100644 --- a/src/bun_core/output.rs +++ b/src/bun_core/output.rs @@ -1544,12 +1544,10 @@ macro_rules! declare_scope { #[macro_export] macro_rules! scoped_log { ($scope:path, $fmt:expr $(, $arg:expr)* $(,)?) => { - // Gate on `env::ENABLE_LOGS` (the build's `logs` option, passed as - // `--cfg=bun_logs` by scripts/build/rust.ts) so builds without it - // dead-strip the body. Not `IS_DEBUG`: `release-assertions` and - // `--logs=on` carry logs in a non-Debug build, `--logs=off` drops them - // from a Debug one. Do NOT gate on a Cargo feature: there is no - // `debug_logs` feature and §Forbidden bans silent no-ops. + // Gate on `env::ENABLE_LOGS` (the build's `logs` option, which the + // `--logs` / `release-assertions` configs set independently of + // `IS_DEBUG`) so builds without logs dead-strip the body. Do NOT gate + // on a Cargo feature: there is none and §Forbidden bans silent no-ops. if $crate::env::ENABLE_LOGS && $scope.is_visible() { const __NL: &str = $crate::output::_needs_nl($crate::pretty_fmt!($fmt, false)); // Branch on ANSI *before* `format_args!` so each `$arg` evaluates @@ -2594,10 +2592,8 @@ fn init_scoped_debug_writer_at_startup() { } fn scoped_writer() -> QuietWriter { - // All callers are already gated on `Environment::ENABLE_LOGS`; this is a - // Debug-build self-check (release-asan enables `debug_assertions` with - // `ENABLE_LOGS == false`, so keying on `debug_assertions` would turn it - // into a guaranteed abort there). + // Callers are gated on `ENABLE_LOGS`; this self-check is `bun_debug`, not + // `debug_assertions`, which release-asan enables with logs off. #[cfg(bun_debug)] if !Environment::ENABLE_LOGS { unreachable!("scopedWriter() should only be called when logs are enabled");