Rewrite Bun in Rust#30412
Conversation
|
Updated 1:06 AM PT - May 14th, 2026
✅ @Jarred-Sumner, your commit ed1a70f81708d7d137de8de057d11668c5f4e220 passed in 🧪 To try this PR locally: bunx bun-pr 30412That installs a local version of the PR into your bun-30412 --bun |
|
Caution Review failedThe pull request is closed. Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (218)
Disabled knowledge base sources:
WalkthroughThe repository migrates from Zig to a Rust-first build: adds a Cargo workspace, replaces Zig build/CI with Rust cargo build integration and artifacts, rewires codegen for Rust, adjusts dependencies and flags, removes Zig tooling, adds numerous Claude automation workflows, and introduces new scripts and docs. ChangesRust-first build and automation stack
Possibly related PRs
✨ Finishing Touches⚔️ Resolve merge conflicts
|
| inherits = "release" | ||
| lto = "thin" | ||
| codegen-units = 16 | ||
|
|
||
| # Release with debuginfo retained for `perf record` / flamegraphs. Same opt | ||
| # level as release; binary is large but profilable. |
There was a problem hiding this comment.
🟡 nit: [profile.release-profiling] sets debug = "line-tables-only" and strip = "none", which are exactly the values [profile.release] already defines (lines 141-142), so this profile is a no-op alias. The comment "binary is large but profilable" suggests the intent was debug = true (full DWARF) — and note that cargoProfile() in scripts/build/rust.ts:67-68 only ever selects dev or release, so this profile is currently unreachable from the build system anyway. Consider setting debug = true or dropping the profile.
Extended reasoning...
What the bug is
Cargo.toml defines a custom [profile.release-profiling] profile at lines 153-156:
[profile.release-profiling]
inherits = "release"
debug = "line-tables-only"
strip = "none"But the base [profile.release] profile at lines 138-142 already sets exactly these values:
[profile.release]
lto = "fat"
codegen-units = 1
debug = "line-tables-only"
strip = "none"Since release-profiling inherits from release and then "overrides" two keys with the identical values they already have, the resulting profile is byte-for-byte identical to release. It changes nothing.
Why the comment contradicts the config
The doc comment above the profile (lines 151-152) reads:
Release with debuginfo retained for
perf record/ flamegraphs. Same opt level as release; binary is large but profilable.
"Binary is large" implies the profile was intended to emit more debuginfo than the regular release build — i.e., debug = true (full DWARF, level 2). With debug = "line-tables-only" the binary is the same size as release because it is release. Compare with the sibling [profile.release-dev] at lines 146-149, which actually overrides lto and codegen-units to differ from its parent — that's the pattern release-profiling was presumably meant to follow.
Step-by-step proof
- Cargo resolves
release-profilingby starting fromrelease:{lto: "fat", codegen-units: 1, debug: "line-tables-only", strip: "none", opt-level: 3, ...}. - It then applies the two explicit keys from
release-profiling:debug = "line-tables-only"(unchanged) andstrip = "none"(unchanged). - Final resolved profile: identical to step 1.
cargo build --profile=release-profilingproduces the same artifact ascargo build --release(modulo the output directory name).
It's also unreachable
scripts/build/rust.ts:67-68 defines:
function cargoProfile(cfg: Config): { name: string; subdir: string } {
return cfg.buildType === "Debug" ? { name: "dev", subdir: "debug" } : { name: "release", subdir: "release" };
}This only ever returns dev or release, and a repo-wide grep for release-profiling finds only the definition site in Cargo.toml. So even if the profile did differ, nothing in the build system can select it.
Impact
None — this is purely redundant/misleading config. Nothing breaks: line-tables-only is actually sufficient for basic perf record/flamegraph symbolication, so anyone who manually ran cargo build --profile=release-profiling would get a working (if not ideally detailed) profiling build. The only harm is that the comment misdescribes what the profile does, and a future reader might waste time figuring out why "the profiling build" is no different from release.
How to fix
Either:
- Set
debug = true(ordebug = 2) to match the comment's intent and get full DWARF for richer profiling (inlined frames, local variables), or - Delete the
[profile.release-profiling]section entirely since[profile.release]already retains line tables and is unstripped.
If kept, also consider wiring it into cargoProfile() so it's actually selectable.
There was a problem hiding this comment.
Actionable comments posted: 78
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
docs/RUST_PATTERNS.md (1)
486-488: 🧹 Nitpick | 🔵 Trivial | 💤 Low valueMinor: Missing trailing newline.
The file ends at line 488 without a trailing newline, which can cause issues with some tools and
git diffoutput.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/RUST_PATTERNS.md` around lines 486 - 488, The file ends without a trailing newline; add a single newline character at EOF to terminate the file. Open docs/RUST_PATTERNS.md, locate the table rows such as the line containing "`ptr::read(self) to clone`" (near the `*mut c_void` ctx callback / `ptr::read(self)` entries) and ensure the file ends with a newline, then save and commit the change.src/aio/Cargo.toml (1)
1-28: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winAdd workspace lint opt-in to
bun_aio.Workspace-defined lints from
[workspace.lints.clippy]require explicit opt-in in each member crate. Without adding[lints] workspace = trueto this manifest, theptr_as_ptr = "warn"audit configured in the root workspace will not run on this crate.Suggested fix
[package] name = "bun_aio" version.workspace = true edition.workspace = true +[lints] +workspace = true + [lib] path = "lib.rs"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/aio/Cargo.toml` around lines 1 - 28, The crate manifest for package "bun_aio" (Cargo.toml) is missing the workspace lints opt-in so workspace-defined Clippy lints like ptr_as_ptr won’t apply; update the Cargo.toml for the bun_aio crate by adding a top-level [lints] table with workspace = true (i.e., add `[lints]` followed by `workspace = true`) so the workspace lints declared in the root are opt‑in for this member crate.scripts/build/bun.ts (1)
470-474:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPreserve Rust-only exports at link time.
libbun_rust.ais linked as a plain archive in both full and link-only modes, so any#[no_mangle]member that is only surfaced through the dynamic-list/N-API export path and has no static ref from the C++ objects can still be discarded.scripts/build/rust.tsalready providesrustLinkFlags()for the required whole-archive/force-load handling; use that helper here and track the Rust archive as a dependency instead of feeding it as a plain$inarchive.💡 Suggested wiring
-import { emitRust, rustLibPath } from "./rust.ts"; +import { emitRust, rustLibPath, rustLinkFlags } from "./rust.ts";- const exe = link(n, cfg, exeName, [...allObjects, ...rustObjects, ...windowsRes], { + const exe = link(n, cfg, exeName, [...allObjects, ...windowsRes], { libs: depLibs, - flags: [...flags.ldflags, ...systemLibs(cfg), ...manifestLinkFlags(cfg), ...shims.ldflags], - implicitInputs: [...linkImplicitInputs(cfg), ...shims.implicitInputs], + flags: [ + ...flags.ldflags, + ...rustLinkFlags(cfg, rustObjects), + ...systemLibs(cfg), + ...manifestLinkFlags(cfg), + ...shims.ldflags, + ], + implicitInputs: [...linkImplicitInputs(cfg), ...rustObjects, ...shims.implicitInputs], });- const exe = link(n, cfg, exeName, [archive, ...rustObjects, ...windowsRes], { + const exe = link(n, cfg, exeName, [archive, ...windowsRes], { libs: depLibs, - flags: [...flags.ldflags, ...systemLibs(cfg), ...manifestLinkFlags(cfg), ...shims.ldflags], - implicitInputs: [...linkImplicitInputs(cfg), ...shims.implicitInputs], + flags: [ + ...flags.ldflags, + ...rustLinkFlags(cfg, rustObjects), + ...systemLibs(cfg), + ...manifestLinkFlags(cfg), + ...shims.ldflags, + ], + implicitInputs: [...linkImplicitInputs(cfg), ...rustObjects, ...shims.implicitInputs], });Also applies to: 610-614
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.claude/workflows/lifetime-classify.workflow.js:
- Line 11: The REPO constant is hardcoded to "/root/bun-5" making the workflow
environment-specific; change the code that defines REPO so it reads from a
configurable source (e.g., process.env.REPO_PATH or a workflow input like
inputs.repo) with a sensible fallback and a clear warning/error if neither is
provided. Specifically replace the literal const REPO = "/root/bun-5"; with
logic that checks process.env.REPO_PATH || workflow input (e.g., inputs.repo) ||
a default, and ensure any code that uses REPO still works; add
validation/logging so missing configuration fails fast or logs the chosen path.
- Around line 139-141: The consensus selection can become undefined when
votes.find(... )?.correct_class returns undefined; update the logic that
computes consensus so it falls back to the original f.class when no valid
correct_class is found. Concretely, inside the block that computes const refutes
= votes.filter(...).length and const consensus = ..., replace the ternary branch
that uses votes.find(v => v && v.refuted)?.correct_class with a safe selection
(e.g., assign foundCorrect = votes.find(v => v && v.refuted)?.correct_class and
then set consensus = refutes >= 2 ? (foundCorrect ?? f.class) : f.class) so
final_class is never undefined; ensure you update usages of consensus and return
{ ...f, refutes, final_class: consensus, verified: true } unchanged.
- Line 106: The current then handler assumes r.fields exists and calls
r.fields.map(...), which can throw if r is an object without fields; update the
handler that returns fields for the promise (the arrow function using r and
f.zig/f.crate) to guard/normalize r.fields first — e.g., check
Array.isArray(r.fields) or use optional chaining/defaulting so you only call
.map when r.fields is an array and otherwise return an empty array; keep the
mapping that spreads each field and adds file: f.zig and crate: f.crate.
- Around line 157-165: The return block can produce NaN when final.length === 0
and repeats the same unsafe toFixed usage noted earlier; change the computation
of unknown_rate to guard against division-by-zero by computing unknown_rate =
final.length === 0 ? "0.000" : ((byClass.UNKNOWN || 0) /
final.length).toFixed(3), and ensure the returned value is a string/number
consistently; keep the rest of the fields (total_fields, by_class,
verified_count, overturned, tsv_preview, tsv) unchanged and reference the
variables final, byClass, unknown_rate, verified and tsv_preview when applying
the fix.
In @.claude/workflows/phase-b0-movein.workflow.js:
- Line 37: The workflow currently references hardcoded line ranges in PORTING.md
which can become stale; update the workflow that reads PORTING.md to locate
sections by header names instead of fixed line ranges (search for the
"§Dispatch" and "§Concurrency" headers), or add a validation step that confirms
the text found at the referenced ranges contains the expected "§Dispatch" and
"§Concurrency" headers and fails with a clear error if not, or alternatively
document that the PORTING.md line numbers must remain stable—implement one of
these fixes and ensure the code uses the "§Dispatch" and "§Concurrency" markers
to locate content reliably.
- Line 36: The workflow references the grep against movein-skipped.txt but
there's no producer for that file; update the phase-b0 move-in workflow to
either (A) add a setup step that materializes the move-out `skipped` array into
the expected movein-skipped file (so the grep -iE
'\b(${t.name}|${t.cyclebreak_key}|bun_${t.cyclebreak_key})\b' has real data), or
(B) add robust error-handling before the grep (ensure the file exists or create
an empty placeholder from the move-out `skipped` data, or short-circuit the grep
with a safe fallback) so explicit move-out requests aren’t silently missed.
Ensure you modify the step that runs the grep command and/or the move-out
producer that exposes the `skipped` array to produce the expected artifact.
In @.claude/workflows/phase-b1-tier.workflow.js:
- Line 10: The code assumes all input crates share the same tier by using
CRATES[0].tier for the batch log and final output; add a validation in the
beginning of the workflow to verify homogeneity (e.g., compute a set of tiers
from args.crates/CRATES and if its size > 1 throw or normalize), and then use a
safe value (the single tier) only after validation; update references where
CRATES[0].tier is used (the initial log call and the final batch summary around
line 53) to either compute the tier from the validated set or to report that
multiple tiers were received if you choose to error instead.
In @.claude/workflows/phase-b2-fill.workflow.js:
- Line 21: The dir_for helper currently maps a few special crate names and
otherwise strips a bun_ prefix, which can produce incorrect directories for
unexpected crate names; update dir_for to validate inputs and document the
mapping: add a short comment explaining the special cases (bun_str→"string",
bun_core, bun_alloc) and the fallback behavior, and implement explicit
validation in the dir_for function to either (a) check the crate name against an
allowed set of known crate keys and throw or log an error when unknown, or (b)
return a clearly marked fallback only after confirming the name begins with
"bun_"—use the existing dir_for identifier to locate and change the mapping
logic and add the comment nearby.
In @.claude/workflows/phase-b2-fix-bugs.workflow.js:
- Line 7: The comment referencing a possible input "bugs_path: \"/tmp/...\"" is
inconsistent with the actual code which only reads args.bugs; either remove that
misleading comment or implement support for loading bugs from a file: add
handling for args.bugs_path in the workflow's argument parsing (check
args.bugs_path, read and parse the file into the same data structure as
args.bugs), or delete the alternate example from the comment so it matches the
existing behavior (update the comment where "// args: { bugs_path: \"/tmp/...\",
or bugs: [{module,fn,what,fix}] }" appears to only show supported inputs).
- Around line 38-39: The template currently injects the full bugs array via
"${JSON.stringify(bugs, null, 2)}", which can exceed token limits for large
lists; modify the workflow to check bugs.length and either truncate or summarize
before embedding: e.g., if bugs.length > MAX_BUGS include a short summary ("N
bugs, showing first M") plus JSON.stringify of the first M entries, or call a
helper like summarizeBugs(bugs) to produce a condensed text; update the template
insertion to use the truncated/summarized string instead of the full JSON and
ensure MAX_BUGS and summarizeBugs are defined/adjustable.
- Line 23: Validate b.module before using b.module.split("/") to compute crate:
ensure b.module is a non-empty string and contains the expected "/" delimiter
(or match a regex like /^[^\/]+\/.+$/) and handle failures (e.g., log a warning
and skip grouping, throw, or default to a safe value) instead of blindly doing
const crate = b.module.split("/")[0]; update the code around the crate
extraction to perform the check, use the validated part when present, and add a
clear log message documenting the expected "crate/..." format when invalid.
In @.claude/workflows/phase-b2-keystone.workflow.js:
- Line 17: The file contains top-level return statements (e.g., the check "if
(!TASK) return { error: 'args.task required' }") which are invalid in ES
modules; wrap the executable workflow code in a function or immediately-invoked
async function (IIFE) that the workflow runtime expects, move all top-level
logic (including checks at TASK and the other return sites referenced) into that
function, and replace those top-level "return" statements with returns from that
function (or throw errors/resolve values from the function) so no return appears
at module top-level; update the checks around the TASK variable and the other
early-exit points so they return from the newly created function scope.
In @.claude/workflows/phase-d-blocked-on-resolve.workflow.js:
- Around line 81-85: The workflow contains literal placeholders in the
PortUpstream command steps (e.g., '<method_name>', '<crate>', '<caller_crate>')
which make automated grep and cargo checks unusable; update the commands in the
PortUpstream steps to interpolate the actual variables (use ${g.symbol} for the
method name, ${g.upstream_crate} or a fallback for the upstream crate instead of
'<crate>', and a computed ${g.caller_crate} or caller list instead of
'<caller_crate>'), so step 1 runs grep -rn "${g.symbol}" src/**/*.zig, step 3
runs cargo check -p ${g.upstream_crate} (or resolved crate name), and step 4
runs cargo check -p for each resolved caller crate; ensure all placeholder
tokens in those commands are replaced with the corresponding ${g.*} variables
used elsewhere in the workflow.
In @.claude/workflows/phase-d-build-queue.workflow.js:
- Line 65: The workflow string in the HARD constant currently stages all of
'src/' and uses git merge with -X ours which can silently discard concurrent
agents' changes; update the commit/push command in
.claude/workflows/phase-d-build-queue.workflow.js (HARD) to stage and commit
only the exact touched paths (not "src/") and remove the `-X ours` merge flag so
merges are not force-resolved locally; instead have the orchestrator perform the
final merge or require each agent to operate on an isolated branch/worktree
before pushing.
In @.claude/workflows/phase-d-bundler-shard.workflow.js:
- Line 102: The prompt currently returns a key named blocks_on which mismatches
the FIX_S schema's blocked_on field; update the template string "Return
{file:\"${u.file}\", before:${u.n}, fns_touched:[...], blocks_on:[...], notes}."
to use blocked_on instead of blocks_on so agents produce the recognized key
(referencing FIX_S).
- Around line 51-114: The agent prompt returns a field named "blocks_on" but the
schema expects "blocked_on": update the return template inside the inner agent
call (the agent invoked for each unit with label starting with "fix:")—replace
"blocks_on:[...]" with "blocked_on:[...]" in the final Return {...} string (the
template that currently reads Return {file:"${u.file}", before:${u.n},
fns_touched:[...], blocks_on:[...], notes}) so the returned object matches the
FIX_S schema.
In @.claude/workflows/phase-d-recursive-ungate.workflow.js:
- Line 100: The HARD rule string currently contains an embedded "Commit+push
with retry" shell snippet that runs git pull -X ours and pushes, which allows
concurrent workers to merge/clobber siblings; update the HARD constant (the
template string assigned to HARD in
.claude/workflows/phase-d-recursive-ungate.workflow.js) to remove the
pull/merge/push steps and only include commit-only instructions (e.g.,
add/commit retry loop without git pull or git push), or otherwise document that
workers must not perform pull/merge/push and that a single orchestrator will
serialize branch integration; keep the rest of the HARD text intact and ensure
the offending git pull -X ours / push sequence is not present.
In @.claude/workflows/phase-e-body-port.workflow.js:
- Line 63: The workflow's HARD constant contains a commit command that does git
-c core.hooksPath=/dev/null add -A 'src/' which can accidentally stage unrelated
edits; change the worker so it tracks exactly which files it modifies and
replace that command with one that git-adds only the recorded paths (e.g., git
-c core.hooksPath=/dev/null add <each-recorded-path> && git -c
core.hooksPath=/dev/null commit -q -m "phase-e(port): <file> real bodies").
Update the code that builds the HARD string (reference the HARD constant in
.claude/workflows/phase-e-body-port.workflow.js) to remove the add -A 'src/' and
instead use the recorded-files list when invoking git, ensuring hooksPath
behavior is preserved and only the touched files are committed.
- Line 78: The module contains top-level return statements (e.g., the line with
"if (!survey || survey.total === 0) return { rounds: round, done: true, history
};" and other returns inside the same module-scope loop) which are invalid in ES
modules; wrap the entire executable logic block (the loop and surrounding code
currently at module scope) inside a named async function (for example
runPhaseEBodyPort or executeWorkflow) and export or invoke that function from
the module entry point, or alternatively replace those top-level return
statements with control-flow that sets a result variable and breaks the loop;
update any callers to call the new function name (e.g., runPhaseEBodyPort) so no
return remains at top-level.
In @.claude/workflows/phase-e-test-bringup.workflow.js:
- Line 126: The workflow hard-coded policy (HARD constant) instructs parallel
agents to pull and push to the same branch using the merge strategy `-X ours`
inside the retry loop (the commit+push retry snippet referencing
`origin/claude/phase-a-port`), which allows sibling fixes to be silently
overwritten; change the workflow to avoid `-X ours` and instead give each agent
a unique branch or commit ref (e.g., include an agent/PR-specific suffix) and
push to that branch, or make pulls fail on conflict so the agent rebases/merges
locally before pushing; update the retry loop logic that references the `commit
-m`/`pull --no-rebase --no-edit -X ours`/`push origin claude/phase-a-port`
sequence to use agent-scoped branch names and remove `-X ours` (or switch to
explicit rebase/merge conflict handling) to prevent nondeterministic overwrites.
- Around line 200-206: The reproduction command is hard-coded using the ternary
on u.sample.name (the `${u.sample.name.startsWith("e3") ? "test /tmp/e3/" :
"test /tmp/e1.test.ts"}` expression), so it may not re-run the actual failing
probe; update the workflow step in
.claude/workflows/phase-e-test-bringup.workflow.js to use the real recorded
probe command (use the probe/failure command field produced when the probe
failed — e.g. replace the ternary with the failing probe command variable such
as u.probe.command or u.sample.probeCommand, with a safe fallback to the current
expression) so the "After fix" rebuild actually re-runs the exact failing case.
- Around line 131-224: The workflow body must be moved into an exported async
entry function so the top-level return statements become valid; wrap the entire
loop and subsequent logic (the for loop that uses phase(), agent(), probes/fixes
calculation, and the final return) inside an exported async function (e.g.,
export async function runWorkflow() or export default async function main()) and
ensure any module-level constants/metadata remain outside; update any internal
references (phase, agent, MAX_ROUNDS, TARGET_RUNG, history, etc.) to use the
same scope and replace the top-level returns with returns from that function so
callers import/await the exported async function.
In @.claude/workflows/phase-f-accessor-sweep.workflow.js:
- Around line 106-114: The workflow's Step 3 is overprescriptive: it assumes
every pointer accessor returns &T and rewrites unsafe usages to self.x(), which
is incorrect for accessors that return Option<&T> or raw pointers; update the
guidance so that when Step 2 chooses Option<&T> or a raw-pointer accessor (e.g.
x() -> Option<&T>, x_ptr() -> *mut T) the caller-rewrite guidance uses the
corresponding safe pattern (handle Option via match/unwrap or map, and keep
raw-pointer uses as-is or document unsafe deref patterns) and only suggest
replacing unsafe { &*self.x } with self.x() for the specific case where the
accessor is defined as fn x(&self) -> &T; mention x_mut() and x_ptr() by name so
reviewers know which rewrite applies to which accessor shape.
- Around line 179-181: The code marks every surveyed struct as completed by
doing for (const s of survey.structs) done_structs.add(s.struct) before the
pipeline succeeds; move this mutation so structs are only added to done_structs
after the accessor/retry/fix pipeline actually succeeds. Remove or defer the
loop that adds to done_structs from this location and instead add each s.struct
to done_structs in the success branch of the pipeline handler (the code path
that confirms review accepted and fix step completed), leaving history.push({
round, structs: survey.structs.length, derefs_targeted: removed }) unchanged or
adjust it to reflect only attempted vs completed counts as needed. Ensure you
update any unit tests that check done_structs state to assert addition happens
only on pipeline success.
In @.claude/workflows/phase-f-test-swarm.workflow.js:
- Around line 64-77: The workflow uses the nonstandard runner
"./target/debug/bun-rs test ${area.glob}" for pass/fail and conflict resolution
which can diverge from repo expectations; replace all invocations of that exact
command with "bun bd test ${area.glob}" (and likewise use "bun bd test" wherever
./target/debug/bun-rs test is used in the loop or BUGHUNT steps) so decisions
use the repo's debug-test path, and keep the existing verification guidance
around USE_SYSTEM_BUN=1 bun test vs bun bd test; ensure the git pull/cherry-pick
loop still runs but bases its pass/fail checks on bun bd test rather than
./target/debug/bun-rs test.
- Line 122: The top-level return causes a parse error because this file is an ES
module; wrap the executable body into an exported async function (e.g., export
async function runPhaseTest()) that contains the phase() calls, the await
parallel(...) call, the for loop, and the final return statement; ensure the
function returns the object { areas: AREAS.length, all_pass: ok.length, partial:
partial.length, results, cherry } and keep references to AREAS, ok, partial,
results, cherry and calls to phase() and parallel() unchanged so callers can
invoke the exported function from the workflow runtime.
In @.claude/workflows/phase-g-mega-swarm.workflow.js:
- Line 113: The build-wait step currently reads global files
/tmp/mega-diag/.built and /tmp/mega-diag/.build.log which conflicts across
swarms; change the command to use the workflow's GDIAG namespace (replace
hardcoded /tmp/mega-diag paths with ${GDIAG}/.built and ${GDIAG}/.build.log) and
update the inline message string accordingly (the template using GDIAG and the
backtick command that contains `mkdir -p ${GDIAG}; for i in $(seq 1 60); do test
-f /tmp/mega-diag/.built && break; sleep 5; done; test -f /tmp/mega-diag/.built
&& tail -3 /tmp/mega-diag/.build.log`) so the sentinel and log are created/read
under ${GDIAG} instead of the global /tmp path.
- Line 163: The file contains top-level return statements inside a module-scope
loop (returning { rounds: round, done: true, passing: survey.passing, total:
survey.total, history }) which makes the module syntactically invalid; fix by
removing the top-level returns and either (A) wrap the loop and its logic in a
function (e.g., async function runPhaseGMegaSwarm() { ... } and return the
result from that function) or (B) build the result object (rounds/round, done,
passing, total, history) into a variable and export it (export const result =
...) instead of returning at module scope so the module parses as a valid ES
module. Ensure you update both places that currently use top-level return so
they follow the same approach.
In @.claude/workflows/phase-g-test-swarm-v3.workflow.js:
- Around line 201-206: The workflow currently replays hard-coded paths (/tmp/e3/
for e3* failures and /tmp/e1.test.ts for everything else) which skips many
failing cases; change the reproduction step to derive the replay path from the
actual failing probe identifier instead of those fallbacks—use the failing-test
variable/argument (the probe name, pattern or file provided by the test harness)
to construct the replay path and fall back only if that variable is empty, and
update the logic that checks for "e3*" / "e1" to instead use the real
failingProbe/failingTestPath value so cases like e1-fail, e1-hooks, e2-multi,
etc. are re-run.
- Around line 146-159: The shard slice is only described but never written to
disk or a variable, so the probe loop's "for f in $(cat your-slice)" has nothing
to read; materialize the slice before the probe by running the sed selection
used in the prompt and saving it (e.g., write the sed range expression from the
original selection into a file like ${DIAG}/your-slice or into a shell
variable), then have the probe loop iterate that concrete file/variable; update
references to "your files"/"your-slice" to the same identifier (e.g.,
${DIAG}/your-slice) so the for loop actually enumerates the selected test
filenames.
In @.claude/workflows/phase-g-test-swarm.workflow.js:
- Line 99: The module contains top-level return statements (e.g., the return
that produces { rounds: round, done: true, completing: survey.completing.length,
total: survey.total, history } and the other return near the end) which are
invalid in ES modules; wrap the logic that performs the loop and computes
rounds/round/history/survey into a function (sync or async) and return from that
function, or instead build the result into a local variable and export it (or
export an async function that returns it), ensuring all references to round,
rounds, survey, history now live inside that function body so no return occurs
at module scope.
In @.claude/workflows/phase-h-deep-dive.workflow.js:
- Line 91: The top-level "return { results: results.filter(Boolean) }" is
invalid in an ES module; move the return into a function that the workflow
runtime calls (for example wrap the workflow body in an exported async function
like runPhaseDeepDive or an exported handler function) so that the line becomes
a return inside that function; update any references to the workflow entrypoint
accordingly so the workflow runtime invokes the new function (look for the
top-level return and the symbol "results" to relocate into the new function).
In @.claude/workflows/phase-h-idioms-audit.workflow.js:
- Around line 144-183: The 2-vote unanimity is too strict: change the post-vote
logic around real_votes in the Verify phase (the code building the returned
object where verified is currently set using real_votes >= 2) to distinguish
"verified" (real_votes >= 2) from a new "disputed" state when real_votes === 1
(or add a status enum like verified|disputed|rejected). Update the returned
object produced in the pipeline promise (the block that computes real_votes,
votes, refutes, perf, better_fix) to keep verified: real_votes >= 2 and add
disputed: real_votes === 1 (or status: real_votes >=2 ? "verified" : real_votes
===1 ? "disputed" : "rejected") so a single-yes case is recorded rather than
silently treated as rejected.
- Around line 119-125: The key normalization for grouping patterns (variable
byPattern and computed key from f.pattern) is too aggressive—removing all
non-letters and truncating causes different phrases to collide; update the
normalization to preserve word boundaries and spaces before truncation (e.g.,
extract/keep word tokens, collapse multiple spaces, trim, then slice) so
distinct phrases like "*mut T fields with no invariant" and "*mut T fields with
no lifetime" remain distinct; apply this change where key is computed (the
expression using f.pattern.toLowerCase().replace(...).slice(...)) so downstream
grouping uses the word-preserving normalized key.
- Around line 218-222: Add an inline comment immediately above the git command
that uses "-c core.hooksPath=/dev/null" in
.claude/workflows/phase-h-idioms-audit.workflow.js explaining why hooks are
being bypassed for this automated workflow (e.g., to avoid local developer hooks
blocking CI-generated commits, ensure reproducible automation, and note that
this is intentional and reviewed); reference the exact command string "git -c
core.hooksPath=/dev/null add 'docs/' && git commit -q -m ..." so reviewers can
find it, and keep the comment concise and clear about the safety/approval
rationale and any intended exceptions or follow-up (no code changes beyond the
comment).
- Around line 86-91: The workflow hardcodes the repo path "/root/bun-5" inside
the agent prompt (and elsewhere); update the prompt construction to use a
configurable path instead (e.g., read from an environment variable like
process.env.REPO_DIR with a sensible default such as '.' or the repository root)
so the agent call in phase("Scan") and other agent prompts use that variable;
search for the string "/root/bun-5" referenced near the agent(...) calls and
replace with the variable/interpolated value so SCAN_S, TOP_N_CRATES and other
uses remain unchanged while the repo path becomes portable across environments.
In @.claude/workflows/porting-md-zigleakage.workflow.js:
- Around line 90-134: The comment and runtime log are out of sync with the SEED
array: SEED has three entries but the surrounding comment says "two user-flagged
issues" and the log formats "2 seed". Update the human-facing comment near the
SEED declaration to reflect three seeds (e.g., "three user-flagged issues" or
pluralize), and update the log call that prints `audit: ${all.length} raw
findings (incl. 2 seed)` to use the correct seeded count or pluralized wording
(e.g., "(incl. 3 seed)" or better "(incl. 3 seeds)"); locate and change the text
near the SEED constant and the log(...) call in this file to keep them
consistent.
- Around line 198-212: The module currently mixes a top-level export (export
const meta) with top-level await and a bare return; wrap all executable workflow
logic (the phase(...), agent(...) calls, any awaits and the final return object
that builds raw_findings/confirmed/by_dimension/refuted/patch/confirmed_detail)
inside an exported async function: add export default async function() { /* move
all code that uses await, phase(), agent(), and the return {...} into this
function */ } while keeping export const meta at module scope so the runtime can
import metadata and then invoke the default async function to execute the
workflow.
In `@bench/bundle/index.ts`:
- Around line 1-100: The file exports Three1..Three100 from non-existent
packages (Three1..Three100 symbols in bench/bundle/index.ts) while package.json
only provides "three"; change the exports to reference the actual installed
module instead of non-existent packages—e.g., re-export the real "three" module
for the intended number of copies (per README, export Ten aliases like
Three1..Three10 from 'three' and remove or stop exporting Three11..Three100), or
else add proper aliases/path mappings if you truly need distinct package names;
update bench/bundle/index.ts accordingly so all export statements import from
'three' (or match your tsconfig/packaging aliases).
In `@CLAUDE.md`:
- Line 1: Add a top-level H1 heading as the very first line of the file to
satisfy markdownlint MD041: insert a line like "# Bun" (or another appropriate
project title) above the existing body text so the file begins with an H1
header; ensure you update the first line in CLAUDE.md (where the current body
text starts) rather than appending the heading later.
In `@docs/BABYLIST_REPLACEMENT.md`:
- Line 3: The docs file BABYLIST_REPLACEMENT.md contains a host-specific repo
path `/root/bun-5`; replace that machine-local path with a repo-relative
reference (e.g., `.` or the repository name) and optionally include the commit
hash or tag for reproducibility, ensuring the shared document no longer contains
any absolute filesystem paths.
- Around line 39-41: Add a blank line immediately before the fenced Rust block
that contains the `pub type Arena = bumpalo::Bump;` (and related `pub type
MimallocArena` alias) so the fenced code block has a preceding empty line to
satisfy markdownlint MD031; ensure the fence keeps the language tag (```rust)
and the existing lines `pub type Arena = bumpalo::Bump;` and `pub type
MimallocArena = bumpalo::Bump;` remain unchanged.
In `@docs/CI_RUST_MIGRATION.md`:
- Around line 55-58: The fenced code block showing artifact paths (lines
containing "rust-target/<triple>/<profile>/libbun_rust.a.gz" and
"rust-target/<triple>/<profile>/bun_rust.lib") should include a language
specifier (e.g., "text") on the opening triple-backtick; update the block
delimiter from ``` to ```text so the example is treated as plain text in
rendered docs.
In `@docs/LAYERING_AUDIT.md`:
- Around line 30-324: The markdown headings in the audit section lack blank
lines before and after the level-3 headings (the lines starting with "###" such
as "### `src/bun_core/util.rs:1018` — `__bun_fd_path` / `__bun_fd_path_w`" and
all subsequent `###` entries), which triggers MD022; fix by inserting a single
blank line before and after each "### ..." heading throughout the document so
each heading is separated from surrounding paragraphs and lists, and re-run
markdownlint to verify the MD022 warnings are resolved.
In `@docs/rust-migration-spine.html`:
- Line 2: The root HTML element is missing a document language attribute; update
the root <html> element in the docs/rust-migration-spine.html file to include
lang="en" (i.e., change <html> to <html lang="en">) so assistive technologies
and the HTML linter recognize the document language.
In `@docs/rust-migration-tree.md`:
- Around line 59-64: The fenced code block containing the three file mapping
lines (starting with "src/http/HTTPThread.zig ←→ crates/bun_http/src/thread.rs"
and including "src/runtime/api/glob.zig ..." and "src/jsc/JSRef.zig ...") is
missing a language identifier; update the opening fence to include a language
(e.g., use ```text) so the block becomes a labeled fenced code block to enable
proper syntax highlighting and satisfy the markdownlint rule.
In `@docs/rust-rewrite-plan.md`:
- Around line 97-107: The fenced ASCII diagram blocks in
docs/rust-rewrite-plan.md (the block showing "C++ JSCell wrapper ... C-ABI
extern fns ... Native struct") and the other fenced blocks around lines 613-667
are missing a language tag and trigger MD040; update those backtick-fenced
blocks by adding a language identifier (e.g. text or rust) after the opening ```
so the diagram and any code sections (search for the ASCII diagram and other
```...``` blocks) are explicitly marked (e.g. ```text or ```rust) to satisfy
markdownlint and improve rendering.
- Around line 304-311: Several table cells contain literal pipe characters which
break the Markdown table; update the affected rows (the ones describing
hasPendingActivity, JSRef {weak, strong, finalized}, Strong / OptionalStrong,
JSValue.protect/unprotect, Async.KeepAlive / jsc.Ref, and MarkedArgumentBuffer)
to escape each '|' inside cell content (e.g., replace '|' with '\|' or use an
equivalent HTML entity) so the table keeps its column structure; ensure you edit
the exact cell texts that include `hasPendingActivity`, `JSRef {weak, strong,
finalized}`, `Strong`/`OptionalStrong`, `JSValue.protect`/`unprotect`,
`KeepAlive`/`ActiveTaskRef` and `MarkedArgumentBuffer` occurrences (and apply
the same fix to the other occurrences mentioned) without changing surrounding
formatting.
In `@docs/SPAWN_SYS_PROPOSAL.md`:
- Around line 24-76: The Markdown code fences in this file (the crate-graph
block that starts with "bun_libuv_sys (already exists...)" and the Cargo deps
block that begins with "libc, bstr, scopeguard" / the ```toml snippet) need
explicit fence languages and surrounding blank lines; change the unlabeled
triple-backticks to labeled fences (e.g., ```text for the crate-graph/diagram
block and ```toml for the Cargo deps section) and ensure there is a blank line
before and after each fenced block so markdownlint and GitHub syntax
highlighting stop flagging them.
In `@docs/zig-restructure-plan.md`:
- Around line 1028-1030: The markdown table rows for `zigGeneratedClasses`,
`cxx`, and `c` contain raw pipe characters (e.g. the substitution snippets like
`s|src/bun.js/bindings|src/jsc/bindings|g`) that break the table; either escape
each `|` as `\|` inside those table cells or convert those rows into bullet list
items so the substitution strings aren’t parsed as column separators — update
the rows referencing `zigGeneratedClasses` (`:45`), `cxx` (`:79-95`) and `c`
(`:100-109`) (and the similar rows at 1042-1043) to use escaped pipes or bullets
consistently.
- Around line 944-945: The table and sections use inconsistent module paths
(e.g., references to bun.schema/bun.Semver pointing to ./api/schema.zig,
./api_schema/schema.zig, ./semver.zig and ./semver/semver.zig) which drift from
Section 2 that moves src/api/schema.zig into src/options_types/schema.zig; make
the document consistent by choosing one canonical directory layout (either
options_types or api_schema/cli_types) and update all occurrences of the symbols
`bun.schema` and `bun.Semver` and their file path examples to that layout, and
also update the other affected locations (the two additional occurrences noted)
so every reference points to the same directory names and filenames used in
Section 2.
In `@scripts/b2-aggregate-blocked.ts`:
- Line 6: Replace the hardcoded TASKS constant with a parameterized value: read
the tasks directory from a CLI argument or environment variable (e.g.,
process.argv or process.env.TASKS_DIR) in the script, validate that the path
exists and is a directory before scanning (fs.existsSync/fs.statSync or
fs.promises.stat), and if invalid log an error and exit; update any references
to the TASKS constant to use the new variable so the script works across
different runs/sessions.
In `@scripts/b2-cycle-args.ts`:
- Around line 92-97: The loop currently calls readdirSync("src") and join("src",
d) which assume the current working directory is the repo root; resolve the
repository src directory once to an absolute path and use that everywhere
instead. Create a resolvedSrc (or similar) using path.resolve/path.join with
__dirname to point at the repo's src directory, call readdirSync(resolvedSrc),
use statSync(path.join(resolvedSrc, d)) and pass absolute paths into gateCount
and any other functions (instead of join("src", d)) so the script works
regardless of the caller's CWD; keep KEYSTONE, crates push, and tier(d) logic
unchanged except to pass the correct path or basename as needed.
In `@scripts/build/bun.ts`:
- Around line 877-890: The escape detection fails on Windows when realBuild is
on a different drive because relative(realCwd, realBuild) can return an absolute
path; update the escapes calculation to treat an absolute rel as escaping (i.e.,
make escapes true when rel startsWith("..") or rel === "" or
path.isAbsolute(rel)). Modify the code around realpathSync/relative (variables
rel and escapes) and ensure you reference/import path.isAbsolute (the Node
path.isAbsolute function) so the assert still correctly detects symlinked build
directories outside the source tree.
In `@scripts/build/codegen.ts`:
- Around line 600-635: The filter building rsInputs in emitHostExports fails on
Windows because it compares absolute paths with platform backslashes; change the
filter on sources.rust to normalize each input to a forward-slash relative path
(e.g. path.relative(cfg.cwd, p).replace(/\\/g, "/")) and then test that relative
path startsWith "src/runtime/" or "src/jsc/" and does not end with
"generated_host_exports.rs" (keep the existing exclusion). Update references to
the test from p.includes(...) to use the normalized relative path so rsInputs is
populated correctly on Windows.
In `@scripts/build/rust.ts`:
- Around line 105-116: The array allRustTargets is missing the two triples that
rustTarget() and rustCanCrossFromLinux() treat as supported, causing
rust:check-all to skip them; update the allRustTargets constant to include
"x86_64-linux-android" and "aarch64-unknown-freebsd" (in the same format as the
other entries) so it matches the supported triples modelled by rustTarget() and
rustCanCrossFromLinux().
- Around line 333-337: The CARGO_TARGET_<TRIPLE>_LINKER env var is always set to
cfg.cxx but for Windows MSVC targets it must use cfg.msvcLinker to avoid
PATH-based linker resolution; update the code that builds the env object (the
entry that sets `CARGO_TARGET_${triple.toUpperCase().replace(/-/g,
"_")}_LINKER`) to choose cfg.msvcLinker when cfg.windows is true and
cfg.msvcLinker is defined, otherwise fall back to cfg.cxx (refer to symbols:
`cfg.msvcLinker`, `cfg.cxx`, `triple`, and the `CARGO_TARGET_${...}_LINKER`
key); follow the same guard logic used in source.ts to ensure Windows MSVC cargo
invocations use the explicit msvc linker.
In `@scripts/build/stream.ts`:
- Around line 40-45: The usage/help string emitted by scripts/build/stream.ts is
missing the documented flags --console and --stamp=PATH; update the script's
usage text (the runtime usage string printed by the help/usage routine in
stream.ts) to include both --console and --stamp=PATH so the built-in help
matches the comment block, and ensure any code paths that format or validate
arguments reference the same canonical usage string to keep them in sync.
In `@scripts/build/tools.ts`:
- Around line 562-594: The code in findRustLld currently chooses the LLD wrapper
based on the build-machine os parameter, which breaks cross-compilation; change
the selection to inspect the extracted hostTriple (from vv.match) instead of os
when constructing candidate, e.g. detect Windows/Darwin/Linux by checking
hostTriple for substrings like "windows" or "darwin" (and fall back to os only
if hostTriple is missing/ambiguous), and then pick "rust-lld.exe",
"gcc-ld/ld64.lld" or "gcc-ld/ld.lld" accordingly so the search in bin uses the
target host triple rather than the local os.
In `@scripts/fix-imports.ts`:
- Around line 11-12: The script currently uses process.cwd() via the root
variable which makes git pathspecs and readFileSync/writeFileSync calls relative
to the current working directory; replace that by resolving the repository root
once (via running git rev-parse --show-toplevel) into a repoRoot constant and
use path.resolve(repoRoot, ...) for all file I/O and any git pathspecs. Update
the declaration of root to repoRoot, change every readFileSync/writeFileSync and
any child_process/git invocation that previously used root or relative paths
(the code around the current root usage and the blocks that read/write files) to
build absolute paths from repoRoot, and ensure the git command is executed from
repoRoot (or passed the resolved path) so discovery and writes are anchored to
the repository root.
In `@scripts/fix-mod-paths.ts`:
- Around line 47-49: The script currently uses a relative target
(process.argv[2] || "src") which is resolved from the caller's cwd; instead,
compute an absolute path anchored at the repository root and pass that into
walk() so file mutations always operate on the repo tree. Replace how target is
computed by resolving the repo root (e.g., based on __dirname or known repo
layout) and join/resolve the user argument or default "src" into an absolute
path, then call walk(absTarget) and keep the final console message using the
same absolute/normalized target and the fixed counter.
In `@scripts/fix-vm-import.ts`:
- Around line 10-13: The cleanup loop that builds the Glob list omits the
valkey_jsc tree, so misplaced or duplicate imports like "use
bun_jsc::virtual_machine::VirtualMachine;" in that tree are never visited;
update the array of Glob patterns used with Glob(...).scanSync by adding the
pattern "src/valkey_jsc/**/*.rs" alongside the existing "src/runtime/**/*.rs"
and "src/sql_jsc/**/*.rs" entries so the scanSync loop covers that directory and
removes the stray imports.
In `@scripts/gen-cargo.ts`:
- Around line 164-167: The generated lib.rs template in scripts/gen-cargo.ts
currently inserts a blanket allow line (`#![allow(unused, non_snake_case,
non_camel_case_types, non_upper_case_globals, clippy::all)]`) which should be
annotated for future cleanup; update the template string (the multi-line string
that contains the allow attributes and the `${mods.map(...).join("\n")}`
insertion) to include a clear TODO comment such as `// TODO(port): remove
blanket allows in Phase B` immediately above or on the same line as the allow
attribute so the generated files explicitly document that these suppressions are
temporary and must be removed in Phase B.
In `@scripts/gen-link-stubs.sh`:
- Around line 24-29: The script gen-link-stubs.sh currently pipes grep on
"$UNDEFS" and treats “file missing” as no matches; add an explicit
existence/readability check for the UNDEFS input before the pipeline (e.g. test
-f or -r "$UNDEFS") and if the file is absent, emit a clear error message and
exit nonzero so the failure is visible; update the block that builds the tmp
file (the pipeline referencing CAT1, UNDEFS, and the grep filters for JS2Zig__,
Sink__, bindgen_) to rely on an ensured UNDEFS file or short-circuit early with
the error, so you no longer silently ignore a missing UNDEFS.
- Line 67: The script currently writes generated Rust directly to "$OUT" (the
command ending with ' "$tmp" > "$OUT"'), which can leave a partial file if
interrupted; change the flow to write into a safe temporary file (use "$tmp" or
create one with mktemp), ensure successful generation and any required
validations complete, then atomically replace "$OUT" by moving the temp file
into place with mv -f "$tmp" "$OUT" (and clean up the temp on errors); update
gen-link-stubs.sh to follow this write-then-mv pattern so link_stubs.rs is
replaced atomically.
- Line 19: The OUT assignment uses dirname "$0" which can be wrong depending on
invocation; change the OUT default assignment (variable OUT) to resolve script
path via ${BASH_SOURCE[0]} and canonicalize it (e.g., resolve to an absolute
path once) before appending /../src/runtime/link_stubs.rs so all file operations
use an absolute, canonical path; update the assignment that sets OUT to use
BASH_SOURCE[0] and a canonicalization step instead of dirname "$0".
In `@scripts/migrate-hostfn-vm.ts`:
- Around line 189-195: The current import-detection uses a too-broad regex (/use
.*VirtualMachine/) and can false-match or miss multiline/grouped imports; update
the check in scripts/migrate-hostfn-vm.ts to mirror scripts/fix-vm-import.ts by
testing for a whole-word, semicolon-terminated import of VirtualMachine (e.g.,
ensure the pattern matches strings like "use ...::VirtualMachine;" and accounts
for grouped imports or exact word boundaries) before deciding to inject use
bun_jsc::virtual_machine::VirtualMachine; so you neither skip needed injections
nor create duplicates; adjust the condition that examines txt (the file content)
and reuse the same regex logic/symbols (VirtualMachine, txt) as the referenced
script.
In `@scripts/patch-genclasses-paths.ts`:
- Around line 10-12: The GEN constant currently uses a relative path; change it
to an absolute path resolved from the repository root (derived from
import.meta.url / fileURLToPath and path.resolve or a repoRoot variable) and use
that absolute GEN for both the git show execSync call and
readFileSync/writeFileSync operations so the script works when invoked outside
the repo root; update the GEN declaration and the two usages (the execSync(`git
show HEAD:${GEN}`) call and readFileSync(GEN, ...)) to reference the resolved
absolute path.
In `@scripts/port-batch.ts`:
- Around line 8-14: The code calls readFileSync to build the manifest (const
manifest = readFileSync(...).trim().split(...)) without checking for the file's
existence or catching errors; wrap the readFileSync call in a guard (e.g., check
fs.existsSync("/tmp/port-manifest-filtered.tsv") or try/catch around
readFileSync) and, if the file is missing or reading fails, log a clear error
message including the path and error details (use processLogger or
console.error) and terminate (e.g., process.exit(1)) before attempting to parse
into manifest.
In `@scripts/port-cycle.sh`:
- Around line 14-17: The script currently counts and commits the entire index;
restrict both the staged-file count and the commit to only the Rust pathspecs by
changing the N computation to list cached names filtered to the intended
pathspecs (e.g., use git diff --cached --name-only -- 'src/**/*.rs' 'src/*.rs'
when computing N) and invoke git commit with those same pathspecs (e.g., git
commit ... -- 'src/**/*.rs' 'src/*.rs') so only the files the script manages are
counted and committed; update the commands in scripts/port-cycle.sh where N is
set and where git commit is invoked.
- Around line 4-12: The script currently can continue on errors (missing
/tmp/port-results, cd, cp, printf) because it lacks errexit; update the top of
the script to fail fast (add -e to the existing set -uo pipefail) and ensure the
artifact dir exists before use by creating /tmp/port-results (mkdir -p
/tmp/port-results) and aborting if that fails; also ensure critical commands (cd
"$(dirname "$0")/..", command cp -f "$OUT" "/tmp/port-results/$NAME.json", and
the printf append to /tmp/port-results/log.tsv) are allowed to propagate errors
(rely on set -e) or explicitly check their exit codes so the script stops on
failure, referencing variables NAME, OUT and the target files
/tmp/port-results/$NAME.json and /tmp/port-results/log.tsv.
In `@scripts/rust-check-all.ts`:
- Around line 25-27: The rustup output split leaves CRLFs on Windows so entries
can have trailing "\r" and never match `triple`; update the check around the
`installed` result (the `spawnSync("rustup", ["target", "list", "--installed"],
...)` call and the subsequent `installed.stdout.split("\n").includes(triple)`
logic) to normalize line endings and whitespace before matching (e.g., split on
CRLF-aware pattern or map and trim each entry) so `triple` is matched correctly
across platforms.
In `@scripts/test-scoreboard.sh`:
- Around line 43-56: The script runs the xargs loop over "$DIAG/all.txt" without
guarding the zero-case which can produce misleading output; before invoking the
xargs blocks (the pipelines that read "$DIAG/all.txt" and append to
"$DIAG/results.txt"), add an explicit check for an empty or non-existent
"$DIAG/all.txt" (e.g. test -s or wc -l) and short-circuit: write a clear
empty-results / zero-tests marker to "$DIAG/results.txt" (or exit 0) and skip
the xargs/unshare/timeout invocation (the block that executes '"$BIN"' test "{}"
> '"$DIAG"'/"$slug".log and its duplicate later) so totals and reporting aren’t
produced when there are no tests to run.
- Line 5: Replace the hardcoded "cd /root/bun-5" with robust repo-root detection
and ensure the script exits on failure: compute REPO_ROOT (e.g. via git
rev-parse --show-toplevel || realpath "$(dirname "$0")/.."), then run cd
"$REPO_ROOT" || { echo "Failed to change directory to $REPO_ROOT" >&2; exit 1;
}; also enable failing on errors if not already (add set -e or use set -euo
pipefail) so a failed cd stops execution.
- Around line 29-30: The pids.max cgroup is being set to "max" which disables
PID limiting and contradicts the fork-bomb containment intent; change the write
to "$CG/pids.max" to a sensible numeric cap (e.g., 1024 or another value based
on expected load) instead of "max" so the script enforces a PID limit for the
cgroup (locate the echo that writes to "$CG/pids.max" and replace the "max"
token with the chosen numeric limit and optionally document the chosen value in
the comment).
In `@scripts/utils.mjs`:
- Around line 2665-2669: The blank-line sentinel passed to readUntil is matching
immediately because originalLines contains newline tokens; replace the call
readUntil(/^$/, 30) with a pattern that matches an actual newline token (for
example readUntil(/^\r?\n$/, 30)) so the function readUntil will consume the
full rustc diagnostic body (span, caret, help/note lines) after the --> line
(see the surrounding use of readUntil and locMatch to locate the diagnostic
header).
In `@src/aio/ParentDeathWatchdog.rs`:
- Line 568: The cast from info.pbi_ppid (u32) to libc::pid_t currently uses
libc::pid_t::try_from(info.pbi_ppid).expect("int cast"), which can panic on
overflow; change it to safely handle overflow by using .unwrap_or(0) (or
equivalent non-panicking fallback) so an out-of-range parent PID becomes 0
instead of causing a panic—update the expression around info.pbi_ppid /
libc::pid_t::try_from in ParentDeathWatchdog.rs to use unwrap_or(0) and return
that value.
- Line 362: Replace the explicit to_visit.swap_remove(to_visit.len() - 1) with
to_visit.pop() and handle the returned Option; either use
to_visit.pop().expect("...")/unwrap() if empty is impossible in this context, or
match/if-let to handle None safely. This change targets the code that removes
the last element from the Vec (the variable to_visit) so update the call site
accordingly to use pop() and adjust for the Option return type.
---
Outside diff comments:
In `@docs/RUST_PATTERNS.md`:
- Around line 486-488: The file ends without a trailing newline; add a single
newline character at EOF to terminate the file. Open docs/RUST_PATTERNS.md,
locate the table rows such as the line containing "`ptr::read(self) to clone`"
(near the `*mut c_void` ctx callback / `ptr::read(self)` entries) and ensure the
file ends with a newline, then save and commit the change.
In `@src/aio/Cargo.toml`:
- Around line 1-28: The crate manifest for package "bun_aio" (Cargo.toml) is
missing the workspace lints opt-in so workspace-defined Clippy lints like
ptr_as_ptr won’t apply; update the Cargo.toml for the bun_aio crate by adding a
top-level [lints] table with workspace = true (i.e., add `[lints]` followed by
`workspace = true`) so the workspace lints declared in the root are opt‑in for
this member crate.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
| ], | ||
| }; | ||
|
|
||
| const REPO = "/root/bun-5"; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Hardcoded repository path is environment-specific.
/root/bun-5 assumes a specific container/CI environment. Consider accepting this as a workflow argument or deriving from an environment variable for portability.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.claude/workflows/lifetime-classify.workflow.js at line 11, The REPO
constant is hardcoded to "/root/bun-5" making the workflow environment-specific;
change the code that defines REPO so it reads from a configurable source (e.g.,
process.env.REPO_PATH or a workflow input like inputs.repo) with a sensible
fallback and a clear warning/error if neither is provided. Specifically replace
the literal const REPO = "/root/bun-5"; with logic that checks
process.env.REPO_PATH || workflow input (e.g., inputs.repo) || a default, and
ensure any code that uses REPO still works; add validation/logging so missing
configuration fails fast or logs the chosen path.
| Return ONLY fields that are pointer types. Skip [*]const u8 / []u8 (slices, handled separately).`, | ||
| { label: `classify:${f.zig.replace(/^src\//, "")}`, phase: "Classify", schema: CLASSIFY_SCHEMA }, | ||
| ).then(r => (r ? r.fields.map(x => ({ ...x, file: f.zig, crate: f.crate })) : [])), |
There was a problem hiding this comment.
Potential null reference if r.fields is undefined.
The check r ? guards against falsy r, but if the agent returns an object without fields (e.g., { error: "..." }), calling .map() on undefined will throw.
🛡️ Proposed fix
- ).then(r => (r ? r.fields.map(x => ({ ...x, file: f.zig, crate: f.crate })) : [])),
+ ).then(r => (r?.fields ? r.fields.map(x => ({ ...x, file: f.zig, crate: f.crate })) : [])),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ).then(r => (r ? r.fields.map(x => ({ ...x, file: f.zig, crate: f.crate })) : [])), | |
| ).then(r => (r?.fields ? r.fields.map(x => ({ ...x, file: f.zig, crate: f.crate })) : [])), |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.claude/workflows/lifetime-classify.workflow.js at line 106, The current
then handler assumes r.fields exists and calls r.fields.map(...), which can
throw if r is an object without fields; update the handler that returns fields
for the promise (the arrow function using r and f.zig/f.crate) to
guard/normalize r.fields first — e.g., check Array.isArray(r.fields) or use
optional chaining/defaulting so you only call .map when r.fields is an array and
otherwise return an empty array; keep the mapping that spreads each field and
adds file: f.zig and crate: f.crate.
| return { | ||
| total_fields: final.length, | ||
| by_class: byClass, | ||
| unknown_rate: ((byClass.UNKNOWN || 0) / final.length).toFixed(3), | ||
| verified_count: verified.length, | ||
| overturned: verified.filter(v => v.refutes >= 2).length, | ||
| tsv_preview: tsv.split("\n").slice(0, 20).join("\n"), | ||
| tsv, | ||
| }; |
There was a problem hiding this comment.
Division by zero when no fields are found.
If all files are processed but contain no pointer fields, final.length === 0 and unknown_rate becomes "NaN". The early return on line 13 only guards against empty FILES, not empty results.
Additionally, this top-level return has the same parse error as line 13.
🛡️ Proposed fix
return {
total_fields: final.length,
by_class: byClass,
- unknown_rate: ((byClass.UNKNOWN || 0) / final.length).toFixed(3),
+ unknown_rate: final.length > 0 ? ((byClass.UNKNOWN || 0) / final.length).toFixed(3) : "0.000",
verified_count: verified.length,
overturned: verified.filter(v => v.refutes >= 2).length,
tsv_preview: tsv.split("\n").slice(0, 20).join("\n"),
tsv,
};🧰 Tools
🪛 Biome (2.4.14)
[error] 157-165: Illegal return statement outside of a function
(parse)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.claude/workflows/lifetime-classify.workflow.js around lines 157 - 165, The
return block can produce NaN when final.length === 0 and repeats the same unsafe
toFixed usage noted earlier; change the computation of unknown_rate to guard
against division-by-zero by computing unknown_rate = final.length === 0 ?
"0.000" : ((byClass.UNKNOWN || 0) / final.length).toFixed(3), and ensure the
returned value is a string/number consistently; keep the rest of the fields
(total_fields, by_class, verified_count, overturned, tsv_preview, tsv) unchanged
and reference the variables final, byClass, unknown_rate, verified and
tsv_preview when applying the fix.
| notes: { type: "string" }, | ||
| }, | ||
| }; | ||
| const dir_for = c => ({ bun_str: "string", bun_core: "bun_core", bun_alloc: "bun_alloc" })[c] || c.replace(/^bun_/, ""); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Document or validate dir_for mapping assumptions.
The dir_for mapping includes special cases (bun_str→"string", bun_core→"bun_core", bun_alloc→"bun_alloc") and a fallback that strips bun_ prefix. This suggests the crate-to-directory mapping isn't uniform.
If an unexpected crate name is passed (e.g., one not matching these patterns), the fallback may produce an incorrect directory. Consider adding validation or a comment documenting which crates require special handling.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.claude/workflows/phase-b2-fill.workflow.js at line 21, The dir_for helper
currently maps a few special crate names and otherwise strips a bun_ prefix,
which can produce incorrect directories for unexpected crate names; update
dir_for to validate inputs and document the mapping: add a short comment
explaining the special cases (bun_str→"string", bun_core, bun_alloc) and the
fallback behavior, and implement explicit validation in the dir_for function to
either (a) check the crate name against an allowed set of known crate keys and
throw or log an error when unknown, or (b) return a clearly marked fallback only
after confirming the name begins with "bun_"—use the existing dir_for identifier
to locate and change the mapping logic and add the comment nearby.
| const manifest = readFileSync("/tmp/port-manifest-filtered.tsv", "utf8") | ||
| .trim() | ||
| .split("\n") | ||
| .map(l => { | ||
| const [zig, loc] = l.split("\t"); | ||
| return { zig, loc: Number(loc) }; | ||
| }); |
There was a problem hiding this comment.
Missing error handling for manifest file.
If /tmp/port-manifest-filtered.tsv doesn't exist, readFileSync will throw an unhandled exception. Consider adding a guard with a helpful error message.
🛡️ Proposed fix
+import { existsSync, readFileSync } from "node:fs";
+import { basename, dirname, join } from "node:path";
+
const REPO = process.cwd();
+const MANIFEST_PATH = "/tmp/port-manifest-filtered.tsv";
+if (!existsSync(MANIFEST_PATH)) {
+ console.error(`error: ${MANIFEST_PATH} not found. Run port-manifest.sh first.`);
+ process.exit(1);
+}
-const manifest = readFileSync("/tmp/port-manifest-filtered.tsv", "utf8")
+const manifest = readFileSync(MANIFEST_PATH, "utf8")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/port-batch.ts` around lines 8 - 14, The code calls readFileSync to
build the manifest (const manifest = readFileSync(...).trim().split(...))
without checking for the file's existence or catching errors; wrap the
readFileSync call in a guard (e.g., check
fs.existsSync("/tmp/port-manifest-filtered.tsv") or try/catch around
readFileSync) and, if the file is missing or reading fails, log a clear error
message including the path and error details (use processLogger or
console.error) and terminate (e.g., process.exit(1)) before attempting to parse
into manifest.
| # Full test-suite scoreboard. Runs all tests vs current build/debug/bun-debug, | ||
| # writes /tmp/SCOREBOARD.md. Idempotent; safe to re-run anytime. | ||
| set -uo pipefail | ||
| cd /root/bun-5 |
There was a problem hiding this comment.
Harden repo-root resolution and fail fast on cd failure.
Line 5 hardcodes /root/bun-5, and with set -uo pipefail (no -e) a failed cd won’t stop execution. The script can continue from the wrong working directory and produce invalid discovery/report output.
Suggested fix
-cd /root/bun-5
+REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || {
+ echo "[scoreboard] not inside a git repository" >&2
+ exit 1
+}
+cd "$REPO_ROOT" || {
+ echo "[scoreboard] failed to cd to repo root: $REPO_ROOT" >&2
+ exit 1
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| cd /root/bun-5 | |
| REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || { | |
| echo "[scoreboard] not inside a git repository" >&2 | |
| exit 1 | |
| } | |
| cd "$REPO_ROOT" || { | |
| echo "[scoreboard] failed to cd to repo root: $REPO_ROOT" >&2 | |
| exit 1 | |
| } |
🧰 Tools
🪛 Shellcheck (0.11.0)
[warning] 5-5: Use 'cd ... || exit' or 'cd ... || return' in case cd fails.
(SC2164)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/test-scoreboard.sh` at line 5, Replace the hardcoded "cd /root/bun-5"
with robust repo-root detection and ensure the script exits on failure: compute
REPO_ROOT (e.g. via git rev-parse --show-toplevel || realpath "$(dirname
"$0")/.."), then run cd "$REPO_ROOT" || { echo "Failed to change directory to
$REPO_ROOT" >&2; exit 1; }; also enable failing on errors if not already (add
set -e or use set -euo pipefail) so a failed cd stops execution.
| echo 32G > "$CG/memory.max" 2>/dev/null || true | ||
| echo max > "$CG/pids.max" 2>/dev/null || true |
There was a problem hiding this comment.
PID limiting is currently disabled despite the safety comment.
Line 30 sets pids.max to max, which removes the PID cap entirely. That contradicts the “fork-bomb” containment goal in the comment above and weakens isolation.
Suggested fix
echo 32G > "$CG/memory.max" 2>/dev/null || true
-echo max > "$CG/pids.max" 2>/dev/null || true
+echo 4096 > "$CG/pids.max" 2>/dev/null || true📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| echo 32G > "$CG/memory.max" 2>/dev/null || true | |
| echo max > "$CG/pids.max" 2>/dev/null || true | |
| echo 32G > "$CG/memory.max" 2>/dev/null || true | |
| echo 4096 > "$CG/pids.max" 2>/dev/null || true |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/test-scoreboard.sh` around lines 29 - 30, The pids.max cgroup is
being set to "max" which disables PID limiting and contradicts the fork-bomb
containment intent; change the write to "$CG/pids.max" to a sensible numeric cap
(e.g., 1024 or another value based on expected load) instead of "max" so the
script enforces a PID limit for the cgroup (locate the echo that writes to
"$CG/pids.max" and replace the "max" token with the chosen numeric limit and
optionally document the chosen value in the comment).
| cat "$DIAG/all.txt" | xargs -P 32 -I{} sh -c ' | ||
| slug=$(echo "{}" | tr / _) | ||
| td='"$TMPROOT"'/"$slug"; mkdir -p "$td" | ||
| # PID namespace: test sees only its own children, so kill(-1)/pkill cannot | ||
| # touch the host. --mount-proc gives it a clean /proc. --kill-child reaps | ||
| # everything in the namespace on timeout. | ||
| TMPDIR="$td" TEMP="$td" TMP="$td" \ | ||
| timeout --kill-after=5 15 \ | ||
| unshare --pid --fork --mount-proc --kill-child \ | ||
| '"$BIN"' test "{}" > '"$DIAG"'/"$slug".log 2>&1 | ||
| rc=$? | ||
| rm -rf "$td" | ||
| echo "{}|$rc" | ||
| ' >> "$DIAG/results.txt" |
There was a problem hiding this comment.
Add an explicit zero-test guard to avoid misleading runs/reporting.
When no files are discovered, execution/reporting is not explicitly guarded. This can produce misleading output depending on command behavior and makes totals fragile.
Suggested fix
TOTAL=$(wc -l < "$DIAG/all.txt")
echo "[scoreboard] running $TOTAL files (timeout 15s each, -P32, pid-ns isolated)..." >&2
+
+if [ "$TOTAL" -eq 0 ]; then
+ echo "[scoreboard] no test files discovered" >&2
+fi
@@
-cat "$DIAG/all.txt" | xargs -P 32 -I{} sh -c '
+if [ "$TOTAL" -gt 0 ]; then
+cat "$DIAG/all.txt" | xargs -P 32 -I{} sh -c '
@@
-' >> "$DIAG/results.txt"
+' >> "$DIAG/results.txt"
+fi
@@
- awk -F'|' '
- { n++; if($2==0)p++; else if($2==1)f++; else if($2==124)h++; else if($2>=128)c++; else o++ }
- END{ printf "| **TOTAL** | **%d** | **%d** (%.0f%%) | **%d** | **%d** | **%d** | **%d** |\n", n, p, 100.0*p/n, f, h, c, o }
- ' "$DIAG/results.txt"
+ awk -F'|' '
+ { n++; if($2==0)p++; else if($2==1)f++; else if($2==124)h++; else if($2>=128)c++; else o++ }
+ END{
+ pct = (n > 0) ? (100.0*p/n) : 0
+ printf "| **TOTAL** | **%d** | **%d** (%.0f%%) | **%d** | **%d** | **%d** | **%d** |\n", n, p, pct, f, h, c, o
+ }
+ ' "$DIAG/results.txt"Also applies to: 76-79
🧰 Tools
🪛 Shellcheck (0.11.0)
[info] 43-45: Expressions don't expand in single quotes, use double quotes for that.
(SC2016)
[info] 45-52: Expressions don't expand in single quotes, use double quotes for that.
(SC2016)
[info] 52-56: Expressions don't expand in single quotes, use double quotes for that.
(SC2016)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/test-scoreboard.sh` around lines 43 - 56, The script runs the xargs
loop over "$DIAG/all.txt" without guarding the zero-case which can produce
misleading output; before invoking the xargs blocks (the pipelines that read
"$DIAG/all.txt" and append to "$DIAG/results.txt"), add an explicit check for an
empty or non-existent "$DIAG/all.txt" (e.g. test -s or wc -l) and short-circuit:
write a clear empty-results / zero-tests marker to "$DIAG/results.txt" (or exit
0) and skip the xargs/unshare/timeout invocation (the block that executes
'"$BIN"' test "{}" > '"$DIAG"'/"$slug".log and its duplicate later) so totals
and reporting aren’t produced when there are no tests to run.
| const { match: locMatch } = readUntil(/-->\s+(.+?):(\d+):(\d+)/, 3); | ||
| // Swallow the diagnostic body up to the blank-line separator (rustc | ||
| // always emits one between diagnostics in the human format; cap at 30 | ||
| // for `--message-format=short` which doesn't). | ||
| readUntil(/^$/, 30); |
There was a problem hiding this comment.
Consume the full rustc diagnostic body.
Because originalLines still contains newline tokens, readUntil(/^$/, 30) matches the first \n immediately after the --> line and stops there. That means the annotation body loses the rendered span, caret, and help/note lines, which are the most useful part of the CI preview.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/utils.mjs` around lines 2665 - 2669, The blank-line sentinel passed
to readUntil is matching immediately because originalLines contains newline
tokens; replace the call readUntil(/^$/, 30) with a pattern that matches an
actual newline token (for example readUntil(/^\r?\n$/, 30)) so the function
readUntil will consume the full rustc diagnostic body (span, caret, help/note
lines) after the --> line (see the surrounding use of readUntil and locMatch to
locate the diagnostic header).
| const rsInputs = sources.rust.filter( | ||
| p => | ||
| p.endsWith(".rs") && | ||
| (p.includes(`${cfg.cwd}/src/runtime/`.replace(/\//g, "/")) || | ||
| p.includes(`${cfg.cwd}/src/jsc/`.replace(/\//g, "/"))) && | ||
| !p.endsWith("generated_host_exports.rs"), | ||
| ); |
There was a problem hiding this comment.
🔴 The .replace(/\//g, "/") calls here replace forward-slash with forward-slash — a complete no-op (presumably meant to be .replace(/\\/g, "/")). On a Windows host, cfg.cwd and sources.rust entries both use backslashes (from process.cwd() / path.resolve()), so the mixed-separator search string C:\repo\bun/src/runtime/ never matches and rsInputs is always empty — meaning the ninja edge for generated_host_exports.rs gets zero .rs implicit inputs and editing a host-fn .rs file won't trigger regeneration on incremental Windows builds. Both p and the search string need normalization, e.g. p.replace(/\\/g, "/").includes(...).
Extended reasoning...
What the bug is
In emitHostExports() (new code added by this PR), the rsInputs filter at lines 609–615 builds the list of .rs files that should be implicit inputs to the generated_host_exports.rs ninja edge. The filter checks whether each path p contains ${cfg.cwd}/src/runtime/ or ${cfg.cwd}/src/jsc/, with each search string passed through .replace(/\//g, "/"):
const rsInputs = sources.rust.filter(
p =>
p.endsWith(".rs") &&
(p.includes(`${cfg.cwd}/src/runtime/`.replace(/\//g, "/")) ||
p.includes(`${cfg.cwd}/src/jsc/`.replace(/\//g, "/"))) &&
!p.endsWith("generated_host_exports.rs"),
);The regex /\//g matches the forward-slash character and replaces it with… a forward-slash. This is provably a no-op — the .replace() call returns its input unchanged on every platform. The presence of the call shows the author intended to normalize path separators (almost certainly /\\/g → "/" to convert Windows backslashes), but the regex was mistyped.
The code path that triggers it
On a Windows host, both sides of the .includes() comparison use native backslash separators, but in incompatible ways:
cfg.cwdcomes fromfindRepoRoot()(config.ts:987–995), which returnsprocess.cwd()walked upward viapath.join(). On Windows this yields e.g.C:\repo\bun.sources.rustentries are produced byfiles.push(resolve(root, normalized))inglob-sources.ts:141, whereresolveisnode:path'sresolve()— also native backslashes on Windows, e.g.C:\repo\bun\src\runtime\foo.rs.- The template literal therefore produces a mixed-separator search string:
C:\repo\bun+/src/runtime/=C:\repo\bun/src/runtime/. - The no-op
.replace()does nothing to fix this. p.includes("C:\\repo\\bun/src/runtime/")againstp = "C:\\repo\\bun\\src\\runtime\\foo.rs"→ never matches.
Result: rsInputs is an empty array on Windows, so the ninja build edge at line 617 gets implicitInputs: [].
Why nothing prevents it
There's no fallback. The only other inputs to this edge are the codegen script itself (inputs: [script]) and the directory stamp (orderOnlyInputs: [dirStamp]). Neither changes when a developer edits a #[host_fn] marker in src/runtime/*.rs or src/jsc/*.rs. On Linux/macOS the bug is invisible because both cfg.cwd and sources.rust already use forward slashes, so the no-op .replace() is harmless and .includes() matches correctly — but the dead .replace() call is still a clear signal that Windows normalization was intended and miswritten.
Step-by-step proof
- Windows host, repo at
C:\b\bun. cfg.cwd = "C:\\b\\bun"(fromprocess.cwd()).sources.rustcontains"C:\\b\\bun\\src\\runtime\\jsc_hooks.rs"(frompath.resolve(root, ...)inglob-sources.ts:141).- Search string:
`${cfg.cwd}/src/runtime/`="C:\\b\\bun/src/runtime/". .replace(/\//g, "/")on that string →"C:\\b\\bun/src/runtime/"(unchanged)."C:\\b\\bun\\src\\runtime\\jsc_hooks.rs".includes("C:\\b\\bun/src/runtime/")→false(the\src\runtime\substring uses backslashes, the needle uses forward slashes).- Filter returns
[]. Ninja edge has no.rsimplicit inputs. - Developer edits
src/runtime/jsc_hooks.rsto add a#[host_fn]. Ninja sees no input changed →generated_host_exports.rsis not regenerated → stale codegen until a clean build or until the script itself changes.
Impact
Per getRustAgent() in .buildkite/ci.mjs, the Windows build-rust step runs natively on a Windows agent, so this affects Windows incremental builds both locally and in CI. Fresh/clean builds still work (the output doesn't exist yet, so ninja runs the rule regardless), which is why CI's typical clean-build won't catch it — but any incremental Windows workflow will silently use stale generated_host_exports.rs.
How to fix
Note that fixing only the search string is insufficient — p also has backslashes, so both sides need normalization. Simplest fix:
const rsInputs = sources.rust.filter(p => {
const np = p.replace(/\\/g, "/");
return (
np.endsWith(".rs") &&
(np.includes(`${cfg.cwd.replace(/\\/g, "/")}/src/runtime/`) ||
np.includes(`${cfg.cwd.replace(/\\/g, "/")}/src/jsc/`)) &&
!np.endsWith("generated_host_exports.rs")
);
});Or use path.sep in the search strings instead of hard-coded /.
| /** Outputs that zig `@embedFile`s or imports. */ | ||
| zigInputs: string[]; | ||
| rustInputs: string[]; | ||
|
|
||
| /** Outputs that zig needs to exist but doesn't embed (debug bake runtime). */ | ||
| zigOrderOnly: string[]; | ||
| rustOrderOnly: string[]; |
There was a problem hiding this comment.
🟡 The fields were renamed zigInputs→rustInputs and zigOrderOnly→rustOrderOnly (commit f8fd66c: "the build is Rust now"), but the per-field JSDoc on lines 212/215 still says "Outputs that zig @embedFiles or imports" and "Outputs that zig needs to exist but doesn't embed", and line ~632 in emitHostExports still says "both zig and the cargo edge consume" even though build.zig and scripts/build/zig.ts are deleted in this PR. Update these to reference Rust include!/include_bytes! and the cargo edge only.
Extended reasoning...
What the bug is
Commit f8fd66c in this PR renamed two fields on the CodegenOutputs interface in scripts/build/codegen.ts:
zigInputs→rustInputszigOrderOnly→rustOrderOnly
The commit message is explicit about intent: "the build is Rust now; these are the include!d generated .rs files the cargo edge order-depends on, the 'zig' name was a leftover from before the port". The same commit updated several surrounding comments — the interface-level doc ("zig build" → "rust build"), the cppAll comment, and the bindgenV2Zig comment — but missed three:
- Line 212: JSDoc on
rustInputsstill reads "Outputs that zig@embedFiles or imports — these are implicit deps of the zig build edge." - Line 215: JSDoc on
rustOrderOnlystill reads "Outputs that zig needs to exist but doesn't embed — these are order-only deps…" - Line ~632 (
emitHostExports): "rustInputs is the implicit-dep list both zig and the cargo edge consume"
Why nothing prevents it
These are pure JSDoc/inline comments — neither TypeScript nor any tooling validates that the prose matches the field name or the build graph it describes. The rename was clearly done with a search-and-replace on the identifier; the doc strings on the very next lines didn't match the search term and were left behind.
Step-by-step proof
- Read
scripts/build/codegen.ts:206— the interface-level doc says "…the rust build (which theninclude!s/embeds them)". This line was updated by f8fd66c. - Read line 212 —
/** Outputs that zig@embedfiles or imports … */immediately aboverustInputs: string[];. The field name says Rust; the doc says Zig@embedFile(a Zig builtin that does not exist in Rust). - Read line 215 —
/** Outputs that zig needs to exist but doesn't embed … */immediately aboverustOrderOnly: string[];. Same mismatch. - Read line ~632 in
emitHostExports()— "rustInputs is the implicit-dep list both zig and the cargo edge consume". But this PR deletesbuild.zig(1152 lines, → /dev/null) andscripts/build/zig.ts, andrules.tsno longer registers anyzig_buildrule. There is no zig edge that could consumerustInputs. - The diff confirms adjacent comments at lines 206, 232, and 247 were updated from "zig" to "rust" in this same commit, demonstrating the intent was a complete rename — these three were simply overlooked.
Impact
None at runtime — comments only. The cost is reader confusion: someone reading CodegenOutputs will see a field named rustInputs documented as "things zig @embedFiles", and will wonder whether there's still a zig consumer somewhere. There isn't. The line-632 comment is more actively misleading because it asserts a second consumer ("both zig and the cargo edge") that no longer exists.
How to fix
Three one-line edits:
/** Outputs that Rust `include!`s or `include_bytes!`s — these are *implicit deps* of the cargo build edge. */
rustInputs: string[];
/** Outputs that Rust needs to exist but doesn't embed — these are *order-only deps* of the cargo build edge. */
rustOrderOnly: string[];and at line ~632:
// rustInputs is the implicit-dep list the cargo edge consumes — …|
|
||
| **Your task list:** | ||
| 1. Read \`${REPO}/docs/CYCLEBREAK.md\` — find section "### → \\\`${t.cyclebreak_key}\\\`" under "Per-target-crate move-in tasks". That lists \`from <crate>: <symbol>, ...\` entries. | ||
| 2. \`grep -iE '\\b(${t.name}|${t.cyclebreak_key}|bun_${t.cyclebreak_key})\\b' /tmp/movein-skipped.txt\` — these are explicit "move-in pass must add X to ${t.name}" requests from move-out agents. Higher priority than CYCLEBREAK.md (more specific). |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find where movein-skipped.txt is created
rg -n 'movein-skipped' .claude/ scripts/Repository: oven-sh/bun
Length of output: 351
🏁 Script executed:
cat -n .claude/workflows/phase-b0-movein.workflow.js | head -50Repository: oven-sh/bun
Length of output: 3821
🏁 Script executed:
find .claude -type f -name "*.md" -o -name "*.txt" | head -20Repository: oven-sh/bun
Length of output: 560
🏁 Script executed:
find .claude -name "*moveout*" -type fRepository: oven-sh/bun
Length of output: 102
🏁 Script executed:
rg -n 'movein-skipped|tmp.*txt' .claude/workflows/ --context 3Repository: oven-sh/bun
Length of output: 2189
🏁 Script executed:
cat -n .claude/workflows/phase-b0-moveout.workflow.js | grep -A 5 -B 5 'movein-skipped\|/tmp'Repository: oven-sh/bun
Length of output: 37
🏁 Script executed:
wc -l .claude/workflows/phase-b0-moveout.workflow.jsRepository: oven-sh/bun
Length of output: 105
🏁 Script executed:
cat -n .claude/workflows/phase-b0-moveout.workflow.jsRepository: oven-sh/bun
Length of output: 4387
🏁 Script executed:
find .claude -name "*.js" -o -name "*.md" | xargs grep -l "phase-b0-moveout\|phase-b0-movein" 2>/dev/null | head -10Repository: oven-sh/bun
Length of output: 148
🏁 Script executed:
rg -n 'skipped' .claude/workflows/phase-b0-moveout.workflow.jsRepository: oven-sh/bun
Length of output: 474
Add validation or error handling for /tmp/movein-skipped.txt.
Line 36 references /tmp/movein-skipped.txt without any setup in this workflow or the preceding moveout phase. The moveout workflow collects items in a skipped array field, but no visible code transforms that into the temp file that movein expects.
If the file is missing, the grep silently produces no matches, and agents skip the "explicit requests from move-out agents" that are marked as higher priority than CYCLEBREAK.md. Either add a setup step that creates the file from moveout's skipped results, or add error handling (e.g., test -f /tmp/movein-skipped.txt || touch /tmp/movein-skipped.txt) to ensure the grep doesn't fail.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.claude/workflows/phase-b0-movein.workflow.js at line 36, The workflow
references the grep against movein-skipped.txt but there's no producer for that
file; update the phase-b0 move-in workflow to either (A) add a setup step that
materializes the move-out `skipped` array into the expected movein-skipped file
(so the grep -iE '\b(${t.name}|${t.cyclebreak_key}|bun_${t.cyclebreak_key})\b'
has real data), or (B) add robust error-handling before the grep (ensure the
file exists or create an empty placeholder from the move-out `skipped` data, or
short-circuit the grep with a safe fallback) so explicit move-out requests
aren’t silently missed. Ensure you modify the step that runs the grep command
and/or the move-out producer that exposes the `skipped` array to produce the
expected artifact.
| **Your task list:** | ||
| 1. Read \`${REPO}/docs/CYCLEBREAK.md\` — find section "### → \\\`${t.cyclebreak_key}\\\`" under "Per-target-crate move-in tasks". That lists \`from <crate>: <symbol>, ...\` entries. | ||
| 2. \`grep -iE '\\b(${t.name}|${t.cyclebreak_key}|bun_${t.cyclebreak_key})\\b' /tmp/movein-skipped.txt\` — these are explicit "move-in pass must add X to ${t.name}" requests from move-out agents. Higher priority than CYCLEBREAK.md (more specific). | ||
| 3. Read \`${REPO}/docs/PORTING.md\` §Dispatch (lines ~377-440) and §Concurrency (lines ~352-376) for vtable/hook/OnceLock patterns. |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Hardcoded line ranges may become stale.
The prompt references specific line ranges in PORTING.md (lines ~377-440 and ~352-376). If PORTING.md is edited and sections move, these line numbers will become incorrect, causing agents to read the wrong sections or miss important guidance.
Consider either:
- Using section headers (e.g., "§Dispatch", "§Concurrency") which are more stable
- Adding a validation step to check that the referenced line ranges contain the expected section headers
- Documenting that PORTING.md line numbers must be kept stable
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.claude/workflows/phase-b0-movein.workflow.js at line 37, The workflow
currently references hardcoded line ranges in PORTING.md which can become stale;
update the workflow that reads PORTING.md to locate sections by header names
instead of fixed line ranges (search for the "§Dispatch" and "§Concurrency"
headers), or add a validation step that confirms the text found at the
referenced ranges contains the expected "§Dispatch" and "§Concurrency" headers
and fails with a clear error if not, or alternatively document that the
PORTING.md line numbers must remain stable—implement one of these fixes and
ensure the code uses the "§Dispatch" and "§Concurrency" markers to locate
content reliably.
| const REPO = "/root/bun-5"; | ||
| const CRATES = (args && args.crates) || []; // [{name, tier}] | ||
| if (!CRATES.length) return { error: "no crates" }; | ||
| log(`B-1 tier ${CRATES[0].tier}: ${CRATES.length} crates`); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check how this workflow is invoked to confirm single-tier batching
rg -n -C5 'phase-b1-tier' .claude/ scripts/Repository: oven-sh/bun
Length of output: 640
🏁 Script executed:
cat -n .claude/workflows/phase-b1-tier.workflow.jsRepository: oven-sh/bun
Length of output: 3072
🏁 Script executed:
rg -n 'CRATES\[0\]\.tier|\.tier' .claude/workflows/phase-b1-tier.workflow.jsRepository: oven-sh/bun
Length of output: 166
🏁 Script executed:
# Search for orchestration or calling patterns that control how this workflow receives crates
rg -n -B3 -A3 'phase-b1-tier' .claude/ | grep -E '(invoke|call|phase|tier)' | head -30Repository: oven-sh/bun
Length of output: 519
🏁 Script executed:
# Search for files that reference or invoke phase-b1-tier
rg -l 'phase-b1-tier' .claude/ scripts/ --type-list | head -20Repository: oven-sh/bun
Length of output: 445
🏁 Script executed:
# Look for workflow orchestration or phase definitions
fd -name '*.js' .claude/ | xargs rg -l 'phase.*tier|workflow' | head -10Repository: oven-sh/bun
Length of output: 702
🏁 Script executed:
# Search for where crates are prepared/batched for this workflow
rg -n 'phase-b1|batch.*tier' .claude/ scripts/ -B2 -A2 | head -50Repository: oven-sh/bun
Length of output: 470
🏁 Script executed:
# Find all workflow files
find .claude/workflows -name '*.js' -type f | head -20Repository: oven-sh/bun
Length of output: 1026
🏁 Script executed:
# Search for references to phase-b1-tier in all workflow files
find .claude/workflows -name '*.js' -type f -exec grep -l 'phase-b1-tier' {} \;Repository: oven-sh/bun
Length of output: 99
🏁 Script executed:
# Look for orchestration that might batch crates by tier
rg -i 'crate.*tier|batch.*tier|group.*tier' .claude/ -B2 -A2Repository: oven-sh/bun
Length of output: 8779
🏁 Script executed:
# Look for main orchestration or invocation patterns
find .claude -name '*.js' -o -name '*.ts' -o -name '*.json' | grep -v workflows | head -20Repository: oven-sh/bun
Length of output: 151
🏁 Script executed:
# Search for workflow invocation patterns
rg -i 'invoke|call|run.*workflow|phase-b1' .claude --type-list 2>/dev/null || rg -i 'invoke|call|run.*workflow|phase-b1' .claude 2>/dev/null | head -30Repository: oven-sh/bun
Length of output: 6625
🏁 Script executed:
# Check if there's a main index or runner
ls -la .claude/ | head -20Repository: oven-sh/bun
Length of output: 464
🏁 Script executed:
# Check commands and skills directories
ls -la .claude/commands/
ls -la .claude/skills/Repository: oven-sh/bun
Length of output: 1109
🏁 Script executed:
# Search for phase-b1-tier invocation in commands and skills
find .claude/commands .claude/skills -type f -exec grep -l 'phase-b1-tier' {} \;Repository: oven-sh/bun
Length of output: 37
🏁 Script executed:
# Look for batching or tier grouping logic anywhere in .claude
rg -i 'batch|group.*crate|crate.*batch' .claude/commands .claude/skills 2>/dev/null | head -20Repository: oven-sh/bun
Length of output: 37
Validate tier homogeneity assumption.
Lines 10 and 53 assume all crates in args.crates share the same tier value (using CRATES[0].tier). While the workflow description states "one tier at a time," the code doesn't enforce this constraint—it only assumes it. If mixed-tier input reaches this workflow, the log message and final output will report the wrong tier.
Note the inconsistency: individual results correctly preserve per-crate tier (line 47), but the batch output discards this information by assuming a single tier.
🛡️ Proposed validation check
const CRATES = (args && args.crates) || []; // [{name, tier}]
if (!CRATES.length) return { error: "no crates" };
+const tiers = [...new Set(CRATES.map(c => c.tier))];
+if (tiers.length > 1) return { error: `mixed tiers: ${tiers.join(", ")} - expected single tier` };
log(`B-1 tier ${CRATES[0].tier}: ${CRATES.length} crates`);Also applies to: 53-53
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.claude/workflows/phase-b1-tier.workflow.js at line 10, The code assumes all
input crates share the same tier by using CRATES[0].tier for the batch log and
final output; add a validation in the beginning of the workflow to verify
homogeneity (e.g., compute a set of tiers from args.crates/CRATES and if its
size > 1 throw or normalize), and then use a safe value (the single tier) only
after validation; update references where CRATES[0].tier is used (the initial
log call and the final batch summary around line 53) to either compute the tier
from the validated set or to report that multiple tiers were received if you
choose to error instead.
|
|
||
| const by_crate = {}; | ||
| for (const b of BUGS) { | ||
| const crate = b.module.split("/")[0]; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check how bug objects are created/passed to understand module field format
rg -n -C3 'module.*:' .claude/ scripts/ | rg -i 'bug|verify'Repository: oven-sh/bun
Length of output: 5532
🏁 Script executed:
# Read the target file to understand context around line 23
cat -n .claude/workflows/phase-b2-fix-bugs.workflow.js | head -50Repository: oven-sh/bun
Length of output: 2449
🏁 Script executed:
# Search for all places where module field is created/assigned
rg -n "module.*=" .claude/workflows/ -A2Repository: oven-sh/bun
Length of output: 1752
🏁 Script executed:
# Check how phase-b2-fix-bugs.workflow.js receives input/bugs
rg -n "bugs\|inputs\|params" .claude/workflows/phase-b2-fix-bugs.workflow.js -B2 -A2Repository: oven-sh/bun
Length of output: 37
🏁 Script executed:
# Search for all places where bugs are created with module field
rg -n "module:" .claude/workflows/ | rg -v "Binary"Repository: oven-sh/bun
Length of output: 683
🏁 Script executed:
# Search for places that invoke phase-b2-fix-bugs
rg -n "phase-b2-fix-bugs" .claude/workflows/Repository: oven-sh/bun
Length of output: 134
Add validation for module field format to prevent silent grouping failures.
Line 23 assumes b.module follows the format "crate/..." when extracting the crate name via .split("/")[0]. While this format is consistently produced by phase-b2-verify.workflow.js, the code lacks validation. If a bug entry has a malformed module field (e.g., "crate::file" using a different delimiter, or an empty string), unexpected grouping behavior occurs. Add a format check or clear documentation of the expected format.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.claude/workflows/phase-b2-fix-bugs.workflow.js at line 23, Validate
b.module before using b.module.split("/") to compute crate: ensure b.module is a
non-empty string and contains the expected "/" delimiter (or match a regex like
/^[^\/]+\/.+$/) and handle failures (e.g., log a warning and skip grouping,
throw, or default to a safe value) instead of blindly doing const crate =
b.module.split("/")[0]; update the code around the crate extraction to perform
the check, use the validated part when present, and add a clear log message
documenting the expected "crate/..." format when invalid.
| const FOCUS = args.verify_focus || "logic correctness vs spec"; | ||
| const SCOPE = (args.edit_scope || []).join(", ") || "the files you created"; | ||
|
|
||
| if (!TASK) return { error: "args.task required" }; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does JavaScript allow a return statement at the top level of an ES module?
💡 Result:
No. A JavaScript return statement is not allowed at the top level of an ES module; it must appear inside a function body. Reasoning (spec + docs): The ECMAScript specification defines ReturnStatement as something that “causes a function to cease execution” and (in most cases) returns a value to the caller [1]. MDN similarly states that the return statement “can only be used within function bodies” [2] and documents the SyntaxError: return not in function that occurs when return is used outside a function [3]. Example that fails in module top-level code: - // in an ES module file\nreturn 1; This is invalid because there is no surrounding function to return from [3]. If you need to end module execution, use regular control flow in your own functions (e.g., wrap logic in an IIFE or exported function) rather than attempting return at the module’s top level.
Citations:
- 1: https://tc39.es/ecma262/multipage/ecmascript-language-statements-and-declarations.html
- 2: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/return
- 3: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Bad_return
Top-level return statements will cause parse errors.
ES modules do not allow return statements outside function bodies. Wrap the executable code in an IIFE or function closure that the workflow runtime expects.
This applies to lines 17, 59, 87, and 103.
🧰 Tools
🪛 Biome (2.4.14)
[error] 17-17: Illegal return statement outside of a function
(parse)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.claude/workflows/phase-b2-keystone.workflow.js at line 17, The file
contains top-level return statements (e.g., the check "if (!TASK) return {
error: 'args.task required' }") which are invalid in ES modules; wrap the
executable workflow code in a function or immediately-invoked async function
(IIFE) that the workflow runtime expects, move all top-level logic (including
checks at TASK and the other return sites referenced) into that function, and
replace those top-level "return" statements with returns from that function (or
throw errors/resolve values from the function) so no return appears at module
top-level; update the checks around the TASK variable and the other early-exit
points so they return from the newly created function scope.
| set -uo pipefail | ||
| NAME="$1"; OUT="$2"; NEXT="${3:-100}" | ||
| cd "$(dirname "$0")/.." | ||
|
|
||
| command cp -f "$OUT" "/tmp/port-results/$NAME.json" | ||
| STATS=$(jq -r '.result | "\(.total)\t\(.clean)\t\(.fixed)\t\([.results[]|select(.==null)]|length)\t\(.by_confidence.high)\t\(.by_confidence.medium)\t\(.by_confidence.low)"' "$OUT" 2>/dev/null || echo "? ? ? ? ? ? ?") | ||
| DUR=$(jq -r '.usage.duration_ms // "?"' "$OUT" 2>/dev/null) | ||
| AG=$(jq -r '.usage.agent_count // "?"' "$OUT" 2>/dev/null) | ||
| printf "%s\t%s\t%s\t%s\n" "$NAME" "$STATS" "$DUR" "$AG" >> /tmp/port-results/log.tsv |
There was a problem hiding this comment.
Fail fast before archiving and logging.
cd, cp, and the append to /tmp/port-results/log.tsv can all fail here without stopping the script. On a fresh runner, a missing /tmp/port-results silently drops the batch artifact/log and the script still proceeds to commit/push.
Suggested hardening
-set -uo pipefail
+set -euo pipefail
NAME="$1"; OUT="$2"; NEXT="${3:-100}"
-cd "$(dirname "$0")/.."
+cd "$(dirname "$0")/.." || exit 1
+mkdir -p /tmp/port-results📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| set -uo pipefail | |
| NAME="$1"; OUT="$2"; NEXT="${3:-100}" | |
| cd "$(dirname "$0")/.." | |
| command cp -f "$OUT" "/tmp/port-results/$NAME.json" | |
| STATS=$(jq -r '.result | "\(.total)\t\(.clean)\t\(.fixed)\t\([.results[]|select(.==null)]|length)\t\(.by_confidence.high)\t\(.by_confidence.medium)\t\(.by_confidence.low)"' "$OUT" 2>/dev/null || echo "? ? ? ? ? ? ?") | |
| DUR=$(jq -r '.usage.duration_ms // "?"' "$OUT" 2>/dev/null) | |
| AG=$(jq -r '.usage.agent_count // "?"' "$OUT" 2>/dev/null) | |
| printf "%s\t%s\t%s\t%s\n" "$NAME" "$STATS" "$DUR" "$AG" >> /tmp/port-results/log.tsv | |
| set -euo pipefail | |
| NAME="$1"; OUT="$2"; NEXT="${3:-100}" | |
| cd "$(dirname "$0")/.." || exit 1 | |
| mkdir -p /tmp/port-results | |
| command cp -f "$OUT" "/tmp/port-results/$NAME.json" | |
| STATS=$(jq -r '.result | "\(.total)\t\(.clean)\t\(.fixed)\t\([.results[]|select(.==null)]|length)\t\(.by_confidence.high)\t\(.by_confidence.medium)\t\(.by_confidence.low)"' "$OUT" 2>/dev/null || echo "? ? ? ? ? ? ?") | |
| DUR=$(jq -r '.usage.duration_ms // "?"' "$OUT" 2>/dev/null) | |
| AG=$(jq -r '.usage.agent_count // "?"' "$OUT" 2>/dev/null) | |
| printf "%s\t%s\t%s\t%s\n" "$NAME" "$STATS" "$DUR" "$AG" >> /tmp/port-results/log.tsv |
🧰 Tools
🪛 Shellcheck (0.11.0)
[warning] 6-6: Use 'cd ... || exit' or 'cd ... || return' in case cd fails.
(SC2164)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/port-cycle.sh` around lines 4 - 12, The script currently can continue
on errors (missing /tmp/port-results, cd, cp, printf) because it lacks errexit;
update the top of the script to fail fast (add -e to the existing set -uo
pipefail) and ensure the artifact dir exists before use by creating
/tmp/port-results (mkdir -p /tmp/port-results) and aborting if that fails; also
ensure critical commands (cd "$(dirname "$0")/..", command cp -f "$OUT"
"/tmp/port-results/$NAME.json", and the printf append to
/tmp/port-results/log.tsv) are allowed to propagate errors (rely on set -e) or
explicitly check their exit codes so the script stops on failure, referencing
variables NAME, OUT and the target files /tmp/port-results/$NAME.json and
/tmp/port-results/log.tsv.
| git -c core.hooksPath=/dev/null add 'src/**/*.rs' src/*.rs 2>/dev/null | ||
| N=$(git diff --cached --name-only | wc -l) | ||
| if [ "$N" -gt 0 ]; then | ||
| git -c core.hooksPath=/dev/null commit -q -m "phase-a: draft batch $NAME ($N files)" |
There was a problem hiding this comment.
Commit only the Rust paths this script stages.
git diff --cached --name-only and git commit are both operating on the whole index, so any unrelated staged files get swept into the draft batch commit. This script should count and commit only the src/*.rs paths it owns.
Scope the staged-count and commit to the intended paths
-git -c core.hooksPath=/dev/null add 'src/**/*.rs' src/*.rs 2>/dev/null
-N=$(git diff --cached --name-only | wc -l)
+git -c core.hooksPath=/dev/null add -- 'src/**/*.rs' 'src/*.rs' 2>/dev/null
+N=$(git diff --cached --name-only -- 'src/**/*.rs' 'src/*.rs' | wc -l)
if [ "$N" -gt 0 ]; then
- git -c core.hooksPath=/dev/null commit -q -m "phase-a: draft batch $NAME ($N files)"
+ git -c core.hooksPath=/dev/null commit -q -m "phase-a: draft batch $NAME ($N files)" -- 'src/**/*.rs' 'src/*.rs'🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/port-cycle.sh` around lines 14 - 17, The script currently counts and
commits the entire index; restrict both the staged-file count and the commit to
only the Rust pathspecs by changing the N computation to list cached names
filtered to the intended pathspecs (e.g., use git diff --cached --name-only --
'src/**/*.rs' 'src/*.rs' when computing N) and invoke git commit with those same
pathspecs (e.g., git commit ... -- 'src/**/*.rs' 'src/*.rs') so only the files
the script manages are counted and committed; update the commands in
scripts/port-cycle.sh where N is set and where git commit is invoked.
| const installed = spawnSync("rustup", ["target", "list", "--installed"], { encoding: "utf8" }); | ||
| if (installed.status === 0 && !installed.stdout.split("\n").includes(triple)) { | ||
| console.log(`\x1b[2m[skip]\x1b[0m ${triple} (rustup target add ${triple})`); |
There was a problem hiding this comment.
Normalize rustup output before matching targets.
On Windows, rustup target list --installed is CRLF-delimited, so split("\n").includes(triple) leaves a trailing \r on each entry and falsely marks installed targets as missing. That makes this script skip every installed target on Windows.
Proposed fix
- const installed = spawnSync("rustup", ["target", "list", "--installed"], { encoding: "utf8" });
- if (installed.status === 0 && !installed.stdout.split("\n").includes(triple)) {
+ const installed = spawnSync("rustup", ["target", "list", "--installed"], { encoding: "utf8" });
+ const installedTargets = new Set(
+ installed.stdout
+ .split(/\r?\n/)
+ .map(line => line.trim())
+ .filter(Boolean),
+ );
+ if (installed.status === 0 && !installedTargets.has(triple)) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const installed = spawnSync("rustup", ["target", "list", "--installed"], { encoding: "utf8" }); | |
| if (installed.status === 0 && !installed.stdout.split("\n").includes(triple)) { | |
| console.log(`\x1b[2m[skip]\x1b[0m ${triple} (rustup target add ${triple})`); | |
| const installed = spawnSync("rustup", ["target", "list", "--installed"], { encoding: "utf8" }); | |
| const installedTargets = new Set( | |
| installed.stdout | |
| .split(/\r?\n/) | |
| .map(line => line.trim()) | |
| .filter(Boolean), | |
| ); | |
| if (installed.status === 0 && !installedTargets.has(triple)) { | |
| console.log(`\x1b[2m[skip]\x1b[0m ${triple} (rustup target add ${triple})`); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/rust-check-all.ts` around lines 25 - 27, The rustup output split
leaves CRLFs on Windows so entries can have trailing "\r" and never match
`triple`; update the check around the `installed` result (the
`spawnSync("rustup", ["target", "list", "--installed"], ...)` call and the
subsequent `installed.stdout.split("\n").includes(triple)` logic) to normalize
line endings and whitespace before matching (e.g., split on CRLF-aware pattern
or map and trim each entry) so `triple` is matched correctly across platforms.
| let mut buf: [libc::pid_t; 4096] = [0; 4096]; | ||
| // Hard cap on tree size so a fork bomb under us can't make exit hang. | ||
| while !to_visit.is_empty() && to_kill.len() < 4096 { | ||
| let parent = to_visit.swap_remove(to_visit.len() - 1); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Consider using pop() instead of swap_remove(len - 1).
Vec::pop() is clearer and accomplishes the same thing:
♻️ Suggested simplification
- while !to_visit.is_empty() && to_kill.len() < 4096 {
- let parent = to_visit.swap_remove(to_visit.len() - 1);
+ while let Some(parent) = to_visit.pop() {
+ if to_kill.len() >= 4096 {
+ break;
+ }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/aio/ParentDeathWatchdog.rs` at line 362, Replace the explicit
to_visit.swap_remove(to_visit.len() - 1) with to_visit.pop() and handle the
returned Option; either use to_visit.pop().expect("...")/unwrap() if empty is
impossible in this context, or match/if-let to handle None safely. This change
targets the code that removes the last element from the Vec (the variable
to_visit) so update the call site accordingly to use pop() and adjust for the
Option return type.
| if rc != size { | ||
| return 0; | ||
| } | ||
| return libc::pid_t::try_from(info.pbi_ppid).expect("int cast"); |
There was a problem hiding this comment.
Potential overflow casting pbi_ppid (u32) to pid_t (i32).
info.pbi_ppid is a u32, and pid_t is typically i32 on macOS. If pbi_ppid > i32::MAX (unlikely in practice, but possible), try_from().expect() will panic. Consider using unwrap_or(0) to treat overflow as "no parent" rather than panicking in an exit handler.
🛡️ Proposed fix
- return libc::pid_t::try_from(info.pbi_ppid).expect("int cast");
+ return libc::pid_t::try_from(info.pbi_ppid).unwrap_or(0);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return libc::pid_t::try_from(info.pbi_ppid).expect("int cast"); | |
| return libc::pid_t::try_from(info.pbi_ppid).unwrap_or(0); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/aio/ParentDeathWatchdog.rs` at line 568, The cast from info.pbi_ppid
(u32) to libc::pid_t currently uses
libc::pid_t::try_from(info.pbi_ppid).expect("int cast"), which can panic on
overflow; change it to safely handle overflow by using .unwrap_or(0) (or
equivalent non-panicking fallback) so an out-of-range parent PID becomes 0
instead of causing a panic—update the expression around info.pbi_ppid /
libc::pid_t::try_from in ParentDeathWatchdog.rs to use unwrap_or(0) and return
that value.
| * Release + assertions + logs. RelWithDebInfo → cargo `release` profile | ||
| * with `debug-assertions = true` (runtime safety checks), matching the | ||
| * old cmake build:assert script. |
There was a problem hiding this comment.
🔴 The new comment says "cargo release profile with debug-assertions = true", but nothing actually sets that: cargoProfile() (rust.ts:68-69) maps RelWithDebInfo to plain release, [profile.release] in Cargo.toml doesn't set debug-assertions, and emitRust() never passes CARGO_PROFILE_RELEASE_DEBUG_ASSERTIONS — cfg.assertions only sets the BUN_RELEASE_SAFE env var (rust.ts:385), which drives bun's custom ALLOW_ASSERT const, not cfg!(debug_assertions)/debug_assert!(). Either fix the comment to describe BUN_RELEASE_SAFE/ALLOW_ASSERT, or (to match the profile's name and the old Zig ReleaseSafe behavior) have emitRust() set CARGO_PROFILE_RELEASE_DEBUG_ASSERTIONS=true when cfg.assertions is set.
Extended reasoning...
What the bug is
This PR rewrote the doc comment on the release-assertions profile in scripts/build/profiles.ts:110-112 from the accurate
Release + assertions + logs. RelWithDebInfo → zig gets ReleaseSafe
to
Release + assertions + logs. RelWithDebInfo → cargo
releaseprofile withdebug-assertions = true(runtime safety checks), matching the old cmake build:assert script.
The new comment makes a specific, technically false claim: nothing in the build sets cargo's debug-assertions = true for this profile.
The code path
-
cargoProfile()atscripts/build/rust.ts:68-69maps every non-DebugbuildTypeto the plainreleaseprofile:return cfg.buildType === "Debug" ? { name: "dev", subdir: "debug" } : { name: "release", subdir: "release" };
So
buildType: "RelWithDebInfo"→ cargo profilerelease. -
[profile.release]inCargo.toml:138-142sets onlylto,codegen-units,debug, andstrip. It does not setdebug-assertions, so cargo's default offalsefor the release profile applies. -
emitRust()never passes-C debug-assertions=onin rustflags and never setsCARGO_PROFILE_RELEASE_DEBUG_ASSERTIONS. Greppingscripts/build/rust.tsforassertions|DEBUG_ASSERTIONS|debug-assertionsyields exactly one hit:// rust.ts:385 BUN_RELEASE_SAFE: String(cfg.assertions),
-
BUN_RELEASE_SAFEis consumed atsrc/bun_core/lib.rs:100viabuild_opt_bool!intobuild_options::RELEASE_SAFE, which feedsbun_core::env::ALLOW_ASSERT(env.rs:48). This is bun's own custom assertion gate — it is notcfg!(debug_assertions), so it has no effect on Rust's standarddebug_assert!()/debug_assert_eq!()macros or on integer-overflow checks, in either bun's own crates or any of the ~80 third-party dependencies.
Step-by-step proof
A developer reads the release-assertions profile comment, sees "debug-assertions = true (runtime safety checks)", and runs bun run build:release-assertions. The build:
- resolves
cfg = { buildType: "RelWithDebInfo", assertions: true, ... }; cargoProfile(cfg)returns{ name: "release" };- cargo builds with
--profile release, and since[profile.release]has nodebug-assertionskey, rustc receives-C debug-assertions=off(the release default); BUN_RELEASE_SAFE=trueis set in the env, soALLOW_ASSERT == trueand bun's hand-writtenbun_core::assert!-style checks are compiled in;- but every
debug_assert!()insrc/**/*.rsand in every dependency crate is compiled out, and release-mode wrapping integer arithmetic is in effect.
The comment claims (3) does the opposite of what it actually does.
Why nothing prevents it
This is build configuration prose; nothing type-checks it. The PR diff (around line 15372 of pr_diff.txt) shows the comment was deliberately rewritten as part of dropping the Zig path, but the corresponding cargo plumbing was never added. The old comment was accurate for the old build (zigOptimize() mapped RelWithDebInfo → ReleaseSafe, which in Zig enables bounds + overflow checks); the new comment describes plumbing that doesn't exist.
Impact
This is more than a doc nit because the profile's name is release-assertions and the comment is the only spec of what "assertions" means here. The old Zig ReleaseSafe mode enabled language-level safety checks (bounds, overflow, unreachable). The Rust replacement enables only bun's custom ALLOW_ASSERT flag. A developer choosing this profile to catch a suspected debug_assert!() failure or integer overflow in an optimized build will get neither — the class of checks the comment explicitly promises ("debug-assertions = true") is disabled.
How to fix
Two options:
(a) Make the comment true (matches the profile's intent and the prior Zig behavior): in emitRust(), when cfg.assertions is true, add to the env block:
...(cfg.assertions ? { CARGO_PROFILE_RELEASE_DEBUG_ASSERTIONS: "true" } : {}),(and optionally CARGO_PROFILE_RELEASE_OVERFLOW_CHECKS: "true" to fully match Zig's ReleaseSafe).
(b) Make the comment accurate: change it to e.g. "RelWithDebInfo → cargo release profile with BUN_RELEASE_SAFE=true (enables bun's ALLOW_ASSERT checks; note: does not enable Rust debug_assert!())."
Option (a) is likely the intended fix given the profile's name and the explicit "matching the old cmake build:assert script" claim.
| const rustHeader = line.match(/^(error|warning)(\[[A-Z0-9]+\])?: (.+)$/); | ||
| if (rustHeader && !/\b(generated|emitted)\b/.test(line) /* "warning: 3 warnings emitted" */) { | ||
| const [, level, code, title] = rustHeader; | ||
| const { match: locMatch } = readUntil(/-->\s+(.+?):(\d+):(\d+)/, 3); | ||
| // Swallow the diagnostic body up to the blank-line separator (rustc | ||
| // always emits one between diagnostics in the human format; cap at 30 | ||
| // for `--message-format=short` which doesn't). | ||
| readUntil(/^$/, 30); |
There was a problem hiding this comment.
🔴 The readUntil(/^$/, 30) call at line 2669 is meant to consume the rustc diagnostic body up to the blank-line separator, but because originalLines is produced by content.split(/(\r?\n)/) (capturing group → newline chars become separate array elements), the very first element examined after the --> line is a bare '\n' token which .trim()s to '' and matches /^$/ immediately. The annotation body therefore loses the rendered span (|, ^^^^ carets, help:/note: lines), defeating the explicit intent in the comment at lines 2659-2661. Fix: match the newline token itself (readUntil(/^\r?\n$/, 30)) and likewise bump the maxLength=3 on the locMatch readUntil since it currently counts newline tokens too.
Extended reasoning...
What the bug is
The new rustc-diagnostic branch in parseAnnotations() (added by this PR to replace the old Zig-error parser) builds each BuildKite annotation body by accumulating lines into bufferedLines via two readUntil() calls: first to find the --> file:line:col line, then to swallow everything up to the blank line that separates rustc diagnostics. The second call, readUntil(/^$/, 30), is supposed to stop at that blank-line separator. The inline comment at lines 2659-2661 is explicit about the intent: "Read until the blank line that separates rustc diagnostics so the annotation body contains the rendered span + help/note lines."
However, originalLines is built at line 2572 by content.split(/(\r?\n)/) — note the capturing group. With a capturing group, String.prototype.split interleaves the captured separators into the result array, so for input like:
error[E0308]: mismatched types
--> src/http/lib.rs:553:5
|
553| return foo;
| ^^^^^^ expected `Bar`, found `Foo`
= help: ...
originalLines becomes ['error[E0308]: mismatched types', '\n', ' --> src/http/lib.rs:553:5', '\n', ' |', '\n', '553| return foo;', '\n', ...] — every other element is a bare '\n'.
Inside readUntil (lines 2590-2591), each element is run through stripAnsi(originalLine).trim(). A '\n' element trims to the empty string '', and /^$/.exec('') returns a truthy match. So /^$/ matches the first newline token, not the actual blank-line separator between diagnostics.
Step-by-step proof
Using the example above with indices:
| i | originalLines[i] | trimmed |
|---|---|---|
| 0 | error[E0308]: mismatched types |
error[E0308]: mismatched types |
| 1 | \n |
"" |
| 2 | --> src/http/lib.rs:553:5 |
--> src/http/lib.rs:553:5 |
| 3 | \n |
"" |
| 4 | | |
| |
| 5 | \n |
"" |
| 6 | 553| return foo; |
… |
| … | … | … |
| 12 | "" |
"" ← the real blank-line separator |
- Outer loop is at
i = 0(the header line).bufferedLines = ['error[E0308]: ...']. - First
readUntil(/-->\s+(.+?):(\d+):(\d+)/, 3): starts atlength=0, readsoriginalLines[0](header, no match),originalLines[1]('\n'→'', no match —/^$/isn't the pattern here),originalLines[2](matches-->).length = 3.originalLines.slice(1, i += 3)=['\n', ' --> ...']is pushed tobufferedLines. Nowi = 3. - Second
readUntil(/^$/, 30): starts atlength=0, readsoriginalLines[3]='\n', trims to'', matches/^$/immediately.length = 1.originalLines.slice(4, i += 1)=slice(4, 4)=[]— nothing is pushed tobufferedLines. Nowi = 4. - Final
bufferedLines = ['error[E0308]: mismatched types', '\n', ' --> src/http/lib.rs:553:5'].
The rendered span (the | gutter, the ^^^^ caret line, and the help:/note: lines — i.e., elements 4-11) never reaches bufferedLines and is therefore absent from annotation.content.
Why existing code doesn't prevent it
The capturing-group split at line 2572 is pre-existing (it's used so that bufferedLines round-trips with original line endings preserved when the annotation body is later joined). The pre-existing parser branches (CMake's /Call Stack/, Github's ::error::) all match against non-empty content patterns, so they never accidentally match a newline token. This new rustc branch is the first to use /^$/ as a sentinel, and it's the only one bitten by the newline-token interleaving.
The maxLength=3 on the first readUntil at line 2665 has the same shape — it counts newline tokens, so it's effectively ~1.5 real lines of lookahead. That happens to work for standard rustc human output (where --> is always the line immediately after the header, found at length=3 as shown above), but it's tighter than intended.
Impact
BuildKite annotations for rustc compile errors will contain only the one-line title + file:line:col, but not the rendered source span, caret underline, or any help:/note: suggestions — which are the most actionable part of a rustc diagnostic. The annotation isn't useless (file/line/title still let you click through), but it's materially degraded relative to the stated intent and to what the replaced Zig parser provided. Since this PR's whole purpose is migrating CI from Zig to Rust, the rustc annotation quality is squarely on the critical path.
How to fix
Change line 2669 to match the actual newline token rather than the trimmed empty string:
readUntil(/^\r?\n$/, 60);(The maxLength should also roughly double since half the elements are newline tokens.) Alternatively, split without the capturing group — but other branches of parseAnnotations rely on the newline tokens being preserved in bufferedLines for round-tripping, so matching the token is the minimal fix. The maxLength=3 at line 2665 should similarly become 6 for the same reason.
🔬 also observed by coderabbitai
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🟡
scripts/build/source.ts:669-672— This PR removes the only callers ofdepSourceStamp()(the import and bothdepSourceStamp(cfg, "zstd")call sites inbun.tsare gone;emitRustOnly()now usesresolveDep()→lolhtmlDep.outputsinstead), yet the same PR also rewrote this function's JSDoc from "Used by zig-only mode to depend on zstd's source" to "Used by rust-only mode to depend on lolhtml's source being on disk without resolving the full dep graph" — a claim that is now false (a repo-wide grep finds zero callers, and rust-only mode does resolve the dep viaresolveDep()). Either deletedepSourceStamp()entirely or revert/correct the docstring.Extended reasoning...
What the bug is
depSourceStamp()atscripts/build/source.ts:672is now dead code, but this PR actively rewrote its JSDoc to describe a use that doesn't exist. The new docstring at lines 668-671 reads:Path to a dep's fetch stamp. Used by rust-only mode to depend on lolhtml's source being on disk without resolving the full dep graph.
This is false on both counts: nothing in rust-only mode (or anywhere else) calls
depSourceStamp(), and rust-only mode does resolve the full dep graph for lolhtml viaresolveDep().The code path
Before this PR,
depSourceStamp()had two callers, both inscripts/build/bun.ts:- the full-build path:
zstdStamp: depSourceStamp(cfg, "zstd")(passed toemitZig) emitZigOnly(): same call
This PR removes the import (
-import { ..., depSourceStamp, ... }→+import { computeDepLibs, resolveDep, ... }) and replaces both call sites withvendorStamps: lolhtmlDep.outputs/vendorStamps: depsByName.get("lolhtml")?.outputs ?? [], wherelolhtmlDepcomes fromresolveDep(n, cfg, lolhtml, ...)atbun.ts:539. So the new mechanism for depending on lolhtml's source isresolveDep().outputs, notdepSourceStamp().In the same PR,
source.tsline ~669 was changed from:- * Path to a dep's fetch stamp. Used by zig-only mode to depend on zstd's + * Path to a dep's fetch stamp. Used by rust-only mode to depend on lolhtml's * source being on disk without resolving the full dep graph.
So the PR author updated the docstring to track the zig→rust / zstd→lolhtml rename, but didn't notice they'd simultaneously removed every caller and switched the consumer to a different API.
Step-by-step proof
grep -rn depSourceStampover the repo at HEAD returns exactly one line:scripts/build/source.ts:672:export function depSourceStamp(...). No imports, no calls.- The PR diff shows
bun.tsremovingdepSourceStampfrom its import line and removing bothdepSourceStamp(cfg, "zstd")call sites. emitRustOnly()inbun.ts:537-557(the "rust-only mode" the docstring references) callsresolveDep(n, cfg, lolhtml, new Map())and passeslolhtmlDep.outputsasvendorStampstoemitRust(). It never referencesdepSourceStamp.- The phrase "without resolving the full dep graph" is therefore doubly wrong: rust-only mode does call
resolveDep()(line 539), which is exactly the "full dep graph" resolution the docstring claims to avoid.
Why nothing prevents it
This is documentation drift on an exported-but-unused function. TypeScript doesn't warn on unused exports, and there's no doc/lint pass that validates JSDoc claims against call sites. The change slipped through because the docstring edit and the caller removal happened in the same PR but in different files.
Impact
Zero runtime impact — the function isn't called, so the misleading docstring can't cause incorrect behavior. The cost is purely maintainability: a future reader grepping for how rust-only mode wires the lolhtml fetch dependency will find this docstring claiming
depSourceStampis the mechanism, follow it, find no callers, and have to re-derive thatresolveDep().outputsis the actual path. The docstring is worse than stale — it was freshly rewritten by this PR to make a specific, false claim about new code.How to fix
Either:
- Delete
depSourceStamp()entirely (preferred — it's a 3-line function with zero callers, anddepSourceDir()right above it remains for anything that needs the directory path), or - Revert the docstring to something accurate, e.g. "Path to a dep's fetch stamp (`.ref` file written by the fetch step). Currently unused;
resolveDep().outputsis the canonical way to depend on a fetched dep."
- the full-build path:
| /** | ||
| * Parse the major component out of an X.Y.Z version string. | ||
| * Returns undefined for undefined/unparseable input so callers can | ||
| * compare without `!` assertions. | ||
| */ | ||
| function majorOf(version: string | undefined): number | undefined { | ||
| if (version === undefined) return undefined; | ||
| const m = version.match(/^(\d+)\./); | ||
| return m ? Number(m[1]) : undefined; | ||
| } |
There was a problem hiding this comment.
🟡 The two new helper functions majorOf() and readRustToolchainChannel() were inserted between getGitRevision()'s JSDoc block (lines 1009-1016) and the function getGitRevision() definition itself (now at line 1045), so the "Get the current git revision..." doc comment is now orphaned and getGitRevision() has no documentation. Move the two helpers either above line 1009 or below getGitRevision()'s closing brace at line 1056.
Extended reasoning...
What the bug is
The PR adds two new helper functions, majorOf() (config.ts:1022-1026) and readRustToolchainChannel() (config.ts:1038-1043), each with their own JSDoc block. They were inserted at the wrong location: immediately after the closing */ of the pre-existing JSDoc for getGitRevision(), but before the function getGitRevision(cwd: string) declaration that JSDoc was meant to document.
How it manifests
After this PR, scripts/build/config.ts:1009-1045 reads:
/**
* Get the current git revision (HEAD sha).
*
* Uses `git rev-parse` rather than reading .git/HEAD directly — the sha
* is baked into the binary and surfaces in bug reports, so correctness
* matters more than the ~20ms spawn. Git's plumbing has edge cases
* (packed-refs, worktrees, symbolic refs) that rev-parse handles for free.
*/
/**
* Parse the major component out of an X.Y.Z version string.
* ...
*/
function majorOf(version: string | undefined): number | undefined { ... }
/**
* Read `channel` from `rust-toolchain.toml`. ...
*/
function readRustToolchainChannel(cwd: string): string | undefined { ... }
function getGitRevision(cwd: string): string { ... }Lines 1016-1017 are back-to-back */ /** — two adjacent JSDoc blocks with nothing between them. The first block (about git rev-parse) is now orphaned: per JSDoc/TSDoc rules a doc comment attaches to the immediately following declaration, but what follows is another /** block, not a declaration. Tooling (TypeScript language server hover, typedoc, IDE quickdoc) will therefore associate nothing with the first block, and getGitRevision() at line 1045 — which now has no preceding /** — loses its documentation entirely.
Why nothing prevents it
TypeScript treats both as ordinary block comments and compiles fine; back-to-back JSDoc blocks are syntactically valid. There is no lint rule in this repo that flags orphaned JSDoc. The diff hunk header @@ -982,6 +1014,34 @@ confirms the insertion point sits exactly between the existing */ and function getGitRevision.
Step-by-step proof
- Before this PR, lines ~1009-1016 contain the JSDoc "Get the current git revision (HEAD sha)... rev-parse handles for free." immediately followed by
function getGitRevision(cwd: string): string {. - The PR's diff inserts 28 new lines (the
majorOfJSDoc + body, then thereadRustToolchainChannelJSDoc + body) starting at the line after the closing*/of that JSDoc. - After insertion: line 1016 is
*/, line 1017 is/**(start ofmajorOf's doc). The git-revision JSDoc has no following declaration to attach to. function getGitRevisionis pushed to line 1045 with no/**block immediately preceding it.- Hovering
getGitRevisionin an editor or running typedoc would show no description; the carefully-written rationale about whygit rev-parseis used (vs. reading.git/HEAD) is effectively dead documentation.
Impact
Zero runtime impact — this is purely a documentation/code-organization defect. The git-revision JSDoc contains non-obvious rationale ("correctness matters more than the ~20ms spawn", packed-refs/worktrees edge cases) that a future maintainer would want to see when hovering getGitRevision, and it's now detached from that function. It is, however, clearly an editing slip introduced by this PR rather than a behavioral bug.
How to fix
Move the two new helpers (lines 1017-1043, including their JSDoc blocks) to either:
- above the
getGitRevisionJSDoc, i.e. before line 1009 (afterfindRepoRoot's closing brace at line 1007), or - below
getGitRevision()'s closing brace at line 1056.
Either restores the /** Get the current git revision... */ block to immediately precede function getGitRevision(cwd: string): string {.
| 2. `resolveConfig(partial, toolchain)` — produce the flat `Config`. Detect host, derive all target booleans, compute paths, read package.json version + git sha. | ||
| 3. `validateBunConfig(cfg)` + `checkWorkarounds(cfg)` — fail early with clear errors. | ||
| - `generateCargoConfig(cfg)` — write the repo-root `.cargo/config.toml` (git-ignored) with the per-target `linker = ` from the discovered `cfg.cxx`. Advisory only for `bun bd` (the ninja cargo edge sets the linker via env); it's there for `cargo build`/`cargo check`/rust-analyzer run directly. | ||
| 4. `globAllSources()` — one filesystem snapshot of all `.cpp`/`.c`/`.zig`/codegen-input globs. |
There was a problem hiding this comment.
🟡 This line lists globAllSources() as snapshotting .cpp/.c/.zig/codegen-input globs, but this PR added the rust glob group (scripts/glob-sources.ts:81-83, src/**/*.rs + Cargo manifests) and updated the surrounding lines (142, 145, etc.) for the rust migration while missing this one. Since .zig is still globbed (kept as the porting spec), the accurate text is .cpp/.c/.zig/.rs/codegen-input.
Extended reasoning...
What the bug is
scripts/build/CLAUDE.md:146 documents Phase 1 step 4 of the configure flow:
globAllSources()— one filesystem snapshot of all.cpp/.c/.zig/codegen-input globs.
This PR added a new rust glob group to globAllSources() (scripts/glob-sources.ts:81-83), which collects src/**/*.rs, src/**/Cargo.toml, Cargo.toml, Cargo.lock, and rust-toolchain.toml as implicit inputs to the cargo build edge. The doc line was not updated to mention .rs, so it now under-describes what the function does.
How it manifests / step-by-step proof
- Before this PR,
globAllSources()had glob groups for cpp/c/zig/codegen-input (and a few internal ones), andscripts/build/CLAUDE.md:146accurately summarized that as ".cpp/.c/.zig/codegen-input". - This PR adds the
rust:group atscripts/glob-sources.ts:81-83withpaths: ["src/**/*.rs", "src/**/Cargo.toml", "Cargo.toml", "Cargo.lock", "rust-toolchain.toml"]. - This PR also extensively updates
scripts/build/CLAUDE.mdfor the zig→rust migration: line 142 addscargoto theresolveToolchain()list, line 145 adds thegenerateCargoConfig(cfg)step, the module-inventory table swapszig.ts→rust.ts+cargo-config.ts,emitZig→emitRust,bun-zig.o→libbun_rust.a, the split CI modeszig-only→rust-only, etc. - Line 146, sandwiched between two of those edits, was missed in the sweep. It still lists only
.cpp/.c/.zig/codegen-input. - Note the
zig:group atglob-sources.ts:73-75(src/**/*.zig) is still present — the .zig files remain in-tree as the porting spec — so the existing.zigmention is not strictly wrong; the line is incomplete by omitting.rs, which is now the primary build input feeding the cargo step.
Why nothing prevents it
This is prose in a developer-facing CLAUDE.md; nothing type-checks it against the implementation. The PR's own doc-sync sweep over this exact file (lines 142, 145, 157, 167, the entire module table) demonstrates the intent to keep it accurate; this line was simply overlooked because the surrounding hunk boundary fell between the lines that were edited.
Impact
Documentation drift only, no functional effect. scripts/build/CLAUDE.md is the developer-facing spec for the build system, and globAllSources() is the function that determines which source edits invalidate which ninja edges, so an inaccurate description here is mildly misleading for anyone reading the build-system overview — but no more so than the analogous already-posted nits on root CLAUDE.md (line 268) and codegen.ts JSDoc (lines 212/215), which are the same class of "PR's own zig→rust doc-sync sweep missed a line".
How to fix
Change line 146 to:
globAllSources()— one filesystem snapshot of all.cpp/.c/.zig/.rs/codegen-input globs.
(Listing both .zig and .rs is the most accurate since both groups exist; if the intent is to list only build-relevant inputs, .cpp/.c/.rs/codegen-input would also be defensible since .zig is no longer fed to a build edge — but keeping .zig matches the actual glob-group set.)
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
scripts/build/bun.ts (1)
461-474:⚠️ Potential issue | 🟠 Major | ⚡ Quick winWrap
libbun_rustwithrustLinkFlags()in both link paths.Right now both full and link-only treat the Rust staticlib as a plain archive input. That lets the linker drop members with no inbound static reference, which is exactly the case
rustLinkFlags()was added for inscripts/build/rust.ts.Suggested fix
-import { emitRust, rustLibPath } from "./rust.ts"; +import { emitRust, rustLibPath, rustLinkFlags } from "./rust.ts";const exe = link(n, cfg, exeName, [...allObjects, ...rustObjects, ...windowsRes], { libs: depLibs, - flags: [...flags.ldflags, ...systemLibs(cfg), ...manifestLinkFlags(cfg), ...shims.ldflags], + flags: [...flags.ldflags, ...rustLinkFlags(cfg, rustObjects), ...systemLibs(cfg), ...manifestLinkFlags(cfg), ...shims.ldflags], implicitInputs: [...linkImplicitInputs(cfg), ...shims.implicitInputs], });const exe = link(n, cfg, exeName, [archive, ...rustObjects, ...windowsRes], { libs: depLibs, - flags: [...flags.ldflags, ...systemLibs(cfg), ...manifestLinkFlags(cfg), ...shims.ldflags], + flags: [...flags.ldflags, ...rustLinkFlags(cfg, rustObjects), ...systemLibs(cfg), ...manifestLinkFlags(cfg), ...shims.ldflags], implicitInputs: [...linkImplicitInputs(cfg), ...shims.implicitInputs], });Also applies to: 609-613
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/build/bun.ts` around lines 461 - 474, The Rust staticlib (libbun_rust) is currently passed as a plain archive to link(), allowing the linker to drop unused members; call rustLinkFlags(cfg) and include its ldflags and implicitInputs in both places where link(...) is invoked (the full-exe link that builds exeName with [...allObjects, ...rustObjects, ...windowsRes] and the link-only path mentioned later) so the Rust archive is wrapped correctly; update the flags and implicitInputs arrays to merge ...rustLinkFlags(cfg).ldflags and ...rustLinkFlags(cfg).implicitInputs (or the single rustLinkFlags(cfg) result) alongside the existing flags/shims values when calling link().scripts/build/config.ts (1)
525-533:⚠️ Potential issue | 🟠 Major | ⚡ Quick winSkip NDK runtime patching in
rust-onlymode.Changing this path to warn-only still leaves
resolveConfig()callinglinkNdkRuntimesIntoClang()for every Android target, so--mode=rust-onlycan still try to mutate the host Clang resource dir and emit a sudo hint even though no C/C++ link step will run. Please gate the call at the caller oncemodeis known.Proposed fix
const buildType = partial.buildType ?? "Debug"; + const mode = partial.mode ?? "full"; const debug = buildType === "Debug"; const release = buildType === "Release" || buildType === "RelWithDebInfo" || buildType === "MinSizeRel"; const smol = buildType === "MinSizeRel"; @@ - linkNdkRuntimesIntoClang(toolchain.cc, androidNdk, host, crossTarget); + if (mode !== "rust-only") { + linkNdkRuntimesIntoClang(toolchain.cc, androidNdk, host, crossTarget); + } @@ - mode: partial.mode ?? "full", + mode,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/build/config.ts` around lines 525 - 533, resolveConfig() is still invoking linkNdkRuntimesIntoClang() for every Android target even when the build mode is "rust-only"; gate that call at the caller when the mode is known so we never attempt to mutate host Clang dirs or emit sudo hints in rust-only mode. Specifically, change the logic that calls resolveConfig()/linkNdkRuntimesIntoClang() so the caller checks config.mode (or the equivalent mode flag) and skips calling linkNdkRuntimesIntoClang() when mode === "rust-only" (or isRustOnlyMode()), ensuring linkNdkRuntimesIntoClang() is only invoked for modes that actually perform C/C++ linking.
♻️ Duplicate comments (6)
docs/SPAWN_SYS_PROPOSAL.md (1)
24-38:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd a language tag to the crate-graph code fence.
Line 24 uses an unlabeled fenced block, which will keep triggering markdownlint (MD040). Please label it (for example
text).Suggested fix
-``` +```text bun_libuv_sys (already exists, src/libuv_sys/) │ raw uv_* FFI: uv_spawn, uv_process_t, uv_pipe_t, uv_loop_t │ Windows-only at runtime; no bun deps. ▼ bun_spawn_sys (NEW, src/spawn_sys/) │ deps: libc, bun_sys (Fd/E/Tag/Result), bun_libuv_sys (cfg(windows)) │ NO bun_event_loop / bun_aio / bun_io / bun_threading. ▼ bun_spawn (src/spawn/, slimmed) │ deps: bun_spawn_sys, bun_aio, bun_event_loop, bun_io, bun_ptr, │ bun_threading, bun_crash_handler ▼ bun_runtime (re-exports api::bun_process from bun_spawn)</details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.In
@docs/SPAWN_SYS_PROPOSAL.mdaround lines 24 - 38, The unlabeled fenced code
block that begins with "bun_libuv_sys (already exists, src/libuv_sys/)" should
be updated to include a language tag (e.g., text) to satisfy markdownlint MD040;
edit the fenced block in SPAWN_SYS_PROPOSAL.md that contains the crate-graph
ASCII diagram and change the opening triple backticks to include the tag (for
example, ```text) so the block is recognized as labeled code.</details> </blockquote></details> <details> <summary>docs/rust-migration-tree.md (1)</summary><blockquote> `59-63`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_ **Add a language identifier to the fenced block.** This code fence is still unlabeled; use something like ```text to satisfy MD040. <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.In
@docs/rust-migration-tree.mdaround lines 59 - 63, The fenced code block
showing the file mapping entries (e.g., "src/http/HTTPThread.zig ←→
crates/bun_http/src/thread.rs", "src/runtime/api/glob.zig ←→
crates/bun_runtime/src/api/glob.rs", "src/jsc/JSRef.zig ←→
crates/bun_jsc/src/jsref.rs") is missing a language identifier; update the
opening fence to include a language token such astext (ordiff if you
prefer highlighting) so the block is labeled and satisfies MD040.</details> </blockquote></details> <details> <summary>scripts/b2-cycle-args.ts (1)</summary><blockquote> `92-97`: _⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ **Resolve the src directory once to an absolute path.** This script depends on the current working directory being the repository root. The calls to `readdirSync("src")`, `statSync(join("src", d))`, and `gateCount(join("src", d))` will fail or scan the wrong directory tree if invoked from elsewhere. This issue has already been flagged in a previous review with a detailed fix suggestion. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/b2-cycle-args.ts` around lines 92 - 97, The loop uses relative "src" paths which break when cwd isn't repo root; resolve the src directory once to an absolute path (e.g., const SRC = path.resolve(process.cwd(), "src") or similar) and then replace all literal "src" uses in readdirSync, statSync(join("src", d)), and gateCount(join("src", d)) with SRC and join(SRC, d); keep existing checks (KEYSTONE.has(d), gateCount result, crates.push with tier(d)) unchanged. ``` </details> </blockquote></details> <details> <summary>scripts/build/bun.ts (1)</summary><blockquote> `877-883`: _⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ **Treat absolute `relative()` results as escaping on Windows.** When `realBuild` resolves to another drive, `path.relative(realCwd, realBuild)` comes back as an absolute path like `D:\...`, so the current `startsWith("..")` check misses the cross-volume symlink case this guard is meant to reject. <details> <summary>Suggested fix</summary> ```diff -import { relative, resolve, sep } from "node:path"; +import { isAbsolute, relative, resolve, sep } from "node:path"; ``` ```diff const realCwd = realpathSync(cfg.cwd); const realBuild = realpathSync(cfg.buildDir); const rel = relative(realCwd, realBuild); - const escapes = rel.startsWith("..") || rel === ""; + const escapes = isAbsolute(rel) || rel.startsWith("..") || rel === ""; ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/build/bun.ts` around lines 877 - 883, The check that sets escapes based on rel (computed via path.relative(realCwd, realBuild)) fails on Windows when rel is an absolute path (cross-drive) — update the escapes condition in scripts/build/bun.ts to treat absolute relative() results as escaping by including a check for path.isAbsolute(rel) (i.e., set escapes = rel.startsWith("..") || rel === "" || path.isAbsolute(rel)); reference the variables/funcs realpathSync, relative, rel and the escapes assignment when making the change. ``` </details> </blockquote></details> <details> <summary>scripts/build/rust.ts (1)</summary><blockquote> `106-117`: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_ **Keep `allRustTargets` aligned with the supported triples.** This list still omits `x86_64-linux-android` and `aarch64-unknown-freebsd`, so `rust:check-all` skips paths the rest of the build logic already models. <details> <summary>Suggested fix</summary> ```diff export const allRustTargets = [ "x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "aarch64-unknown-linux-musl", + "x86_64-linux-android", "x86_64-apple-darwin", "aarch64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc", "x86_64-unknown-freebsd", + "aarch64-unknown-freebsd", "aarch64-linux-android", ] as const; ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/build/rust.ts` around lines 106 - 117, The allRustTargets array is missing two supported triples so rust:check-all skips some paths; update the exported constant allRustTargets to include "x86_64-linux-android" and "aarch64-unknown-freebsd" alongside the existing entries (preserve the const tuple and ordering consistent with other build logic) so the list matches the rest of the build's supported triples. ``` </details> </blockquote></details> <details> <summary>scripts/build/tools.ts (1)</summary><blockquote> `604-615`: _⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ **Select the rust-lld wrapper from `hostTriple`, not `os`.** `lib/rustlib/<host>/bin` is keyed by rustc's host triple, but this branch still picks the wrapper name from the requested `os`. When those differ, `findRustLld()` looks for the wrong binary and silently returns `undefined`. <details> <summary>Suggested fix</summary> ```diff const candidate = - os === "windows" + hostTriple.includes("windows") ? join(bin, "rust-lld.exe") - : os === "darwin" + : hostTriple.includes("darwin") ? join(bin, "gcc-ld", "ld64.lld") : join(bin, "gcc-ld", "ld.lld"); ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/build/tools.ts` around lines 604 - 615, The code chooses the rust-lld wrapper based on the requested os instead of the actual host triple, causing wrong candidate paths; update the selection logic to derive the platform from hostTriple (e.g., test hostTriple for "windows", "darwin"/"apple" or default Linux-like) when constructing candidate in the function that computes rustLld (the code around hostTriple, candidate, isExecutable and findRustLld). Replace uses of the external os variable with a hostTriple-based check (string contains/regex for "windows" and "darwin"/"apple") so the correct wrapper names ("rust-lld.exe", "gcc-ld/ld64.lld", "gcc-ld/ld.lld") are picked from lib/rustlib/<host>/bin and then test with isExecutable as before. ``` </details> </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary>Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.Inline comments:
In@docs/CYCLEBREAK.md:
- Around line 4-6: Update the wording around the “NEVER run git” rule to
explicitly exempt the human maintainer step that performs Tier-6 collapse
operations: change the sentence that currently reads “NEVER run git” to
something like “Agents must never run git; the human maintainer performs any
required git operations (e.g., git mv) during Tier-6 collapse).” Apply this
clarification where the phrase “NEVER run git” appears and in the section
describing Tier-6 collapse mechanics (references: the literal strings “NEVER run
git”, “git mv”, and “Tier-6 collapse”) so it’s clear “git mv” is a human
maintainer step, not an agent action.- Around line 55-68: The opening sentence incorrectly says "These five
union(enum)sites are per-tick hot" while the table lists six entries
(bun_event_loop::Task, bun_event_loop::ConcurrentTask, bun_aio::FilePoll,
bun_event_loop::EventLoopTimer, bun_io::Source, bun_threading::WorkPoolTask);
update that sentence to "These sixunion(enum)sites are per-tick hot" (or
remove one table row if you intend five) so the count matches the table and the
scope is unambiguous.In
@docs/RUST_IDIOMS_AUDIT.md:
- Around line 3-4: Update the intro sentence that claims "two reviewers
verified" by adding a concise "Method/Artifacts" pointer showing where the
verification evidence lives (for example a CI workflow run link, the generated
audit report location, or specific commit/PR IDs); edit the paragraph containing
the phrase "two reviewers confirmed the finding" to append a short parenthetical
or footnote reference like "Method/Artifacts: <workflow/report/commit refs>" so
readers can reproduce the votes and see the verification artifacts.In
@docs/RUST_PATTERNS.md:
- Around line 3-4: The docs/RUST_PATTERNS.md header currently contains a
machine-local path string "/root/bun-5"; remove that environment-specific path
and replace it with a repo-relative reference or no path at all (e.g., "Repo: /
(branch claude/phase-a-port)" or simply omit the path), updating the header line
that embeds "/root/bun-5" so the document does not contain machine-local
information.- Around line 23-24: The text incorrectly attributes the stability of the
deref-and-field form(*ptr).fieldto Rust 1.82; update docs/RUST_PATTERNS.md
to either remove the version annotation or explicitly distinguish the two
features: keep(*ptr).field = xas the generic example (no MSRV claim) and
correct the 1.82 note to refer specifically to the raw-pointer operator syntax
&raw const (*ptr).field/&raw mut (*ptr).field(RFC 2582) instead of saying
(*ptr).fieldwas stabilized in 1.82; also update the comparison line that
shows(&mut *ptr).field = xto clarify that it’s older syntax and not the 1.82
addition.In
@scripts/build/config.ts:
- Around line 1046-1050: The function readRustToolchainChannel currently returns
undefined whether the rust-toolchain.toml file is missing or
present-but-unparsable; change it so that when the file exists (existsSync(path)
is true) but the regex match (m) fails, the function throws a clear error
instead of returning undefined, so callers can distinguish "file missing" from
"file present but no channel parsed"; update readRustToolchainChannel to read
the file (readFileSync(path, "utf8")), attempt the
/^\schannel\s=\s*"([^"]+)"/m match, return m[1] on success, and throw an error
mentioning the file and that the channel could not be parsed when m is falsy
(refer to readRustToolchainChannel, resolve, existsSync, readFileSync, and the
regex match).In
@scripts/fix-imports.ts:
- Line 14: Replace the shell-quoted execSync calls (used to populate the diff
and the other git result) with execFileSync from child_process, calling
execFileSync("git", [...args...], { encoding: "utf8", cwd }) instead of
execSync("git ..."); specifically, import execFileSync, then change the call
that sets the diff variable (currently using execSync with a single string
containing "git diff --cached --name-status -M90% -- '.zig'") to call
execFileSync with args ["diff","--cached","--name-status","-M90%","--",".zig"]
and include encoding and cwd options, and do the same for the other execSync
call at the other location so both use argument arrays and an explicit cwd.In
@scripts/fix-vm-import.ts:
- Around line 26-30: The current hasImport logic misclassifies grouped imports
because the exclusion regex is applied without first isolating the matched
import and ensuring plain "VirtualMachine" is absent; change the code to first
capture the matched import string (the result of body.match(/use
[^;]\bVirtualMachine\b[^;];/s)?.[0] ?? ""), then set hasImport by testing that
this import string contains a plain \bVirtualMachine\b and does NOT only contain
excluded variants — i.e. use something like importStmt = ...; hasImport =
/\bVirtualMachine\b/.test(importStmt) &&
!/\bVirtualMachine(?:Ref|SqlExt|InitOptions)\b/.test(importStmt) (or equivalent
logic) so grouped imports like ::{VirtualMachine, VirtualMachineRef} are
recognized as having the plain VirtualMachine.In
@scripts/migrate-hostfn-vm.ts:
- Around line 111-118: The grouping currently collapses calls by method name
into byName, ignoring the owning typePath and allowing rewrites of unrelated
impls; change the grouping so it includes the owner type (use the collected
typePath when building the key) instead of just the method name—e.g., key by
${typePath}::${name}or maintain a nested Map keyed first by typePath then
name—so that later rewrites (the logic that inspects calls, Shape, and
JSGlobalObject parameters) only apply when both the name and owning type match;
apply the same change to the other similar block that also collapses by name
(the block referenced in the comment).In
@src/aio/posix_event_loop.rs:
- Around line 1713-1718: In wake(), casting a usize to [u8; 8] is UB on 32-bit
platforms; change the written value to a u64 and serialize it to an 8-byte
buffer (e.g., via to_ne_bytes()) before calling bun_sys::write(self.fd, ...).
Update the wake() implementation (referencing wake, self.fd, and bun_sys::write)
to create a u64 constant (1u64), convert it to an [u8; 8], and pass that buffer
to bun_sys::write so the code always writes exactly 8 bytes safely across
platforms.- Around line 209-212: The disable() method on KeepAlive currently calls
unref(get_vm_ctx(AllocatorType::Js)) which hardcodes AllocatorType::Js and can
be wrong for Mini event loops; update KeepAlive so disable knows the correct
allocator: either add a field to KeepAlive to store the AllocatorType (like
FilePoll does) and use that stored allocator when calling get_vm_ctx in
disable(), or change disable(&mut self) to disable(&mut self, ctx: EventLoopCtx)
(or similar) and call unref(ctx) with the passed-in EventLoopCtx; ensure
references to KeepAlive::disable, KeepAlive struct, unref and get_vm_ctx are
updated accordingly.
Outside diff comments:
In@scripts/build/bun.ts:
- Around line 461-474: The Rust staticlib (libbun_rust) is currently passed as a
plain archive to link(), allowing the linker to drop unused members; call
rustLinkFlags(cfg) and include its ldflags and implicitInputs in both places
where link(...) is invoked (the full-exe link that builds exeName with
[...allObjects, ...rustObjects, ...windowsRes] and the link-only path mentioned
later) so the Rust archive is wrapped correctly; update the flags and
implicitInputs arrays to merge ...rustLinkFlags(cfg).ldflags and
...rustLinkFlags(cfg).implicitInputs (or the single rustLinkFlags(cfg) result)
alongside the existing flags/shims values when calling link().In
@scripts/build/config.ts:
- Around line 525-533: resolveConfig() is still invoking
linkNdkRuntimesIntoClang() for every Android target even when the build mode is
"rust-only"; gate that call at the caller when the mode is known so we never
attempt to mutate host Clang dirs or emit sudo hints in rust-only mode.
Specifically, change the logic that calls
resolveConfig()/linkNdkRuntimesIntoClang() so the caller checks config.mode (or
the equivalent mode flag) and skips calling linkNdkRuntimesIntoClang() when mode
=== "rust-only" (or isRustOnlyMode()), ensuring linkNdkRuntimesIntoClang() is
only invoked for modes that actually perform C/C++ linking.
Duplicate comments:
In@docs/rust-migration-tree.md:
- Around line 59-63: The fenced code block showing the file mapping entries
(e.g., "src/http/HTTPThread.zig ←→ crates/bun_http/src/thread.rs",
"src/runtime/api/glob.zig ←→ crates/bun_runtime/src/api/glob.rs",
"src/jsc/JSRef.zig ←→ crates/bun_jsc/src/jsref.rs") is missing a language
identifier; update the opening fence to include a language token such astext (ordiff if you prefer highlighting) so the block is labeled and satisfies
MD040.In
@docs/SPAWN_SYS_PROPOSAL.md:
- Around line 24-38: The unlabeled fenced code block that begins with
"bun_libuv_sys (already exists, src/libuv_sys/)" should be updated to include a
language tag (e.g., text) to satisfy markdownlint MD040; edit the fenced block
in SPAWN_SYS_PROPOSAL.md that contains the crate-graph ASCII diagram and change
the opening triple backticks to include the tag (for example, ```text) so the
block is recognized as labeled code.In
@scripts/b2-cycle-args.ts:
- Around line 92-97: The loop uses relative "src" paths which break when cwd
isn't repo root; resolve the src directory once to an absolute path (e.g., const
SRC = path.resolve(process.cwd(), "src") or similar) and then replace all
literal "src" uses in readdirSync, statSync(join("src", d)), and
gateCount(join("src", d)) with SRC and join(SRC, d); keep existing checks
(KEYSTONE.has(d), gateCount result, crates.push with tier(d)) unchanged.In
@scripts/build/bun.ts:
- Around line 877-883: The check that sets escapes based on rel (computed via
path.relative(realCwd, realBuild)) fails on Windows when rel is an absolute path
(cross-drive) — update the escapes condition in scripts/build/bun.ts to treat
absolute relative() results as escaping by including a check for
path.isAbsolute(rel) (i.e., set escapes = rel.startsWith("..") || rel === "" ||
path.isAbsolute(rel)); reference the variables/funcs realpathSync, relative, rel
and the escapes assignment when making the change.In
@scripts/build/rust.ts:
- Around line 106-117: The allRustTargets array is missing two supported triples
so rust:check-all skips some paths; update the exported constant allRustTargets
to include "x86_64-linux-android" and "aarch64-unknown-freebsd" alongside the
existing entries (preserve the const tuple and ordering consistent with other
build logic) so the list matches the rest of the build's supported triples.In
@scripts/build/tools.ts:
- Around line 604-615: The code chooses the rust-lld wrapper based on the
requested os instead of the actual host triple, causing wrong candidate paths;
update the selection logic to derive the platform from hostTriple (e.g., test
hostTriple for "windows", "darwin"/"apple" or default Linux-like) when
constructing candidate in the function that computes rustLld (the code around
hostTriple, candidate, isExecutable and findRustLld). Replace uses of the
external os variable with a hostTriple-based check (string contains/regex for
"windows" and "darwin"/"apple") so the correct wrapper names ("rust-lld.exe",
"gcc-ld/ld64.lld", "gcc-ld/ld.lld") are picked from lib/rustlib//bin and
then test with isExecutable as before.</details> <details> <summary>🪄 Autofix (Beta)</summary> Fix all unresolved CodeRabbit comments on this PR: - [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended) - [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes </details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: Path: .coderabbit.yaml **Review profile**: ASSERTIVE **Plan**: Pro **Run ID**: `c3b606e1-2e54-474f-89a7-e4d8f468881e` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between c272a7740f59d48513e7499f15a95c50cfa4d3ed and b8c43118810c52de59ec9843253d285c3102a283. </details> <details> <summary>⛔ Files ignored due to path filters (1)</summary> * `Cargo.lock` is excluded by `!**/*.lock` </details> <details> <summary>📒 Files selected for processing (52)</summary> * `.prettierignore` * `Cargo.toml` * `docs/.rust-rewrite-verified-claims.md` * `docs/BABYLIST_REPLACEMENT.md` * `docs/CI_RUST_MIGRATION.md` * `docs/CYCLEBREAK.md` * `docs/LAYERING_AUDIT.md` * `docs/PORTING.md` * `docs/RUST_IDIOMS_AUDIT.md` * `docs/RUST_PATTERNS.md` * `docs/SPAWN_SYS_PROPOSAL.md` * `docs/rust-migration-tree.md` * `docs/rust-rewrite-plan.md` * `docs/zig-restructure-plan.md` * `rust-toolchain.toml` * `scripts/b2-cycle-args.ts` * `scripts/build/CLAUDE.md` * `scripts/build/bun.ts` * `scripts/build/config.ts` * `scripts/build/rust.ts` * `scripts/build/source.ts` * `scripts/build/tools.ts` * `scripts/fix-imports.ts` * `scripts/fix-vm-import.ts` * `scripts/migrate-hostfn-vm.ts` * `scripts/patch-genclasses-paths.ts` * `src/aio/posix_event_loop.rs` * `src/base64/lib.rs` * `src/bun_bin/Cargo.toml` * `src/bun_bin/lib.rs` * `src/bun_bin/phase_c_exports.rs` * `src/bun_core/Cargo.toml` * `src/bun_core/env.rs` * `src/bun_core/heap.rs` * `src/bun_core/lib.rs` * `src/bun_core/result.rs` * `src/bun_core/util.rs` * `src/bundler/Cargo.toml` * `src/bundler/OutputFile.rs` * `src/bundler/ParseTask.rs` * `src/bundler/bundle_v2.rs` * `src/bundler/linker_context/findImportedFilesInCSSOrder.rs` * `src/bundler/linker_context/generateCodeForLazyExport.rs` * `src/bundler/linker_context/generateCompileResultForCssChunk.rs` * `src/bundler/linker_context/prepareCssAstsForChunk.rs` * `src/bundler/linker_context/scanImportsAndExports.rs` * `src/bundler/transpiler.rs` * `src/bundler/ungate_support.rs` * `src/bundler_jsc/Cargo.toml` * `src/bunfig/Cargo.toml` * `src/codegen/generate-classes.ts` * `src/crash_handler/lib.rs` </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
| Agents read **only their crate's section** below. Edit only `src/<your-crate>/`. | ||
| NEVER run git. NEVER edit another crate. | ||
|
|
There was a problem hiding this comment.
Clarify who is allowed to run git mv.
The doc currently states “NEVER run git” but also prescribes git mv for Tier-6 collapse mechanics. Please explicitly scope this (e.g., “human maintainer step” vs “agent step”) to prevent workflow mistakes.
Also applies to: 26-30
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/CYCLEBREAK.md` around lines 4 - 6, Update the wording around the “NEVER
run git” rule to explicitly exempt the human maintainer step that performs
Tier-6 collapse operations: change the sentence that currently reads “NEVER run
git” to something like “Agents must never run git; the human maintainer performs
any required git operations (e.g., git mv) during Tier-6 collapse).” Apply this
clarification where the phrase “NEVER run git” appears and in the section
describing Tier-6 collapse mechanics (references: the literal strings “NEVER run
git”, “git mv”, and “Tier-6 collapse”) so it’s clear “git mv” is a human
maintainer step, not an agent action.
| These five `union(enum)` sites are per-tick hot. Use the **hoisted-match** | ||
| pattern (PORTING.md §Dispatch): low tier stores `(tag: u8, ptr: *mut ())`, | ||
| `bun_runtime` owns the `match`. Tag constants live in the low tier as bare | ||
| `pub const TAG_X: u8`; `runtime` defines what each maps to. | ||
|
|
||
| | low-tier owner | Zig type | runtime dispatch fn | tag module | | ||
| | -------------------------------- | ------------------------------------ | -------------------------------------------- | ------------------------------------- | | ||
| | `bun_event_loop::Task` | `Task = union(Tag)` | `runtime::dispatch::run_task(Task)` | `event_loop::task_tag` (~70 variants) | | ||
| | `bun_event_loop::ConcurrentTask` | `ConcurrentTask` | `runtime::dispatch::run_concurrent(..)` | shares `task_tag` | | ||
| | `bun_aio::FilePoll` | `Owner = TaggedPointerUnion(.{...})` | `runtime::dispatch::on_poll(tag, ptr, ev)` | `aio::poll_tag` (~13) | | ||
| | `bun_event_loop::EventLoopTimer` | `Tag = enum` | `runtime::dispatch::fire_timer(tag, ptr)` | `event_loop::timer_tag` (~20) | | ||
| | `bun_io::Source` | `Source = union(enum)` | `runtime::dispatch::on_source(tag, ptr, ev)` | `io::source_tag` (~6) | | ||
| | `bun_threading::WorkPoolTask` | `WorkPool.Task` | `runtime::dispatch::run_work(tag, ptr)` | `threading::work_tag` (~8) | | ||
|
|
There was a problem hiding this comment.
Fix the hot-site count mismatch.
Line 55 says “five union(enum) sites,” but the table currently lists six rows. Please align the sentence with the table to avoid ambiguous implementation scope.
🧰 Tools
🪛 LanguageTool
[grammar] ~56-~56: Use a hyphen to join words.
Context: ...ch** pattern (PORTING.md §Dispatch): low tier stores (tag: u8, ptr: *mut ()), `...
(QB_NEW_EN_HYPHEN)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/CYCLEBREAK.md` around lines 55 - 68, The opening sentence incorrectly
says "These five `union(enum)` sites are per-tick hot" while the table lists six
entries (bun_event_loop::Task, bun_event_loop::ConcurrentTask,
bun_aio::FilePoll, bun_event_loop::EventLoopTimer, bun_io::Source,
bun_threading::WorkPoolTask); update that sentence to "These six `union(enum)`
sites are per-tick hot" (or remove one table row if you intend five) so the
count matches the table and the scope is unambiguous.
| Adversarial review of 30 crates. Each candidate pattern was independently verified by two reviewers reading the cited code in context; only patterns where **both** reviewers confirmed the finding as a genuine, sweep-worthy anti-pattern appear in §1. Patterns where either reviewer demonstrated the finding was context-blind, factually wrong, or already-policy are listed in §2 with the refutation. Ranked by (severity × instances). | ||
|
|
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Add an evidence pointer for the “two reviewers verified” claim.
Line 3 makes a strong process assertion, but the intro doesn’t link to where those two votes/artifacts live. Add a short “Method/Artifacts” pointer (workflow run, generated report location, or commit references) so readers can reproduce and trust the ranking.
Based on learnings: "Never overstate what you got done or what actually works in commits, PRs, or messages to users."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/RUST_IDIOMS_AUDIT.md` around lines 3 - 4, Update the intro sentence that
claims "two reviewers verified" by adding a concise "Method/Artifacts" pointer
showing where the verification evidence lives (for example a CI workflow run
link, the generated audit report location, or specific commit/PR IDs); edit the
paragraph containing the phrase "two reviewers confirmed the finding" to append
a short parenthetical or footnote reference like "Method/Artifacts:
<workflow/report/commit refs>" so readers can reproduce the votes and see the
verification artifacts.
| Repo: `/root/bun-5` @ branch `claude/phase-a-port` (HEAD `9712c4542e2`) | ||
| Scope: 1,414 `.rs` files under `src/` (excludes `vendor/`, `test/`, `packages/`) |
There was a problem hiding this comment.
Remove machine-local pathing from the doc header.
Line 3 embeds /root/bun-5, which is environment-specific and quickly stale in repo docs. Prefer repo-relative wording (or omit local path entirely).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/RUST_PATTERNS.md` around lines 3 - 4, The docs/RUST_PATTERNS.md header
currently contains a machine-local path string "/root/bun-5"; remove that
environment-specific path and replace it with a repo-relative reference or no
path at all (e.g., "Repo: / (branch claude/phase-a-port)" or simply omit the
path), updating the header line that embeds "/root/bun-5" so the document does
not contain machine-local information.
| 1. **Short-lived, non-overlapping:** keep `*mut`, but only materialize `&mut` for a single statement; never hold across a call that could re-derive. Use `(*ptr).field = x` raw-place syntax (stable since 1.82) instead of `(&mut *ptr).field = x`. | ||
| 2. **Tree/graph backrefs (Scope, parent links):** wrap node payload in `UnsafeCell<T>` (or `Cell<T>` for Copy fields), store `NonNull<ScopeNode>`, expose `fn cell(&self) -> &UnsafeCell<Scope>`. The `UnsafeCell` is the _only_ legal way to get interior mutability through a shared path. |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== rust toolchain files =="
fd -HI 'rust-toolchain(\.toml)?$' .
echo
echo "== contents =="
for f in $(fd -HI 'rust-toolchain(\.toml)?$' .); do
echo "--- $f ---"
sed -n '1,160p' "$f"
done
echo
echo "== doc line with version claim =="
rg -n --fixed-strings 'stable since 1.82' docs/RUST_PATTERNS.md
echo
echo "== nearby PORTING/toolchain guidance (if present) =="
rg -n 'MSRV|rust-version|toolchain|minimum rust|Rust [0-9]+\.[0-9]+' docs src Cargo.toml Cargo.lock 2>/dev/null || trueRepository: oven-sh/bun
Length of output: 1863
🌐 Web query:
Rust 1.82 release stabilized raw-place syntax (*ptr).field
💡 Result:
Rust 1.82.0 stabilized the native raw-pointer borrow operators &raw const <place> and &raw mut <place> (RFC 2582) [1][2]. What that means for the “raw-place syntax (*ptr).field” part: - The <place> grammar is a “place expression” (e.g., a field projection like (*ptr).field) and Rust’s existing field-access syntax defines expr.field as a field expression (a kind of place expression) [3][4]. - Therefore you can write &raw const (*ptr).field / &raw mut (*ptr).field to directly produce a raw pointer to that field, without creating an intermediate reference (the key motivation of RFC 2582) [5][6][7]. Where it was announced/stabilized: - The Rust 1.82.0 release announcement lists “Native syntax for creating a raw pointer” / “Stabilized APIs” in the 1.82.0 highlights [2]. - The 1.82.0 release notes/changelog explicitly mention that &raw const and &raw mut operators were stabilized (RFC 2582) [8][2]. Example (shape): - Stable (>=1.82): let p: *const T = &raw const (*ptr).field; / let p: *mut T = &raw mut (*ptr).field; [5][7].
Citations:
- 1: https://github.com/rust-lang/rust/releases/tag/1.82.0
- 2: https://blog.rust-lang.org/2024/10/17/Rust-1.82.0/
- 3: https://doc.rust-lang.org/1.87.0/reference/expressions/field-expr.html
- 4: https://www.ralfj.de/blog/2024/08/14/places.html
- 5: https://github.com/rust-lang/rfcs/blob/master/text/2582-raw-reference-mir-operator.md
- 6: RFC for an operator to take a raw reference rust-lang/rfcs#2582
- 7: https://rust-lang.github.io/rfcs/2582-raw-reference-mir-operator.html
- 8: https://releases.rs/docs/1.82.0/
🌐 Web query:
bun project minimum supported Rust version MSRV
💡 Result:
The Bun project (oven-sh/bun) primarily uses Zig as its implementation language, with minimal Rust components such as the native plugin API (bun-native-plugin crate) [1][2][3][4]. There is no documented Minimum Supported Rust Version (MSRV) policy in official documentation, README, or Cargo.toml files [5][6][7][4]. The project's rust-toolchain.toml specifies a pinned nightly channel ("nightly-2025-12-10"), which overrides any MSRV and requires contributors to use that exact toolchain for Rust development [2]. An experimental Rust port branch exists as of May 2026, but it is not committed to main and remains unmerged [8][9]. For the stable Bun project, no explicit MSRV is defined or required for end-users, as Rust is not central to its runtime or core functionality.
Citations:
- 1: https://bun.com/blog/release-notes/bun-v1.1.39
- 2: https://github.com/oven-sh/bun/blob/main/rust-toolchain.toml
- 3: https://github.com/oven-sh/bun/blob/1cc83768/packages/bun-native-plugin-rs/Cargo.lock
- 4: https://github.com/oven-sh/bun/blob/7abe6c38/packages/bun-native-plugin-rs/Cargo.toml
- 5: https://github.com/oven-sh/bun/blob/main/README.md
- 6: https://github.com/oven-sh/bun
- 7: https://bun.com/docs/installation
- 8: https://www.theregister.com/2026/05/05/bun_rust_port/?td=keepreading
- 9: https://www.reddit.com/r/rust/comments/1t4033y/buns_rewrite_it_in_rust_branch/
Clarify the Rust-version claim for raw-pointer syntax—the cited version is inaccurate.
Line 23 claims "(*ptr).field = x raw-place syntax (stable since 1.82)". However, the dereference-and-field syntax (*ptr).field has been stable for far longer than Rust 1.82. What did stabilize in 1.82 (RFC 2582) is the raw-pointer operator syntax &raw const (*ptr).field and &raw mut (*ptr).field—a different feature. Since the Bun project pins a nightly Rust toolchain and doesn't enforce an MSRV for its Rust code, either use a plain example without a version annotation, or explicitly distinguish between the old dereference syntax and the 1.82 raw-pointer operators.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/RUST_PATTERNS.md` around lines 23 - 24, The text incorrectly attributes
the stability of the deref-and-field form `(*ptr).field` to Rust 1.82; update
docs/RUST_PATTERNS.md to either remove the version annotation or explicitly
distinguish the two features: keep `(*ptr).field = x` as the generic example (no
MSRV claim) and correct the 1.82 note to refer specifically to the raw-pointer
operator syntax `&raw const (*ptr).field` / `&raw mut (*ptr).field` (RFC 2582)
instead of saying `(*ptr).field` was stabilized in 1.82; also update the
comparison line that shows `(&mut *ptr).field = x` to clarify that it’s older
syntax and not the 1.82 addition.
| const root = process.cwd(); | ||
|
|
||
| // 1. Build rename map from staged diff (R<score>\told\tnew) | ||
| const diff = execSync("git diff --cached --name-status -M90% -- '*.zig'", { encoding: "utf8" }); |
There was a problem hiding this comment.
Use argumentized git execution instead of shell-quoted command strings.
At Line 14 and Line 26, single-quoted globs inside execSync("...") are shell-dependent and can break on Windows. Use execFileSync("git", [...]) with explicit args and cwd.
Suggested fix
-import { execSync } from "node:child_process";
+import { execFileSync } from "node:child_process";
-const diff = execSync("git diff --cached --name-status -M90% -- '*.zig'", { encoding: "utf8" });
+const diff = execFileSync("git", ["diff", "--cached", "--name-status", "-M90%", "--", "*.zig"], {
+ encoding: "utf8",
+ cwd: repoRoot,
+});
-const all = execSync("git ls-files -- 'src/**/*.zig' 'src/*.zig'", { encoding: "utf8" })
+const all = execFileSync("git", ["ls-files", "--", "src/**/*.zig", "src/*.zig"], {
+ encoding: "utf8",
+ cwd: repoRoot,
+})
.trim()
.split("\n")
.filter(Boolean);Also applies to: 26-26
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/fix-imports.ts` at line 14, Replace the shell-quoted execSync calls
(used to populate the diff and the other git result) with execFileSync from
child_process, calling execFileSync("git", [...args...], { encoding: "utf8", cwd
}) instead of execSync("git ..."); specifically, import execFileSync, then
change the call that sets the diff variable (currently using execSync with a
single string containing "git diff --cached --name-status -M90% -- '*.zig'") to
call execFileSync with args
["diff","--cached","--name-status","-M90%","--","*.zig"] and include encoding
and cwd options, and do the same for the other execSync call at the other
location so both use argument arrays and an explicit cwd.
| const hasImport = | ||
| /use [^;]*\bVirtualMachine\b[^;]*;/s.test(body) && | ||
| !/use [^;]*\bVirtualMachine(?:Ref|SqlExt|InitOptions)\b/s.test( | ||
| body.match(/use [^;]*\bVirtualMachine\b[^;]*;/s)?.[0] ?? "", | ||
| ); |
There was a problem hiding this comment.
Grouped imports are misclassified as “missing”.
hasImport flips to false for statements like use ...::{VirtualMachine, VirtualMachineRef}; because the exclusion regex is applied to the whole matched import. The cleanup then reinserts TARGET, creating a duplicate import instead of recognizing the existing one.
Suggested fix
- const hasImport =
- /use [^;]*\bVirtualMachine\b[^;]*;/s.test(body) &&
- !/use [^;]*\bVirtualMachine(?:Ref|SqlExt|InitOptions)\b/s.test(
- body.match(/use [^;]*\bVirtualMachine\b[^;]*;/s)?.[0] ?? "",
- );
+ const hasImport = /use [^;]*\bVirtualMachine\b[^;]*;/s.test(body);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/fix-vm-import.ts` around lines 26 - 30, The current hasImport logic
misclassifies grouped imports because the exclusion regex is applied without
first isolating the matched import and ensuring plain "VirtualMachine" is
absent; change the code to first capture the matched import string (the result
of body.match(/use [^;]*\bVirtualMachine\b[^;]*;/s)?.[0] ?? ""), then set
hasImport by testing that this import string contains a plain \bVirtualMachine\b
and does NOT only contain excluded variants — i.e. use something like importStmt
= ...; hasImport = /\bVirtualMachine\b/.test(importStmt) &&
!/\bVirtualMachine(?:Ref|SqlExt|InitOptions)\b/.test(importStmt) (or equivalent
logic) so grouped imports like ::{VirtualMachine, VirtualMachineRef} are
recognized as having the plain VirtualMachine.
| // Map method name → set of shapes (a method name may be shared across types | ||
| // with the same shape; if shapes conflict we apply per-occurrence heuristics). | ||
| const byName = new Map<string, Set<Shape>>(); | ||
| for (const [k, s] of calls) { | ||
| const name = k.split("::")[1]; | ||
| if (!byName.has(name)) byName.set(name, new Set()); | ||
| byName.get(name)!.add(s); | ||
| } |
There was a problem hiding this comment.
Constrain rewrites by owning type, not just method name.
typePath is collected but never used; after collapsing everything into byName, any unrelated fn <name> in the scanned trees with a compatible shape and JSGlobalObject parameter can be rewritten if some generated host-fn shares that name. That makes this migration capable of silently mutating the wrong impls.
Also applies to: 154-177
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/migrate-hostfn-vm.ts` around lines 111 - 118, The grouping currently
collapses calls by method name into byName, ignoring the owning typePath and
allowing rewrites of unrelated impls; change the grouping so it includes the
owner type (use the collected typePath when building the key) instead of just
the method name—e.g., key by `${typePath}::${name}` or maintain a nested Map
keyed first by typePath then name—so that later rewrites (the logic that
inspects calls, Shape, and JSGlobalObject parameters) only apply when both the
name and owning type match; apply the same change to the other similar block
that also collapses by name (the block referenced in the comment).
| pub fn wake(&self) { | ||
| let bytes: usize = 1; | ||
| // SAFETY: usize is 8 bytes; reinterpret as [u8; 8]. | ||
| let buf = unsafe { &*(&raw const bytes).cast::<[u8; 8]>() }; | ||
| let _ = bun_sys::write(self.fd, buf); | ||
| } |
There was a problem hiding this comment.
Potential UB on 32-bit platforms: usize cast to [u8; 8].
The wake() method casts a usize to [u8; 8], but usize is only 4 bytes on 32-bit platforms. This would read past the allocated storage (undefined behavior). eventfd always requires an 8-byte write.
🐛 Fix: Use u64 instead of usize
pub fn wake(&self) {
- let bytes: usize = 1;
- // SAFETY: usize is 8 bytes; reinterpret as [u8; 8].
- let buf = unsafe { &*(&raw const bytes).cast::<[u8; 8]>() };
+ let bytes: u64 = 1;
+ let buf = bytes.to_ne_bytes();
let _ = bun_sys::write(self.fd, buf);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| pub fn wake(&self) { | |
| let bytes: usize = 1; | |
| // SAFETY: usize is 8 bytes; reinterpret as [u8; 8]. | |
| let buf = unsafe { &*(&raw const bytes).cast::<[u8; 8]>() }; | |
| let _ = bun_sys::write(self.fd, buf); | |
| } | |
| pub fn wake(&self) { | |
| let bytes: u64 = 1; | |
| let buf = bytes.to_ne_bytes(); | |
| let _ = bun_sys::write(self.fd, buf); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/aio/posix_event_loop.rs` around lines 1713 - 1718, In wake(), casting a
usize to [u8; 8] is UB on 32-bit platforms; change the written value to a u64
and serialize it to an 8-byte buffer (e.g., via to_ne_bytes()) before calling
bun_sys::write(self.fd, ...). Update the wake() implementation (referencing
wake, self.fd, and bun_sys::write) to create a u64 constant (1u64), convert it
to an [u8; 8], and pass that buffer to bun_sys::write so the code always writes
exactly 8 bytes safely across platforms.
| @@ -1,8 +1,32 @@ | |||
| [toolchain] | |||
| channel = "nightly-2025-12-10" | |||
| channel = "nightly-2026-05-06" | |||
There was a problem hiding this comment.
🔴 Bumping channel to nightly-2026-05-06 makes CI red on pre-existing code this PR doesn't otherwise touch (see robobun build #52929, x64-asan build-rust): src/sys_jsc/error_jsc.rs:151,170 now hits the deny-by-default fn-item-to-integer cast lint (sentry as usize), and src/sys/lib.rs:1588-1592 triggers 14× unreachable_pub (the pub use libc::{statx, STATX_*} lives inside the private mod linux_statx). The bump needs to be paired with the lint fixes — sentry as extern "C" fn(c_int) as usize and pub(super) use respectively — or the lints allow-listed, before this can merge green.
Extended reasoning...
What the bug is
This PR changes rust-toolchain.toml line 2 from channel = "nightly-2025-12-10" to channel = "nightly-2026-05-06". The newer nightly tightens two lints that pre-existing code in src/sys_jsc/ and src/sys/ violates, and CI builds with -D warnings (or the relevant lints have been promoted to deny-by-default), so the build now fails. Neither of those source files is touched by this PR — they appear nowhere in the 300-file diff — so the toolchain bump is the proximate cause of the CI red.
The two failure classes
(1) src/sys_jsc/error_jsc.rs:151,170 — "direct cast of function item into an integer". Line 127 defines extern "C" fn sentry(_: c_int) {}. In Rust, the name sentry has a unique zero-sized function item type, not a function pointer. Line 151 does sa_sigaction: sentry as usize, and line 170 does sentry as usize as f64. Casting a fn item directly to an integer is now linted (rustc's function_item_references/clippy's fn_to_numeric_cast_* family — the newer nightly denies it under the build's lint config). The fix is to coerce to a fn pointer first: sentry as extern "C" fn(core::ffi::c_int) as usize. This is a one-token insertion at each site and changes nothing semantically — the resulting integer is the same address.
(2) src/sys/lib.rs:1588-1592 — 14× "unreachable pub item". The crate has #![warn(unreachable_pub)] at line 11. At line 1585, mod linux_statx is declared private (no pub), and inside it lines 1588-1592 do pub use libc::{statx, AT_STATX_SYNC_AS_STAT, STATX_ATIME, STATX_BLOCKS, STATX_BTIME, STATX_CTIME, STATX_GID, STATX_INO, STATX_MODE, STATX_MTIME, STATX_NLINK, STATX_SIZE, STATX_TYPE, STATX_UID}. A pub item inside a private module is unreachable from outside the crate — exactly what unreachable_pub flags. The 14 re-exported names produce 14 diagnostics. On the old nightly this was either not detected for re-exports or only warned; on the new nightly under CI's -D warnings it's a hard error. The fix is pub(super) use libc::{...} (or pub(crate)), which expresses the actual intent — these names are consumed by the parent module's statx() wrapper, not exported from the crate.
Why nothing prevents it
These files compile cleanly on nightly-2025-12-10. The PR author bumped the channel (likely to pick up a newer rustc for other reasons — the diff shows extensive build-system work) without doing a full cargo check --workspace on the new toolchain across all targets. The robobun CI comment on this PR's own head commit (e4db751) lists these exact file:line failures on "🐧 x64-asan - build-rust", confirming the bump is the trigger. The only commit after that (983412f) touches an unrelated test-output cleanup, so the failures are still present at HEAD.
Step-by-step proof
- PR diff at
rust-toolchain.toml:-channel = "nightly-2025-12-10"/+channel = "nightly-2026-05-06". grep '^diff --git' pr_diff.txt | grep -E 'sys/lib.rs|sys_jsc/error_jsc'→ no match. Neither file is in the PR.src/sys_jsc/error_jsc.rs:127definesextern "C" fn sentry(_: c_int) {}— a fn item, ZST.src/sys_jsc/error_jsc.rs:151→sa_sigaction: sentry as usize,;:170→sentry as usize as f64. Both cast the ZST fn item directly to an integer.src/sys/lib.rs:11→#![warn(unreachable_pub)].src/sys/lib.rs:1585→mod linux_statx {(nopub). Lines 1588-1592 →pub use libc::{statx, AT_STATX_SYNC_AS_STAT, STATX_*}— 14 names, eachpubbut unreachable.- robobun comment on this PR, build #52929: lists exactly
src/sys_jsc/error_jsc.rs:151+:170("direct cast of function item into an integer") andsrc/sys/lib.rs:1589-1591× 14 ("unreachablepubitem") on x64-asan build-rust.
Impact
CI is red on the x64-asan build-rust step, which gates the link step and therefore the test matrix. The PR cannot merge green without addressing this. The fixes are mechanical and risk-free (a fn-pointer coercion and a visibility narrowing), but they must be included in this PR since the toolchain bump is what surfaces them.
How to fix
In src/sys_jsc/error_jsc.rs:
// line 151
sa_sigaction: sentry as extern "C" fn(core::ffi::c_int) as usize,
// line 170
installed.put(global, b"handler", JSValue::js_number(sentry as extern "C" fn(core::ffi::c_int) as usize as f64));In src/sys/lib.rs:1588 (and the corresponding musl branch below it if it has the same shape):
pub(super) use libc::{
statx, AT_STATX_SYNC_AS_STAT, STATX_ATIME, /* … */
};Alternatively, if the intent is to land the toolchain bump separately from code fixes, add #[allow(unreachable_pub)] on the mod linux_statx block and #[allow(clippy::fn_to_numeric_cast_any)] (or the rustc equivalent) on the sentry casts — but the real fixes above are trivially small and strictly better.
| # Mirrors `allRustTargets` in scripts/build/rust.ts minus aarch64-unknown-freebsd | ||
| # (Tier 3 — no prebuilt rust-std, cargo `-Zbuild-std` builds it from rust-src). |
There was a problem hiding this comment.
🟡 The comment claims this list "Mirrors allRustTargets in scripts/build/rust.ts minus aarch64-unknown-freebsd", but the lists actually diverge: allRustTargets (rust.ts:106-117) has 10 entries and is missing x86_64-linux-android (and doesn't contain aarch64-unknown-freebsd either, so the "minus" subtracts nothing), while this file lists 11 targets including x86_64-linux-android. Since rustTarget() produces x86_64-linux-android and CI builds it (robobun shows "🐧 x64-android - build-bun"), allRustTargets is the incomplete one — bun run rust:check-all and the generated .cargo/config.toml (cargo-config.ts:79) skip that triple. Note: CodeRabbit flagged allRustTargets at rust.ts:117 earlier and it was auto-marked "✅ Addressed in commits d5945cf to 808905b", but those commits only touched docs/build.zig and never modified rust.ts — re-flagging here from the rust-toolchain.toml side since the comment text is also wrong.
Extended reasoning...
What the bug is
rust-toolchain.toml:13-14 carries a comment asserting that its targets list "Mirrors allRustTargets in scripts/build/rust.ts minus aarch64-unknown-freebsd (Tier 3 — no prebuilt rust-std, cargo -Zbuild-std builds it from rust-src)." This claim is false on both halves: (a) aarch64-unknown-freebsd is not in allRustTargets to begin with, so subtracting it is vacuous; and (b) the targets list in rust-toolchain.toml contains x86_64-linux-android (line 26) which allRustTargets does not contain. The actual relationship is rust-toolchain.toml.targets = allRustTargets ∪ {x86_64-linux-android}, not allRustTargets ∖ {aarch64-unknown-freebsd}.
Step-by-step proof
allRustTargetsatscripts/build/rust.ts:106-117(verified at HEAD b8c4311) lists exactly 10 entries:x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu,x86_64-unknown-linux-musl,aarch64-unknown-linux-musl,x86_64-apple-darwin,aarch64-apple-darwin,x86_64-pc-windows-msvc,aarch64-pc-windows-msvc,x86_64-unknown-freebsd,aarch64-linux-android.- It does not contain
x86_64-linux-android. It does not containaarch64-unknown-freebsd.
rust-toolchain.tomltargets(lines 20-32) lists exactly 11 entries: all 10 fromallRustTargetsplusx86_64-linux-android(line 26).- So the comment's claimed relation ("mirrors … minus aarch64-unknown-freebsd") is wrong: subtracting
aarch64-unknown-freebsdfromallRustTargetsyields the same 10 entries (it was never present), and rust-toolchain.toml then has one extra entry, not one fewer. - Which side is wrong?
rustTarget()at rust.ts:47-54 maps{abi: 'android', x64: true}→'x86_64-linux-android',rustCanCrossFromLinux()returns true for it, and the robobun comment on this PR shows🐧 x64-android - build-bunactually building. So CI does build that triple via the ninja path;allRustTargetsis the one that's incomplete.
Functional impact
allRustTargets has two consumers:
scripts/rust-check-all.ts:21—bun run rust:check-alliterates this array runningcargo check --target <t>. Withx86_64-linux-androidmissing, the dev-convenience "check all platforms" command silently skips it.scripts/build/cargo-config.ts:79— the generated.cargo/config.tomlemits one[target.<triple>]section per entry. Withx86_64-linux-androidmissing, directcargo build --target x86_64-linux-android(outside ninja) gets no per-target linker config. CI is unaffected because the ninja-drivenemitRust()path setsCARGO_TARGET_<TRIPLE>_LINKERandCARGO_ENCODED_RUSTFLAGSvia env per-build, not via the config file.
So the impact is limited to (a) a misleading code comment in a file added by this PR, and (b) two dev-tooling gaps. CI builds are unaffected, hence nit severity.
On the refutation (duplicate of CodeRabbit comment 3211780323)
One verifier flagged this as a duplicate of the existing CodeRabbit inline comment at rust.ts:117, which proposed adding x86_64-linux-android and aarch64-unknown-freebsd to allRustTargets. That comment is marked resolved="true" with "✅ Addressed in commits d5945cf to 808905b" appended. However, that resolved-mark is incorrect:
git show --stat d5945cf→ touches onlybuild.zig,src/**/*.zig, and test files.git show --stat 808905b→ an autofix.ci commit touching onlydocs/.rust-rewrite-verified-claims.md,docs/RUST_IDIOMS_AUDIT.md,docs/zig-restructure-plan.md.- Neither commit touches
scripts/build/rust.ts.allRustTargetsat HEAD still has 10 entries.
This report is therefore not redundant: (1) the original CodeRabbit thread is marked resolved despite not being fixed, so it will not surface to the author; (2) this report targets a different file and line — the rust-toolchain.toml comment text added by this PR, which is independently wrong regardless of whether allRustTargets gets fixed (if you only add x86_64-linux-android to allRustTargets, the "minus aarch64-unknown-freebsd" wording is still vacuous and confusing). The two are sibling symptoms of the same root divergence, but flagging the comment here gives the author the full picture from the file they're more likely to read when bumping the toolchain channel.
How to fix
Either fix is acceptable; doing both is best:
- Add
"x86_64-linux-android"toallRustTargetsin scripts/build/rust.ts (betweenx86_64-unknown-freebsdandaarch64-linux-android). This makes the two lists actually mirror each other modulo the Tier-3 triple. - Reword the comment at rust-toolchain.toml:13-14 to be accurate, e.g.: "Mirrors
allRustTargetsin scripts/build/rust.ts (Tier 1/2 triples only —aarch64-unknown-freebsdis Tier 3 with no prebuilt rust-std, so it's omitted here and built via-Zbuild-stdfrom rust-src)." — and only after (1) makes the mirror claim true.
| const rustup = findTool({ names: ["rustup"], paths: [join(cargoHome, "bin")], required: false })?.path; | ||
| const channel = readRustToolchainChannel(); | ||
| if (rustup !== undefined && channel !== undefined) { | ||
| spawnSync(rustup, ["toolchain", "install", channel, "--force", "--profile", "minimal", "--component", "rust-src"], { |
There was a problem hiding this comment.
🟡 This rustup toolchain install --force --profile minimal contradicts the policy this same PR establishes at rust.ts:213-217 (commit e4db751 deliberately removed --profile minimal from the other --force reinstalls, noting "requesting a narrower profile on a --force reinstall is asking for trouble"). For consistency, drop "--profile", "minimal" here too — --component rust-src already ensures the only extra component this path needs.
Extended reasoning...
What the inconsistency is
Commit e4db751 in this PR ("build: surface rustup repair output; drop --profile minimal") removed --profile minimal from the rustup toolchain install --force invocations in scripts/build/rust.ts and scripts/build/source.ts, and codified the rationale in a comment that now lives at rust.ts:213-217:
No
--profile minimal: the agent already has the default profile, and rustup applies--profileto the install spec, not just first-install — requesting a narrower profile on a--forcereinstall is asking for trouble. We only care thatrust-srcandrust-std-<triple>exist on top of whatever profile is there.
However, the later commit b8c4311 (30 minutes after e4db751, same PR) added findRustLld() in scripts/build/tools.ts, and at line 581 it does:
spawnSync(rustup, ["toolchain", "install", channel, "--force", "--profile", "minimal", "--component", "rust-src"], { ... });— exactly the --force + --profile minimal pattern that e4db751 just removed elsewhere and documented as undesirable. A grep of scripts/build/ confirms tools.ts:581 is now the only remaining --profile minimal in the build scripts.
The code path
findRustLld() runs at configure time (called from resolveLlvmToolchain) on every build, including the link-only CI agent — which is precisely the case the function's own comment (lines 568-577) describes: an agent that downloads libbun_rust.a rather than building it, so it may or may not already have the pinned toolchain installed. If that agent (or a developer machine) already has the toolchain installed with the default profile, this --force --profile minimal reinstall is the same "narrowing on --force" hazard the rust.ts comment warns about.
Step-by-step proof
- e4db751 (2026-05-08 16:58) removes
--profile minimalfromrust.tsandsource.ts, and adds the comment now at rust.ts:213-217 explaining why. - b8c4311 (2026-05-08 17:28, same PR, later) adds
findRustLld()at tools.ts:562-… with["toolchain", "install", channel, "--force", "--profile", "minimal", "--component", "rust-src"]at line 581. rust_build_crossat rust.ts:222 now readsrustup toolchain install ${channel} --force --component rust-src $rust_target_arg— no--profile.grep -rn 'profile.*minimal' scripts/build/→ only tools.ts:581.- The hazard described at rust.ts:215 ("requesting a narrower profile on a
--forcereinstall") applies identically to tools.ts:581: samerustupbinary, same pinned channel, same--force, same risk that the agent has a wider profile already installed.
Why nothing prevents it
This is a build-script convention/policy the PR itself just introduced; nothing type-checks or tests it. The two commits were authored ~30 minutes apart and the second one simply missed applying the convention the first one established — a textbook same-PR oversight.
Impact
Low / defensive. The link-only agent's functional needs are satisfied either way: rust-lld ships in the rustc component (present in the minimal profile) and rust-src is explicitly requested via --component. The concern is consistency with the PR's own documented convention, plus the theoretical case where (a) the link agent shares ~/.rustup with a step that needs default-profile components like rustfmt/clippy, and (b) rustup's --force + narrower-profile actually drops those components. Whether (b) happens in practice depends on rustup version behavior — which is exactly why rust.ts:213-217 phrases it as "asking for trouble" rather than a definite breakage.
How to fix
Drop two array elements:
spawnSync(rustup, ["toolchain", "install", channel, "--force", "--component", "rust-src"], {This matches the form used at rust.ts:222 and source.ts. --component rust-src is kept (it's what -Zbuild-std needs and is harmless here); only the profile narrowing is removed.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
scripts/build/rust.ts (1)
106-117:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRe-add the omitted supported triples to
allRustTargets.This list no longer matches the targets the rest of this file models as supported. In particular,
x86_64-linux-androidis still produced byrustTarget(), andaarch64-unknown-freebsdis still documented below as the CI matrix's Tier 3 triple. As written,rust:check-allskips both the x64 Android path and the only-Zbuild-stdTier 3 path.💡 Proposed fix
export const allRustTargets = [ "x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "aarch64-unknown-linux-musl", "x86_64-apple-darwin", "aarch64-apple-darwin", "x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc", "x86_64-unknown-freebsd", + "aarch64-unknown-freebsd", + "x86_64-linux-android", "aarch64-linux-android", ] as const;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/build/rust.ts` around lines 106 - 117, The allRustTargets array is missing triples that other codepaths expect; update allRustTargets to include the omitted platform triples so rustTarget() and the CI/Tier3 logic are covered: add "x86_64-linux-android" (to match rustTarget's produced triple) and "aarch64-unknown-freebsd" (the documented Tier 3 triple) to the exported allRustTargets const so rust:check-all and -Zbuild-std paths are not skipped.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@scripts/build/rust.ts`:
- Around line 106-117: The allRustTargets array is missing triples that other
codepaths expect; update allRustTargets to include the omitted platform triples
so rustTarget() and the CI/Tier3 logic are covered: add "x86_64-linux-android"
(to match rustTarget's produced triple) and "aarch64-unknown-freebsd" (the
documented Tier 3 triple) to the exported allRustTargets const so rust:check-all
and -Zbuild-std paths are not skipped.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: a646d80a-080f-4e42-9be1-3452b4aba2fb
📒 Files selected for processing (3)
scripts/build/rust.tssrc/bun_core/env.rssrc/bun_core/lib.rs
| { | ||
| id: "rust-lld-for-crosslang-lto", | ||
| issue: "https://rustc-dev-guide.rust-lang.org/backend/updating-llvm.html", | ||
| description: | ||
| "rustc's bundled LLVM is newer than clang's, so clang's ld.lld can't read " + | ||
| "-Clinker-plugin-lto bitcode (forward-compatible only). Link with rust-lld instead.", | ||
| applies: cfg => cfg.lto && cfg.rustLlvmVersion !== undefined && cfg.clangVersion !== undefined, | ||
| expectedToBeFixed: cfg => { | ||
| // Obsolete once clang's LLVM major catches up to (or passes) rustc's — | ||
| // at that point clang's own ld.lld reads rustc's bitcode and the | ||
| // rust-lld swap in resolveConfig() never fires. | ||
| const clangMajor = Number(cfg.clangVersion!.split(".")[0]); | ||
| const rustMajor = Number(cfg.rustLlvmVersion!.split(".")[0]); | ||
| return clangMajor >= rustMajor; | ||
| }, | ||
| cleanup: | ||
| `Delete the rust-lld swap block in resolveConfig() (config.ts), findRustLld() and its call ` + | ||
| `in resolveLlvmToolchain() (tools.ts), the rustLld/rustLlvmVersion fields on Toolchain/Config, ` + | ||
| `and this entry.`, | ||
| }, |
There was a problem hiding this comment.
🔴 The rust-lld-for-crosslang-lto entry's applies() is true on every LTO build (both versions are always populated) and expectedToBeFixed() returns true whenever clangMajor >= rustMajor — so the moment clang's LLVM catches up to rustc's (a normal recurring state, e.g. on the next LLVM_MAJOR bump), every LTO configure will throw BuildError: Workaround 'rust-lld-for-crosslang-lto' is obsolete and instruct deleting findRustLld()/rustLld/rustLlvmVersion/the swap block. The swap at config.ts:629-637 is already self-gating (only fires when rustLlvmMajor > clangMajor) and harmless when dormant, so this entry adds no functional value and should be dropped from workarounds[] (or have applies() narrowed to rustMajor > clangMajor).
Extended reasoning...
What the bug is
The workarounds[] registry in scripts/build/workarounds.ts is designed (per its file-level doc, lines 1-22) for one-time upstream bugs that get permanently fixed in some release: each entry has an expectedToBeFixed() predicate that compares a config-derived version against a constant threshold, and when it trips, checkWorkarounds() (lines 104-116) hard-fails configure with a BuildError instructing the developer to delete the workaround code. The error's escape hatch — "bump the threshold in expectedToBeFixed()" — assumes there is a constant to bump.
The new rust-lld-for-crosslang-lto entry (lines 78-97) misuses this mechanism for a recurring bidirectional version skew: rustc periodically bumps its bundled LLVM ahead of the pinned clang's, then clang catches up, then rustc bumps again. Its expectedToBeFixed() compares two config-derived values (clangMajor >= rustMajor) rather than one config value against a constant — so there's no threshold to bump, and the "fixed" state is not permanent.
The code path that triggers it
checkWorkarounds(cfg)is called unconditionally atconfigure.ts:202afterresolveConfig().applies: cfg => cfg.lto && cfg.rustLlvmVersion !== undefined && cfg.clangVersion !== undefined— both versions are always populated on any rustc-installed host (rustc is now mandatory;findRustc()populatesrustLlvmVersion,resolveLlvmToolchain()populatesclangVersion), so this is effectivelycfg.lto.expectedToBeFixed: cfg => clangMajor >= rustMajor(lines 89-91).- When both predicates are true, line 109 throws
BuildError.
Meanwhile, the actual workaround code at config.ts:629-637 is already self-gating: it only swaps ld = toolchain.rustLld when rustLlvmMajor > clangMajor. When clangMajor >= rustMajor, the swap branch is dormant and clang's own ld.lld is used — exactly the correct behavior, with no intervention needed.
Step-by-step proof
Suppose the next LLVM_MAJOR bump moves LLVM_VERSION_RANGE to accept clang 22 (the sibling asan-dyld-shim entry's FIXED_IN_LLVM = "22.1.4" already anticipates this). With clang 22.x and rustc nightly-2026-05-06 bundling LLVM 22:
cfg.lto = true(release CI build),cfg.clangVersion = "22.1.4",cfg.rustLlvmVersion = "22.0.0"→applies()returns true.clangMajor = 22,rustMajor = 22→expectedToBeFixed()returns22 >= 22= true.checkWorkarounds()throws: "Workaround 'rust-lld-for-crosslang-lto' is obsolete — upstream fix is available" with cleanup instructions to "Delete the rust-lld swap block in resolveConfig(), findRustLld() and its call in resolveLlvmToolchain(), the rustLld/rustLlvmVersion fields on Toolchain/Config, and this entry."- Every LTO configure on every platform now hard-fails.
- A maintainer reads the error. The "bump the threshold" hint doesn't apply (no constant). They follow the cleanup instructions and delete
findRustLld(), therustLld/rustLlvmVersionfields, and the swap block. - Three months later,
rust-toolchain.tomlis bumped to a nightly with LLVM 23. NowrustLlvmMajor = 23 > clangMajor = 22, but the swap infrastructure is gone — clang's ld.lld can't read rustc's bitcode → link fails withUnknown attribute kind(the exact error documented at config.ts:617-618). The deleted code has to be re-written from scratch.
The comment at config.ts:624-625 ("so this branch self-obsoletes once clang's LLVM catches up") confirms the author expected the entry to fire and intended the deletion — but didn't account for the cycle restarting.
Why existing code doesn't prevent it
The workarounds.ts framework has no concept of a "dormant" or "recurring" workaround — it only models monotonic obsolescence. Compare with the well-formed sibling entry asan-dyld-shim (lines 59-77): it checks cfg.clangVersion >= FIXED_IN_LLVM against a constant "22.1.4", which is monotonic and bumpable. The rust-lld entry's predicate is non-monotonic over time and has no constant.
Currently this doesn't fire: clang is pinned to 21.1.x (tools.ts:268-270) and rust nightly-2026-05-06 has LLVM 22, so 21 >= 22 is false. But it's a one-version-bump landmine.
Impact
When triggered: every release/LTO configure hard-fails until a maintainer either deletes permanent infrastructure (creating a future link failure) or figures out that the entry itself is the bug and removes it. Either way it blocks CI.
How to fix
Preferred: Remove the rust-lld-for-crosslang-lto entry from workarounds[] entirely, and delete the misleading "Tracked in workarounds.ts ... self-obsoletes" comment at config.ts:624-625. The swap branch at config.ts:629-637 already self-gates on rustLlvmMajor > clangMajor and is harmless when dormant — it doesn't need an obsolescence tracker because it's a permanent compat shim, not a workaround for a one-time bug.
Alternative: Narrow applies() to cfg.lto && ... && rustMajor > clangMajor so the entry only matches when the swap is actually active. This makes expectedToBeFixed() unreachable (it would only be evaluated when rustMajor > clangMajor, where it returns false), reducing the entry to pure documentation — at which point a code comment is simpler.
| // rust-only/link-only don't run LUT codegen — skip the check so split-CI | ||
| // steps don't require perl on the rust cross-compile box. | ||
| if (cfg.mode === "full" || cfg.mode === "cpp-only") { |
There was a problem hiding this comment.
🟡 The comment at lines 214-215 says "rust-only/link-only don't run LUT codegen", but this PR's own change at codegen.ts:920 (o.rustInputs.push(jssinkRs)) makes rust-only depend on the JSSink edge — whose comment at codegen.ts:917-919 explicitly says "Without this, mode: rust-only … never runs this edge" — and that edge spawns create-hash-table.ts which invokes perl. The build likely still succeeds without perl (generate-jssink.ts:1206 writes generated_jssink.rs before the perl spawn and discards the spawnSync result, and rust-only consumes nothing that needs JSSink.lut.h), so this is a stale-comment / consistency issue rather than a CI breaker — but the comment should either be corrected or the check extended to rust-only.
Extended reasoning...
What the bug is
This PR makes two changes that contradict each other:
-
scripts/build/configure.ts:214-216— The comment was updated (zig→rust rename) to read: "rust-only/link-only don't run LUT codegen — skip the check so split-CI steps don't require perl on the rust cross-compile box", and the perl check remains gated tocfg.mode === "full" || cfg.mode === "cpp-only". -
scripts/build/codegen.ts:920— Addedo.rustInputs.push(jssinkRs), with an explicit 6-line comment (914-919) stating "Without this,mode: \"rust-only\"(CI's build-rust job, which compiles no C++ so nothing else pulls JSSink.cpp/.h) never runs this edge and cargo hits the missing file."
So the same PR (a) wrote a comment asserting rust-only never runs LUT codegen, and (b) added a dependency whose own comment asserts rust-only does run it.
The dependency chain (verified)
bun.ts:531-548—emitRustOnly()callsemitCodegen()then passescodegen.rustInputstoemitRust()ascodegenInputs.codegen.ts:882-921—emitJsSink()declares a single ninja edge with four co-outputs:[JSSink.cpp, JSSink.h, JSSink.lut.h, generated_jssink.rs], and pushesgenerated_jssink.rsintoo.rustInputs.- The cargo build edge depends on
rustInputs, so ninja must buildgenerated_jssink.rs, which means running the JSSink edge. - The JSSink edge runs
generate-jssink.ts, which at line 1210 doesBun.spawnSync([..., "create-hash-table.ts", ...]). create-hash-table.ts:23spawns["perl", create_hash_table, "-"].
So rust-only mode does run LUT codegen and does invoke perl. The configure.ts comment is factually wrong.
Why the impact is overstated
The original bug description claims that without perl, "the build now fails at build-time with a cryptic error instead of at configure-time." Tracing through the actual code shows this is unlikely:
generate-jssink.ts:1206writesgenerated_jssink.rsbefore the perl spawn at line 1210.generate-jssink.ts:1210-1221callsBun.spawnSync(...)and discards the result — no exit-code check, no error propagation. The script ends immediately after.- So even when
create-hash-table.tsexits 1 (which it does at line 39 when perl is missing),generate-jssink.tsitself exits 0. JSSink.lut.his never written, but in rust-only mode nothing consumes it: it goes intoo.cppHeaders(codegen.ts:913), and per the comment atbun.ts:542-544, "The cpp-related outputs … have no consumer in this graph — ninja skips them." The codegen rule hasrestat: true(codegen.ts:142), so ninja stat()s the missing output but doesn't fail the build.- The cargo edge's only dependency from this step is
generated_jssink.rs, which exists.
Net effect: missing perl on the rust cross-compile agent → stderr noise from create-hash-table.ts's "Failed to generate JSSink.lut.h" message, but the rust-only build succeeds. This is messy but not a CI break.
Step-by-step proof
On a rust-only configure with perl absent from PATH:
configure.ts:216evaluatescfg.mode === "full" || cfg.mode === "cpp-only"→false→ perl check skipped.- ninja runs the JSSink edge (pulled by
generated_jssink.rs∈rustInputs∈ cargo's implicit deps). generate-jssink.tswritesJSSink.h,JSSink.cpp,JSSink.lut.txt,generated_jssink.rs(lines 1201-1206).generate-jssink.ts:1210spawnscreate-hash-table.ts; that script triesspawn({cmd: ["perl", ...]}), fails, prints error to stderr, exits 1.Bun.spawnSyncreturns{exitCode: 1, ...}; generate-jssink.ts ignores it; falls off the end → exit 0.- ninja sees exit 0; with
restat: trueit stats outputs, findsJSSink.lut.hmissing — but nothing downstream in the rust-only graph needs it. - cargo edge runs with
generated_jssink.rspresent → success.
Why nothing prevents it
The configure-time perl check exists precisely to catch this class of error early, but it's gated on mode and the gate condition wasn't updated when the dependency was added. There's no test or assertion linking the comment to the actual ninja graph shape.
How to fix
Two reasonable options:
(a) Extend the check to include rust-only (most defensive — catches the case where someone later makes generate-jssink.ts check the spawn result, and avoids the stderr noise):
if (cfg.mode === "full" || cfg.mode === "cpp-only" || cfg.mode === "rust-only") {…and update the comment to "link-only doesn't run LUT codegen".
(b) Just fix the comment to be accurate, e.g. "rust-only runs the JSSink edge (which spawns perl) but only consumes generated_jssink.rs, which is written before perl is invoked, so a missing perl is non-fatal there; link-only runs no codegen at all." — and accept the stderr noise.
Option (a) is cleaner since the whole point of this check is to fail early with a clear hint rather than emit confusing stderr mid-build.
| * link's `--gc-sections` still DCEs per-function. `rustLinkFlags()` wraps | ||
| * the archive in `--whole-archive` so members that are *only* referenced via | ||
| * the dynamic-list / NAPI surface (no inbound static ref) are retained too. |
There was a problem hiding this comment.
🟡 nit: This module doc says (present tense) "rustLinkFlags() wraps the archive in --whole-archive…", implying it's active in the link, but rustLinkFlags() (defined at line 521) has zero callers — bun.ts:470 puts rustObjects directly in the link's $in list and bun.ts:466-468 explicitly says "no --whole-archive wrapping is needed; if a member ever isn't, rustLinkFlags() in rust.ts is the wrapping helper." The two new comments contradict each other; change "wraps" → "can wrap" (or describe it as an available helper) so the module doc matches reality.
Extended reasoning...
What the bug is
The module-level doc comment at scripts/build/rust.ts:22-24 describes the link flow in present indicative:
The staticlib goes into the link's
$inlist between the C++ objects and the dependency archives; crt1.o's undefinedmainplus the C++ side's hundreds ofextern "C"Bun__*/Zig*references pull every reachable member, and the release link's--gc-sectionsstill DCEs per-function.rustLinkFlags()wraps the archive in--whole-archiveso members that are only referenced via the dynamic-list / NAPI surface (no inbound static ref) are retained too.
The bolded sentence reads as describing what the build does. But rustLinkFlags() is never called anywhere — it's a defined-but-unused helper.
Step-by-step proof
grep -rn rustLinkFlags scripts/returns exactly 3 hits:rust.ts:22— the module doc sentence in questionrust.ts:521—export function rustLinkFlags(cfg: Config, libs: string[]): string[] {(the definition)bun.ts:467— a comment at the actual link site
- Zero call sites. The function is exported but not imported anywhere.
- The actual link happens at
bun.ts:470:link(n, cfg, exeName, [...allObjects, ...rustObjects, ...windowsRes], {...})—rustObjects(thelibbun_rust.apath) is placed directly in the$inarray with no--whole-archivewrapping. - The comment at
bun.ts:461-468is explicit and authoritative:rustObjects (the staticlib) sits between cppAll and depLibs … Every
#[no_mangle]export the C++ side touches is reached transitively from those roots, so no--whole-archivewrapping is needed; if a member ever isn't,rustLinkFlags()in rust.ts is the wrapping helper. - So bun.ts says "not used, available as fallback"; rust.ts:22 says "wraps" (active). Both comments were added in this PR and contradict each other.
Why this is just a nit
This is prose-only with no behavioral impact:
- The actual link is correctly configured per the bun.ts call-site comment, which gives a detailed and correct rationale (crt1.o's
main+ C++'sBun__*/Zig*extern refs reach every archive member transitively). - The original bug report's option (b) — that NAPI/dynamic-list-only
#[no_mangle]symbols might actually be silently dropped — has no supporting evidence; bun.ts:465-466 explicitly addresses why that doesn't happen, andemitSmokeTestprovides runtime coverage. If symbols were dropped, link errors or test failures would surface it. - The function's own JSDoc at line 512-520 is fine (it describes what the function does when called, which is correct).
The only defect is that the module-header overview at lines 18-24 narrates the build flow as if rustLinkFlags() participates in it, when it's actually a dormant helper. A reader following the module doc to understand "what happens at link time" would be misled into thinking whole-archive wrapping is active.
Why nothing prevents it
These are two comments in two files added by the same PR; nothing type-checks prose. The bun.ts comment was clearly written after deciding not to call rustLinkFlags() (it explains why it's unnecessary), but the rust.ts module header was either written earlier or wasn't updated to match.
How to fix
One-word change at rust.ts:22: "rustLinkFlags() wraps" → "rustLinkFlags() can wrap". Or reword the sentence to make the helper's status explicit, e.g.:
…
--gc-sectionsstill DCEs per-function. (rustLinkFlags()is available to wrap the archive in--whole-archiveif a member ever turns out to be referenced only via the dynamic-list / NAPI surface with no inbound static ref — currently unused since every export is reached transitively; see bun.ts.)
This is the same class of zig→rust doc-sync nit as the others already filed on this PR (CLAUDE.md line 268, codegen.ts JSDoc lines 212/215, scripts/build/CLAUDE.md line 146).
| const rustup = findTool({ names: ["rustup"], paths: [join(cargoHome, "bin")], required: false })?.path; | ||
| const channel = readRustToolchainChannel(); | ||
| if (rustup !== undefined && channel !== undefined) { | ||
| spawnSync(rustup, ["toolchain", "install", channel, "--force", "--profile", "minimal", "--component", "rust-src"], { | ||
| encoding: "utf8", | ||
| timeout: 300_000, | ||
| stdio: ["ignore", "ignore", "inherit"], // surface download/error output | ||
| }); |
There was a problem hiding this comment.
🟡 findRustLld() runs rustup toolchain install <channel> --force --profile minimal at configure time on every build, but rust.ts:213-217 (also added in this PR) explicitly warns against exactly this combination: "No --profile minimal: … requesting a narrower profile on a --force reinstall is asking for trouble" — and rust_build_cross accordingly omits --profile. Either drop "--profile", "minimal" here to match rust.ts's reasoning, or remove the rust.ts caveat if it was unfounded; both can't be right.
Extended reasoning...
What the inconsistency is
This PR adds two rustup toolchain install --force call sites that disagree on whether --profile minimal is safe to combine with --force:
scripts/build/tools.ts:581 (findRustLld()):
spawnSync(rustup, ["toolchain", "install", channel, "--force", "--profile", "minimal", "--component", "rust-src"], …)scripts/build/rust.ts:213-217 (comment on rust_build_cross):
No
--profile minimal: the agent already has the default profile, and rustup applies--profileto the install spec, not just first-install — requesting a narrower profile on a--forcereinstall is asking for trouble. We only care thatrust-srcandrust-std-<triple>exist on top of whatever profile is there.
And indeed the rust_build_cross rule's command (rust.ts:222) deliberately omits --profile. Both files are new in this PR, so this is an internal contradiction within the same change.
Why it matters where it runs
findRustLld() is called from resolveLlvmToolchain() (tools.ts:487), which runs at configure time on every build mode — local bun bd, rust-only, cpp-only, and link-only CI agents alike (per its own comment at lines 568-577). So unlike rust_build_cross (which only fires when the build edge runs), this --force --profile minimal invocation happens unconditionally on every machine that has rustup + a pinned channel.
If the rust.ts comment's concern is valid — that --force reinstalling with a narrower profile can drop components like rustfmt/clippy from an existing default-profile install — then every bun bd would silently degrade the developer's pinned-nightly toolchain. This PR also adds "fmt:rust": "cargo fmt --all" to package.json (line 64), which depends on rustfmt being present.
Step-by-step trace
- Developer has
nightly-2026-05-06installed with the default profile (rustc, cargo, rustfmt, clippy, rust-docs, rust-std). - Developer runs
bun bd. configure()→resolveToolchain()→resolveLlvmToolchain()→findRustLld(os)(tools.ts:487).findRustLld()findsrustupand readschannel = "nightly-2026-05-06"fromrust-toolchain.toml, then runsrustup toolchain install nightly-2026-05-06 --force --profile minimal --component rust-src(tools.ts:581).- Per rust.ts:213-215's own stated concern, rustup "applies
--profileto the install spec, not just first-install" — so--force+--profile minimalis, in this PR's own words, "asking for trouble." - If components are dropped, the developer's next
bun run fmt:rustfails with "rustfmt not installed".
Why this is a nit, not a blocker
The observable harm is speculative: in practice, modern rustup generally treats the profile as the minimum component set to ensure, not the exact set to enforce, so already-installed components like rustfmt are typically retained across a --force --profile minimal reinstall. The rust.ts comment may be overcautious. But that's exactly the point — one of these two pieces of code is wrong about rustup's behavior, and they were both written in this PR. Either:
- the rust.ts warning is unfounded → remove it (it actively misleads the next person who touches
rust_build_cross), or - the rust.ts warning is correct → drop
"--profile", "minimal"from tools.ts:581 (the conservative fix; the install still works, just downloads ~30MB more on a fresh agent).
Suggested fix
Drop --profile minimal from tools.ts to match rust.ts's reasoning — it's the conservative choice and costs nothing on agents that already have the default profile (which the rust.ts comment says they do):
spawnSync(rustup, ["toolchain", "install", channel, "--force", "--component", "rust-src"], {This makes both call sites consistent and removes the risk the rust.ts author was worried about, without needing to settle the question of whether that risk is real.
| export const allRustTargets = [ | ||
| "x86_64-unknown-linux-gnu", | ||
| "aarch64-unknown-linux-gnu", | ||
| "x86_64-unknown-linux-musl", | ||
| "aarch64-unknown-linux-musl", | ||
| "x86_64-apple-darwin", | ||
| "aarch64-apple-darwin", | ||
| "x86_64-pc-windows-msvc", | ||
| "aarch64-pc-windows-msvc", | ||
| "x86_64-unknown-freebsd", | ||
| "aarch64-linux-android", | ||
| ] as const; |
There was a problem hiding this comment.
🔴 allRustTargets lists 10 triples but rustTarget() produces 12 — it's missing aarch64-unknown-freebsd (which rustTargetIsTier3() at L126 explicitly handles) and x86_64-linux-android, both of which CI builds (visible in this PR's robobun output). This means bun run rust:check-all skips 2 of 12 CI platforms and cargo-config.ts omits their [target.<triple>] linker stanzas. Note: CodeRabbit flagged this earlier and the thread is marked "✅ Addressed in commits d5945cf to 808905b", but those commits don't touch allRustTargets and at HEAD the list still has only 10 entries — the resolution marker is stale.
Extended reasoning...
What the bug is
allRustTargets (scripts/build/rust.ts:106-117) is documented at L102-104 as "All target triples CI builds. Exposed so rust:check-all can iterate". It contains 10 entries. But rustTarget() (L47-57), which maps a build Config to its triple, can return 12 distinct values — the array omits two that CI actually builds:
aarch64-unknown-freebsd—rustTarget()returns this forcfg.freebsd && !cfg.x64(L51).rustTargetIsTier3()at L125-126 special-cases exactly this triple, and its doc comment (L122-123) says "the only Tier 3 triple in CI's matrix is aarch64-freebsd".rustCanCrossFromLinux()at L82/L97 explicitly says "freebsd × {x64,aarch64}: yes".x86_64-linux-android—rustTarget()returns this forcfg.abi === 'android' && cfg.x64(L54).rustCanCrossFromLinux()at L80 says "linux-{gnu,musl,android} × {x64,aarch64}".
Both targets appear in this PR's own robobun CI output: 🐧 x64-android - build-rust and :freebsd: aarch64 - build-rust are running jobs in Build #52948.
The code path that triggers it
allRustTargets has two consumers:
-
scripts/rust-check-all.ts:21— iterates the array runningcargo check --target <triple>for each. This is wired asbun run rust:check-allin package.json, replacing the oldzig:check-allper CLAUDE.md anddocs/CI_RUST_MIGRATION.md. It's the dev-facing "does my change compile on every CI platform?" check. With the array short by 2, a developer runningrust:check-allgets a green result while CI may still fail on x64-android or aarch64-freebsd — the exact thing this script exists to prevent. -
scripts/build/cargo-config.ts:79— iterates the array to emit[target.<triple>] linker = "..."stanzas in the generated.cargo/config.toml. The two missing triples get no linker stanza, so a localcargo check --target aarch64-unknown-freebsdor--target x86_64-linux-androidfalls back to defaultccresolution instead of the discovered cross-toolchain.
Why nothing prevents it
CI itself is unaffected: the build-rust step calls rustTarget(cfg) directly (not allRustTargets), so all 12 platforms still build. The bug is in dev-facing tooling that's supposed to match CI but silently doesn't. There's no test or assertion that allRustTargets is exhaustive over rustTarget()'s range.
Step-by-step proof
rustTarget({freebsd: true, x64: false, ...})→"aarch64-unknown-freebsd"(rust.ts:51).allRustTargets.includes("aarch64-unknown-freebsd")→false(L106-117: onlyx86_64-unknown-freebsdis listed).rustTarget({linux: true, abi: "android", x64: true, ...})→"x86_64-linux-android"(rust.ts:54).allRustTargets.includes("x86_64-linux-android")→false(L106-117: onlyaarch64-linux-androidis listed).- A developer makes a change that breaks compilation only on aarch64-freebsd (e.g., the
libc::max_align_tissue visible in this PR's CI). They runbun run rust:check-all→ the loop at rust-check-all.ts:21 never reaches that triple → script reports "10 ok, 0 failed" → developer pushes → CI fails on:freebsd: aarch64 - build-rust.
On the refutation ("duplicate of existing CodeRabbit comment")
CodeRabbit did flag this in inline comment 3211780323 on rust.ts:117. However, that thread is marked resolved="true" with the annotation "✅ Addressed in commits d5945cf to 808905b". I checked: git log d5945cf..808905b -- scripts/build/rust.ts returns commits about cargo-config generation, codegen-output renames, and cross-target nightly fixes — none modify allRustTargets. At HEAD (f843206), L106-117 still lists exactly 10 entries. The resolution marker is incorrect (likely an autofix.ci heuristic mismatch — the same range was applied to several unrelated comments). Since the thread is marked resolved, the author will not see it in their open-conversations view; re-flagging is appropriate precisely because the prior feedback was incorrectly closed.
How to fix
Add the two missing triples to the array:
export const allRustTargets = [
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-musl",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"x86_64-pc-windows-msvc",
"aarch64-pc-windows-msvc",
"x86_64-unknown-freebsd",
"aarch64-unknown-freebsd",
"x86_64-linux-android",
"aarch64-linux-android",
] as const;Optionally, add a unit test or static assertion that derives the set of triples rustTarget() can produce (12 = 2 archs × {darwin, windows, freebsd, linux-gnu, linux-musl, linux-android}) and checks allRustTargets covers it, so future additions to rustTarget() can't silently desync.
8b95b37 to
0de544c
Compare
| # Mirrors `allRustTargets` in scripts/build/rust.ts minus aarch64-unknown-freebsd | ||
| # (Tier 3 — no prebuilt rust-std, cargo `-Zbuild-std` builds it from rust-src). |
There was a problem hiding this comment.
🟡 The comment "Mirrors allRustTargets in scripts/build/rust.ts minus aarch64-unknown-freebsd" is inaccurate: this targets list has 11 entries including x86_64-linux-android (line 26), but allRustTargets (rust.ts:106-117) has only 10 and omits x86_64-linux-android — so bun run rust:check-all and the generated .cargo/config.toml skip a target CI actually builds. (CodeRabbit flagged the allRustTargets side at rust.ts:117 and marked it "✅ Addressed in commits d5945cf to 808905b", but those changes are not at HEAD dd4b1a5 — allRustTargets still has 10 entries.)
Extended reasoning...
What the bug is
rust-toolchain.toml:13 claims:
Mirrors
allRustTargetsin scripts/build/rust.ts minus aarch64-unknown-freebsd (Tier 3 — no prebuilt rust-std, cargo-Zbuild-stdbuilds it from rust-src).
This is doubly inaccurate at HEAD (dd4b1a5):
allRustTargets(scripts/build/rust.ts:106-117) does not containaarch64-unknown-freebsd, so there is nothing to subtract.rust-toolchain.toml'stargetsarray (lines 20-32) contains 11 entries includingx86_64-linux-android(line 26), whileallRustTargetscontains only 10 and omitsx86_64-linux-android.
So the two lists are not mirrors: the actual delta is +x86_64-linux-android in the toml, not -aarch64-unknown-freebsd.
Step-by-step proof
rust-toolchain.toml:20-32— count the entries:aarch64-unknown-linux-gnu,x86_64-unknown-linux-gnu,aarch64-unknown-linux-musl,x86_64-unknown-linux-musl,aarch64-linux-android,x86_64-linux-android,x86_64-unknown-freebsd,aarch64-apple-darwin,x86_64-apple-darwin,aarch64-pc-windows-msvc,x86_64-pc-windows-msvc= 11.scripts/build/rust.ts:106-117—allRustTargetshas exactly 10 entries: the same set minusx86_64-linux-android.aarch64-unknown-freebsdappears in neither list.rustTarget()(rust.ts) maps{os:"linux", arch:"x64", abi:"android"}→x86_64-linux-android, and the robobun CI comment on this PR shows a🐧 x64-android - build-rustjob — so CI does build this triple viagetBuildRustStepdirectly.scripts/rust-check-all.ts:21iteratesallRustTargets→bun run rust:check-allsilently skipsx86_64-linux-android.scripts/build/cargo-config.ts:79iteratesallRustTargetsto emit per-target[target.<triple>]linker sections in the generated.cargo/config.toml→ no linker entry is written forx86_64-linux-android.
Why nothing prevents it
The comment is prose; nothing validates it. allRustTargets is the only place the "all triples" list is materialized for dev tooling, and its consumers (rust-check-all.ts, cargo-config.ts) silently iterate whatever it contains. CI itself is unaffected because getBuildRustStep calls rustTarget(cfg) per-platform (not allRustTargets), and emitRust() sets CARGO_TARGET_<TRIPLE>_LINKER via env directly — so the missing .cargo/config.toml section does not break the CI build.
On the existing CodeRabbit comment
CodeRabbit inline comment id=3211780323 on scripts/build/rust.ts:117 flagged the allRustTargets side of this same inconsistency (recommending adding both x86_64-linux-android and aarch64-unknown-freebsd). It is marked resolved=true with "✅ Addressed in commits d5945cf to 808905b". However, at HEAD dd4b1a5 allRustTargets still has exactly 10 entries — those commits' changes are not present in the PR branch. The auto-resolution appears to be incorrect, which is why re-flagging this from the rust-toolchain.toml angle has independent value.
Impact
Minor — this only affects developer-convenience tooling, not CI correctness:
bun run rust:check-all(thezig:check-allreplacement) skips one target that CI exercises, so a developer running it locally before pushing could miss x64-android-onlycfgerrors.- The generated
.cargo/config.tomllacks a[target.x86_64-linux-android]linker section, so a directcargo build --target x86_64-linux-androidfrom a dev machine may fail to find the cross-linker (CI is fine becauseemitRust()passes the linker via env). - The comment itself is misleading for anyone trying to keep the two lists in sync.
How to fix
Either:
- Add
"x86_64-linux-android"toallRustTargetsinscripts/build/rust.ts(and optionally"aarch64-unknown-freebsd"per the original CodeRabbit suggestion, though that one is Tier 3 andrust:check-allwould skip it anyway sincerustup target addfails for it). This makes the comment true. - Or update the comment at
rust-toolchain.toml:13to accurately describe the actual delta, e.g. "Superset ofallRustTargetsin scripts/build/rust.ts: includesx86_64-linux-android(in CI matrix but not yet inallRustTargets); excludesaarch64-unknown-freebsd(Tier 3 — …)."
The first option is preferable since it fixes the functional gap in rust:check-all rather than just documenting it.
| const rustup = findTool({ names: ["rustup"], paths: [join(cargoHome, "bin")], required: false })?.path; | ||
| const channel = readRustToolchainChannel(); | ||
| if (rustup !== undefined && channel !== undefined) { | ||
| spawnSync(rustup, ["toolchain", "install", channel, "--force", "--profile", "minimal", "--component", "rust-src"], { |
There was a problem hiding this comment.
🟡 This passes --force --profile minimal to rustup toolchain install, but scripts/build/rust.ts:213-217 (also new in this PR) explicitly documents not passing --profile minimal with --force ("requesting a narrower profile on a --force reinstall is asking for trouble") and accordingly omits it at line 222. findRustLld() runs at configure time on every bun bd — including local dev machines with full default-profile installs and the same CI agents rust.ts is protecting — so the two call sites apply contradictory policies to the same toolchain. Either drop --profile minimal here to match rust.ts's reasoning, or update the rust.ts comment if it's overly cautious.
Extended reasoning...
What the inconsistency is
This PR adds two pieces of code that both run rustup toolchain install <channel> --force against the pinned nightly, but with directly contradictory rationale about --profile:
scripts/build/rust.ts:213-217 (the comment above the rust_build_cross ninja rule) says:
No
--profile minimal: the agent already has the default profile, and rustup applies--profileto the install spec, not just first-install — requesting a narrower profile on a--forcereinstall is asking for trouble. We only care thatrust-srcandrust-std-<triple>exist on top of whatever profile is there.
…and accordingly rust.ts:222 runs:
rustup toolchain install ${cfg.rustToolchain} --force --component rust-src $rust_target_arg
with --profile deliberately omitted.
scripts/build/tools.ts:581 (findRustLld()) does the opposite:
spawnSync(rustup, ["toolchain", "install", channel, "--force", "--profile", "minimal", "--component", "rust-src"], ...)Both lines were added in this same PR.
Why they hit the same toolchain on the same machines
findRustLld() is called from resolveLlvmToolchain() (tools.ts:487), which is called from resolveToolchain() (configure.ts:31), which runs unconditionally during every bun bd configure pass — on local dev machines and on every CI agent. The rust_build_cross rule then runs at ninja time on the build-rust agents. So on a CI build-rust agent, configure first runs the tools.ts version (--force --profile minimal), and then ninja runs the rust.ts version (--force, no profile) — two consecutive invocations with contradictory flags against the same ~/.rustup/toolchains/<channel>-<host>/.
On a local dev machine (which typically has the default profile installed — i.e. rustfmt, clippy, rust-docs), only the configure-time tools.ts call runs (since the ninja-time rule may not fire on a non-cross host), so the version with --profile minimal is the one that always executes.
Step-by-step proof of the contradiction
- Read
rust.ts:213-217: explicit, considered rationale for not passing--profile minimalwith--force. - Read
rust.ts:222:--profileis absent, matching that rationale. - Read
tools.ts:581:"--force", "--profile", "minimal"— does exactly what the rust.ts comment says is "asking for trouble". - Trace callers:
tools.ts:487→configure.ts:31→ runs on every configure, on every host. There is no gate that restrictsfindRustLld()to CI link-only agents (the comment at tools.ts:568-577 mentions link-only as the motivation, but the call is unconditional). - Therefore one of the two pieces of reasoning in this PR is wrong, and they target the same install.
Why this is a nit and not a normal bug
The actual harm is uncertain. Per rustup's documented semantics, --force means "force an update, even if some of the requested components are missing" — it does not mean "wipe and reinstall from scratch". And --profile selects the initial component set; on an already-installed toolchain rustup typically just ensures the requested components exist and updates to the latest build of that channel without removing extra components. So the rust.ts comment may be overly cautious FUD rather than describing real component-stripping behavior, and tools.ts:581 may in practice be harmless.
But that's exactly the point: the PR contains two pieces of new code, one of which carries a multi-line comment explaining why pattern X is dangerous, and the other of which does pattern X. Either:
- (a) rust.ts's concern is valid → tools.ts:581 should drop
--profile minimal(and possibly--force, since the tools.ts use case is "ensure the toolchain exists for sysroot lookup", not "repair a broken-manifest install" — a plaintoolchain install <channel> --component rust-srcis idempotent and sufficient there); or - (b) rust.ts's concern is overstated → the rust.ts:213-217 comment should be removed or rewritten so future readers aren't misled into thinking
--profile minimalis unsafe.
Either way the two should be reconciled before merging.
How to fix
Simplest: drop "--profile", "minimal" from tools.ts:581 to match rust.ts:222. This makes both call sites consistent with the documented rationale and costs nothing (rustup will just keep whatever profile is already installed, plus rust-src).
| export const allRustTargets = [ | ||
| "x86_64-unknown-linux-gnu", | ||
| "aarch64-unknown-linux-gnu", | ||
| "x86_64-unknown-linux-musl", | ||
| "aarch64-unknown-linux-musl", | ||
| "x86_64-apple-darwin", | ||
| "aarch64-apple-darwin", | ||
| "x86_64-pc-windows-msvc", | ||
| "aarch64-pc-windows-msvc", | ||
| "x86_64-unknown-freebsd", | ||
| "aarch64-linux-android", | ||
| ] as const; |
There was a problem hiding this comment.
🟡 allRustTargets (doc'd as "All target triples CI builds") is missing aarch64-unknown-freebsd and x86_64-linux-android, both of which rustTarget() returns and CI's buildPlatforms actually builds (.buildkite/ci.mjs:136,140) — so rust:check-all and generateCargoConfig() silently skip them. Note: CodeRabbit already flagged this and the thread is marked "✅ Addressed in commits d5945cf to 808905b", but no commit in that range touched this array (808905b is a docs-only autofix; 71348ae added rustTargetIsTier3() referencing aarch64-unknown-freebsd after the array but didn't add it to the array) — at HEAD it still has 10 entries.
Extended reasoning...
What the bug is
allRustTargets at scripts/build/rust.ts:106-117 is a 10-entry const array whose doc comment (line 103) says "All target triples CI builds. Exposed so rust:check-all can iterate cargo check --target <t> without re-deriving the list." It omits two triples that the rest of the file and CI both treat as supported:
aarch64-unknown-freebsd— produced byrustTarget()line 51 whencfg.freebsd && !cfg.x64; explicitly handled byrustTargetIsTier3()at line 126 (whose own doc comment at line 122-123 says it's "the only Tier 3 triple in CI's matrix"); built by CI per.buildkite/ci.mjs:140({os:'freebsd', arch:'aarch64'}).x86_64-linux-android— produced byrustTarget()line 54 whencfg.abi === 'android' && cfg.x64; built by CI per.buildkite/ci.mjs:136({os:'linux', arch:'x64', abi:'android'}).
The two consumers
allRustTargets has exactly two callers:
-
scripts/rust-check-all.ts:21iterates it to runcargo check --target <triple>. With these two triples missing,bun run rust:check-allnever type-checks the freebsd-aarch64 or android-x64 cfg paths. A contributor could land code that failscargo checkonly on those targets and not find out until the actual CI build step. -
scripts/build/cargo-config.ts:79iterates it to emit[target.<triple>] linker = "..."sections into the generated.cargo/config.toml. With these two triples missing, a developer runningcargo build --target aarch64-unknown-freebsddirectly (outside the ninja-driven build) gets no linker configured for that target.
Why this is a nit, not a normal-severity bug
The actual CI build path is not broken: emitRust() sets CARGO_TARGET_<TRIPLE>_LINKER as an env var on the ninja edge (rust.ts line ~337), so the ninja-invoked cargo build for these targets gets the right linker regardless of what's in .cargo/config.toml. And rust-check-all.ts is a developer-convenience script (it already skips any target whose std isn't rustup target add-installed locally), not a CI gate. The impact is limited to (a) two triples missing from local cross-target type-check coverage and (b) direct-cargo invocations losing linker config for those triples — both real but low-impact gaps in tooling consistency.
Why re-flag despite the resolved marker
CodeRabbit's inline comment 3211780323 already flagged exactly this and shows resolved="true" with "✅ Addressed in commits d5945cf to 808905b". I checked: in that range, 808905b is an autofix.ci commit touching only docs/*.md; 71348ae did touch rust.ts but only to add rustTargetIsTier3() after the array (the diff context line is @@ export const allRustTargets = [ … ] as const; followed by the new function — the array body is unchanged); 7636b8e is the commit that created the 10-entry array; the others (f8fd66c, 1e6186b, dbf906b) don't touch the array. git show HEAD:scripts/build/rust.ts | sed -n '106,117p' confirms 10 entries at HEAD. The resolution marker is a false positive — without re-flagging, this ships with the doc comment claiming completeness it doesn't have.
Step-by-step proof
rustTarget({freebsd:true, x64:false, ...})→ line 51 returns"aarch64-unknown-freebsd".rustTarget({linux:true, abi:'android', x64:true, ...})→ line 54 returns"x86_64-linux-android"..buildkite/ci.mjsbuildPlatformscontains{os:'freebsd', arch:'aarch64'}(line 140) and{os:'linux', arch:'x64', abi:'android'}(line 136), so both feed throughgetBuildRustStep()→getBuildArgs(... 'rust-only')→rustTarget().allRustTargets(lines 106-117) lists exactly: linux-gnu×2, linux-musl×2, apple-darwin×2, pc-windows-msvc×2,x86_64-unknown-freebsd,aarch64-linux-android= 10 entries. Neither triple from steps 1-2 is present.rust-check-all.ts:21for (const triple of allRustTargets)therefore never reaches either triple.cargo-config.ts:79for (const triple of allRustTargets)therefore writes no[target.aarch64-unknown-freebsd]or[target.x86_64-linux-android]section.
How to fix
Add the two entries (CodeRabbit's committable suggestion was correct):
export const allRustTargets = [
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-musl",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"x86_64-pc-windows-msvc",
"aarch64-pc-windows-msvc",
"x86_64-unknown-freebsd",
"aarch64-unknown-freebsd",
"x86_64-linux-android",
"aarch64-linux-android",
] as const;Note that rust-check-all.ts will still skip aarch64-unknown-freebsd locally unless -Zbuild-std handling is added there too (it's Tier 3, no rustup target add), but listing it keeps the array honest with its doc comment and lets cargo-config.ts emit the linker section.
| join(cwd, "src/jsc/modules"), | ||
| join(cwd, "src/js/builtins"), | ||
| join(cwd, "src/napi"), | ||
| join(cwd, "src/runtime/napi"), |
There was a problem hiding this comment.
🟡 This PR moves src/napi/ → src/runtime/napi/ (commit 0336c86) and updates the include path here, but scripts/run-clang-format.sh:35 still excludes via [[ "$file" =~ src/napi/ ]], which won't match src/runtime/napi/... — so format.yml/autofix.ci will now run clang-format over the vendored upstream Node-API headers (js_native_api.h, node_api.h, etc.). Note line 34 immediately above (src/runtime/ffi/ffi-) was updated for the same tier-6 collapse, so line 35 was simply missed. .github/workflows/CLAUDE.md:24 also still documents the stale src/napi/ path.
Extended reasoning...
What the bug is
This PR's tier-6 collapse (documented in the PR's own docs/CYCLEBREAK.md: "napi become[s] modules of bun_runtime") moves src/napi/ → src/runtime/napi/. The PR updates scripts/build/flags.ts:1126 to reflect the new include path (join(cwd, "src/napi") → join(cwd, "src/runtime/napi")), but the clang-format exclusion list in scripts/run-clang-format.sh was not updated: line 35 still reads [[ "$file" =~ src/napi/ ]]. The bash regex pattern src/napi/ requires the literal sequence s-r-c-/-n-a-p-i-/, which does not appear in src/runtime/napi/js_native_api.h (the character before napi/ is /, but the character before that is e, not c).
The code path that triggers it
scripts/run-clang-format.sh:43 runs find src packages -type f \( -name "*.h" -o -name "*.hpp" \), which now discovers:
src/runtime/napi/js_native_api.hsrc/runtime/napi/js_native_api_types.hsrc/runtime/napi/node_api.hsrc/runtime/napi/node_api_types.h
The exclusion loop at lines 32-41 then tests each file against five regex patterns. None match (src/napi/ doesn't, and the others are for ffi/libuv/sqlite/usockets/vendor), so all four headers are appended to FILES_TO_FORMAT and passed to clang-format -i.
Why nothing prevents it
The sibling exclusion at line 34 (src/runtime/ffi/ffi-) was updated for the same restructuring — proving the intent was to keep these exclusions current with the directory layout, and that line 35 was simply overlooked in the same sweep. There is no test or CI check that verifies the exclusion list points at directories that exist.
Step-by-step proof
ls src/napi→No such file or directory(verified).ls src/runtime/napi/→js_native_api.h js_native_api_types.h mod.rs napi.zig napi_body.rs node_api.h node_api_types.h(verified).git diff <merge-base>..HEAD --name-onlyshows bothsrc/napi/node_api*.h(deleted) andsrc/runtime/napi/*(added) — the move is part of this PR's 2082-file, 116-commit range, landing in commit 0336c86.scripts/run-clang-format.shis not in the PR's changed-files list — the script was not touched.- In bash,
[[ "src/runtime/napi/node_api.h" =~ src/napi/ ]]→ exit 1 (no match). The exclusion is dead. .github/workflows/format.ymlinvokes this script viabun run clang-format, andautofix-cicommits the result.
Impact
The next format.yml run (and every subsequent autofix.ci pass) will reformat four vendored upstream Node-API headers to Bun's local .clang-format style. This doesn't break the build or change runtime behavior, but it (a) pollutes git blame/history on third-party code, (b) creates spurious diffs that complicate future syncs from nodejs/node upstream, and (c) is exactly the outcome the exclusion was written to prevent. Hence nit rather than normal — concrete unwanted CI behavior, but cosmetic/maintenance impact only.
How to fix
In scripts/run-clang-format.sh:35, change:
[[ "$file" =~ src/napi/ ]] || \to:
[[ "$file" =~ src/runtime/napi/ ]] || \And in .github/workflows/CLAUDE.md:24, change src/napi/ → src/runtime/napi/ to keep the documentation in sync.
| const rustup = findTool({ names: ["rustup"], paths: [join(cargoHome, "bin")], required: false })?.path; | ||
| const channel = readRustToolchainChannel(); | ||
| if (rustup !== undefined && channel !== undefined) { | ||
| spawnSync(rustup, ["toolchain", "install", channel, "--force", "--profile", "minimal", "--component", "rust-src"], { | ||
| encoding: "utf8", | ||
| timeout: 300_000, |
There was a problem hiding this comment.
🟡 This PR's own rust.ts:213-217 documents why --profile minimal is omitted from the rust_build_cross rustup invocation ("requesting a narrower profile on a --force reinstall is asking for trouble"), but findRustLld() here — which runs unconditionally at configure time on every bun bd, before rust_build_cross — does exactly that with --force --profile minimal. If the rust.ts comment's reasoning is correct, this could narrow a default-profile install on dev machines (breaking bun run fmt:rust/rust:clippy); if it's not, the rust.ts comment is misleading. Either drop --profile minimal here to match, or update the rust.ts comment.
Extended reasoning...
What the inconsistency is
This PR adds two new rustup toolchain install --force invocations targeting the same pinned channel, with contradictory positions on --profile minimal:
scripts/build/rust.ts:213-217 (above the rust_build_cross ninja rule):
No
--profile minimal: the agent already has the default profile, and rustup applies--profileto the install spec, not just first-install — requesting a narrower profile on a--forcereinstall is asking for trouble.
and accordingly line 222 omits --profile:
toolchain install ${cfg.rustToolchain} --force --component rust-src $rust_target_arg
scripts/build/tools.ts:581 (inside findRustLld()):
spawnSync(rustup, ["toolchain", "install", channel, "--force", "--profile", "minimal", "--component", "rust-src"], { ... });Both target the same channel (readRustToolchainChannel() reads the same rust-toolchain.toml in both files). The two pieces of code disagree on whether --force --profile minimal is safe.
Why the ordering matters
findRustLld() is called from resolveLlvmToolchain() at tools.ts:487, which runs unconditionally at configure time on every bun bd (per scripts/build/CLAUDE.md's configure-flow description, and confirmed by workarounds.ts:94 which documents this same call chain). The comment at tools.ts:568-577 explains the configure-time install exists so the link-only CI agent has the pinned nightly before rustc --print sysroot is queried — but the call is not gated to link-only CI; it fires on every developer's bun bd too.
rust_build_cross runs at ninja time, strictly after configure. So if the rust.ts comment's hazard is real, tools.ts has already triggered it before rust.ts's careful avoidance ever runs.
Step-by-step
- Developer has the pinned nightly installed with the default profile (rustfmt, clippy, rust-docs) — the normal state after
rustupauto-installs fromrust-toolchain.tomlon firstcargoinvocation. - Developer runs
bun bd. - Configure →
resolveToolchain()→resolveLlvmToolchain()→findRustLld()(tools.ts:487). findRustLld()finds rustup and the pinned channel, runsrustup toolchain install <channel> --force --profile minimal --component rust-src(tools.ts:581).- If rust.ts:213-216's reasoning is correct ("rustup applies --profile to the install spec, not just first-install"), this reinstalls the toolchain to the minimal profile, removing rustfmt/clippy/rust-docs.
- Later, ninja runs
rust_build_crosswhich deliberately omits--profile minimal— but the damage from step 4 is already done. bun run fmt:rust(cargo fmt --all, package.json:64) andbun run rust:clippy(package.json:73) — both new in this PR — would then fail with "component 'rustfmt' is not installed".
Addressing the refutation
One verifier argued this is a non-issue because rustup's --profile is additive (it specifies which components to ensure are present, not which to remove) and --force means "proceed even if components are unavailable", not "wipe and reinstall to exact spec". Under that reading, step 5 is a fast no-op for the extra components, and the tools.ts:575 comment "idempotent ~70ms when already installed" suggests the author tested exactly this.
That may well be correct — rustup's documented behavior leans that way. But it means the rust.ts:213-216 comment is wrong, and that comment is the only documented rationale for why rust_build_cross omits --profile minimal. So the inconsistency stands either way:
- If rust.ts's comment is right → tools.ts:581 risks stripping rustfmt/clippy on dev machines, and runs first.
- If rust.ts's comment is wrong → it's misleading documentation that should be removed (and
--profile minimalcould be added torust_build_crossfor symmetry / faster fresh-install).
Both code paths are new in this PR, so this is a self-introduced contradiction, not a pre-existing issue.
Why nothing prevents it
There's no test for component presence after configure, and CI agents are typically provisioned with minimal profiles anyway (so wouldn't notice). The two contexts also differ in intent: tools.ts:568-577 is written for link-only CI agents that may have no toolchain (where --profile minimal minimizes a fresh download), while rust.ts:213-217 is written for build agents that already have the default profile. Neither comment acknowledges that the tools.ts call also runs on the latter class of machine.
How to fix
Simplest: drop "--profile", "minimal" from tools.ts:581 to match rust.ts:222's reasoning. rustup toolchain install --force without --profile uses the configured default profile and won't narrow an existing install regardless of which interpretation of rustup's behavior is correct. The first-install download on a bare link-only CI agent gets slightly larger (default vs minimal profile), but that's a one-time cost per agent.
Alternatively, if the tools.ts behavior has been verified as safe, delete or correct the rust.ts:213-216 comment so the two sites don't contradict each other.
| export const allRustTargets = [ | ||
| "x86_64-unknown-linux-gnu", | ||
| "aarch64-unknown-linux-gnu", | ||
| "x86_64-unknown-linux-musl", | ||
| "aarch64-unknown-linux-musl", | ||
| "x86_64-apple-darwin", | ||
| "aarch64-apple-darwin", | ||
| "x86_64-pc-windows-msvc", | ||
| "aarch64-pc-windows-msvc", | ||
| "x86_64-unknown-freebsd", | ||
| "aarch64-linux-android", | ||
| ] as const; |
There was a problem hiding this comment.
🟡 allRustTargets omits x86_64-linux-android, but rust-toolchain.toml:26 lists it (with the comment "Mirrors allRustTargets in scripts/build/rust.ts"), rustTarget() produces it, and CI builds it (robobun shows a 🐧 x64-android - build-rust job on this PR). This means bun run rust:check-all skips a target CI builds — add "x86_64-linux-android" to the array. (Note: coderabbitai flagged this earlier but it was auto-marked "✅ Addressed in commits d5945cf to 808905b" — those commits are about bun.sys.sigaction and did not touch this array, so the omission persists at HEAD.)
Extended reasoning...
What the bug is
allRustTargets at scripts/build/rust.ts:106-117 is documented (lines 102-104) as "All target triples CI builds. Exposed so rust:check-all can iterate cargo check --target <t>". It contains 10 entries but omits x86_64-linux-android, even though that triple is part of the CI build matrix and is listed in the file that claims to mirror this array.
Evidence the omission is unintentional
Four independent sources establish that x86_64-linux-android is a supported, CI-built target that allRustTargets should include:
rust-toolchain.toml:26lists"x86_64-linux-android"intargets = [...], and the comment at line 13 explicitly says "MirrorsallRustTargetsin scripts/build/rust.ts minus aarch64-unknown-freebsd". So the toolchain file declares itself the mirror of this array — and it has 11 Tier 1/2 entries whileallRustTargetshas 10.rustTarget()atscripts/build/rust.ts:54producesx86_64-linux-androidwhencfg.abi === "android" && cfg.x64, so the build system models it as a valid target.rustCanCrossFromLinux()at line 96 returnstruefor it (cfg.linuxcovers android), so it cross-compiles from the shared Linux box like its aarch64 sibling.- CI actually builds it: the robobun comment on this PR shows a
🐧 x64-android - build-rustjob (Build #52979), proving the buildkite pipeline includes it.
The aarch64-linux-android entry is already in the array at line 116, so the asymmetry (aarch64-android present, x64-android absent) is clearly an oversight rather than a deliberate exclusion.
Why the existing coderabbitai comment doesn't cover it
CodeRabbit raised this exact issue in inline-comment 3211780323 (at line 117), but that comment is now marked resolved="true" with the footer "✅ Addressed in commits d5945cf to 808905b". Checking those commits: d5945cf is PR #30389 "Add bun.sys.sigaction" (Android bionic struct layout), and 808905b is an autofix.ci formatting commit. Neither touches scripts/build/rust.ts — git log -S 'x86_64-linux-android' -- scripts/build/rust.ts returns nothing. The "addressed" marker is a misattribution (coderabbit's auto-resolution heuristic matched on the word "android" in unrelated commits), and since the comment is now hidden as resolved it will not surface to the author. Re-flagging is therefore warranted.
Step-by-step proof
- Read
scripts/build/rust.ts:106-117at HEAD (0eae3f3): the array contains exactlyx86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu,x86_64-unknown-linux-musl,aarch64-unknown-linux-musl,x86_64-apple-darwin,aarch64-apple-darwin,x86_64-pc-windows-msvc,aarch64-pc-windows-msvc,x86_64-unknown-freebsd,aarch64-linux-android— 10 entries, nox86_64-linux-android. - Read
rust-toolchain.toml:20-32:targetscontains 11 entries includingx86_64-linux-androidat line 26. scripts/rust-check-all.tsimportsallRustTargetsand iterates it, callingcargo check --target <triple>for each. With the entry missing, a developer runningbun run rust:check-alllocally will check 10 targets and get a green result — but a#[cfg(all(target_os = "android", target_arch = "x86_64"))]compile error would still fail the🐧 x64-android - build-rustCI job.- Diff
rust-toolchain.tomltargets vsallRustTargets: the only delta (besides the documented Tier-3aarch64-unknown-freebsdexclusion) isx86_64-linux-android— present in the former, absent in the latter.
Impact
Low — this only affects the local bun run rust:check-all convenience script (scripts/rust-check-all.ts), not CI itself. CI builds x64-android via the buildkite pipeline (.buildkite/ci.mjs iterates buildPlatforms), not via this list, so any x64-android-specific compile error would be caught — just one push later than if rust:check-all had covered it locally. The bug is a tooling-consistency gap, not a correctness hole.
Note on the secondary aarch64-unknown-freebsd claim from the original report: that omission is intentional per rust-toolchain.toml:13-14 ("minus aarch64-unknown-freebsd, Tier 3 — no prebuilt rust-std"). rust-check-all.ts would need -Zbuild-std plumbing to handle Tier 3, which is a separate feature; the actionable fix here is just the x86_64-linux-android entry.
How to fix
Add one line to allRustTargets:
export const allRustTargets = [
// ...
"x86_64-unknown-freebsd",
"x86_64-linux-android",
"aarch64-linux-android",
] as const;| // rust-only/link-only don't run LUT codegen — skip the check so split-CI | ||
| // steps don't require perl on the rust cross-compile box. | ||
| if (cfg.mode === "full" || cfg.mode === "cpp-only") { |
There was a problem hiding this comment.
🔴 The comment at line 214 says "rust-only/link-only don't run LUT codegen", but this PR adds o.rustInputs.push(jssinkRs) at codegen.ts:954 (with an explicit comment at codegen.ts:951-953: "Without this, mode: rust-only ... never runs this edge"), so rust-only now does run the JSSink ninja edge, which spawns create-hash-table.ts → perl. Before this PR zig-only genuinely skipped JSSink (it had no zigInputs entry); the comment here was just text-replaced zig-only→rust-only without re-evaluating. Either add || cfg.mode === "rust-only" to line 216 so a missing perl on the rust cross-compile agent fails at configure time with the clear hint instead of a cryptic spawn error mid-codegen, or update the comment if perl is in fact guaranteed on those agents (docs/CI_RUST_MIGRATION.md §Agent image requirements doesn't mention it).
Extended reasoning...
What the bug is
configure.ts:211-222 contains a configure-time perl check, gated by cfg.mode === "full" || cfg.mode === "cpp-only". The justifying comment at lines 214-215 says "rust-only/link-only don't run LUT codegen — skip the check so split-CI steps don't require perl on the rust cross-compile box". That claim is false after this PR: rust-only mode now runs the JSSink codegen edge, and that edge invokes perl. The comment was mechanically renamed from zig-only → rust-only (diff at configure.ts shows a pure text replacement of zig-only/link-only → rust-only/link-only and zig cross-compile box → rust cross-compile box, with the condition on line 216 left untouched), but the underlying invariant the comment describes was simultaneously broken by another change in the same PR.
The specific code path that triggers it
emitRustOnly()atbun.ts:531-550is the rust-only mode entry point. It callsemitCodegen(n, cfg, sources)(line 545), which callsemitJsSink()(codegen.ts:302), then passescodegen.rustInputsascodegenInputstoemitRust()(line 548). Those become implicit inputs to the cargo ninja edge.emitJsSink()atcodegen.ts:920-955declares a single ninja edge with four outputs:JSSink.cpp,JSSink.h,JSSink.lut.h, andgenerated_jssink.rs. This PR adds line 954:o.rustInputs.push(jssinkRs)— brand new, not a rename (the pre-PRemitJsSinkpushed only tocppSources/cppHeaders, never tozigInputs). The PR's own comment at codegen.ts:951-953 is unambiguous about why: "Without this,mode: \"rust-only\"(CI's build-rust job, which compiles no C++ so nothing else pulls JSSink.cpp/.h) never runs this edge and cargo hits the missing file."- The JSSink ninja edge runs
generate-jssink.ts(codegen.ts:933-943), which at line 1210-1217 unconditionallyBun.spawnSyncscreate-hash-table.ts. create-hash-table.ts:23spawnsperl:spawn({ cmd: ["perl", create_hash_table, "-"], ... }).
So in rust-only mode: ninja sees the cargo edge depends on generated_jssink.rs → runs the JSSink edge → runs generate-jssink.ts → spawns create-hash-table.ts → spawns perl. If perl is absent, this fails mid-build.
Why existing code doesn't prevent it
The whole point of the configure-time check at lines 216-222 is to catch missing-perl early with a clear hint ("LUT codegen (create-hash-table.ts) needs perl. Install it: apt install perl / brew install perl"). But because the condition excludes rust-only, that mode skips the check entirely. The comment justifying the skip is now factually wrong — and is in fact directly contradicted by the comment this same PR adds at codegen.ts:951-953.
The PR's docs/CI_RUST_MIGRATION.md §"Agent image requirements" lists rustup, the pinned nightly, rust-src, and rustup target add — but does not mention perl. Per .buildkite/ci.mjs getRustAgent(), the rust-only step runs on (a) the shared r8g.2xlarge linux-aarch64 box for linux/freebsd/android targets, (b) the build-darwin queue for macOS, and (c) Azure Windows agents for Windows. Perl is ubiquitous on Linux/macOS base images so this likely won't fire there in practice, but Windows agents are the most plausible place for perl to be absent — and even on the Linux box, the design intent recorded in the (now-stale) comment was explicitly that the rust cross-compile box should not need perl.
Step-by-step proof
Before this PR:
emitJsSinkpushed nothing tozigInputs. The diff confirms this: theo.rustInputs.push(jssinkRs)line at codegen.ts:954 appears as a pure+addition (diff line 13013), not azigInputs→rustInputsrename like the other dozen sites in the same file.- Therefore
emitZigOnly()→emitCodegen()→codegen.zigInputsdid not include any JSSink output, so ninja never scheduled the JSSink edge inzig-onlymode. - The comment "zig-only/link-only don't run LUT codegen" was true.
After this PR:
emitJsSinkpushesjssinkRstorustInputs(line 954).emitRustOnly()passesrustInputsto the cargo edge (bun.ts:548).- Ninja schedules the JSSink edge in
rust-onlymode → perl is invoked. - The comment "rust-only/link-only don't run LUT codegen" is false.
- The condition at line 216 still excludes
rust-onlyfrom the perl check.
Impact
If perl is missing on a rust-only build agent, the build fails at codegen time with a spawn ENOENT-style error from create-hash-table.ts:23 — exactly the "fails cryptically" scenario the configure-time check exists to prevent. The user sees no hint about installing perl. This is most likely to bite on Windows build-rust agents (which run natively per getRustAgent()) where perl is not a default system component, or on any minimal container image used for the linux cross-compile box.
How to fix
Either:
(a) Add || cfg.mode === "rust-only" to the condition on line 216, and update the comment to reflect that only link-only skips LUT codegen:
// link-only doesn't run LUT codegen — skip the check so the link-only CI
// step doesn't require perl. rust-only DOES need it (JSSink codegen → perl).
if (cfg.mode === "full" || cfg.mode === "cpp-only" || cfg.mode === "rust-only") {(b) If perl is in fact guaranteed on every build-rust agent, just fix the stale comment at lines 214-215 (and ideally add perl to docs/CI_RUST_MIGRATION.md §Agent image requirements so the guarantee is documented).
Option (a) is the conservative fix — the check is cheap (findSystemTool("perl")) and the failure mode it guards against is real.
| # Mirrors `allRustTargets` in scripts/build/rust.ts minus aarch64-unknown-freebsd | ||
| # (Tier 3 — no prebuilt rust-std, cargo `-Zbuild-std` builds it from rust-src). |
There was a problem hiding this comment.
🟡 nit: This comment says the targets list "Mirrors allRustTargets in scripts/build/rust.ts minus aarch64-unknown-freebsd", but it doesn't — this file includes x86_64-linux-android (line 26) while allRustTargets (rust.ts:106-117) omits it, and allRustTargets doesn't contain aarch64-unknown-freebsd to subtract in the first place. Since rustTarget() (rust.ts:54) returns x86_64-linux-android for real builds, bun run rust:check-all silently skips a supported target. Note the related coderabbit comment on rust.ts:117 was marked "Addressed in commits d5945cf to 808905b" / resolved, but at HEAD allRustTargets still has only 10 entries — that fix did not land. Either add x86_64-linux-android to allRustTargets or fix this comment.
Extended reasoning...
What the bug is
rust-toolchain.toml lines 13-14 carry a comment claiming the targets = [...] list "Mirrors allRustTargets in scripts/build/rust.ts minus aarch64-unknown-freebsd (Tier 3 — no prebuilt rust-std)." This claim is doubly inaccurate:
rust-toolchain.tomlline 26 includes"x86_64-linux-android", butallRustTargetsatscripts/build/rust.ts:106-117does not — it lists onlyaarch64-linux-android. So the toolchain file has a target that the "mirrored" array lacks.allRustTargetsdoes not containaarch64-unknown-freebsdin the first place, so describing the relationship as "allRustTargetsminus aarch64-unknown-freebsd" is also wrong — there is nothing to subtract.
The toolchain file has 11 entries; allRustTargets has 10; the symmetric difference is {x86_64-linux-android}, not {aarch64-unknown-freebsd}.
Why it matters (the functional gap behind the comment drift)
rustTarget() at scripts/build/rust.ts:47-57 returns `${arch}-linux-android` for cfg.abi === "android" with either arch — so x86_64-linux-android is a real build target the system can produce. allRustTargets is what scripts/rust-check-all.ts (the bun run rust:check-all script) iterates, so cargo check --target x86_64-linux-android is silently never run by that dev convenience script even though (a) rustup installs std for it via rust-toolchain.toml, and (b) the build system can target it.
Step-by-step proof
rust-toolchain.toml:13-14:# MirrorsallRustTargetsin scripts/build/rust.ts minus aarch64-unknown-freebsdrust-toolchain.toml:20-32: 11 targets, including line 26"x86_64-linux-android".scripts/build/rust.ts:106-117:allRustTargetshas exactly 10 string literals;grep x86_64-linux-androidover those lines → 0 matches;grep aarch64-unknown-freebsd→ 0 matches.scripts/build/rust.ts:54:if (cfg.abi === "android") return \${arch}-linux-android`;— withcfg.x64 === truethis yieldsx86_64-linux-android`.scripts/rust-check-all.tsiteratesallRustTargets→ never checksx86_64-linux-android.- Therefore the comment's "mirrors … minus …" claim is false in both directions, and a supported target is skipped by
rust:check-all.
Relationship to the (incorrectly resolved) coderabbit comment
CodeRabbit inline-comment 3211780323 on scripts/build/rust.ts:117 already suggested adding both x86_64-linux-android and aarch64-unknown-freebsd to allRustTargets. That comment is marked resolved=true with "✅ Addressed in commits d5945cf to 808905b" — but at PR HEAD (4aef65c3), allRustTargets still has exactly 10 entries with neither triple present. Inspection of commit 808905b shows it is an autofix.ci formatting commit touching only docs/.rust-rewrite-verified-claims.md, docs/RUST_IDIOMS_AUDIT.md, and docs/zig-restructure-plan.md — not scripts/build/rust.ts. So the coderabbit auto-resolution heuristic misfired and the underlying issue is still open.
This finding is therefore (a) framed on a different file (rust-toolchain.toml's stale comment, both files added by this PR) and (b) flags that a previously-marked-resolved review comment did not actually land.
Impact
Minor. This is comment-accuracy drift plus a coverage gap in a developer-convenience script (rust:check-all), not a build-correctness issue — CI builds drive rustTarget(cfg) per-platform rather than iterating allRustTargets, so production builds are unaffected. Hence nit severity.
How to fix
Either:
- Add
"x86_64-linux-android"toallRustTargetsinscripts/build/rust.ts(the cleaner fix, sincerustTarget()already supports it; optionally also add"aarch64-unknown-freebsd"so the comment's "minus" clause becomes accurate), or - Rewrite the
rust-toolchain.toml:13-14comment to accurately describe the actual relationship between the two lists.
🔬 also observed by coderabbitai
| export * as Three1 from 'three-1'; | ||
| export * as Three2 from 'three-2'; | ||
| export * as Three3 from 'three-3'; | ||
| export * as Three4 from 'three-4'; | ||
| export * as Three5 from 'three-5'; | ||
| export * as Three6 from 'three-6'; | ||
| export * as Three7 from 'three-7'; | ||
| export * as Three8 from 'three-8'; | ||
| export * as Three9 from 'three-9'; | ||
| export * as Three10 from 'three-10'; |
There was a problem hiding this comment.
🟡 This replaces the stub with 100 export * as ThreeN from 'three-N' lines, but bench/bundle/package.json only declares "three": "^0.184.0" — there are no three-1..three-100 packages, no tsconfig path mappings, and no setup step that creates them (run-bench.sh clones a separate esbuild fork and never bundles this file). The README also says "10 copies" but the file has 100. Either revert to a stub, add a setup step that materializes node_modules/three-N, or align the count with the README.
Extended reasoning...
What the issue is
bench/bundle/index.ts was changed from console.log("Hello via Bun!") to 100 namespace re-exports:
export * as Three1 from 'three-1';
export * as Three2 from 'three-2';
...
export * as Three100 from 'three-100';None of the specifiers three-1 through three-100 resolve. bench/bundle/package.json declares exactly one dependency ("three": "^0.184.0"), bun.lockb locks only three, tsconfig.json has no paths mapping, and .gitignore lists only esbuild (the cloned dir from run-bench.sh) — no generated three-N directories. There is no setup script in this directory or anywhere in the repo that creates the three-N packages.
There is also a count mismatch: bench/bundle/README.md says "The benchmark bundles 10 copies of the large three.js", but the file has 100 imports.
Why nothing in the repo makes this work
The intent appears to follow esbuild's bench-three pattern, where a Makefile copies node_modules/three → node_modules/three-N N times before bundling. But run-bench.sh here is just:
git clone git@github.com:colinhacks/esbuild.git
cd esbuild
make bench-threeIt clones a separate esbuild fork and runs make bench-three inside that clone — it never references bench/bundle/index.ts, never runs bun build against it, and never sets up three-N packages in this directory's node_modules. So index.ts is dead with respect to the documented workflow.
Step-by-step proof
cd bench/bundle && bun install→ installs onlythreeandbun-types(per package.json + lockfile).ls node_modules/ | grep three→ onlythree, nothree-1..three-100.bun build index.ts→ 100 errors of the formerror: Could not resolve: "three-1"(one per import)../run-bench.sh→ clonescolinhacks/esbuild, cds into it, runsmake bench-threethere.index.tsin the parent directory is never touched.
Impact (and why this is a nit)
This is bench-fixture scaffolding, not production code. Nothing in CI, the build system, or the test suite references bench/bundle/index.ts. The documented entrypoint (./run-bench.sh) bypasses it entirely. Before this PR the file was console.log("Hello via Bun!") — a bun init placeholder that was equally non-functional as a bundler benchmark. So the practical regression is: a dead stub became a broken dead stub. The only people affected are those who cd bench/bundle && bun build index.ts directly, which the README does not instruct anyone to do.
Addressing the objection that this is a duplicate
CodeRabbit already flagged this same issue (inline comment 3211775312 on bench/bundle/index.ts:100). That observation is correct and is credited below. The finding itself is real; the duplication is why it's filed as a corroboration rather than a fresh report, and the negligible blast radius is why it's filed as a nit rather than a blocking issue.
How to fix
Any of:
- (a) Revert
index.tsto a stub (or delete it) sincerun-bench.shdoesn't use it. - (b) Add a setup step that materializes the copies, e.g.
for i in $(seq 1 10); do cp -r node_modules/three node_modules/three-$i; done, and trim the file to 10 imports to match the README. - (c) Change all 100 imports to
from 'three'(though re-exporting the same module 100× under different namespaces doesn't replicate esbuild's bench-three semantics, where each copy is a distinct module graph).
🔬 also observed by coderabbitai
| join(cwd, "src/jsc/modules"), | ||
| join(cwd, "src/js/builtins"), | ||
| join(cwd, "src/napi"), | ||
| join(cwd, "src/runtime/napi"), |
There was a problem hiding this comment.
🟡 This updates the napi include path to src/runtime/napi (the directory move — src/napi/ no longer exists), but scripts/run-clang-format.sh:35 still excludes the old src/napi/ path, so vendored Node-API headers (node_api.h, js_native_api.h, etc.) under src/runtime/napi/ will now be picked up and reformatted by autofix-ci. Note line 34 was already updated to src/runtime/ffi/, so napi was missed in the same sweep; .github/workflows/CLAUDE.md:24 also still documents the stale src/napi/ path.
Extended reasoning...
What the bug is
The napi directory has moved from src/napi/ to src/runtime/napi/ (this PR's own docs/CYCLEBREAK.md documents "napi become modules of bun_runtime"). This PR updates scripts/build/flags.ts:1126 accordingly:
- join(cwd, "src/napi"),
+ join(cwd, "src/runtime/napi"),However, scripts/run-clang-format.sh:35 still excludes the old path:
if [[ "$file" =~ src/runtime/ffi/ffi- ]] || \
[[ "$file" =~ src/napi/ ]] || \ # ← stale; matches nothing
[[ "$file" =~ src/jsc/bindings/libuv/ ]] || \Line 34 was already updated from src/bun.js/api/ffi- → src/runtime/ffi/ffi- in the same restructure, so napi was simply missed in that sweep. Additionally, .github/workflows/CLAUDE.md:24 still documents "src/napi/ - Node API headers (third-party)".
Step-by-step proof
src/napi/does not exist at HEAD:ls src/napi/→ "No such file or directory".src/runtime/napi/contains vendored third-party headers:js_native_api.h,js_native_api_types.h,node_api.h,node_api_types.h— these are upstream Node-API headers, not Bun-authored code.- The exclusion no longer matches: bash
[[ "$file" =~ src/napi/ ]]treats the RHS as a regex;src/napi/is a literal substring that does not appear insrc/runtime/napi/node_api.h(the path segment is/napi/, but the prefixsrc/immediately before it is required by the pattern). So thecontinuenever fires for these files. - The
finddiscovers them:run-clang-format.sh:43runsfind src packages -type f \( -name "*.h" -o -name "*.hpp" \)with no-not -pathclause coveringsrc/runtime/napi/, so all four headers enter the loop and reachFILES_TO_FORMAT. - CI runs it and auto-commits:
.github/workflows/format.yml:54invokes./scripts/run-clang-format.sh format, and the workflow ends withautofix-ci/action, which commits any resulting diff. - It has likely already fired:
git log --diff-filter=A -- src/runtime/napi/node_api.hshows the file was added at its new path by commitf0a32bf5 "[autofix.ci] apply automated fixes"— i.e., autofix-ci has already touched the vendored header at the new location.
Why nothing prevents it
The exclusion list in run-clang-format.sh is hand-maintained string matching with no validation that the excluded paths exist. When the directory moved, the regex silently became a no-op. The adjacent src/runtime/ffi/ entry on line 34 was updated, demonstrating the intent to keep this list in sync — napi was simply overlooked.
Impact
clang-format will reformat upstream Node-API headers to Bun's .clang-format style, and autofix-ci will commit the result. This is not a runtime or build correctness issue, but it (a) pollutes git blame/history on vendored files, (b) creates spurious diffs against upstream Node.js when those headers are next synced, and (c) generates autofix-ci commit noise on every PR that touches anything triggering the format workflow. run-clang-format.sh itself is not in this PR's changed-files set (the directory move appears to predate the diff), so this is borderline pre-existing — but since this PR is explicitly sweeping path references for the napi move (flags.ts:1126, CYCLEBREAK.md) and already updated the sibling ffi exclusion on line 34, it's the natural place to complete the sweep.
How to fix
Two one-line changes:
# scripts/run-clang-format.sh:35
- [[ "$file" =~ src/napi/ ]] || \
+ [[ "$file" =~ src/runtime/napi/ ]] || \# .github/workflows/CLAUDE.md:24
-- `src/napi/` - Node API headers (third-party)
+- `src/runtime/napi/` - Node API headers (third-party)| "rust:check-all": "bun scripts/rust-check-all.ts", | ||
| "rust:clippy": "cargo clippy --workspace --keep-going", | ||
| "codegen:string-maps": "for f in src/**/*.string-map.ts; do bun src/codegen/generate-string-map.ts \"$f\" \"${f%.string-map.ts}.generated.rs\"; done", | ||
| "codegen:verify": "bun run codegen:string-maps && git diff --exit-code 'src/**/*.generated.rs' || (echo '\\n*.generated.rs is stale — run `bun run codegen:string-maps` and commit the result.' >&2; exit 1)", |
There was a problem hiding this comment.
🟡 nit: Because A && B || C evaluates as (A && B) || C, if bun run codegen:string-maps itself fails (e.g. syntax error in a .string-map.ts), the || branch still fires and prints "*.generated.rs is stale — run `bun run codegen:string-maps`" — which is misleading since that command just failed for a different reason. The generator's own error appears above it in the log so this only affects diagnostic clarity; consider bun run codegen:string-maps || exit 1; git diff --exit-code ... || (echo stale; exit 1) to distinguish the two cases.
Extended reasoning...
What the bug is
The new codegen:verify script at package.json:75 is:
bun run codegen:string-maps && git diff --exit-code 'src/**/*.generated.rs' || (echo '...stale...' >&2; exit 1)
In POSIX sh, && and || have equal precedence and are left-associative, so this parses as (A && B) || C. The intent is for C (the "stale" message) to fire only when B (git diff --exit-code) detects uncommitted regenerated output. But C also fires whenever A (the generator) fails, since a non-zero A short-circuits the &&, making the whole left side non-zero, which triggers ||.
Step-by-step proof
- Suppose
src/foo/bar.string-map.tshas a syntax error, orsrc/codegen/generate-string-map.tsthrows. bun run codegen:string-mapsexits non-zero (theforloop in that script propagates the last iteration's exit code, and bun's script runner propagates non-zero).Ais non-zero →A && Bshort-circuits without runninggit diff→ the compound(A && B)is non-zero.|| Cfires: stderr gets*.generated.rs is stale — runbun run codegen:string-mapsand commit the result.- CI fails with exit 1 (correct behavior), but the trailing diagnostic line is factually wrong: the files aren't stale, the generator crashed — and the suggested remedy is the very command that just failed.
Why this is only a nit
Three mitigating factors keep the impact small:
- The real error is still visible. The generator's own stderr (stack trace, syntax error, etc.) prints before the misleading "stale" line. A developer reading the CI log top-to-bottom sees the actual cause first; the bad message is a confusing footer, not the only signal.
- CI correctness is unaffected. Exit code is 1 either way; the step fails as it should.
- The suggested remedy isn't harmful. Running
bun run codegen:string-mapslocally (as the message says) reproduces the same generator error, so the developer lands on the real cause anyway — just via a slightly confusing route.
This is also a very common npm-script idiom (cmd && check || fail), and most uses of it don't bother distinguishing the two failure modes. It's flagged here only because (a) the message is unusually specific about which failure occurred ("is stale"), making it actively wrong rather than merely vague, and (b) this script is newly added in this PR and wired into CI at .github/workflows/format.yml:113, so it's cheap to fix now.
Addressing the counter-argument
One could argue this is below the bar for a 300-file PR. That's fair — it's purely a UX wart in an edge case (broken generator), not a correctness issue. But the fix is a one-line restructure with no downside, and the script's whole purpose is to give a clear signal about codegen state, so having it emit a wrong diagnosis in one of its two failure modes seems worth the nit.
How to fix
Split the chain so generator failure exits before the staleness check:
"codegen:verify": "bun run codegen:string-maps || exit 1; git diff --exit-code 'src/**/*.generated.rs' || (echo '\\n*.generated.rs is stale — run `bun run codegen:string-maps` and commit the result.' >&2; exit 1)"Or equivalently with set -e, or by grouping with braces: { A && B; } || C doesn't help (same evaluation), but A; B || C with set -e does.
| - name: Verify checked-in codegen | ||
| # `*.generated.rs` are deterministic outputs of `*.string-map.ts`; the | ||
| # source `.ts` is the truth. This step regenerates and fails if the | ||
| # committed `.generated.rs` is stale, so a forgotten regen surfaces in | ||
| # CI rather than as a confusing behaviour diff later. | ||
| run: | | ||
| bun run codegen:verify | ||
| - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 |
There was a problem hiding this comment.
🔴 Running codegen:verify before autofix-ci/action defeats autofix: when .generated.rs is stale, codegen:string-maps writes the corrected files to disk and then the step exits 1, so the autofix-ci step (which has no if: always()) is skipped — the regenerated files are never committed, and any prettier/clang-format/zig-fmt fixes from the "Format Code" step are also lost. Either replace codegen:verify with codegen:string-maps here (regenerate-only, exit 0, let autofix commit — matching how the formatters work) or move this step after the autofix-ci action as a pure verification gate.
Extended reasoning...
What the bug is
The new step at format.yml:107-113 runs bun run codegen:verify immediately before the autofix-ci/action step at line 114. Per package.json:75, codegen:verify is:
bun run codegen:string-maps && git diff --exit-code 'src/**/*.generated.rs' || (echo '...stale...'; exit 1)
So when checked-in codegen is stale, this step (1) regenerates the correct files to disk via codegen:string-maps, (2) sees a non-empty git diff, and (3) exits 1. The fix is literally sitting in the working tree when the job dies.
The specific code path
Format Codestep (lines 38-103) runs prettier / clang-format / zig fmt, which write fixes to disk and exit 0 — they rely on autofix-ci to commit.Verify checked-in codegen(lines 107-113) runs. If any.generated.rsis stale,codegen:string-mapswrites the regenerated content, thengit diff --exit-codefails, then the step exits 1.autofix-ci/action(line 114) has noif:condition, so per GitHub Actions' defaultif: success()semantics it is skipped when any prior step failed.- Result: neither the regenerated
.generated.rsfiles nor the unrelated formatting fixes from step 1 are committed back to the PR.
Why existing code doesn't prevent it
The inline comment (lines 108-111) shows the author intended a hard gate ("fails if the committed .generated.rs is stale, so a forgotten regen surfaces in CI"). That intent IS achieved — the job goes red. But the implementation is inconsistent with the rest of this workflow: the Format Code step (lines 38-103) follows the autofix.ci pattern of "write the fix, exit 0, let the action commit it". This new step instead writes the fix and then fails, throwing away both its own work and any pending formatting changes.
The pre-existing Ban Words step at line 104 also sits before autofix-ci and can fail, but ban-words has no autofixable output (you can't auto-fix a banned word), so its placement is sensible. By contrast, stale codegen is the expected common case (someone edits a .string-map.ts and forgets to regen), and the script literally produces the fix on disk — making it the perfect candidate for autofix rather than a hard fail.
Step-by-step proof
Concrete scenario:
- A contributor edits
src/foo.string-map.tsand also makes a TypeScript change with bad formatting. They forget to runbun run codegen:string-maps. - PR opened →
autofix.ciworkflow runs. Format Codestep: prettier reformats the .ts file → working tree now has formatting fix → exit 0.Ban Wordsstep: passes → exit 0.Verify checked-in codegenstep:codegen:string-mapsregeneratessrc/foo.generated.rs→ working tree now has the correct codegen →git diff --exit-code 'src/**/*.generated.rs'exits 1 →|| (echo ...; exit 1)→ step fails.autofix-ci/action: defaultif: success()evaluates false → skipped.- Job fails. The contributor sees a red X with "*.generated.rs is stale" but gets neither the codegen fix nor the formatting fix auto-pushed. They must manually run
codegen:string-mapsAND prettier locally, even though CI already computed both fixes.
Impact
This is a CI ergonomics regression introduced by this PR, not a runtime bug. The primary intent (surface staleness) works, but: (a) the workflow is named autofix.ci and the job is named autofix, yet this step makes an autofixable problem non-autofixable; (b) it has collateral damage — when codegen is stale, unrelated prettier/clang-format/zig-fmt fixes are also blocked from being committed, which is a regression in pre-existing autofix behavior.
How to fix
Two clean options:
(a) Match the existing Format Code pattern — change line 113 to bun run codegen:string-maps (regenerate only, exit 0). The autofix-ci action then commits the regenerated files alongside any formatting fixes. Optionally add a second bun run codegen:verify step after autofix-ci as a belt-and-suspenders gate (it would only fail if codegen is non-deterministic).
(b) Keep codegen:verify but move the step to after autofix-ci/action (and add the regenerate-only step before it), so formatting fixes always get committed and the verify step acts as a pure post-commit gate.
Option (a) is the smaller diff and matches the workflow's existing design.
| "codegen:string-maps": "for f in src/**/*.string-map.ts; do bun src/codegen/generate-string-map.ts \"$f\" \"${f%.string-map.ts}.generated.rs\"; done", | ||
| "codegen:verify": "bun run codegen:string-maps && git diff --exit-code 'src/**/*.generated.rs' || (echo '\\n*.generated.rs is stale — run `bun run codegen:string-maps` and commit the result.' >&2; exit 1)", |
There was a problem hiding this comment.
🟡 The for f in src/**/*.string-map.ts loop here relies on **, but bun-run scripts execute via bash without shopt -s globstar, so ** is treated as * and only matches one directory level (src/*/*.string-map.ts). It happens to work today because the only .string-map.ts is at depth 1, but any future deeper file (e.g. src/a/b/foo.string-map.ts) would be silently skipped — and codegen:verify (now wired into CI by this PR) would not catch the staleness since the unregenerated .generated.rs produces no git diff. Fix: prefix with shopt -s globstar && for f in ..., or use find src -name "*.string-map.ts".
Extended reasoning...
What the bug is
The codegen:string-maps script in package.json:74 is:
"codegen:string-maps": "for f in src/**/*.string-map.ts; do bun src/codegen/generate-string-map.ts \"$f\" \"${f%.string-map.ts}.generated.rs\"; done",The src/**/*.string-map.ts pattern is intended to recurse arbitrarily deep, but ** only does that when bash's globstar shell option is enabled. bun run on POSIX executes scripts via the system shell (src/runtime/cli/run_command.rs:161 has SHELLS_TO_SEARCH = ["bash", "sh", "zsh"], and src/options_types/Context.rs:302 defaults use_system_shell = !cfg!(windows)), so on Linux/macOS CI this runs as bash -c '<script>'. Bash's globstar is off by default in non-interactive shells, so src/**/*.string-map.ts is parsed identically to src/*/*.string-map.ts — exactly one directory level under src/.
Why it works today (by coincidence)
find src -name '*.string-map.ts' returns exactly one file: src/js_parser/defines_table.string-map.ts, which is at depth 1 under src/ and therefore matches src/*/*.string-map.ts. So the script currently regenerates everything it should.
Why it's still a bug
This PR wires codegen:verify into CI via .github/workflows/format.yml with the explicit purpose (per the inline comment) that "a forgotten regen surfaces in CI rather than as a confusing behaviour diff later." But codegen:verify is:
"codegen:verify": "bun run codegen:string-maps && git diff --exit-code 'src/**/*.generated.rs' || (...)"The git diff pathspec is quoted, so git handles ** (correctly, recursively). But the regenerate step that precedes it would skip any deeper file. So for a hypothetical src/css/properties/foo.string-map.ts:
codegen:string-mapsdoes not regeneratesrc/css/properties/foo.generated.rs(depth 2, doesn't matchsrc/*/*).git diff --exit-code 'src/**/*.generated.rs'checks the file but sees no change (it wasn't touched).- CI passes ✅ even though the checked-in
.generated.rsis stale relative to the.string-map.tssource.
The verify step's whole stated purpose is silently defeated for exactly the case it's meant to catch.
Step-by-step proof
$ bash -c 'for f in src/**/*.string-map.ts; do echo "$f"; done'
src/js_parser/defines_table.string-map.ts # depth 1: matches
$ bash -c 'shopt -s globstar; for f in src/**/*.string-map.ts; do echo "$f"; done'
src/js_parser/defines_table.string-map.ts # same (only file)
# Simulate a depth-2 file:
$ mkdir -p /tmp/t/src/a/b && touch /tmp/t/src/a/b/x.string-map.ts /tmp/t/src/a/y.string-map.ts
$ bash -c 'cd /tmp/t && for f in src/**/*.string-map.ts; do echo "$f"; done'
src/a/y.string-map.ts # depth-2 file SKIPPED
$ bash -c 'cd /tmp/t && shopt -s globstar && for f in src/**/*.string-map.ts; do echo "$f"; done'
src/a/b/x.string-map.ts
src/a/y.string-map.ts # both found
Why nothing else catches it
The actual ninja build (emitStringMaps in scripts/build/codegen.ts, fed by sources.stringMaps from scripts/glob-sources.ts) uses a proper recursive glob and is unaffected — so the product is correct. Only this convenience script and the CI verify step are wrong, which is precisely why staleness would go undetected: the build regenerates into build/<profile>/codegen/, not into src/, so it never updates the checked-in .generated.rs.
Impact
None today (the only .string-map.ts is at depth 1). This is a latent/forward-looking robustness issue: the pattern looks recursive but isn't, and the CI guard this PR adds would silently miss the very staleness it documents itself as catching, the moment a second .string-map.ts lands at depth ≥2.
Fix
Either of:
"codegen:string-maps": "shopt -s globstar && for f in src/**/*.string-map.ts; do ...; done"or (portable to sh/zsh):
"codegen:string-maps": "find src -name '*.string-map.ts' | while read -r f; do bun src/codegen/generate-string-map.ts \"$f\" \"${f%.string-map.ts}.generated.rs\"; done"| * build still uses `dev`. RelWithDebInfo / MinSizeRel collapse to `release` — | ||
| * cargo's stock release already keeps debuginfo (`debug = 1` is the workspace | ||
| * default), and we don't ship a `MinSizeRel` Rust path yet. |
There was a problem hiding this comment.
🟡 The parenthetical says "(debug = 1 is the workspace default)", but [profile.release] in Cargo.toml:141 actually sets debug = "line-tables-only" — a lower debuginfo level than 1 (limited). The broader point (release retains debuginfo, so RelWithDebInfo can collapse to it) is still correct; just the specific value cited is wrong.
Extended reasoning...
What the bug is
The JSDoc on cargoProfile() at scripts/build/rust.ts:64-66 reads:
RelWithDebInfo / MinSizeRel collapse to release — cargo's stock release already keeps debuginfo (
debug = 1is the workspace default) so a separate "with-debinfo" profile is unnecessary.
The parenthetical asserts that the workspace [profile.release] sets debug = 1. It does not — Cargo.toml:141 (also new in this PR) sets:
[profile.release]
lto = "fat"
codegen-units = 1
debug = "line-tables-only"
strip = "none"In Cargo's debuginfo levels these are distinct settings: "none" (0) < "line-tables-only" < "limited" (1) < "full" (2). line-tables-only emits only file:line mappings for backtraces; debug = 1 ("limited") additionally includes type information and some variable info. So the comment cites the wrong value.
Why nothing prevents it
This is a JSDoc comment — neither TypeScript nor cargo validates that the prose matches Cargo.toml. Both files are new in this PR, so the drift was introduced together rather than one going stale. The actual code on the next line (return cfg.buildType === "Debug" ? dev : release;) is unaffected: it just selects the cargo profile name, and whatever Cargo.toml configures for that profile is what gets used.
Step-by-step proof
- Read
scripts/build/rust.ts:65→"(debug = 1is the workspace default)". - Read
Cargo.toml:141→debug = "line-tables-only". - Per the Cargo reference on profile.debug,
1/"limited"and"line-tables-only"are different levels;line-tables-onlyis strictly less debuginfo than1. - Therefore the parenthetical is factually incorrect about what the workspace configures.
The comment's conclusion — that release already retains debuginfo, so collapsing RelWithDebInfo→release doesn't lose backtrace info — remains valid: line-tables-only is non-zero debuginfo and is sufficient for symbolicated backtraces. Only the specific value cited is wrong.
Impact
Zero runtime/build impact; this is purely a comment-accuracy issue. The minor harm is that a reader trusting the JSDoc might assume a RelWithDebInfo build carries debug = 1-level info (e.g. some type info for variable inspection in a debugger) when it actually only has line tables. This is the same comment-vs-Cargo.toml drift theme as the already-posted comments on Cargo.toml:152 (release-profiling no-op) and scripts/build/profiles.ts:112 (debug-assertions claim).
How to fix
Change the parenthetical at line 65 to match the actual config:
* already keeps debuginfo (`debug = "line-tables-only"` is the workspace default) so aOr, if debug = 1 was the intended workspace setting for RelWithDebInfo-equivalent builds, change Cargo.toml:141 instead — but given the explicit choice of line-tables-only (smaller binaries, sufficient for backtraces), updating the comment is almost certainly the right fix.
| "codegen:string-maps": "for f in src/**/*.string-map.ts; do bun src/codegen/generate-string-map.ts \"$f\" \"${f%.string-map.ts}.generated.rs\"; done", | ||
| "codegen:verify": "bun run codegen:string-maps && git diff --exit-code 'src/**/*.generated.rs' || (echo '\\n*.generated.rs is stale — run `bun run codegen:string-maps` and commit the result.' >&2; exit 1)", |
There was a problem hiding this comment.
🟡 nit: for f in src/**/*.string-map.ts relies on globstar, but bun run executes package.json scripts via the system shell on POSIX (use_system_shell = !isWindows), and neither dash nor bash-without-shopt -s globstar treats ** as recursive — it expands the same as src/*/*.string-map.ts. It works today only because the sole file (src/js_parser/defines_table.string-map.ts) happens to be at depth 2; a future .string-map.ts at any other depth would be silently skipped by both regen and the new CI codegen:verify step (git diff would show no change → false pass). Use find src -name "*.string-map.ts" or Bun.Glob instead.
Extended reasoning...
What the bug is
The new codegen:string-maps script (package.json:74) is written as:
for f in src/**/*.string-map.ts; do bun src/codegen/generate-string-map.ts "$f" "${f%.string-map.ts}.generated.rs"; doneThe ** here is intended as a recursive glob. However, bun run <script> on POSIX executes package.json scripts via the system shell, not Bun Shell — src/options_types/Context.rs:302 sets use_system_shell: !cfg!(windows), and run_command.rs resolves the shell via findShell() (search order: bash → sh → zsh) and invokes <shell> -c <script>. Neither POSIX sh (dash on Ubuntu CI), bash without shopt -s globstar, nor zsh in sh-emulation mode treats ** as recursive; in all of them ** is exactly equivalent to *, so the glob expands as src/*/*.string-map.ts — files at exactly depth 2 only.
Why it works today
find src -name '*.string-map.ts' returns exactly one file: src/js_parser/defines_table.string-map.ts, which is at depth 2 (src/<dir>/<file>). So the non-recursive glob happens to match it. Empirically:
$ bash -c 'for f in src/**/*.string-map.ts; do echo "$f"; done'
src/js_parser/defines_table.string-map.ts
The failure mode
This PR also wires codegen:verify (package.json:75) into CI via .github/workflows/format.yml ("Verify checked-in codegen"). That step runs codegen:string-maps and then git diff --exit-code 'src/**/*.generated.rs' to catch stale checked-in codegen.
If someone later adds e.g. src/css/properties/colors.string-map.ts (depth 3) or src/foo.string-map.ts (depth 1):
bun run codegen:string-mapssilently skips it — theforloop never sees the file, no error is raised.bun run codegen:verifyregenerates nothing for that file →git diffsees no change → exit 0, CI passes.- The checked-in
.generated.rsgoes stale with no signal whatsoever.
This is the worst kind of footgun: the CI step whose explicit purpose (per its own comment: "so a forgotten regen surfaces in CI rather than as a confusing behaviour diff later") is to catch staleness would itself silently miss the stale file. Note the git diff pathspec 'src/**/*.generated.rs' would catch a depth-3 file (git pathspecs do support recursive **), but only if the regen step actually touched it — which it won't.
Step-by-step proof
bun runon Linux/macOS resolves a system shell (run_command.rs:161:SHELLS_TO_SEARCH = ["bash", "sh", "zsh"]) and runs<shell> -c '<script>'.- In bash,
globstaris off by default (shopt globstar→off); perman bash: "If set, the pattern**used in a pathname expansion context will match all files and zero or more directories and subdirectories. If not set,**behaves like*." In dash there is no globstar at all. - So
src/**/*.string-map.ts≡src/*/*.string-map.ts→ matchessrc/<one-dir>/<file>.string-map.tsonly. - Create
src/css/values/x.string-map.ts(depth 3). Runbun run codegen:string-maps→ loop body never executes for it. Runbun run codegen:verify→ exit 0.
How to fix
Replace the glob with portable recursion, e.g.:
"codegen:string-maps": "find src -name '*.string-map.ts' -print0 | xargs -0 -I{} sh -c 'bun src/codegen/generate-string-map.ts \"{}\" \"${0%.string-map.ts}.generated.rs\"' {}"or, more cleanly, use Bun's own glob (which does support ** recursively and is shell-independent):
"codegen:string-maps": "bun -e 'for (const f of new Bun.Glob(\"src/**/*.string-map.ts\").scanSync()) await Bun.$\`bun src/codegen/generate-string-map.ts ${f} ${f.replace(/\\.string-map\\.ts$/, \".generated.rs\")}\`'"or simply prefix with shopt -s globstar 2>/dev/null; (bash-only, breaks on dash/Alpine).
Severity is nit: nothing is broken today (only one file, at the depth that happens to match), this is dev/CI tooling not shipped code, and the fix is trivial. But it's worth fixing in this PR since both the script and the CI step that depends on it are newly introduced here, and the failure mode is completely silent.
| // rust_build | ||
| registerRustRules(n, cfg); | ||
|
|
||
| // shim_dylib (darwin+asan only) |
There was a problem hiding this comment.
🟡 nit: This PR's shims.ts change adds a second rule shim_crt_decompress (gated on needsMuslCrtDecompress: linux+musl+rust-lld) to registerShimRules(), restructuring it from single-purpose into two conditional branches — but the call-site comment here still says // shim_dylib (darwin+asan only). The "(darwin+asan only)" parenthetical is now actively misleading; consider // shim_dylib (darwin+asan), shim_crt_decompress (musl+rust-lld).
Extended reasoning...
What the bug is
scripts/build/rules.ts:55 carries a one-line comment describing what registerShimRules() registers:
// shim_dylib (darwin+asan only)
registerShimRules(n, cfg);Before this PR, that comment was exactly right — registerShimRules opened with if (!(cfg.darwin && cfg.asan)) return; and only ever defined the shim_dylib rule. This PR's diff to scripts/build/shims.ts removes that early return and restructures the function into two independent conditional branches:
if (cfg.darwin && cfg.asan)→ registersshim_dylib(line 59)if (needsMuslCrtDecompress(cfg))→ registersshim_crt_decompress(line 69), whereneedsMuslCrtDecompressiscfg.linux && cfg.abi === "musl" && cfg.rustLld !== undefined && cfg.ld === cfg.rustLld
The second branch is brand new in this PR (the rust-lld-musl-crt-zlib workaround, also referenced from this PR's new workarounds.ts:99 entry). The rules.ts:55 comment was not updated and is now stale.
Step-by-step proof
- Pre-PR
shims.ts:registerShimRulesbody isif (!(cfg.darwin && cfg.asan)) return; … n.rule("shim_dylib", …). Single rule, single condition. - Pre-PR
rules.ts:55:// shim_dylib (darwin+asan only)— accurate. - This PR's
shims.tsdiff (verified viagit diff merge-base..HEAD): removes the early return, addsneedsMuslCrtDecompress(),MUSL_CRT_OBJECTS, and a secondn.rule("shim_crt_decompress", …)block at lines 65–74. - This PR does not touch
rules.ts:55(it appears as unchanged context in the diff). The line still reads// shim_dylib (darwin+asan only). - So on a linux-musl release build with cross-language LTO (where
cfg.ld === cfg.rustLld),registerShimRulesregistersshim_crt_decompressand notshim_dylib— the comment describes neither the rule nor the condition that actually applies.
Why nothing prevents it
This is a pure inline comment; nothing type-checks or tests it. rules.ts itself wasn't substantively changed by this PR (only the import on line 26 and the registerRustRules call on lines 52–53 are new), so the stale line on 55 is unchanged context that's easy to miss during review.
On the local convention
One verifier noted that this PR's own new comment on line 52 (// rust_build) is similarly terse — registerRustRules actually registers rust_build, rust_build_cross, and rust_build_tier3. That's fair, and arguably the file's convention is "name the primary rule" rather than "exhaustively list every rule." However, line 55 is worse than line 52: the parenthetical (darwin+asan only) makes a positive claim about the condition under which the function does anything, and that claim is now false. An incomplete list is one thing; an incorrect gating assertion is more misleading. The neighboring lines 45–46 (// dep_fetch, dep_fetch_prebuilt, dep_configure, dep_build, dep_cargo) and 49 (// codegen, esbuild, bun_install + …) do enumerate, so updating 55 to match would also be consistent with the file's prevailing style.
Impact
Zero runtime impact — comment-only. The cost is reader confusion: someone scanning rules.ts to understand which rules exist on a musl build would conclude registerShimRules is a no-op there, when in fact it's the only place shim_crt_decompress is defined. This is the same class of doc-drift as the already-flagged codegen.ts:212/215 JSDoc nit (field renamed but per-field doc not updated) and the CLAUDE.md nit — a comment describing what a function does wasn't updated when the function gained a new branch in the same PR.
How to fix
One-line edit at rules.ts:55:
// shim_dylib (darwin+asan), shim_crt_decompress (musl+rust-lld)
registerShimRules(n, cfg);Optionally also tighten line 52 to // rust_build, rust_build_cross, rust_build_tier3 while in the file, since this PR introduced that line too.
| }, | ||
| { | ||
| id: "rust-lld-musl-crt-zlib", | ||
| issue: "https://github.com/rust-lang/rust/issues/data-compression-not-enabled", |
There was a problem hiding this comment.
🟡 The issue field here is a placeholder — https://github.com/rust-lang/rust/issues/data-compression-not-enabled is not a valid GitHub issue path (issue URLs use numeric IDs) and 404s. Since this string is rendered as Tracked: ${w.issue} in the obsolescence error (line 133) so a maintainer can verify upstream status, it should point to the real rust-lang issue tracking rust-lld's missing LLVM_ENABLE_ZLIB, or be replaced with a non-URL note like "no upstream issue filed" if none exists.
Extended reasoning...
What the bug is
The new rust-lld-musl-crt-zlib workaround entry at scripts/build/workarounds.ts:100 sets:
issue: "https://github.com/rust-lang/rust/issues/data-compression-not-enabled",GitHub issue URLs are of the form /issues/<numeric-id>; a slug like data-compression-not-enabled is not a valid issue path and resolves to a 404. This is a descriptive placeholder dressed up as a URL — most likely the author wrote a slug describing the problem and never replaced it with the actual upstream tracker reference.
Where it surfaces
The Workaround.issue field is documented at line 39 as "Upstream tracker reference (issue URL, PR number, etc.)". It is consumed in exactly one place: checkWorkarounds() at line 133, which embeds it in the BuildError hint as Tracked: ${w.issue} when expectedToBeFixed() trips. The whole point of that line — per the file-level docstring at lines 4-11 — is to tell a future maintainer where to go check whether the upstream fix actually shipped before they delete the workaround. A 404 link defeats that purpose: the maintainer follows the link, gets nothing, and has to re-derive what upstream condition the workaround is waiting on.
Why nothing else catches it
The string is opaque metadata; nothing type-checks or validates it as a URL, and the error path it appears in only fires once clangMajor >= rustMajor (i.e., some future toolchain bump). So this would sit dormant until exactly the moment a maintainer needs the link to work.
Step-by-step proof
- The two sibling entries use real, resolvable URLs: line 61 →
https://github.com/llvm/llvm-project/issues/182943(numeric issue ID), line 80 →https://rustc-dev-guide.rust-lang.org/backend/updating-llvm.html(a docs page). Both demonstrate the intended convention. - Line 100 →
https://github.com/rust-lang/rust/issues/data-compression-not-enabled. GitHub's issue router only accepts numeric IDs after/issues/; navigating to this URL returns 404. - At a future configure where
cfg.linux && cfg.abi === "musl" && cfg.ld === cfg.rustLldand clang's LLVM major has caught up to rustc's,checkWorkarounds()throws aBuildErrorwhose hint containsTracked: https://github.com/rust-lang/rust/issues/data-compression-not-enabled. - The maintainer following that link gets a 404 and has no upstream reference to verify against.
Impact
Zero functional impact today — the workaround logic, description, applies, expectedToBeFixed, and cleanup fields are all correct and complete. This is purely a documentation/maintainability defect in a string that surfaces only in a future developer-facing error. Hence nit severity.
How to fix
Replace the string with either (a) the actual rust-lang/rust issue number that tracks rust-lld being built without LLVM_ENABLE_ZLIB (the relevant upstream context is around src/bootstrap's lld config), or (b) if no upstream issue exists, a non-URL note such as "(no upstream issue — rust-lld dist build omits LLVM_ENABLE_ZLIB)" so the field doesn't masquerade as a clickable link.
| export * as Three1 from 'three-1'; | ||
| export * as Three2 from 'three-2'; | ||
| export * as Three3 from 'three-3'; | ||
| export * as Three4 from 'three-4'; | ||
| export * as Three5 from 'three-5'; | ||
| export * as Three6 from 'three-6'; | ||
| export * as Three7 from 'three-7'; | ||
| export * as Three8 from 'three-8'; | ||
| export * as Three9 from 'three-9'; | ||
| export * as Three10 from 'three-10'; |
There was a problem hiding this comment.
🟡 nit: index.ts now imports three-1..three-100, but package.json only declares three and nothing in this directory creates the aliased copies (run-bench.sh clones a separate esbuild fork and never references index.ts), so bun build bench/bundle/index.ts would fail with 100 unresolved-module errors. README.md also says the bench bundles "10 copies" of three.js, not 100. Since this is bench/ scaffolding off any CI path (the previous content was just the bun init placeholder), no real impact — but consider either reverting, adding a setup step (e.g. for i in $(seq 1 100); do cp -r node_modules/three node_modules/three-$i; done), or syncing the README count.
Extended reasoning...
What changed
This PR replaces bench/bundle/index.ts (previously the one-line console.log("Hello via Bun!") from bun init) with 100 namespace re-exports:
export * as Three1 from 'three-1';
export * as Three2 from 'three-2';
...
export * as Three100 from 'three-100';and adds "three": "^0.184.0" to bench/bundle/package.json dependencies.
Why it doesn't work as committed
The 100 imports are unresolvable. bench/bundle/package.json declares only the single three package; there are no three-1..three-100 packages on npm, no entries for them in package.json or bun.lockb, no tsconfig.json path mappings, and no setup script in bench/bundle/ that creates them. The standard convention for this style of bundler benchmark (esbuild's bench-three does the same) is a manual for i in 1..N; do cp -r node_modules/three node_modules/three-$i; done step, but that step is not checked in here.
run-bench.sh doesn't use index.ts at all. The documented entrypoint is:
git clone git@github.com:colinhacks/esbuild.git
cd esbuild
make bench-threeIt clones a separate esbuild fork and runs that repo's Makefile target — bench/bundle/index.ts is never referenced. So the new file is effectively dead code, or a manual local-stress-test fixture that requires undocumented setup.
README is out of sync. bench/bundle/README.md:11 says the benchmark "bundles 10 copies of the large three.js", but index.ts now imports 100 copies.
Step-by-step proof
bench/bundle/package.jsondependencies ={ "three": "^0.184.0" }— nothree-Nentries.bench/bundle/contains:.gitignore,README.md,bun.lockb,index.ts,package.json,run-bench.sh,tsconfig.json— no setup script.tsconfig.jsonhas nopathsmapping forthree-*.run-bench.sh(3 lines) clonescolinhacks/esbuildand runsmake bench-three— never touchesindex.ts.- Therefore
cd bench/bundle && bun install && bun build index.ts→ 100×error: Could not resolve "three-N". README.md:11: "bundles 10 copies" vsindex.ts: 100 imports.
Why nothing prevents it
bench/ is manual benchmark scaffolding, not on any CI or test path. Nothing in .buildkite/ci.mjs, test/, or any package.json script references bench/bundle/index.ts, so the unresolvable imports never surface in CI. The previous content (console.log("Hello via Bun!")) was equally non-functional as a bundler fixture, so this isn't a regression in any executed path.
Impact
Nil for runtime/CI/tests. This appears to be local-benchmarking scratch (likely from the bundler perf work referenced in docs/.rust-rewrite-verified-claims.md around bun-bench::bundler::prepare()) that got swept into this 300-file PR. The only practical effect is that someone trying to use bench/bundle/ per its README will find index.ts doesn't match what run-bench.sh actually does, and can't be built standalone.
How to fix
Any of:
- Revert
bench/bundle/index.tsand thepackage.json/bun.lockbchanges (cleanest if this was accidental). - Add a setup step, e.g. a
prepare.shwithbun install && for i in $(seq 1 100); do cp -r node_modules/three node_modules/three-$i; done, and reference it from README. - At minimum, update
README.md:11from "10 copies" → "100 copies" so the doc matches the fixture.
🔬 also observed by coderabbitai
| // rust-only/link-only don't run LUT codegen — skip the check so split-CI | ||
| // steps don't require perl on the rust cross-compile box. | ||
| if (cfg.mode === "full" || cfg.mode === "cpp-only") { |
There was a problem hiding this comment.
🔴 The comment at lines 214-215 ("rust-only/link-only don't run LUT codegen") is now false: this same PR makes emitJsSink() push generated_jssink.rs to o.rustInputs (codegen.ts:954), so rust-only builds pull the JSSink edge → generate-jssink.ts:1214 → create-hash-table.ts:23 spawns perl. On a perl-less rust-only agent, configure passes but the build fails with a cryptic spawn error instead of this configure-time hint. Add || cfg.mode === "rust-only" to line 216 and update the comment.
Extended reasoning...
What the bug is
The configure-time perl check at scripts/build/configure.ts:216 only runs when cfg.mode is "full" or "cpp-only", with the comment at lines 214-215 stating "rust-only/link-only don't run LUT codegen — skip the check so split-CI steps don't require perl on the rust cross-compile box." This claim was correct for the old zig-only mode (the JSSink codegen edge fed only cppSources/cppHeaders, never zigInputs), but this same PR makes it false for rust-only: the diff shows the comment was mechanically renamed from zig-only/link-only → rust-only/link-only without re-evaluating whether the invariant still holds — and a different commit in the same PR added the dependency that breaks it.
The code path that triggers it
emitRustOnly()atbun.ts:545callsemitCodegen(), then atbun.ts:548passescodegen.rustInputstoemitRust()ascodegenInputs.emitCodegen()atcodegen.ts:302callsemitJsSink().emitJsSink()(codegen.ts:925-954, new in this PR) declaresgenerated_jssink.rsas a fourth output of the JSSink ninja edge and pushes it too.rustInputs. The inline comment at codegen.ts:948-953 is explicit about this: "Without this, mode: "rust-only" (CI's build-rust job, which compiles no C++ so nothing else pulls JSSink.cpp/.h) never runs this edge and cargo hits the missing file." — i.e., the PR author knowingly wired rust-only to depend on this edge.emitRust()atrust.ts:503listscodegenInputs(which now includesgenerated_jssink.rs) inimplicitInputsof therust_buildedge.- The JSSink edge runs
generate-jssink.ts, which at line ~1214 spawnscreate-hash-table.ts, which at line 23 spawnsperldirectly:cmd: ["perl", create_hash_table, "-"].
So building the default bun target in rust-only mode pulls generated_jssink.rs → runs the JSSink codegen → spawns perl. Rust-only mode now requires perl at build time.
Why nothing prevents it
The perl check at lines 211-222 exists precisely to catch this case at configure time with the clear hint "perl not found in PATH — install it: apt install perl / brew install perl". But because the condition at line 216 excludes rust-only, that check is skipped, configure succeeds, and the failure surfaces later inside ninja as a less actionable spawn-ENOENT (or similar) error from Bun.spawn(["perl", ...]) in create-hash-table.ts.
Step-by-step proof
On a rust-only CI agent without perl (e.g. a minimal Alpine linux-aarch64-musl cross-compile box, per getRustAgent() in .buildkite/ci.mjs, or a Windows agent without perl in PATH):
- CI invokes
bun scripts/build.ts --profile=ci-rust-only --os=... --arch=.... configure.ts:216:cfg.mode === "rust-only", so theifis false → perl check skipped → configure succeeds.emitRustOnly()writesbuild.ninjawithlibbun_rust.adepending (implicitly) ongenerated_jssink.rs.ninja bun→ needslibbun_rust.a→ needsgenerated_jssink.rs→ runs thecodegenrule for JSSink.generate-jssink.tsruns, writesJSSink.lut.txt, then spawnscreate-hash-table.ts(line ~1214).create-hash-table.ts:23doesBun.spawn({ cmd: ["perl", create_hash_table, "-"], ... })→ fails with ENOENT/spawn error because perl is not installed.- Build fails mid-codegen with a stack trace pointing at a spawn failure, instead of the intended "perl not found in PATH — install it: apt install perl / brew install perl" configure-time hint.
The whole stated purpose of lines 211-213 ("Check here so the error is at configure time with a clear hint") is defeated for rust-only mode.
Impact
This degrades a clear, actionable configure-time error into a cryptic mid-build codegen failure on any rust-only build agent that lacks perl. Per getRustAgent(), linux/freebsd/android targets cross-compile on a shared linux-aarch64-musl (Alpine) box, and Windows/darwin run natively — minimal Alpine images and Windows agents commonly lack perl. The bug doesn't break agents that do have perl, but it makes the failure mode on perl-less agents materially worse, which is exactly what the check exists to prevent.
How to fix
Change line 216 to include rust-only:
if (cfg.mode === "full" || cfg.mode === "cpp-only" || cfg.mode === "rust-only") {and update the comment at lines 214-215 to reflect that only link-only skips LUT codegen (link-only consumes pre-built artifacts and runs no codegen at all).
| # Mirrors `allRustTargets` in scripts/build/rust.ts minus aarch64-unknown-freebsd | ||
| # (Tier 3 — no prebuilt rust-std, cargo `-Zbuild-std` builds it from rust-src). | ||
| # darwin/windows triples build natively on their own agents, so the host std | ||
| # comes with the toolchain install regardless; they're listed here so a dev | ||
| # running `bun run rust:check-all` or `cargo check --target` from any host | ||
| # still gets prebuilt std for every Tier 1/2 triple. | ||
| components = ["rust-src"] | ||
| targets = ["x86_64-unknown-freebsd", "aarch64-linux-android", "x86_64-linux-android"] No newline at end of file | ||
| targets = [ | ||
| "aarch64-unknown-linux-gnu", | ||
| "x86_64-unknown-linux-gnu", | ||
| "aarch64-unknown-linux-musl", | ||
| "x86_64-unknown-linux-musl", | ||
| "aarch64-linux-android", | ||
| "x86_64-linux-android", | ||
| "x86_64-unknown-freebsd", | ||
| "aarch64-apple-darwin", | ||
| "x86_64-apple-darwin", | ||
| "aarch64-pc-windows-msvc", | ||
| "x86_64-pc-windows-msvc", | ||
| ] |
There was a problem hiding this comment.
🟡 The comment at line 13 says this list "Mirrors allRustTargets in scripts/build/rust.ts minus aarch64-unknown-freebsd", but it doesn't: allRustTargets (rust.ts:106-117) has 10 entries and contains neither x86_64-linux-android (which this list adds at line 26) nor aarch64-unknown-freebsd (so there's nothing to subtract). CodeRabbit flagged the allRustTargets side at rust.ts:117 as '✅ Addressed' but the array is unchanged at HEAD — either add x86_64-linux-android (and aarch64-unknown-freebsd) to allRustTargets so rust:check-all and generateCargoConfig() cover it, or correct this comment.
Extended reasoning...
What the bug is
This PR adds a new comment at rust-toolchain.toml:13-14 claiming the targets list "Mirrors allRustTargets in scripts/build/rust.ts minus aarch64-unknown-freebsd (Tier 3 — no prebuilt rust-std…)". That claim is false on two independent counts at HEAD:
rust-toolchain.toml'stargetslist (lines 20-32, 11 entries) includesx86_64-linux-android(line 26), whichallRustTargets(rust.ts:106-117, 10 entries) does not contain — onlyaarch64-linux-androidis there.allRustTargetsdoes not containaarch64-unknown-freebsdeither, so "minus aarch64-unknown-freebsd" describes subtracting an element that isn't present.
For the comment's set arithmetic to hold, allRustTargets would need 12 entries (the 11 in the toml + aarch64-unknown-freebsd); it has 10.
Step-by-step proof
allRustTargets (rust.ts:106-117) |
rust-toolchain.toml targets (20-32) |
|
|---|---|---|
| count | 10 | 11 |
x86_64-linux-android |
❌ absent | ✅ line 26 |
aarch64-unknown-freebsd |
❌ absent | ❌ absent (correctly excluded — Tier 3) |
So {toml targets} = {allRustTargets} ∖ {aarch64-unknown-freebsd} is false: the LHS has x86_64-linux-android and the RHS doesn't, and the subtrahend isn't in the minuend to begin with.
Why the surrounding code doesn't catch it
rustTarget() at rust.ts:54 emits ${arch}-linux-android for either arch, so x86_64-linux-android is a triple the build system can target. The two consumers of allRustTargets are scripts/rust-check-all.ts:21 (iterates the list to run cargo check --target <t>) and generateCargoConfig() in scripts/build/cargo-config.ts:79 (writes a [target.<triple>] linker section per entry). Both therefore silently skip x86_64-linux-android. The toml's extra entry is harmless in the other direction — rustup just installs an unused prebuilt std component.
Relation to the prior CodeRabbit comment
CodeRabbit inline comment 3211780323 (on rust.ts:117) already flagged "add x86_64-linux-android and aarch64-unknown-freebsd to allRustTargets" and is marked resolved / "✅ Addressed in commits d5945cf to 808905b" — but allRustTargets at HEAD is unchanged (still 10 entries, still missing both), and verifiers confirmed those commit SHAs are not in the repo history. So the resolved marker appears stale. This report is anchored at a different location — the new comment text in rust-toolchain.toml added by this PR — which is factually self-contradicting on day one regardless of how the allRustTargets side is resolved.
Impact
Low. x86_64-linux-android is not in docs/CI_RUST_MIGRATION.md's build matrix, so CI doesn't depend on it; the practical effect is that bun run rust:check-all skips one buildable triple and direct cargo check --target x86_64-linux-android won't get a generated [target.…].linker hint (the ninja build path sets the linker via env anyway, so this is advisory). The main cost is reader confusion: the comment promises a single source of truth that doesn't exist.
How to fix
Either:
- Make
allRustTargetsthe source of truth — add"x86_64-linux-android"and"aarch64-unknown-freebsd"to it (per the original CodeRabbit suggestion). Then the comment becomes true andrust:check-all/generateCargoConfig()cover the full set. - Or fix the comment — e.g. "Tier 1/2 triples that
rustTarget()can produce, minus Tier-3aarch64-unknown-freebsd", and accept that it's maintained independently ofallRustTargets.
| // shim_dylib (darwin+asan only) | ||
| registerShimRules(n, cfg); |
There was a problem hiding this comment.
🟡 The inventory comment still reads // shim_dylib (darwin+asan only), but this PR's changes to shims.ts make registerShimRules() also register shim_crt_decompress (gated on needsMuslCrtDecompress(cfg), i.e. linux+musl+rust-lld). The same diff hunk updated the adjacent line 52 comment from // zig_fetch, zig_build to // rust_build — this one was missed in the same sweep. Should read e.g. // shim_dylib (darwin+asan), shim_crt_decompress (musl+rust-lld).
Extended reasoning...
What the bug is
scripts/build/rules.ts maintains an inventory comment above each registerXxxRules() call enumerating the ninja rule names that call registers — e.g. line 42 // cxx, cc, pch, link, ar, line 45-46 // dep_fetch, dep_fetch_prebuilt, dep_configure, dep_build, dep_cargo, line 49 // codegen, esbuild, bun_install + codegen/stamps dir stamps. Line 55 above registerShimRules(n, cfg) reads:
// shim_dylib (darwin+asan only)
registerShimRules(n, cfg);But this PR's diff to scripts/build/shims.ts changes registerShimRules() so it now registers two rules under separate gates, not one:
shim_dylib(shims.ts:59), gated oncfg.darwin && cfg.asanshim_crt_decompress(shims.ts:69), gated onneedsMuslCrtDecompress(cfg)→cfg.linux && cfg.abi === "musl" && cfg.rustLld !== undefined && cfg.ld === cfg.rustLld
The comment is therefore stale: it both omits the second rule and asserts "darwin+asan only", which is no longer the only condition under which registerShimRules emits a rule.
Step-by-step proof
-
The convention exists and is actively maintained. Every other call in
registerAllRules()(rules.ts:37-57) carries a comment listing its rule names.scripts/build/CLAUDE.mddocuments this file as the central rule registry. -
This PR updated the adjacent comment in the same hunk. The PR diff for rules.ts shows:
- // zig_fetch, zig_build - registerZigRules(n, cfg); + // rust_build + registerRustRules(n, cfg); // shim_dylib (darwin+asan only) registerShimRules(n, cfg);
Line 52's comment was rewritten as part of the zig→rust migration; line 55 sits two lines below in the same hunk's context and was left untouched.
-
This PR is what added the second rule. The shims.ts changes in this PR remove the early-return
if (!(cfg.darwin && cfg.asan)) return;fromregisterShimRulesand add theneedsMuslCrtDecompress()helper plus then.rule("shim_crt_decompress", ...)registration.workarounds.ts(also touched in this PR) consumes it. Before this PR, "darwin+asan only" was accurate. -
Result: the comment now under-describes what the function does and the "only" qualifier is actively misleading.
Why nothing prevents it
These are inventory comments for human readers/grepping; nothing type-checks them against n.rule() calls. The PR's own doc-sync sweep over rules.ts (line 52) and over the whole scripts/build/CLAUDE.md module table demonstrates the intent to keep these accurate; line 55 was simply overlooked because the diff hunk's edit was two lines above it.
Impact
None functionally — comment-only drift. A reader grepping rules.ts to find which registerXxx owns shim_crt_decompress won't find it, and the "(darwin+asan only)" qualifier will mislead them about when shim rules are emitted. Same class of zig→rust doc-sync miss as the already-flagged scripts/build/CLAUDE.md:146 and codegen.ts:212/215 JSDoc nits on this PR.
How to fix
One-line edit at rules.ts:55:
// shim_dylib (darwin+asan), shim_crt_decompress (musl+rust-lld)
registerShimRules(n, cfg);| { | ||
| id: "rust-lld-musl-crt-zlib", | ||
| issue: "https://github.com/rust-lang/rust/issues/data-compression-not-enabled", |
There was a problem hiding this comment.
🟡 The issue URL https://github.com/rust-lang/rust/issues/data-compression-not-enabled is a placeholder, not a valid GitHub issue URL (issue paths are always numeric IDs, never slugs — this 404s). Since this field is surfaced to maintainers via Tracked: ${w.issue} in the BuildError hint at line 133, replace it with the real rust-lang/rust issue number for rust-lld lacking LLVM_ENABLE_ZLIB, or use a description-only string if no upstream issue exists.
Extended reasoning...
What the bug is
The new rust-lld-musl-crt-zlib workaround entry at scripts/build/workarounds.ts:98-118 sets:
issue: "https://github.com/rust-lang/rust/issues/data-compression-not-enabled",This is not a valid GitHub issue URL. GitHub issue URLs always take the form /issues/<numeric-id> — slugs like data-compression-not-enabled are never valid issue identifiers. Visiting this URL returns a 404. The string data-compression-not-enabled looks like a placeholder note (perhaps the LLVM error message fragment, or a TODO marker) that was never replaced with the real upstream issue number before commit.
The code path that surfaces it
The Workaround.issue field is documented at workarounds.ts:39-40 as "Upstream tracker reference (issue URL, PR number, etc.)", and the file's module-level docstring (lines 1-30) explains that the entire purpose of this registry is to give future maintainers a pointer to verify when the upstream fix ships so the workaround can be removed.
The field is consumed at workarounds.ts:133, inside checkWorkarounds():
throw new BuildError(`Workaround '${w.id}' is obsolete — upstream fix is available`, {
hint:
`${w.description}\n` +
` Tracked: ${w.issue}\n\n` +
...
});So when this workaround's expectedToBeFixed predicate trips (i.e., when clang's LLVM major catches up to rustc's bundled LLVM major on a musl build), the maintainer sees Tracked: https://github.com/rust-lang/rust/issues/data-compression-not-enabled and clicks through to a 404 — defeating the field's documented purpose.
Why nothing prevents it
The issue field is typed as plain string (workarounds.ts:40), and scripts/build/CLAUDE.md (referenced in the bug description) says only that workarounds should reference an upstream tracking issue — there's no validation. The two sibling entries in the same array follow the convention correctly: asan-dyld-shim at line 61 uses a real numeric LLVM issue URL (llvm-project/issues/182943), and rust-lld-for-crosslang-lto at line 80 uses a rustc-dev-guide doc page. This entry is the outlier.
Step-by-step proof
- workarounds.ts:100 —
issue: "https://github.com/rust-lang/rust/issues/data-compression-not-enabled". - GitHub's issue URL schema is
/<owner>/<repo>/issues/<integer>.data-compression-not-enabledis not an integer. The URL 404s. - workarounds.ts:39-40 documents
issueas "Upstream tracker reference (issue URL, PR number, etc.)" — i.e., something resolvable. - workarounds.ts:133 interpolates
w.issueinto a user-facingBuildErrorhint asTracked: ${w.issue}. - The
expectedToBeFixedpredicate (lines 107-114) returns true whenclangMajor >= rustMajoron a musl LTO build with rust-lld selected. When that fires (next LLVM release that catches rustc's bundled major), configure fails and prints the broken link. - The neighboring entries at lines 61 and 80 use real, resolvable URLs, demonstrating the intended convention this entry violates.
Impact
Metadata-only — no runtime or build behavior changes. The error message only appears when the workaround becomes obsolete, and even then the broken link is annoying rather than blocking (the cleanup instructions at line 115-117 are still actionable). But it's a clear leftover placeholder in newly-added code that defeats the explicit, documented purpose of the field, and is trivially fixable.
How to fix
Replace line 100 with the actual rust-lang/rust (or rust-lang/cargo, or LLVM) issue number tracking "rust-lld is built without LLVM_ENABLE_ZLIB". If no upstream issue exists, either file one, or use a description string that doesn't masquerade as a URL (e.g., issue: "rust-dist: rust-lld built without LLVM_ENABLE_ZLIB (no upstream tracker)") so a maintainer doesn't waste time clicking a 404.
| [profile.release] | ||
| lto = "fat" | ||
| codegen-units = 1 | ||
| debug = "line-tables-only" | ||
| strip = "none" |
There was a problem hiding this comment.
🔴 This [profile.release] (no debug-assertions = true) plus the build-zig→build-rust CI rewiring causes src/install/windows-shim/bun_shim_impl.rs to be release-compiled on Windows for the first time, where it fails with E0080 ~85× across all three Windows build-rust jobs (Build #53015). The debug! macro at line 144 contains const _: () = assert!(cfg!(debug_assertions)); — but unlike Zig's comptime assert(builtin.mode == .Debug) (skipped in dead branches), Rust evaluates const items unconditionally even inside if DBG { ... } with DBG = false, so every expansion becomes const _: () = assert!(false). Fix: delete line 144 (the if DBG wrapper at every call site already gates execution) or wrap the macro body in #[cfg(debug_assertions)].
Extended reasoning...
What the bug is
The debug! macro in src/install/windows-shim/bun_shim_impl.rs:142-154 is a port of Zig's fn debug (bun_shim_impl.zig:108-115), which used comptime assert(builtin.mode == .Debug) to ensure it was never invoked in release builds. The Rust port translates this to:
macro_rules! debug {
($fmt:literal $(, $arg:expr)* $(,)?) => {{
const _: () = assert!(cfg!(debug_assertions)); // line 144
...
}};
}Every call site is wrapped if DBG { debug!(...) } where const DBG: bool = cfg!(debug_assertions) (line 54). In Zig this pattern works: with dbg comptime-false in release, the function body is never analyzed and the comptime assert never fires. In Rust it does not work: const items declared inside a block expression are items, not statements — they are hoisted and evaluated at compile time regardless of runtime control-flow reachability. if false { const _: () = assert!(false); } is a hard E0080 compile error.
Why this PR triggers it
The broken line itself isn't modified by this PR, but this PR is what causes it to be compiled in release mode for the first time:
- The new workspace
Cargo.toml(this hunk, lines 138-142) defines[profile.release]withlto,codegen-units,debug,strip— but notdebug-assertions = true, so cargo's release default (debug-assertions = false) applies andcfg!(debug_assertions)evaluates tofalse. .buildkite/ci.mjsrewires CI fromgetBuildZigStep→getBuildRustStep, andgetRustAgent()runs Windows builds natively on a Windows agent.src/installis a workspace member, andbun_shim_impl.rsis gated#, so only Windows targets compile it.
Before this PR, CI built Zig (getBuildZigStep); the .rs file existed but was never compiled by CI's release profile.
Step-by-step proof
- Windows CI runs
cargo build --profile release(viascripts/build/rust.ts,cargoProfile()maps non-Debug →release). cfg!(debug_assertions)=falseunder[profile.release].- rustc parses
bun_shim_impl.rs. At each of ~30if DBG { debug!(...) }call sites (lines 469-470, 549-551, 584-585, 636-637, 664-665, 679-680, 701-702, 724-725, 764, …, 1154), the macro expands to a block containingconst _: () = assert!(false);. - Item collection hoists every
const _into the enclosing function's set of required-consts. Const evaluation runs before dead-code elimination at the MIR level — theif falseguard is irrelevant to whether the const is evaluated. - Each const evaluates
assert!(false)→ panic in const context →error[E0080]: evaluation panicked: assertion failed: cfg!(debug_assertions).
This is exactly what robobun reports on this PR's Build #53015 (commit 16fbc07): [E0080] evaluation panicked: assertion failed: cfg!(debug_assertions) at src/install/windows-shim/bun_shim_impl.rs#L144, repeated ~28× per job across 🪟 x64, 🪟 x64-baseline, and 🪟 aarch64 build-rust. No commits between 16fbc07 and HEAD (a5bc96d) touch this file.
Why the existing code doesn't prevent it
The if DBG { ... } wrapper is a runtime (well, const-folded-to-nothing) guard. It successfully ensures the body of the macro never executes in release. But const _: () = ...; is not an executable statement — it's an item declaration, and item declarations inside blocks are processed unconditionally during compilation, just like a fn or struct declared inside a block. This is the same mechanism the static_assertions crate exploits: a const item's initializer is always evaluated, period.
The other Windows blockers already commented on this PR — the && shell-chaining bug (#3212044052), the cfg.msvcLinker bug (#3211780331), the tier3 rustup self-heal (#3211967077) — all gate the build before cargo gets to compile this crate. But CI Build #53015 demonstrates cargo did reach this file (the E0080 is a rustc compile error, not a build-script/linker error), so even after those are fixed, this E0080 fires next.
Impact
All three Windows release build-rust jobs fail with ~85 instances of E0080. Windows CI is hard-blocked.
How to fix
Simplest: delete line 144. The if DBG { debug!(...) } wrapper at every call site already guarantees the macro body only executes in debug builds, and in release the entire if false { ... } block is DCE'd anyway. The const-assert was Zig belt-and-suspenders that doesn't translate to Rust.
Alternative: replace the macro body with conditional compilation so it's a true no-op in release:
macro_rules! debug {
($fmt:literal $(, $arg:expr)* $(,)?) => {{
#[cfg(debug_assertions)]
{
#[cfg(not(feature = "shim_standalone"))]
{ bun_output::scoped_log!(bun_shim_impl, $fmt $(, $arg)*); }
#[cfg(feature = "shim_standalone")]
{ let _ = ($($arg,)*); }
}
}};
}This also lets the if DBG { } wrappers at call sites be dropped if desired.
|
Must be the most epic rewrite in history |
Blog post with details coming soon.
It passes Bun's pre-existing test suite on all platforms (and fixes several memory leaks and flaky tests), the binary size shrinks by 3 MB - 8 MB, the benchmarks are between neutral and faster - and most importantly, we now have compiler-assisted tools for catching & preventing memory bugs, which have costed the team an enormous amount of development & debugging time over the years.
The codebase is otherwise largely the same. The same architecture, the same data structures. Bun still uses few 3rd party libraries. No async rust.
To try this, run:
Please do file issues if you run into any. If this thread gets crazy I will lock it.
Note: