From 6b086aaa5c5b9d5a103277893fafd5cb87b9d767 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:48:06 +0000 Subject: [PATCH 01/13] Remove NLL borrow-checker workarounds that Polonius makes unnecessary Requires building with -Zpolonius=next. Deletes 7 unsafe raw-pointer reborrows in bun_which and the open command's editor detection, converts the editor helpers from bool plus out-param to returned Option borrows, and replaces two contains_key-then-get double lookups with single lookups. --- .../WorkspacePackageJSONCache.rs | 8 +-- src/runtime/cli/open.rs | 57 ++++++------------- src/runtime/test_runner/snapshot.rs | 7 +-- src/which/lib.rs | 21 +------ 4 files changed, 24 insertions(+), 69 deletions(-) diff --git a/src/install/PackageManager/WorkspacePackageJSONCache.rs b/src/install/PackageManager/WorkspacePackageJSONCache.rs index 41287a66c09c..a4ad73d25cac 100644 --- a/src/install/PackageManager/WorkspacePackageJSONCache.rs +++ b/src/install/PackageManager/WorkspacePackageJSONCache.rs @@ -152,12 +152,8 @@ impl WorkspacePackageJSONCache { &buf[..abs_package_json_path.len()] }; - // reshaped for borrowck — we cannot hold an entry borrow across - // `self.map.remove`, so check - // membership up front and only insert into the map after a successful - // read+parse. Net map state is identical on every path. - if self.map.contains_key(path) { - return GetResult::Entry(self.map.get_mut(path).unwrap()); + if let Some(entry) = self.map.get_mut(path) { + return GetResult::Entry(entry); } // Owned NUL-terminated copy reused diff --git a/src/runtime/cli/open.rs b/src/runtime/cli/open.rs index 1f0f6a43c6a0..0c0657b7d2a4 100644 --- a/src/runtime/cli/open.rs +++ b/src/runtime/cli/open.rs @@ -85,22 +85,18 @@ impl Editor { editor: Editor, buf: &'a mut PathBuffer, cwd: &[u8], - out: &mut &'a [u8], - ) -> bool { - let Some(path_env) = env.get(b"PATH") else { - return false; - }; + ) -> Option<&'a [u8]> { + let path_env = env.get(b"PATH")?; if let Some(path) = BIN_NAME[editor] { if !path.is_empty() { if let Some(bin) = which(buf, path_env, cwd, path) { - *out = bin.as_bytes(); - return true; + return Some(bin.as_bytes()); } } } - false + None } pub(crate) fn by_fallback_path_for_editor( @@ -129,22 +125,15 @@ impl Editor { env: &mut dot_env::Loader, buf: &'a mut PathBuffer, cwd: &[u8], - out: &mut &'a [u8], - ) -> Option { - // Note: borrowck — see `by_path` above; same Polonius-case reborrow. - let buf_ptr: *mut PathBuffer = buf; + ) -> Option<(Editor, &'a [u8])> { for &editor in &DEFAULT_PREFERENCE_LIST { - // SAFETY: exclusive per-iteration reborrow; we return immediately on hit. - if Self::by_path_for_editor(env, editor, unsafe { &mut *buf_ptr }, cwd, out) { - return Some(editor); + if let Some(bin) = Self::by_path_for_editor(env, editor, &mut *buf, cwd) { + return Some((editor, bin)); } - // Note: reshaped for borrowck — by_fallback_path_for_editor writes a - // 'static slice; we widen `out` to accept it via a temporary. let mut static_out: &'static [u8] = b""; if Self::by_fallback_path_for_editor(editor, Some(&mut static_out)) { - *out = static_out; - return Some(editor); + return Some((editor, static_out)); } } @@ -459,12 +448,6 @@ impl EditorContext { pub(crate) fn detect_editor(&mut self, env: &mut dot_env::Loader) { let mut buf = PathBuffer::uninit(); - // Note: borrowck — `by_path_for_editor`/`by_fallback` tie `out`'s lifetime - // to `&'a mut buf`. On the `false` path NLL conservatively keeps `buf` borrowed - // (Polonius case). Re-borrow through a raw pointer at each call site; on a hit - // we return immediately so only one `&mut` is ever live. - let buf_ptr: *mut PathBuffer = &raw mut buf; - let mut out: &[u8] = b""; // first: choose from user preference if !self.name.is_empty() { @@ -478,18 +461,16 @@ impl EditorContext { // "vscode" if let Some(editor_) = Editor::by_name(bun_paths::basename(self.name)) { - if Editor::by_path_for_editor( + if let Some(bin) = Editor::by_path_for_editor( env, editor_, - // SAFETY: see note above — exclusive per-call reborrow. - unsafe { &mut *buf_ptr }, + &mut buf, Fs::FileSystem::instance().top_level_dir, - &mut out, ) { self.editor = Some(editor_); self.path = Fs::FileSystem::instance() .dirname_store - .append_slice(out) + .append_slice(bin) .expect("unreachable"); return; } @@ -509,18 +490,16 @@ impl EditorContext { // EDITOR=code if let Some(editor_) = Editor::detect(env) { - if Editor::by_path_for_editor( + if let Some(bin) = Editor::by_path_for_editor( env, editor_, - // SAFETY: see note above — exclusive per-call reborrow. - unsafe { &mut *buf_ptr }, + &mut buf, Fs::FileSystem::instance().top_level_dir, - &mut out, ) { self.editor = Some(editor_); self.path = Fs::FileSystem::instance() .dirname_store - .append_slice(out) + .append_slice(bin) .expect("unreachable"); return; } @@ -538,17 +517,15 @@ impl EditorContext { } // Don't know, so we will just guess based on what exists - if let Some(editor_) = Editor::by_fallback( + if let Some((editor_, bin)) = Editor::by_fallback( env, - // SAFETY: see note above — exclusive per-call reborrow. - unsafe { &mut *buf_ptr }, + &mut buf, Fs::FileSystem::instance().top_level_dir, - &mut out, ) { self.editor = Some(editor_); self.path = Fs::FileSystem::instance() .dirname_store - .append_slice(out) + .append_slice(bin) .expect("unreachable"); return; } diff --git a/src/runtime/test_runner/snapshot.rs b/src/runtime/test_runner/snapshot.rs index 379275165dc1..dcf2eae57c00 100644 --- a/src/runtime/test_runner/snapshot.rs +++ b/src/runtime/test_runner/snapshot.rs @@ -157,11 +157,8 @@ impl<'a> Snapshots<'a> { name_with_counter.extend_from_slice(counter_string); let name_hash: u64 = hash(&name_with_counter); - // reshaped for borrowck — `get` then early-return borrows `*self.values` - // immutably for the whole fn body (NLL limitation with returned borrows), preventing - // the later `insert`. Probe with `contains_key` first; re-lookup on hit. - if self.values.contains_key(&name_hash) { - return Ok(Some(&**self.values.get(&name_hash).unwrap())); + if let Some(value) = self.values.get(&name_hash) { + return Ok(Some(&**value)); } // doesn't exist. append to file bytes and add to hashmap. diff --git a/src/which/lib.rs b/src/which/lib.rs index ba340abd8f93..46a26e8d2be7 100644 --- a/src/which/lib.rs +++ b/src/which/lib.rs @@ -82,12 +82,7 @@ pub fn which_for_spawn<'a>( let mut rel: Vec = Vec::with_capacity(bin.len() + 2); rel.extend_from_slice(b"./"); rel.extend_from_slice(bin); - // PORT NOTE: NLL Polonius limitation — raw-ptr reborrow so the None - // branch can fall through without `buf` appearing borrowed. - // SAFETY: the borrow does not escape this block on the None path. - let buf_reborrow: &'a mut PathBuffer = - unsafe { &mut *std::ptr::from_mut::(buf) }; - if let Some(found) = which(buf_reborrow, b"", cwd, &rel) { + if let Some(found) = which(&mut *buf, b"", cwd, &rel) { return Some(found); } } @@ -359,13 +354,8 @@ pub(crate) fn which_win<'a>( // check if bin is in cwd if strings::index_of_char(bin, b'/').is_some() || strings::index_of_char(bin, b'\\').is_some() { - // NLL/Polonius limitation — raw-ptr reborrow so the None branch can - // fall through without `buf` appearing borrowed. - // SAFETY: bin_path borrow does not escape this block on the None path. - let buf_reborrow: &'a mut WPathBuffer = - unsafe { &mut *std::ptr::from_mut::(buf) }; if let Some(bin_path) = search_bin_in_path( - buf_reborrow, + &mut *buf, &mut *path_buf, cwd, strings::without_prefix_comptime(bin, b"./"), @@ -380,13 +370,8 @@ pub(crate) fn which_win<'a>( // iterate over system path delimiter for segment_part in path.split(|b| *b == b';').filter(|s| !s.is_empty()) { - // NLL/Polonius limitation — re-borrowing `buf` across loop iterations - // when returning a reference tied to its lifetime. - // SAFETY: on None the borrow ends; on Some we return immediately. - let buf_reborrow: &'a mut WPathBuffer = - unsafe { &mut *std::ptr::from_mut::(buf) }; if let Some(bin_path) = search_bin_in_path( - buf_reborrow, + &mut *buf, &mut *path_buf, segment_part, bin, From 95b7d2762c96dd9a0cfd3208d1ac927f4d5effa5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:52:11 +0000 Subject: [PATCH 02/13] build: enable -Zpolonius=next for all Rust compilation Wires the polonius alpha borrow checker into the ninja cargo edge and the generated .cargo/config.toml (including new sections for the windows-msvc triples, which previously had none) so bun bd, CI, plain cargo check, rust:check-all, and rust-analyzer all accept the same code. The pinned toolchain is already nightly. Measured cost: ~2% on a cold cargo check of the workspace. --- scripts/build/cargo-config.ts | 14 ++++++++++++-- scripts/build/rust.ts | 8 ++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/scripts/build/cargo-config.ts b/scripts/build/cargo-config.ts index 2d42608e7697..39a333dba1f5 100644 --- a/scripts/build/cargo-config.ts +++ b/scripts/build/cargo-config.ts @@ -79,10 +79,20 @@ export function generateCargoConfig(cfg: Config): string { "# file is correct on whatever machine ran configure.", ]; + // `-Zpolonius=next` everywhere: workspace code relies on the polonius + // borrow checker (see the matching push in rust.ts), so every rustc + // invocation that type-checks workspace crates needs it or borrowck + // fails. Windows-msvc triples get a section for it too (they are + // otherwise omitted — the MSVC linker path is env-only, see above) so + // `cargo check --target *-windows-msvc` / `rust:check-all` work. + const polonius = `"-Z", "polonius=next"`; for (const triple of allRustTargets) { - if (tripleOs(triple) === "windows") continue; lines.push(""); lines.push(`[target.${triple}]${triple === host ? " # host" : ""}`); + if (tripleOs(triple) === "windows") { + lines.push(`rustflags = [${polonius}]`); + continue; + } lines.push(`linker = ${JSON.stringify(linkerFor(triple, cfg))}`); // -Qunused-arguments: rustc passes link args that don't apply to every // artifact kind (e.g. `-no-pie` when it links a target cdylib; none @@ -95,7 +105,7 @@ export function generateCargoConfig(cfg: Config): string { // `cargo build`/`cargo check`, rust-analyzer); real linker errors still // fail the link. lines.push( - `rustflags = ["-C", "link-arg=-fuse-ld=lld", "-C", "link-arg=-Qunused-arguments", "-A", "linker_messages"]`, + `rustflags = ["-C", "link-arg=-fuse-ld=lld", "-C", "link-arg=-Qunused-arguments", "-A", "linker_messages", ${polonius}]`, ); } lines.push(""); diff --git a/scripts/build/rust.ts b/scripts/build/rust.ts index 4e3a41ae477f..9145f7c875ae 100644 --- a/scripts/build/rust.ts +++ b/scripts/build/rust.ts @@ -410,6 +410,14 @@ export function cargoBuildInvocation(cfg: Config): CargoInvocation { if (!cfg.ci) { rustflags.push("-Zthreads=8"); } + // Polonius alpha borrow checker: accepts NLL "problem case 3" (a borrow + // returned/escaping on one path no longer blocks the other paths), which + // workspace code now relies on — grep `polonius` in src/ for the patterns. + // Nightly-only; the pinned toolchain is nightly. Must stay in sync with the + // generated `.cargo/config.toml` (cargo-config.ts) so plain `cargo check`, + // rust-analyzer, and `rust:check-all` accept the same code the ninja build + // does. + rustflags.push("-Zpolonius=next"); // rustc does not emit `.llvm_addrsig` by default on *any* target (verified // empirically — Linux-gnu, musl, darwin, msvc all missing it). lld's // `--icf=safe` (flags.ts:960) and lld-link's `/OPT:SAFEICF` (flags.ts:778) From ad80a05250173e6c6395d45464d6cfe8a53f5f81 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:06:39 +0000 Subject: [PATCH 03/13] Remove more NLL workarounds across the workspace Rewrites 25 borrow-checker workaround sites to their natural form: single lookups instead of contains_key-then-get, direct early returns of borrows instead of index/len round-trips, plain reborrows instead of raw-pointer reborrows. Several sites (linear_fifo, env_loader, patchPackage, Chunk, fmt) now require the polonius borrow checker enabled in the previous commit; the rest simply lost their scaffolding. --- src/bun_core/fmt.rs | 23 +++++------- src/bundler/Chunk.rs | 27 ++++++-------- src/bundler/transpiler.rs | 31 ++++++---------- src/collections/array_hash_map.rs | 8 +---- src/collections/linear_fifo.rs | 7 ++-- src/dotenv/env_loader.rs | 32 +++++++---------- src/exe_format/elf.rs | 9 ++--- src/install/PackageManager/patchPackage.rs | 30 +++++++--------- src/install/isolated_install.rs | 10 +++--- src/install/isolated_install/Hardlinker.rs | 39 +++++--------------- src/jsc/ConsoleObject.rs | 16 +++++---- src/runtime/api/bun/spawn/stdio.rs | 4 --- src/runtime/bake/production.rs | 9 ++--- src/runtime/cli/upgrade_command.rs | 5 +-- src/runtime/dns_jsc/dns.rs | 4 +-- src/runtime/node/node_fs.rs | 15 ++------ src/runtime/node/path.rs | 42 ++++++++++------------ src/runtime/node/types.rs | 15 +++----- src/runtime/socket/Listener.rs | 11 +++--- src/runtime/webcore/Body.rs | 5 --- src/sql_jsc/postgres/DataCell.rs | 9 +---- 21 files changed, 116 insertions(+), 235 deletions(-) diff --git a/src/bun_core/fmt.rs b/src/bun_core/fmt.rs index f35f4e17bbfa..11ad9c79752d 100644 --- a/src/bun_core/fmt.rs +++ b/src/bun_core/fmt.rs @@ -2314,29 +2314,24 @@ pub fn format_ip<'a>( write!(cursor, "{}", address).map_err(|_| crate::CrateError::NoSpaceLeft)?; let written = cursor.position() as usize; - // Reshaped for borrowck — compute (start, end) offsets against `into` - // instead of iteratively reborrowing a `result` slice, so the final - // returned `&mut into[start..end]` carries the caller's `'a` lifetime - // cleanly. - let mut start = 0usize; - let mut end = written; + let mut result = &mut into[..written]; // Strip `:` - if let Some(colon) = into[start..end].iter().rposition(|&b| b == b':') { - end = start + colon; + if let Some(colon) = result.iter().rposition(|&b| b == b':') { + result = &mut result[..colon]; } // Strip brackets - if start < end && into[start] == b'[' && into[end - 1] == b']' { - start += 1; - end -= 1; + if result.first() == Some(&b'[') && result.last() == Some(&b']') { + let len = result.len(); + result = &mut result[1..len - 1]; } // Strip `%` — Node formats addresses via uv_inet_ntop on the bare // in6_addr and never includes the zone identifier; the scope is exposed // separately (e.g. `scopeid` in os.networkInterfaces()). - if let Some(percent) = into[start..end].iter().position(|&b| b == b'%') { - end = start + percent; + if let Some(percent) = result.iter().position(|&b| b == b'%') { + result = &mut result[..percent]; } - Ok(&mut into[start..end]) + Ok(result) } // ─────────────────────────────────────────────────────────────────────────── diff --git a/src/bundler/Chunk.rs b/src/bundler/Chunk.rs index 1c029dfe9890..77dd4086f205 100644 --- a/src/bundler/Chunk.rs +++ b/src/bundler/Chunk.rs @@ -308,27 +308,20 @@ impl Chunk { // Look up the CSS chunk via the JS chunk's css_chunks indices. // This correctly handles deduplicated CSS chunks that are shared // across multiple HTML entry points (see issue #23668). - // Note: reshaped for borrowck — we scan immutably for the JS chunk, copy the - // css-chunk index into a local, drop the borrow, then re-borrow mutably. let entry_point_id = self.entry_point.entry_point_id(); - let css_idx: Option = 'find: { - for other in chunks.iter() { - if let Content::Javascript(js) = &other.content { - if other.entry_point.is_entry_point() - && other.entry_point.entry_point_id() == entry_point_id - { - let css_chunk_indices = &js.css_chunks[..]; - if !css_chunk_indices.is_empty() { - break 'find Some(css_chunk_indices[0] as usize); - } - break 'find None; + for other in chunks.iter() { + if let Content::Javascript(js) = &other.content { + if other.entry_point.is_entry_point() + && other.entry_point.entry_point_id() == entry_point_id + { + let css_chunk_indices = &js.css_chunks[..]; + if !css_chunk_indices.is_empty() { + let idx = css_chunk_indices[0] as usize; + return Some(&mut chunks[idx]); } + break; } } - None - }; - if let Some(idx) = css_idx { - return Some(&mut chunks[idx]); } // Fallback: match by entry_point_id for cases without a JS chunk. for other in chunks.iter_mut() { diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index 682929fa4d3a..5017a81d3d9f 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -463,28 +463,18 @@ impl<'a> Transpiler<'a> { let mut cache_bust_buf = bun_paths::PathBuffer::uninit(); // Bust directory cache and try again - // reshaped for borrowck — a single labelled block would - // return a slice that aliases either `entry_point` (via - // `dirname`) or `cache_bust_buf`. Rust can't unify the two - // disjoint mutable borrows of `cache_bust_buf` across `break`, - // so compute `busted` directly instead. - let busted: bool = 'name: { + let buster_name = 'name: { if bun_paths::is_absolute(entry_point) { let dir = bun_paths::resolve_path::dirname::( entry_point, ); if !dir.is_empty() { // Normalized with trailing slash - let buster_name = bun_paths::string_paths::normalize_slashes_only( + break 'name bun_paths::string_paths::normalize_slashes_only( &mut cache_bust_buf[..], dir, bun_paths::SEP, ); - break 'name self.resolver.bust_dir_cache( - bun_paths::string_paths::without_trailing_slash_windows_path( - buster_name, - ), - ); } } @@ -492,17 +482,16 @@ impl<'a> Transpiler<'a> { let parts: [&[u8]; 2] = [entry_point, b".."]; let top_level_dir = self.fs().top_level_dir; - let buster_name = bun_paths::resolve_path::join_abs_string_buf_z::< - bun_paths::platform::Auto, - >( - top_level_dir, &mut cache_bust_buf[..], &parts - ); - self.resolver.bust_dir_cache( - bun_paths::string_paths::without_trailing_slash_windows_path( - buster_name.as_bytes(), - ), + bun_paths::resolve_path::join_abs_string_buf_z::( + top_level_dir, + &mut cache_bust_buf[..], + &parts, ) + .as_bytes() }; + let busted = self.resolver.bust_dir_cache( + bun_paths::string_paths::without_trailing_slash_windows_path(buster_name), + ); // Only re-query if we previously had something cached. if busted { diff --git a/src/collections/array_hash_map.rs b/src/collections/array_hash_map.rs index 8a3c209364d1..05f3ba5eaac9 100644 --- a/src/collections/array_hash_map.rs +++ b/src/collections/array_hash_map.rs @@ -1199,15 +1199,9 @@ impl, A: MapAllocator> ArrayHashMap Result, AllocError> { let gop = self.get_or_put(key)?; if !gop.found_existing { - // SAFETY: re-borrow at same index — `gop` borrows `self` so go - // through the slot it already points at. *gop.value_ptr = value; } - // Can't return `gop` while it borrows in the branch above without - // NLL gymnastics; recompute via index. - let i = gop.index; - let found = gop.found_existing; - Ok(self.gop_at(i, found)) + Ok(gop) } } diff --git a/src/collections/linear_fifo.rs b/src/collections/linear_fifo.rs index 4e639ca3c74b..1c763d8f5297 100644 --- a/src/collections/linear_fifo.rs +++ b/src/collections/linear_fifo.rs @@ -448,12 +448,11 @@ impl> LinearFifo { self.ensure_unused_capacity(size)?; // try to avoid realigning buffer - // reshaped for borrowck — check len, drop borrow, maybe - // realign, then take the final borrow. - if self.writable_slice(0).len() < size { + let mut slice = self.writable_slice(0); + if slice.len() < size { self.realign(); + slice = self.writable_slice(0); } - let slice = self.writable_slice(0); debug_assert!(slice.len() >= size); Ok(&mut slice[..size]) } diff --git a/src/dotenv/env_loader.rs b/src/dotenv/env_loader.rs index d1fd92cec9ca..8e5faf035d97 100644 --- a/src/dotenv/env_loader.rs +++ b/src/dotenv/env_loader.rs @@ -191,20 +191,16 @@ impl Loader { buf: &'b mut PathBuffer, ) -> Option<&'b ZStr> { // Check NODE or npm_node_execpath env var, but only use it if the file actually exists. - // NLL workaround: compute the length in an inner scope so the borrow of `buf` for the - // executable check ends before we either return a fresh borrow or fall through to `which`. - let env_len = self + if let Some(node) = self .get(b"NODE") .or_else(|| self.get(b"npm_node_execpath")) .filter(|n| !n.is_empty() && n.len() < MAX_PATH_BYTES) - .map(|node| { - buf[..node.len()].copy_from_slice(node); - buf[node.len()] = 0; - node.len() - }); - if let Some(len) = env_len { - if bun_sys::is_executable_file_path(ZStr::from_buf(&buf[..], len)) { - return Some(ZStr::from_buf(&buf[..], len)); + { + buf[..node.len()].copy_from_slice(node); + buf[node.len()] = 0; + let node_path = ZStr::from_buf(&buf[..], node.len()); + if bun_sys::is_executable_file_path(node_path) { + return Some(node_path); } } @@ -1104,17 +1100,13 @@ impl<'a> Parser<'a> { if end >= self.src.len() { return Ok(&self.src[self.src.len()..]); } - // reshaped for borrowck — `parse_quoted` returns a borrow of - // `self.value_buffer`; capture only its length, then re-borrow the buffer - // after the match so the unquoted fallthrough can re-borrow `self`. - let quoted_len: Option = match self.src[end] { - b'`' => self.parse_quoted::()?.map(|v| v.len()), - b'"' => self.parse_quoted::()?.map(|v| v.len()), - b'\'' => self.parse_quoted::()?.map(|v| v.len()), + let quoted = match self.src[end] { + b'`' => self.parse_quoted::()?, + b'"' => self.parse_quoted::()?, + b'\'' => self.parse_quoted::()?, _ => None, }; - if let Some(len) = quoted_len { - let value = &self.value_buffer[..len]; + if let Some(value) = quoted { return Ok(if IS_PROCESS { value } else { diff --git a/src/exe_format/elf.rs b/src/exe_format/elf.rs index 9d231ec56e0a..6272cb245f1d 100644 --- a/src/exe_format/elf.rs +++ b/src/exe_format/elf.rs @@ -167,17 +167,14 @@ impl ElfFile { if strtab_end > self.data.len() as u64 { return; } - // reshaped for borrowck — copy strtab bounds out so we can - // re-borrow self.data mutably below. - let strtab_off = usize::try_from(strtab_shdr.sh_offset).expect("int cast"); - let strtab_len = usize::try_from(strtab_shdr.sh_size).expect("int cast"); + let strtab = &self.data[usize::try_from(strtab_shdr.sh_offset).expect("int cast")..] + [..usize::try_from(strtab_shdr.sh_size).expect("int cast")]; for i in 0..shnum as usize { let shdr = self.read_shdr(ehdr.e_shoff, u16::try_from(i).expect("int cast")); - if shdr.sh_name as usize >= strtab_len { + if shdr.sh_name as usize >= strtab.len() { continue; } - let strtab = &self.data[strtab_off..][..strtab_len]; let name = slice_to_nul(&strtab[shdr.sh_name as usize..]); if name != b".interp" { continue; diff --git a/src/install/PackageManager/patchPackage.rs b/src/install/PackageManager/patchPackage.rs index a416be8d9293..13c8fb2aba16 100644 --- a/src/install/PackageManager/patchPackage.rs +++ b/src/install/PackageManager/patchPackage.rs @@ -1242,16 +1242,10 @@ fn overwrite_package_in_node_modules_folder( type NodeModulesIterator<'a> = tree::Iterator<'a, { tree::IteratorPathStyle::NodeModules }>; -// reshaped for borrowck — `tree::Iterator::next` returns an -// `IteratorNext<'_>` borrowing the iterator's internal `path_buf`, so we -// cannot return it from inside a `while let` (borrowck rejects the next -// iteration's reborrow even though it's unreachable). Callers only need -// `relative_path`, so copy it out into an owned `Vec`. - -fn node_modules_folder_for_dependency_ids( - iterator: &mut NodeModulesIterator<'_>, +fn node_modules_folder_for_dependency_ids<'a>( + iterator: &'a mut NodeModulesIterator<'_>, ids: &[IdPair], -) -> Option> { +) -> Option<&'a ZStr> { loop { let node_modules = iterator.next(None)?; let mut found = false; @@ -1262,21 +1256,21 @@ fn node_modules_folder_for_dependency_ids( } } if found { - return Some(node_modules.relative_path.as_bytes().to_vec()); + return Some(node_modules.relative_path); } } } -fn node_modules_folder_for_dependency_id( - iterator: &mut NodeModulesIterator<'_>, +fn node_modules_folder_for_dependency_id<'a>( + iterator: &'a mut NodeModulesIterator<'_>, dependency_id: DependencyID, -) -> Option> { +) -> Option<&'a ZStr> { loop { let node_modules = iterator.next(None)?; if !node_modules.dependencies.contains(&dependency_id) { continue; } - return Some(node_modules.relative_path.as_bytes().to_vec()); + return Some(node_modules.relative_path); } } @@ -1350,7 +1344,7 @@ fn pkg_info_for_name_and_version( Global::crash(); } }; - return (pkg_id, folder); + return (pkg_id, folder.as_bytes().to_vec()); } // we found multiple dependents of the supplied pkg + version @@ -1368,7 +1362,7 @@ fn pkg_info_for_name_and_version( } }; - return (pkg_id, folder); + return (pkg_id, folder.as_bytes().to_vec()); } // Otherwise the user did not supply a version, just the pkg name @@ -1386,7 +1380,7 @@ fn pkg_info_for_name_and_version( Global::crash(); } }; - return (pkg_id, folder); + return (pkg_id, folder.as_bytes().to_vec()); } // Otherwise we have multiple matches @@ -1419,7 +1413,7 @@ fn pkg_info_for_name_and_version( Global::crash(); } }; - return (pkg_id, folder); + return (pkg_id, folder.as_bytes().to_vec()); } bun_core::pretty_errorln!( diff --git a/src/install/isolated_install.rs b/src/install/isolated_install.rs index 12e174d959c4..cf299779ed5d 100644 --- a/src/install/isolated_install.rs +++ b/src/install/isolated_install.rs @@ -1474,10 +1474,8 @@ pub(crate) fn install_isolated_packages( } unreachable!(); }; - // Reshaped for borrowck — copy members to - // avoid holding a borrow into scc_stack while mutating. - let members: Vec = scc_stack[start..].to_vec(); - for &m in &members { + let members = &scc_stack[start..]; + for &m in members { on_stack[m as usize] = false; } if members.len() == 1 { @@ -1542,7 +1540,7 @@ pub(crate) fn install_isolated_packages( scc_ext.clear_retaining_capacity(); let mut member_sub: Vec = Vec::new(); let mut any_ineligible = false; - for &m in &members { + for &m in members { if entry_hashes[m as usize] == 0 { any_ineligible = true; } @@ -1604,7 +1602,7 @@ pub(crate) fn install_isolated_packages( h = 1; } let final_h: u64 = if any_ineligible { 0 } else { h }; - for &m in &members { + for &m in members { entry_hashes[m as usize] = final_h; } } diff --git a/src/install/isolated_install/Hardlinker.rs b/src/install/isolated_install/Hardlinker.rs index 4856f2de0c90..6664f40f6ebd 100644 --- a/src/install/isolated_install/Hardlinker.rs +++ b/src/install/isolated_install/Hardlinker.rs @@ -60,32 +60,14 @@ impl Hardlinker { #[cfg(windows)] { let mut cwd_buf = bun_paths::w_path_buffer_pool::get(); - // `get_fd_path_w` writes the raw `\\?\C:\...` result into - // `cwd_buf` and returns a SUB-SLICE (offset 4, or 6 for UNC) after - // stripping the long-path prefix. We can't keep that slice borrowed - // across the loop (borrowck vs `cwd_buf`), so capture both its start - // OFFSET and length, then reslice `cwd_buf[off..off+len]` per-iter. - // Slicing from 0 would yield `\\?\C:\…` with the last 4 chars of the - // real cwd dropped — wrong path for every project-relative hardlink. - let (dest_cwd_off, dest_cwd_len) = { - let dest_cwd: &[u16] = match sys::get_fd_path_w(Fd::cwd(), &mut cwd_buf[..]) { - Ok(s) => &*s, - Err(_) => { - return Ok(sys::Result::Err(sys::Error::from_code( - sys::E::ACCES, - sys::Tag::link, - ))); - } - }; - // SAFETY: `dest_cwd` is a sub-slice of `cwd_buf` by contract of - // `get_fd_path_w` (it returns `&mut out_buffer[off..]`). - // NB: capture `len`/`dest_ptr` first so NLL drops the `&mut cwd_buf` - // loan (held via `dest_cwd`) before `cwd_buf.as_ptr()` takes `&cwd_buf` - // — otherwise E0502 on x86_64-pc-windows-msvc. - let len = dest_cwd.len(); - let dest_ptr = dest_cwd.as_ptr(); - let off = unsafe { dest_ptr.offset_from(cwd_buf.as_ptr()) } as usize; - (off, len) + let dest_cwd: &[u16] = match sys::get_fd_path_w(Fd::cwd(), &mut cwd_buf[..]) { + Ok(s) => &*s, + Err(_) => { + return Ok(sys::Result::Err(sys::Error::from_code( + sys::E::ACCES, + sys::Tag::link, + ))); + } }; loop { @@ -131,10 +113,7 @@ impl Hardlinker { { &[dest_slice] } else { - &[ - &cwd_buf[dest_cwd_off..dest_cwd_off + dest_cwd_len], - dest_slice, - ] + &[dest_cwd, dest_slice] }; let joined = bun_paths::resolve_path::join_string_buf_w_same::< bun_paths::platform::Windows, diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index 3c8ad219bee8..bbad606bc9d1 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -776,14 +776,15 @@ impl<'a> TablePrinter<'a> { let value = cols_iter.value; // find or create the column for the property - let col_idx: usize = 'brk: { + let (col_idx, column) = 'brk: { let col_str = BunString::init(col_key); - // reshaped for borrowck — split find/append. - if let Some(idx) = - columns[1..].iter().position(|col| col.name.eql(&col_str)) + if let Some((idx, col)) = columns[1..] + .iter_mut() + .enumerate() + .find(|(_, col)| col.name.eql(&col_str)) { - break 'brk 1 + idx; + break 'brk (1 + idx, col); } // Need to ref this string because JSPropertyIterator @@ -795,11 +796,12 @@ impl<'a> TablePrinter<'a> { name: col_str, width: 1, }); - break 'brk columns.len() - 1; + let idx = columns.len() - 1; + break 'brk (idx, &mut columns[idx]); }; let cell = self.format_cell::(cell_text, value)?; - columns[col_idx].width = columns[col_idx].width.max(cell.width); + column.width = column.width.max(cell.width); let slot = col_idx - 1; if row.cells.len() <= slot { row.cells.resize(slot + 1, None); diff --git a/src/runtime/api/bun/spawn/stdio.rs b/src/runtime/api/bun/spawn/stdio.rs index a6ba3fafb659..68739681f76a 100644 --- a/src/runtime/api/bun/spawn/stdio.rs +++ b/src/runtime/api/bun/spawn/stdio.rs @@ -191,10 +191,6 @@ impl Stdio { } } - // Note: reshaped for borrowck — `remain` borrows `*self`, so we - // must drop it before mutating `self`. Shadowing ends the borrow here. - let _ = remain; - // Assigning to `*self` drops the previous variant via `Drop` // (and closes a prior `.memfd`). *self = Stdio::Memfd(fd); diff --git a/src/runtime/bake/production.rs b/src/runtime/bake/production.rs index dc586fbe917c..5a6098eadd41 100644 --- a/src/runtime/bake/production.rs +++ b/src/runtime/bake/production.rs @@ -91,9 +91,6 @@ pub fn build_command(ctx: Context) -> crate::Result<()> { Global::crash(); } }; - // Note: reshaped for borrowck — clone the cwd slice so the PathBuffer - // borrow doesn't span the rest of the function (the buffer is never reused). - let cwd: Box<[u8]> = Box::from(cwd); // Create a VM + global for loading the config file, plugins, and // performing build time prerendering. @@ -207,7 +204,7 @@ pub fn build_command(ctx: Context) -> crate::Result<()> { // LIFO order — under the API lock, before the VM is destroyed. let mut pt = PerThread::placeholder(vm_ptr); - match build_with_vm(ctx, &cwd, &mut pt) { + match build_with_vm(ctx, cwd, &mut pt) { Ok(()) => {} Err(crate::Error::JSError) => { // SAFETY: vm.global is live for VM lifetime. @@ -525,8 +522,6 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< let mut root_dir_buf = PathBuffer::uninit(); let root_dir_path = resolve_path::join_abs_string_buf::(cwd, &mut root_dir_buf.0, &[b"dist"]); - // Note: reshaped for borrowck — copy out so root_dir_buf can drop. - let root_dir_path: Box<[u8]> = Box::from(root_dir_path); // Note: borrowck — `framework` is `&mut options.framework`; reborrow // through it instead of `options.framework` to avoid stacking borrows. @@ -1178,7 +1173,7 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< let render_promise = unsafe { &mut *BakeRenderRoutesForProdStatic( global, - BunString::init(&*root_dir_path), + BunString::init(root_dir_path), pt.all_server_files.as_ref().unwrap().get(), server_render_funcs, server_param_funcs, diff --git a/src/runtime/cli/upgrade_command.rs b/src/runtime/cli/upgrade_command.rs index fcd16bf1b0bf..8325b8db3bda 100644 --- a/src/runtime/cli/upgrade_command.rs +++ b/src/runtime/cli/upgrade_command.rs @@ -900,9 +900,6 @@ impl UpgradeCommand { .expect("oom"); let mut buf = PathBuffer::uninit(); - // Separate fallback buffer — borrowck holds `buf` for the lifetime - // of `which`'s returned `Option<&ZStr>` even across the `None` arm. - let mut buf2 = PathBuffer::uninit(); let powershell_path: &ZStr = match which( &mut buf, bun_core::env_var::PATH.get().unwrap_or(b""), @@ -917,7 +914,7 @@ impl UpgradeCommand { let hardcoded_system_powershell = bun_paths::join_abs_string_buf_z::( system_root, - &mut buf2[..], + &mut buf[..], &[ system_root, b"System32\\WindowsPowerShell\\v1.0\\powershell.exe", diff --git a/src/runtime/dns_jsc/dns.rs b/src/runtime/dns_jsc/dns.rs index d80a70f830c0..ebce00ba2a3d 100644 --- a/src/runtime/dns_jsc/dns.rs +++ b/src/runtime/dns_jsc/dns.rs @@ -4693,9 +4693,6 @@ impl Resolver { *poll_entry.value_ptr } else { let new_poll = UvDnsPoll::new(this_ptr, fd); - // Publish into the map first so the `GetOrPutResult` borrow can - // end (NLL) before we may need to `swap_remove` on init failure. - *poll_entry.value_ptr = new_poll; // SAFETY: `Loop::get()` is the live per-thread uws loop; // `new_poll` is a fresh heap allocation with a zeroed `uv_poll_t`. if unsafe { @@ -4706,6 +4703,7 @@ impl Resolver { let _ = polls.swap_remove(&fd); return; } + *poll_entry.value_ptr = new_poll; new_poll }; diff --git a/src/runtime/node/node_fs.rs b/src/runtime/node/node_fs.rs index d29acb23a245..c53f9eb0f31d 100644 --- a/src/runtime/node/node_fs.rs +++ b/src/runtime/node/node_fs.rs @@ -10175,12 +10175,7 @@ fn zig_delete_tree_min_stack_size_with_kind_hint( // ever store a single path component that was returned from the // filesystem. let mut dir_name_buf = PathBuffer::uninit(); - let mut dir_name_len = sub_path.len().min(dir_name_buf.len()); - dir_name_buf[..dir_name_len].copy_from_slice(&sub_path[..dir_name_len]); - // `dir_name` conceptually aliases either `sub_path` or `dir_name_buf`; - // the borrow checker won't let that alias survive the copy/reassignment - // below, so track `(is_sub_path, len)` and re-slice on each use. - let mut dir_name_is_sub_path = true; + let mut dir_name: &[u8] = sub_path; // Here we must avoid recursion, in order to provide O(1) memory guarantee of this function. // Go through each entry and if it is not a directory, delete it. If it is a directory, @@ -10203,8 +10198,7 @@ fn zig_delete_tree_min_stack_size_with_kind_hint( dir = new_dir; let n = entry_name.len().min(dir_name_buf.len()); dir_name_buf[..n].copy_from_slice(&entry_name[..n]); - dir_name_len = n; - dir_name_is_sub_path = false; + dir_name = &dir_name_buf[..n]; continue 'scan_dir; } Err(E::ENOTDIR) => { @@ -10241,11 +10235,6 @@ fn zig_delete_tree_min_stack_size_with_kind_hint( // Now to remove the directory itself. dir.close(); - let dir_name: &[u8] = if dir_name_is_sub_path { - sub_path - } else { - &dir_name_buf[..dir_name_len] - }; if let Some(d) = cleanup_dir_parent { match dt_delete_dir(&d, dir_name) { Ok(()) | Err(E::ENOENT) | Err(E::ENOTEMPTY) | Err(E::EEXIST) => { diff --git a/src/runtime/node/path.rs b/src/runtime/node/path.rs index d4da1a4db33d..889bdc2d991f 100644 --- a/src/runtime/node/path.rs +++ b/src/runtime/node/path.rs @@ -1087,11 +1087,10 @@ fn format_t<'a, T: PathCharCwd>( // const base = pathObject.base || // `${pathObject.name || ''}${formatExt(pathObject.ext)}`; let mut base_len = base.len(); - // Borrowck: track range into buf instead of slice. - let base_or_name_ext_range: (usize, usize) = if base_len > 0 { + let base_or_name_ext = if base_len > 0 { memmove(&mut buf[0..base_len], base); - (0, base_len) + &buf[0..base_len] } else { let formatted_ext_len = { // Borrowck: inline format_ext_t to avoid overlapping &mut. @@ -1120,9 +1119,9 @@ fn format_t<'a, T: PathCharCwd>( memmove(&mut buf[0..name_len], _name); } if buf_size > 0 { - (0, buf_size) + &buf[0..buf_size] } else { - (0, base_len) + &buf[0..base_len] } }; @@ -1131,20 +1130,17 @@ fn format_t<'a, T: PathCharCwd>( // return base; // } if dir_len == 0 { - return &buf[base_or_name_ext_range.0..base_or_name_ext_range.1]; + return base_or_name_ext; } // Translated from the following JS code: // return dir === pathObject.root ? `${dir}${base}` : `${dir}${sep}${base}`; - base_len = base_or_name_ext_range.1 - base_or_name_ext_range.0; + base_len = base_or_name_ext.len(); if base_len > 0 { buf_offset = if dir_is_root { dir_len } else { dir_len + 1 }; // Move all bytes to the right by dirLen + (maybe 1 for the separator). // Use copy_within because baseOrNameExt and buf overlap. - buf.copy_within( - base_or_name_ext_range.0..base_or_name_ext_range.1, - buf_offset, - ); + buf.copy_within(0..base_len, buf_offset); } memmove(&mut buf[0..dir_len], dir_or_root); buf_size = dir_len + base_len; @@ -1340,8 +1336,7 @@ fn join_posix_t<'a, T: PathCharCwd>( let mut buf_offset: usize; // Back joined by expandable buf2 in case it is long. - // Borrowck: track length instead of slice into buf2. - let mut joined_len: usize = 0; + let mut joined: &[T] = &[]; for path in paths { // validateString of `path is performed in pub fn join. @@ -1363,13 +1358,13 @@ fn join_posix_t<'a, T: PathCharCwd>( buf_size += len; memmove(&mut buf2[buf_offset..buf_size], path); - joined_len = buf_size; + joined = &buf2[0..buf_size]; } } if buf_size == 0 { return l::(CHAR_STR_DOT); } - normalize_posix_t(&buf2[0..joined_len], buf) + normalize_posix_t(joined, buf) } /// # Safety @@ -3475,10 +3470,9 @@ fn to_namespaced_path_windows_t<'a, T: PathCharCwd>( ) -> MaybeSlice<'a, T> { // validateString of `path` is performed in pub fn toNamespacedPath. // Backed by buf. - // Borrowck: capture length, then re-borrow buf. - let resolved_len = resolve_windows_t(&[path], buf, buf2)?.len(); + let resolved = resolve_windows_t(&[path], buf, buf2)?; - let len = resolved_len; + let len = resolved.len(); if len <= 2 { buf[0..path.len()].copy_from_slice(path); buf[path.len()] = T::default(); @@ -3488,11 +3482,11 @@ fn to_namespaced_path_windows_t<'a, T: PathCharCwd>( let buf_offset: usize; let buf_size: usize; - let byte0 = buf[0]; + let byte0 = resolved[0]; if byte0 == T::from_u8(CHAR_BACKWARD_SLASH) { // Possible UNC root - if buf[1] == T::from_u8(CHAR_BACKWARD_SLASH) { - let byte2 = buf[2]; + if resolved[1] == T::from_u8(CHAR_BACKWARD_SLASH) { + let byte2 = resolved[2]; if byte2 != T::from_u8(CHAR_QUESTION_MARK) && byte2 != T::from_u8(CHAR_DOT) { // Matched non-long UNC root, convert the path to a long UNC path @@ -3517,8 +3511,8 @@ fn to_namespaced_path_windows_t<'a, T: PathCharCwd>( } } } else if is_windows_device_root_t(byte0) - && buf[1] == T::from_u8(CHAR_COLON) - && buf[2] == T::from_u8(CHAR_BACKWARD_SLASH) + && resolved[1] == T::from_u8(CHAR_COLON) + && resolved[2] == T::from_u8(CHAR_BACKWARD_SLASH) { // Matched device root, convert the path to a long UNC path @@ -3536,7 +3530,7 @@ fn to_namespaced_path_windows_t<'a, T: PathCharCwd>( buf[buf_size] = T::default(); return Ok(&buf[0..buf_size]); } - Ok(&buf[0..resolved_len]) + Ok(resolved) } fn to_namespaced_path_windows_js_t( diff --git a/src/runtime/node/types.rs b/src/runtime/node/types.rs index 995e5ca1df13..ecc1c5e526c8 100644 --- a/src/runtime/node/types.rs +++ b/src/runtime/node/types.rs @@ -960,20 +960,15 @@ impl PathLikeExt for PathLike { // SAFETY: buf[4+n] == 0 written above. return ZStr::from_buf(&buf[..], 4 + n); } - // reshaped for borrowck — capture the length so - // the `Ok` borrow ends at the match, then re-derive. - let resolved_len = match bun_paths::resolve_path::PosixToWinNormalizer::resolve_cwd_with_external_buf_z(buf, sliced) { - Ok(res) => Some(res.len()), + match bun_paths::resolve_path::PosixToWinNormalizer::resolve_cwd_with_external_buf_z( + buf, sliced, + ) { + Ok(res) => return res, // The cwd root + path don't fit `buf` (UNC cwds can push // a near-MAX_PATH_BYTES path over); fall through to the // plain copy / too-long handling below. - Err(bun_paths::Error::Sys(bun_errno::SystemErrno::ENAMETOOLONG)) => None, + Err(bun_paths::Error::Sys(bun_errno::SystemErrno::ENAMETOOLONG)) => {} Err(e) => panic!("Error while resolving path: {e:?}"), - }; - if let Some(len) = resolved_len { - // SAFETY: `resolve_cwd_with_external_buf_z` wrote the NUL - // at `buf[len]`. - return ZStr::from_buf(&buf[..], len); } } } diff --git a/src/runtime/socket/Listener.rs b/src/runtime/socket/Listener.rs index 8164c943e028..28dcdc960f7b 100644 --- a/src/runtime/socket/Listener.rs +++ b/src/runtime/socket/Listener.rs @@ -1127,15 +1127,12 @@ impl Listener { use bun_sys::FdExt as _; let mut buf = PathBuffer::uninit(); - // Note: reshaped for borrowck — `normalize_pipe_name` borrows - // `buf` for the returned slice; store length and re-borrow after the - // `connection` match drops. - let mut pipe_name_len: Option = None; + let mut pipe_name: Option<&[u8]> = None; let is_named_pipe = match &mut connection { // we check if the path is a named pipe otherwise we try to connect using AF_UNIX UnixOrHost::Unix(slice) => match normalize_pipe_name(slice, buf.as_mut_slice()) { Some(name) => { - pipe_name_len = Some(name.len()); + pipe_name = Some(name); true } None => false, @@ -1242,7 +1239,7 @@ impl Listener { let named_pipe_result = match tls_ref.connection.get().as_ref().unwrap() { UnixOrHost::Unix(_) => WindowsNamedPipeContext::connect( global, - &buf[..pipe_name_len.unwrap()], + pipe_name.unwrap(), ssl_taken.take(), ctx_for_pipe, PipeSocketType::Tls(tls_ref), @@ -1315,7 +1312,7 @@ impl Listener { let named_pipe_result = match tcp_ref.connection.get().as_ref().unwrap() { UnixOrHost::Unix(_) => WindowsNamedPipeContext::connect( global, - &buf[..pipe_name_len.unwrap()], + pipe_name.unwrap(), None, None, PipeSocketType::Tcp(tcp_ref), diff --git a/src/runtime/webcore/Body.rs b/src/runtime/webcore/Body.rs index 93041722ba08..4c895df5bd52 100644 --- a/src/runtime/webcore/Body.rs +++ b/src/runtime/webcore/Body.rs @@ -1547,11 +1547,6 @@ impl Value { _ => {} } - // reshaped for borrowck — re-borrow locked after the early *self = Null path above. - let Value::Locked(locked) = self else { - unreachable!() - }; - reader.producer.set(locked.producer); let context_ptr: *mut ByteStream = &raw mut reader.context; diff --git a/src/sql_jsc/postgres/DataCell.rs b/src/sql_jsc/postgres/DataCell.rs index 153924396a5b..23b11c25a8ec 100644 --- a/src/sql_jsc/postgres/DataCell.rs +++ b/src/sql_jsc/postgres/DataCell.rs @@ -422,10 +422,6 @@ fn parse_array( let mut has_exponent = false; let mut has_negative_sign = false; let mut has_positive_sign = false; - // reshaped for borrowck — cannot mutate `slice` mid-loop while - // iterating it (the Infinity arm). We capture the advance amount and - // apply after the loop. - let mut advance_after: Option = None; for (index, &byte) in slice.iter().enumerate() { match byte { b'0'..=b'9' => {} @@ -491,7 +487,7 @@ fn parse_array( } else { array.push(SQLDataCell::float8(val)); } - advance_after = Some(8 + (is_negative as usize)); + slice = try_slice(slice, 8 + (is_negative as usize)); break; } @@ -502,9 +498,6 @@ fn parse_array( } } } - if let Some(n) = advance_after { - slice = try_slice(slice, n); - } if is_infinity { continue; } From 9e5b3c985311ceb4417b6ce81c6fa678b8aebb7e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:20:28 +0000 Subject: [PATCH 04/13] test: cover the control flow rewritten in the polonius cleanup Adds cases for Bun.which PATH-segment iteration (hit in a later segment after misses), path.win32.toNamespacedPath branch coverage (long-path fall-through, device root, UNC conversion, dot-dot resolution), and inspect.table column discovery across heterogeneous rows. --- .../bun-inspect-table.test.ts.snap | 24 +++++++++++++++++++ test/js/bun/console/bun-inspect-table.test.ts | 3 +++ test/js/bun/util/which.test.ts | 19 +++++++++++++++ test/js/node/path/to-namespaced-path.test.js | 11 +++++++++ 4 files changed, 57 insertions(+) diff --git a/test/js/bun/console/__snapshots__/bun-inspect-table.test.ts.snap b/test/js/bun/console/__snapshots__/bun-inspect-table.test.ts.snap index f166e92f896f..320a9403b54e 100644 --- a/test/js/bun/console/__snapshots__/bun-inspect-table.test.ts.snap +++ b/test/js/bun/console/__snapshots__/bun-inspect-table.test.ts.snap @@ -287,3 +287,27 @@ exports[`inspect.table (with colors in 2nd position) { a: 1, b: 2 } 2`] = ` └───┴────────┘ " `; + +exports[`inspect.table [ { a: 1 }, { b: 2 }, { a: 3, c: 4 }, { a: 6, c: 5, d: 7 } ] 1`] = ` +"┌───┬───┬───┬───┬───┐ +│ │ a │ b │ c │ d │ +├───┼───┼───┼───┼───┤ +│ 0 │ 1 │ │ │ │ +│ 1 │ │ 2 │ │ │ +│ 2 │ 3 │ │ 4 │ │ +│ 3 │ 6 │ │ 5 │ 7 │ +└───┴───┴───┴───┴───┘ +" +`; + +exports[`inspect.table (ansi) [ { a: 1 }, { b: 2 }, { a: 3, c: 4 }, { a: 6, c: 5, d: 7 } ] 1`] = ` +"┌───┬───┬───┬───┬───┐ +│ \x1B[0m\x1B[1m \x1B[0m │ \x1B[0m\x1B[1ma\x1B[0m │ \x1B[0m\x1B[1mb\x1B[0m │ \x1B[0m\x1B[1mc\x1B[0m │ \x1B[0m\x1B[1md\x1B[0m │ +├───┼───┼───┼───┼───┤ +│ 0 │ \x1B[0m\x1B[33m1\x1B[0m │ │ │ │ +│ 1 │ │ \x1B[0m\x1B[33m2\x1B[0m │ │ │ +│ 2 │ \x1B[0m\x1B[33m3\x1B[0m │ │ \x1B[0m\x1B[33m4\x1B[0m │ │ +│ 3 │ \x1B[0m\x1B[33m6\x1B[0m │ │ \x1B[0m\x1B[33m5\x1B[0m │ \x1B[0m\x1B[33m7\x1B[0m │ +└───┴───┴───┴───┴───┘ +" +`; diff --git a/test/js/bun/console/bun-inspect-table.test.ts b/test/js/bun/console/bun-inspect-table.test.ts index 951c22318da5..0d46e5b9fa7a 100644 --- a/test/js/bun/console/bun-inspect-table.test.ts +++ b/test/js/bun/console/bun-inspect-table.test.ts @@ -18,6 +18,9 @@ const inputs = [ [1, 2, 3], ["a", 1, "b", 2, "c", 3], [/a/, 1, /b/, 2, /c/, 3], + // columns discovered across rows: later rows re-find earlier columns and + // append new ones, in first-seen order + [{ a: 1 }, { b: 2 }, { a: 3, c: 4 }, { c: 5, a: 6, d: 7 }], ]; describe("inspect.table", () => { diff --git a/test/js/bun/util/which.test.ts b/test/js/bun/util/which.test.ts index 55762ea6e797..7698b00ae15f 100644 --- a/test/js/bun/util/which.test.ts +++ b/test/js/bun/util/which.test.ts @@ -270,3 +270,22 @@ test("Bun.which can find executables in a non-ascii directory", async () => { process.chdir(cwd); } }); + +test("Bun.which finds a bin in a later PATH segment after earlier misses", async () => { + await using dir = tempDir("which-path-segments", { + "first/.keep": "", + "third/prog_in_third": "#!/usr/bin/env sh\necho posix\nexit 0\n", + "third/prog_in_third.cmd": "@echo win32\n@exit 0\n", + }); + const d = String(dir); + if (!isWindows) { + await $`chmod +x ${join(d, "third/prog_in_third")}`; + } + + const delim = isWindows ? ";" : ":"; + // first segment exists but misses, second doesn't exist, third hits + const PATH = [join(d, "first"), join(d, "does-not-exist"), join(d, "third")].join(delim); + const suffix = isWindows ? ".cmd" : ""; + expect(which("prog_in_third", { PATH })).toBe(join(d, "third", "prog_in_third" + suffix)); + expect(which("prog_in_nowhere", { PATH })).toBe(null); +}); diff --git a/test/js/node/path/to-namespaced-path.test.js b/test/js/node/path/to-namespaced-path.test.js index 19893dd6787e..a2ed199ce71b 100644 --- a/test/js/node/path/to-namespaced-path.test.js +++ b/test/js/node/path/to-namespaced-path.test.js @@ -70,6 +70,17 @@ describe("path.toNamespacedPath", () => { assert.strictEqual(path.win32.toNamespacedPath(emptyObj), emptyObj); }); + test("win32 branch coverage", () => { + // already-namespaced input is returned as resolved, unchanged + assert.strictEqual(path.win32.toNamespacedPath("\\\\?\\UNC\\a\\b"), "\\\\?\\UNC\\a\\b"); + // device root, trailing separator preserved + assert.strictEqual(path.win32.toNamespacedPath("C:\\"), "\\\\?\\C:\\"); + // resolve() collapses dot-dot segments and keeps the drive letter's case + assert.strictEqual(path.win32.toNamespacedPath("c:/foo/../bar"), "\\\\?\\c:\\bar"); + // UNC root conversion inserts the UNC prefix and keeps the trailing slash + assert.strictEqual(path.win32.toNamespacedPath("\\\\server\\share"), "\\\\?\\UNC\\server\\share\\"); + }); + test("posix", () => { assert.strictEqual(path.posix.toNamespacedPath("/foo/bar"), "/foo/bar"); assert.strictEqual(path.posix.toNamespacedPath("foo/bar"), "foo/bar"); From a5dcc4d41795c3b5dc2409af779ffeaf419ba433 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:22:49 +0000 Subject: [PATCH 05/13] [autofix.ci] apply automated fixes --- src/runtime/cli/open.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/runtime/cli/open.rs b/src/runtime/cli/open.rs index 0c0657b7d2a4..277b0d80f501 100644 --- a/src/runtime/cli/open.rs +++ b/src/runtime/cli/open.rs @@ -517,11 +517,9 @@ impl EditorContext { } // Don't know, so we will just guess based on what exists - if let Some((editor_, bin)) = Editor::by_fallback( - env, - &mut buf, - Fs::FileSystem::instance().top_level_dir, - ) { + if let Some((editor_, bin)) = + Editor::by_fallback(env, &mut buf, Fs::FileSystem::instance().top_level_dir) + { self.editor = Some(editor_); self.path = Fs::FileSystem::instance() .dirname_store From 2851dd287b96d931b7d10f7199d125c60aa9438b Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:34:50 +0000 Subject: [PATCH 06/13] test: address review feedback on table ordering and chmod Make the table column-discovery input non-alphabetical so the snapshot distinguishes first-seen order from lexical sorting, add an unsorted variant, and use chmodSync instead of shelling out. --- .../bun-inspect-table.test.ts.snap | 48 ++++++++++++------- test/js/bun/console/bun-inspect-table.test.ts | 11 ++++- test/js/bun/util/which.test.ts | 2 +- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/test/js/bun/console/__snapshots__/bun-inspect-table.test.ts.snap b/test/js/bun/console/__snapshots__/bun-inspect-table.test.ts.snap index 320a9403b54e..e5f146bdd788 100644 --- a/test/js/bun/console/__snapshots__/bun-inspect-table.test.ts.snap +++ b/test/js/bun/console/__snapshots__/bun-inspect-table.test.ts.snap @@ -288,26 +288,38 @@ exports[`inspect.table (with colors in 2nd position) { a: 1, b: 2 } 2`] = ` " `; -exports[`inspect.table [ { a: 1 }, { b: 2 }, { a: 3, c: 4 }, { a: 6, c: 5, d: 7 } ] 1`] = ` -"┌───┬───┬───┬───┬───┐ -│ │ a │ b │ c │ d │ -├───┼───┼───┼───┼───┤ -│ 0 │ 1 │ │ │ │ -│ 1 │ │ 2 │ │ │ -│ 2 │ 3 │ │ 4 │ │ -│ 3 │ 6 │ │ 5 │ 7 │ -└───┴───┴───┴───┴───┘ +exports[`inspect.table [ { d: 1 }, { b: 2 }, { a: 4, d: 3 }, { a: 5, c: 6, e: 7 } ] 1`] = ` +"┌───┬───┬───┬───┬───┬───┐ +│ │ d │ b │ a │ c │ e │ +├───┼───┼───┼───┼───┼───┤ +│ 0 │ 1 │ │ │ │ │ +│ 1 │ │ 2 │ │ │ │ +│ 2 │ 3 │ │ 4 │ │ │ +│ 3 │ │ │ 5 │ 6 │ 7 │ +└───┴───┴───┴───┴───┴───┘ " `; -exports[`inspect.table (ansi) [ { a: 1 }, { b: 2 }, { a: 3, c: 4 }, { a: 6, c: 5, d: 7 } ] 1`] = ` -"┌───┬───┬───┬───┬───┐ -│ \x1B[0m\x1B[1m \x1B[0m │ \x1B[0m\x1B[1ma\x1B[0m │ \x1B[0m\x1B[1mb\x1B[0m │ \x1B[0m\x1B[1mc\x1B[0m │ \x1B[0m\x1B[1md\x1B[0m │ -├───┼───┼───┼───┼───┤ -│ 0 │ \x1B[0m\x1B[33m1\x1B[0m │ │ │ │ -│ 1 │ │ \x1B[0m\x1B[33m2\x1B[0m │ │ │ -│ 2 │ \x1B[0m\x1B[33m3\x1B[0m │ │ \x1B[0m\x1B[33m4\x1B[0m │ │ -│ 3 │ \x1B[0m\x1B[33m6\x1B[0m │ │ \x1B[0m\x1B[33m5\x1B[0m │ \x1B[0m\x1B[33m7\x1B[0m │ -└───┴───┴───┴───┴───┘ +exports[`inspect.table columns keep first-seen order across rows when not sorted 1`] = ` +"┌───┬───┬───┬───┬───┬───┐ +│ │ d │ b │ a │ c │ e │ +├───┼───┼───┼───┼───┼───┤ +│ 0 │ 1 │ │ │ │ │ +│ 1 │ │ 2 │ │ │ │ +│ 2 │ 3 │ │ 4 │ │ │ +│ 3 │ │ │ 5 │ 6 │ 7 │ +└───┴───┴───┴───┴───┴───┘ +" +`; + +exports[`inspect.table (ansi) [ { d: 1 }, { b: 2 }, { a: 4, d: 3 }, { a: 5, c: 6, e: 7 } ] 1`] = ` +"┌───┬───┬───┬───┬───┬───┐ +│ \x1B[0m\x1B[1m \x1B[0m │ \x1B[0m\x1B[1md\x1B[0m │ \x1B[0m\x1B[1mb\x1B[0m │ \x1B[0m\x1B[1ma\x1B[0m │ \x1B[0m\x1B[1mc\x1B[0m │ \x1B[0m\x1B[1me\x1B[0m │ +├───┼───┼───┼───┼───┼───┤ +│ 0 │ \x1B[0m\x1B[33m1\x1B[0m │ │ │ │ │ +│ 1 │ │ \x1B[0m\x1B[33m2\x1B[0m │ │ │ │ +│ 2 │ \x1B[0m\x1B[33m3\x1B[0m │ │ \x1B[0m\x1B[33m4\x1B[0m │ │ │ +│ 3 │ │ │ \x1B[0m\x1B[33m5\x1B[0m │ \x1B[0m\x1B[33m6\x1B[0m │ \x1B[0m\x1B[33m7\x1B[0m │ +└───┴───┴───┴───┴───┴───┘ " `; diff --git a/test/js/bun/console/bun-inspect-table.test.ts b/test/js/bun/console/bun-inspect-table.test.ts index 0d46e5b9fa7a..df41d7fa480e 100644 --- a/test/js/bun/console/bun-inspect-table.test.ts +++ b/test/js/bun/console/bun-inspect-table.test.ts @@ -19,8 +19,8 @@ const inputs = [ ["a", 1, "b", 2, "c", 3], [/a/, 1, /b/, 2, /c/, 3], // columns discovered across rows: later rows re-find earlier columns and - // append new ones, in first-seen order - [{ a: 1 }, { b: 2 }, { a: 3, c: 4 }, { c: 5, a: 6, d: 7 }], + // append new ones + [{ d: 1 }, { b: 2 }, { d: 3, a: 4 }, { a: 5, c: 6, e: 7 }], ]; describe("inspect.table", () => { @@ -30,6 +30,13 @@ describe("inspect.table", () => { }); }); + test("columns keep first-seen order across rows when not sorted", () => { + // d, b, a, c, e: discovery order is deliberately non-alphabetical so this + // fails if discovered columns were ever sorted instead of appended + expect(inspect.table([{ d: 1 }, { b: 2 }, { d: 3, a: 4 }, { a: 5, c: 6, e: 7 }], { colors: false })) + .toMatchSnapshot(); + }); + it.each([ null, undefined, diff --git a/test/js/bun/util/which.test.ts b/test/js/bun/util/which.test.ts index 7698b00ae15f..3c2502a979e5 100644 --- a/test/js/bun/util/which.test.ts +++ b/test/js/bun/util/which.test.ts @@ -279,7 +279,7 @@ test("Bun.which finds a bin in a later PATH segment after earlier misses", async }); const d = String(dir); if (!isWindows) { - await $`chmod +x ${join(d, "third/prog_in_third")}`; + chmodSync(join(d, "third/prog_in_third"), 0o755); } const delim = isWindows ? ";" : ":"; From 9b05b6bdcf3c6ac1deb64e64e106258242250d52 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:37:10 +0000 Subject: [PATCH 07/13] [autofix.ci] apply automated fixes --- test/js/bun/console/bun-inspect-table.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/js/bun/console/bun-inspect-table.test.ts b/test/js/bun/console/bun-inspect-table.test.ts index df41d7fa480e..2bf36b015237 100644 --- a/test/js/bun/console/bun-inspect-table.test.ts +++ b/test/js/bun/console/bun-inspect-table.test.ts @@ -33,8 +33,9 @@ describe("inspect.table", () => { test("columns keep first-seen order across rows when not sorted", () => { // d, b, a, c, e: discovery order is deliberately non-alphabetical so this // fails if discovered columns were ever sorted instead of appended - expect(inspect.table([{ d: 1 }, { b: 2 }, { d: 3, a: 4 }, { a: 5, c: 6, e: 7 }], { colors: false })) - .toMatchSnapshot(); + expect( + inspect.table([{ d: 1 }, { b: 2 }, { d: 3, a: 4 }, { a: 5, c: 6, e: 7 }], { colors: false }), + ).toMatchSnapshot(); }); it.each([ From 850981a574a2b01b138074d9f49175977d6a6f3e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:46:54 +0000 Subject: [PATCH 08/13] build: fix stale comments about polonius sites and windows config sections --- scripts/build/cargo-config.ts | 13 +++++++------ scripts/build/rust.ts | 11 ++++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/scripts/build/cargo-config.ts b/scripts/build/cargo-config.ts index 39a333dba1f5..bc8f109145cb 100644 --- a/scripts/build/cargo-config.ts +++ b/scripts/build/cargo-config.ts @@ -60,9 +60,10 @@ function linkerFor(triple: string, cfg: Config): string { * Write `.cargo/config.toml` next to the workspace `Cargo.toml` (repo root). * Returns the absolute path written. * - * Windows-msvc targets are omitted: the MSVC linker isn't a clang driver and - * doesn't take `-fuse-ld=lld`; that path is handled entirely via env in - * `rust.ts` (`CARGO_TARGET_..._LINKER = cfg.msvcLinker`). + * Windows-msvc targets get a rustflags-only section (no `linker =` line): + * the MSVC linker isn't a clang driver and doesn't take `-fuse-ld=lld`; + * that path is handled entirely via env in `rust.ts` + * (`CARGO_TARGET_..._LINKER = cfg.msvcLinker`). */ export function generateCargoConfig(cfg: Config): string { const outPath = resolve(cfg.cwd, ".cargo", "config.toml"); @@ -82,9 +83,9 @@ export function generateCargoConfig(cfg: Config): string { // `-Zpolonius=next` everywhere: workspace code relies on the polonius // borrow checker (see the matching push in rust.ts), so every rustc // invocation that type-checks workspace crates needs it or borrowck - // fails. Windows-msvc triples get a section for it too (they are - // otherwise omitted — the MSVC linker path is env-only, see above) so - // `cargo check --target *-windows-msvc` / `rust:check-all` work. + // fails. Windows-msvc triples get a rustflags-only section (their linker + // is env-only, see the doc comment above) so `cargo check --target + // *-windows-msvc` / `rust:check-all` work. const polonius = `"-Z", "polonius=next"`; for (const triple of allRustTargets) { lines.push(""); diff --git a/scripts/build/rust.ts b/scripts/build/rust.ts index 9145f7c875ae..5535d914580b 100644 --- a/scripts/build/rust.ts +++ b/scripts/build/rust.ts @@ -412,11 +412,12 @@ export function cargoBuildInvocation(cfg: Config): CargoInvocation { } // Polonius alpha borrow checker: accepts NLL "problem case 3" (a borrow // returned/escaping on one path no longer blocks the other paths), which - // workspace code now relies on — grep `polonius` in src/ for the patterns. - // Nightly-only; the pinned toolchain is nightly. Must stay in sync with the - // generated `.cargo/config.toml` (cargo-config.ts) so plain `cargo check`, - // rust-analyzer, and `rust:check-all` accept the same code the ninja build - // does. + // workspace code now relies on — e.g. `src/collections/linear_fifo.rs` + // no longer compiles under the stock checker. Nightly-only; the pinned + // toolchain is nightly. Must stay in sync with the generated + // `.cargo/config.toml` (cargo-config.ts) so plain `cargo check`, + // rust-analyzer, and `rust:check-all` accept the same code the ninja + // build does. rustflags.push("-Zpolonius=next"); // rustc does not emit `.llvm_addrsig` by default on *any* target (verified // empirically — Linux-gnu, musl, darwin, msvc all missing it). lld's From cf1687175753587ad56219f8f985d43ad1cb13b0 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:08:31 +0000 Subject: [PATCH 09/13] test: pin -Zpolonius=next in the cargo edge and generated cargo config Removing the flag from rust.ts already fails the build (bun_collections no longer compiles under the stock borrow checker), but nothing guarded the generated .cargo/config.toml, which plain cargo check, rust:check-all and rust-analyzer rely on. Covers every target triple, including the rustflags-only windows-msvc sections. --- test/internal/rust-polonius-flag.test.ts | 151 +++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 test/internal/rust-polonius-flag.test.ts diff --git a/test/internal/rust-polonius-flag.test.ts b/test/internal/rust-polonius-flag.test.ts new file mode 100644 index 000000000000..dac68e4d6b27 --- /dev/null +++ b/test/internal/rust-polonius-flag.test.ts @@ -0,0 +1,151 @@ +/** + * The workspace relies on the polonius borrow checker (-Zpolonius=next), so + * every rustc invocation that type-checks workspace crates has to carry the + * flag: the ninja cargo edge (rust.ts, via CARGO_ENCODED_RUSTFLAGS) and the + * generated .cargo/config.toml (cargo-config.ts) used by plain cargo, + * rust:check-all and rust-analyzer. Configure-time logic only; nothing here + * spawns a compiler. + */ +import { describe, expect, test } from "bun:test"; +import { tempDir } from "harness"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { generateCargoConfig } from "../../scripts/build/cargo-config.ts"; +import { resolveConfig, type Config, type PartialConfig, type Toolchain } from "../../scripts/build/config.ts"; +import { allRustTargets, cargoBuildInvocation } from "../../scripts/build/rust.ts"; + +/** A fully-populated fake toolchain; resolveConfig never spawns any of these. */ +function mockToolchain(): Toolchain { + return { + cc: "/fake/llvm/bin/clang", + cxx: "/fake/llvm/bin/clang++", + clangVersion: "21.1.8", + clangResourceDir: "/fake/llvm/lib/clang/21", + ar: "/fake/llvm/bin/llvm-ar", + ranlib: "/fake/llvm/bin/llvm-ranlib", + ld: "/fake/llvm/bin/ld.lld", + ld64Lld: "/fake/llvm/bin/ld64.lld", + rustLld: undefined, + rustLlvmVersion: "22.1.4", + rustSysroot: undefined, + rustHostTriple: undefined, + strip: "/fake/bin/strip", + llvmStrip: "/fake/llvm/bin/llvm-strip", + dsymutil: "/fake/llvm/bin/dsymutil", + bun: "/fake/bin/bun", + jsRuntime: "/fake/bin/bun", + esbuild: "/fake/bin/esbuild", + ccache: undefined, + cmake: "/fake/bin/cmake", + cargo: "/fake/bin/cargo", + cargoHome: undefined, + rustupHome: undefined, + msvcLinker: "/fake/msvc/lld-link", + rc: undefined, + mt: undefined, + nasm: undefined, + }; +} + +const configs: Record = { + "linux x64 debug": { + os: "linux", + arch: "x64", + abi: "gnu", + buildType: "Debug", + assertions: true, + linuxSysroot: "/fake", + }, + "linux aarch64 release (ci)": { + os: "linux", + arch: "aarch64", + abi: "gnu", + buildType: "Release", + ci: true, + buildkite: false, + linuxSysroot: "/fake", + }, + "linux x64 release-asan": { + os: "linux", + arch: "x64", + abi: "gnu", + buildType: "Release", + asan: true, + linuxSysroot: "/fake", + }, + "windows x64 release (ci)": { + os: "windows", + arch: "x64", + buildType: "Release", + ci: true, + buildkite: false, + winsysroot: "/fake/winsysroot", + }, + "darwin aarch64 release": { os: "darwin", arch: "aarch64", buildType: "Release" }, +}; + +function resolve(partial: PartialConfig): Config { + return resolveConfig(partial, mockToolchain()); +} + +/** Decode the U+001F-separated CARGO_ENCODED_RUSTFLAGS the ninja edge sets. */ +function encodedRustflags(cfg: Config): string[] { + const encoded = cargoBuildInvocation(cfg).env.CARGO_ENCODED_RUSTFLAGS; + expect(encoded).toBeString(); + return encoded.split("\x1f"); +} + +describe("-Zpolonius=next reaches every rustc invocation", () => { + test.each(Object.entries(configs))("ninja cargo edge: %s", (_name, partial) => { + expect(encodedRustflags(resolve(partial))).toContain("-Zpolonius=next"); + }); + + test("generated .cargo/config.toml carries the flag for every target triple", () => { + using dir = tempDir("polonius-cargo-config", {}); + const cfg: Config = { ...resolve(configs["linux x64 debug"]), cwd: String(dir) }; + + const written = generateCargoConfig(cfg); + expect(written).toBe(join(String(dir), ".cargo", "config.toml")); + const toml = readFileSync(written, "utf8"); + + // Parse the file into { [triple]: rustflags line } so every triple is + // checked individually, including the rustflags-only windows sections. + const sections = new Map(); + for (const block of toml.split(/\n(?=\[target\.)/)) { + const header = /^\[target\.([^\]\s]+)\]/.exec(block); + if (!header) continue; + const rustflags = /^rustflags = (.+)$/m.exec(block); + sections.set(header[1], rustflags ? rustflags[1] : ""); + } + + expect([...sections.keys()].sort()).toEqual([...allRustTargets].sort()); + for (const triple of allRustTargets) { + const flags: string[] = JSON.parse(sections.get(triple)!); + const joined = flags.join(" "); + expect(joined).toContain("-Z polonius=next"); + + const isWindowsTriple = triple.includes("windows"); + // Non-windows triples keep the lld link flags; windows triples are + // linked via env in rust.ts and must not grow a clang-style link-arg. + expect(joined.includes("link-arg=-fuse-ld=lld")).toBe(!isWindowsTriple); + expect(toml).toMatch( + new RegExp( + `^\\[target\\.${triple.replaceAll(".", "\\.")}\\][^\\n]*\\n${isWindowsTriple ? "rustflags" : "linker"} = `, + "m", + ), + ); + } + }); + + test("the two flag sources agree", () => { + // rust.ts pushes the single-token spelling; cargo-config.ts writes the + // two-token TOML array form. rustc accepts both; this pins that neither + // side is dropped independently of the other. + using dir = tempDir("polonius-cargo-config-agree", {}); + const cfg: Config = { ...resolve(configs["windows x64 release (ci)"]), cwd: String(dir) }; + const toml = readFileSync(generateCargoConfig(cfg), "utf8"); + expect(toml).toContain(`"-Z", "polonius=next"`); + expect(encodedRustflags(cfg)).toContain("-Zpolonius=next"); + }); +}); From f4bf3ecc489ca4a643494968abf10dc835277fcc Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:58:38 +0000 Subject: [PATCH 10/13] test: drop the build-flag unit test --- test/internal/rust-polonius-flag.test.ts | 151 ----------------------- 1 file changed, 151 deletions(-) delete mode 100644 test/internal/rust-polonius-flag.test.ts diff --git a/test/internal/rust-polonius-flag.test.ts b/test/internal/rust-polonius-flag.test.ts deleted file mode 100644 index dac68e4d6b27..000000000000 --- a/test/internal/rust-polonius-flag.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * The workspace relies on the polonius borrow checker (-Zpolonius=next), so - * every rustc invocation that type-checks workspace crates has to carry the - * flag: the ninja cargo edge (rust.ts, via CARGO_ENCODED_RUSTFLAGS) and the - * generated .cargo/config.toml (cargo-config.ts) used by plain cargo, - * rust:check-all and rust-analyzer. Configure-time logic only; nothing here - * spawns a compiler. - */ -import { describe, expect, test } from "bun:test"; -import { tempDir } from "harness"; -import { readFileSync } from "node:fs"; -import { join } from "node:path"; - -import { generateCargoConfig } from "../../scripts/build/cargo-config.ts"; -import { resolveConfig, type Config, type PartialConfig, type Toolchain } from "../../scripts/build/config.ts"; -import { allRustTargets, cargoBuildInvocation } from "../../scripts/build/rust.ts"; - -/** A fully-populated fake toolchain; resolveConfig never spawns any of these. */ -function mockToolchain(): Toolchain { - return { - cc: "/fake/llvm/bin/clang", - cxx: "/fake/llvm/bin/clang++", - clangVersion: "21.1.8", - clangResourceDir: "/fake/llvm/lib/clang/21", - ar: "/fake/llvm/bin/llvm-ar", - ranlib: "/fake/llvm/bin/llvm-ranlib", - ld: "/fake/llvm/bin/ld.lld", - ld64Lld: "/fake/llvm/bin/ld64.lld", - rustLld: undefined, - rustLlvmVersion: "22.1.4", - rustSysroot: undefined, - rustHostTriple: undefined, - strip: "/fake/bin/strip", - llvmStrip: "/fake/llvm/bin/llvm-strip", - dsymutil: "/fake/llvm/bin/dsymutil", - bun: "/fake/bin/bun", - jsRuntime: "/fake/bin/bun", - esbuild: "/fake/bin/esbuild", - ccache: undefined, - cmake: "/fake/bin/cmake", - cargo: "/fake/bin/cargo", - cargoHome: undefined, - rustupHome: undefined, - msvcLinker: "/fake/msvc/lld-link", - rc: undefined, - mt: undefined, - nasm: undefined, - }; -} - -const configs: Record = { - "linux x64 debug": { - os: "linux", - arch: "x64", - abi: "gnu", - buildType: "Debug", - assertions: true, - linuxSysroot: "/fake", - }, - "linux aarch64 release (ci)": { - os: "linux", - arch: "aarch64", - abi: "gnu", - buildType: "Release", - ci: true, - buildkite: false, - linuxSysroot: "/fake", - }, - "linux x64 release-asan": { - os: "linux", - arch: "x64", - abi: "gnu", - buildType: "Release", - asan: true, - linuxSysroot: "/fake", - }, - "windows x64 release (ci)": { - os: "windows", - arch: "x64", - buildType: "Release", - ci: true, - buildkite: false, - winsysroot: "/fake/winsysroot", - }, - "darwin aarch64 release": { os: "darwin", arch: "aarch64", buildType: "Release" }, -}; - -function resolve(partial: PartialConfig): Config { - return resolveConfig(partial, mockToolchain()); -} - -/** Decode the U+001F-separated CARGO_ENCODED_RUSTFLAGS the ninja edge sets. */ -function encodedRustflags(cfg: Config): string[] { - const encoded = cargoBuildInvocation(cfg).env.CARGO_ENCODED_RUSTFLAGS; - expect(encoded).toBeString(); - return encoded.split("\x1f"); -} - -describe("-Zpolonius=next reaches every rustc invocation", () => { - test.each(Object.entries(configs))("ninja cargo edge: %s", (_name, partial) => { - expect(encodedRustflags(resolve(partial))).toContain("-Zpolonius=next"); - }); - - test("generated .cargo/config.toml carries the flag for every target triple", () => { - using dir = tempDir("polonius-cargo-config", {}); - const cfg: Config = { ...resolve(configs["linux x64 debug"]), cwd: String(dir) }; - - const written = generateCargoConfig(cfg); - expect(written).toBe(join(String(dir), ".cargo", "config.toml")); - const toml = readFileSync(written, "utf8"); - - // Parse the file into { [triple]: rustflags line } so every triple is - // checked individually, including the rustflags-only windows sections. - const sections = new Map(); - for (const block of toml.split(/\n(?=\[target\.)/)) { - const header = /^\[target\.([^\]\s]+)\]/.exec(block); - if (!header) continue; - const rustflags = /^rustflags = (.+)$/m.exec(block); - sections.set(header[1], rustflags ? rustflags[1] : ""); - } - - expect([...sections.keys()].sort()).toEqual([...allRustTargets].sort()); - for (const triple of allRustTargets) { - const flags: string[] = JSON.parse(sections.get(triple)!); - const joined = flags.join(" "); - expect(joined).toContain("-Z polonius=next"); - - const isWindowsTriple = triple.includes("windows"); - // Non-windows triples keep the lld link flags; windows triples are - // linked via env in rust.ts and must not grow a clang-style link-arg. - expect(joined.includes("link-arg=-fuse-ld=lld")).toBe(!isWindowsTriple); - expect(toml).toMatch( - new RegExp( - `^\\[target\\.${triple.replaceAll(".", "\\.")}\\][^\\n]*\\n${isWindowsTriple ? "rustflags" : "linker"} = `, - "m", - ), - ); - } - }); - - test("the two flag sources agree", () => { - // rust.ts pushes the single-token spelling; cargo-config.ts writes the - // two-token TOML array form. rustc accepts both; this pins that neither - // side is dropped independently of the other. - using dir = tempDir("polonius-cargo-config-agree", {}); - const cfg: Config = { ...resolve(configs["windows x64 release (ci)"]), cwd: String(dir) }; - const toml = readFileSync(generateCargoConfig(cfg), "utf8"); - expect(toml).toContain(`"-Z", "polonius=next"`); - expect(encodedRustflags(cfg)).toContain("-Zpolonius=next"); - }); -}); From 750fa2ded82014829c23a3b22d3d1079b89af60c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 02:44:35 +0000 Subject: [PATCH 11/13] Bump Rust to nightly-2026-08-11 and drop the dead regular-LTO fix-up Polonius alpha is the default borrow checker on nightlies from 2026-08-06 on (rust-lang/rust#159343), so the workspace now builds on the pinned toolchain with or without -Zpolonius=next. The flag stays in rust.ts and the generated .cargo/config.toml so a future toolchain bump cannot silently change which borrow checker builds bun if upstream toggles the nightly default while the alpha bakes. The new nightly bundles LLVM 23, which trips the rustc-no-regular-lto-summary workaround's re-check threshold. That fix-up has been unreachable since every LTO platform moved to ThinLTO (rustLtoLinkInputs returned early whenever cfg.lto was set, and crossLangLto implies lto), so take the cleanup path the entry prescribes: delete rust-lto-fix-cli.ts, the rust_lto_fix rule and rustLtoLinkInputs, unwrap its call sites, drop the llvm-tools component, and remove the entry. rust-lld and llvm-objcopy come from the rustc component and the clang toolchain respectively, so nothing else used llvm-tools. The four GitHub workflows that pin RUSTUP_TOOLCHAIN are kept in sync with rust-toolchain.toml. --- .github/workflows/clippy.yml | 2 +- .github/workflows/format.yml | 2 +- .github/workflows/lolhtml.yml | 2 +- .github/workflows/miri.yml | 2 +- rust-toolchain.toml | 8 +- scripts/build/bun.ts | 14 +-- scripts/build/cargo-config.ts | 10 +- scripts/build/rust-lto-fix-cli.ts | 173 ------------------------------ scripts/build/rust.ts | 69 ++---------- scripts/build/workarounds.ts | 24 ----- 10 files changed, 25 insertions(+), 281 deletions(-) delete mode 100644 scripts/build/rust-lto-fix-cli.ts diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index 5d9a350671c8..e5b5cb944239 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -28,7 +28,7 @@ env: # Pin the toolchain explicitly so rustup ignores rust-toolchain.toml's # `targets` list (11 cross triples ≈ 450 MB of prebuilt std we don't need # to lint the host). Keep in sync with `channel` in rust-toolchain.toml. - RUSTUP_TOOLCHAIN: nightly-2026-07-20 + RUSTUP_TOOLCHAIN: nightly-2026-08-11 jobs: clippy: diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 8b7690c34e0d..0a120a872b66 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -42,7 +42,7 @@ jobs: # Pin the toolchain explicitly so rustup ignores rust-toolchain.toml's # `targets` list (11 cross triples ≈ 450 MB of prebuilt std we don't # need just to run rustfmt). Keep this in sync with `channel` there. - RUSTUP_TOOLCHAIN: nightly-2026-07-20 + RUSTUP_TOOLCHAIN: nightly-2026-08-11 run: | # Without pipefail, `cmd | sed` always reports sed's exit status, so a # failing formatter is invisible to the `wait $PID` checks below. diff --git a/.github/workflows/lolhtml.yml b/.github/workflows/lolhtml.yml index 35a5b4e9bc5d..5ee4a0834c73 100644 --- a/.github/workflows/lolhtml.yml +++ b/.github/workflows/lolhtml.yml @@ -21,7 +21,7 @@ env: BUN_VERSION: "1.3.2" LLVM_VERSION_MAJOR: "21" # Keep in sync with `channel` in rust-toolchain.toml. - RUSTUP_TOOLCHAIN: nightly-2026-07-20 + RUSTUP_TOOLCHAIN: nightly-2026-08-11 jobs: test: diff --git a/.github/workflows/miri.yml b/.github/workflows/miri.yml index 1ee1447252a2..a794f188a29b 100644 --- a/.github/workflows/miri.yml +++ b/.github/workflows/miri.yml @@ -37,7 +37,7 @@ env: LLVM_VERSION_MAJOR: "21" # Pin so rustup ignores rust-toolchain.toml's `targets` list (11 cross # triples ≈ 450 MB we don't need). Keep in sync with `channel` there. - RUSTUP_TOOLCHAIN: nightly-2026-07-20 + RUSTUP_TOOLCHAIN: nightly-2026-08-11 jobs: miri: diff --git a/rust-toolchain.toml b/rust-toolchain.toml index e48bb2731da8..c314b660fecf 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "nightly-2026-07-20" +channel = "nightly-2026-08-11" # rust-src is needed for -Zbuild-std (Tier 3 targets like # aarch64-unknown-freebsd have no prebuilt std). miri is for # `bun run rust:miri`. targets ensures the @@ -17,11 +17,7 @@ channel = "nightly-2026-07-20" # 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. -# llvm-tools provides llvm-link/opt for the ELF cross-language LTO -# regular-LTO-summary fix-up (scripts/build/rust-lto-fix-cli.ts). CI agents -# that pin via RUSTUP_TOOLCHAIN bypass this list; that script self-heals by -# running `rustup component add llvm-tools` when the tools are missing. -components = ["rust-src", "rustfmt", "clippy", "miri", "llvm-tools"] +components = ["rust-src", "rustfmt", "clippy", "miri"] targets = [ "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu", diff --git a/scripts/build/bun.ts b/scripts/build/bun.ts index 340666caced4..a7c39d7afcd9 100644 --- a/scripts/build/bun.ts +++ b/scripts/build/bun.ts @@ -38,7 +38,7 @@ import { assert } from "./error.ts"; import { bunIncludes, computeFlags, extraFlagsFor, linkDepends } from "./flags.ts"; import { writeIfChanged } from "./fs.ts"; import type { BuildNode, Ninja } from "./ninja.ts"; -import { emitRust, linkerMapPath, rustLibPath, rustLtoLinkInputs } from "./rust.ts"; +import { emitRust, linkerMapPath, rustLibPath } from "./rust.ts"; import { quote, slash } from "./shell.ts"; import { emitShims, machoPostlinkCommand, machoPostlinkImplicitInputs } from "./shims.ts"; import { computeDepLibs, resolveDep, type ResolvedDep } from "./source.ts"; @@ -496,9 +496,7 @@ export function emitBun(n: Ninja, cfg: Config, sources: Sources): BunOutput { // is needed; if a member ever isn't, `rustLinkFlags()` in rust.ts is the // wrapping helper. const shims = emitShims(n, cfg); - // rustLtoLinkInputs(): on ELF cross-language LTO targets the Rust bitcode - // is rewritten with a regular-LTO summary first (identity elsewhere). - const linkObjects = [...allObjects, ...rustLtoLinkInputs(n, cfg, rustObjects), ...windowsRes]; + const linkObjects = [...allObjects, ...rustObjects, ...windowsRes]; const ldflags = [...flags.ldflags, ...systemLibs(cfg), ...shims.ldflags]; const exe = link(n, cfg, exeName, linkObjects, { libs: depLibs, @@ -594,10 +592,8 @@ function emitLinkOnly(n: Ninja, cfg: Config): BunOutput { // libbun_rust.a from rust-only: same path emitRust writes to. Shared // helper so both sides of the CI split agree (cargo's - // `///` layout). rustLtoLinkInputs(): on ELF - // cross-language LTO targets the downloaded archive's bitcode is rewritten - // with a regular-LTO summary on this (link) agent before the link. - const rustObjects = rustLtoLinkInputs(n, cfg, [rustLibPath(cfg)]); + // `///` layout). + const rustObjects = [rustLibPath(cfg)]; // Only need ldflags + stripflags (no cflags/cxxflags — no compile). const flags = computeFlags(cfg); @@ -686,7 +682,7 @@ function emitRustAndLink(n: Ninja, cfg: Config, sources: Sources): BunOutput { const windowsRes = cfg.windows ? [emitWindowsResources(n, cfg)] : []; const shims = emitShims(n, cfg); - const linkObjects = [archive, ...rustLtoLinkInputs(n, cfg, rustObjects), ...windowsRes]; + const linkObjects = [archive, ...rustObjects, ...windowsRes]; const ldflags = [...flags.ldflags, ...systemLibs(cfg), ...shims.ldflags]; const exe = link(n, cfg, exeName, linkObjects, { libs: depLibs, diff --git a/scripts/build/cargo-config.ts b/scripts/build/cargo-config.ts index bc8f109145cb..6336436e4a77 100644 --- a/scripts/build/cargo-config.ts +++ b/scripts/build/cargo-config.ts @@ -81,11 +81,11 @@ export function generateCargoConfig(cfg: Config): string { ]; // `-Zpolonius=next` everywhere: workspace code relies on the polonius - // borrow checker (see the matching push in rust.ts), so every rustc - // invocation that type-checks workspace crates needs it or borrowck - // fails. Windows-msvc triples get a rustflags-only section (their linker - // is env-only, see the doc comment above) so `cargo check --target - // *-windows-msvc` / `rust:check-all` work. + // borrow checker (see the matching push in rust.ts for why it's explicit), + // so every rustc invocation that type-checks workspace crates pins the + // same checker the ninja build uses. Windows-msvc triples get a + // rustflags-only section (their linker is env-only, see the doc comment + // above) so `cargo check --target *-windows-msvc` / `rust:check-all` work. const polonius = `"-Z", "polonius=next"`; for (const triple of allRustTargets) { lines.push(""); diff --git a/scripts/build/rust-lto-fix-cli.ts b/scripts/build/rust-lto-fix-cli.ts deleted file mode 100644 index 9e9fcabf7a01..000000000000 --- a/scripts/build/rust-lto-fix-cli.ts +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Rust regular-LTO summary fix-up — the ninja build-time CLI for the - * `rust_lto_fix` rule (see `rustLtoLinkInputs()` in rust.ts and the - * `rustc-no-regular-lto-summary` entry in workarounds.ts). - * - * ## Why this exists - * - * The ELF release link is full (regular) LTO: every C/C++ object — ours, - * the direct deps', the WebKit `-lto` prebuilts' — is clang full-LTO - * bitcode, and clang unconditionally writes a per-module *regular-LTO - * summary* with `EnableSplitLTOUnit=1` into such objects on ELF - * (`shouldEmitRegularLTOSummary()` in clang's BackendUtil; neither - * `-fno-split-lto-unit` nor any other driver flag turns that off). - * - * The Rust side reaches the link as `-Clinker-plugin-lto` + `lto = "fat"` - * bitcode: one merged module with *no* summary at all. lld's - * `getLTOInfo()` reports a summary-less module as `EnableSplitLTOUnit=0`, - * the link becomes "partially split", and because `-fwhole-program-vtables` - * puts `llvm.type.test` calls in the merged C++ module, - * `LTO::checkPartiallySplit()` aborts the link with - * "inconsistent LTO Unit splitting (recompile with -fsplit-lto-unit)". - * rustc has no option to emit a regular-LTO summary, so this step bolts - * one on: - * - * 1. extract the bitcode member(s) from `libbun_rust.a`, - * 2. `llvm-link` in a stub that adds the `ThinLTO=0` module flag — that - * flag is what makes the bitcode writer emit a FULL_LTO summary block - * instead of a ThinLTO one, - * 3. re-emit with `opt --module-summary`, which builds the per-module - * summary from the IR. Its `EnableSplitLTOUnit` bit is copied from the - * module flag that `-Zsplit-lto-unit` stamped on every CGU (rust.ts - * passes it on ELF for exactly this reason), so the result matches the - * clang objects and the consistency check passes. - * - * The tools must come from rustc's own LLVM (the rustup `llvm-tools` - * component, installed next to rust-lld) — clang's older LLVM cannot read - * rustc's newer bitcode. If the component is missing, this script installs - * it (`rustup component add llvm-tools`), mirroring how the - * `rust_build_cross` rule self-heals missing `rust-std` targets on CI - * agents that pin the toolchain via `RUSTUP_TOOLCHAIN`. - * - * argv: [node, rust-lto-fix-cli.ts, , , , ] - */ - -import { spawnSync } from "node:child_process"; -import { closeSync, existsSync, mkdirSync, openSync, readSync, readdirSync, rmSync, writeFileSync } from "node:fs"; -import { join, resolve } from "node:path"; -import { BuildError, assert } from "./error.ts"; - -/** Absolute path to this file — referenced by the `rust_lto_fix` ninja rule. */ -export const rustLtoFixCliPath: string = import.meta.filename; - -/** Run a tool, streaming its output; throw a BuildError on failure. */ -function run(cmd: string, args: string[], cwd?: string): void { - const res = spawnSync(cmd, args, { stdio: "inherit", cwd }); - if (res.error !== undefined || res.status !== 0) { - throw new BuildError(`${cmd} ${args.join(" ")} failed${res.status !== null ? ` (exit ${res.status})` : ""}`, { - cause: res.error, - }); - } -} - -/** First bytes of an LLVM bitcode file: 'BC\xC0\xDE', or the wrapper magic 0x0B17C0DE (LE). */ -function isBitcode(path: string): boolean { - const buf = Buffer.alloc(4); - const fd = openSync(path, "r"); - try { - if (readSync(fd, buf, 0, 4, 0) < 4) return false; - } finally { - closeSync(fd); - } - if (buf[0] === 0x42 && buf[1] === 0x43 && buf[2] === 0xc0 && buf[3] === 0xde) return true; - return buf[0] === 0xde && buf[1] === 0xc0 && buf[2] === 0x17 && buf[3] === 0x0b; -} - -/** - * Make sure llvm-link/opt/llvm-as exist in rustc's host tool dir. They ship - * with the rustup `llvm-tools` component (rust-toolchain.toml lists it, but - * CI agents pin via `RUSTUP_TOOLCHAIN` which bypasses that file's component - * list), so install it on demand. - */ -function ensureLlvmTools(llvmBin: string): void { - const needed = ["llvm-link", "opt", "llvm-as", "llvm-dis"]; - const missing = () => needed.filter(t => !existsSync(join(llvmBin, t))); - if (missing().length === 0) return; - - // `<...>/toolchains//lib/rustlib//bin` → ``. - const toolchain = /[\\/]toolchains[\\/]([^\\/]+)[\\/]/.exec(llvmBin)?.[1]; - const args = ["component", "add", "llvm-tools"]; - if (toolchain !== undefined) args.push("--toolchain", toolchain); - console.log(`rust-lto-fix: ${missing().join(", ")} not found in ${llvmBin}, running rustup ${args.join(" ")}`); - const res = spawnSync("rustup", args, { stdio: "inherit" }); - assert( - res.error === undefined && res.status === 0 && missing().length === 0, - `missing ${missing().join(", ")} in ${llvmBin}`, - { - hint: `Install rustc's LLVM tools: rustup component add llvm-tools${toolchain !== undefined ? ` --toolchain ${toolchain}` : ""}`, - }, - ); -} - -function main(): void { - const argv = process.argv.slice(2); - assert( - argv[0] !== undefined && argv[1] !== undefined && argv[2] !== undefined && argv[3] !== undefined, - "usage: rust-lto-fix-cli.ts ", - ); - // Ninja passes buildDir-relative $in/$out and runs us with cwd=buildDir, - // but the archive is extracted with cwd set to the scratch dir below — - // make them absolute first. The tool paths are already absolute. - const [rustLib, outObj, llvmBin, ar] = [resolve(argv[0]), resolve(argv[1]), argv[2], argv[3]]; - assert(existsSync(rustLib), `${rustLib} does not exist`); - ensureLlvmTools(llvmBin); - - // Scratch space next to the output; recreated from scratch every run. - const tmp = `${outObj}.tmp`; - rmSync(tmp, { recursive: true, force: true }); - mkdirSync(tmp, { recursive: true }); - - try { - // Extract the archive and pick out the bitcode member(s). With - // `lto = "fat"` there is exactly one (the merged module); the rest are - // native objects (compiler_builtins) that stay in the archive. - run(ar, ["x", rustLib], tmp); - const bitcode = readdirSync(tmp) - .filter(f => isBitcode(join(tmp, f))) - .map(f => join(tmp, f)); - assert(bitcode.length > 0, `no LLVM bitcode members found in ${rustLib}`, { - hint: - "The ELF cross-language LTO build expects cargo to emit fat bitcode " + - "(-Clinker-plugin-lto with CARGO_PROFILE_RELEASE_LTO=fat — see emitRust() in rust.ts).", - }); - - // The `ThinLTO=0` module flag is the bitcode writer's "this is a regular - // LTO module" marker — without it `--module-summary` writes a ThinLTO - // summary block and lld would send the module to a ThinLTO backend. - // Carry the module's target data layout on the stub too: without it the - // stub's empty layout mismatches the real module and llvm-link prints a - // "Linking two modules of different data layouts" warning on every link. - // llvm-dis streams the .ll header first, so a bounded read suffices. - const dis = spawnSync(join(llvmBin, "llvm-dis"), ["-o", "-", bitcode[0]], { - encoding: "utf8", - maxBuffer: 256 * 1024, - }); - const dataLayout = /^target datalayout = "[^"]*"/m.exec(dis.stdout || "")?.[0]; - const stubLl = join(tmp, "regular-lto-flag-stub.ll"); - const stubBc = join(tmp, "regular-lto-flag-stub.bc"); - writeFileSync( - stubLl, - `${dataLayout ? `${dataLayout}\n` : ""}!llvm.module.flags = !{!0}\n!0 = !{i32 1, !"ThinLTO", i32 0}\n`, - ); - run(join(llvmBin, "llvm-as"), [stubLl, "-o", stubBc]); - - const merged = join(tmp, "merged.bc"); - // The stub goes FIRST: llvm-link uses the first module as the link - // destination, and IRMover silently inherits the data layout / target - // triple when the destination has none. With the stub last it is a - // *source* module whose empty layout differs from the destination's, - // and every build-bun job warns "Linking two modules of different data - // layouts". Same merged output either way (verified: the module flag and - // the real layout both survive). - run(join(llvmBin, "llvm-link"), [stubBc, ...bitcode, "-o", merged]); - run(join(llvmBin, "opt"), ["--module-summary", merged, "-o", outObj]); - } finally { - rmSync(tmp, { recursive: true, force: true }); - } -} - -// Imported by rust.ts for `rustLtoFixCliPath`; only act as a CLI when ninja -// invokes this file directly. -if (process.argv[1] === import.meta.filename) { - main(); -} diff --git a/scripts/build/rust.ts b/scripts/build/rust.ts index 5535d914580b..f966197f4d43 100644 --- a/scripts/build/rust.ts +++ b/scripts/build/rust.ts @@ -29,7 +29,6 @@ import { dirname, join, resolve } from "node:path"; import { bunExeName, type Config } from "./config.ts"; import { assert } from "./error.ts"; import type { Ninja } from "./ninja.ts"; -import { rustLtoFixCliPath } from "./rust-lto-fix-cli.ts"; import { quote, quoteArgs } from "./shell.ts"; import { streamPath } from "./stream.ts"; @@ -149,18 +148,6 @@ export function registerRustRules(n: Ninja, cfg: Config): void { const hostWin = cfg.host.os === "windows"; const q = (p: string) => quote(p, hostWin); - // Regular-LTO summary fix-up for the ELF cross-language LTO link (see - // rustLtoLinkInputs() below). Registered before the cargo gate: the - // link-only CI agents emit this edge too, and it needs rustc's - // llvm-tools, not cargo. Not darwin/windows: their ThinLTO links keep the - // per-CGU summaries (CARGO_PROFILE_RELEASE_LTO=off) and need no fix-up. - if (cfg.crossLangLto && !cfg.darwin && !cfg.windows) { - n.rule("rust_lto_fix", { - command: `${cfg.jsRuntime} ${q(rustLtoFixCliPath)} $in $out $llvm_bin $ar`, - description: "regular-LTO summary → $out", - }); - } - if (cfg.cargo === undefined) return; // emitRust() asserts with a hint const stream = `${cfg.jsRuntime} ${q(streamPath)} rust`; @@ -412,12 +399,15 @@ export function cargoBuildInvocation(cfg: Config): CargoInvocation { } // Polonius alpha borrow checker: accepts NLL "problem case 3" (a borrow // returned/escaping on one path no longer blocks the other paths), which - // workspace code now relies on — e.g. `src/collections/linear_fifo.rs` - // no longer compiles under the stock checker. Nightly-only; the pinned - // toolchain is nightly. Must stay in sync with the generated - // `.cargo/config.toml` (cargo-config.ts) so plain `cargo check`, - // rust-analyzer, and `rust:check-all` accept the same code the ninja - // build does. + // workspace code relies on — e.g. `src/collections/linear_fifo.rs` does + // not compile under NLL. It is the default on nightlies since 2026-08-06 + // (rust-lang/rust#159343), so this is a no-op on the pinned toolchain; + // it's passed explicitly because upstream reserves the right to flip the + // nightly default back off while bugs get fixed ahead of stabilization, + // and a toolchain bump must not change which borrow checker builds bun. + // Must stay in sync with the generated `.cargo/config.toml` + // (cargo-config.ts) so plain `cargo check`, rust-analyzer, and + // `rust:check-all` accept the same code the ninja build does. rustflags.push("-Zpolonius=next"); // rustc does not emit `.llvm_addrsig` by default on *any* target (verified // empirically — Linux-gnu, musl, darwin, msvc all missing it). lld's @@ -882,47 +872,6 @@ export function emitRust(n: Ninja, cfg: Config, inputs: RustBuildInputs): string return [lib]; } -/** - * Link inputs for the Rust side of the binary. - * - * On ELF cross-language LTO targets the fat Rust bitcode member can't go - * into the link as-is: it has no per-module summary, so lld reads it as - * EnableSplitLTOUnit=0 while every clang-produced full-LTO object (ours, - * the deps', the WebKit -lto prebuilts') says 1, and the link aborts with - * "inconsistent LTO Unit splitting". rustc has no way to emit a regular-LTO - * summary (clang hardcodes one in shouldEmitRegularLTOSummary()), so a - * build step rewrites the bitcode with rustc's own LLVM tools — see - * rust-lto-fix-cli.ts and the `rustc-no-regular-lto-summary` workaround - * entry. - * - * Returns [fixed bitcode .o, original .a]: the .o defines every Rust symbol - * (so the archive's bitcode member is never pulled), and the archive still - * supplies its native members (compiler_builtins). On every other config - * this is the identity function. - */ -export function rustLtoLinkInputs(n: Ninja, cfg: Config, rustObjects: string[]): string[] { - const rustLib = rustObjects[0]; - // All LTO platforms now use ThinLTO with -fno-split-lto-unit and per-CGU - // rust bitcode (CARGO_PROFILE_RELEASE_LTO=off), so the regular-LTO summary - // fix-up below is never needed. Delete this function once confirmed. - if (cfg.lto || !cfg.crossLangLto || cfg.darwin || cfg.windows || rustLib === undefined) return rustObjects; - assert( - cfg.rustSysroot !== undefined && cfg.host.rustTriple !== undefined, - "ELF cross-language LTO needs rustc's sysroot to locate its LLVM tools (llvm-link/opt) for the regular-LTO summary fix-up, but rustc wasn't found", - { hint: "Install the pinned rust toolchain (rustup show active-toolchain), or build with --lto=off" }, - ); - const llvmBin = join(cfg.rustSysroot, "lib", "rustlib", cfg.host.rustTriple, "bin"); - const out = resolve(cfg.buildDir, "bun_rust.lto.o"); - n.build({ - outputs: [out], - rule: "rust_lto_fix", - inputs: [rustLib], - implicitInputs: [rustLtoFixCliPath], - vars: { llvm_bin: llvmBin, ar: cfg.ar }, - }); - return [out, ...rustObjects]; -} - /** `${buildDir}/${exe}.linker-map` — lld's `-Wl,-Map=` output (see flags.ts). */ export function linkerMapPath(cfg: Config): string { return join(cfg.buildDir, `${bunExeName(cfg)}.linker-map`); diff --git a/scripts/build/workarounds.ts b/scripts/build/workarounds.ts index b97b59fb2325..68fd61d8a2d2 100644 --- a/scripts/build/workarounds.ts +++ b/scripts/build/workarounds.ts @@ -84,30 +84,6 @@ export const workarounds: Workaround[] = [ }, cleanup: `Delete scripts/build/shims/asan-dyld-shim.c, scripts/build/shims.ts, the emitShims() calls in bun.ts, registerShimRules in rules.ts, and this entry.`, }, - { - id: "rustc-no-regular-lto-summary", - issue: - "https://github.com/rust-lang/rust/issues/ (none filed yet — rustc has no equivalent of clang's shouldEmitRegularLTOSummary())", - description: - 'Under -Clinker-plugin-lto + lto = "fat", rustc emits the merged bitcode module without a ' + - "per-module summary, so lld reads it as EnableSplitLTOUnit=0 while every clang full-LTO " + - "object (ours and the WebKit -lto prebuilts) hardcodes 1 — the ELF release link aborts " + - 'with "inconsistent LTO Unit splitting". rust-lto-fix-cli.ts re-emits the Rust bitcode ' + - "with a regular-LTO summary using rustc's own llvm-tools (rustLtoLinkInputs() in rust.ts).", - applies: cfg => cfg.crossLangLto && !cfg.darwin, - expectedToBeFixed: cfg => { - // Re-evaluate when the pinned rustc moves to its next LLVM major: - // either rustc grew a way to emit regular-LTO summaries (delete the - // fix-up), or linux moved to ThinLTO (it's moot), or neither — bump - // the threshold and keep it. - const RECHECK_AT_RUST_LLVM = "23.0.0"; - return cfg.rustLlvmVersion !== undefined && satisfiesRange(cfg.rustLlvmVersion, `>=${RECHECK_AT_RUST_LLVM}`); - }, - cleanup: - `Delete scripts/build/rust-lto-fix-cli.ts, the rust_lto_fix rule and rustLtoLinkInputs() in ` + - `rust.ts, unwrap its call sites in bun.ts, drop "llvm-tools" from rust-toolchain.toml's ` + - `components, and delete this entry.`, - }, { id: "rust-lld-for-crosslang-lto", issue: "https://rustc-dev-guide.rust-lang.org/backend/updating-llvm.html", From 24ac60000d7eeb6aa700fe71b79090512b36bb0b Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 02:57:21 +0000 Subject: [PATCH 12/13] Revert "Bump Rust to nightly-2026-08-11 and drop the dead regular-LTO fix-up" This reverts commit 750fa2ded82014829c23a3b22d3d1079b89af60c. --- .github/workflows/clippy.yml | 2 +- .github/workflows/format.yml | 2 +- .github/workflows/lolhtml.yml | 2 +- .github/workflows/miri.yml | 2 +- rust-toolchain.toml | 8 +- scripts/build/bun.ts | 14 ++- scripts/build/cargo-config.ts | 10 +- scripts/build/rust-lto-fix-cli.ts | 173 ++++++++++++++++++++++++++++++ scripts/build/rust.ts | 69 ++++++++++-- scripts/build/workarounds.ts | 24 +++++ 10 files changed, 281 insertions(+), 25 deletions(-) create mode 100644 scripts/build/rust-lto-fix-cli.ts diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index e5b5cb944239..5d9a350671c8 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -28,7 +28,7 @@ env: # Pin the toolchain explicitly so rustup ignores rust-toolchain.toml's # `targets` list (11 cross triples ≈ 450 MB of prebuilt std we don't need # to lint the host). Keep in sync with `channel` in rust-toolchain.toml. - RUSTUP_TOOLCHAIN: nightly-2026-08-11 + RUSTUP_TOOLCHAIN: nightly-2026-07-20 jobs: clippy: diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 0a120a872b66..8b7690c34e0d 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -42,7 +42,7 @@ jobs: # Pin the toolchain explicitly so rustup ignores rust-toolchain.toml's # `targets` list (11 cross triples ≈ 450 MB of prebuilt std we don't # need just to run rustfmt). Keep this in sync with `channel` there. - RUSTUP_TOOLCHAIN: nightly-2026-08-11 + RUSTUP_TOOLCHAIN: nightly-2026-07-20 run: | # Without pipefail, `cmd | sed` always reports sed's exit status, so a # failing formatter is invisible to the `wait $PID` checks below. diff --git a/.github/workflows/lolhtml.yml b/.github/workflows/lolhtml.yml index 5ee4a0834c73..35a5b4e9bc5d 100644 --- a/.github/workflows/lolhtml.yml +++ b/.github/workflows/lolhtml.yml @@ -21,7 +21,7 @@ env: BUN_VERSION: "1.3.2" LLVM_VERSION_MAJOR: "21" # Keep in sync with `channel` in rust-toolchain.toml. - RUSTUP_TOOLCHAIN: nightly-2026-08-11 + RUSTUP_TOOLCHAIN: nightly-2026-07-20 jobs: test: diff --git a/.github/workflows/miri.yml b/.github/workflows/miri.yml index a794f188a29b..1ee1447252a2 100644 --- a/.github/workflows/miri.yml +++ b/.github/workflows/miri.yml @@ -37,7 +37,7 @@ env: LLVM_VERSION_MAJOR: "21" # Pin so rustup ignores rust-toolchain.toml's `targets` list (11 cross # triples ≈ 450 MB we don't need). Keep in sync with `channel` there. - RUSTUP_TOOLCHAIN: nightly-2026-08-11 + RUSTUP_TOOLCHAIN: nightly-2026-07-20 jobs: miri: diff --git a/rust-toolchain.toml b/rust-toolchain.toml index c314b660fecf..e48bb2731da8 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "nightly-2026-08-11" +channel = "nightly-2026-07-20" # rust-src is needed for -Zbuild-std (Tier 3 targets like # aarch64-unknown-freebsd have no prebuilt std). miri is for # `bun run rust:miri`. targets ensures the @@ -17,7 +17,11 @@ channel = "nightly-2026-08-11" # 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", "rustfmt", "clippy", "miri"] +# llvm-tools provides llvm-link/opt for the ELF cross-language LTO +# regular-LTO-summary fix-up (scripts/build/rust-lto-fix-cli.ts). CI agents +# that pin via RUSTUP_TOOLCHAIN bypass this list; that script self-heals by +# running `rustup component add llvm-tools` when the tools are missing. +components = ["rust-src", "rustfmt", "clippy", "miri", "llvm-tools"] targets = [ "aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu", diff --git a/scripts/build/bun.ts b/scripts/build/bun.ts index a7c39d7afcd9..340666caced4 100644 --- a/scripts/build/bun.ts +++ b/scripts/build/bun.ts @@ -38,7 +38,7 @@ import { assert } from "./error.ts"; import { bunIncludes, computeFlags, extraFlagsFor, linkDepends } from "./flags.ts"; import { writeIfChanged } from "./fs.ts"; import type { BuildNode, Ninja } from "./ninja.ts"; -import { emitRust, linkerMapPath, rustLibPath } from "./rust.ts"; +import { emitRust, linkerMapPath, rustLibPath, rustLtoLinkInputs } from "./rust.ts"; import { quote, slash } from "./shell.ts"; import { emitShims, machoPostlinkCommand, machoPostlinkImplicitInputs } from "./shims.ts"; import { computeDepLibs, resolveDep, type ResolvedDep } from "./source.ts"; @@ -496,7 +496,9 @@ export function emitBun(n: Ninja, cfg: Config, sources: Sources): BunOutput { // is needed; if a member ever isn't, `rustLinkFlags()` in rust.ts is the // wrapping helper. const shims = emitShims(n, cfg); - const linkObjects = [...allObjects, ...rustObjects, ...windowsRes]; + // rustLtoLinkInputs(): on ELF cross-language LTO targets the Rust bitcode + // is rewritten with a regular-LTO summary first (identity elsewhere). + const linkObjects = [...allObjects, ...rustLtoLinkInputs(n, cfg, rustObjects), ...windowsRes]; const ldflags = [...flags.ldflags, ...systemLibs(cfg), ...shims.ldflags]; const exe = link(n, cfg, exeName, linkObjects, { libs: depLibs, @@ -592,8 +594,10 @@ function emitLinkOnly(n: Ninja, cfg: Config): BunOutput { // libbun_rust.a from rust-only: same path emitRust writes to. Shared // helper so both sides of the CI split agree (cargo's - // `///` layout). - const rustObjects = [rustLibPath(cfg)]; + // `///` layout). rustLtoLinkInputs(): on ELF + // cross-language LTO targets the downloaded archive's bitcode is rewritten + // with a regular-LTO summary on this (link) agent before the link. + const rustObjects = rustLtoLinkInputs(n, cfg, [rustLibPath(cfg)]); // Only need ldflags + stripflags (no cflags/cxxflags — no compile). const flags = computeFlags(cfg); @@ -682,7 +686,7 @@ function emitRustAndLink(n: Ninja, cfg: Config, sources: Sources): BunOutput { const windowsRes = cfg.windows ? [emitWindowsResources(n, cfg)] : []; const shims = emitShims(n, cfg); - const linkObjects = [archive, ...rustObjects, ...windowsRes]; + const linkObjects = [archive, ...rustLtoLinkInputs(n, cfg, rustObjects), ...windowsRes]; const ldflags = [...flags.ldflags, ...systemLibs(cfg), ...shims.ldflags]; const exe = link(n, cfg, exeName, linkObjects, { libs: depLibs, diff --git a/scripts/build/cargo-config.ts b/scripts/build/cargo-config.ts index 6336436e4a77..bc8f109145cb 100644 --- a/scripts/build/cargo-config.ts +++ b/scripts/build/cargo-config.ts @@ -81,11 +81,11 @@ export function generateCargoConfig(cfg: Config): string { ]; // `-Zpolonius=next` everywhere: workspace code relies on the polonius - // borrow checker (see the matching push in rust.ts for why it's explicit), - // so every rustc invocation that type-checks workspace crates pins the - // same checker the ninja build uses. Windows-msvc triples get a - // rustflags-only section (their linker is env-only, see the doc comment - // above) so `cargo check --target *-windows-msvc` / `rust:check-all` work. + // borrow checker (see the matching push in rust.ts), so every rustc + // invocation that type-checks workspace crates needs it or borrowck + // fails. Windows-msvc triples get a rustflags-only section (their linker + // is env-only, see the doc comment above) so `cargo check --target + // *-windows-msvc` / `rust:check-all` work. const polonius = `"-Z", "polonius=next"`; for (const triple of allRustTargets) { lines.push(""); diff --git a/scripts/build/rust-lto-fix-cli.ts b/scripts/build/rust-lto-fix-cli.ts new file mode 100644 index 000000000000..9e9fcabf7a01 --- /dev/null +++ b/scripts/build/rust-lto-fix-cli.ts @@ -0,0 +1,173 @@ +/** + * Rust regular-LTO summary fix-up — the ninja build-time CLI for the + * `rust_lto_fix` rule (see `rustLtoLinkInputs()` in rust.ts and the + * `rustc-no-regular-lto-summary` entry in workarounds.ts). + * + * ## Why this exists + * + * The ELF release link is full (regular) LTO: every C/C++ object — ours, + * the direct deps', the WebKit `-lto` prebuilts' — is clang full-LTO + * bitcode, and clang unconditionally writes a per-module *regular-LTO + * summary* with `EnableSplitLTOUnit=1` into such objects on ELF + * (`shouldEmitRegularLTOSummary()` in clang's BackendUtil; neither + * `-fno-split-lto-unit` nor any other driver flag turns that off). + * + * The Rust side reaches the link as `-Clinker-plugin-lto` + `lto = "fat"` + * bitcode: one merged module with *no* summary at all. lld's + * `getLTOInfo()` reports a summary-less module as `EnableSplitLTOUnit=0`, + * the link becomes "partially split", and because `-fwhole-program-vtables` + * puts `llvm.type.test` calls in the merged C++ module, + * `LTO::checkPartiallySplit()` aborts the link with + * "inconsistent LTO Unit splitting (recompile with -fsplit-lto-unit)". + * rustc has no option to emit a regular-LTO summary, so this step bolts + * one on: + * + * 1. extract the bitcode member(s) from `libbun_rust.a`, + * 2. `llvm-link` in a stub that adds the `ThinLTO=0` module flag — that + * flag is what makes the bitcode writer emit a FULL_LTO summary block + * instead of a ThinLTO one, + * 3. re-emit with `opt --module-summary`, which builds the per-module + * summary from the IR. Its `EnableSplitLTOUnit` bit is copied from the + * module flag that `-Zsplit-lto-unit` stamped on every CGU (rust.ts + * passes it on ELF for exactly this reason), so the result matches the + * clang objects and the consistency check passes. + * + * The tools must come from rustc's own LLVM (the rustup `llvm-tools` + * component, installed next to rust-lld) — clang's older LLVM cannot read + * rustc's newer bitcode. If the component is missing, this script installs + * it (`rustup component add llvm-tools`), mirroring how the + * `rust_build_cross` rule self-heals missing `rust-std` targets on CI + * agents that pin the toolchain via `RUSTUP_TOOLCHAIN`. + * + * argv: [node, rust-lto-fix-cli.ts, , , , ] + */ + +import { spawnSync } from "node:child_process"; +import { closeSync, existsSync, mkdirSync, openSync, readSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { BuildError, assert } from "./error.ts"; + +/** Absolute path to this file — referenced by the `rust_lto_fix` ninja rule. */ +export const rustLtoFixCliPath: string = import.meta.filename; + +/** Run a tool, streaming its output; throw a BuildError on failure. */ +function run(cmd: string, args: string[], cwd?: string): void { + const res = spawnSync(cmd, args, { stdio: "inherit", cwd }); + if (res.error !== undefined || res.status !== 0) { + throw new BuildError(`${cmd} ${args.join(" ")} failed${res.status !== null ? ` (exit ${res.status})` : ""}`, { + cause: res.error, + }); + } +} + +/** First bytes of an LLVM bitcode file: 'BC\xC0\xDE', or the wrapper magic 0x0B17C0DE (LE). */ +function isBitcode(path: string): boolean { + const buf = Buffer.alloc(4); + const fd = openSync(path, "r"); + try { + if (readSync(fd, buf, 0, 4, 0) < 4) return false; + } finally { + closeSync(fd); + } + if (buf[0] === 0x42 && buf[1] === 0x43 && buf[2] === 0xc0 && buf[3] === 0xde) return true; + return buf[0] === 0xde && buf[1] === 0xc0 && buf[2] === 0x17 && buf[3] === 0x0b; +} + +/** + * Make sure llvm-link/opt/llvm-as exist in rustc's host tool dir. They ship + * with the rustup `llvm-tools` component (rust-toolchain.toml lists it, but + * CI agents pin via `RUSTUP_TOOLCHAIN` which bypasses that file's component + * list), so install it on demand. + */ +function ensureLlvmTools(llvmBin: string): void { + const needed = ["llvm-link", "opt", "llvm-as", "llvm-dis"]; + const missing = () => needed.filter(t => !existsSync(join(llvmBin, t))); + if (missing().length === 0) return; + + // `<...>/toolchains//lib/rustlib//bin` → ``. + const toolchain = /[\\/]toolchains[\\/]([^\\/]+)[\\/]/.exec(llvmBin)?.[1]; + const args = ["component", "add", "llvm-tools"]; + if (toolchain !== undefined) args.push("--toolchain", toolchain); + console.log(`rust-lto-fix: ${missing().join(", ")} not found in ${llvmBin}, running rustup ${args.join(" ")}`); + const res = spawnSync("rustup", args, { stdio: "inherit" }); + assert( + res.error === undefined && res.status === 0 && missing().length === 0, + `missing ${missing().join(", ")} in ${llvmBin}`, + { + hint: `Install rustc's LLVM tools: rustup component add llvm-tools${toolchain !== undefined ? ` --toolchain ${toolchain}` : ""}`, + }, + ); +} + +function main(): void { + const argv = process.argv.slice(2); + assert( + argv[0] !== undefined && argv[1] !== undefined && argv[2] !== undefined && argv[3] !== undefined, + "usage: rust-lto-fix-cli.ts ", + ); + // Ninja passes buildDir-relative $in/$out and runs us with cwd=buildDir, + // but the archive is extracted with cwd set to the scratch dir below — + // make them absolute first. The tool paths are already absolute. + const [rustLib, outObj, llvmBin, ar] = [resolve(argv[0]), resolve(argv[1]), argv[2], argv[3]]; + assert(existsSync(rustLib), `${rustLib} does not exist`); + ensureLlvmTools(llvmBin); + + // Scratch space next to the output; recreated from scratch every run. + const tmp = `${outObj}.tmp`; + rmSync(tmp, { recursive: true, force: true }); + mkdirSync(tmp, { recursive: true }); + + try { + // Extract the archive and pick out the bitcode member(s). With + // `lto = "fat"` there is exactly one (the merged module); the rest are + // native objects (compiler_builtins) that stay in the archive. + run(ar, ["x", rustLib], tmp); + const bitcode = readdirSync(tmp) + .filter(f => isBitcode(join(tmp, f))) + .map(f => join(tmp, f)); + assert(bitcode.length > 0, `no LLVM bitcode members found in ${rustLib}`, { + hint: + "The ELF cross-language LTO build expects cargo to emit fat bitcode " + + "(-Clinker-plugin-lto with CARGO_PROFILE_RELEASE_LTO=fat — see emitRust() in rust.ts).", + }); + + // The `ThinLTO=0` module flag is the bitcode writer's "this is a regular + // LTO module" marker — without it `--module-summary` writes a ThinLTO + // summary block and lld would send the module to a ThinLTO backend. + // Carry the module's target data layout on the stub too: without it the + // stub's empty layout mismatches the real module and llvm-link prints a + // "Linking two modules of different data layouts" warning on every link. + // llvm-dis streams the .ll header first, so a bounded read suffices. + const dis = spawnSync(join(llvmBin, "llvm-dis"), ["-o", "-", bitcode[0]], { + encoding: "utf8", + maxBuffer: 256 * 1024, + }); + const dataLayout = /^target datalayout = "[^"]*"/m.exec(dis.stdout || "")?.[0]; + const stubLl = join(tmp, "regular-lto-flag-stub.ll"); + const stubBc = join(tmp, "regular-lto-flag-stub.bc"); + writeFileSync( + stubLl, + `${dataLayout ? `${dataLayout}\n` : ""}!llvm.module.flags = !{!0}\n!0 = !{i32 1, !"ThinLTO", i32 0}\n`, + ); + run(join(llvmBin, "llvm-as"), [stubLl, "-o", stubBc]); + + const merged = join(tmp, "merged.bc"); + // The stub goes FIRST: llvm-link uses the first module as the link + // destination, and IRMover silently inherits the data layout / target + // triple when the destination has none. With the stub last it is a + // *source* module whose empty layout differs from the destination's, + // and every build-bun job warns "Linking two modules of different data + // layouts". Same merged output either way (verified: the module flag and + // the real layout both survive). + run(join(llvmBin, "llvm-link"), [stubBc, ...bitcode, "-o", merged]); + run(join(llvmBin, "opt"), ["--module-summary", merged, "-o", outObj]); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } +} + +// Imported by rust.ts for `rustLtoFixCliPath`; only act as a CLI when ninja +// invokes this file directly. +if (process.argv[1] === import.meta.filename) { + main(); +} diff --git a/scripts/build/rust.ts b/scripts/build/rust.ts index f966197f4d43..5535d914580b 100644 --- a/scripts/build/rust.ts +++ b/scripts/build/rust.ts @@ -29,6 +29,7 @@ import { dirname, join, resolve } from "node:path"; import { bunExeName, type Config } from "./config.ts"; import { assert } from "./error.ts"; import type { Ninja } from "./ninja.ts"; +import { rustLtoFixCliPath } from "./rust-lto-fix-cli.ts"; import { quote, quoteArgs } from "./shell.ts"; import { streamPath } from "./stream.ts"; @@ -148,6 +149,18 @@ export function registerRustRules(n: Ninja, cfg: Config): void { const hostWin = cfg.host.os === "windows"; const q = (p: string) => quote(p, hostWin); + // Regular-LTO summary fix-up for the ELF cross-language LTO link (see + // rustLtoLinkInputs() below). Registered before the cargo gate: the + // link-only CI agents emit this edge too, and it needs rustc's + // llvm-tools, not cargo. Not darwin/windows: their ThinLTO links keep the + // per-CGU summaries (CARGO_PROFILE_RELEASE_LTO=off) and need no fix-up. + if (cfg.crossLangLto && !cfg.darwin && !cfg.windows) { + n.rule("rust_lto_fix", { + command: `${cfg.jsRuntime} ${q(rustLtoFixCliPath)} $in $out $llvm_bin $ar`, + description: "regular-LTO summary → $out", + }); + } + if (cfg.cargo === undefined) return; // emitRust() asserts with a hint const stream = `${cfg.jsRuntime} ${q(streamPath)} rust`; @@ -399,15 +412,12 @@ export function cargoBuildInvocation(cfg: Config): CargoInvocation { } // Polonius alpha borrow checker: accepts NLL "problem case 3" (a borrow // returned/escaping on one path no longer blocks the other paths), which - // workspace code relies on — e.g. `src/collections/linear_fifo.rs` does - // not compile under NLL. It is the default on nightlies since 2026-08-06 - // (rust-lang/rust#159343), so this is a no-op on the pinned toolchain; - // it's passed explicitly because upstream reserves the right to flip the - // nightly default back off while bugs get fixed ahead of stabilization, - // and a toolchain bump must not change which borrow checker builds bun. - // Must stay in sync with the generated `.cargo/config.toml` - // (cargo-config.ts) so plain `cargo check`, rust-analyzer, and - // `rust:check-all` accept the same code the ninja build does. + // workspace code now relies on — e.g. `src/collections/linear_fifo.rs` + // no longer compiles under the stock checker. Nightly-only; the pinned + // toolchain is nightly. Must stay in sync with the generated + // `.cargo/config.toml` (cargo-config.ts) so plain `cargo check`, + // rust-analyzer, and `rust:check-all` accept the same code the ninja + // build does. rustflags.push("-Zpolonius=next"); // rustc does not emit `.llvm_addrsig` by default on *any* target (verified // empirically — Linux-gnu, musl, darwin, msvc all missing it). lld's @@ -872,6 +882,47 @@ export function emitRust(n: Ninja, cfg: Config, inputs: RustBuildInputs): string return [lib]; } +/** + * Link inputs for the Rust side of the binary. + * + * On ELF cross-language LTO targets the fat Rust bitcode member can't go + * into the link as-is: it has no per-module summary, so lld reads it as + * EnableSplitLTOUnit=0 while every clang-produced full-LTO object (ours, + * the deps', the WebKit -lto prebuilts') says 1, and the link aborts with + * "inconsistent LTO Unit splitting". rustc has no way to emit a regular-LTO + * summary (clang hardcodes one in shouldEmitRegularLTOSummary()), so a + * build step rewrites the bitcode with rustc's own LLVM tools — see + * rust-lto-fix-cli.ts and the `rustc-no-regular-lto-summary` workaround + * entry. + * + * Returns [fixed bitcode .o, original .a]: the .o defines every Rust symbol + * (so the archive's bitcode member is never pulled), and the archive still + * supplies its native members (compiler_builtins). On every other config + * this is the identity function. + */ +export function rustLtoLinkInputs(n: Ninja, cfg: Config, rustObjects: string[]): string[] { + const rustLib = rustObjects[0]; + // All LTO platforms now use ThinLTO with -fno-split-lto-unit and per-CGU + // rust bitcode (CARGO_PROFILE_RELEASE_LTO=off), so the regular-LTO summary + // fix-up below is never needed. Delete this function once confirmed. + if (cfg.lto || !cfg.crossLangLto || cfg.darwin || cfg.windows || rustLib === undefined) return rustObjects; + assert( + cfg.rustSysroot !== undefined && cfg.host.rustTriple !== undefined, + "ELF cross-language LTO needs rustc's sysroot to locate its LLVM tools (llvm-link/opt) for the regular-LTO summary fix-up, but rustc wasn't found", + { hint: "Install the pinned rust toolchain (rustup show active-toolchain), or build with --lto=off" }, + ); + const llvmBin = join(cfg.rustSysroot, "lib", "rustlib", cfg.host.rustTriple, "bin"); + const out = resolve(cfg.buildDir, "bun_rust.lto.o"); + n.build({ + outputs: [out], + rule: "rust_lto_fix", + inputs: [rustLib], + implicitInputs: [rustLtoFixCliPath], + vars: { llvm_bin: llvmBin, ar: cfg.ar }, + }); + return [out, ...rustObjects]; +} + /** `${buildDir}/${exe}.linker-map` — lld's `-Wl,-Map=` output (see flags.ts). */ export function linkerMapPath(cfg: Config): string { return join(cfg.buildDir, `${bunExeName(cfg)}.linker-map`); diff --git a/scripts/build/workarounds.ts b/scripts/build/workarounds.ts index 68fd61d8a2d2..b97b59fb2325 100644 --- a/scripts/build/workarounds.ts +++ b/scripts/build/workarounds.ts @@ -84,6 +84,30 @@ export const workarounds: Workaround[] = [ }, cleanup: `Delete scripts/build/shims/asan-dyld-shim.c, scripts/build/shims.ts, the emitShims() calls in bun.ts, registerShimRules in rules.ts, and this entry.`, }, + { + id: "rustc-no-regular-lto-summary", + issue: + "https://github.com/rust-lang/rust/issues/ (none filed yet — rustc has no equivalent of clang's shouldEmitRegularLTOSummary())", + description: + 'Under -Clinker-plugin-lto + lto = "fat", rustc emits the merged bitcode module without a ' + + "per-module summary, so lld reads it as EnableSplitLTOUnit=0 while every clang full-LTO " + + "object (ours and the WebKit -lto prebuilts) hardcodes 1 — the ELF release link aborts " + + 'with "inconsistent LTO Unit splitting". rust-lto-fix-cli.ts re-emits the Rust bitcode ' + + "with a regular-LTO summary using rustc's own llvm-tools (rustLtoLinkInputs() in rust.ts).", + applies: cfg => cfg.crossLangLto && !cfg.darwin, + expectedToBeFixed: cfg => { + // Re-evaluate when the pinned rustc moves to its next LLVM major: + // either rustc grew a way to emit regular-LTO summaries (delete the + // fix-up), or linux moved to ThinLTO (it's moot), or neither — bump + // the threshold and keep it. + const RECHECK_AT_RUST_LLVM = "23.0.0"; + return cfg.rustLlvmVersion !== undefined && satisfiesRange(cfg.rustLlvmVersion, `>=${RECHECK_AT_RUST_LLVM}`); + }, + cleanup: + `Delete scripts/build/rust-lto-fix-cli.ts, the rust_lto_fix rule and rustLtoLinkInputs() in ` + + `rust.ts, unwrap its call sites in bun.ts, drop "llvm-tools" from rust-toolchain.toml's ` + + `components, and delete this entry.`, + }, { id: "rust-lld-for-crosslang-lto", issue: "https://rustc-dev-guide.rust-lang.org/backend/updating-llvm.html", From e43da769652d64c5538591870f5f4078b827c589 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:14:30 +0000 Subject: [PATCH 13/13] ci: retrigger