diff --git a/Cargo.lock b/Cargo.lock index 7b8ae79d6080..90844add059f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1610,7 +1610,6 @@ dependencies = [ "bun_core", "bun_errno", "bun_http_types", - "bun_js_parser", "bun_options_types", "bun_paths", "bun_ptr", @@ -2157,6 +2156,7 @@ dependencies = [ "bun_alloc", "bun_collections", "bun_core", + "bun_opaque", "bun_paths", "bun_wyhash", "const_format", diff --git a/src/bun_core/output.rs b/src/bun_core/output.rs index 17433f88d5d6..7985a4d7d0a2 100644 --- a/src/bun_core/output.rs +++ b/src/bun_core/output.rs @@ -1749,16 +1749,6 @@ pub fn clear_to_end() { // - reset // - reset -/// Lowercase lookup wrapper. The table -/// itself lives in `bun_output_tags` (shared with the `pretty_fmt!` proc-macro -/// so there is exactly one copy). -pub mod color_map { - #[inline] - pub fn get(name: &[u8]) -> Option<&'static str> { - bun_output_tags::color_for_bytes(name) - } -} - pub use ansi::{BOLD, DIM, RESET}; pub use bun_output_tags::{ansi, ansi_b}; @@ -2060,82 +2050,9 @@ pub fn pretty_fmt_args( } /// Runtime `` → ANSI rewriter, used for testing the proc-macro and for -/// the rare dynamic case. -/// -/// Colour table lives in `bun_output_tags`; the state machine is kept duplicated -/// vs `bun_core_macros::rewrite` because the two intentionally diverge in the -/// `{` arm (proc-macro rewrites specs `{s}`→`{}`; this side copies braces -/// verbatim) and on unknown tags (proc-macro errors; this side emits `""`). -pub fn pretty_fmt_runtime(fmt: &[u8], is_enabled: bool) -> Vec { - let mut out = Vec::with_capacity(fmt.len() * 4); - let mut i = 0usize; - while i < fmt.len() { - match fmt[i] { - b'\\' => { - i += 1; - if i < fmt.len() { - match fmt[i] { - b'<' | b'>' => { - out.push(fmt[i]); - i += 1; - } - _ => { - out.push(b'\\'); - out.push(fmt[i]); - i += 1; - } - } - } - } - b'>' => { - i += 1; - } - b'{' => { - while i < fmt.len() && fmt[i] != b'}' { - out.push(fmt[i]); - i += 1; - } - } - b'<' => { - i += 1; - let mut is_reset = i < fmt.len() && fmt[i] == b'/'; - if is_reset { - i += 1; - } - let start = i; - while i < fmt.len() && fmt[i] != b'>' { - i += 1; - } - let color_name = &fmt[start..i]; - let color_str: &str = 'picker: { - if let Some(lit) = color_map::get(color_name) { - break 'picker lit; - } else if color_name == b"r" { - is_reset = true; - break 'picker ""; - } else { - // Unknown tag: the `pretty_fmt!` proc-macro rejects - // this at its call sites; this runtime path drops the - // tag. - break 'picker ""; - } - }; - if is_enabled { - out.extend_from_slice(if is_reset { - RESET.as_bytes() - } else { - color_str.as_bytes() - }); - } - } - _ => { - out.push(fmt[i]); - i += 1; - } - } - } - out -} +/// the rare dynamic case. The implementation lives in `bun_output_tags` so the +/// `bun_clap_macros` proc-macro crate shares the same state machine. +pub use bun_output_tags::pretty_fmt_runtime; #[doc(hidden)] #[inline] diff --git a/src/bun_core/string/mod.rs b/src/bun_core/string/mod.rs index ef9c0306bea3..c08f25c8c043 100644 --- a/src/bun_core/string/mod.rs +++ b/src/bun_core/string/mod.rs @@ -2434,8 +2434,11 @@ pub mod printer { /// `MutableString`, and any other `crate::io::Write` sink. pub use crate::io::Write as PrinterWriter; + // PERF: `ascii_only` is a runtime arg so the large callers collapse to a + // single monomorphization instead of one per (ascii_only × quote_char × …) + // combo; see `write_pre_quoted_string_inner` for the same trade-off. #[inline] - pub(crate) fn can_print_without_escape(c: i32, ascii_only: bool) -> bool { + pub fn can_print_without_escape(c: i32, ascii_only: bool) -> bool { if c <= LAST_ASCII as i32 { c >= FIRST_ASCII as i32 && c != b'\\' as i32 @@ -2452,9 +2455,20 @@ pub mod printer { } } - /// Same algorithm as `bun_js_printer::write_pre_quoted_string`. - /// PERF: (quote_char, ascii_only, json, encoding) are runtime params — - /// profile if it shows up on a hot path. + /// `strings::Encoding` stand-in that derives `ConstParamTy` so it can be + /// used as a const-generic parameter (`const ENCODING: Encoding`). The + /// variant set is identical; convert at the boundary if a + /// `strings::Encoding` is ever needed. + #[derive(Clone, Copy, Debug, PartialEq, Eq, core::marker::ConstParamTy)] + pub enum Encoding { + Ascii, + Utf8, + Latin1, + Utf16, + } + + /// Runtime-encoding adapter: selects the matching monomorphized + /// [`write_pre_quoted_string_inner`] instance. pub fn write_pre_quoted_string( text_in: &[u8], writer: &mut W, @@ -2463,68 +2477,119 @@ pub mod printer { json: bool, encoding: StrEncoding, ) -> crate::CrateResult<()> { - debug_assert!(!json || quote_char == b'"'); - // utf16 view over the same bytes (only used when encoding == Utf16). - // Callers pass 2-byte-aligned even-length input for Utf16; `cast_slice` - // panics (rather than UB) if that contract is violated. - let text16: &[u16] = if encoding == StrEncoding::Utf16 { - crate::cast_slice::(text_in) - } else { - &[] - }; - let n: usize = if encoding == StrEncoding::Utf16 { - text16.len() - } else { - text_in.len() - }; + match encoding { + StrEncoding::Ascii => write_pre_quoted_string_inner::( + text_in, writer, quote_char, ascii_only, json, + ), + StrEncoding::Utf8 => write_pre_quoted_string_inner::( + text_in, writer, quote_char, ascii_only, json, + ), + StrEncoding::Latin1 => write_pre_quoted_string_inner::( + text_in, writer, quote_char, ascii_only, json, + ), + StrEncoding::Utf16 => write_pre_quoted_string_inner::( + text_in, writer, quote_char, ascii_only, json, + ), + } + } + + /// `quote_char` / `ascii_only` / `json` are runtime args: the branches on + /// them are cheap and well-predicted, and collapsing the monomorphizations + /// keeps the hot transpile pages dense. `ENCODING` stays `const` — it + /// changes the code-unit indexing structure of the loop, so a per-encoding + /// copy is genuinely different code. + #[inline(never)] + pub fn write_pre_quoted_string_inner( + text_in: &[u8], + writer: &mut W, + quote_char: u8, + ascii_only: bool, + json: bool, + ) -> crate::CrateResult<()> + where + W: PrinterWriter + ?Sized, + { + debug_assert!( + !(json && quote_char != b'"'), + "for json, quote_char must be '\"'" + ); + + let text = text_in; let mut i: usize = 0; + let n: usize = match ENCODING { + Encoding::Utf16 => text.len() / 2, + _ => text.len(), + }; + + macro_rules! code_unit_at { + ($idx:expr) => { + match ENCODING { + Encoding::Utf16 => { + let lo = text[$idx * 2]; + let hi = text[$idx * 2 + 1]; + u16::from_le_bytes([lo, hi]) as i32 + } + _ => text[$idx] as i32, + } + }; + } while i < n { - let width: u8 = match encoding { - StrEncoding::Latin1 | StrEncoding::Ascii | StrEncoding::Utf16 => 1, - StrEncoding::Utf8 => strings::wtf8_byte_sequence_length_with_invalid(text_in[i]), + let width: u8 = match ENCODING { + Encoding::Latin1 | Encoding::Ascii => 1, + Encoding::Utf8 => strings::wtf8_byte_sequence_length_with_invalid(text[i]), + Encoding::Utf16 => 1, }; let clamped_width = (width as usize).min(n.saturating_sub(i)); - let c: i32 = match encoding { - StrEncoding::Utf8 => { - let mut buf = [0u8; 4]; - buf[..clamped_width].copy_from_slice(&text_in[i..i + clamped_width]); - strings::decode_wtf8_rune_t::(buf, width, 0) + let c: i32 = match ENCODING { + Encoding::Utf8 => { + let bytes: [u8; 4] = match clamped_width { + 1 => [text[i], 0, 0, 0], + 2 => [text[i], text[i + 1], 0, 0], + 3 => [text[i], text[i + 1], text[i + 2], 0], + 4 => [text[i], text[i + 1], text[i + 2], text[i + 3]], + _ => unreachable!(), + }; + strings::decode_wtf8_rune_t::(bytes, width, 0) + } + Encoding::Ascii => { + debug_assert!(text[i] <= 0x7F); + text[i] as i32 } - StrEncoding::Ascii => { - debug_assert!(text_in[i] <= 0x7F); - text_in[i] as i32 + Encoding::Latin1 => text[i] as i32, + Encoding::Utf16 => { + // TODO: if this is a part of a surrogate pair, we could parse the whole codepoint in order + // to emit it as a single \u{result} rather than two paired \uLOW\uHIGH. + // eg: "\u{10334}" will convert to "𐌴" without this. + code_unit_at!(i) } - StrEncoding::Latin1 => text_in[i] as i32, - StrEncoding::Utf16 => text16[i] as i32, }; if can_print_without_escape(c, ascii_only) { - match encoding { - StrEncoding::Ascii | StrEncoding::Utf8 => { - let remain = &text_in[i + clamped_width..]; + match ENCODING { + Encoding::Ascii | Encoding::Utf8 => { + let remain = &text[i + clamped_width..]; if let Some(j) = strings::index_of_needs_escape_for_java_script_string( remain, quote_char, ) { - writer.write_all(&text_in[i..i + clamped_width])?; - i += clamped_width; - writer.write_all(&remain[..j as usize])?; - i += j as usize; + let j = j as usize; + writer.write_all(&text[i..i + clamped_width + j])?; + i += clamped_width + j; } else { - writer.write_all(&text_in[i..])?; + writer.write_all(&text[i..])?; break; } } - StrEncoding::Latin1 | StrEncoding::Utf16 => { - let mut cp = [0u8; 4]; - let cp_len = strings::encode_wtf8_rune(&mut cp, c as u32); - writer.write_all(&cp[..cp_len])?; + Encoding::Latin1 | Encoding::Utf16 => { + let mut codepoint_bytes = [0u8; 4]; + let codepoint_len = + strings::encode_wtf8_rune(&mut codepoint_bytes, c as u32); + writer.write_all(&codepoint_bytes[..codepoint_len])?; i += clamped_width; } } continue; } - match c { 0x07 => { writer.write_all(b"\\x07")?; @@ -2568,11 +2633,16 @@ pub mod printer { } 0x24 => { if quote_char == b'`' { - let next_is_brace = match encoding { - StrEncoding::Utf16 => i + 1 < n && text16[i + 1] == b'{' as u16, - _ => i + 1 < n && text_in[i + 1] == b'{', + let next = if i + clamped_width < n { + Some(code_unit_at!(i + clamped_width)) + } else { + None }; - writer.write_all(if next_is_brace { b"\\$" } else { b"$" })?; + if next == Some(b'{' as i32) { + writer.write_all(b"\\$")?; + } else { + writer.write_all(b"$")?; + } } else { writer.write_all(b"$")?; } @@ -2584,6 +2654,7 @@ pub mod printer { } _ => { i += width as usize; + if c <= 0xFF && !json { let h = hex2_upper(c as u8); writer.write_all(&[b'\\', b'x', h[0], h[1]])?; @@ -2605,9 +2676,14 @@ pub mod printer { bytes: &mut MutableString, ascii_only: bool, ) -> crate::CrateResult<()> { - // PERF: consider pre-growing via an estimated UTF-8 length — profile if it shows up on a hot path. + // ~12.5% slack heuristic: tab-indented JS (three.js) escapes ~9.4% of + // bytes, so `>> 4` (6.25%) would under-shoot and force a 2x doubling + // memcpy. Writer still grows on demand if this under-shoots. + bytes.grow_if_needed(text.len() + (text.len() >> 3) + 8)?; bytes.append_char(b'"')?; - write_pre_quoted_string(text, bytes, b'"', ascii_only, true, StrEncoding::Utf8)?; + write_pre_quoted_string_inner::<_, { Encoding::Utf8 }>( + text, bytes, b'"', ascii_only, true, + )?; bytes.append_char(b'"').expect("unreachable"); Ok(()) } diff --git a/src/bun_output_tags/lib.rs b/src/bun_output_tags/lib.rs index 239866f15050..93846fee2a70 100644 --- a/src/bun_output_tags/lib.rs +++ b/src/bun_output_tags/lib.rs @@ -1,10 +1,13 @@ -//! Single source of truth for Bun's `` → ANSI colour table **and** the +//! Single source of truth for Bun's `` → ANSI colour table, the //! named ANSI escape constants used directly by REPL / diff-printer / multi-run -//! output. Zero-dep `#![no_std]` leaf so both the proc-macro crate and runtime -//! crates can import it without a cycle. +//! output, **and** the runtime `` rewriter ([`pretty_fmt_runtime`]). +//! Zero-dep `#![no_std]` (+`alloc`) leaf so both the proc-macro crates and +//! runtime crates can import it without a cycle. #![no_std] +extern crate alloc; + /// Named ANSI SGR escape sequences. One canonical literal per colour/attribute; /// every other crate aliases this module rather than re-declaring the bytes. /// @@ -100,3 +103,84 @@ pub fn color_for_bytes(name: &[u8]) -> Option<&'static str> { } None } + +/// Runtime `` → ANSI rewriter: expands tags to escape sequences when +/// `is_enabled`, or strips them when not. `\<`/`\>` escapes pass the bracket +/// through, `{…}` spec bodies are copied verbatim, `` and `` emit +/// [`RESET`], and unknown tags are dropped silently. +/// +/// Shared by `bun_core::output` (CLI logging) and `bun_clap_macros` (which runs +/// it at macro-expansion time with `is_enabled = false` to bake tag-stripped +/// help strings into rodata). The `pretty_fmt!` proc-macro keeps its own +/// deliberately divergent variant (`{s}`→`{}` spec rewriting, compile errors on +/// unknown tags). +#[inline] +pub fn pretty_fmt_runtime(fmt: &[u8], is_enabled: bool) -> alloc::vec::Vec { + // `* 2` covers the worst-case expansion: the shortest tags (``, ``, + // 3 bytes) become 4-byte escapes, a ratio under 2. + let mut out = alloc::vec::Vec::with_capacity(fmt.len() * 2); + let mut i = 0usize; + while i < fmt.len() { + match fmt[i] { + b'\\' => { + i += 1; + if i < fmt.len() { + match fmt[i] { + b'<' | b'>' => { + out.push(fmt[i]); + i += 1; + } + _ => { + out.push(b'\\'); + out.push(fmt[i]); + i += 1; + } + } + } + } + b'>' => { + i += 1; + } + b'{' => { + while i < fmt.len() && fmt[i] != b'}' { + out.push(fmt[i]); + i += 1; + } + } + b'<' => { + i += 1; + let mut is_reset = i < fmt.len() && fmt[i] == b'/'; + if is_reset { + i += 1; + } + let start = i; + while i < fmt.len() && fmt[i] != b'>' { + i += 1; + } + let name = &fmt[start..i]; + let seq: &str = if let Some(c) = color_for_bytes(name) { + c + } else if name == b"r" { + is_reset = true; + "" + } else { + // Unknown tag: dropped silently (the `pretty_fmt!` + // proc-macro rejects these at compile time instead). + "" + }; + if is_enabled { + out.extend_from_slice(if is_reset { + RESET.as_bytes() + } else { + seq.as_bytes() + }); + } + } + _ => { + out.push(fmt[i]); + i += 1; + } + } + } + out +} diff --git a/src/clap_macros/lib.rs b/src/clap_macros/lib.rs index 0969a83031f5..0d2451ab89c8 100644 --- a/src/clap_macros/lib.rs +++ b/src/clap_macros/lib.rs @@ -265,91 +265,15 @@ fn byte_str_b(s: &[u8]) -> LitByteStr { LitByteStr::new(s, Span::call_site()) } -/// 1:1 port of `bun_core::output::pretty_fmt_runtime` — rewrites Bun's `` -/// colour markup to ANSI escape sequences when `is_enabled`, or strips it when -/// not. Run here at macro-expansion time with `is_enabled = false` so each param -/// description's tag-stripped form (`Help::msg_plain`) is a `const` byte literal -/// in rodata. The ANSI form is *not* baked in — it is rare (only `bun --help` on -/// a colour TTY) and would otherwise roughly triple the help-string rodata, so -/// `bun_clap::pretty_help_desc` derives it from `Help::msg` on demand instead. -fn pretty_rewrite(fmt: &[u8], is_enabled: bool) -> Vec { - use bun_output_tags::{RESET, color_for_bytes}; - let mut out: Vec = Vec::with_capacity(fmt.len() * 2); - let mut i = 0usize; - while i < fmt.len() { - match fmt[i] { - b'\\' => { - i += 1; - if i < fmt.len() { - match fmt[i] { - b'<' | b'>' => { - out.push(fmt[i]); - i += 1; - } - _ => { - out.push(b'\\'); - out.push(fmt[i]); - i += 1; - } - } - } - } - b'>' => { - i += 1; - } - b'{' => { - while i < fmt.len() && fmt[i] != b'}' { - out.push(fmt[i]); - i += 1; - } - } - b'<' => { - i += 1; - let mut is_reset = i < fmt.len() && fmt[i] == b'/'; - if is_reset { - i += 1; - } - let start = i; - while i < fmt.len() && fmt[i] != b'>' { - i += 1; - } - let name = &fmt[start..i]; - let seq: &str = if let Some(c) = color_for_bytes(name) { - c - } else if name == b"r" { - is_reset = true; - "" - } else { - // Unknown tag: `pretty_fmt_runtime` (the path this replaces) - // drops it silently. Match - // the lenient runtime behaviour — a compile error would be - // stricter than what shipped, and param specs don't carry - // unknown tags anyway. - "" - }; - if is_enabled { - out.extend_from_slice(if is_reset { - RESET.as_bytes() - } else { - seq.as_bytes() - }); - } - } - _ => { - out.push(fmt[i]); - i += 1; - } - } - } - out -} - fn emit_param(krate: &Path, p: &Param) -> TokenStream2 { let msg = byte_str(&p.id.msg); - // Precompute only the tag-stripped form (the non-TTY help path needs it ready - // without a TTY check); the ANSI form is derived lazily from `msg` by - // `bun_clap::pretty_help_desc`, so it stays out of rodata. - let msg_plain = byte_str_b(&pretty_rewrite(p.id.msg.as_bytes(), false)); + // Precompute only the tag-stripped form as a rodata `const` (non-TTY help + // needs it without a TTY check); the ANSI form is derived lazily by + // `bun_clap::pretty_help_desc` since baking it would ~triple help rodata. + let msg_plain = byte_str_b(&bun_output_tags::pretty_fmt_runtime( + p.id.msg.as_bytes(), + false, + )); let value = byte_str(&p.id.value); let short = match p.names.short { diff --git a/src/collections/StaticHashMap.rs b/src/collections/StaticHashMap.rs index 4769089f1c2f..b4b0a5fbf23d 100644 --- a/src/collections/StaticHashMap.rs +++ b/src/collections/StaticHashMap.rs @@ -281,10 +281,6 @@ impl< Ok(()) } - pub fn put_context(&mut self, key: K, value: V, _ctx: Ctx) -> Result<(), AllocError> { - self.put(key, value) - } - pub fn get_or_put(&mut self, key: K) -> Result, AllocError> { self.ensure_unused_capacity(1)?; Ok(self.get_or_put_assume_capacity(key)) diff --git a/src/collections/array_hash_map.rs b/src/collections/array_hash_map.rs index 8a16ba3a3ca9..2c5af57a372c 100644 --- a/src/collections/array_hash_map.rs +++ b/src/collections/array_hash_map.rs @@ -602,18 +602,6 @@ impl ArrayHashMap { self.drop_index(); } - /// Same as `ensure_total_capacity` but takes an explicit `ctx` for the - /// stored key type. Capacity reservation is purely a Vec operation here, - /// so the context is accepted and ignored. - #[inline] - pub fn ensure_total_capacity_context( - &mut self, - n: usize, - _ctx: Ctx, - ) -> Result<(), AllocError> { - self.ensure_total_capacity(n) - } - /// Insert/replace using an externally-supplied /// hash/eql context instead of the stored `C`. Used when `C = AutoContext` /// can't satisfy `K: Hash` (e.g. `bun_semver::String`, whose hash needs the @@ -1972,15 +1960,6 @@ impl StringHashMap Ok(()) } - /// Insert a pre-boxed key without re-allocating it. Uses `try_reserve` so - /// OOM surfaces as `Err` instead of aborting; callers can roll back side - /// effects on failure. - pub fn put_owned(&mut self, key: Box<[u8], A>, value: V) -> Result<(), AllocError> { - self.inner.try_reserve(1).map_err(|_| AllocError)?; - self.inner.insert(StringHashMapKey::owned(key), value); - Ok(()) - } - /// PERF: std::HashMap cannot skip the grow check, so this is /// just `put` without the `Result`. #[inline] diff --git a/src/collections/array_list.rs b/src/collections/array_list.rs index 510ddce2f969..e3a7e9badfe4 100644 --- a/src/collections/array_list.rs +++ b/src/collections/array_list.rs @@ -79,24 +79,11 @@ impl ArrayListAlignedIn { } } - // Sentinel-terminated owned slices are not a Rust type, so this takes a `Box<[T]>` with - // the sentinel already stripped. - pub fn from_owned_slice_sentinel(/* sentinel: T, */ slice: Slice) -> Self { - Self { - unmanaged: Vec::from(slice), - } - } - /// This method empties `self`. pub fn move_to_unmanaged(&mut self) -> Unmanaged { mem::take(&mut self.unmanaged) } - /// Unlike `move_to_unmanaged`, this method *consumes* `self`. - pub fn into_unmanaged_with_allocator(self) -> (Unmanaged, ()) { - (self.unmanaged, ()) - } - /// The contents of `unmanaged` must have been allocated by the global allocator. /// This function takes ownership of `unmanaged`. pub fn from_unmanaged(unmanaged: Unmanaged) -> Self { @@ -134,31 +121,6 @@ impl ArrayListAlignedIn { self.unmanaged.insert(i, item); } - /// Note that this creates *shallow* copies of `value`. - pub fn add_many_at( - &mut self, - index: usize, - value: T, - count: usize, - ) -> Result<&mut [T], AllocError> - where - T: Clone, - { - self.unmanaged - .splice(index..index, core::iter::repeat_n(value, count)); - Ok(&mut self.unmanaged[index..index + count]) - } - - /// Note that this creates *shallow* copies of `value`. - pub fn add_many_at_assume_capacity(&mut self, index: usize, value: T, count: usize) -> &mut [T] - where - T: Clone, - { - self.unmanaged - .splice(index..index, core::iter::repeat_n(value, count)); - &mut self.unmanaged[index..index + count] - } - /// Note that this `Clone`s each element of `new_items`. pub fn insert_slice(&mut self, index: usize, new_items: &[T]) -> Result<(), AllocError> where @@ -186,15 +148,6 @@ impl ArrayListAlignedIn { Ok(()) } - /// This method `Drop`s the removed items. - /// Note that this `Clone`s each element of `new_items` (see `insert_slice`). - pub fn replace_range_assume_capacity(&mut self, start: usize, len: usize, new_items: &[T]) - where - T: Clone, - { - let _ = self.replace_range(start, len, new_items); - } - pub fn append(&mut self, item: T) -> Result<(), AllocError> { self.unmanaged.push(item); Ok(()) @@ -229,25 +182,6 @@ impl ArrayListAlignedIn { self.unmanaged.extend_from_slice(new_items); } - /// Note that this `Clone`s each element of `new_items` (see `insert_slice`). - pub fn append_unaligned_slice(&mut self, new_items: &[T]) -> Result<(), AllocError> - where - T: Clone, - { - // Rust `&[T]` is always naturally aligned, so this is identical to `append_slice`; a - // caller that truly has unaligned bytes needs `ptr::read_unaligned` at the call site. - self.unmanaged.extend_from_slice(new_items); - Ok(()) - } - - /// Note that this `Clone`s each element of `new_items` (see `insert_slice`). - pub fn append_unaligned_slice_assume_capacity(&mut self, new_items: &[T]) - where - T: Clone, - { - self.unmanaged.extend_from_slice(new_items); - } - /// Note that this creates *shallow* copies of `value`. #[inline] pub fn append_n_times(&mut self, value: T, n: usize) -> Result<(), AllocError> @@ -340,19 +274,6 @@ impl ArrayListAlignedIn { &items[items.len() - 1] } - pub fn get_last_mut(&mut self) -> &mut T { - let len = self.unmanaged.len(); - &mut self.unmanaged[len - 1] - } - - pub fn get_last_or_null(&self) -> Option<&T> { - if self.is_empty() { - None - } else { - Some(self.get_last()) - } - } - pub fn is_empty(&self) -> bool { self.items().is_empty() } diff --git a/src/collections/bit_set.rs b/src/collections/bit_set.rs index c1b843e51396..1674c321f8f1 100644 --- a/src/collections/bit_set.rs +++ b/src/collections/bit_set.rs @@ -317,23 +317,6 @@ impl IntegerBitSet { other.subset_of(self) } - /// Returns the complement bit sets. Bits in the result - /// are set if the corresponding bits were not set. - pub fn complement(self) -> Self { - let mut result = self; - result.toggle_all(); - result - } - - /// Returns the union of two bit sets. Bits in the - /// result are set if the corresponding bits were set - /// in either input. - pub fn union_with(self, other: Self) -> Self { - let mut result = self; - result.set_union(other); - result - } - /// Returns the intersection of two bit sets. Bits in /// the result are set if the corresponding bits were /// set in both inputs. @@ -343,24 +326,6 @@ impl IntegerBitSet { result } - /// Returns the xor of two bit sets. Bits in the - /// result are set if the corresponding bits were - /// not the same in both inputs. - pub fn xor_with(self, other: Self) -> Self { - let mut result = self; - result.toggle_set(other); - result - } - - /// Returns the difference of two bit sets. Bits in - /// the result are set if set in the first but not - /// set in the second set. - pub fn difference_with(self, other: Self) -> Self { - let mut result = self; - result.set_intersection(other.complement()); - result - } - /// Iterates through the items in the set, according to the options. /// The default options (.{}) will iterate indices of set bits in /// ascending order. Modifications to the underlying bit set may @@ -669,23 +634,6 @@ impl ArrayBitSet { other.subset_of(self) } - /// Returns the complement bit sets. Bits in the result - /// are set if the corresponding bits were not set. - pub fn complement(&self) -> Self { - let mut result = *self; - result.toggle_all(); - result - } - - /// Returns the union of two bit sets. Bits in the - /// result are set if the corresponding bits were set - /// in either input. - pub fn union_with(&self, other: &Self) -> Self { - let mut result = *self; - result.set_union(other); - result - } - /// Returns the intersection of two bit sets. Bits in /// the result are set if the corresponding bits were /// set in both inputs. @@ -705,24 +653,6 @@ impl ArrayBitSet { false } - /// Returns the xor of two bit sets. Bits in the - /// result are set if the corresponding bits were - /// not the same in both inputs. - pub fn xor_with(&self, other: &Self) -> Self { - let mut result = *self; - result.toggle_set(other); - result - } - - /// Returns the difference of two bit sets. Bits in - /// the result are set if set in the first but not - /// set in the second set. - pub fn difference_with(&self, other: &Self) -> Self { - let mut result = *self; - result.set_intersection(&other.complement()); - result - } - /// Iterates through the items in the set, according to the options. /// The default options (.{}) will iterate indices of set bits in /// ascending order. Modifications to the underlying bit set may @@ -823,15 +753,6 @@ impl DynamicBitSetUnmanaged { unsafe { slice::from_raw_parts_mut(self.masks, n) } } - /// Raw pointer to the mask words. Use this (not `masks_slice{,_mut}`) when - /// `self` and another `DynamicBitSetUnmanaged` may point at the same - /// storage and both are accessed in the same operation — forming - /// overlapping `&mut [usize]` / `&[usize]` would be UB. - #[inline(always)] - pub fn masks_ptr(&self) -> *mut usize { - self.masks - } - /// `self.masks[i] = f(self.masks[i], other.masks[i])` for every mask word. /// Centralises the binary set-op loop (`set_union` / `set_intersection` / /// `set_exclude` / `toggle_set` / `copy_into`) behind a single audited @@ -1149,14 +1070,6 @@ impl DynamicBitSetUnmanaged { self.zip_masks_raw(other, |a, b| a & b); } - pub fn set_exclude_two(&mut self, other: &Self, third: &Self) { - debug_assert!(other.bit_length == self.bit_length); - // Two passes is equivalent to the original fused loop: each word is - // independent, so `(a & !b) & !c` per index is associative across passes. - self.zip_masks_raw(other, |a, b| a & !b); - self.zip_masks_raw(third, |a, c| a & !c); - } - pub fn set_exclude(&mut self, other: &Self) { debug_assert!(other.bit_length == self.bit_length); self.zip_masks_raw(other, |a, b| a & !b); diff --git a/src/collections/lib.rs b/src/collections/lib.rs index 57547112bce9..76e481857433 100644 --- a/src/collections/lib.rs +++ b/src/collections/lib.rs @@ -420,13 +420,6 @@ impl SmallList { self.0.insert_many(index as usize, items.iter().cloned()) } #[inline] - pub fn insert_slice_assume_capacity(&mut self, index: u32, items: &[T]) - where - T: Clone, - { - self.0.insert_many(index as usize, items.iter().cloned()) - } - #[inline] pub fn pop(&mut self) -> Option { self.0.pop() } diff --git a/src/collections/multi_array_list.rs b/src/collections/multi_array_list.rs index 883f3d72888b..85f9d835002b 100644 --- a/src/collections/multi_array_list.rs +++ b/src/collections/multi_array_list.rs @@ -536,7 +536,7 @@ impl<'a, F> ColMut<'a, F> { } } -/// Index-based comparison context for `sort` / `sort_span` / `sort_unstable`. +/// Index-based comparison context for `sort` / `sort_unstable`. pub trait SortContext { fn less_than(&self, a_index: usize, b_index: usize) -> bool; } @@ -1208,21 +1208,11 @@ impl MultiArrayList { self.sort_internal::(0, self.len, ctx); } - /// Stable sort of `[a, b)` by index-based context. - pub fn sort_span(&mut self, a: usize, b: usize, ctx: &C) { - self.sort_internal::(a, b, ctx); - } - /// Unstable sort by index-based context. pub fn sort_unstable(&mut self, ctx: &C) { self.sort_internal::(0, self.len, ctx); } - /// Unstable sort of `[a, b)` by index-based context. - pub fn sort_span_unstable(&mut self, a: usize, b: usize, ctx: &C) { - self.sort_internal::(a, b, ctx); - } - pub fn capacity_in_bytes(capacity: usize) -> usize { Reflected::::ELEM_BYTES * capacity } diff --git a/src/collections/pool.rs b/src/collections/pool.rs index 6341d620af60..dfdba6023c14 100644 --- a/src/collections/pool.rs +++ b/src/collections/pool.rs @@ -66,20 +66,16 @@ impl Node { self.next = std::ptr::from_mut::>(new_node); } - /// Remove a node from the list. - /// - /// Arguments: - /// node: Pointer to the node to be removed. - /// Returns: - /// node removed - pub fn remove_next(&mut self) -> Option<*mut Node> { - let next_node = if self.next.is_null() { - return None; - } else { - self.next - }; - self.next = Node::next_of(next_node); - Some(next_node) + /// Iterate over each next node, returning the count of all nodes except the starting one. + /// This operation is O(N). + pub fn count_children(&self) -> usize { + let mut count: usize = 0; + let mut it: *const Node = self.next; + while !it.is_null() { + count += 1; + it = Node::next_of(it); + } + count } /// Iterate over the singly-linked list from this node, until the final node is found. @@ -94,18 +90,6 @@ impl Node { it = next; } } - - /// Iterate over each next node, returning the count of all nodes except the starting one. - /// This operation is O(N). - pub fn count_children(&self) -> usize { - let mut count: usize = 0; - let mut it: *const Node = self.next; - while !it.is_null() { - count += 1; - it = Node::next_of(it); - } - count - } } pub struct SinglyLinkedList { diff --git a/src/ini/lib.rs b/src/ini/lib.rs index 194aedb197ec..b5b718ca186e 100644 --- a/src/ini/lib.rs +++ b/src/ini/lib.rs @@ -1496,7 +1496,7 @@ mod draft { if let Some(public_hoist_pattern_expr) = out.get(b"public-hoist-pattern") { install.public_hoist_pattern = - match pnpm_matcher_from_expr(&public_hoist_pattern_expr, log, source, bump) { + match PnpmMatcher::from_expr(&public_hoist_pattern_expr, log, source) { Ok(v) => Some(v), Err(FromExprError::OutOfMemory) => return Err(AllocError), Err(_) => { @@ -1508,16 +1508,15 @@ mod draft { } if let Some(hoist_pattern_expr) = out.get(b"hoist-pattern") { - install.hoist_pattern = - match pnpm_matcher_from_expr(&hoist_pattern_expr, log, source, bump) { - Ok(v) => Some(v), - Err(FromExprError::OutOfMemory) => return Err(AllocError), - Err(_) => { - // error.InvalidRegExp, error.UnexpectedExpr - log.reset(); - None - } - }; + install.hoist_pattern = match PnpmMatcher::from_expr(&hoist_pattern_expr, log, source) { + Ok(v) => Some(v), + Err(FromExprError::OutOfMemory) => return Err(AllocError), + Err(_) => { + // error.InvalidRegExp, error.UnexpectedExpr + log.reset(); + None + } + }; } let mut registry_map = install.scoped.take().unwrap_or_default(); @@ -1803,125 +1802,7 @@ mod draft { Ok(()) } - use bun_install_types::NodeLinker::{ - Behavior as PnpmBehavior, CreateMatcherError, FromExprError, Matcher as PnpmMatcherEntry, - PnpmMatcher, create_matcher, - }; - - /// `PnpmMatcher.fromExpr` operating on - /// `bun_ast::Expr` instead of the lower-tier `bun_ast::Expr`. - /// - /// `bun_install_types` (T2) cannot depend on `bun_js_parser` (T4), - /// and the two `ExprData` enums are distinct (closed Rust enums; only the leaf - /// `E::*` payloads are shared). `bun_ini` depends on both, so the T4-typed - /// overload lives here. The matcher construction is delegated to the shared - /// `create_matcher` helper in `bun_install_types::NodeLinker`. - fn pnpm_matcher_from_expr( - expr: &Expr, - log: &mut Log, - source: &Source, - bump: &Arena, - ) -> Result { - let mut buf: Vec = Vec::new(); - - // bun.jsc.initialize(false) is performed lazily inside the regex vtable - // compile hook (tier-6 owns it). - - let mut matchers: Vec = Vec::new(); - let mut has_include = false; - let mut has_exclude = false; - - match &expr.data { - ExprData::EString(s) => { - // SAFETY: arena-backed `EString::slice` mutates only its own - // resolved-data cache; the StoreRef pointee outlives this call. - let s_mut: &mut E::EString = unsafe { &mut *s.as_ptr() }; - let pattern = s_mut.slice(bump); - let matcher = match create_matcher(pattern, &mut buf) { - Ok(m) => m, - Err(CreateMatcherError::OutOfMemory) => return Err(FromExprError::OutOfMemory), - Err(CreateMatcherError::InvalidRegExp) => { - log.add_error_fmt_opts( - format_args!("Invalid regex: {}", bstr::BStr::new(pattern)), - bun_ast::AddErrorOptions { - loc: expr.loc, - redact_sensitive_information: true, - source: Some(source), - ..Default::default() - }, - ); - return Err(FromExprError::InvalidRegExp); - } - }; - has_include = has_include || !matcher.is_exclude; - has_exclude = has_exclude || matcher.is_exclude; - matchers.push(matcher); - } - ExprData::EArray(patterns) => { - for pattern_expr in patterns.items.slice() { - if let Some(pattern) = pattern_expr.as_string_cloned(bump)? { - let matcher = match create_matcher(pattern, &mut buf) { - Ok(m) => m, - Err(CreateMatcherError::OutOfMemory) => { - return Err(FromExprError::OutOfMemory); - } - Err(CreateMatcherError::InvalidRegExp) => { - log.add_error_fmt_opts( - format_args!("Invalid regex: {}", bstr::BStr::new(pattern)), - bun_ast::AddErrorOptions { - loc: pattern_expr.loc, - redact_sensitive_information: true, - source: Some(source), - ..Default::default() - }, - ); - return Err(FromExprError::InvalidRegExp); - } - }; - has_include = has_include || !matcher.is_exclude; - has_exclude = has_exclude || matcher.is_exclude; - matchers.push(matcher); - } else { - log.add_error_opts( - b"Expected a string or an array of strings", - bun_ast::AddErrorOptions { - loc: pattern_expr.loc, - redact_sensitive_information: true, - source: Some(source), - ..Default::default() - }, - ); - return Err(FromExprError::UnexpectedExpr); - } - } - } - _ => { - log.add_error_opts( - b"Expected a string or an array of strings", - bun_ast::AddErrorOptions { - loc: expr.loc, - redact_sensitive_information: true, - source: Some(source), - ..Default::default() - }, - ); - return Err(FromExprError::UnexpectedExpr); - } - } - - let behavior = if !has_include { - PnpmBehavior::AllMatchersExclude - } else if !has_exclude { - PnpmBehavior::AllMatchersInclude - } else { - PnpmBehavior::HasExcludeAndIncludeMatchers - }; - - Ok(PnpmMatcher { - matchers: matchers.into_boxed_slice(), - behavior, - }) - } + use bun_install_types::NodeLinker::{FromExprError, PnpmMatcher}; fn handle_auth( v: &mut NpmRegistry, diff --git a/src/install/PackageInstall.rs b/src/install/PackageInstall.rs index d21790e80947..3384028992bd 100644 --- a/src/install/PackageInstall.rs +++ b/src/install/PackageInstall.rs @@ -1324,232 +1324,153 @@ impl<'a> PackageInstall<'a> { return res; } - #[cfg(windows)] - type WinSlice<'b> = &'b mut [u16]; #[cfg(not(windows))] - type WinSlice<'b> = (); - #[cfg(windows)] - type WinOffset = usize; - #[cfg(not(windows))] - type WinOffset = (); - - // Two overlapping slices into the same buffer (`head` is the whole - // buffer, `to_copy_into` is its tail) would be two live aliasing - // `&mut [u16]`, which is UB — pass head buffer + tail offset and - // reslice inside. fn copy( destination_dir_: &Dir, walker: &mut Walker, mut progress_: Option<&mut Progress>, - to_copy_into1_offset: WinOffset, - head1: WinSlice<'_>, - to_copy_into2_offset: WinOffset, - head2: WinSlice<'_>, ) -> crate::Result { - #[cfg(not(windows))] let mut real_file_count: u32 = 0; - #[cfg(windows)] - let real_file_count: u32 = 0; - #[cfg(not(windows))] let mut copy_file_state = bun_sys::copy_file::CopyFileState::default(); - #[cfg(not(windows))] - let _ = (to_copy_into1_offset, head1, to_copy_into2_offset, head2); while let Some(entry) = walker.next()? { - #[cfg(windows)] - { - use bun_sys::windows::{self, Win32ErrorExt as _}; - match entry.kind { - EntryKind::Directory | EntryKind::File => {} - _ => continue, - } - - if entry.path.len() > head1.len() - to_copy_into1_offset - || entry.path.len() > head2.len() - to_copy_into2_offset - { - return Err(crate::Error::Sys(bun_errno::SystemErrno::ENAMETOOLONG)); - } - - let dest_len = to_copy_into1_offset + entry.path.len(); - head1[to_copy_into1_offset..dest_len].copy_from_slice(entry.path.as_slice()); - head1[dest_len] = 0; - let dest = bun_core::WStr::from_buf(head1, dest_len); + if entry.kind != EntryKind::File { + continue; + } + real_file_count += 1; - let src_len = to_copy_into2_offset + entry.path.len(); - head2[to_copy_into2_offset..src_len].copy_from_slice(entry.path.as_slice()); - head2[src_len] = 0; - let src = bun_core::WStr::from_buf(head2, src_len); + let in_file = sys::openat(entry.dir, entry.basename, sys::O::RDONLY, 0)?; + let _close_in = sys::CloseOnDrop::new(in_file); - match entry.kind { - EntryKind::Directory => { - // SAFETY: FFI — src/dest are valid NUL-terminated WStr buffers built - // into head1/head2 above. - if unsafe { - windows::CreateDirectoryExW( - src.as_ptr(), - dest.as_ptr(), - core::ptr::null_mut(), - ) - } == 0 - { - let _ = bun_sys::MakePath::make_path_u16( - destination_dir_, - entry.path.as_slice(), - ); - } + bun_output::scoped_log!( + install, + "createFile {} {}\n", + destination_dir_.fd(), + bstr::BStr::new(entry.path.as_bytes()) + ); + // Open O_WRONLY|O_CREAT|O_TRUNC, mode 0o666. + let create = |path: &ZStr| { + sys::openat( + destination_dir_.fd(), + path, + sys::O::WRONLY | sys::O::CREAT | sys::O::TRUNC, + 0o666, + ) + }; + let outfile = match create(entry.path) { + Ok(f) => f, + Err(_) => 'brk: { + let entry_dirname = bun_paths::resolve_path::dirname::< + bun_paths::platform::Auto, + >(entry.path.as_bytes()); + if !entry_dirname.is_empty() { + let _ = bun_sys::MakePath::make_path::( + destination_dir_, + entry_dirname, + ); } - EntryKind::File => { - // SAFETY: FFI — src/dest are valid NUL-terminated WStr buffers. - if unsafe { windows::CopyFileW(src.as_ptr(), dest.as_ptr(), 0) } == 0 { - if let Some(entry_dirname) = - bun_paths::Dirname::dirname_u16(entry.path.as_slice()) - { - let _ = bun_sys::MakePath::make_path_u16( - destination_dir_, - entry_dirname, - ); - // SAFETY: FFI — src/dest are valid NUL-terminated WStr buffers. - if unsafe { windows::CopyFileW(src.as_ptr(), dest.as_ptr(), 0) } - != 0 - { - continue; - } - } - - if let Some(progress) = progress_.as_deref_mut() { + match create(entry.path) { + Ok(f) => break 'brk f, + Err(err) => { + if let Some(progress) = progress_ { progress.root.end(); progress.refresh(); } - if let Some(err) = windows::Win32Error::get().to_system_errno() { - bun_core::pretty_errorln!( - "{}: copying file {}", - <&'static str>::from(err), - bun_core::fmt::fmt_os_path( - entry.path.as_slice(), - Default::default() - ) - ); - } else { - bun_core::pretty_errorln!( - "error copying file {}", - bun_core::fmt::fmt_os_path( - entry.path.as_slice(), - Default::default() - ) - ); - } - + bun_core::pretty_errorln!( + "{}: copying file {}", + bstr::BStr::new(err.name()), + bun_core::fmt::fmt_os_path( + entry.path.as_bytes(), + Default::default() + ) + ); Global::crash(); } } - _ => unreachable!(), // handled above } - } - #[cfg(not(windows))] + }; + let _close_out = sys::CloseOnDrop::new(outfile); + + #[cfg(unix)] { - if entry.kind != EntryKind::File { + let Ok(stat) = sys::fstat(in_file) else { continue; - } - real_file_count += 1; + }; + // `sys::fchmod` is the safe by-value-fd wrapper (kernel + // validates the fd; no memory-safety preconditions). + // Result intentionally ignored. + let _ = sys::fchmod(outfile, stat.st_mode as bun_sys::Mode); + } - let in_file = sys::openat(entry.dir, entry.basename, sys::O::RDONLY, 0)?; - let _close_in = sys::CloseOnDrop::new(in_file); + if let Err(err) = + bun_sys::copy_file::copy_file_with_state(in_file, outfile, &mut copy_file_state) + { + if let Some(progress) = progress_.as_deref_mut() { + progress.root.end(); + progress.refresh(); + } - bun_output::scoped_log!( - install, - "createFile {} {}\n", - destination_dir_.fd(), - bstr::BStr::new(entry.path.as_bytes()) + bun_core::pretty_errorln!( + "{}: copying file {}", + bstr::BStr::new(err.name()), + bun_core::fmt::fmt_os_path(entry.path.as_bytes(), Default::default()) ); - // Open O_WRONLY|O_CREAT|O_TRUNC, mode 0o666. - let create = |path: &ZStr| { - sys::openat( - destination_dir_.fd(), - path, - sys::O::WRONLY | sys::O::CREAT | sys::O::TRUNC, - 0o666, - ) - }; - let outfile = match create(entry.path) { - Ok(f) => f, - Err(_) => 'brk: { - let entry_dirname = bun_paths::resolve_path::dirname::< - bun_paths::platform::Auto, - >(entry.path.as_bytes()); - if !entry_dirname.is_empty() { - let _ = bun_sys::MakePath::make_path::( - destination_dir_, - entry_dirname, - ); - } - match create(entry.path) { - Ok(f) => break 'brk f, - Err(err) => { - if let Some(progress) = progress_ { - progress.root.end(); - progress.refresh(); - } + Global::crash(); + } + } - bun_core::pretty_errorln!( - "{}: copying file {}", - bstr::BStr::new(err.name()), - bun_core::fmt::fmt_os_path( - entry.path.as_bytes(), - Default::default() - ) - ); - Global::crash(); - } + Ok(real_file_count) + } + + #[cfg(windows)] + let result = { + use bun_sys::windows::{self, Win32ErrorExt as _}; + let destination_dir_ = &state.subdir; + let mut progress_ = self.progress.as_deref_mut(); + walk_install_dir_windows( + destination_dir_, + state.walker.as_mut().unwrap(), + state.to_copy_buf_off, + &mut state.buf[..], + state.to_copy_buf2_off, + &mut state.buf2[..], + |dest, src, entry_path| { + // SAFETY: FFI — src/dest are valid NUL-terminated WStr buffers. + if unsafe { windows::CopyFileW(src.as_ptr(), dest.as_ptr(), 0) } == 0 { + if let Some(entry_dirname) = bun_paths::Dirname::dirname_u16(entry_path) { + let _ = + bun_sys::MakePath::make_path_u16(destination_dir_, entry_dirname); + // SAFETY: FFI — src/dest are valid NUL-terminated WStr buffers. + if unsafe { windows::CopyFileW(src.as_ptr(), dest.as_ptr(), 0) } != 0 { + return Ok(()); } } - }; - let _close_out = sys::CloseOnDrop::new(outfile); - - #[cfg(unix)] - { - let Ok(stat) = sys::fstat(in_file) else { - continue; - }; - // `sys::fchmod` is the safe by-value-fd wrapper (kernel - // validates the fd; no memory-safety preconditions). - // Result intentionally ignored. - let _ = sys::fchmod(outfile, stat.st_mode as bun_sys::Mode); - } - if let Err(err) = bun_sys::copy_file::copy_file_with_state( - in_file, - outfile, - &mut copy_file_state, - ) { if let Some(progress) = progress_.as_deref_mut() { progress.root.end(); progress.refresh(); } - bun_core::pretty_errorln!( - "{}: copying file {}", - bstr::BStr::new(err.name()), - bun_core::fmt::fmt_os_path(entry.path.as_bytes(), Default::default()) - ); + if let Some(err) = windows::Win32Error::get().to_system_errno() { + bun_core::pretty_errorln!( + "{}: copying file {}", + <&'static str>::from(err), + bun_core::fmt::fmt_os_path(entry_path, Default::default()) + ); + } else { + bun_core::pretty_errorln!( + "error copying file {}", + bun_core::fmt::fmt_os_path(entry_path, Default::default()) + ); + } + Global::crash(); } - } - } - - Ok(real_file_count) - } - - #[cfg(windows)] - let result = copy( - &state.subdir, - state.walker.as_mut().unwrap(), - self.progress.as_deref_mut(), - state.to_copy_buf_off, - &mut state.buf[..], - state.to_copy_buf2_off, - &mut state.buf2[..], - ); + Ok(()) + }, + ) + }; #[cfg(not(windows))] let result = copy( &state.subdir, @@ -1557,10 +1478,6 @@ impl<'a> PackageInstall<'a> { // (`state.walker()` would reborrow `&mut state` and conflict). state.walker.as_mut().unwrap(), self.progress.as_deref_mut(), - (), - (), - (), - (), ); self.file_count = match result { @@ -1795,37 +1712,18 @@ impl<'a> PackageInstall<'a> { } } - #[cfg(windows)] - type WinSlice<'b> = &'b mut [u16]; - #[cfg(not(windows))] - type WinSlice<'b> = (); - #[cfg(windows)] - type WinOffset = usize; - #[cfg(not(windows))] - type WinOffset = (); - #[cfg(windows)] - type Head2Char = u16; - #[cfg(not(windows))] - type Head2Char = u8; - // Two overlapping slices into the same buffer (`head` is the whole // buffer, `to_copy_into` is its tail) would be two live aliasing // `&mut`, which is UB — pass head buffer + tail offset and reslice // inside. + #[cfg(not(windows))] fn copy( destination_dir: &Dir, walker: &mut Walker, - to_copy_into1_offset: WinOffset, - head1: WinSlice<'_>, to_copy_into2_offset: usize, - head2: &mut [Head2Char], + head2: &mut [u8], ) -> crate::Result { - #[cfg(not(windows))] let mut real_file_count: u32 = 0; - #[cfg(windows)] - let real_file_count: u32 = 0; - #[cfg(not(windows))] - let _ = (to_copy_into1_offset, head1); while let Some(entry) = walker.next()? { #[cfg(unix)] { @@ -1860,99 +1758,51 @@ impl<'a> PackageInstall<'a> { _ => {} } } - #[cfg(not(unix))] - { - use bun_sys::windows; - match entry.kind { - EntryKind::Directory | EntryKind::File => {} - _ => continue, - } - - if entry.path.len() > head1.len() - to_copy_into1_offset - || entry.path.len() > head2.len() - to_copy_into2_offset - { - return Err(crate::Error::Sys(bun_errno::SystemErrno::ENAMETOOLONG)); - } - - let dest_len = to_copy_into1_offset + entry.path.len(); - head1[to_copy_into1_offset..dest_len].copy_from_slice(entry.path.as_slice()); - head1[dest_len] = 0; - let dest = bun_core::WStr::from_buf(head1, dest_len); + } - let src_len = to_copy_into2_offset + entry.path.len(); - head2[to_copy_into2_offset..src_len].copy_from_slice(entry.path.as_slice()); - head2[src_len] = 0; - let src = bun_core::WStr::from_buf(head2, src_len); + Ok(real_file_count) + } - match entry.kind { - EntryKind::Directory => { - // SAFETY: FFI — src/dest are valid NUL-terminated WStr buffers built - // into head1/head2 above. - if unsafe { - windows::CreateDirectoryExW( - src.as_ptr(), - dest.as_ptr(), - core::ptr::null_mut(), - ) - } == 0 - { - let _ = bun_sys::MakePath::make_path_u16( - destination_dir, - entry.path.as_slice(), - ); + #[cfg(windows)] + let result = { + let destination_dir = &state.subdir; + walk_install_dir_windows( + destination_dir, + state.walker.as_mut().unwrap(), + state.to_copy_buf_off, + &mut state.buf[..], + state.to_copy_buf2_off, + &mut state.buf2[..], + |dest, src, entry_path| match sys::symlink_w(dest, src, Default::default()) { + Err(err) => { + if let Some(entry_dirname) = bun_paths::Dirname::dirname_u16(entry_path) { + let _ = + bun_sys::MakePath::make_path_u16(destination_dir, entry_dirname); + if sys::symlink_w(dest, src, Default::default()).is_ok() { + return Ok(()); } } - EntryKind::File => match sys::symlink_w(dest, src, Default::default()) { - Err(err) => { - if let Some(entry_dirname) = - bun_paths::Dirname::dirname_u16(entry.path.as_slice()) - { - let _ = bun_sys::MakePath::make_path_u16( - destination_dir, - entry_dirname, - ); - if sys::symlink_w(dest, src, Default::default()).is_ok() { - continue; - } - } - if PackageManager::verbose_install() { - bun_core::run_once! {{ - bun_core::warn!( - "CreateHardLinkW failed, falling back to CopyFileW: {} -> {}\n", - bun_core::fmt::fmt_os_path(src.as_slice(), Default::default()), - bun_core::fmt::fmt_os_path(dest.as_slice(), Default::default()), - ); - }} - } + if PackageManager::verbose_install() { + bun_core::run_once! {{ + bun_core::warn!( + "CreateHardLinkW failed, falling back to CopyFileW: {} -> {}\n", + bun_core::fmt::fmt_os_path(src.as_slice(), Default::default()), + bun_core::fmt::fmt_os_path(dest.as_slice(), Default::default()), + ); + }} + } - return Err(err.into()); - } - Ok(_) => {} - }, - _ => unreachable!(), // handled above + Err(err.into()) } - } - } - - Ok(real_file_count) - } - - #[cfg(windows)] - let result = copy( - &state.subdir, - state.walker.as_mut().unwrap(), - state.to_copy_buf_off, - &mut state.buf[..], - state.to_copy_buf2_off, - &mut state.buf2[..], - ); + Ok(_) => Ok(()), + }, + ) + }; #[cfg(not(windows))] let result = copy( &state.subdir, state.walker.as_mut().unwrap(), - (), - (), to_copy_buf2_offset, &mut buf2[..], ); @@ -2557,3 +2407,66 @@ impl<'a> PackageInstall<'a> { } type Walker = walker_skippable::Walker; + +/// Shared Windows directory walk for the copyfile/symlink install backends: +/// builds NUL-terminated wide dest/src paths for each entry, creates +/// directories (`CreateDirectoryExW` with a `make_path` fallback), and calls +/// `per_file(dest, src, entry_path)` for each file. +/// +/// Two overlapping slices into the same buffer (`head` is the whole buffer, +/// `to_copy_into` is its tail) would be two live aliasing `&mut [u16]`, which +/// is UB — pass head buffer + tail offset and reslice inside. +#[cfg(windows)] +fn walk_install_dir_windows( + destination_dir: &Dir, + walker: &mut Walker, + to_copy_into1_offset: usize, + head1: &mut [u16], + to_copy_into2_offset: usize, + head2: &mut [u16], + mut per_file: impl FnMut(&bun_core::WStr, &bun_core::WStr, &[u16]) -> crate::Result<()>, +) -> crate::Result { + use bun_sys::windows; + + while let Some(entry) = walker.next()? { + match entry.kind { + EntryKind::Directory | EntryKind::File => {} + _ => continue, + } + + if entry.path.len() > head1.len() - to_copy_into1_offset + || entry.path.len() > head2.len() - to_copy_into2_offset + { + return Err(crate::Error::Sys(bun_errno::SystemErrno::ENAMETOOLONG)); + } + + let dest_len = to_copy_into1_offset + entry.path.len(); + head1[to_copy_into1_offset..dest_len].copy_from_slice(entry.path.as_slice()); + head1[dest_len] = 0; + let dest = bun_core::WStr::from_buf(head1, dest_len); + + let src_len = to_copy_into2_offset + entry.path.len(); + head2[to_copy_into2_offset..src_len].copy_from_slice(entry.path.as_slice()); + head2[src_len] = 0; + let src = bun_core::WStr::from_buf(head2, src_len); + + match entry.kind { + EntryKind::Directory => { + // SAFETY: FFI — src/dest are valid NUL-terminated WStr buffers built + // into head1/head2 above. + if unsafe { + windows::CreateDirectoryExW(src.as_ptr(), dest.as_ptr(), core::ptr::null_mut()) + } == 0 + { + let _ = + bun_sys::MakePath::make_path_u16(destination_dir, entry.path.as_slice()); + } + } + EntryKind::File => per_file(dest, src, entry.path.as_slice())?, + _ => unreachable!(), // handled above + } + } + + // Windows installs don't track a real file count. + Ok(0) +} diff --git a/src/install/PackageInstaller.rs b/src/install/PackageInstaller.rs index ccc1b663f85f..a01d6e203ca4 100644 --- a/src/install/PackageInstaller.rs +++ b/src/install/PackageInstaller.rs @@ -367,6 +367,16 @@ pub(crate) fn alias_is_safe_install_target(alias: &[u8]) -> bool { component_count == 1 || (component_count == 2 && alias[0] == b'@') } +/// Where to record a newly trusted dependency once its lifecycle scripts are +/// enqueued. +#[derive(Clone, Copy)] +struct TrustedDepRecord { + /// Add the alias to `trusted_deps_to_add_to_package_json`. + package_json: bool, + /// Add the alias to the lockfile's `trusted_dependencies`. + lockfile: bool, +} + impl<'a> PackageInstaller<'a> { // ────────────────────────────────────────────────────────────────────── // BACKREF accessors @@ -1840,74 +1850,20 @@ impl<'a> PackageInstaller<'a> { if resolution.tag != resolution::Tag::Root && (resolution.tag == resolution::Tag::Workspace || is_trusted) { - let mut folder_path = - AutoAbsPath::from(self.node_modules.path.as_slice()).unwrap_or_oom(); - // `defer folder_path.deinit()` — AbsPath impls Drop. - folder_path - .append(alias.slice(string_buf!())) - .unwrap_or_oom(); - - 'enqueue_lifecycle_scripts: { - if self - .manager() - .postinstall_optimizer - .should_ignore_lifecycle_scripts( - &postinstall_optimizer::PkgInfo { - name_hash: pkg_name_hash, - version: if resolution.tag == resolution::Tag::Npm { - Some(resolution.npm().version) - } else { - None - }, - version_buf: string_buf!(), - }, - self.lockfile().packages.items_resolutions() - [package_id as usize] - .get(self.lockfile().buffers.resolutions.as_slice()), - self.lockfile().packages.items_meta(), - self.manager().options.cpu, - self.manager().options.os, - Some(self.current_tree_id), - ) - { - if PackageManager::verbose_install() { - bun_core::pretty_errorln!( - "[Lifecycle Scripts] ignoring {} lifecycle scripts", - bstr::BStr::new(pkg_name.slice(string_buf!())), - ); - } - break 'enqueue_lifecycle_scripts; - } - - if self.enqueue_lifecycle_scripts( - alias.slice(string_buf!()), - log_level, - &mut folder_path, - package_id, - dep_behavior.contains(crate::dependency::Behavior::OPTIONAL), - resolution, - ) { - if is_trusted_through_update_request { - self.manager_mut() - .trusted_deps_to_add_to_package_json - .push(Box::<[u8]>::from(alias.slice(string_buf!()))); - - if self.lockfile().trusted_dependencies.is_none() { - self.lockfile_mut().trusted_dependencies = - Some(Default::default()); - } - self.lockfile_mut() - .trusted_dependencies - .as_mut() - .unwrap() - .put( - truncated_dep_name_hash, - Box::<[u8]>::from(alias.slice(string_buf!())), - ) - .unwrap_or_oom(); - } - } - } + self.enqueue_lifecycle_scripts_for_trusted( + log_level, + package_id, + pkg_name, + pkg_name_hash, + alias, + truncated_dep_name_hash, + dep_behavior.contains(crate::dependency::Behavior::OPTIONAL), + resolution, + TrustedDepRecord { + package_json: is_trusted_through_update_request, + lockfile: is_trusted_through_update_request, + }, + ); } match resolution.tag { @@ -2152,73 +2108,20 @@ impl<'a> PackageInstaller<'a> { }; if resolution.tag != resolution::Tag::Root && is_trusted { - let mut folder_path = - AutoAbsPath::from(self.node_modules.path.as_slice()).unwrap_or_oom(); - folder_path - .append(alias.slice(string_buf!())) - .unwrap_or_oom(); - - 'enqueue_lifecycle_scripts: { - if self - .manager() - .postinstall_optimizer - .should_ignore_lifecycle_scripts( - &postinstall_optimizer::PkgInfo { - name_hash: pkg_name_hash, - version: if resolution.tag == resolution::Tag::Npm { - Some(resolution.npm().version) - } else { - None - }, - version_buf: string_buf!(), - }, - self.lockfile().packages.items_resolutions()[package_id as usize] - .get(self.lockfile().buffers.resolutions.as_slice()), - self.lockfile().packages.items_meta(), - self.manager().options.cpu, - self.manager().options.os, - Some(self.current_tree_id), - ) - { - if PackageManager::verbose_install() { - bun_core::pretty_errorln!( - "[Lifecycle Scripts] ignoring {} lifecycle scripts", - bstr::BStr::new(pkg_name.slice(string_buf!())), - ); - } - break 'enqueue_lifecycle_scripts; - } - - if self.enqueue_lifecycle_scripts( - alias.slice(string_buf!()), - log_level, - &mut folder_path, - package_id, - dep_behavior.contains(crate::dependency::Behavior::OPTIONAL), - resolution, - ) { - if is_trusted_through_update_request { - self.manager_mut() - .trusted_deps_to_add_to_package_json - .push(Box::<[u8]>::from(alias.slice(string_buf!()))); - } - - if add_to_lockfile { - if self.lockfile().trusted_dependencies.is_none() { - self.lockfile_mut().trusted_dependencies = Some(Default::default()); - } - self.lockfile_mut() - .trusted_dependencies - .as_mut() - .unwrap() - .put( - truncated_dep_name_hash, - Box::<[u8]>::from(alias.slice(string_buf!())), - ) - .unwrap_or_oom(); - } - } - } + self.enqueue_lifecycle_scripts_for_trusted( + log_level, + package_id, + pkg_name, + pkg_name_hash, + alias, + truncated_dep_name_hash, + dep_behavior.contains(crate::dependency::Behavior::OPTIONAL), + resolution, + TrustedDepRecord { + package_json: is_trusted_through_update_request, + lockfile: add_to_lockfile, + }, + ); } // `destination_dir` is `LazyPackageDestinationDir::NodeModulesPath` @@ -2248,6 +2151,98 @@ impl<'a> PackageInstaller<'a> { ); } + /// Enqueue lifecycle scripts for a trusted (or workspace) dependency, then + /// record it in `trusted_deps_to_add_to_package_json` and/or the lockfile's + /// `trusted_dependencies` as requested. + fn enqueue_lifecycle_scripts_for_trusted( + &mut self, + log_level: Options::LogLevel, + package_id: PackageID, + pkg_name: String, + pkg_name_hash: PackageNameHash, + alias: String, + truncated_dep_name_hash: TruncatedPackageNameHash, + optional: bool, + resolution: &Resolution, + record: TrustedDepRecord, + ) { + // SAFETY: `buffers.string_bytes` is append-only and never freed + // for the lifetime of this `PackageInstaller`. + let string_buf_ptr = + bun_ptr::RawSlice::new(self.lockfile().buffers.string_bytes.as_slice()); + macro_rules! string_buf { + () => { + string_buf_ptr.slice() + }; + } + + let mut folder_path = AutoAbsPath::from(self.node_modules.path.as_slice()).unwrap_or_oom(); + // `defer folder_path.deinit()` — AbsPath impls Drop. + folder_path + .append(alias.slice(string_buf!())) + .unwrap_or_oom(); + + if self + .manager() + .postinstall_optimizer + .should_ignore_lifecycle_scripts( + &postinstall_optimizer::PkgInfo { + name_hash: pkg_name_hash, + version: if resolution.tag == resolution::Tag::Npm { + Some(resolution.npm().version) + } else { + None + }, + version_buf: string_buf!(), + }, + self.lockfile().packages.items_resolutions()[package_id as usize] + .get(self.lockfile().buffers.resolutions.as_slice()), + self.lockfile().packages.items_meta(), + self.manager().options.cpu, + self.manager().options.os, + Some(self.current_tree_id), + ) + { + if PackageManager::verbose_install() { + bun_core::pretty_errorln!( + "[Lifecycle Scripts] ignoring {} lifecycle scripts", + bstr::BStr::new(pkg_name.slice(string_buf!())), + ); + } + return; + } + + if self.enqueue_lifecycle_scripts( + alias.slice(string_buf!()), + log_level, + &mut folder_path, + package_id, + optional, + resolution, + ) { + if record.package_json { + self.manager_mut() + .trusted_deps_to_add_to_package_json + .push(Box::<[u8]>::from(alias.slice(string_buf!()))); + } + + if record.lockfile { + if self.lockfile().trusted_dependencies.is_none() { + self.lockfile_mut().trusted_dependencies = Some(Default::default()); + } + self.lockfile_mut() + .trusted_dependencies + .as_mut() + .unwrap() + .put( + truncated_dep_name_hash, + Box::<[u8]>::from(alias.slice(string_buf!())), + ) + .unwrap_or_oom(); + } + } + } + /// returns true if scripts are enqueued fn enqueue_lifecycle_scripts( &mut self, diff --git a/src/install/PackageManager.rs b/src/install/PackageManager.rs index 929dd885bc4b..4dc657df97b5 100644 --- a/src/install/PackageManager.rs +++ b/src/install/PackageManager.rs @@ -1412,6 +1412,129 @@ pub fn get() -> *mut PackageManager { // init // ────────────────────────────────────────────────────────────────────────── +/// Placement-writes one field of the `PackageManager` singleton through the +/// raw pointer `$p` — see the PERF NOTE in [`init`] for why the struct must +/// not be built by value. +macro_rules! wr { + ($p:ident, $field:ident, $val:expr) => { + core::ptr::addr_of_mut!((*$p).$field).write($val) + }; +} + +/// Writes the `PackageManager` fields that take identical default values in +/// both init paths ([`init`] and [`init_with_runtime_once`]). Each caller +/// writes the remaining (divergent) fields itself; together they must fully +/// initialize the singleton. +/// +/// Uses per-field placement writes — see the PERF NOTE in [`init`] for why +/// the struct must not be built by value. +/// +/// # Safety +/// `p` must point to the allocated (possibly uninitialized) singleton from +/// `allocate_package_manager()`, with no other references to it live. +unsafe fn write_shared_default_fields(p: *mut PackageManager) { + // SAFETY: caller guarantees `p` is valid for per-field placement writes. + unsafe { + // The two large pools: in-place init that only zeros the 256 B + // occupancy bitset and leaves `[MaybeUninit; N]` untouched — no + // stack temporary, no memcpy. + PreallocatedNetworkTasks::init_in_place(core::ptr::addr_of_mut!( + (*p).preallocated_network_tasks + )); + PreallocatedTaskStore::init_in_place(core::ptr::addr_of_mut!( + (*p).preallocated_resolve_tasks + )); + + wr!(p, cache_directory_, None); + wr!(p, cache_directory_path, ZBox::from_bytes(b"")); + wr!( + p, + active_lifecycle_scripts, + crate::lifecycle_script_runner::List { + root: core::ptr::null_mut(), + // `lifecycle_script_runner::List`'s heap comparator never + // dereferences its context arg, so it is modeled as a ZST + // (`StartedAtCtx`) instead of threading a back-pointer. + context: crate::lifecycle_script_runner::StartedAtCtx, + } + ); + wr!(p, network_task_fifo, NetworkQueue::init()); + wr!(p, patch_task_fifo, PatchTaskFifo::init()); + wr!(p, ast_arena, bun_alloc::Arena::new()); + wr!(p, resolve_tasks, ResolveTaskQueue::default()); + // `Lockfile` contains `HashMap`/`Vec`/`NonNull` fields, so a + // zero-bit pattern is UB; allocate the real (empty) lockfile here directly. + // `Lockfile::default()` ≡ `Lockfile::init_empty()`. + wr!(p, lockfile, Box::new(Lockfile::default())); + wr!(p, timestamp_for_manifest_cache_control, 0); + wr!(p, extracted_count, 0); + wr!(p, default_features, Features::default()); + wr!(p, summary, Default::default()); + wr!(p, progress, Progress::default()); + wr!(p, downloads_node, None); + wr!(p, scripts_node, None); + wr!(p, progress_name_buf, [0; 768]); + wr!(p, progress_name_buf_dynamic, Vec::new()); + wr!(p, track_installed_bin, TrackInstalledBin::None); + wr!(p, root_progress_node, core::ptr::null_mut()); + wr!(p, to_update, false); + wr!(p, update_requests, Box::default()); + wr!(p, root_package_id, RootPackageId::default()); + wr!(p, task_batch, thread_pool::Batch::default()); + wr!(p, task_queue, TaskDependencyQueue::default()); + wr!(p, manifests, PackageManifestMap::default()); + wr!(p, folders, Default::default()); + wr!(p, git_repositories, RepositoryMap::default()); + wr!(p, network_dedupe_map, Default::default()); + wr!( + p, + async_network_task_queue, + AsyncNetworkTaskQueue::default() + ); + wr!(p, network_tarball_batch, thread_pool::Batch::default()); + wr!(p, network_resolve_batch, thread_pool::Batch::default()); + wr!(p, patch_apply_batch, thread_pool::Batch::default()); + wr!(p, patch_calc_hash_batch, thread_pool::Batch::default()); + wr!(p, patch_task_queue, PatchTaskQueue::default()); + wr!(p, pending_pre_calc_hashes, AtomicU32::new(0)); + wr!(p, pending_tasks, AtomicU32::new(0)); + wr!(p, total_tasks, 0); + wr!( + p, + lifecycle_script_time_log, + LifecycleScriptTimeLog::default() + ); + wr!(p, pending_lifecycle_script_tasks, AtomicU32::new(0)); + wr!(p, finished_installing, AtomicBool::new(false)); + wr!(p, total_scripts, 0); + wr!(p, root_lifecycle_scripts, None); + wr!(p, node_gyp_tempdir_name, Box::default()); + wr!(p, preinstall_state, Vec::new()); + wr!(p, postinstall_optimizer, Default::default()); + wr!(p, global_link_dir, None); + wr!(p, global_dir, None); + wr!(p, global_link_dir_path, Box::default()); + wr!(p, on_wake, WakeHandler::default()); + wr!( + p, + ci_mode, + LazyBool::new(PackageManager::compute_is_continuous_integration) + ); + wr!( + p, + peer_dependencies, + LinearFifo::>::init() + ); + wr!(p, known_npm_aliases, NpmAliasMap::default()); + wr!(p, trusted_deps_to_add_to_package_json, Vec::new()); + wr!(p, any_failed_to_install, false); + wr!(p, updating_packages, StringArrayHashMap::default()); + wr!(p, patched_dependencies_to_remove, ArrayHashMap::default()); + wr!(p, last_reported_slow_lifecycle_script_at, 0); + wr!(p, cached_tick_for_slow_lifecycle_script_logging, 0); + } +} + /// Returns `&'static mut PackageManager` — the process-singleton (held in /// `holder::RAW_PTR`) is leaked for the process lifetime and `init()` is called /// exactly once on the single CLI dispatch thread. Every @@ -1924,129 +2047,45 @@ pub fn init( // directly to the heap and keeps the frame under 16 KB. unsafe { let p = manager_ptr; - macro_rules! wr { - ($field:ident, $val:expr) => { - core::ptr::addr_of_mut!((*p).$field).write($val) - }; - } - // The two large pools: in-place init that only zeros the 256 B - // occupancy bitset and leaves `[MaybeUninit; N]` untouched — no - // stack temporary, no memcpy. - PreallocatedNetworkTasks::init_in_place(core::ptr::addr_of_mut!( - (*p).preallocated_network_tasks - )); - PreallocatedTaskStore::init_in_place(core::ptr::addr_of_mut!( - (*p).preallocated_resolve_tasks - )); + write_shared_default_fields(p); - wr!(cache_directory_, None); - wr!(cache_directory_path, ZBox::from_bytes(b"")); - wr!(options, options); - wr!( - active_lifecycle_scripts, - crate::lifecycle_script_runner::List { - root: core::ptr::null_mut(), - // `lifecycle_script_runner::List`'s heap comparator never - // dereferences its context arg, so it is modeled as a ZST - // (`StartedAtCtx`) instead of threading a back-pointer. - context: crate::lifecycle_script_runner::StartedAtCtx, - } - ); - wr!(network_task_fifo, NetworkQueue::init()); - wr!(patch_task_fifo, PatchTaskFifo::init()); - wr!(log, ctx.log); - wr!(root_dir, entries_option); - wr!(ast_arena, bun_alloc::Arena::new()); + wr!(p, options, options); + wr!(p, log, ctx.log); + wr!(p, root_dir, entries_option); // reborrow `&mut *env` so the local stays usable for // the post-construction `BUN_MANIFEST_CACHE` / `options.load` // reads. `BackRef` stores a raw pointer — // ending the reborrow here does not alias the later uses. - wr!(env, Some(bun_ptr::BackRef::new_mut(&mut *env))); - wr!(cpu_count, cpu_count); + wr!(p, env, Some(bun_ptr::BackRef::new_mut(&mut *env))); + wr!(p, cpu_count, cpu_count); wr!( + p, thread_pool, ThreadPool::init(thread_pool::Config { max_threads: cpu_count, ..Default::default() }) ); - wr!(resolve_tasks, ResolveTaskQueue::default()); - // `Lockfile` contains `HashMap`/`Vec`/`NonNull` fields, so a - // zero-bit pattern is UB; allocate the real (empty) lockfile here directly. - // `Lockfile::default()` ≡ `Lockfile::init_empty()`. - wr!(lockfile, Box::new(Lockfile::default())); - wr!(root_package_json_file, root_package_json_file); + wr!(p, root_package_json_file, root_package_json_file); // .progress - wr!(event_loop, AnyEventLoop::init()); + wr!(p, event_loop, AnyEventLoop::init()); wr!( + p, original_package_json_path, ZBox::from_vec_with_nul(original_package_json_path_buf) ); - wr!(workspace_package_json_cache, workspace_package_json_cache); - wr!(workspace_name_hash, workspace_name_hash); - wr!(subcommand, subcommand); wr!( - root_package_json_name_at_time_of_init, - root_package_json_name_at_time_of_init - ); - - // remaining defaults: - wr!(timestamp_for_manifest_cache_control, 0); - wr!(extracted_count, 0); - wr!(default_features, Features::default()); - wr!(summary, Default::default()); - wr!(progress, Progress::default()); - wr!(downloads_node, None); - wr!(scripts_node, None); - wr!(progress_name_buf, [0; 768]); - wr!(progress_name_buf_dynamic, Vec::new()); - wr!(track_installed_bin, TrackInstalledBin::None); - wr!(root_progress_node, core::ptr::null_mut()); - wr!(to_update, false); - wr!(update_requests, Box::default()); - wr!(root_package_id, RootPackageId::default()); - wr!(task_batch, thread_pool::Batch::default()); - wr!(task_queue, TaskDependencyQueue::default()); - wr!(manifests, PackageManifestMap::default()); - wr!(folders, Default::default()); - wr!(git_repositories, RepositoryMap::default()); - wr!(network_dedupe_map, Default::default()); - wr!(async_network_task_queue, AsyncNetworkTaskQueue::default()); - wr!(network_tarball_batch, thread_pool::Batch::default()); - wr!(network_resolve_batch, thread_pool::Batch::default()); - wr!(patch_apply_batch, thread_pool::Batch::default()); - wr!(patch_calc_hash_batch, thread_pool::Batch::default()); - wr!(patch_task_queue, PatchTaskQueue::default()); - wr!(pending_pre_calc_hashes, AtomicU32::new(0)); - wr!(pending_tasks, AtomicU32::new(0)); - wr!(total_tasks, 0); - wr!(lifecycle_script_time_log, LifecycleScriptTimeLog::default()); - wr!(pending_lifecycle_script_tasks, AtomicU32::new(0)); - wr!(finished_installing, AtomicBool::new(false)); - wr!(total_scripts, 0); - wr!(root_lifecycle_scripts, None); - wr!(node_gyp_tempdir_name, Box::default()); - wr!(preinstall_state, Vec::new()); - wr!(postinstall_optimizer, Default::default()); - wr!(global_link_dir, None); - wr!(global_dir, None); - wr!(global_link_dir_path, Box::default()); - wr!(on_wake, WakeHandler::default()); - wr!( - ci_mode, - LazyBool::new(PackageManager::compute_is_continuous_integration) + p, + workspace_package_json_cache, + workspace_package_json_cache ); + wr!(p, workspace_name_hash, workspace_name_hash); + wr!(p, subcommand, subcommand); wr!( - peer_dependencies, - LinearFifo::>::init() + p, + root_package_json_name_at_time_of_init, + root_package_json_name_at_time_of_init ); - wr!(known_npm_aliases, NpmAliasMap::default()); - wr!(trusted_deps_to_add_to_package_json, Vec::new()); - wr!(any_failed_to_install, false); - wr!(updating_packages, StringArrayHashMap::default()); - wr!(patched_dependencies_to_remove, ArrayHashMap::default()); - wr!(last_reported_slow_lifecycle_script_at, 0); - wr!(cached_tick_for_slow_lifecycle_script_logging, 0); } holder::INITIALIZED.store(true, core::sync::atomic::Ordering::Release); // The per-field placement above fully initialized the singleton; the @@ -2358,23 +2397,10 @@ pub(crate) fn init_with_runtime_once( // directly to the heap singleton. unsafe { let p = manager_ptr; - macro_rules! wr { - ($field:ident, $val:expr) => { - core::ptr::addr_of_mut!((*p).$field).write($val) - }; - } - // The two large pools: in-place init that only zeros the 256 B - // occupancy bitset and leaves `[MaybeUninit; N]` untouched. - PreallocatedNetworkTasks::init_in_place(core::ptr::addr_of_mut!( - (*p).preallocated_network_tasks - )); - PreallocatedTaskStore::init_in_place(core::ptr::addr_of_mut!( - (*p).preallocated_resolve_tasks - )); + write_shared_default_fields(p); - wr!(cache_directory_, None); - wr!(cache_directory_path, ZBox::from_bytes(b"")); wr!( + p, options, Options { max_concurrent_lifecycle_scripts: cli @@ -2383,114 +2409,46 @@ pub(crate) fn init_with_runtime_once( ..Default::default() } ); - wr!( - active_lifecycle_scripts, - crate::lifecycle_script_runner::List { - root: core::ptr::null_mut(), - context: crate::lifecycle_script_runner::StartedAtCtx, - } - ); - wr!(network_task_fifo, NetworkQueue::init()); - wr!(log, std::ptr::from_mut(log)); - wr!(root_dir, root_dir); - wr!(ast_arena, bun_alloc::Arena::new()); + wr!(p, log, std::ptr::from_mut(log)); + wr!(p, root_dir, root_dir); // reborrow `&mut *env` so the local stays usable for // the post-construction `BUN_MANIFEST_CACHE` / `options.load` // reads. `BackRef` stores a raw pointer — // ending the reborrow here does not alias the later uses. - wr!(env, Some(bun_ptr::BackRef::new_mut(&mut *env))); - wr!(cpu_count, cpu_count); + wr!(p, env, Some(bun_ptr::BackRef::new_mut(&mut *env))); + wr!(p, cpu_count, cpu_count); wr!( + p, thread_pool, ThreadPool::init(thread_pool::Config { max_threads: cpu_count, ..Default::default() }) ); - // `Lockfile` holds `HashMap`/`Vec`/`NonNull` (zero-bit pattern is - // UB), so allocate the real empty lockfile here directly instead of a zeroed placeholder. - wr!(lockfile, Box::new(Lockfile::default())); // `.root_package_json_file` is never read in the runtime // path. Use the explicit invalid-fd sentinel rather than `mem::zeroed()` — // on posix `Fd(0)` is stdin, not the invalid marker. wr!( + p, root_package_json_file, bun_sys::File::from_fd(Fd::invalid()) ); // erased *mut () set by tier-6; `js_current()` resolves the per-thread JS // event loop via `bun_io::__bun_get_vm_ctx` (link-time, definer in bun_runtime). - wr!(event_loop, AnyEventLoop::js_current()); + wr!(p, event_loop, AnyEventLoop::js_current()); wr!( + p, original_package_json_path, ZBox::from_vec_with_nul(original_package_json_path) ); - wr!(subcommand, Subcommand::Install); - - // remaining defaults: - wr!(resolve_tasks, ResolveTaskQueue::default()); - wr!(timestamp_for_manifest_cache_control, 0); - wr!(extracted_count, 0); - wr!(default_features, Features::default()); - wr!(summary, Default::default()); - wr!(progress, Progress::default()); - wr!(downloads_node, None); - wr!(scripts_node, None); - wr!(progress_name_buf, [0; 768]); - wr!(progress_name_buf_dynamic, Vec::new()); - wr!(track_installed_bin, TrackInstalledBin::None); - wr!(root_progress_node, core::ptr::null_mut()); - wr!(to_update, false); - wr!(update_requests, Box::default()); - wr!(root_package_json_name_at_time_of_init, Box::default()); - wr!(root_package_id, RootPackageId::default()); - wr!(task_batch, thread_pool::Batch::default()); - wr!(task_queue, TaskDependencyQueue::default()); - wr!(manifests, PackageManifestMap::default()); - wr!(folders, Default::default()); - wr!(git_repositories, RepositoryMap::default()); - wr!(network_dedupe_map, Default::default()); - wr!(async_network_task_queue, AsyncNetworkTaskQueue::default()); - wr!(network_tarball_batch, thread_pool::Batch::default()); - wr!(network_resolve_batch, thread_pool::Batch::default()); - wr!(patch_apply_batch, thread_pool::Batch::default()); - wr!(patch_calc_hash_batch, thread_pool::Batch::default()); - wr!(patch_task_fifo, PatchTaskFifo::init()); - wr!(patch_task_queue, PatchTaskQueue::default()); - wr!(pending_pre_calc_hashes, AtomicU32::new(0)); - wr!(pending_tasks, AtomicU32::new(0)); - wr!(total_tasks, 0); - wr!(lifecycle_script_time_log, LifecycleScriptTimeLog::default()); - wr!(pending_lifecycle_script_tasks, AtomicU32::new(0)); - wr!(finished_installing, AtomicBool::new(false)); - wr!(total_scripts, 0); - wr!(root_lifecycle_scripts, None); - wr!(node_gyp_tempdir_name, Box::default()); - wr!(preinstall_state, Vec::new()); - wr!(postinstall_optimizer, Default::default()); - wr!(global_link_dir, None); - wr!(global_dir, None); - wr!(global_link_dir_path, Box::default()); - wr!(on_wake, WakeHandler::default()); - wr!( - ci_mode, - LazyBool::new(PackageManager::compute_is_continuous_integration) - ); - wr!( - peer_dependencies, - LinearFifo::>::init() - ); - wr!(known_npm_aliases, NpmAliasMap::default()); - wr!(trusted_deps_to_add_to_package_json, Vec::new()); - wr!(any_failed_to_install, false); - wr!(workspace_name_hash, None); + wr!(p, subcommand, Subcommand::Install); + wr!(p, root_package_json_name_at_time_of_init, Box::default()); + wr!(p, workspace_name_hash, None); wr!( + p, workspace_package_json_cache, WorkspacePackageJSONCache::default() ); - wr!(updating_packages, StringArrayHashMap::default()); - wr!(patched_dependencies_to_remove, ArrayHashMap::default()); - wr!(last_reported_slow_lifecycle_script_at, 0); - wr!(cached_tick_for_slow_lifecycle_script_logging, 0); } holder::INITIALIZED.store(true, core::sync::atomic::Ordering::Release); // SAFETY: per-field placement above fully initialized the PackageManager; diff --git a/src/install/PackageManager/PackageJSONEditor.rs b/src/install/PackageManager/PackageJSONEditor.rs index 9ed87945fee7..29a664d7ed7c 100644 --- a/src/install/PackageManager/PackageJSONEditor.rs +++ b/src/install/PackageManager/PackageJSONEditor.rs @@ -39,6 +39,46 @@ fn arena_dup<'a>(arena: &'a bun_alloc::Arena, bytes: &[u8]) -> &'a [u8] { arena.alloc_slice_copy(bytes) } +/// Builds the replacement version string for an updated npm dependency, +/// preserving the original pin style (`1.2.3` / `~1.2.3` / `^1.2.3`) and, for +/// aliases, the `npm:@scope/pkg@` prefix from `dep_literal`. +fn replacement_version_literal( + version_fmt: impl std::fmt::Display, + original_version_literal: &[u8], + is_alias: bool, + dep_literal: &[u8], + exact_versions: bool, +) -> Vec { + let mut v = Vec::new(); + if is_alias { + // negative because the real package might have a scope + // e.g. "dep": "npm:@foo/bar@1.2.3" + if let Some(at_index) = strings::last_index_of_char(dep_literal, b'@') { + write!(&mut v, "{}@", bstr::BStr::new(&dep_literal[0..at_index])) + .expect("infallible: in-memory write"); + } + } + let pin_prefix = if exact_versions { + "" + } else { + let version_literal = if is_alias { + match strings::last_index_of_char(original_version_literal, b'@') { + Some(at_index) => &original_version_literal[at_index + 1..], + None => original_version_literal, + } + } else { + original_version_literal + }; + match semver::Version::which_version_is_pinned(version_literal) { + semver::PinnedVersion::Patch => "", + semver::PinnedVersion::Minor => "~", + semver::PinnedVersion::Major => "^", + } + }; + write!(&mut v, "{}{}", pin_prefix, version_fmt).expect("infallible: in-memory write"); + v +} + /// Shallow-copy a `G::Property` for the JSON-editing path. Only `key`/`value` /// (both `Option`, `Copy`) are populated by the JSON parser; the rest /// (`ts_decorators`, `class_static_block`, …) are always default for parsed @@ -435,82 +475,14 @@ pub(crate) fn edit_update_no_args( } } - let new_version: Vec = 'new_version: { - // `resolution.tag == Npm` checked above. - let version_fmt = resolution.npm().version.fmt(string_buf); - if options.exact_versions { - let mut v = Vec::new(); - write!(&mut v, "{}", version_fmt) - .expect("infallible: in-memory write"); - break 'new_version v; - } - - let version_literal: &[u8] = 'version_literal: { - if !is_alias { - break 'version_literal &entry - .value - .original_version_literal; - } - if let Some(at_index) = strings::last_index_of_char( - &entry.value.original_version_literal, - b'@', - ) { - break 'version_literal &entry - .value - .original_version_literal[at_index + 1..]; - } - &entry.value.original_version_literal - }; - - let pinned_version = - semver::Version::which_version_is_pinned( - version_literal, - ); - let mut v = Vec::new(); - match pinned_version { - semver::PinnedVersion::Patch => { - write!(&mut v, "{}", version_fmt) - .expect("infallible: in-memory write") - } - semver::PinnedVersion::Minor => { - write!(&mut v, "~{}", version_fmt) - .expect("infallible: in-memory write") - } - semver::PinnedVersion::Major => { - write!(&mut v, "^{}", version_fmt) - .expect("infallible: in-memory write") - } - } - v - }; - - if is_alias { - let dep_literal = - workspace_dep.version.literal.slice(string_buf); - - // negative because the real package might have a scope - // e.g. "dep": "npm:@foo/bar@1.2.3" - if let Some(at_index) = - strings::last_index_of_char(dep_literal, b'@') - { - let mut v = Vec::new(); - write!( - &mut v, - "{}@{}", - bstr::BStr::new(&dep_literal[0..at_index]), - bstr::BStr::new(&new_version) - ) - .unwrap(); - dep.value = Some(Expr::allocate( - arena, - E::EString::init(arena_str(arena, &v)), - bun_ast::Loc::EMPTY, - )); - break 'updated; - } - - // fallthrough and replace entire version. - } + // `resolution.tag == Npm` checked above. + let new_version = replacement_version_literal( + resolution.npm().version.fmt(string_buf), + &entry.value.original_version_literal, + is_alias, + workspace_dep.version.literal.slice(string_buf), + options.exact_versions, + ); dep.value = Some(Expr::allocate( arena, @@ -1132,73 +1104,16 @@ pub(crate) fn edit( if let Some(entry) = manager.updating_packages.fetch_swap_remove(request.name) { - let new_version: Vec = 'new_version: { - let version_fmt = resolutions[request.package_id as usize] + let new_version = replacement_version_literal( + resolutions[request.package_id as usize] .npm() .version - .fmt(manager.lockfile.buffers.string_bytes.as_slice()); - if options.exact_versions { - let mut v = Vec::new(); - write!(&mut v, "{}", version_fmt) - .expect("infallible: in-memory write"); - break 'new_version v; - } - - let version_literal: &[u8] = 'version_literal: { - if !entry.value.is_alias { - break 'version_literal &entry - .value - .original_version_literal; - } - if let Some(at_index) = strings::last_index_of_char( - &entry.value.original_version_literal, - b'@', - ) { - break 'version_literal &entry - .value - .original_version_literal[at_index + 1..]; - } - - &entry.value.original_version_literal - }; - - let pinned_version = - semver::Version::which_version_is_pinned(version_literal); - let mut v = Vec::new(); - match pinned_version { - semver::PinnedVersion::Patch => { - write!(&mut v, "{}", version_fmt) - .expect("infallible: in-memory write") - } - semver::PinnedVersion::Minor => { - write!(&mut v, "~{}", version_fmt) - .expect("infallible: in-memory write") - } - semver::PinnedVersion::Major => { - write!(&mut v, "^{}", version_fmt) - .expect("infallible: in-memory write") - } - } - v - }; - - if entry.value.is_alias { - let dep_literal = &entry.value.original_version_literal; - - if let Some(at_index) = - strings::last_index_of_char(dep_literal, b'@') - { - let mut v = Vec::new(); - write!( - &mut v, - "{}@{}", - bstr::BStr::new(&dep_literal[0..at_index]), - bstr::BStr::new(&new_version) - ) - .unwrap(); - break 'npm arena_str(arena, &v); - } - } + .fmt(manager.lockfile.buffers.string_bytes.as_slice()), + &entry.value.original_version_literal, + entry.value.is_alias, + &entry.value.original_version_literal, + options.exact_versions, + ); break 'npm arena_str(arena, &new_version); } diff --git a/src/install/PackageManager/PackageManagerEnqueue.rs b/src/install/PackageManager/PackageManagerEnqueue.rs index 8316c27b7c1a..0f9f5ff5fe49 100644 --- a/src/install/PackageManager/PackageManagerEnqueue.rs +++ b/src/install/PackageManager/PackageManagerEnqueue.rs @@ -645,6 +645,36 @@ pub unsafe fn enqueue_patch_task_pre(this: &mut PackageManager, task: *mut Patch let _ = this.pending_pre_calc_hashes.fetch_add(1, Ordering::Relaxed); } +/// Returns the task-callback list for `task_id`, creating and initializing it +/// if this is the first callback registered for the task. +fn task_callback_list<'a>( + this: &'a mut PackageManager, + task_id: Task::Id, +) -> crate::Result<&'a mut TaskCallbackList> { + let entry = this.task_queue.get_or_put_context(task_id, ())?; + if !entry.found_existing { + *entry.value_ptr = TaskCallbackList::default(); + } + Ok(entry.value_ptr) +} + +/// Registers dependency `id` as a callback for `task_id`, tagging it as a +/// root or transitive dependency. +fn push_dependency_task_callback( + this: &mut PackageManager, + task_id: Task::Id, + id: DependencyID, + is_root: bool, +) -> crate::Result<()> { + let ctx = if is_root { + TaskCallbackContext::RootDependency(id) + } else { + TaskCallbackContext::Dependency(id) + }; + task_callback_list(this, task_id)?.push(ctx); + Ok(()) +} + /// Q: "What do we do with a dependency in a package.json?" /// A: "We enqueue it!" pub fn enqueue_dependency_with_main_and_success_fn( @@ -1179,18 +1209,7 @@ pub fn enqueue_dependency_with_main_and_success_fn( return Ok(()); } - let manifest_entry_parse = - this.task_queue.get_or_put_context(task_id, ())?; - if !manifest_entry_parse.found_existing { - *manifest_entry_parse.value_ptr = TaskCallbackList::default(); - } - - let ctx = if is_root { - TaskCallbackContext::RootDependency(id) - } else { - TaskCallbackContext::Dependency(id) - }; - manifest_entry_parse.value_ptr.push(ctx); + push_dependency_task_callback(this, task_id, id, is_root)?; } return Ok(()); } @@ -1215,11 +1234,6 @@ pub fn enqueue_dependency_with_main_and_success_fn( let alias = this.lockfile.str_detached(&dependency.name); let url = this.lockfile.str_detached(&dep.repo); let clone_id = Task::Id::for_git_clone(url); - let ctx = if is_root { - TaskCallbackContext::RootDependency(id) - } else { - TaskCallbackContext::Dependency(id) - }; if cfg!(debug_assertions) { bun_output::scoped_log!( @@ -1246,15 +1260,11 @@ pub fn enqueue_dependency_with_main_and_success_fn( let needs_ctx = this.lockfile.buffers.resolutions[id as usize] == invalid_package_id; - let entry = this - .task_queue - .get_or_put_context(checkout_id, ()) - .expect("unreachable"); - if !entry.found_existing { - *entry.value_ptr = TaskCallbackList::default(); - } if needs_ctx { - entry.value_ptr.push(ctx); + push_dependency_task_callback(this, checkout_id, id, is_root) + .expect("unreachable"); + } else { + task_callback_list(this, checkout_id).expect("unreachable"); } if dependency.behavior.is_peer() { @@ -1280,14 +1290,7 @@ pub fn enqueue_dependency_with_main_and_success_fn( ); this.task_batch.push(ThreadPool::Batch::from(task)); } else { - let entry = this - .task_queue - .get_or_put_context(clone_id, ()) - .expect("unreachable"); - if !entry.found_existing { - *entry.value_ptr = TaskCallbackList::default(); - } - entry.value_ptr.push(ctx); + push_dependency_task_callback(this, clone_id, id, is_root).expect("unreachable"); if dependency.behavior.is_peer() { if !install_peer { @@ -1332,24 +1335,7 @@ pub fn enqueue_dependency_with_main_and_success_fn( ); } - let ctx = if is_root { - TaskCallbackContext::RootDependency(id) - } else { - TaskCallbackContext::Dependency(id) - }; - // reshaped for borrowck — `entry` mutably borrows - // `this.task_queue`; scope it tightly so the calls below can - // reborrow `*this`. - { - let entry = this - .task_queue - .get_or_put_context(task_id, ()) - .expect("unreachable"); - if !entry.found_existing { - *entry.value_ptr = TaskCallbackList::default(); - } - entry.value_ptr.push(ctx); - } + push_dependency_task_callback(this, task_id, id, is_root).expect("unreachable"); if dependency.behavior.is_peer() { if !install_peer { @@ -1532,22 +1518,7 @@ pub fn enqueue_dependency_with_main_and_success_fn( ); } - let ctx = if is_root { - TaskCallbackContext::RootDependency(id) - } else { - TaskCallbackContext::Dependency(id) - }; - // reshaped for borrowck — scope `entry` tightly. - { - let entry = this - .task_queue - .get_or_put_context(task_id, ()) - .expect("unreachable"); - if !entry.found_existing { - *entry.value_ptr = TaskCallbackList::default(); - } - entry.value_ptr.push(ctx); - } + push_dependency_task_callback(this, task_id, id, is_root).expect("unreachable"); if dependency.behavior.is_peer() { if !install_peer { @@ -2188,6 +2159,41 @@ fn get_or_put_resolved_package_with_find_result( // `guard` drops here → success_fn(this, dependency_id, package.meta.id) } +/// Scans the root package's dependencies for a workspace entry matching +/// `name_hash`; on a match, records the resolution via `success_fn` and +/// returns the already-resolved workspace package. +fn resolve_root_workspace_package( + this: &mut PackageManager, + name_hash: PackageNameHash, + dependency_id: DependencyID, + success_fn: SuccessFn, +) -> Option { + let root_package = this.lockfile.root_package()?; + let root_dependencies = root_package + .dependencies + .get(this.lockfile.buffers.dependencies.as_slice()); + let root_resolutions = root_package + .resolutions + .get(this.lockfile.buffers.resolutions.as_slice()); + + debug_assert_eq!(root_dependencies.len(), root_resolutions.len()); + for (root_dep, &workspace_package_id) in root_dependencies.iter().zip(root_resolutions) { + if workspace_package_id != invalid_package_id + && root_dep.version.tag == dependency::version::Tag::Workspace + && root_dep.name_hash == name_hash + { + // make sure verifyResolutions sees this resolution as a valid package id + success_fn(this, dependency_id, workspace_package_id); + return Some(ResolvedPackageResult { + package: *this.lockfile.packages.get(workspace_package_id as usize), + is_first_time: false, + task: None, + }); + } + } + None +} + fn get_or_put_resolved_package( this: &mut PackageManager, name_hash: PackageNameHash, @@ -2314,54 +2320,27 @@ fn get_or_put_resolved_package( match version.tag { dependency::version::Tag::Npm | dependency::version::Tag::DistTag => { - 'resolve_from_workspace: { - if version.tag == dependency::version::Tag::Npm { - let workspace_path = if this.lockfile.workspace_paths.count() > 0 { - this.lockfile.workspace_paths.get(&name_hash) - } else { - None - }; - let workspace_version = this.lockfile.workspace_versions.get(&name_hash); - let buf = this.lockfile.buffers.string_bytes.as_slice(); - let npm_group = &version.npm().version; - if this.options.link_workspace_packages - && ((workspace_version.is_some() - && npm_group.satisfies(*workspace_version.unwrap(), buf, buf)) - // https://github.com/oven-sh/bun/pull/10899#issuecomment-2099609419 - // if the workspace doesn't have a version, it can still be used if - // dependency version is wildcard - || (workspace_path.is_some() && npm_group.is_star())) + if version.tag == dependency::version::Tag::Npm { + let workspace_path = if this.lockfile.workspace_paths.count() > 0 { + this.lockfile.workspace_paths.get(&name_hash) + } else { + None + }; + let workspace_version = this.lockfile.workspace_versions.get(&name_hash); + let buf = this.lockfile.buffers.string_bytes.as_slice(); + let npm_group = &version.npm().version; + if this.options.link_workspace_packages + && ((workspace_version.is_some() + && npm_group.satisfies(*workspace_version.unwrap(), buf, buf)) + // https://github.com/oven-sh/bun/pull/10899#issuecomment-2099609419 + // if the workspace doesn't have a version, it can still be used if + // dependency version is wildcard + || (workspace_path.is_some() && npm_group.is_star())) + { + if let Some(resolved) = + resolve_root_workspace_package(this, name_hash, dependency_id, success_fn) { - let Some(root_package) = this.lockfile.root_package() else { - break 'resolve_from_workspace; - }; - let root_dependencies = root_package - .dependencies - .get(this.lockfile.buffers.dependencies.as_slice()); - let root_resolutions = root_package - .resolutions - .get(this.lockfile.buffers.resolutions.as_slice()); - - debug_assert_eq!(root_dependencies.len(), root_resolutions.len()); - for (root_dep, &workspace_package_id) in - root_dependencies.iter().zip(root_resolutions) - { - if workspace_package_id != invalid_package_id - && root_dep.version.tag == dependency::version::Tag::Workspace - && root_dep.name_hash == name_hash - { - // make sure verifyResolutions sees this resolution as a valid package id - success_fn(this, dependency_id, workspace_package_id); - return Ok(Some(ResolvedPackageResult { - package: *this - .lockfile - .packages - .get(workspace_package_id as usize), - is_first_time: false, - task: None, - })); - } - } + return Ok(Some(resolved)); } } } @@ -2475,46 +2454,21 @@ fn get_or_put_resolved_package( let find_result = match find_result_opt { Some(r) => r, None => { - 'resolve_workspace_from_dist_tag: { - // choose a workspace for a dist_tag only if a version was not found - if version.tag == dependency::version::Tag::DistTag { - let workspace_path = if this.lockfile.workspace_paths.count() > 0 { - this.lockfile.workspace_paths.get(&name_hash) - } else { - None - }; - if workspace_path.is_some() { - let Some(root_package) = this.lockfile.root_package() else { - break 'resolve_workspace_from_dist_tag; - }; - let root_dependencies = root_package - .dependencies - .get(this.lockfile.buffers.dependencies.as_slice()); - let root_resolutions = root_package - .resolutions - .get(this.lockfile.buffers.resolutions.as_slice()); - - debug_assert_eq!(root_dependencies.len(), root_resolutions.len()); - for (root_dep, &workspace_package_id) in - root_dependencies.iter().zip(root_resolutions) - { - if workspace_package_id != invalid_package_id - && root_dep.version.tag - == dependency::version::Tag::Workspace - && root_dep.name_hash == name_hash - { - // make sure verifyResolutions sees this resolution as a valid package id - success_fn(this, dependency_id, workspace_package_id); - return Ok(Some(ResolvedPackageResult { - package: *this - .lockfile - .packages - .get(workspace_package_id as usize), - is_first_time: false, - task: None, - })); - } - } + // choose a workspace for a dist_tag only if a version was not found + if version.tag == dependency::version::Tag::DistTag { + let workspace_path = if this.lockfile.workspace_paths.count() > 0 { + this.lockfile.workspace_paths.get(&name_hash) + } else { + None + }; + if workspace_path.is_some() { + if let Some(resolved) = resolve_root_workspace_package( + this, + name_hash, + dependency_id, + success_fn, + ) { + return Ok(Some(resolved)); } } } diff --git a/src/install/PackageManager/WorkspacePackageJSONCache.rs b/src/install/PackageManager/WorkspacePackageJSONCache.rs index 087e52430d57..248d74d461e1 100644 --- a/src/install/PackageManager/WorkspacePackageJSONCache.rs +++ b/src/install/PackageManager/WorkspacePackageJSONCache.rs @@ -2,6 +2,7 @@ use crate::Error; use bun_collections::StringHashMap; +use bun_core::{Global, Output}; // `Expr` here is the JSON parser's AST node (`bun_ast::Expr`, re- // exported via `crate::bun_json`). It is intentionally NOT `bun_ast::Expr` // — that lives in a higher-tier crate and is a distinct type. Consumers of @@ -206,6 +207,30 @@ impl WorkspacePackageJSONCache { GetResult::Entry(entry.value_ptr) } + /// `get_with_path`, except read/parse failures are fatal: pending log + /// messages and the error are printed to stderr, then the process exits. + pub fn get_with_path_or_exit( + &mut self, + log: &mut Log, + abs_package_json_path: &[u8], + opts: GetJSONOptions, + ) -> &mut MapEntry { + let (err, fmt) = match self.get_with_path(log, abs_package_json_path, opts) { + GetResult::Entry(entry) => return entry, + GetResult::ReadErr(err) => (err, "failed to read '{}'"), + GetResult::ParseErr(err) => (err, "failed to parse '{}'"), + }; + if log.errors > 0 { + let _ = log.print(std::ptr::from_mut(Output::error_writer())); + } + Output::err( + err, + fmt, + format_args!("{}", bstr::BStr::new(abs_package_json_path)), + ); + Global::crash(); + } + /// source path is used as the key, needs to be absolute pub fn get_with_source( &mut self, diff --git a/src/install/PackageManager/install_with_manager.rs b/src/install/PackageManager/install_with_manager.rs index 058a17fd9517..b26eb90970f7 100644 --- a/src/install/PackageManager/install_with_manager.rs +++ b/src/install/PackageManager/install_with_manager.rs @@ -8,7 +8,6 @@ use bun_core::{ZStr, strings}; use bun_glob as glob; use bun_semver::String as SemverString; -use crate::GetJsonResult as WorkspacePackageJsonCacheResult; use crate::Subcommand; use crate::dependency::{DependencyExt as _, Tag as DependencyVersionTag}; use crate::lockfile::{self, Lockfile}; @@ -140,72 +139,12 @@ pub fn install_with_manager( let mut lockfile = Lockfile::default(); let mut maybe_root = lockfile::Package::default(); - // SAFETY: `manager.log` is a non-null backref to the CLI log set at init(). - let root_package_json_entry = match manager - .workspace_package_json_cache - .get_with_path( - manager.log_mut(), - root_package_json_path.as_bytes(), - Default::default(), - ) { - WorkspacePackageJsonCacheResult::Entry(entry) => entry, - WorkspacePackageJsonCacheResult::ReadErr(err) => { - if manager.log_mut().errors > 0 { - manager - .log_mut() - .print(std::ptr::from_mut(Output::error_writer()))?; - } - Output::err( - err, - "failed to read '{}'", - format_args!("{}", bstr::BStr::new(root_package_json_path.as_bytes())), - ); - Global::exit(1); - } - WorkspacePackageJsonCacheResult::ParseErr(err) => { - if manager.log_mut().errors > 0 { - manager - .log_mut() - .print(std::ptr::from_mut(Output::error_writer()))?; - } - Output::err( - err, - "failed to parse '{}'", - format_args!("{}", bstr::BStr::new(root_package_json_path.as_bytes())), - ); - Global::exit(1); - } - }; - - // `Source` is not `Copy`, so - // clone it (cheap — `Source` is a few `Box<[u8]>` handles) so the - // `&mut *mgr` reborrow below doesn't conflict with the cache borrow. - let source_copy = root_package_json_entry.source.clone(); - - let mut resolver: () = (); - // `parse` needs `manager`, `manager.log` and a fresh - // stack `lockfile` simultaneously. Route through raw ptrs so - // borrowck doesn't see overlapping `&mut PackageManager` / - // `&mut Lockfile`. - { - // `log_mut()` reads the BACKREF `self.log: *mut Log` and - // returns the disjoint CLI `Log` allocation (lifetime - // decoupled from `&self`), so call it safely through - // `manager` *before* establishing the raw-ptr split — no - // borrow on `*manager` survives into the `&mut *mgr` below. - let log = manager.log_mut(); - let mgr: *mut PackageManager = manager; - maybe_root.parse( - &mut lockfile, - // SAFETY: `mgr` is the sole provenance root for `*manager`; `log` is a - // disjoint backref and `lockfile` is a stack local, so this `&mut` is unique. - unsafe { &mut *mgr }, - log, - &source_copy, - &mut resolver, - Features::main(), - )?; - } + parse_root_package( + manager, + root_package_json_path, + &mut lockfile, + &mut maybe_root, + )?; let mut mapping = vec![invalid_package_id; maybe_root.dependencies.len as usize] .into_boxed_slice(); // @memset already done via vec! init @@ -1484,6 +1423,49 @@ fn record_updating_package_versions(manager: &mut PackageManager) { } } +/// Load the root package.json from the workspace cache (exiting on read/parse +/// errors) and parse it as the root `Package` into `lockfile`. `lockfile` must +/// be storage disjoint from `*manager` (a stack local, or the heap allocation +/// behind `manager.lockfile`'s `Box`). +fn parse_root_package( + manager: &mut PackageManager, + root_package_json_path: &ZStr, + lockfile: &mut Lockfile, + root: &mut lockfile::Package, +) -> crate::Result<()> { + // SAFETY: `manager.log` is a non-null backref to the CLI log set at init(). + let root_package_json_entry = manager.workspace_package_json_cache.get_with_path_or_exit( + manager.log_mut(), + root_package_json_path.as_bytes(), + Default::default(), + ); + + // `Source` is not `Copy`, so clone it (cheap — `Source` is a few + // `Box<[u8]>` handles) so the `&mut *mgr` reborrow below doesn't conflict + // with the cache borrow. + let source_copy = root_package_json_entry.source.clone(); + + let mut resolver: () = (); + // `log_mut()` reads the BACKREF `self.log: *mut Log` and returns the + // disjoint CLI `Log` allocation (lifetime decoupled from `&self`), so call + // it safely through `manager` *before* establishing the raw-ptr split — no + // borrow on `*manager` survives into the `&mut *mgr` below. + let log = manager.log_mut(); + let mgr: *mut PackageManager = manager; + root.parse( + lockfile, + // SAFETY: `mgr` is the sole provenance root for `*manager`; `log` is a + // disjoint backref and `lockfile` is caller-guaranteed disjoint + // storage, so this `&mut` is unique. + unsafe { &mut *mgr }, + log, + &source_copy, + &mut resolver, + Features::main(), + )?; + Ok(()) +} + #[cold] #[inline(never)] fn create_new_lockfile_and_enqueue( @@ -1521,63 +1503,21 @@ fn create_new_lockfile_and_enqueue( Global::crash(); } - // SAFETY: `manager.log` is a non-null backref to the CLI log set at init(). - let root_package_json_entry = match manager.workspace_package_json_cache.get_with_path( - manager.log_mut(), - root_package_json_path.as_bytes(), - Default::default(), - ) { - WorkspacePackageJsonCacheResult::Entry(entry) => entry, - WorkspacePackageJsonCacheResult::ReadErr(err) => { - if manager.log_mut().errors > 0 { - manager - .log_mut() - .print(std::ptr::from_mut(Output::error_writer()))?; - } - Output::err( - err, - "failed to read '{}'", - format_args!("{}", bstr::BStr::new(root_package_json_path.as_bytes())), - ); - Global::exit(1); - } - WorkspacePackageJsonCacheResult::ParseErr(err) => { - if manager.log_mut().errors > 0 { - manager - .log_mut() - .print(std::ptr::from_mut(Output::error_writer()))?; - } - Output::err( - err, - "failed to parse '{}'", - format_args!("{}", bstr::BStr::new(root_package_json_path.as_bytes())), - ); - Global::exit(1); - } - }; - - let source_copy = root_package_json_entry.source.clone(); - - let mut resolver: () = (); { - // `log_mut()` reads the BACKREF `self.log` and returns the disjoint - // CLI `Log` allocation (lifetime decoupled from `&self`); call it - // safely *before* the raw-ptr split. - let log = manager.log_mut(); let mgr: *mut PackageManager = manager; - // SAFETY: `mgr` is the sole provenance root; `parse` reborrows the - // disjoint `lockfile` field through it. No other live `&mut` to - // `*mgr` exists across the call. - root.parse( - // SAFETY: disjoint field projection through the sole provenance root `mgr`. - unsafe { &mut (*mgr).lockfile }, - // SAFETY: `parse` touches only `PackageManager` fields disjoint from - // `lockfile` through this borrow; `mgr` is the sole provenance root. + // SAFETY: `mgr` is the sole provenance root; `manager.lockfile` is an + // owned `Box`, so this projects its heap allocation, which is disjoint + // storage from the `PackageManager` struct itself. + let lockfile: *mut Lockfile = unsafe { &raw mut *(*mgr).lockfile }; + parse_root_package( + // SAFETY: `parse_root_package` touches only `PackageManager` fields + // disjoint from `lockfile` through this borrow; `mgr` is the sole + // provenance root. unsafe { &mut *mgr }, - log, - &source_copy, - &mut resolver, - Features::main(), + root_package_json_path, + // SAFETY: points to the Box-owned heap allocation, disjoint from `*mgr`. + unsafe { &mut *lockfile }, + &mut root, )?; } diff --git a/src/install/PackageManager/patchPackage.rs b/src/install/PackageManager/patchPackage.rs index 04dfd2bc0100..8cecded7436b 100644 --- a/src/install/PackageManager/patchPackage.rs +++ b/src/install/PackageManager/patchPackage.rs @@ -147,101 +147,7 @@ pub fn do_patch_commit( let (cache_dir, cache_dir_subpath, changes_dir, pkg): (Fd, &ZStr, Vec, Package) = match arg_kind { PatchArgKind::Path => 'result: { - let package_json_path = - resolve_path::join_z::(&[argument, b"package.json"]); - let package_json_source: bun_ast::Source = - match bun_ast::to_source(package_json_path, Default::default()) { - Ok(s) => s, - Err(e) => { - Output::err( - e, - "failed to read {f}", - (bun_fmt::quote(package_json_path.as_bytes()),), - ); - Global::crash(); - } - }; - - initialize_store(); - let log = manager.log_mut(); - let parsed = match JSON::ParsedJson::parse_package_json(&package_json_source, log) { - Ok(p) => p, - Err(err) => { - let _ = log.print(std::ptr::from_mut(Output::error_writer())); - bun_core::pretty_errorln!( - "{} parsing package.json in \"{}\"", - err.name(), - bstr::BStr::new(package_json_source.path.pretty_dir()), - ); - Global::crash(); - } - }; - let json = parsed.root; - - let version: &[u8] = 'version: { - if let Some(v) = json.get(b"version") { - if let bun_ast::ExprData::EString(s) = &v.data { - let s = s.data.slice(); - break 'version s; - } - } - bun_core::pretty_error!( - "error: invalid package.json, missing or invalid property \"version\": {}\n", - bstr::BStr::new(package_json_source.path.text()), - ); - Global::crash(); - }; - - let mut resolver: () = (); - let mut package = Package::default(); - let log = manager.log_mut(); - package.parse_with_json::<()>( - &mut lockfile, - manager, - log, - &package_json_source, - json, - &mut resolver, - Features::FOLDER, - )?; - - let actual_package = match lockfile.package_index.get(&package.name_hash) { - None => { - bun_core::pretty_error!( - "error: failed to find package in lockfile package index, this is a bug in Bun. Please file a GitHub issue.\n", - ); - Global::crash(); - } - Some(PackageIndexEntry::Id(id)) => *lockfile.packages.get(*id as usize), - Some(PackageIndexEntry::Ids(ids)) => 'brk: { - for &id in ids.as_slice() { - let pkg = *lockfile.packages.get(id as usize); - let total = resolution_buf.len(); - let mut cursor: &mut [u8] = &mut resolution_buf[..]; - write!( - &mut cursor, - "{}", - pkg.resolution - .fmt(lockfile.buffers.string_bytes.as_slice(), PathSep::Posix) - ) - .expect("unreachable"); - let written = total - cursor.len(); - let resolution_label = &resolution_buf[..written]; - if resolution_label == version { - break 'brk pkg; - } - } - bun_core::pretty_error!( - "error: could not find package with name: {}\n", - bstr::BStr::new( - package.name.slice(lockfile.buffers.string_bytes.as_slice()) - ), - ); - Global::crash(); - } - }; - - let name = lockfile.str(&package.name).to_vec(); + let (name, actual_package) = load_path_package(manager, &mut lockfile, argument)?; let resolution_clone = actual_package.resolution; let cache_result = compute_cache_dir_and_subpath( manager, @@ -724,7 +630,6 @@ pub fn prepare_patch(manager: &mut PackageManager) -> Result<(), crate::Error> { let arg_kind: PatchArgKind = PatchArgKind::from_arg(argument); let mut folder_path_buf = PathBuffer::uninit(); - let mut resolution_buf = [0u8; 1024]; #[cfg(windows)] let mut win_normalizer = PathBuffer::uninit(); @@ -758,109 +663,18 @@ pub fn prepare_patch(manager: &mut PackageManager) -> Result<(), crate::Error> { let (cache_dir, cache_dir_subpath, module_folder, pkg_name): (Fd, &[u8], Vec, Vec) = match arg_kind { PatchArgKind::Path => 'brk: { - let package_json_path = - resolve_path::join_z::(&[argument, b"package.json"]); - let package_json_source: bun_ast::Source = - match bun_ast::to_source(package_json_path, Default::default()) { - Ok(s) => s, - Err(e) => { - Output::err( - e, - "failed to read {f}", - (bun_fmt::quote(package_json_path.as_bytes()),), - ); - Global::crash(); - } - }; - - initialize_store(); - let log = manager.log_mut(); - let parsed = match JSON::ParsedJson::parse_package_json(&package_json_source, log) { - Ok(p) => p, - Err(err) => { - let _ = log.print(std::ptr::from_mut(Output::error_writer())); - bun_core::pretty_errorln!( - "{} parsing package.json in \"{}\"", - err.name(), - bstr::BStr::new(package_json_source.path.pretty_dir()), - ); - Global::crash(); - } - }; - let json = parsed.root; - - let version: &[u8] = 'version: { - if let Some(v) = json.get(b"version") { - if let bun_ast::ExprData::EString(s) = &v.data { - let s = s.data.slice(); - break 'version s; - } - } - bun_core::pretty_error!( - "error: invalid package.json, missing or invalid property \"version\": {}\n", - bstr::BStr::new(package_json_source.path.text()), - ); - Global::crash(); - }; - - let mut resolver: () = (); - let mut package = Package::default(); - let log = manager.log_mut(); - // borrowck — `parse_with_json` needs `&mut Lockfile` and + // borrowck — `load_path_package` needs `&mut Lockfile` and // `&mut PackageManager` simultaneously, but the lockfile here is // `manager.lockfile`. Temporarily move the Box out so the two - // borrows are disjoint; `parse_with_json` never reads `pm.lockfile` - // (it takes the lockfile as its own parameter). Restore before - // propagating any error so `manager` is never left half-torn. + // borrows are disjoint. Restore before propagating any error so + // `manager` is never left half-torn. let mut lockfile: Box = core::mem::take(&mut manager.lockfile); - let parse_result = package.parse_with_json::<()>( - &mut lockfile, - manager, - log, - &package_json_source, - json, - &mut resolver, - Features::FOLDER, - ); + let result = load_path_package(manager, &mut lockfile, argument); manager.lockfile = lockfile; - parse_result?; + let (name, actual_package) = result?; let lockfile: &Lockfile = &manager.lockfile; let strbuf = lockfile.buffers.string_bytes.as_slice(); - let actual_package = match lockfile.package_index.get(&package.name_hash) { - None => { - bun_core::pretty_error!( - "error: failed to find package in lockfile package index, this is a bug in Bun. Please file a GitHub issue.\n", - ); - Global::crash(); - } - Some(PackageIndexEntry::Id(id)) => *lockfile.packages.get(*id as usize), - Some(PackageIndexEntry::Ids(ids)) => 'id: { - for &id in ids.as_slice() { - let pkg = *lockfile.packages.get(id as usize); - let total = resolution_buf.len(); - let mut cursor: &mut [u8] = &mut resolution_buf[..]; - write!( - &mut cursor, - "{}", - pkg.resolution.fmt(strbuf, PathSep::Posix) - ) - .expect("unreachable"); - let written = total - cursor.len(); - let resolution_label = &resolution_buf[..written]; - if resolution_label == version { - break 'id pkg; - } - } - bun_core::pretty_error!( - "error: could not find package with name: {}\n", - bstr::BStr::new(package.name.slice(strbuf)), - ); - Global::crash(); - } - }; - - let name = lockfile.str(&package.name).to_vec(); let existing_patchfile_hash: Option = 'existing_patchfile_hash: { let mut name_and_version = Vec::new(); write!( @@ -1240,6 +1054,112 @@ fn node_modules_folder_for_dependency_id( } } +/// Shared `PatchArgKind::Path` handling for `bun patch` and `bun patch --commit`: +/// parse `/package.json`, register it against the lockfile, and find +/// the matching package in the lockfile's package index. Returns the package +/// name and the lockfile's entry for the package. +/// +/// `parse_with_json` never reads `manager.lockfile` (it takes the lockfile as +/// its own parameter), so callers whose lockfile lives in `manager.lockfile` +/// may temporarily move the Box out to make the two `&mut` borrows disjoint. +fn load_path_package( + manager: &mut PackageManager, + lockfile: &mut Lockfile, + argument: &[u8], +) -> crate::Result<(Vec, Package)> { + let package_json_path = resolve_path::join_z::(&[argument, b"package.json"]); + let package_json_source: bun_ast::Source = + match bun_ast::to_source(package_json_path, Default::default()) { + Ok(s) => s, + Err(e) => { + Output::err( + e, + "failed to read {f}", + (bun_fmt::quote(package_json_path.as_bytes()),), + ); + Global::crash(); + } + }; + + initialize_store(); + let log = manager.log_mut(); + let parsed = match JSON::ParsedJson::parse_package_json(&package_json_source, log) { + Ok(p) => p, + Err(err) => { + let _ = log.print(std::ptr::from_mut(Output::error_writer())); + bun_core::pretty_errorln!( + "{} parsing package.json in \"{}\"", + err.name(), + bstr::BStr::new(package_json_source.path.pretty_dir()), + ); + Global::crash(); + } + }; + let json = parsed.root; + + let version: &[u8] = 'version: { + if let Some(v) = json.get(b"version") { + if let bun_ast::ExprData::EString(s) = &v.data { + break 'version s.data.slice(); + } + } + bun_core::pretty_error!( + "error: invalid package.json, missing or invalid property \"version\": {}\n", + bstr::BStr::new(package_json_source.path.text()), + ); + Global::crash(); + }; + + let mut resolver: () = (); + let mut package = Package::default(); + package.parse_with_json::<()>( + lockfile, + manager, + log, + &package_json_source, + json, + &mut resolver, + Features::FOLDER, + )?; + + let strbuf = lockfile.buffers.string_bytes.as_slice(); + let actual_package = match lockfile.package_index.get(&package.name_hash) { + None => { + bun_core::pretty_error!( + "error: failed to find package in lockfile package index, this is a bug in Bun. Please file a GitHub issue.\n", + ); + Global::crash(); + } + Some(PackageIndexEntry::Id(id)) => *lockfile.packages.get(*id as usize), + Some(PackageIndexEntry::Ids(ids)) => 'brk: { + let mut resolution_buf = [0u8; 1024]; + for &id in ids.as_slice() { + let pkg = *lockfile.packages.get(id as usize); + let total = resolution_buf.len(); + let mut cursor: &mut [u8] = &mut resolution_buf[..]; + write!( + &mut cursor, + "{}", + pkg.resolution.fmt(strbuf, PathSep::Posix) + ) + .expect("unreachable"); + let written = total - cursor.len(); + if &resolution_buf[..written] == version { + break 'brk pkg; + } + } + bun_core::pretty_error!( + "error: could not find package with name: {}\n", + bstr::BStr::new(package.name.slice(strbuf)), + ); + Global::crash(); + } + }; + + let name = lockfile.str(&package.name).to_vec(); + Ok((name, actual_package)) +} + type IdPair = (DependencyID, PackageID); fn pkg_info_for_name_and_version( diff --git a/src/install/PackageManager/runTasks.rs b/src/install/PackageManager/runTasks.rs index 90c984a207b9..a43d6d4bdca0 100644 --- a/src/install/PackageManager/runTasks.rs +++ b/src/install/PackageManager/runTasks.rs @@ -448,16 +448,7 @@ pub fn run_tasks( ); } - if manager.subcommand != Subcommand::Remove { - for request in manager.update_requests.iter_mut() { - if strings::eql(request.name, name) { - request.failed = true; - manager.options.do_.remove(Do::SAVE_LOCKFILE); - manager.options.do_.remove(Do::SAVE_YARN_LOCK); - manager.options.do_.remove(Do::INSTALL_PACKAGES); - } - } - } + mark_update_request_failed(manager, name); } continue; @@ -500,16 +491,7 @@ pub fn run_tasks( response.status_code, ); } - if manager.subcommand != Subcommand::Remove { - for request in manager.update_requests.iter_mut() { - if strings::eql(request.name, name) { - request.failed = true; - manager.options.do_.remove(Do::SAVE_LOCKFILE); - manager.options.do_.remove(Do::SAVE_YARN_LOCK); - manager.options.do_.remove(Do::INSTALL_PACKAGES); - } - } - } + mark_update_request_failed(manager, name); continue; } @@ -706,45 +688,11 @@ pub fn run_tasks( .map(crate::Error::from) .unwrap_or(crate::Error::TarballFailedToDownload); - // The download will not be retried for this task_id, so - // drop the dedupe state before dispatching the error. - // Otherwise a later `enqueuePackageForDownload` for the - // same package sees `found_existing`, never schedules a - // network task, and waits forever for a callback that - // will not arrive. `Store.Installer.onPackageDownloadError` - // drains `task_queue` itself but does not touch - // `network_dedupe_map`, so this must run on the callback - // path too. Capture `is_required` first — - // `isNetworkTaskRequired` reads the map and returns `true` - // when the entry is gone, which would upgrade optional-dep - // warnings to errors on the void-callback fallback below. - let is_required = manager.is_network_task_required(task.task_id); - let _ = manager.network_dedupe_map.remove(&task.task_id); - - if C::HAS_ON_PACKAGE_DOWNLOAD_ERROR { - if C::IS_STORE_INSTALLER { - C::on_package_download_error_store( - extract_ctx, - task.task_id, - extract.name.slice(), - &extract.resolution, - err, - &task.url_buf, - ); - } else { - let package_id = manager.lockfile.buffers.resolutions - [extract.dependency_id as usize]; - C::on_package_download_error_pkg( - extract_ctx, - package_id, - extract.name.slice(), - &extract.resolution, - err, - &task.url_buf, - ); - } + let Some(is_required) = + dispatch_tarball_error::(manager, extract_ctx, task, extract, err) + else { continue; - } + }; if is_required { bun_ast::add_error_pretty!( @@ -771,16 +719,7 @@ pub fn run_tasks( .fmt(&manager.lockfile.buffers.string_bytes, PathSep::Auto,), ); } - if manager.subcommand != Subcommand::Remove { - for request in manager.update_requests.iter_mut() { - if strings::eql(request.name, extract.name.slice()) { - request.failed = true; - manager.options.do_.remove(Do::SAVE_LOCKFILE); - manager.options.do_.remove(Do::SAVE_YARN_LOCK); - manager.options.do_.remove(Do::INSTALL_PACKAGES); - } - } - } + mark_update_request_failed(manager, extract.name.slice()); if let Some(removed) = manager.task_queue.remove(&task.task_id) { drop(removed); @@ -792,50 +731,21 @@ pub fn run_tasks( let response = &metadata.response; if response.status_code > 399 { - // Non-retryable HTTP error: drop dedupe state so a later - // enqueue for this task_id schedules a fresh network task - // instead of waiting on this failed one. Runs before the - // callback branch so `Store.Installer` (which `continue`s - // from the callback) is covered too. Capture - // `is_required` first — `isNetworkTaskRequired` reads the - // map and returns `true` when the entry is gone. - let is_required = manager.is_network_task_required(task.task_id); - let _ = manager.network_dedupe_map.remove(&task.task_id); - - if C::HAS_ON_PACKAGE_DOWNLOAD_ERROR { - let err = match response.status_code { - 400 => crate::Error::TarballHTTP400, - 401 => crate::Error::TarballHTTP401, - 402 => crate::Error::TarballHTTP402, - 403 => crate::Error::TarballHTTP403, - 404 => crate::Error::TarballHTTP404, - 405..=499 => crate::Error::TarballHTTP4xx, - _ => crate::Error::TarballHTTP5xx, - }; + let err = match response.status_code { + 400 => crate::Error::TarballHTTP400, + 401 => crate::Error::TarballHTTP401, + 402 => crate::Error::TarballHTTP402, + 403 => crate::Error::TarballHTTP403, + 404 => crate::Error::TarballHTTP404, + 405..=499 => crate::Error::TarballHTTP4xx, + _ => crate::Error::TarballHTTP5xx, + }; - if C::IS_STORE_INSTALLER { - C::on_package_download_error_store( - extract_ctx, - task.task_id, - extract.name.slice(), - &extract.resolution, - err, - &task.url_buf, - ); - } else { - let package_id = manager.lockfile.buffers.resolutions - [extract.dependency_id as usize]; - C::on_package_download_error_pkg( - extract_ctx, - package_id, - extract.name.slice(), - &extract.resolution, - err, - &task.url_buf, - ); - } + let Some(is_required) = + dispatch_tarball_error::(manager, extract_ctx, task, extract, err) + else { continue; - } + }; if is_required { bun_ast::add_error_pretty!( @@ -856,16 +766,7 @@ pub fn run_tasks( response.status_code, ); } - if manager.subcommand != Subcommand::Remove { - for request in manager.update_requests.iter_mut() { - if strings::eql(request.name, extract.name.slice()) { - request.failed = true; - manager.options.do_.remove(Do::SAVE_LOCKFILE); - manager.options.do_.remove(Do::SAVE_YARN_LOCK); - manager.options.do_.remove(Do::INSTALL_PACKAGES); - } - } - } + mark_update_request_failed(manager, extract.name.slice()); if let Some(removed) = manager.task_queue.remove(&task.task_id) { drop(removed); @@ -1564,6 +1465,68 @@ pub fn run_tasks( Ok(()) } +/// Non-retryable tarball download failure. Drops the dedupe state before +/// dispatching the error — otherwise a later `enqueuePackageForDownload` for +/// the same package sees `found_existing`, never schedules a network task, +/// and waits forever for a callback that will not arrive. +/// `Store.Installer.onPackageDownloadError` drains `task_queue` itself but +/// does not touch `network_dedupe_map`, so this must run on the callback path +/// too. Returns `None` when the `on_package_download_error_*` callback +/// consumed the error, otherwise `Some(is_required)` for the caller's +/// log-and-mark fallback. `is_required` is captured before the dedupe entry +/// is removed — `isNetworkTaskRequired` reads the map and returns `true` when +/// the entry is gone, which would upgrade optional-dep warnings to errors. +fn dispatch_tarball_error( + manager: &mut PackageManager, + extract_ctx: &mut C::Ctx, + task: &NetworkTask, + extract: &ExtractTarball, + err: crate::Error, +) -> Option { + let is_required = manager.is_network_task_required(task.task_id); + let _ = manager.network_dedupe_map.remove(&task.task_id); + + if C::HAS_ON_PACKAGE_DOWNLOAD_ERROR { + if C::IS_STORE_INSTALLER { + C::on_package_download_error_store( + extract_ctx, + task.task_id, + extract.name.slice(), + &extract.resolution, + err, + &task.url_buf, + ); + } else { + let package_id = manager.lockfile.buffers.resolutions[extract.dependency_id as usize]; + C::on_package_download_error_pkg( + extract_ctx, + package_id, + extract.name.slice(), + &extract.resolution, + err, + &task.url_buf, + ); + } + return None; + } + + Some(is_required) +} + +fn mark_update_request_failed(manager: &mut PackageManager, name: &[u8]) { + if manager.subcommand == Subcommand::Remove { + return; + } + for request in manager.update_requests.iter_mut() { + if strings::eql(request.name, name) { + request.failed = true; + manager.options.do_.remove(Do::SAVE_LOCKFILE); + manager.options.do_.remove(Do::SAVE_YARN_LOCK); + manager.options.do_.remove(Do::INSTALL_PACKAGES); + } + } +} + #[inline] pub fn pending_task_count(manager: &PackageManager) -> u32 { manager.pending_tasks.load(Ordering::Acquire) diff --git a/src/install/PackageManager/updatePackageJSONAndInstall.rs b/src/install/PackageManager/updatePackageJSONAndInstall.rs index c033b9f19c4e..69cecbaca0db 100644 --- a/src/install/PackageManager/updatePackageJSONAndInstall.rs +++ b/src/install/PackageManager/updatePackageJSONAndInstall.rs @@ -1,10 +1,9 @@ use crate::lockfile::package::PackageColumns as _; +use bstr::BStr; use bun_collections::VecExt; use core::fmt; use std::borrow::Cow; -use bstr::BStr; - use crate::Error; use crate::ShellCompletions; use crate::bun_fs::FileSystem; @@ -618,25 +617,22 @@ fn update_package_json_and_install_with_manager_with_updates( let (source, path): (&[u8], &ZStr) = if matches!(manager.options.patch_features, PatchFeatures::Commit { .. }) { 'source_and_path: { - let root_package_json_entry = match manager - .workspace_package_json_cache - .get_with_path( + let root_package_json_entry = + match manager.workspace_package_json_cache.get_with_path( manager.log_mut(), root_package_json_path.as_bytes(), GetJSONOptions::default(), - ) - .unwrap() - { - Ok(e) => e, - Err(err) => { - Output::err( - err, - "failed to read/parse package.json at '{s}'", - (BStr::new(root_package_json_path.as_bytes()),), - ); - Global::exit(1); - } - }; + ) { + GetResult::Entry(entry) => entry, + GetResult::ReadErr(err) | GetResult::ParseErr(err) => { + Output::err( + err, + "failed to read/parse package.json at '{s}'", + (BStr::new(root_package_json_path.as_bytes()),), + ); + Global::exit(1); + } + }; break 'source_and_path ( &root_package_json_entry.source.contents, diff --git a/src/install/hosted_git_info.rs b/src/install/hosted_git_info.rs index bb14f8fb174c..d4aa843def83 100644 --- a/src/install/hosted_git_info.rs +++ b/src/install/hosted_git_info.rs @@ -1070,6 +1070,45 @@ pub mod formatters { } } + /// Shared writer for every URL template: the variants differ only in the + /// literal prefix, the separator after the host, whether auth/user + /// segments are present, and the suffix. + /// + /// Produces `{prefix}{auth}@{host}{host_sep}{user}/{project}{suffix}#{committish}`, + /// omitting the `@`, `/`, and `#` separators alongside their absent segments. + fn format_template( + prefix: &str, + auth: Option<&[u8]>, + host: &[u8], + host_sep: &str, + user: Option<&[u8]>, + project: &[u8], + suffix: &str, + committish: Option<&[u8]>, + ) -> Result, AllocError> { + let auth_str: &[u8] = auth.unwrap_or(b""); + let auth_sep = if auth_str.is_empty() { "" } else { "@" }; + let (user_str, user_sep): (&[u8], &str) = match user { + Some(u) => (u, "/"), + None => (b"", ""), + }; + let cmsh: &[u8] = committish.unwrap_or(b""); + let cmsh_sep = if cmsh.is_empty() { "" } else { "#" }; + + let mut v = Vec::new(); + write!( + &mut v, + "{prefix}{}{auth_sep}{}{host_sep}{}{user_sep}{}{suffix}{cmsh_sep}{}", + BStr::new(auth_str), + BStr::new(host), + BStr::new(user_str), + BStr::new(project), + BStr::new(cmsh), + ) + .map_err(|_| AllocError)?; + Ok(v) + } + /// Mirrors hosts.js's sshtemplate pub mod ssh { use super::*; @@ -1088,21 +1127,16 @@ pub mod formatters { committish: Option<&[u8]>, ) -> Result, AllocError> { requires_user(user); - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "git@{}:{}/{}.git{}{}", - BStr::new(self_.domain()), - BStr::new(user.unwrap()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "git@", + None, + self_.domain(), + ":", + user, + project, + ".git", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } pub(crate) fn gist( @@ -1111,20 +1145,16 @@ pub mod formatters { project: &[u8], committish: Option<&[u8]>, ) -> Result, AllocError> { - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "git@{}:{}.git{}{}", - BStr::new(self_.domain()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "git@", + None, + self_.domain(), + ":", + None, + project, + ".git", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } } @@ -1146,21 +1176,16 @@ pub mod formatters { committish: Option<&[u8]>, ) -> Result, AllocError> { requires_user(user); - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "git+ssh://git@{}/{}/{}.git{}{}", - BStr::new(self_.domain()), - BStr::new(user.unwrap()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "git+ssh://git@", + None, + self_.domain(), + "/", + user, + project, + ".git", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } pub(crate) fn gist( @@ -1169,20 +1194,16 @@ pub mod formatters { project: &[u8], committish: Option<&[u8]>, ) -> Result, AllocError> { - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "git+ssh://git@{}/{}.git{}{}", - BStr::new(self_.domain()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "git+ssh://git@", + None, + self_.domain(), + "/", + None, + project, + ".git", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } } @@ -1206,26 +1227,16 @@ pub mod formatters { committish: Option<&[u8]>, ) -> Result, AllocError> { requires_user(user); - - let auth_str: &[u8] = auth.unwrap_or(b""); - let auth_sep: &[u8] = if !auth_str.is_empty() { b"@" } else { b"" }; - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "git+https://{}{}{}/{}/{}.git{}{}", - BStr::new(auth_str), - BStr::new(auth_sep), - BStr::new(self_.domain()), - BStr::new(user.unwrap()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "git+https://", + auth, + self_.domain(), + "/", + user, + project, + ".git", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } pub(crate) fn gist( @@ -1235,20 +1246,16 @@ pub mod formatters { project: &[u8], committish: Option<&[u8]>, ) -> Result, AllocError> { - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "git+https://{}/{}.git{}{}", - BStr::new(self_.domain()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "git+https://", + None, + self_.domain(), + "/", + None, + project, + ".git", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } pub(crate) fn sourcehut( @@ -1259,22 +1266,16 @@ pub mod formatters { committish: Option<&[u8]>, ) -> Result, AllocError> { requires_user(user); - - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "https://{}/{}/{}.git{}{}", - BStr::new(self_.domain()), - BStr::new(user.unwrap()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "https://", + None, + self_.domain(), + "/", + user, + project, + ".git", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } } @@ -1296,22 +1297,16 @@ pub mod formatters { committish: Option<&[u8]>, ) -> Result, AllocError> { requires_user(user); - - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "{}{}/{}{}{}", - BStr::new(self_.shortcut()), - BStr::new(user.unwrap()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "", + None, + self_.shortcut(), + "", + user, + project, + "", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } pub(crate) fn gist( @@ -1320,20 +1315,16 @@ pub mod formatters { project: &[u8], committish: Option<&[u8]>, ) -> Result, AllocError> { - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "{}{}{}{}", - BStr::new(self_.shortcut()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "", + None, + self_.shortcut(), + "", + None, + project, + "", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } } @@ -1343,6 +1334,42 @@ pub mod formatters { pub type Type = fn(url: &JscUrl) -> Result, HostedGitInfoError>; + /// Percent-decode the parts into a single owned buffer and build the + /// `ExtractResult` over it. + fn build_result( + user: Option<&[u8]>, + project: &[u8], + committish: Option<&[u8]>, + ) -> Result { + let mut sb = StringBuilder::default(); + if let Some(u) = user { + sb.count(u); + } + sb.count(project); + if let Some(c) = committish { + sb.count(c); + } + + sb.allocate()?; + + let user_slice = match user { + Some(u) => Some(HostedGitInfo::decode_and_append(&mut sb, u)?), + None => None, + }; + let project_slice = HostedGitInfo::decode_and_append(&mut sb, project)?; + let committish_slice = match committish { + Some(c) => Some(HostedGitInfo::decode_and_append(&mut sb, c)?), + None => None, + }; + + Ok(ExtractResult { + user: user_slice, + project: project_slice, + committish: committish_slice, + _owned_buffer: Some(sb.move_to_slice()), + }) + } + pub(crate) fn github(url: &JscUrl) -> Result, HostedGitInfoError> { let pathname_owned = url.pathname().to_owned_slice(); let pathname = strings::trim_prefix(&pathname_owned, b"/"); @@ -1387,54 +1414,54 @@ pub mod formatters { committish_part }; - let mut sb = StringBuilder::default(); - sb.count(user_part); - sb.count(project); - if let Some(c) = committish { - sb.count(c); - } - - sb.allocate()?; - - let user_slice = HostedGitInfo::decode_and_append(&mut sb, user_part)?; - let project_slice = HostedGitInfo::decode_and_append(&mut sb, project)?; - let committish_slice = match committish { - Some(c) => Some(HostedGitInfo::decode_and_append(&mut sb, c)?), - None => None, - }; - - Ok(Some(ExtractResult { - user: Some(user_slice), - project: project_slice, - committish: committish_slice, - _owned_buffer: Some(sb.move_to_slice()), - })) + Ok(Some(build_result(Some(user_part), project, committish)?)) } - pub(crate) fn bitbucket(url: &JscUrl) -> Result, HostedGitInfoError> { + /// Shared tail for hosts whose URL shape is `/user/project[.git][/aux]` + /// with the committish in the fragment: reject `aux == reject_aux`, + /// trim `.git`, require a non-empty project (and user, unless + /// `user_optional`, in which case a lone segment is the project), then + /// build the result. With `error_as_none`, allocation or decode + /// failure maps to "not a hosted git URL" rather than an error. + fn user_project_committish( + url: &JscUrl, + reject_aux: &[u8], + user_optional: bool, + error_as_none: bool, + ) -> Result, HostedGitInfoError> { let pathname_owned = url.pathname().to_owned_slice(); let pathname = strings::trim_prefix(&pathname_owned, b"/"); let mut iter = pathname.split(|&b| b == b'/'); - let Some(user_part) = iter.next() else { + let Some(mut user_part) = iter.next() else { return Ok(None); }; - let Some(project_part) = iter.next() else { + let mut project_part = iter.next(); + + if iter.next() == Some(reject_aux) { return Ok(None); - }; - let aux = iter.next(); + } - if let Some(a) = aux { - if a == b"get" { - return Ok(None); - } + if user_optional && project_part.is_none_or(<[u8]>::is_empty) { + project_part = Some(user_part); + user_part = b""; } + let Some(project_part) = project_part else { + return Ok(None); + }; let project = strings::trim_suffix(project_part, b".git"); - - if user_part.is_empty() || project.is_empty() { + if project.is_empty() { return Ok(None); } + let user: Option<&[u8]> = if user_part.is_empty() { + if !user_optional { + return Ok(None); + } + None + } else { + Some(user_part) + }; let fragment_str = OwnedString::new(url.fragment_identifier()); let fragment_utf8 = fragment_str.to_utf8(); @@ -1445,28 +1472,17 @@ pub mod formatters { None }; - let mut sb = StringBuilder::default(); - sb.count(user_part); - sb.count(project); - if let Some(c) = committish { - sb.count(c); + match build_result(user, project, committish) { + Ok(result) => Ok(Some(result)), + Err(_) if error_as_none => Ok(None), + Err(err) => Err(err), } + } - sb.allocate()?; - - let user_slice = HostedGitInfo::decode_and_append(&mut sb, user_part)?; - let project_slice = HostedGitInfo::decode_and_append(&mut sb, project)?; - let committish_slice = match committish { - Some(c) => Some(HostedGitInfo::decode_and_append(&mut sb, c)?), - None => None, - }; - - Ok(Some(ExtractResult { - user: Some(user_slice), - project: project_slice, - committish: committish_slice, - _owned_buffer: Some(sb.move_to_slice()), - })) + pub(crate) fn bitbucket(url: &JscUrl) -> Result, HostedGitInfoError> { + user_project_committish( + url, b"get", /* user_optional */ false, /* error_as_none */ false, + ) } pub(crate) fn gitlab(url: &JscUrl) -> Result, HostedGitInfoError> { @@ -1524,176 +1540,15 @@ pub mod formatters { } pub(crate) fn gist(url: &JscUrl) -> Result, HostedGitInfoError> { - let pathname_owned = url.pathname().to_owned_slice(); - let pathname = strings::trim_prefix(&pathname_owned, b"/"); - - let mut iter = pathname.split(|&b| b == b'/'); - let Some(mut user_part) = iter.next() else { - return Ok(None); - }; - let mut project_part = iter.next(); - let aux = iter.next(); - - if let Some(a) = aux { - if a == b"raw" { - return Ok(None); - } - } - - if project_part.is_none() || project_part.unwrap().is_empty() { - project_part = Some(user_part); - user_part = b""; - } - - let project = strings::trim_suffix(project_part.unwrap(), b".git"); - let user: Option<&[u8]> = if !user_part.is_empty() { - Some(user_part) - } else { - None - }; - - if project.is_empty() { - return Ok(None); - } - - let fragment_str = OwnedString::new(url.fragment_identifier()); - let fragment_utf8 = fragment_str.to_utf8(); - let fragment = fragment_utf8.slice(); - let committish: Option<&[u8]> = if !fragment.is_empty() { - Some(fragment) - } else { - None - }; - - let mut sb = StringBuilder::default(); - if let Some(u) = user { - sb.count(u); - } - sb.count(project); - if let Some(c) = committish { - sb.count(c); - } - - let Ok(()) = sb.allocate() else { - return Ok(None); - }; - - let user_slice = match user { - Some(u) => { - let Ok(r) = HostedGitInfo::decode_and_append(&mut sb, u) else { - return Ok(None); - }; - Some(r) - } - None => None, - }; - let Ok(project_slice) = HostedGitInfo::decode_and_append(&mut sb, project) else { - return Ok(None); - }; - let committish_slice = match committish { - Some(c) => { - let Ok(r) = HostedGitInfo::decode_and_append(&mut sb, c) else { - return Ok(None); - }; - Some(r) - } - None => None, - }; - - Ok(Some(ExtractResult { - user: user_slice, - project: project_slice, - committish: committish_slice, - _owned_buffer: Some(sb.move_to_slice()), - })) + user_project_committish( + url, b"raw", /* user_optional */ true, /* error_as_none */ true, + ) } pub(crate) fn sourcehut(url: &JscUrl) -> Result, HostedGitInfoError> { - let pathname_owned = url.pathname().to_owned_slice(); - let pathname = strings::trim_prefix(&pathname_owned, b"/"); - - let mut iter = pathname.split(|&b| b == b'/'); - let Some(user_part) = iter.next() else { - return Ok(None); - }; - let Some(project_part) = iter.next() else { - return Ok(None); - }; - let aux = iter.next(); - - if let Some(a) = aux { - if a == b"archive" { - return Ok(None); - } - } - - let project = strings::trim_suffix(project_part, b".git"); - - if user_part.is_empty() || project.is_empty() { - return Ok(None); - } - - let fragment_str = OwnedString::new(url.fragment_identifier()); - let fragment_utf8 = fragment_str.to_utf8(); - let fragment = fragment_utf8.slice(); - let committish: Option<&[u8]> = if !fragment.is_empty() { - Some(fragment) - } else { - None - }; - - let mut sb = StringBuilder::default(); - sb.count(user_part); - sb.count(project); - if let Some(c) = committish { - sb.count(c); - } - - let Ok(()) = sb.allocate() else { - return Ok(None); - }; - - // Inline percent-decode rather than `decode_and_append`: this path - // returns None instead of erroring on decode failure. - let user_slice = 'blk: { - let start = sb.len; - let writable = sb.writable(); - let Ok(decoded_len) = PercentEncoding::decode_into(writable, user_part) else { - return Ok(None); - }; - let decoded_len = decoded_len as usize; - sb.len += decoded_len; - break 'blk start..start + decoded_len; - }; - let project_slice = 'blk: { - let start = sb.len; - let writable = sb.writable(); - let Ok(decoded_len) = PercentEncoding::decode_into(writable, project) else { - return Ok(None); - }; - let decoded_len = decoded_len as usize; - sb.len += decoded_len; - break 'blk start..start + decoded_len; - }; - let committish_slice = if let Some(c) = committish { - let start = sb.len; - let writable = sb.writable(); - let Ok(decoded_len) = PercentEncoding::decode_into(writable, c) else { - return Ok(None); - }; - let decoded_len = decoded_len as usize; - sb.len += decoded_len; - Some(start..start + decoded_len) - } else { - None - }; - - Ok(Some(ExtractResult { - user: Some(user_slice), - project: project_slice, - committish: committish_slice, - _owned_buffer: Some(sb.move_to_slice()), - })) + user_project_committish( + url, b"archive", /* user_optional */ false, /* error_as_none */ true, + ) } } @@ -1721,26 +1576,16 @@ pub mod formatters { committish: Option<&[u8]>, ) -> Result, AllocError> { requires_user(user); - - let auth_str: &[u8] = auth.unwrap_or(b""); - let auth_sep: &[u8] = if !auth_str.is_empty() { b"@" } else { b"" }; - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "git://{}{}{}/{}/{}.git{}{}", - BStr::new(auth_str), - BStr::new(auth_sep), - BStr::new(self_.domain()), - BStr::new(user.unwrap()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "git://", + auth, + self_.domain(), + "/", + user, + project, + ".git", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } pub(crate) fn gist( @@ -1750,20 +1595,16 @@ pub mod formatters { project: &[u8], committish: Option<&[u8]>, ) -> Result, AllocError> { - let cmsh: &[u8] = committish.unwrap_or(b""); - let cmsh_sep: &[u8] = if !cmsh.is_empty() { b"#" } else { b"" }; - - let mut v = Vec::new(); - write!( - &mut v, - "git://{}/{}.git{}{}", - BStr::new(self_.domain()), - BStr::new(project), - BStr::new(cmsh_sep), - BStr::new(cmsh), + format_template( + "git://", + None, + self_.domain(), + "/", + None, + project, + ".git", + committish, ) - .map_err(|_| AllocError)?; - Ok(v) } } } diff --git a/src/install/isolated_install.rs b/src/install/isolated_install.rs index 1b3242e965c0..d56490630706 100644 --- a/src/install/isolated_install.rs +++ b/src/install/isolated_install.rs @@ -2404,6 +2404,29 @@ pub(crate) fn install_isolated_packages( let dep = &lockfile_ro.buffers.dependencies[dep_id as usize]; + // Shared failure path for the enqueue-for-download arms + // below; the caller `continue`s after invoking it. + let fail_enqueue = |installer: &mut store::Installer, err, what| { + Output::err( + err, + "failed to enqueue {} for download: {}@{}", + ( + what, + BStr::new(pkg_name.slice(string_buf)), + pkg_res.fmt(string_buf, bun_fmt::PathSep::Auto), + ), + ); + Output::flush(); + if installer.manager().options.enable.fail_early() { + Global::exit(1); + } + // .monotonic is okay because an error means the task isn't + // running on another thread. + entry_steps[entry_id.get() as usize] + .store(installer::Step::Done as u32, Ordering::Relaxed); + installer.on_task_complete(entry_id, installer::CompleteState::Fail); + }; + match pkg_res_tag { ResolutionTag::Npm => { match installer.manager_mut().enqueue_package_for_download( @@ -2421,24 +2444,7 @@ pub(crate) fn install_isolated_packages( } Err(err) => { // error.InvalidURL - Output::err( - err, - "failed to enqueue package for download: {}@{}", - ( - BStr::new(pkg_name.slice(string_buf)), - pkg_res.fmt(string_buf, bun_fmt::PathSep::Auto), - ), - ); - Output::flush(); - if installer.manager().options.enable.fail_early() { - Global::exit(1); - } - // .monotonic is okay because an error means the task isn't - // running on another thread. - entry_steps[entry_id.get() as usize] - .store(installer::Step::Done as u32, Ordering::Relaxed); - installer - .on_task_complete(entry_id, installer::CompleteState::Fail); + fail_enqueue(&mut installer, err, "package"); continue; } } @@ -2470,24 +2476,7 @@ pub(crate) fn install_isolated_packages( bun_core::out_of_memory() } Err(err) => { - Output::err( - err, - "failed to enqueue github package for download: {}@{}", - ( - BStr::new(pkg_name.slice(string_buf)), - pkg_res.fmt(string_buf, bun_fmt::PathSep::Auto), - ), - ); - Output::flush(); - if installer.manager().options.enable.fail_early() { - Global::exit(1); - } - // .monotonic is okay because an error means the task isn't - // running on another thread. - entry_steps[entry_id.get() as usize] - .store(installer::Step::Done as u32, Ordering::Relaxed); - installer - .on_task_complete(entry_id, installer::CompleteState::Fail); + fail_enqueue(&mut installer, err, "github package"); continue; } } @@ -2514,24 +2503,7 @@ pub(crate) fn install_isolated_packages( bun_core::out_of_memory() } Err(err) => { - Output::err( - err, - "failed to enqueue tarball for download: {}@{}", - ( - BStr::new(pkg_name.slice(string_buf)), - pkg_res.fmt(string_buf, bun_fmt::PathSep::Auto), - ), - ); - Output::flush(); - if installer.manager().options.enable.fail_early() { - Global::exit(1); - } - // .monotonic is okay because an error means the task isn't - // running on another thread. - entry_steps[entry_id.get() as usize] - .store(installer::Step::Done as u32, Ordering::Relaxed); - installer - .on_task_complete(entry_id, installer::CompleteState::Fail); + fail_enqueue(&mut installer, err, "tarball"); continue; } } diff --git a/src/install/npm.rs b/src/install/npm.rs index 2f27885f2a1f..07c5bdc7d84b 100644 --- a/src/install/npm.rs +++ b/src/install/npm.rs @@ -671,6 +671,38 @@ pub(crate) fn negatable_from_json_value(value: &JSON::E::JsonV this.combine() } +/// Resets and refills `bundled_deps_set` / `bundle_all_deps` from a version's +/// `bundleDependencies` (or legacy `bundledDependencies`) field. +fn extract_bundled_deps( + version_obj: Option<&JSON::E::ObjectJSON>, + bundled_deps_set: &mut StringSet, + bundle_all_deps: &mut bool, +) -> Result<(), AllocError> { + bundled_deps_set.map.clear_retaining_capacity(); + *bundle_all_deps = false; + let Some(bundled_deps_value) = version_obj + .and_then(|o| o.get(b"bundleDependencies")) + .or_else(|| version_obj.and_then(|o| o.get(b"bundledDependencies"))) + else { + return Ok(()); + }; + match bundled_deps_value { + JSON::E::JsonValue::Boolean(boolean) => { + *bundle_all_deps = *boolean; + } + JSON::E::JsonValue::Array(arr) => { + for bundled_dep in arr.get().items() { + let Some(s) = bundled_dep.as_str() else { + continue; + }; + bundled_deps_set.insert(s)?; + } + } + _ => {} + } + Ok(()) +} + // ────────────────────────────────────────────────────────────────────────── #[repr(C)] @@ -2023,7 +2055,7 @@ impl PackageManifest { let mut optional_peer_dep_names: Vec = Vec::new(); let mut bundled_deps_set = StringSet::init(); - let mut bundle_all_deps: bool; + let mut bundle_all_deps = false; let mut bundled_deps_count: usize = 0; @@ -2159,27 +2191,7 @@ impl PackageManifest { } } - bundled_deps_set.map.clear_retaining_capacity(); - bundle_all_deps = false; - if let Some(bundled_deps_value) = version_obj - .and_then(|o| o.get(b"bundleDependencies")) - .or_else(|| version_obj.and_then(|o| o.get(b"bundledDependencies"))) - { - match bundled_deps_value { - JSON::E::JsonValue::Boolean(boolean) => { - bundle_all_deps = *boolean; - } - JSON::E::JsonValue::Array(arr) => { - for bundled_dep in arr.get().items() { - let Some(s) = bundled_dep.as_str() else { - continue; - }; - bundled_deps_set.insert(s)?; - } - } - _ => {} - } - } + extract_bundled_deps(version_obj, &mut bundled_deps_set, &mut bundle_all_deps)?; for pair in &DEPENDENCY_GROUPS { if let Some(obj) = version_obj @@ -2366,27 +2378,7 @@ impl PackageManifest { let version_obj = prop.value.as_object(); - bundled_deps_set.map.clear_retaining_capacity(); - bundle_all_deps = false; - if let Some(bundled_deps_value) = version_obj - .and_then(|o| o.get(b"bundleDependencies")) - .or_else(|| version_obj.and_then(|o| o.get(b"bundledDependencies"))) - { - match bundled_deps_value { - JSON::E::JsonValue::Boolean(boolean) => { - bundle_all_deps = *boolean; - } - JSON::E::JsonValue::Array(arr) => { - for bundled_dep in arr.get().items() { - let Some(s) = bundled_dep.as_str() else { - continue; - }; - bundled_deps_set.insert(s)?; - } - } - _ => {} - } - } + extract_bundled_deps(version_obj, &mut bundled_deps_set, &mut bundle_all_deps)?; let mut package_version: PackageVersion = empty_version; diff --git a/src/install/yarn.rs b/src/install/yarn.rs index c9c8f7f0dcc9..8eba160b8bf9 100644 --- a/src/install/yarn.rs +++ b/src/install/yarn.rs @@ -1806,169 +1806,24 @@ pub(crate) fn migrate_yarn_lockfile<'a>( let deps_off = u32::try_from(this.buffers.dependencies.len()).expect("int cast"); let resolutions_off = u32::try_from(this.buffers.resolutions.len()).expect("int cast"); - if let Some(deps) = &entry.dependencies { - for (dep_name_key, dep_version_ref) in deps.iter() { - let dep_name: &[u8] = dep_name_key.as_ref(); - let dep_version_literal: &[u8] = *dep_version_ref; - - let name_hash = string_hash(dep_name); - let dep_name_string = sbuf!().append_with_hash(dep_name, name_hash)?; - let dep_version_string = sbuf!().append(dep_version_literal)?; - let sliced_string = SlicedString::init( - dep_version_string.slice(this.buffers.string_bytes.as_slice()), - dep_version_string.slice(this.buffers.string_bytes.as_slice()), - ); - - let mut parsed_version = Dependency::parse( - dep_name_string, - Some(name_hash), - dep_version_string.slice(this.buffers.string_bytes.as_slice()), - &sliced_string, - Some(&mut *log), - Some(&mut *manager), - ) - .unwrap_or_default(); - - parsed_version.literal = dep_version_string; - - this.buffers.dependencies.push(Dependency { - name: dep_name_string, - name_hash, - version: parsed_version, - behavior: dependency::Behavior::PROD, - }); - - let mut dep_spec = Vec::new(); - write!( - &mut dep_spec, - "{}@{}", - bstr::BStr::new(dep_name), - bstr::BStr::new(dep_version_literal) - ) - .expect("unreachable"); - - if let Some(res_pkg_id) = spec_to_package_id.get(dep_spec.as_slice()).copied() { - this.buffers.resolutions.push(res_pkg_id); - } else { - this.buffers.resolutions.push(install::INVALID_PACKAGE_ID); - } - - dep_count += 1; - } - } - - if let Some(optional_deps) = &entry.optional_dependencies { - for (dep_name_key, dep_version_ref) in optional_deps.iter() { - let dep_name: &[u8] = dep_name_key.as_ref(); - let dep_version_literal: &[u8] = *dep_version_ref; - - let name_hash = string_hash(dep_name); - let dep_name_string = sbuf!().append_with_hash(dep_name, name_hash)?; - - let dep_version_string = sbuf!().append(dep_version_literal)?; - let sliced_string = SlicedString::init( - dep_version_string.slice(this.buffers.string_bytes.as_slice()), - dep_version_string.slice(this.buffers.string_bytes.as_slice()), - ); - - let mut parsed_version = Dependency::parse( - dep_name_string, - Some(name_hash), - dep_version_string.slice(this.buffers.string_bytes.as_slice()), - &sliced_string, - Some(&mut *log), - Some(&mut *manager), - ) - .unwrap_or_default(); - - parsed_version.literal = dep_version_string; - - this.buffers.dependencies.push(Dependency { - name: dep_name_string, - name_hash, - version: parsed_version, - behavior: dependency::Behavior::OPTIONAL, - }); - - let mut dep_spec = Vec::new(); - write!( - &mut dep_spec, - "{}@{}", - bstr::BStr::new(dep_name), - bstr::BStr::new(dep_version_literal) - ) - .expect("unreachable"); - - if let Some(res_pkg_id) = spec_to_package_id.get(dep_spec.as_slice()).copied() { - this.buffers.resolutions.push(res_pkg_id); - } else { - this.buffers.resolutions.push(install::INVALID_PACKAGE_ID); - } - - dep_count += 1; - } - } - - if let Some(peer_deps) = &entry.peer_dependencies { - for (dep_name_key, dep_version_ref) in peer_deps.iter() { - let dep_name: &[u8] = dep_name_key.as_ref(); - let dep_version_literal: &[u8] = *dep_version_ref; - - let name_hash = string_hash(dep_name); - let dep_name_string = sbuf!().append_with_hash(dep_name, name_hash)?; - - let dep_version_string = sbuf!().append(dep_version_literal)?; - let sliced_string = SlicedString::init( - dep_version_string.slice(this.buffers.string_bytes.as_slice()), - dep_version_string.slice(this.buffers.string_bytes.as_slice()), - ); - - let mut parsed_version = Dependency::parse( - dep_name_string, - Some(name_hash), - dep_version_string.slice(this.buffers.string_bytes.as_slice()), - &sliced_string, - Some(&mut *log), - Some(&mut *manager), - ) - .unwrap_or_default(); - - parsed_version.literal = dep_version_string; - - this.buffers.dependencies.push(Dependency { - name: dep_name_string, - name_hash, - version: parsed_version, - behavior: dependency::Behavior::PEER, - }); - - let mut dep_spec = Vec::new(); - write!( - &mut dep_spec, - "{}@{}", - bstr::BStr::new(dep_name), - bstr::BStr::new(dep_version_literal) - ) - .expect("unreachable"); - - if let Some(res_pkg_id) = spec_to_package_id.get(dep_spec.as_slice()).copied() { - this.buffers.resolutions.push(res_pkg_id); - } else { - this.buffers.resolutions.push(install::INVALID_PACKAGE_ID); - } - - dep_count += 1; - } - } + let dep_groups = [ + (entry.dependencies.as_ref(), dependency::Behavior::PROD), + ( + entry.optional_dependencies.as_ref(), + dependency::Behavior::OPTIONAL, + ), + (entry.peer_dependencies.as_ref(), dependency::Behavior::PEER), + (entry.dev_dependencies.as_ref(), dependency::Behavior::DEV), + ]; - if let Some(dev_deps) = &entry.dev_dependencies { - for (dep_name_key, dep_version_ref) in dev_deps.iter() { + for (deps, behavior) in dep_groups { + let Some(deps) = deps else { continue }; + for (dep_name_key, dep_version_ref) in deps.iter() { let dep_name: &[u8] = dep_name_key.as_ref(); let dep_version_literal: &[u8] = *dep_version_ref; let name_hash = string_hash(dep_name); let dep_name_string = sbuf!().append_with_hash(dep_name, name_hash)?; - let dep_version_string = sbuf!().append(dep_version_literal)?; let sliced_string = SlicedString::init( dep_version_string.slice(this.buffers.string_bytes.as_slice()), @@ -1991,7 +1846,7 @@ pub(crate) fn migrate_yarn_lockfile<'a>( name: dep_name_string, name_hash, version: parsed_version, - behavior: dependency::Behavior::DEV, + behavior, }); let mut dep_spec = Vec::new(); diff --git a/src/io/PipeWriter.rs b/src/io/PipeWriter.rs index e9a80c6cb7cd..ac3edafa1311 100644 --- a/src/io/PipeWriter.rs +++ b/src/io/PipeWriter.rs @@ -2385,44 +2385,56 @@ impl WindowsStreamingWriter { } } + /// Blocking write for the `Source::SyncFile` path. Encodes via + /// `write_or_fallback`, then loops `sys::write` until drained. + #[inline] + fn write_sync_file( + &mut self, + buffer_u8: Option<&[u8]>, + buffer_u16: Option<&[u16]>, + kind: WriteKind, + ) -> WriteResult { + let fd = Fd::from_uv(match &self.source { + Some(Source::SyncFile(f)) => f.file, + _ => unreachable!(), + }); + let result = (|| { + let remain = match self.outgoing.write_or_fallback(buffer_u8, buffer_u16, kind) { + Ok(r) => r, + Err(_) => return WriteResult::Err(sys::Error::oom()), + }; + let initial_len = remain.len(); + let mut remain = remain; + + while remain.len() > 0 { + match sys::write(fd, remain) { + sys::Result::Err(err) => return WriteResult::Err(err), + sys::Result::Ok(wrote) => { + remain = &remain[wrote..]; + if wrote == 0 { + break; + } + } + } + } + + let wrote = initial_len - remain.len(); + if wrote == 0 { + return WriteResult::Done(wrote); + } + WriteResult::Wrote(wrote) + })(); + self.outgoing.reset(); + result + } + fn write_internal_u8(&mut self, buffer: &[u8], kind: WriteKind) -> WriteResult { if self.is_done { return WriteResult::Done(0); } if matches!(self.source, Some(Source::SyncFile(_))) { - let result = (|| { - let remain = match self.outgoing.write_or_fallback(Some(buffer), None, kind) { - Ok(r) => r, - Err(_) => return WriteResult::Err(sys::Error::oom()), - }; - let initial_len = remain.len(); - let mut remain = remain; - let fd = Fd::from_uv(match &self.source { - Some(Source::SyncFile(f)) => f.file, - _ => unreachable!(), - }); - - while remain.len() > 0 { - match sys::write(fd, remain) { - sys::Result::Err(err) => return WriteResult::Err(err), - sys::Result::Ok(wrote) => { - remain = &remain[wrote..]; - if wrote == 0 { - break; - } - } - } - } - - let wrote = initial_len - remain.len(); - if wrote == 0 { - return WriteResult::Done(wrote); - } - WriteResult::Wrote(wrote) - })(); - self.outgoing.reset(); - return result; + return self.write_sync_file(Some(buffer), None, kind); } let had_buffered_data = self.outgoing.is_not_empty(); @@ -2447,42 +2459,7 @@ impl WindowsStreamingWriter { } if matches!(self.source, Some(Source::SyncFile(_))) { - let result = (|| { - let remain = - match self - .outgoing - .write_or_fallback(None, Some(buffer), WriteKind::Utf16) - { - Ok(r) => r, - Err(_) => return WriteResult::Err(sys::Error::oom()), - }; - let initial_len = remain.len(); - let mut remain = remain; - let fd = Fd::from_uv(match &self.source { - Some(Source::SyncFile(f)) => f.file, - _ => unreachable!(), - }); - - while remain.len() > 0 { - match sys::write(fd, remain) { - sys::Result::Err(err) => return WriteResult::Err(err), - sys::Result::Ok(wrote) => { - remain = &remain[wrote..]; - if wrote == 0 { - break; - } - } - } - } - - let wrote = initial_len - remain.len(); - if wrote == 0 { - return WriteResult::Done(wrote); - } - WriteResult::Wrote(wrote) - })(); - self.outgoing.reset(); - return result; + return self.write_sync_file(None, Some(buffer), WriteKind::Utf16); } let had_buffered_data = self.outgoing.is_not_empty(); diff --git a/src/io/posix_event_loop.rs b/src/io/posix_event_loop.rs index 8f36b301a9a7..dd6f9c744b03 100644 --- a/src/io/posix_event_loop.rs +++ b/src/io/posix_event_loop.rs @@ -2,7 +2,6 @@ use core::ffi::c_int; use core::ffi::c_void; use core::fmt; -#[cfg(unix)] use core::ptr; #[cfg(not(windows))] @@ -289,6 +288,108 @@ pub enum AllocatorType { Mini, } +/// Pure flag-accessor methods whose bodies are identical for the POSIX and +/// Windows `FilePoll` types (distinct structs — see `windows_event_loop`). +/// Expanded inside each platform's `impl FilePoll`; `Flags` and `Fd` resolve +/// at the expansion site. Platform-divergent methods (the keep-alive / +/// activate family) stay in their own modules — their flag semantics differ. +macro_rules! impl_file_poll_flag_methods { + () => { + #[inline] + pub fn is_active(&self) -> bool { + self.flags.contains(Flags::HasIncrementedPollCount) + } + + #[inline] + pub fn is_watching(&self) -> bool { + !self.flags.contains(Flags::NeedsRearm) + && (self.flags.contains(Flags::PollReadable) + || self.flags.contains(Flags::PollWritable) + || self.flags.contains(Flags::PollProcess)) + } + + pub fn is_registered(&self) -> bool { + self.flags.contains(Flags::PollWritable) + || self.flags.contains(Flags::PollReadable) + || self.flags.contains(Flags::PollProcess) + || self.flags.contains(Flags::PollMachport) + || self.flags.contains(Flags::PollMemoryPressure) + } + + pub fn clear_event(&mut self, flag: Flags) { + self.flags.remove(flag); + } + + pub fn is_readable(&mut self) -> bool { + let readable = self.flags.contains(Flags::Readable); + self.flags.remove(Flags::Readable); + readable + } + + pub fn is_hup(&mut self) -> bool { + let readable = self.flags.contains(Flags::Hup); + self.flags.remove(Flags::Hup); + readable + } + + pub fn is_eof(&mut self) -> bool { + let readable = self.flags.contains(Flags::Eof); + self.flags.remove(Flags::Eof); + readable + } + + pub fn is_writable(&mut self) -> bool { + let readable = self.flags.contains(Flags::Writable); + self.flags.remove(Flags::Writable); + readable + } + + #[inline] + pub fn can_unref(&self) -> bool { + self.flags.contains(Flags::HasIncrementedPollCount) + } + + #[inline] + pub fn file_descriptor(&self) -> Fd { + self.fd + } + }; +} +// Only `windows_event_loop` needs the path-based re-export; this module +// invokes the macros textually. +#[cfg(windows)] +pub(crate) use impl_file_poll_flag_methods; + +/// `PollSlot` impl shared verbatim by both platform `FilePoll` types; both +/// have the `next_to_free` / `flags` fields the trait contract requires. +macro_rules! impl_poll_slot { + ($t:ty) => { + impl $crate::posix_event_loop::PollSlot for $t { + #[inline] + unsafe fn next_to_free(p: *mut Self) -> *mut Self { + // SAFETY: caller upholds the trait-level contract (`p` is a live hive + // slot; raw-pointer field op only). + unsafe { (*p).next_to_free } + } + #[inline] + unsafe fn set_next_to_free(p: *mut Self, next: *mut Self) { + // SAFETY: caller upholds the trait-level contract. + unsafe { (*p).next_to_free = next } + } + #[inline] + unsafe fn ignore_updates(p: *mut Self) { + // SAFETY: caller upholds the trait-level contract. + unsafe { + (*p).flags + .insert($crate::posix_event_loop::Flags::IgnoreUpdates) + }; + } + } + }; +} +#[cfg(windows)] +pub(crate) use impl_poll_slot; + // `FilePoll`/`Store` here are POSIX-specific (kqueue/epoll registration, // generation_number, allocator_type). On Windows the variants live in // `windows_event_loop`; the shared `EventLoopCtxVTable` above names @@ -381,33 +482,7 @@ impl FilePoll { self.on_update(0); } - pub fn clear_event(&mut self, flag: Flags) { - self.flags.remove(flag); - } - - pub fn is_readable(&mut self) -> bool { - let readable = self.flags.contains(Flags::Readable); - self.flags.remove(Flags::Readable); - readable - } - - pub fn is_hup(&mut self) -> bool { - let readable = self.flags.contains(Flags::Hup); - self.flags.remove(Flags::Hup); - readable - } - - pub fn is_eof(&mut self) -> bool { - let readable = self.flags.contains(Flags::Eof); - self.flags.remove(Flags::Eof); - readable - } - - pub fn is_writable(&mut self) -> bool { - let readable = self.flags.contains(Flags::Writable); - self.flags.remove(Flags::Writable); - readable - } + impl_file_poll_flag_methods!(); // Note: not `impl Drop` — FilePoll is pool-allocated (HiveArray) and explicitly // put back via `Store::put`; Drop would be wrong here. @@ -448,14 +523,6 @@ impl FilePoll { self.deinit_possibly_defer(vm, false); } - pub fn is_registered(&self) -> bool { - self.flags.contains(Flags::PollWritable) - || self.flags.contains(Flags::PollReadable) - || self.flags.contains(Flags::PollProcess) - || self.flags.contains(Flags::PollMachport) - || self.flags.contains(Flags::PollMemoryPressure) - } - pub fn on_update(&mut self, size_or_offset: i64) { if self.flags.contains(Flags::OneShot) && !self.flags.contains(Flags::NeedsRearm) { self.flags.insert(Flags::NeedsRearm); @@ -471,19 +538,6 @@ impl FilePoll { unsafe { __bun_run_file_poll(self, size_or_offset) }; } - #[inline] - pub fn is_active(&self) -> bool { - self.flags.contains(Flags::HasIncrementedPollCount) - } - - #[inline] - pub fn is_watching(&self) -> bool { - !self.flags.contains(Flags::NeedsRearm) - && (self.flags.contains(Flags::PollReadable) - || self.flags.contains(Flags::PollWritable) - || self.flags.contains(Flags::PollProcess)) - } - /// This decrements the active counter if it was previously incremented /// "active" controls whether or not the event loop should potentially idle pub fn disable_keeping_process_alive(&mut self, event_loop_ctx: EventLoopCtx) { @@ -598,11 +652,6 @@ impl FilePoll { !self.flags.contains(Flags::HasIncrementedPollCount) } - #[inline] - pub fn can_unref(&self) -> bool { - self.flags.contains(Flags::HasIncrementedPollCount) - } - /// Prevent a poll from keeping the process alive. pub fn unref(&mut self, event_loop_ctx: EventLoopCtx) { syslog!("unref"); @@ -626,11 +675,6 @@ impl FilePoll { self.deactivate(event_loop_ctx.loop_mut()); } - #[inline] - pub fn file_descriptor(&self) -> Fd { - self.fd - } - pub fn register(&mut self, loop_: &mut Loop, flag: Flags, one_shot: bool) -> sys::Result<()> { self.register_with_fd( loop_, @@ -1401,25 +1445,49 @@ impl fmt::Display for FlagsFormatter { // `bun_alloc::heap_breakdown` is a no-op outside macOS Instruments // heap-breakdown builds, so the 128-slot hive is unconditional here (same // choice as `RuntimeTranspilerStore`'s TranspilerJob hive). -#[cfg(not(windows))] const HIVE_SIZE: usize = 128; + +/// Raw-pointer field accessors implemented by the platform `FilePoll` types so +/// the deferred-free [`PollStore`] can link slots intrusively. +/// +/// Every method shares one contract: `p` is a live, fully-initialized hive +/// slot, and the body performs raw-pointer field ops only — materializing a +/// `&mut Self` would alias the `&mut PollStore` borrow that covers the inline +/// hive buffer (Stacked Borrows UB). +pub trait PollSlot: Sized { + /// # Safety + /// See the trait-level contract. + unsafe fn next_to_free(p: *mut Self) -> *mut Self; + /// # Safety + /// See the trait-level contract. + unsafe fn set_next_to_free(p: *mut Self, next: *mut Self); + /// Insert `Flags::IgnoreUpdates`. + /// # Safety + /// See the trait-level contract. + unsafe fn ignore_updates(p: *mut Self); +} + #[cfg(not(windows))] -type FilePollHive = bun_collections::hive_array::Fallback; +pub type Store = PollStore; + +#[cfg(not(windows))] +impl_poll_slot!(FilePoll); /// We defer freeing FilePoll until the end of the next event loop iteration /// This ensures that we don't free a FilePoll before the next callback is called -#[cfg(not(windows))] -pub struct Store { - hive: FilePollHive, - pending_free_head: *mut FilePoll, - pending_free_tail: *mut FilePoll, +pub struct PollStore { + hive: bun_collections::hive_array::Fallback, + pending_free_head: *mut P, + pending_free_tail: *mut P, } -#[cfg(not(windows))] -impl Store { - pub fn init() -> Store { - Store { - hive: FilePollHive::init(), +impl PollStore

{ + pub fn init() -> Self { + // `hive.put` recycles slots without honoring drop glue on the deferred + // path's assumptions; the store requires plain-old-data slots. + const { assert!(!core::mem::needs_drop::

()) }; + PollStore { + hive: bun_collections::hive_array::Fallback::init(), pending_free_head: ptr::null_mut(), pending_free_tail: ptr::null_mut(), } @@ -1427,7 +1495,7 @@ impl Store { /// Claim a hive slot and move `value` into it. Infallible (heap fallback). #[inline] - pub fn get_init(&mut self, value: FilePoll) -> ptr::NonNull { + pub fn get_init(&mut self, value: P) -> ptr::NonNull

{ self.hive.get_init(value) } @@ -1436,13 +1504,14 @@ impl Store { while !next.is_null() { let current = next; // SAFETY: intrusive list; nodes were allocated by this hive. Walk via - // raw-pointer reads/writes only — materializing a `&mut FilePoll` + // raw-pointer reads/writes only — materializing a `&mut P` // here would alias the `&mut self.hive` borrow taken by `put()` // below (the slot may live inside the inline hive array). unsafe { - next = (*current).next_to_free; - (*current).next_to_free = ptr::null_mut(); - // FilePoll has no drop glue; `put` is a no-op drop + recycle. + next = P::next_to_free(current); + P::set_next_to_free(current, ptr::null_mut()); + // `P` has no drop glue (asserted in `init`); `put` is a no-op + // drop + recycle. self.hive.put(current); } } @@ -1451,29 +1520,29 @@ impl Store { } /// `poll` is a live, fully-initialized slot in `self.hive`. It may point - /// *inside* `self.hive`'s inline `[FilePoll; 128]` buffer, so accepting it - /// as `&mut FilePoll` while `&mut self` is live would retag overlapping + /// *inside* `self.hive`'s inline `[P; 128]` buffer, so accepting it + /// as `&mut P` while `&mut self` is live would retag overlapping /// storage under Stacked Borrows (UB). Take it as a raw pointer and /// touch fields only through raw pointer ops — same /// rationale as `process_deferred_frees` above. - pub fn put(&mut self, poll: ptr::NonNull, vm: EventLoopCtx, ever_registered: bool) { + pub fn put(&mut self, poll: ptr::NonNull

, vm: EventLoopCtx, ever_registered: bool) { let poll = poll.as_ptr(); if !ever_registered { - // SAFETY: `poll` is a fully-initialized hive slot; FilePoll has no + // SAFETY: `poll` is a fully-initialized hive slot; `P` has no // drop glue, so `put` is a no-op drop + recycle. unsafe { self.hive.put(poll) }; return; } // SAFETY: `poll` is a live hive slot (see fn-level comment); raw read of a POD field. - debug_assert!(unsafe { (*poll).next_to_free }.is_null()); + debug_assert!(unsafe { P::next_to_free(poll) }.is_null()); if !self.pending_free_tail.is_null() { debug_assert!(!self.pending_free_head.is_null()); // SAFETY: tail is non-null and points into the hive. unsafe { - debug_assert!((*self.pending_free_tail).next_to_free.is_null()); - (*self.pending_free_tail).next_to_free = poll; + debug_assert!(P::next_to_free(self.pending_free_tail).is_null()); + P::set_next_to_free(self.pending_free_tail, poll); } } @@ -1483,7 +1552,7 @@ impl Store { } // SAFETY: see fn-level comment — raw-pointer field access only. - unsafe { (*poll).flags.insert(Flags::IgnoreUpdates) }; + unsafe { P::ignore_updates(poll) }; self.pending_free_tail = poll; let callback: OpaqueCallback = Self::process_deferred_frees_thunk; @@ -1493,16 +1562,16 @@ impl Store { ); vm.set_after_event_loop_callback( Some(callback), - core::ptr::NonNull::new(std::ptr::from_mut::(self).cast::()), + core::ptr::NonNull::new(std::ptr::from_mut::(self).cast::()), ); } // Safe fn item: module-private thunk, only coerced to the C-ABI // `OpaqueCallback` fn-pointer type — never callable by name outside - // `Store`. Body wraps its raw-ptr op explicitly. + // the store. Body wraps its raw-ptr op explicitly. extern "C" fn process_deferred_frees_thunk(ctx: *mut c_void) { - // SAFETY: ctx was set to `self as *mut Store` in `put` above. - let this = unsafe { bun_ptr::callback_ctx::(ctx) }; + // SAFETY: ctx was set to `self as *mut Self` in `put` above. + let this = unsafe { bun_ptr::callback_ctx::(ctx) }; this.process_deferred_frees(); } } diff --git a/src/io/windows_event_loop.rs b/src/io/windows_event_loop.rs index 09b2416eb120..b1237902e4f0 100644 --- a/src/io/windows_event_loop.rs +++ b/src/io/windows_event_loop.rs @@ -1,4 +1,3 @@ -use core::ffi::c_void; use core::ptr; use bun_sys::Fd; @@ -40,31 +39,13 @@ pub struct FilePoll { } impl FilePoll { - #[inline] - pub fn is_active(&self) -> bool { - self.flags.contains(Flags::HasIncrementedPollCount) - } - - #[inline] - pub fn is_watching(&self) -> bool { - !self.flags.contains(Flags::NeedsRearm) - && (self.flags.contains(Flags::PollReadable) - || self.flags.contains(Flags::PollWritable) - || self.flags.contains(Flags::PollProcess)) - } + posix::impl_file_poll_flag_methods!(); #[inline] pub fn is_keeping_process_alive(&self) -> bool { !self.flags.contains(Flags::Closed) && self.is_active() } - pub fn is_registered(&self) -> bool { - self.flags.contains(Flags::PollWritable) - || self.flags.contains(Flags::PollReadable) - || self.flags.contains(Flags::PollProcess) - || self.flags.contains(Flags::PollMachport) - } - /// Make calling ref() on this poll into a no-op. pub fn disable_keeping_process_alive(&mut self, vm: EventLoopCtx) { if self.flags.contains(Flags::Closed) { @@ -102,11 +83,6 @@ impl FilePoll { self.deinit_with_vm(js_vm_ctx()); } - #[inline] - pub fn file_descriptor(&self) -> Fd { - self.fd - } - pub fn deinit_force_unregister(&mut self) { self.deinit() } @@ -150,34 +126,6 @@ impl FilePoll { vm.file_polls_mut().put(this, vm, was_ever_registered); } - pub fn is_readable(&mut self) -> bool { - let readable = self.flags.contains(Flags::Readable); - self.flags.remove(Flags::Readable); - readable - } - - pub fn is_hup(&mut self) -> bool { - let readable = self.flags.contains(Flags::Hup); - self.flags.remove(Flags::Hup); - readable - } - - pub fn is_eof(&mut self) -> bool { - let readable = self.flags.contains(Flags::Eof); - self.flags.remove(Flags::Eof); - readable - } - - pub fn clear_event(&mut self, flag: Flags) { - self.flags.remove(flag); - } - - pub fn is_writable(&mut self) -> bool { - let readable = self.flags.contains(Flags::Writable); - self.flags.remove(Flags::Writable); - readable - } - pub fn deinit_with_vm(&mut self, vm: EventLoopCtx) { // `loop_mut()` — crate-private nonnull-asref accessor (single deref in // `EventLoopCtx`); the uws loop is a disjoint allocation from `self`. @@ -233,11 +181,6 @@ impl FilePoll { !self.flags.contains(Flags::HasIncrementedPollCount) } - #[inline] - pub fn can_unref(&self) -> bool { - self.flags.contains(Flags::HasIncrementedPollCount) - } - pub fn on_ended(&mut self, event_loop_ctx: EventLoopCtx) { self.flags.remove(Flags::KeepsEventLoopAlive); self.flags.insert(Flags::Closed); @@ -267,104 +210,9 @@ impl FilePoll { } } -type FilePollHiveArray = bun_collections::hive_array::Fallback; - -pub struct Store { - hive: FilePollHiveArray, - pending_free_head: *mut FilePoll, - pending_free_tail: *mut FilePoll, -} - -impl Store { - pub fn init() -> Store { - Store { - hive: FilePollHiveArray::init(), - pending_free_head: ptr::null_mut(), - pending_free_tail: ptr::null_mut(), - } - } - - #[inline] - pub fn get_init(&mut self, value: FilePoll) -> ptr::NonNull { - self.hive.get_init(value) - } - - pub fn process_deferred_frees(&mut self) { - let mut next = self.pending_free_head; - while !next.is_null() { - let current = next; - // SAFETY: intrusive deferred-free list; nodes are valid HiveArray slots - // until put(). Walk via raw-pointer reads/writes only — materializing a - // `&mut FilePoll` here would alias the `&mut self.hive` borrow taken by - // `put()` below (the slot may live inside the inline hive buffer). - unsafe { - next = (*current).next_to_free; - (*current).next_to_free = ptr::null_mut(); - // FilePoll has no drop glue; `put` is a no-op drop + recycle. - self.hive.put(current); - } - } - self.pending_free_head = ptr::null_mut(); - self.pending_free_tail = ptr::null_mut(); - } - - /// `poll` is a live, fully-initialized slot in `self.hive`. Touched only - /// through raw pointer ops to avoid forming a `&mut FilePoll` that would - /// alias `&mut self` (the hive buffer is inline storage). - pub fn put(&mut self, poll: ptr::NonNull, vm: EventLoopCtx, ever_registered: bool) { - let poll = poll.as_ptr(); - if !ever_registered { - // SAFETY: `poll` is a fully-initialized hive slot; FilePoll has no - // drop glue, so `put` is a no-op drop + recycle. - unsafe { self.hive.put(poll) }; - return; - } - - // SAFETY: `poll` is a valid HiveArray slot pointer. It may live inside - // `self.hive.buffer`, so we access it via raw pointer only (no `&mut FilePoll` - // materialized) to avoid aliasing `&mut self`. - debug_assert!(unsafe { (*poll).next_to_free }.is_null()); - - let tail = self.pending_free_tail; - if !tail.is_null() { - debug_assert!(!self.pending_free_head.is_null()); - // SAFETY: `tail` is a valid slot in the intrusive deferred-free list; - // raw-ptr access avoids a second `&mut FilePoll` overlapping `poll`/`self`. - debug_assert!(unsafe { (*tail).next_to_free }.is_null()); - unsafe { (*tail).next_to_free = poll }; - } - - if self.pending_free_head.is_null() { - self.pending_free_head = poll; - debug_assert!(self.pending_free_tail.is_null()); - } - - // SAFETY: see above — short-lived field borrow through raw `poll`, no overlap held. - unsafe { (*poll).flags.insert(Flags::IgnoreUpdates) }; - self.pending_free_tail = poll; +pub type Store = posix::PollStore; - let callback: OpaqueCallback = Self::process_deferred_frees_thunk; - debug_assert!( - vm.after_event_loop_callback().is_none() - || vm.after_event_loop_callback().map(|f| f as usize) == Some(callback as usize) - ); - vm.set_after_event_loop_callback( - Some(callback), - core::ptr::NonNull::new(core::ptr::from_mut::(self).cast::()), - ); - } - - // Safe fn item: module-private thunk, only coerced to the C-ABI - // `OpaqueCallback` fn-pointer type — never callable by name outside - // `Store`. Body wraps its raw-ptr op explicitly. - extern "C" fn process_deferred_frees_thunk(ctx: *mut c_void) { - // SAFETY: `ctx` was set to `self as *mut Store` in `put` above. The thunk fires - // from the event loop's after-tick hook with no other `&mut Store` borrow live, - // so this is the unique accessor (safe-single-owner). - let this = unsafe { bun_ptr::callback_ctx::(ctx) }; - this.process_deferred_frees(); - } -} +posix::impl_poll_slot!(FilePoll); pub struct Waker { // `BackRef`: `WindowsLoop::get()` hands out the shared diff --git a/src/js_parser_jsc/Macro.rs b/src/js_parser_jsc/Macro.rs index 52014122bf93..8b3a31fd8f0e 100644 --- a/src/js_parser_jsc/Macro.rs +++ b/src/js_parser_jsc/Macro.rs @@ -606,9 +606,7 @@ impl<'a> Run<'a> { pub fn run(&mut self, value: JSValue) -> Result { use ConsoleObject::formatter::Tag as T; - // `Tag::get` returns `TagResult { tag: TagPayload, .. }`; - // collapse the payload to its discriminant via `.tag()`. - match T::get(value, self.global)?.tag.tag() { + match T::get(value, self.global)?.tag { T::Error => self.coerce(T::Error, value), T::Undefined => self.coerce(T::Undefined, value), T::Null => self.coerce(T::Null, value), diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index 96dae5137792..36132c632045 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -743,10 +743,8 @@ impl<'a> TablePrinter<'a> { let offset = cell_text.len(); let mut value_formatter = self.value_formatter.shallow_clone(); let tag = formatter::Tag::get(value, self.global_object)?; - value_formatter.quote_strings = !(matches!( - tag.tag, - TagPayload::String | TagPayload::StringPossiblyFormatted - )); + value_formatter.quote_strings = + !(matches!(tag.tag, Tag::String | Tag::StringPossiblyFormatted)); value_formatter.format::(tag, cell_text, value, self.global_object)?; let text = &cell_text[offset..]; @@ -1444,7 +1442,7 @@ pub fn format2( return Ok(()); } - if matches!(tag.tag, TagPayload::String) { + if matches!(tag.tag, Tag::String) { if options.enable_colors { if level == MessageLevel::Error { let _ = writer.write_all(pfmt!("", true).as_bytes()); @@ -1520,8 +1518,8 @@ pub fn format2( any = true; tag = formatter::Tag::get(this_value, global)?; - if matches!(tag.tag, TagPayload::String) && !fmt.remaining().is_empty() { - tag.tag = TagPayload::StringPossiblyFormatted; + if matches!(tag.tag, Tag::String) && !fmt.remaining().is_empty() { + tag.tag = Tag::StringPossiblyFormatted; } fmt.format::(tag, writer, this_value, global)?; @@ -1542,8 +1540,8 @@ pub fn format2( } any = true; tag = formatter::Tag::get(this_value, global)?; - if matches!(tag.tag, TagPayload::String) && !fmt.remaining().is_empty() { - tag.tag = TagPayload::StringPossiblyFormatted; + if matches!(tag.tag, Tag::String) && !fmt.remaining().is_empty() { + tag.tag = Tag::StringPossiblyFormatted; } fmt.format::(tag, writer, this_value, global)?; @@ -1572,7 +1570,7 @@ pub struct CustomFormattedObject { // Formatter // ─────────────────────────────────────────────────────────────────────────── -pub use formatter::{Formatter, Tag, TagOptions, TagPayload, TagResult, visited}; +pub use formatter::{Formatter, Tag, TagOptions, TagResult, visited}; pub mod formatter { use super::*; @@ -2014,156 +2012,20 @@ pub mod formatter { } } - /// Only `CustomFormattedObject` carries a payload. - #[derive(Copy, Clone, PartialEq, Eq)] - pub enum TagPayload { - StringPossiblyFormatted, - String, - Undefined, - Double, - Integer, - Null, - Boolean, - Array, - Object, - Function, - Class, - Error, - TypedArray, - Map, - MapIterator, - SetIterator, - Set, - BigInt, - Symbol, - CustomFormattedObject(CustomFormattedObject), - GlobalObject, - Private, - Promise, - JSON, - ToJSON, - NativeCode, - JSX, - Event, - GetterSetter, - CustomGetterSetter, - Proxy, - RevokedProxy, - } - - impl TagPayload { - /// The constructor lives here as well as on the bare - /// discriminant `Tag`. Callers in sibling modules use either name. - #[inline] - pub fn get(value: JSValue, global_this: &JSGlobalObject) -> JsResult { - Tag::get(value, global_this) - } - /// Delegates to `Tag::get_advanced`. - #[inline] - pub fn get_advanced( - value: JSValue, - global_this: &JSGlobalObject, - opts: TagOptions, - ) -> JsResult { - Tag::get_advanced(value, global_this, opts) - } - pub fn is_primitive(self) -> bool { - self.tag().is_primitive() - } - pub fn tag(self) -> Tag { - match self { - TagPayload::StringPossiblyFormatted => Tag::StringPossiblyFormatted, - TagPayload::String => Tag::String, - TagPayload::Undefined => Tag::Undefined, - TagPayload::Double => Tag::Double, - TagPayload::Integer => Tag::Integer, - TagPayload::Null => Tag::Null, - TagPayload::Boolean => Tag::Boolean, - TagPayload::Array => Tag::Array, - TagPayload::Object => Tag::Object, - TagPayload::Function => Tag::Function, - TagPayload::Class => Tag::Class, - TagPayload::Error => Tag::Error, - TagPayload::TypedArray => Tag::TypedArray, - TagPayload::Map => Tag::Map, - TagPayload::MapIterator => Tag::MapIterator, - TagPayload::SetIterator => Tag::SetIterator, - TagPayload::Set => Tag::Set, - TagPayload::BigInt => Tag::BigInt, - TagPayload::Symbol => Tag::Symbol, - TagPayload::CustomFormattedObject(_) => Tag::CustomFormattedObject, - TagPayload::GlobalObject => Tag::GlobalObject, - TagPayload::Private => Tag::Private, - TagPayload::Promise => Tag::Promise, - TagPayload::JSON => Tag::JSON, - TagPayload::ToJSON => Tag::ToJSON, - TagPayload::NativeCode => Tag::NativeCode, - TagPayload::JSX => Tag::JSX, - TagPayload::Event => Tag::Event, - TagPayload::GetterSetter => Tag::GetterSetter, - TagPayload::CustomGetterSetter => Tag::CustomGetterSetter, - TagPayload::Proxy => Tag::Proxy, - TagPayload::RevokedProxy => Tag::RevokedProxy, - } - } - } - - /// Reverse of [`TagPayload::tag`]. The `CustomFormattedObject` arm gets a - /// default (zero) payload — used by the `ConsoleFormatter` trait bridge in - /// `lib.rs`, which never passes that tag (write_format hooks pick concrete - /// tags like `Double` / `Boolean` / `Object` / `Private`). - impl From for TagPayload { - fn from(t: Tag) -> Self { - match t { - Tag::StringPossiblyFormatted => TagPayload::StringPossiblyFormatted, - Tag::String => TagPayload::String, - Tag::Undefined => TagPayload::Undefined, - Tag::Double => TagPayload::Double, - Tag::Integer => TagPayload::Integer, - Tag::Null => TagPayload::Null, - Tag::Boolean => TagPayload::Boolean, - Tag::Array => TagPayload::Array, - Tag::Object => TagPayload::Object, - Tag::Function => TagPayload::Function, - Tag::Class => TagPayload::Class, - Tag::Error => TagPayload::Error, - Tag::TypedArray => TagPayload::TypedArray, - Tag::Map => TagPayload::Map, - Tag::MapIterator => TagPayload::MapIterator, - Tag::SetIterator => TagPayload::SetIterator, - Tag::Set => TagPayload::Set, - Tag::BigInt => TagPayload::BigInt, - Tag::Symbol => TagPayload::Symbol, - Tag::CustomFormattedObject => { - TagPayload::CustomFormattedObject(CustomFormattedObject::default()) - } - Tag::GlobalObject => TagPayload::GlobalObject, - Tag::Private => TagPayload::Private, - Tag::Promise => TagPayload::Promise, - Tag::JSON => TagPayload::JSON, - Tag::ToJSON => TagPayload::ToJSON, - Tag::NativeCode => TagPayload::NativeCode, - Tag::JSX => TagPayload::JSX, - Tag::Event => TagPayload::Event, - Tag::GetterSetter => TagPayload::GetterSetter, - Tag::CustomGetterSetter => TagPayload::CustomGetterSetter, - Tag::Proxy => TagPayload::Proxy, - Tag::RevokedProxy => TagPayload::RevokedProxy, - } - } - } - #[derive(Copy, Clone)] pub struct TagResult { - pub tag: TagPayload, + pub tag: Tag, pub cell: jsc::JSType, + /// Set only when `tag` is [`Tag::CustomFormattedObject`]. + pub custom: Option, } impl Default for TagResult { fn default() -> Self { Self { - tag: TagPayload::Undefined, + tag: Tag::Undefined, cell: jsc::JSType::Cell, + custom: None, } } } @@ -2190,37 +2052,37 @@ pub mod formatter { ) -> JsResult { if value.is_empty() || value == JSValue::UNDEFINED { return Ok(TagResult { - tag: TagPayload::Undefined, + tag: Tag::Undefined, ..Default::default() }); } if value == JSValue::NULL { return Ok(TagResult { - tag: TagPayload::Null, + tag: Tag::Null, ..Default::default() }); } if value.is_int32() { return Ok(TagResult { - tag: TagPayload::Integer, + tag: Tag::Integer, ..Default::default() }); } else if value.is_number() { return Ok(TagResult { - tag: TagPayload::Double, + tag: Tag::Double, ..Default::default() }); } else if value.is_boolean() { return Ok(TagResult { - tag: TagPayload::Boolean, + tag: Tag::Boolean, ..Default::default() }); } if !value.is_cell() { return Ok(TagResult { - tag: TagPayload::NativeCode, + tag: Tag::NativeCode, ..Default::default() }); } @@ -2229,15 +2091,17 @@ pub mod formatter { if js_type.is_hidden() { return Ok(TagResult { - tag: TagPayload::NativeCode, + tag: Tag::NativeCode, cell: js_type, + custom: None, }); } if js_type == jsc::JSType::Cell { return Ok(TagResult { - tag: TagPayload::NativeCode, + tag: Tag::NativeCode, cell: js_type, + custom: None, }); } @@ -2249,17 +2113,18 @@ pub mod formatter { match value.fast_get(global_this, jsc::BuiltinName::InspectCustom) { Err(_) => { return Ok(TagResult { - tag: TagPayload::RevokedProxy, + tag: Tag::RevokedProxy, ..Default::default() }); } Ok(Some(callback_value)) if callback_value.is_callable() => { return Ok(TagResult { - tag: TagPayload::CustomFormattedObject(CustomFormattedObject { + tag: Tag::CustomFormattedObject, + cell: js_type, + custom: Some(CustomFormattedObject { function: callback_value, this: value, }), - cell: js_type, }); } _ => {} @@ -2268,8 +2133,9 @@ pub mod formatter { if js_type == jsc::JSType::DOMWrapper { return Ok(TagResult { - tag: TagPayload::Private, + tag: Tag::Private, cell: js_type, + custom: None, }); } @@ -2280,8 +2146,9 @@ pub mod formatter { { if value.is_class(global_this) { return Ok(TagResult { - tag: TagPayload::Class, + tag: Tag::Class, cell: js_type, + custom: None, }); } @@ -2293,11 +2160,12 @@ pub mod formatter { // handle the prefix in the .Object formatter. return Ok(TagResult { tag: if js_type == jsc::JSType::InternalFunction { - TagPayload::Object + Tag::Object } else { - TagPayload::Function + Tag::Function }, cell: js_type, + custom: None, }); } @@ -2306,8 +2174,9 @@ pub mod formatter { return Tag::get(value.get_proxy_target(), global_this); } return Ok(TagResult { - tag: TagPayload::GlobalObject, + tag: Tag::GlobalObject, cell: js_type, + custom: None, }); } @@ -2332,8 +2201,9 @@ pub mod formatter { global_this, )? { return Ok(TagResult { - tag: TagPayload::JSX, + tag: Tag::JSX, cell: js_type, + custom: None, }); } } @@ -2341,24 +2211,24 @@ pub mod formatter { use jsc::JSType as T; let tag = match js_type { - T::ErrorInstance => TagPayload::Error, - T::NumberObject => TagPayload::Double, + T::ErrorInstance => Tag::Error, + T::NumberObject => Tag::Double, T::DerivedArray | T::Array | T::DirectArguments | T::ScopedArguments - | T::ClonedArguments => TagPayload::Array, - T::DerivedStringObject | T::String | T::StringObject => TagPayload::String, - T::RegExpObject => TagPayload::String, - T::Symbol => TagPayload::Symbol, - T::BooleanObject => TagPayload::Boolean, - T::JSFunction => TagPayload::Function, - T::WeakMap | T::Map => TagPayload::Map, - T::MapIterator => TagPayload::MapIterator, - T::SetIterator => TagPayload::SetIterator, - T::WeakSet | T::Set => TagPayload::Set, - T::JSDate => TagPayload::JSON, - T::JSPromise => TagPayload::Promise, + | T::ClonedArguments => Tag::Array, + T::DerivedStringObject | T::String | T::StringObject => Tag::String, + T::RegExpObject => Tag::String, + T::Symbol => Tag::Symbol, + T::BooleanObject => Tag::Boolean, + T::JSFunction => Tag::Function, + T::WeakMap | T::Map => Tag::Map, + T::MapIterator => Tag::MapIterator, + T::SetIterator => Tag::SetIterator, + T::WeakSet | T::Set => Tag::Set, + T::JSDate => Tag::JSON, + T::JSPromise => Tag::Promise, T::WrapForValidIterator | T::RegExpStringIterator @@ -2367,43 +2237,31 @@ pub mod formatter { | T::IteratorHelper | T::Object | T::FinalObject - | T::ModuleNamespaceObject => TagPayload::Object, + | T::ModuleNamespaceObject => Tag::Object, T::ProxyObject => { let handler = value.get_proxy_internal_field(jsc::ProxyField::Handler); if handler.is_empty() || handler.is_undefined_or_null() { return Ok(TagResult { - tag: TagPayload::RevokedProxy, + tag: Tag::RevokedProxy, cell: js_type, + custom: None, }); } - TagPayload::Proxy + Tag::Proxy } T::GlobalObject => { if !opts.contains(TagOptions::HIDE_GLOBAL) { - TagPayload::Object + Tag::Object } else { - TagPayload::GlobalObject + Tag::GlobalObject } } - T::ArrayBuffer - | T::Int8Array - | T::Uint8Array - | T::Uint8ClampedArray - | T::Int16Array - | T::Uint16Array - | T::Int32Array - | T::Uint32Array - | T::Float16Array - | T::Float32Array - | T::Float64Array - | T::BigInt64Array - | T::BigUint64Array - | T::DataView => TagPayload::TypedArray, - - T::HeapBigInt => TagPayload::BigInt, + t if t.is_array_buffer_like() => Tag::TypedArray, + + T::HeapBigInt => Tag::BigInt, // None of these should ever exist here // But we're going to check anyway @@ -2426,18 +2284,22 @@ pub mod formatter { | T::LexicalEnvironment | T::ModuleEnvironment | T::StrictEvalActivation - | T::WithScope => TagPayload::NativeCode, + | T::WithScope => Tag::NativeCode, - T::Event => TagPayload::Event, + T::Event => Tag::Event, - T::GetterSetter => TagPayload::GetterSetter, - T::CustomGetterSetter => TagPayload::CustomGetterSetter, + T::GetterSetter => Tag::GetterSetter, + T::CustomGetterSetter => Tag::CustomGetterSetter, - T::JSAsJSONType => TagPayload::ToJSON, + T::JSAsJSONType => Tag::ToJSON, - _ => TagPayload::JSON, + _ => Tag::JSON, }; - Ok(TagResult { tag, cell: js_type }) + Ok(TagResult { + tag, + cell: js_type, + custom: None, + }) } } @@ -2460,11 +2322,7 @@ pub mod formatter { slice_: &[u8], global: &'a JSGlobalObject, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let mut slice = slice_; let mut i: u32 = 0; let mut len: u32 = slice.len() as u32; @@ -2541,11 +2399,8 @@ pub mod formatter { next_value, next_value.js_type(), )?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = + WrappedWriter::new(writer_, &mut self.estimated_line_length); } PercentTag::I => { // 1. If Type(current) is Symbol, let converted be NaN @@ -2693,11 +2548,8 @@ pub mod formatter { next_value, global, )?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = + WrappedWriter::new(writer_, &mut self.estimated_line_length); } PercentTag::C => { @@ -2742,6 +2594,14 @@ pub mod formatter { impl<'w> WrappedWriter<'w> { pub const IS_WRAPPED_WRITER: bool = true; + pub fn new(ctx: &'w mut dyn bun_io::Write, estimated_line_length: &'w mut usize) -> Self { + Self { + ctx, + failed: false, + estimated_line_length, + } + } + /// Mirror of `Formatter::add_for_new_line` routed through the borrowed /// `estimated_line_length` so callers don't need a second `&mut self` /// on the parent `Formatter` while a `WrappedWriter` is live. @@ -3092,11 +2952,8 @@ pub mod formatter { value: JSValue, ) -> JsResult<()> { if value.is_cell() && !value.js_type().is_function() { - let mut writer = WrappedWriter { - ctx: self.writer, - failed: false, - estimated_line_length: &mut self.formatter.estimated_line_length, - }; + let mut writer = + WrappedWriter::new(self.writer, &mut self.formatter.estimated_line_length); if let Some(name_str) = get_object_name(global_this, value)? { writer.print(format_args!("{name_str} ")); @@ -3252,11 +3109,8 @@ pub mod formatter { } } - let mut writer = WrappedWriter { - ctx: &mut *ctx.writer, - failed: false, - estimated_line_length: &mut ctx.formatter.estimated_line_length, - }; + let mut writer = + WrappedWriter::new(&mut *ctx.writer, &mut ctx.formatter.estimated_line_length); if ctx.i > 0 { writer.print_comma::(); } @@ -3543,11 +3397,7 @@ pub mod formatter { &mut self, writer_: &mut dyn bun_io::Write, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); writer.add_for_new_line(9); writer.print(format_args!( "{}undefined{}", @@ -3562,11 +3412,7 @@ pub mod formatter { #[inline(never)] fn print_null(&mut self, writer_: &mut dyn bun_io::Write) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); writer.add_for_new_line(4); writer.print(format_args!( "{}null{}", @@ -3585,11 +3431,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if let Some(class_name) = value.get_class_info_name() { writer.add_for_new_line("[native code: ]".len() + class_name.len()); writer.write_all(b"[native code: "); @@ -3610,11 +3452,7 @@ pub mod formatter { &mut self, writer_: &mut dyn bun_io::Write, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); const FMT: &str = "[Global Object]"; writer.add_for_new_line(FMT.len()); writer.write_all(pfmt!(concat!("", "[Global Object]", ""), C).as_bytes()); @@ -3629,11 +3467,7 @@ pub mod formatter { &mut self, writer_: &mut dyn bun_io::Write, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); writer.add_for_new_line("".len()); writer.print(format_args!( "{}{}", @@ -3688,11 +3522,7 @@ pub mod formatter { // This is called from the '%s' formatter, so it can actually be any value use crate::StringJsc as _; let str = OwnedString::new(BunString::from_js(value, self.global_this)?); - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); writer.add_for_new_line(str.length()); if self.quote_strings && js_type != jsc::JSType::RegExpObject { @@ -3742,11 +3572,7 @@ pub mod formatter { self.failed = true; } self.print_as::(Tag::JSON, writer_, value, jsc::JSType::StringObject)?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); } else { JSPrinter::write_json_string( str.latin1(), @@ -3799,11 +3625,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let int = value.coerce_to_int64(self.global_this)?; writer.add_for_new_line(bun_core::fmt::digit_count(int)); writer.print(format_args!( @@ -3824,11 +3646,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let zstr = value.get_zig_string(self.global_this)?; let out_str = zstr.slice(); writer.add_for_new_line(out_str.len()); @@ -3850,16 +3668,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if value.is_cell() { let mut number_name = ZigString::EMPTY; value.get_class_name(self.global_this, &mut number_name)?; @@ -3873,10 +3682,10 @@ pub mod formatter { ); writer.print(format_args!( "{}[Number ({}): {}]{}", - pf!(""), + pfmt!("", C), number_name, number_value, - pf!("") + pfmt!("", C) )); if writer.failed { self.failed = true; @@ -3887,10 +3696,10 @@ pub mod formatter { writer.add_for_new_line(number_name.len + number_value.len + 4); writer.print(format_args!( "{}[{}: {}]{}", - pf!(""), + pfmt!("", C), number_name, number_value, - pf!("") + pfmt!("", C) )); if writer.failed { self.failed = true; @@ -3902,26 +3711,34 @@ pub mod formatter { if num.is_infinite() && num > 0.0 { writer.add_for_new_line("Infinity".len()); - writer.print(format_args!("{}Infinity{}", pf!(""), pf!(""))); + writer.print(format_args!( + "{}Infinity{}", + pfmt!("", C), + pfmt!("", C) + )); } else if num.is_infinite() && num < 0.0 { writer.add_for_new_line("-Infinity".len()); writer.print(format_args!( "{}-Infinity{}", - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C) )); } else if num.is_nan() { writer.add_for_new_line("NaN".len()); - writer.print(format_args!("{}NaN{}", pf!(""), pf!(""))); + writer.print(format_args!( + "{}NaN{}", + pfmt!("", C), + pfmt!("", C) + )); } else { let mut buf = [0u8; 124]; let formatted = bun_core::fmt::FormatDouble::dtoa_with_negative_zero(&mut buf, num); writer.add_for_new_line(formatted.len()); writer.print(format_args!( "{}{}{}", - pf!(""), + pfmt!("", C), bstr::BStr::new(formatted), - pf!("") + pfmt!("", C) )); } if writer.failed { @@ -3976,11 +3793,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let description = value.get_description(self.global_this); writer.add_for_new_line("Symbol".len()); @@ -4043,16 +3856,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); // Prefer the constructor's own `.name` property over // `getClassName` / `calculatedClassName`. For DOM / WebCore // InternalFunction constructors like `ReadableStreamBYOBReader`, @@ -4082,31 +3886,31 @@ pub mod formatter { if printable_proto.is_empty() { writer.print(format_args!( "{}[class (anonymous)]{}", - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C) )); } else { writer.print(format_args!( "{}[class (anonymous) extends {}]{}", - pf!(""), + pfmt!("", C), printable_proto, - pf!("") + pfmt!("", C) )); } } else if printable_proto.is_empty() { writer.print(format_args!( "{}[class {}]{}", - pf!(""), + pfmt!("", C), printable, - pf!("") + pfmt!("", C) )); } else { writer.print(format_args!( "{}[class {} extends {}]{}", - pf!(""), + pfmt!("", C), printable, printable_proto, - pf!("") + pfmt!("", C) )); } if writer.failed { @@ -4121,16 +3925,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let printable = OwnedString::new(value.get_name(self.global_this)?); let proto = value.get_prototype(self.global_this); @@ -4139,29 +3934,33 @@ pub mod formatter { if printable.is_empty() || func_name.eql(&printable) { if func_name.is_empty() { - writer.print(format_args!("{}[Function]{}", pf!(""), pf!(""))); + writer.print(format_args!( + "{}[Function]{}", + pfmt!("", C), + pfmt!("", C) + )); } else { writer.print(format_args!( "{}[{}]{}", - pf!(""), + pfmt!("", C), func_name, - pf!("") + pfmt!("", C) )); } } else if func_name.is_empty() { writer.print(format_args!( "{}[Function: {}]{}", - pf!(""), + pfmt!("", C), printable, - pf!("") + pfmt!("", C) )); } else { writer.print(format_args!( "{}[{}: {}]{}", - pf!(""), + pfmt!("", C), func_name, printable, - pf!("") + pfmt!("", C) )); } if writer.failed { @@ -4176,11 +3975,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); // `JSCell` is an `opaque_ffi!` ZST handle; `opaque_ref` is the // centralised non-null deref proof (tag only produced for cells). let cell = jsc::JSCell::opaque_ref(value.to_cell().expect("GetterSetter is a cell")); @@ -4222,11 +4017,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if !self.single_line && writer.good_time_for_a_new_line(self.indent) { writer.write_all(b"\n"); writer.write_indent(self.indent); @@ -4258,16 +4049,7 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if value.is_cell() { let mut bool_name = ZigString::EMPTY; value.get_class_name(self.global_this, &mut bool_name)?; @@ -4279,10 +4061,10 @@ pub mod formatter { .add_for_new_line(bool_value.len + bool_name.len + "[Boolean (): ]".len()); writer.print(format_args!( "{}[Boolean ({}): {}]{}", - pf!(""), + pfmt!("", C), bool_name, bool_value, - pf!("") + pfmt!("", C) )); if writer.failed { self.failed = true; @@ -4292,9 +4074,9 @@ pub mod formatter { writer.add_for_new_line(bool_value.len + "[Boolean: ]".len()); writer.print(format_args!( "{}[Boolean: {}]{}", - pf!(""), + pfmt!("", C), bool_value, - pf!("") + pfmt!("", C) )); if writer.failed { self.failed = true; @@ -4303,10 +4085,10 @@ pub mod formatter { } if value.to_boolean() { writer.add_for_new_line(4); - writer.write_all(pf!("true").as_bytes()); + writer.write_all(pfmt!("true", C).as_bytes()); } else { writer.add_for_new_line(5); - writer.write_all(pf!("false").as_bytes()); + writer.write_all(pfmt!("false", C).as_bytes()); } if writer.failed { self.failed = true; @@ -4348,11 +4130,7 @@ pub mod formatter { value: JSValue, js_type: jsc::JSType, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let mut str = OwnedString::new(BunString::empty()); value.json_stringify(self.global_this, self.indent, &mut str)?; @@ -4406,16 +4184,7 @@ pub mod formatter { // function, and `WrappedWriter` holds `&mut self.estimated_line_length` // which prevents calling `&self` methods while it is live. let tag_opts = self.tag_opts(); - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let len = value.get_length(self.global_this)?; @@ -4469,11 +4238,7 @@ pub mod formatter { } self.format::(tag, writer_, element, self.global_this)?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if tag.cell.is_string_like() && C { writer.write_all(pfmt!("", true).as_bytes()); @@ -4514,9 +4279,9 @@ pub mod formatter { "... N more items".len(), format_args!( "{}... {} more items{}", - pf!(""), + pfmt!("", C), len - u64::from(i), - pf!("") + pfmt!("", C) ), ); break; @@ -4541,7 +4306,7 @@ pub mod formatter { if empty_count == 1 { writer.pretty::( "empty item".len(), - format_args!("{}empty item{}", pf!(""), pf!("")), + format_args!("{}empty item{}", pfmt!("", C), pfmt!("", C)), ); } else { writer.add_for_new_line(bun_core::fmt::digit_count(empty_count)); @@ -4549,9 +4314,9 @@ pub mod formatter { " x empty items".len(), format_args!( "{}{} x empty items{}", - pf!(""), + pfmt!("", C), empty_count, - pf!("") + pfmt!("", C) ), ); } @@ -4572,11 +4337,7 @@ pub mod formatter { let tag = Tag::get_advanced(element, self.global_this, tag_opts)?; self.format::(tag, writer_, element, self.global_this)?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if tag.cell.is_string_like() && C { writer.write_all(pfmt!("", true).as_bytes()); @@ -4603,7 +4364,7 @@ pub mod formatter { if empty_count == 1 { writer.pretty::( "empty item".len(), - format_args!("{}empty item{}", pf!(""), pf!("")), + format_args!("{}empty item{}", pfmt!("", C), pfmt!("", C)), ); } else { writer.add_for_new_line(bun_core::fmt::digit_count(empty_count)); @@ -4611,9 +4372,9 @@ pub mod formatter { " x empty items".len(), format_args!( "{}{} x empty items{}", - pf!(""), + pfmt!("", C), empty_count, - pf!("") + pfmt!("", C) ), ); } @@ -4643,11 +4404,7 @@ pub mod formatter { if self.failed { return Ok(()); } - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); } } @@ -4972,12 +4729,6 @@ pub mod formatter { value: JSValue, remove_before_recurse: &mut bool, ) -> JsResult<()> { - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } - let event_type_value: JSValue = 'brk: { let Some(value_) = value.get(self.global_this, "type")? else { break 'brk JSValue::UNDEFINED; @@ -5016,9 +4767,9 @@ pub mod formatter { let _ = writeln!( writer_, "{}{}{} {{", - pf!(""), + pfmt!("", C), event_tag_name, - pf!("") + pfmt!("", C) ); { self.indent += 1; @@ -5034,23 +4785,23 @@ pub mod formatter { let _ = write!( writer_, "{}type: {}\"{}\"{}{},{} ", - pf!(""), - pf!(""), + pfmt!("", C), + pfmt!("", C), bstr::BStr::new(event_type.label()), - pf!(""), - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C), + pfmt!("", C) ); } else { let _ = writeln!( writer_, "{}type: {}\"{}\"{}{},{}", - pf!(""), - pf!(""), + pfmt!("", C), + pfmt!("", C), bstr::BStr::new(event_type.label()), - pf!(""), - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C), + pfmt!("", C) ); } @@ -5064,9 +4815,9 @@ pub mod formatter { let _ = write!( writer_, "{}message{}:{} ", - pf!(""), - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C), + pfmt!("", C) ); let tag = Tag::get_advanced(message_value, self.global_this, self.tag_opts())?; @@ -5089,9 +4840,9 @@ pub mod formatter { let _ = write!( writer_, "{}data{}:{} ", - pf!(""), - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C), + pfmt!("", C) ); let data: JSValue = value .fast_get(self.global_this, jsc::BuiltinName::Data)? @@ -5116,9 +4867,9 @@ pub mod formatter { let _ = write!( writer_, "{}error{}:{} ", - pf!(""), - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C), + pfmt!("", C) ); let tag = Tag::get_advanced(error_value, self.global_this, self.tag_opts())?; @@ -5152,22 +4903,13 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } // Cache once: `disable_inspect_custom` does not change inside this // function, and `WrappedWriter` holds `&mut self.estimated_line_length` // which prevents calling `&self` methods while it is live. let tag_opts = self.tag_opts(); - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); - writer.write_all(pf!("").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); writer.write_all(b"<"); // Both arms of the `type` if/else below assign these, so deferred @@ -5205,13 +4947,13 @@ pub mod formatter { } if !is_tag_kind_primitive { - writer.write_all(pf!("").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); } else { - writer.write_all(pf!("").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); } writer.write_all(tag_name_slice.slice()); if C { - writer.write_all(pf!("").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); } if let Some(key_value) = value.get(self.global_this, "key")? { @@ -5235,11 +4977,7 @@ pub mod formatter { key_value, self.global_this, )?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); needs_space = true; } @@ -5295,10 +5033,10 @@ pub mod formatter { writer.print(format_args!( "{}{}{}={}", - pf!(""), + pfmt!("", C), prop.trunc(128), - pf!(""), - pf!("") + pfmt!("", C), + pfmt!("", C) )); if tag.cell.is_string_like() && C { @@ -5309,11 +5047,7 @@ pub mod formatter { self.failed = true; } self.format::(tag, writer_, property_value, self.global_this)?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); if tag.cell.is_string_like() && C { writer.write_all(pfmt!("", true).as_bytes()); @@ -5344,12 +5078,11 @@ pub mod formatter { if let Some(children) = children_prop { let tag = Tag::get(children, self.global_this)?; - let print_children = - matches!(tag.tag.tag(), Tag::String | Tag::JSX | Tag::Array); + let print_children = matches!(tag.tag, Tag::String | Tag::JSX | Tag::Array); if print_children && !self.single_line { 'print_children: { - match tag.tag.tag() { + match tag.tag { Tag::String => { let children_string = children.get_zig_string(self.global_this)?; @@ -5390,12 +5123,10 @@ pub mod formatter { children, self.global_this, )?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self - .estimated_line_length, - }; + writer = WrappedWriter::new( + writer_, + &mut self.estimated_line_length, + ); } writer.write_all(b"\n"); write_indent_n(self.indent, writer.ctx) @@ -5438,12 +5169,10 @@ pub mod formatter { child, self.global_this, )?; - writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self - .estimated_line_length, - }; + writer = WrappedWriter::new( + writer_, + &mut self.estimated_line_length, + ); if (j as u64) + 1 < length { writer.write_all(b"\n"); write_indent_n(self.indent, writer.ctx) @@ -5461,13 +5190,13 @@ pub mod formatter { writer.write_all(b"").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); } else { - writer.write_all(pf!("").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); } writer.write_all(tag_name_slice.slice()); if C { - writer.write_all(pf!("").as_bytes()); + writer.write_all(pfmt!("", C).as_bytes()); } writer.write_all(b">"); } @@ -5575,11 +5304,6 @@ pub mod formatter { writer_: &mut dyn bun_io::Write, value: JSValue, ) -> JsResult<()> { - macro_rules! pf { - ($s:literal) => { - pfmt!($s, C) - }; - } if self.single_line { let _ = writer_.write_all(b" "); } else if self.always_newline_scope || self.good_time_for_a_new_line() { @@ -5595,9 +5319,9 @@ pub mod formatter { let _ = write!( writer_, "{}[{} ...]{}", - pf!(""), + pfmt!("", C), display_name, - pf!("") + pfmt!("", C) ); Ok(()) } @@ -5647,11 +5371,7 @@ pub mod formatter { value: JSValue, js_type: jsc::JSType, ) -> JsResult<()> { - let mut writer = WrappedWriter { - ctx: writer_, - failed: false, - estimated_line_length: &mut self.estimated_line_length, - }; + let mut writer = WrappedWriter::new(writer_, &mut self.estimated_line_length); let array_buffer = value.as_array_buffer(self.global_this).unwrap(); let slice = array_buffer.byte_slice(); @@ -5790,10 +5510,10 @@ pub mod formatter { let _restore = defer_restore!(self.global_this, prev_global_this); self.global_this = global_this; - if let TagPayload::CustomFormattedObject(obj) = result.tag { + if let Some(obj) = result.custom { self.custom_formatted_object = obj; } - self.print_as::(result.tag.tag(), writer, value, result.cell) + self.print_as::(result.tag, writer, value, result.cell) } } diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index 2dc03fbd4246..78ea408c409c 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -5647,7 +5647,7 @@ impl VirtualMachine { ) -> crate::CrateResult<()> { use crate::JSType; use crate::console_object::formatter::TagOptions; - use crate::console_object::{self, Tag, TagPayload}; + use crate::console_object::{self, Tag}; let prev_had_errors = self.had_errors; self.had_errors = true; @@ -6119,7 +6119,7 @@ impl VirtualMachine { global_ref, TagOptions::DISABLE_INSPECT_CUSTOM | TagOptions::HIDE_GLOBAL, )?; - if !matches!(tag.tag, TagPayload::NativeCode) { + if !matches!(tag.tag, Tag::NativeCode) { let _ = if allow_ansi_color { formatter.format::(tag, writer, error_instance, global_ref) } else { diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index 85c8c8df5f3c..04652e6a45ce 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -396,8 +396,9 @@ impl<'a> ConsoleFormatter for self::console_object::Formatter<'a> { // the const-generic `print_as::<{ Tag::… }, …>` arms. let mut sink = bun_io::FmtAdapter::new(writer); let result = self::console_object::formatter::TagResult { - tag: tag.into(), + tag, cell, + custom: None, }; let global = self.global_this; self.format::(result, &mut sink, value, global) diff --git a/src/paths/lib.rs b/src/paths/lib.rs index 91f89e1e3353..40136edb54ff 100644 --- a/src/paths/lib.rs +++ b/src/paths/lib.rs @@ -690,12 +690,6 @@ pub mod fs { self.base } - /// The dir component, or `"."` when it is empty. - #[inline] - pub fn dir_or_dot(&self) -> &'a [u8] { - if self.dir.is_empty() { b"." } else { self.dir } - } - /// Formats [`Self::non_unique_name_string_base`] as a valid JS /// identifier. #[inline] @@ -931,23 +925,6 @@ pub mod fs { } } - /// Same const-concat caveat as `init_with_namespace_virtual`: - /// callers pass the precomputed `concatcp!` result as `pretty`. - #[inline] - pub const fn init_for_kit_built_in( - namespace: &'static [u8], - pretty: &'static [u8], - text: &'static [u8], - ) -> Path<'static> { - Path { - pretty, - is_symlink: true, - text, - namespace, - is_disabled: false, - } - } - /// Debug-only check that `pretty` /// contains no backslashes (Windows). No-op on POSIX. #[inline] diff --git a/src/paths/resolve_path.rs b/src/paths/resolve_path.rs index b7c2eb32ac9b..829b1a52acad 100644 --- a/src/paths/resolve_path.rs +++ b/src/paths/resolve_path.rs @@ -48,9 +48,6 @@ pub fn z<'a>(input: &[u8], output: &'a mut PathBuffer) -> &'a ZStr { } type IsSeparatorFunc = fn(char: u8) -> bool; -// Rust cannot express "fn(T) -> bool" as a value, so the generic-`T` -// callers dispatch via Platform methods instead of fn pointers. -type LastSeparatorFunction = fn(slice: &[u8]) -> Option; #[inline(always)] fn is_dotdot_with_type(slice: &[T]) -> bool { @@ -1240,14 +1237,6 @@ impl Platform { } } - pub const fn get_last_separator_func(self) -> LastSeparatorFunction { - match self { - Platform::Loose => last_index_of_separator_loose, - Platform::Nt | Platform::Windows => last_index_of_separator_windows, - Platform::Posix => last_index_of_separator_posix, - } - } - #[inline(always)] pub fn is_separator(self, char: u8) -> bool { self.is_separator_t::(char) diff --git a/src/router/Cargo.toml b/src/router/Cargo.toml index b371d53bb519..635cebb2e8b0 100644 --- a/src/router/Cargo.toml +++ b/src/router/Cargo.toml @@ -35,7 +35,3 @@ bun_resolver.workspace = true bun_sys.workspace = true bun_url.workspace = true bun_wyhash.workspace = true - -[dev-dependencies] -# Test harness (Test::make / Test::make_routes) needs the AST stores. -bun_js_parser.workspace = true diff --git a/src/router/lib.rs b/src/router/lib.rs index 797365348028..94f50ffaffb7 100644 --- a/src/router/lib.rs +++ b/src/router/lib.rs @@ -1967,278 +1967,13 @@ pub mod pattern { pub use pattern::Pattern; // ────────────────────────────────────────────────────────────────────────── -// Tests + test helpers +// Tests // ────────────────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; - struct MockRequestContextType { - controlled: bool, - url: URLPath, - match_file_path_buf: [u8; 1024], - - handle_request_called: bool, - redirect_called: bool, - matched_route: Option>, - has_called_done: bool, - } - - impl Default for MockRequestContextType { - fn default() -> Self { - Self { - controlled: false, - url: URLPath::default(), - match_file_path_buf: [0; 1024], - handle_request_called: false, - redirect_called: false, - matched_route: None, - has_called_done: false, - } - } - } - - impl MockRequestContextType { - fn handle_request(&mut self) -> crate::Result<()> { - self.handle_request_called = true; - Ok(()) - } - - fn handle_redirect(&mut self, _: &[u8]) -> crate::Result<()> { - self.redirect_called = true; - Ok(()) - } - } - - struct JavaScriptHandler; - impl JavaScriptHandler { - fn enqueue( - _: &mut MockRequestContextType, - _: &mut MockServer, - _: &mut route_param::List<'_>, - ) -> crate::Result<()> { - Ok(()) - } - } - - pub struct MockServer { - watchloop_handle: Option, - watcher: MockWatcher, - } - - impl Default for MockServer { - fn default() -> Self { - Self { - watchloop_handle: None, - watcher: MockWatcher::default(), - } - } - } - - #[derive(Default)] - pub struct MockWatcher { - watchloop_handle: Option, - } - impl MockWatcher { - pub fn start(&mut self) -> crate::Result<()> { - Ok(()) - } - } - - fn make_test(cwd_path: &[u8], data: &[(&str, &str)]) -> crate::Result<()> { - Output::init_test(); - debug_assert!(cwd_path.len() > 1 && cwd_path != b"/" && !cwd_path.ends_with(b"bun")); - let bun_tests_dir = bun_sys::Dir::cwd() - .make_open_path(b"bun-test-scratch", bun_sys::OpenDirOptions::default())?; - let _ = bun_tests_dir.delete_tree(cwd_path); - - let cwd = bun_tests_dir.make_open_path(cwd_path, bun_sys::OpenDirOptions::default())?; - bun_sys::fchdir(cwd.fd())?; - - for (name, value) in data { - let name_b = name.as_bytes(); - // NOTE: paths without a '/' have no parent dir to create; - // rposition on '/' finds the parent (test fixture paths are - // always forward-slash). - if let Some(slash) = name_b.iter().rposition(|&c| c == b'/') { - if slash > 0 { - cwd.make_path(&name_b[..slash])?; - } - } - let file = bun_sys::File::create(cwd.fd(), name_b, true)?; - file.write_all(value.as_bytes())?; - let _ = file.close(); - } - Ok(()) - } - - /// Newtype so the orphan rule lets us `impl ResolverLike` for a - /// foreign-crate type. - struct TestResolver<'a>(bun_resolver::Resolver<'a>); - - impl<'a> ResolverLike for TestResolver<'a> { - fn fs(&self) -> &'static FileSystem { - // SAFETY: process-static singleton (see `FileSystem::instance`). - unsafe { &*self.0.fs() } - } - fn fs_impl(&self) -> *mut Fs::Implementation { - // SAFETY: `&fs.fs` — the `Implementation` field of the singleton. - unsafe { core::ptr::from_mut(&mut (*self.0.fs()).fs) } - } - fn read_dir_info_ignore_error(&mut self, path: &[u8]) -> Option { - self.0.read_dir_info_ignore_error(path) - } - } - - pub struct Test; - - impl Test { - pub fn make_routes( - test_name: &'static str, - data: &[(&str, &str)], - ) -> crate::Result { - Output::init_test(); - make_test(test_name.as_bytes(), data)?; - bun_ast::initialize_store(); - // const fs = try FileSystem.init(null); - let _ = bun_resolver::fs::FileSystem::init(None)?; - let top_level_dir = bun_resolver::fs::FileSystem::get().top_level_dir; - - // var pages_parts = [_]string{ top_level_dir, "pages" }; - // const pages_dir = try Fs.FileSystem.instance.absAlloc(default_allocator, &pages_parts); - let pages_parts: [&[u8]; 2] = [top_level_dir, b"pages"]; - let pages_dir = bun_resolver::fs::FileSystem::instance() - .abs_alloc(&pages_parts) - .map_err(|_| crate::Error::Alloc(bun_alloc::AllocError))?; - - // const router = try Router.init(&FileSystem.instance, default_allocator, RouteConfig{...}); - // SAFETY: process-static singleton just initialized above. - let fs_opaque: &'static FileSystem = unsafe { &*fs }; - let router = Router::init( - fs_opaque, - RouteConfig { - dir: pages_dir.to_vec().into_boxed_slice(), - routes_enabled: true, - extensions: vec![b"js".as_slice().into()].into_boxed_slice(), - ..RouteConfig::default() - }, - )?; - - let mut log = bun_ast::Log::init(); - // NOTE: `errdefer logger.print(Output.errorWriter())` — Rust has - // no errdefer; the test harness panics on error anyway, but the guard - // still flushes diagnostics on early-return for parity. - let _err_dump = scopeguard::guard(core::ptr::from_mut(&mut log), |log| { - // SAFETY: pointer to a stack local that outlives this guard. - let _ = unsafe { &*log }.print(bun_core::output::error_writer()); - }); - - // const opts = Options.BundleOptions{ .target = .browser, ... }; - // NOTE: the resolver-side `BundleOptions` subset omits - // `loaders`/`define`/`log`/`routes`/`entry_points`/`out_extensions`/ - // `transform_options` — none are read by `Resolver::init1` or the - // dir-info walk, so `Default` + `target` is the faithful projection. - let opts = bun_resolver::options::BundleOptions { - target: bun_ast::Target::Browser, - external: bun_resolver::options::ExternalModules::default(), - ..Default::default() - }; - - // var resolver = Resolver.init1(default_allocator, &logger, &FileSystem.instance, opts); - let mut resolver = TestResolver(bun_resolver::Resolver::init1( - core::ptr::NonNull::from(&mut log), - fs, - opts, - )); - - // const root_dir = (try resolver.readDirInfo(pages_dir)).?; - let root_dir = resolver - .0 - .read_dir_info(pages_dir)? - .ok_or_else(|| crate::Error::Sys(bun_errno::SystemErrno::ENOENT))?; - - // return RouteLoader.loadAll(..., opts.routes, &logger, Resolver, &resolver, root_dir); - // SAFETY: `_err_dump` only re-derives `&*log` on drop (after this borrow ends). - let routes = RouteLoader::load_all( - router.config.clone(), - unsafe { &mut *core::ptr::from_mut(&mut log) }, - &mut resolver, - &root_dir, - top_level_dir, - ); - scopeguard::ScopeGuard::into_inner(_err_dump); - Ok(routes) - } - - pub fn make( - test_name: &'static str, - data: &[(&str, &str)], - ) -> crate::Result> { - make_test(test_name.as_bytes(), data)?; - bun_ast::initialize_store(); - // const fs = try FileSystem.initWithForce(null, true); - let _ = bun_resolver::fs::FileSystem::init_with_force::(None)?; - let top_level_dir = bun_resolver::fs::FileSystem::get().top_level_dir; - - let pages_parts: [&[u8]; 2] = [top_level_dir, b"pages"]; - let pages_dir = bun_resolver::fs::FileSystem::instance() - .abs_alloc(&pages_parts) - .map_err(|_| crate::Error::Alloc(bun_alloc::AllocError))?; - - // var router = try Router.init(&FileSystem.instance, default_allocator, RouteConfig{...}); - // SAFETY: process-static singleton just initialized above. - let fs_opaque: &'static FileSystem = unsafe { &*fs }; - let mut router = Router::init( - fs_opaque, - RouteConfig { - dir: pages_dir.to_vec().into_boxed_slice(), - routes_enabled: true, - extensions: vec![b"js".as_slice().into()].into_boxed_slice(), - ..RouteConfig::default() - }, - )?; - - let mut log = bun_ast::Log::init(); - let _err_dump = scopeguard::guard(core::ptr::from_mut(&mut log), |log| { - // SAFETY: pointer to a stack local that outlives this guard. - let _ = unsafe { &*log }.print(bun_core::output::error_writer()); - }); - - let opts = bun_resolver::options::BundleOptions { - target: bun_ast::Target::Browser, - external: bun_resolver::options::ExternalModules::default(), - ..Default::default() - }; - - let mut resolver = TestResolver(bun_resolver::Resolver::init1( - core::ptr::NonNull::from(&mut log), - fs, - opts, - )); - - // const root_dir = (try resolver.readDirInfo(pages_dir)).?; - let root_dir = resolver - .0 - .read_dir_info(pages_dir)? - .ok_or_else(|| crate::Error::Sys(bun_errno::SystemErrno::ENOENT))?; - - // try router.loadRoutes(&logger, root_dir, Resolver, &resolver, top_level_dir); - // SAFETY: `_err_dump` only re-derives `&*log` on drop (after this borrow ends). - router.load_routes( - unsafe { &mut *core::ptr::from_mut(&mut log) }, - &root_dir, - &mut resolver, - top_level_dir, - )?; - let entry_points = router.get_entry_points(); - - assert_eq!(data.len(), entry_points.len()); - scopeguard::ScopeGuard::into_inner(_err_dump); - Ok(router) - } - } - #[test] fn pattern_match() { type Entry = Param<'static>; diff --git a/src/runtime/cli/bunx_command.rs b/src/runtime/cli/bunx_command.rs index 9be94531906b..28fd208e6652 100644 --- a/src/runtime/cli/bunx_command.rs +++ b/src/runtime/cli/bunx_command.rs @@ -213,6 +213,19 @@ pub(crate) enum GetBinNameError { NeedToInstall, } +/// Inputs shared by both post-install cached-bin probes; invariant across the +/// initial-bin-name and package-json-bin-name attempts. +struct CachedBinProbe<'a> { + bunx_cache_dir: &'a [u8], + ignore_cwd: &'a [u8], + top_level_dir: &'a [u8], + #[cfg(unix)] + uid: libc::uid_t, + #[cfg(not(unix))] + uid: u32, + dirname_store: &'static bun_resolver::fs::DirnameStore, +} + impl BunxCommand { /// Adds `create-` to the string, but also handles scoped packages correctly. /// Always clones the string in the process. @@ -584,6 +597,77 @@ impl BunxCommand { true } + /// Post-install cache probe: builds + /// `/node_modules/.bin/`, resolves it + /// via `bun_which::which`, and execs it via `Run::run_binary` (noreturn) + /// once it passes the `is_trusted_cached_binary` TOCTOU check. Returns + /// `Ok(())` only on a miss: the binary is absent or untrusted. + fn try_run_cached_bin( + ctx: &mut ContextData, + path_buf: &mut PathBuffer, + absolute_in_cache_dir_buf: &mut PathBuffer, + probe: &CachedBinProbe, + bin_name: &[u8], + env_loader: &mut bun_dotenv::Loader<'static>, + passthrough: &[Box<[u8]>], + ) -> crate::Result<()> { + let buf_total = absolute_in_cache_dir_buf.len(); + let absolute_in_cache_dir: &[u8] = { + let mut cursor: &mut [u8] = &mut absolute_in_cache_dir_buf[..]; + write!( + cursor, + "{cache}{sep}node_modules{sep}.bin{sep}{bin}{exe}", + cache = BStr::new(probe.bunx_cache_dir), + sep = bun_paths::SEP as char, + bin = BStr::new(bin_name), + exe = EXE_SUFFIX, + ) + .expect("unreachable"); + let written = buf_total - cursor.len(); + // SAFETY: `written` bytes initialized above + unsafe { core::slice::from_raw_parts(absolute_in_cache_dir_buf.as_ptr(), written) } + }; + + // Similar to "npx": try the bin in the global cache. Do not try $PATH + // because we already checked it above if we should. + if let Some(destination) = bun_which::which( + path_buf, + probe.bunx_cache_dir, + if !probe.ignore_cwd.is_empty() { + b"".as_slice() + } else { + probe.top_level_dir + }, + absolute_in_cache_dir, + ) { + let out: &[u8] = destination.as_bytes(); + // The install we just ran should have created this symlink as the + // current user, but the cache lives in a world-writable temp dir; an + // attacker can race the install and plant a uid-mismatched entry. + // Bail out to the generic error rather than execute it. + if Self::is_trusted_cached_binary(destination, probe.uid) { + let stored = probe.dirname_store.append_slice(out)?; + Run::run_binary( + ctx, + stored, + destination, + probe.top_level_dir, + env_loader, + passthrough, + None, + )?; + // run_binary is noreturn + } else { + bun_output::scoped_log!( + bunx, + "refusing untrusted cached binary: {}", + BStr::new(out) + ); + } + } + Ok(()) + } + fn exit_with_usage() -> ! { crate::cli::command::tag_print_help(Command::Tag::BunxCommand, false); Global::exit(1); @@ -1369,61 +1453,23 @@ impl BunxCommand { _ => {} } - absolute_in_cache_dir = { - let mut cursor: &mut [u8] = &mut absolute_in_cache_dir_buf[..]; - write!( - cursor, - "{cache}{sep}node_modules{sep}.bin{sep}{bin}{exe}", - cache = BStr::new(bunx_cache_dir), - sep = bun_paths::SEP as char, - bin = BStr::new(initial_bin_name), - exe = EXE_SUFFIX, - ) - .expect("unreachable"); - let written = buf_total - cursor.len(); - // SAFETY: `written` bytes initialized above - unsafe { core::slice::from_raw_parts(absolute_in_cache_dir_buf.as_ptr(), written) } + let cached_bin_probe = CachedBinProbe { + bunx_cache_dir, + ignore_cwd: &ignore_cwd, + top_level_dir, + uid, + dirname_store: fs.dirname_store, }; - // Similar to "npx": - // - // 1. Try the bin in the global cache - // Do not try $PATH because we already checked it above if we should - if let Some(destination) = bun_which::which( + Self::try_run_cached_bin( + ctx, &mut path_buf, - bunx_cache_dir, - if !ignore_cwd.is_empty() { - b"".as_slice() - } else { - top_level_dir - }, - absolute_in_cache_dir, - ) { - let out: &[u8] = destination.as_bytes(); - // The install we just ran should have created this symlink as the - // current user, but the cache lives in a world-writable temp dir; an - // attacker can race the install and plant a uid-mismatched entry. - // Bail out to the generic error rather than execute it. - if Self::is_trusted_cached_binary(destination, uid) { - let stored = fs.dirname_store.append_slice(out)?; - Run::run_binary( - ctx, - stored, - destination, - top_level_dir, - env_loader, - passthrough, - None, - )?; - // run_binary is noreturn - } else { - bun_output::scoped_log!( - bunx, - "refusing untrusted cached binary: {}", - BStr::new(out) - ); - } - } + &mut absolute_in_cache_dir_buf, + &cached_bin_probe, + initial_bin_name, + env_loader, + passthrough, + )?; // 2. The "bin" is possibly not the same as the package name, so we load the package.json to figure out what "bin" to use // BUT: Skip this if --package was used, as the user explicitly specified the binary name @@ -1435,55 +1481,15 @@ impl BunxCommand { false, ) { if !strings::eql_long(&package_name_for_bin, initial_bin_name, true) { - absolute_in_cache_dir = { - let mut cursor: &mut [u8] = &mut absolute_in_cache_dir_buf[..]; - write!( - cursor, - "{}/node_modules/.bin/{}{}", - BStr::new(bunx_cache_dir), - BStr::new(&package_name_for_bin), - EXE_SUFFIX, - ) - .expect("unreachable"); - let written = buf_total - cursor.len(); - // SAFETY: `written` bytes initialized above - unsafe { - core::slice::from_raw_parts(absolute_in_cache_dir_buf.as_ptr(), written) - } - }; - - if let Some(destination) = bun_which::which( + Self::try_run_cached_bin( + ctx, &mut path_buf, - bunx_cache_dir, - if !ignore_cwd.is_empty() { - b"".as_slice() - } else { - top_level_dir - }, - absolute_in_cache_dir, - ) { - let out: &[u8] = destination.as_bytes(); - // Same TOCTOU hardening as the post-install probe above. - if Self::is_trusted_cached_binary(destination, uid) { - let stored = fs.dirname_store.append_slice(out)?; - Run::run_binary( - ctx, - stored, - destination, - top_level_dir, - env_loader, - passthrough, - None, - )?; - // run_binary is noreturn - } else { - bun_output::scoped_log!( - bunx, - "refusing untrusted cached binary: {}", - BStr::new(out) - ); - } - } + &mut absolute_in_cache_dir_buf, + &cached_bin_probe, + &package_name_for_bin, + env_loader, + passthrough, + )?; } } } diff --git a/src/runtime/cli/create_command.rs b/src/runtime/cli/create_command.rs index 572840ca716b..420668e69aa0 100644 --- a/src/runtime/cli/create_command.rs +++ b/src/runtime/cli/create_command.rs @@ -1690,64 +1690,40 @@ impl CreateCommand { } if !bun_paths::is_absolute(positional) { - 'outer: { - if let Some(home_dir) = env_loader.map.get(b"BUN_CREATE_DIR") { - let parts = [home_dir, positional]; - let outdir_path = filesystem.abs_buf(&parts, home_dir_buf); - let len = outdir_path.len(); - home_dir_buf[len] = 0; - // SAFETY: home_dir_buf[len] == 0 written above - let outdir_path_ = bun_core::ZStr::from_buf(&home_dir_buf[..], len); - if bun_paths::resolve_path::has_any_illegal_chars(outdir_path_.as_bytes()) { - break 'outer; - } - if bun_sys::directory_exists_at(bun_sys::Fd::cwd(), outdir_path_) - .unwrap_or(false) - { - example_tag = ExampleTag::LocalFolder; - break 'brk &home_dir_buf[..len]; - } - } - } - - 'outer: { - let parts = [filesystem.top_level_dir, BUN_CREATE_DIR, positional]; - let outdir_path = filesystem.abs_buf(&parts, home_dir_buf); - let len = outdir_path.len(); - home_dir_buf[len] = 0; - // SAFETY: home_dir_buf[len] == 0 written above - let outdir_path_ = bun_core::ZStr::from_buf(&home_dir_buf[..], len); - if bun_paths::resolve_path::has_any_illegal_chars(outdir_path_.as_bytes()) { - break 'outer; + // Returns the path length if `parts` joins to an existing template directory. + let probe_template_dir = |buf: &mut PathBuffer, parts: &[&[u8]]| -> Option { + let len = filesystem.abs_buf(parts, buf).len(); + buf[len] = 0; + // SAFETY: buf[len] == 0 written above + let outdir_path = bun_core::ZStr::from_buf(&buf[..], len); + if bun_paths::resolve_path::has_any_illegal_chars(outdir_path.as_bytes()) { + return None; } - if bun_sys::directory_exists_at(bun_sys::Fd::cwd(), outdir_path_) + bun_sys::directory_exists_at(bun_sys::Fd::cwd(), outdir_path) .unwrap_or(false) - { + .then_some(len) + }; + + // Empty parts are skipped by the path join, so the two-part + // BUN_CREATE_DIR candidate pads with `b""`. + let candidates: [Option<[&[u8]; 3]>; 3] = [ + env_loader + .map + .get(b"BUN_CREATE_DIR") + .map(|home_dir| [home_dir, positional, b"".as_slice()]), + Some([filesystem.top_level_dir, BUN_CREATE_DIR, positional]), + env_loader + .map + .get(b"HOME") + .map(|home_dir| [home_dir, BUN_CREATE_DIR, positional]), + ]; + for parts in candidates.into_iter().flatten() { + if let Some(len) = probe_template_dir(&mut *home_dir_buf, &parts) { example_tag = ExampleTag::LocalFolder; break 'brk &home_dir_buf[..len]; } } - 'outer: { - if let Some(home_dir) = env_loader.map.get(b"HOME") { - let parts = [home_dir, BUN_CREATE_DIR, positional]; - let outdir_path = filesystem.abs_buf(&parts, home_dir_buf); - let len = outdir_path.len(); - home_dir_buf[len] = 0; - // SAFETY: home_dir_buf[len] == 0 written above - let outdir_path_ = bun_core::ZStr::from_buf(&home_dir_buf[..], len); - if bun_paths::resolve_path::has_any_illegal_chars(outdir_path_.as_bytes()) { - break 'outer; - } - if bun_sys::directory_exists_at(bun_sys::Fd::cwd(), outdir_path_) - .unwrap_or(false) - { - example_tag = ExampleTag::LocalFolder; - break 'brk &home_dir_buf[..len]; - } - } - } - if bun_paths::is_absolute(positional) { example_tag = ExampleTag::LocalFolder; break 'brk positional; diff --git a/src/runtime/cli/filter_run.rs b/src/runtime/cli/filter_run.rs index 6746d8769c87..210bdf77ed70 100644 --- a/src/runtime/cli/filter_run.rs +++ b/src/runtime/cli/filter_run.rs @@ -1,6 +1,6 @@ use core::ffi::{c_char, c_void}; use std::io::Write as _; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::Ordering; use std::time::Instant; #[cfg(unix)] @@ -9,6 +9,9 @@ use crate::api::bun::process::{self as spawn, Process, Rusage, SpawnOptions, Sta use crate::cli::Command; use crate::cli::filter_arg as FilterArg; use crate::cli::run_command::RunCommand; +use crate::cli::run_processes_shared::{ + AbortHandler, SHOULD_ABORT, aggregate_exit_code, buffered_stdio, watch_or_reap, +}; use bun_collections::StringHashMap; use bun_core::{Global, Output}; use bun_core::{ZStr, strings}; @@ -182,16 +185,7 @@ impl<'a> ProcessHandle<'a> { ) }); - match process.watch_or_reap() { - Ok(_) => {} - Err(err) => { - if !process.has_exited() { - // SAFETY: all-zero is a valid Rusage (POD C struct) - let rusage = bun_core::ffi::zeroed::(); - process.on_exit(Status::Err(err), &rusage); - } - } - } + watch_or_reap(process); Ok(()) } @@ -587,87 +581,11 @@ impl<'a> State<'a> { if self.aborted { let _ = self.redraw(true); } - for handle in self.handles.iter() { - if let Some(proc) = &handle.process { - match &proc.status { - Status::Exited(exited) => { - if exited.code != 0 { - return exited.code; - } - } - Status::Signaled(signal) => { - return bun_sys::SignalCode(*signal).to_exit_code().unwrap_or(1); - } - _ => return 1, - } - } - } - 0 - } -} - -struct AbortHandler; - -static SHOULD_ABORT: AtomicBool = AtomicBool::new(false); -// Atomic because it is set from a signal handler. - -impl AbortHandler { - #[cfg(unix)] - extern "C" fn posix_signal_handler( - sig: i32, - info: *const bun_sys::posix::siginfo_t, - _: *const c_void, - ) { - let _ = sig; - let _ = info; - SHOULD_ABORT.store(true, Ordering::SeqCst); - } - - #[cfg(windows)] - extern "system" fn windows_ctrl_handler( - dw_ctrl_type: bun_sys::windows::DWORD, - ) -> bun_sys::windows::BOOL { - if dw_ctrl_type == bun_sys::windows::CTRL_C_EVENT { - SHOULD_ABORT.store(true, Ordering::SeqCst); - return bun_sys::windows::TRUE; - } - bun_sys::windows::FALSE - } - - pub(crate) fn install() { - #[cfg(unix)] - { - // SAFETY: libc::sigaction is #[repr(C)] POD; all-zero is a valid value (fields overwritten below). - let mut act: libc::sigaction = bun_core::ffi::zeroed(); - act.sa_sigaction = Self::posix_signal_handler as *const () as usize; - act.sa_flags = libc::SA_SIGINFO | libc::SA_RESTART | libc::SA_RESETHAND; - // SAFETY: sa_mask is a valid out-pointer; act is on the stack. - unsafe { - libc::sigemptyset(&raw mut act.sa_mask); - libc::sigaction(libc::SIGINT, &raw const act, core::ptr::null_mut()); - } - } - #[cfg(not(unix))] - { - let res = bun_sys::c::SetConsoleCtrlHandler( - Some(Self::windows_ctrl_handler), - bun_sys::windows::TRUE, - ); - if res == 0 { - if bun_core::env::IS_DEBUG { - bun_core::warn!("Failed to set abort handler\n"); - } - } - } - } - - pub(crate) fn uninstall() { - // only necessary on Windows, as on posix we pass the SA_RESETHAND flag - #[cfg(windows)] - { - // restores default Ctrl+C behavior - let _ = bun_sys::c::SetConsoleCtrlHandler(None, bun_sys::windows::FALSE); - } + aggregate_exit_code( + self.handles + .iter() + .map(|h| h.process.as_ref().map(|p| &p.status)), + ) } } @@ -943,18 +861,8 @@ pub(crate) fn run_scripts_with_filter( process: None, options: SpawnOptions { stdin: spawn::Stdio::Ignore, - #[cfg(unix)] - stdout: spawn::Stdio::Buffer, - #[cfg(not(unix))] - stdout: spawn::Stdio::Buffer(bun_core::heap::into_raw(Box::new( - bun_core::ffi::zeroed::(), - ))), - #[cfg(unix)] - stderr: spawn::Stdio::Buffer, - #[cfg(not(unix))] - stderr: spawn::Stdio::Buffer(bun_core::heap::into_raw(Box::new( - bun_core::ffi::zeroed::(), - ))), + stdout: buffered_stdio(), + stderr: buffered_stdio(), cwd: bun_paths::resolve_path::dirname::( &script.package_json_path, ) diff --git a/src/runtime/cli/install_command.rs b/src/runtime/cli/install_command.rs index 523eed71e266..0967cb445d38 100644 --- a/src/runtime/cli/install_command.rs +++ b/src/runtime/cli/install_command.rs @@ -1,5 +1,4 @@ use crate::Error; -use bun_bundler::bundle_v2::{DependenciesScanner, DependenciesScannerResult}; use bun_core::{Global, Output}; use bun_install::package_manager_real::{ CommandLineArguments, PackageManager, ROOT_PACKAGE_JSON_PATH, Subcommand, install_with_manager, @@ -7,7 +6,7 @@ use bun_install::package_manager_real::{ }; use crate::Cli; -use crate::build_command::BuildCommand; +use crate::cli::pm_update_package_json::analyze_dependencies_and_install; use crate::command::ContextData; pub(crate) struct InstallCommand; @@ -54,97 +53,8 @@ impl InstallCommand { fn install(ctx: &mut ContextData) -> Result<(), Error> { let mut cli = CommandLineArguments::parse(Subcommand::Install)?; - // The way this works: - // 1. Run the bundler on source files - // 2. Rewrite positional arguments to act identically to the developer - // typing in the dependency names - // 3. Run the install command if cli.analyze { - // `ctx` is stored as a raw `*mut ContextData`; the `on_fetch` callback - // re-enters the install path while `BuildCommand::exec` still holds the - // global `Context`, so a `&mut` here would be aliased UB. - struct Analyzer { - ctx: *mut ContextData, - cli: *mut CommandLineArguments, - } - impl bun_bundler::bundle_v2::OnDependenciesAnalyze for Analyzer { - fn on_analyze( - &mut self, - result: &mut DependenciesScannerResult<'_, '_>, - ) -> Result<(), bun_bundler::Error> { - let this = self; - // TODO: add separate argument that makes it so positionals[1..] is not done and instead the positionals are passed - // - // Process-lifetime storage for the rewritten positionals — - // `Global::exit(0)` follows immediately. - // `OnceLock` (not leaking) per PORTING.md §Forbidden. - static OWNED_KEYS: std::sync::OnceLock>> = std::sync::OnceLock::new(); - static POSITIONALS: std::sync::OnceLock> = - std::sync::OnceLock::new(); - - let owned = OWNED_KEYS.get_or_init(|| { - result - .dependencies - .keys() - .iter() - .map(|k| Box::<[u8]>::from(&**k)) - .collect() - }); - let positionals = POSITIONALS.get_or_init(|| { - let mut v: Vec<&'static [u8]> = Vec::with_capacity(owned.len() + 1); - v.push(b"install"); - for k in owned { - v.push(&**k); - } - v - }); - - // SAFETY: `this.cli` / `this.ctx` were set from live stack - // locals in `install()` whose scope encloses the entire - // `BuildCommand::exec` call (and hence this callback). The - // bundler does not touch the global `ContextData` between - // dependency-scan completion and `on_fetch` invocation, so - // forming a fresh `&mut` here is exclusive for the duration of - // `install_with_cli`. - let cli = unsafe { &mut *this.cli }; - cli.positionals = positionals.as_slice(); - // SAFETY: see above — same invariant covers `this.ctx`. - let ctx = unsafe { &mut *this.ctx }; - - install_with_cli(ctx, cli.clone()).map_err(bun_bundler::Error::from)?; - - Global::exit(0); - } - } - - // `DependenciesScanner.entry_points` is `Box<[Box<[u8]>]>`. Clone the - // argv slices into an owned buffer (small one-shot list — no perf - // concern). Captured *before* - // raw-ptr aliasing of `cli` below so the access goes through the live - // `&mut cli` borrow. - let entry_points: Box<[Box<[u8]>]> = cli.positionals[1..] - .iter() - .map(|s| Box::<[u8]>::from(*s)) - .collect(); - - // Derive raw pointers from the existing `&mut` borrows; all subsequent - // access to `ctx` / `cli` in this branch goes through these. - let ctx_ptr: *mut ContextData = ctx; - let mut analyzer = Analyzer { - ctx: ctx_ptr, - cli: &raw mut cli, - }; - - let fetcher = DependenciesScanner::new(&mut analyzer, entry_points); - - // `Command.get()` resolves to the same `*ContextData` already held in - // `ctx`; reborrow through `ctx_ptr` rather than minting a fresh - // `&'static mut` from the global static (which would alias the - // still-live `ctx` parameter under stacked borrows). - // SAFETY: `ctx_ptr` was just derived from the live `ctx: &mut - // ContextData` parameter; `ctx` is not accessed again in this branch. - BuildCommand::exec(unsafe { &mut *ctx_ptr }, Some(&fetcher))?; - return Ok(()); + return analyze_dependencies_and_install(ctx, &mut cli, b"install", &mut install_with_cli); } install_with_cli(ctx, cli) diff --git a/src/runtime/cli/link_command.rs b/src/runtime/cli/link_command.rs index a52f8c3a3429..1df8ba37d3f2 100644 --- a/src/runtime/cli/link_command.rs +++ b/src/runtime/cli/link_command.rs @@ -10,7 +10,7 @@ use bun_install::Features; use bun_install::bin_real as bin; use bun_install::lockfile_real::{Lockfile, package::Package}; use bun_install::package_manager_real::{ - self as pm, CommandLineArguments, Subcommand, attempt_to_create_package_json, + self as pm, CommandLineArguments, PackageManager, Subcommand, attempt_to_create_package_json, options::LogLevel, package_manager_options, setup_global_dir, update_package_json_and_install_with_manager, }; @@ -25,6 +25,108 @@ impl LinkCommand { } } +/// Shared by `bun link` / `bun unlink`: parse the nearest package.json into an +/// empty lockfile and validate that it declares a valid npm package name. +/// Crashes with a user-facing error otherwise. The package name is re-derived +/// by callers via `lockfile.str(&package.name)`. +pub(crate) fn load_package_for_link( + manager: &mut PackageManager, + verb: &str, +) -> crate::Result<(Lockfile, Package)> { + let mut lockfile = Lockfile::default(); + let mut package = Package::default(); + + let package_json_source = match bun_ast::to_source( + manager.original_package_json_path.as_zstr(), + Default::default(), + ) { + Ok(s) => s, + Err(e) => { + Output::err_generic( + "failed to read \"{}\" for {}: {}", + ( + BStr::new(manager.original_package_json_path.as_bytes()), + verb, + BStr::new(e.name()), + ), + ); + Global::crash(); + } + }; + lockfile.init_empty(); + + let mut resolver: () = (); + // `log_mut()` returns a borrow decoupled from `&self`; disjoint + // storage from `&mut PackageManager` (owned by the CLI `Context`). + let log = manager.log_mut(); + package.parse::<()>( + &mut lockfile, + manager, + log, + &package_json_source, + &mut resolver, + Features::FOLDER, + )?; + let name = lockfile.str(&package.name); + if name.is_empty() { + if manager.options.log_level != LogLevel::Silent { + bun_core::pretty_errorln!( + "error: package.json missing \"name\" in \"{}\"", + BStr::new(package_json_source.path.text), + ); + } + Global::crash(); + } else if !strings::is_npm_package_name(name) { + if manager.options.log_level != LogLevel::Silent { + bun_core::pretty_errorln!( + "error: invalid package.json name \"{}\" in \"{}\"", + BStr::new(name), + BStr::new(package_json_source.path.text), + ); + } + Global::crash(); + } + + Ok((lockfile, package)) +} + +/// Shared by `bun link` / `bun unlink`: open the global directory (storing it +/// in `manager.global_dir`) and create+open its `node_modules` folder. +/// Crashes with a user-facing error if `node_modules` cannot be created. +pub(crate) fn open_global_node_modules( + manager: &mut PackageManager, + ctx: &mut command::ContextData, +) -> crate::Result

{ + bin::Linker::ensure_umask(); + let explicit_global_dir: &[u8] = match &ctx.install { + Some(install_) => install_.global_dir.as_deref().unwrap_or(b""), + None => b"", + }; + manager.global_dir = Some(Dir::from_fd(package_manager_options::open_global_dir( + explicit_global_dir, + )?)); + + setup_global_dir(manager, &ctx)?; + + match manager + .global_dir + .as_ref() + .unwrap() + .make_open_path(b"node_modules", Default::default()) + { + Ok(d) => Ok(d), + Err(e) => { + if manager.options.log_level != LogLevel::Silent { + bun_core::pretty_errorln!( + "error: failed to create node_modules in global dir due to error {}", + BStr::new(e.name()), + ); + } + Global::crash(); + } + } +} + fn link(ctx: command::Context) -> crate::Result<()> { let cli = CommandLineArguments::parse(Subcommand::Link)?; let (manager, original_cwd) = match pm::init(&mut *ctx, cli, Subcommand::Link) { @@ -51,98 +153,15 @@ fn link(ctx: command::Context) -> crate::Result<()> { if manager.options.positionals.len() == 1 { // bun link - let mut lockfile = Lockfile::default(); - let mut package = Package::default(); - // Step 1. parse the nearest package.json file - { - let package_json_source = match bun_ast::to_source( - manager.original_package_json_path.as_zstr(), - Default::default(), - ) { - Ok(s) => s, - Err(e) => { - Output::err_generic( - "failed to read \"{s}\" for linking: {s}", - ( - BStr::new(manager.original_package_json_path.as_bytes()), - BStr::new(e.name()), - ), - ); - Global::crash(); - } - }; - lockfile.init_empty(); + let (lockfile, package) = load_package_for_link(manager, "linking")?; - let mut resolver: () = (); - // `log_mut()` returns a borrow decoupled from `&self`; disjoint - // storage from `&mut PackageManager` (owned by the CLI `Context`). - let log = manager.log_mut(); - package.parse::<()>( - &mut lockfile, - manager, - log, - &package_json_source, - &mut resolver, - Features::FOLDER, - )?; - let name = lockfile.str(&package.name); - if name.is_empty() { - if manager.options.log_level != LogLevel::Silent { - bun_core::pretty_errorln!( - "error: package.json missing \"name\" in \"{}\"", - BStr::new(package_json_source.path.text), - ); - } - Global::crash(); - } else if !strings::is_npm_package_name(name) { - if manager.options.log_level != LogLevel::Silent { - bun_core::pretty_errorln!( - "error: invalid package.json name \"{}\" in \"{}\"", - BStr::new(name), - BStr::new(package_json_source.path.text), - ); - } - Global::crash(); - } - } - - // Reshaped for borrowck — re-derive `name` here so its - // lifetime is tied only to `lockfile.buffers.string_bytes`, decoupled - // from `package_json_source` (dropped above). + // `name` is a slice into `lockfile.buffers.string_bytes`, decoupled + // from the helper-local package.json source. let name = lockfile.str(&package.name); // Step 2. Setup the global directory - let node_modules: Dir = 'brk: { - bin::Linker::ensure_umask(); - let explicit_global_dir: &[u8] = match &ctx.install { - Some(install_) => install_.global_dir.as_deref().unwrap_or(b""), - None => b"", - }; - manager.global_dir = Some(Dir::from_fd(package_manager_options::open_global_dir( - explicit_global_dir, - )?)); - - setup_global_dir(manager, &&mut *ctx)?; - - match manager - .global_dir - .as_ref() - .unwrap() - .make_open_path(b"node_modules", Default::default()) - { - Ok(d) => break 'brk d, - Err(e) => { - if manager.options.log_level != LogLevel::Silent { - bun_core::pretty_errorln!( - "error: failed to create node_modules in global dir due to error {}", - BStr::new(e.name()), - ); - } - Global::crash(); - } - } - }; + let node_modules: Dir = open_global_node_modules(manager, &mut *ctx)?; // Step 3a. symlink to the node_modules folder { diff --git a/src/runtime/cli/mod.rs b/src/runtime/cli/mod.rs index d1ab71efe869..d382459ecc08 100644 --- a/src/runtime/cli/mod.rs +++ b/src/runtime/cli/mod.rs @@ -386,6 +386,10 @@ pub use filter_run as FilterRun; #[path = "multi_run.rs"] pub mod multi_run; pub use multi_run as MultiRun; +#[path = "run_processes_shared.rs"] +pub(crate) mod run_processes_shared; +#[path = "workspace_helpers.rs"] +pub(crate) mod workspace_helpers; // ─── crate-local helper for param-table concatenation ──────────────────────── // `bun_clap::parse_param!` is a real proc-macro (const `Param` literal), diff --git a/src/runtime/cli/multi_run.rs b/src/runtime/cli/multi_run.rs index 0647410b1d47..8a1f05210fce 100644 --- a/src/runtime/cli/multi_run.rs +++ b/src/runtime/cli/multi_run.rs @@ -1,6 +1,6 @@ use core::ffi::{c_char, c_void}; use core::ptr; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::Ordering; use std::time::Instant; use crate::Error; @@ -14,6 +14,9 @@ use bun_paths::{self as path, PathBuffer}; use bun_resolver::package_json::{IncludeDependencies, IncludeScripts}; use crate::Command; +use crate::cli::run_processes_shared::{ + AbortHandler, SHOULD_ABORT, aggregate_exit_code, buffered_stdio, watch_or_reap, +}; use crate::filter_arg as FilterArg; use crate::run_command::RunCommand; @@ -22,8 +25,7 @@ use crate::run_command::RunCommand; #[cfg(unix)] use crate::api::bun::process::SpawnResultExt as _; use crate::api::bun::process::{ - self as spawn, Process, Rusage, SpawnOptions, SpawnProcessResult, Status, - event_loop_handle_to_ctx, + self as spawn, Process, SpawnOptions, SpawnProcessResult, Status, event_loop_handle_to_ctx, }; use bun_dotenv::Loader as DotEnvLoader; type OutputWriter = bun_core::io::Writer; @@ -248,16 +250,7 @@ impl<'a> ProcessHandle<'a> { ) }); - match process.watch_or_reap() { - Ok(_) => {} - Err(err) => { - if !process.has_exited() { - // SAFETY: all-zero is a valid Rusage (POD C struct) - let rusage = bun_core::ffi::zeroed::(); - process.on_exit(Status::Err(err), &rusage); - } - } - } + watch_or_reap(process); Ok(()) } @@ -510,84 +503,11 @@ impl<'a> State<'a> { } pub(crate) fn finalize(&self) -> u8 { - for handle in self.handles.iter() { - if let Some(proc) = &handle.process { - match &proc.status { - Status::Exited(exited) => { - if exited.code != 0 { - return exited.code; - } - } - Status::Signaled(signal) => { - return bun_sys::SignalCode(*signal).to_exit_code().unwrap_or(1); - } - _ => return 1, - } - } - } - 0 - } -} - -struct AbortHandler; - -static SHOULD_ABORT: AtomicBool = AtomicBool::new(false); - -impl AbortHandler { - #[cfg(unix)] - extern "C" fn posix_signal_handler( - _sig: i32, - _info: *const bun_sys::posix::siginfo_t, - _: *const c_void, - ) { - SHOULD_ABORT.store(true, Ordering::SeqCst); - } - - #[cfg(windows)] - extern "system" fn windows_ctrl_handler( - dw_ctrl_type: bun_sys::windows::DWORD, - ) -> bun_sys::windows::BOOL { - if dw_ctrl_type == bun_sys::windows::CTRL_C_EVENT { - SHOULD_ABORT.store(true, Ordering::SeqCst); - return bun_sys::windows::TRUE; - } - bun_sys::windows::FALSE - } - - pub(crate) fn install() { - #[cfg(unix)] - { - // bun_sys::posix::Sigaction is a re-export of libc::sigaction; construct - // via zeroed() (POD C struct) and populate sa_sigaction/sa_mask/sa_flags. - // SAFETY: all-zero is a valid `libc::sigaction`; sigemptyset/sigaction are - // FFI calls with no extra preconditions beyond valid pointers. - unsafe { - let mut action: bun_sys::posix::Sigaction = bun_core::ffi::zeroed(); - action.sa_sigaction = Self::posix_signal_handler as *const () as usize; - libc::sigemptyset(&raw mut action.sa_mask); - action.sa_flags = (libc::SA_SIGINFO | libc::SA_RESTART | libc::SA_RESETHAND) as _; - bun_sys::posix::sigaction(libc::SIGINT, &raw const action, core::ptr::null_mut()); - } - } - #[cfg(not(unix))] - { - let res = bun_sys::windows::SetConsoleCtrlHandler( - Some(Self::windows_ctrl_handler), - bun_sys::windows::TRUE, - ); - if res == 0 { - if bun_core::env::IS_DEBUG { - bun_core::warn!("Failed to set abort handler\n"); - } - } - } - } - - pub(crate) fn uninstall() { - #[cfg(windows)] - { - let _ = bun_sys::windows::SetConsoleCtrlHandler(None, bun_sys::windows::FALSE); - } + aggregate_exit_code( + self.handles + .iter() + .map(|h| h.process.as_ref().map(|p| &p.status)), + ) } } @@ -1140,18 +1060,8 @@ pub(crate) fn run(ctx: &mut Command::ContextData) -> Result(), - ))), - #[cfg(unix)] - stderr: spawn::Stdio::Buffer, - #[cfg(not(unix))] - stderr: spawn::Stdio::Buffer(bun_core::heap::into_raw(Box::new( - bun_core::ffi::zeroed::(), - ))), + stdout: buffered_stdio(), + stderr: buffered_stdio(), cwd: config.cwd.clone(), #[cfg(windows)] windows: spawn::WindowsOptions { diff --git a/src/runtime/cli/outdated_command.rs b/src/runtime/cli/outdated_command.rs index 48b36fb19553..bc493503b881 100644 --- a/src/runtime/cli/outdated_command.rs +++ b/src/runtime/cli/outdated_command.rs @@ -8,16 +8,12 @@ use bun_core::{Global, Output}; use bun_glob as glob; use bun_install::dependency::{self, Behavior}; use bun_install::lockfile::package::PackageColumns as _; -use bun_install::lockfile::{LoadResult, LoadStep}; -use bun_install::package_manager::{ - self, LogLevel, ManifestLoad, Subcommand, WorkspaceFilter, populate_manifest_cache, -}; +use bun_install::package_manager::{self, ManifestLoad, Subcommand, populate_manifest_cache}; use bun_install::{CommandLineArguments, DependencyID, PackageID, PackageManager, resolution}; -use bun_paths::{self as path, PathBuffer}; -use bun_resolver::fs::FileSystem; use bun_wyhash::hash; use crate::Command; +use crate::cli::workspace_helpers; pub(crate) struct OutdatedCommand; @@ -89,68 +85,7 @@ impl OutdatedCommand { original_cwd: &[u8], manager: &mut PackageManager, ) -> crate::Result<()> { - // Reshaped for borrowck — `load_from_cwd` would otherwise alias - // `PackageManager` with its `lockfile` field. Project disjoint - // raw pointers from the singleton first; `load_from_cwd` only reads - // `manager.options` / migration helpers and never re-borrows - // `manager.lockfile` through the `pm` argument. - let pm_ptr: *mut PackageManager = manager; - let not_silent = manager.options.log_level != LogLevel::Silent; - let log_ptr: *mut bun_ast::Log = manager.log; - - // SAFETY: `lockfile` is the owned `Box` field on the singleton; - // no other live `&mut Lockfile` exists at this point. - let lockfile: &mut bun_install::lockfile::Lockfile = unsafe { &mut *(*pm_ptr).lockfile }; - // SAFETY: `manager.log` is set non-null by `PackageManager::init`. - let log = unsafe { &mut *log_ptr }; - match lockfile.load_from_cwd::( - // SAFETY: see comment above — `load_from_cwd` accesses `manager` - // fields disjoint from `lockfile`. - Some(unsafe { &mut *pm_ptr }), - log, - ) { - LoadResult::NotFound => { - if not_silent { - Output::err_generic("missing lockfile, nothing outdated", ()); - } - Global::crash(); - } - LoadResult::Err(cause) => { - if not_silent { - match cause.step { - LoadStep::OpenFile => Output::err_generic( - "failed to open lockfile: {s}", - (cause.value.name(),), - ), - LoadStep::ParseFile => Output::err_generic( - "failed to parse lockfile: {s}", - (cause.value.name(),), - ), - LoadStep::ReadFile => Output::err_generic( - "failed to read lockfile: {s}", - (cause.value.name(),), - ), - LoadStep::Migrating => Output::err_generic( - "failed to migrate lockfile: {s}", - (cause.value.name(),), - ), - } - if ctx.log_ref().has_errors() { - // SAFETY: `log_ptr` aliases `manager.log` which is the - // `*logger.Log` borrowed from `Command::Context`; no - // other `&mut Log` is live here. - let _ = - unsafe { (*log_ptr).print(std::ptr::from_mut(Output::error_writer())) }; - } - } - Global::crash(); - } - LoadResult::Ok(_) => { - // `load_from_cwd(&mut self, ..)` populates the - // lockfile in place, so the `ok.lockfile: &mut Lockfile` reborrow - // is the same storage and no reassignment is needed. - } - } + workspace_helpers::load_lockfile_or_crash(ctx, manager); if Output::enable_ansi_colors_stdout() { Self::outdated_dispatch::(original_cwd, manager) @@ -165,14 +100,15 @@ impl OutdatedCommand { ) -> crate::Result<()> { if !manager.options.filter_patterns.is_empty() { let filters = manager.options.filter_patterns; - let workspace_pkg_ids = Self::find_matching_workspaces(original_cwd, manager, filters); + let workspace_pkg_ids = + workspace_helpers::find_matching_workspaces(original_cwd, manager, filters); populate_manifest_cache::populate_manifest_cache( manager, populate_manifest_cache::Packages::Ids(&workspace_pkg_ids), )?; Self::print_outdated_info_table::(manager, &workspace_pkg_ids, true) } else if manager.options.do_.recursive() { - let all_workspaces = Self::get_all_workspaces(manager); + let all_workspaces = workspace_helpers::get_all_workspaces(manager); populate_manifest_cache::populate_manifest_cache( manager, populate_manifest_cache::Packages::Ids(&all_workspaces), @@ -194,118 +130,6 @@ impl OutdatedCommand { } } - fn get_all_workspaces(manager: &PackageManager) -> Vec { - let lockfile = &manager.lockfile; - let packages = lockfile.packages.slice(); - let pkg_resolutions = packages.items_resolution(); - - let mut workspace_pkg_ids: Vec = Vec::new(); - for (pkg_id, resolution) in pkg_resolutions.iter().enumerate() { - if resolution.tag != resolution::Tag::Workspace - && resolution.tag != resolution::Tag::Root - { - continue; - } - workspace_pkg_ids.push(pkg_id as PackageID); - } - workspace_pkg_ids - } - - fn find_matching_workspaces( - original_cwd: &[u8], - manager: &PackageManager, - filters: &[&[u8]], - ) -> Vec { - let lockfile = &manager.lockfile; - let packages = lockfile.packages.slice(); - let pkg_names = packages.items_name(); - let pkg_resolutions = packages.items_resolution(); - let string_buf = lockfile.buffers.string_bytes.as_slice(); - - let mut workspace_pkg_ids: Vec = Vec::new(); - for (pkg_id, resolution) in pkg_resolutions.iter().enumerate() { - if resolution.tag != resolution::Tag::Workspace - && resolution.tag != resolution::Tag::Root - { - continue; - } - workspace_pkg_ids.push(pkg_id as PackageID); - } - - let mut path_buf = PathBuffer::uninit(); - - let converted_filters: Vec = filters - .iter() - .map(|filter| { - bun_core::handle_oom(WorkspaceFilter::init(filter, original_cwd, &mut path_buf.0)) - }) - .collect(); - // `defer { filter.deinit(allocator); allocator.free(...) }` — implicit via Drop. - - // SAFETY: `FileSystem::init` runs during `PackageManager::init` so the - // process-singleton is populated. - let top_level_dir = FileSystem::get().top_level_dir; - - // move all matched workspaces to front of array - let mut i: usize = 0; - while i < workspace_pkg_ids.len() { - let workspace_pkg_id = workspace_pkg_ids[i]; - - let matched = 'matched: { - for filter in &converted_filters { - match filter { - WorkspaceFilter::Path(pattern) => { - if pattern.is_empty() { - continue; - } - let res = &pkg_resolutions[workspace_pkg_id as usize]; - let res_path: &[u8] = match res.tag { - resolution::Tag::Workspace => { - // Borrow the field in-place so the returned slice (which may - // point into the inline small-string storage) stays valid. - res.workspace().slice(string_buf) - } - resolution::Tag::Root => top_level_dir, - _ => unreachable!(), - }; - - let abs_res_path = path::resolve_path::join_abs_string_buf::< - path::platform::Posix, - >( - top_level_dir, &mut path_buf.0, &[res_path] - ); - - if !glob::r#match( - pattern, - strings::without_trailing_slash(abs_res_path), - ) - .matches() - { - break 'matched false; - } - } - WorkspaceFilter::Name(pattern) => { - let name = pkg_names[workspace_pkg_id as usize].slice(string_buf); - if !glob::r#match(pattern, name).matches() { - break 'matched false; - } - } - WorkspaceFilter::All => {} - } - } - true - }; - - if matched { - i += 1; - } else { - workspace_pkg_ids.swap_remove(i); - } - } - - workspace_pkg_ids - } - fn group_catalog_dependencies( manager: &PackageManager, outdated_items: &[OutdatedInfo], diff --git a/src/runtime/cli/pack_command.rs b/src/runtime/cli/pack_command.rs index ae5bfc7610f9..4cf2fa85aedc 100644 --- a/src/runtime/cli/pack_command.rs +++ b/src/runtime/cli/pack_command.rs @@ -33,9 +33,7 @@ use bun_glob::matcher::MatchResult as GlobMatchResult; use bun_paths::resolve_path; use bun_semver as Semver; use bun_sha_hmac::sha; -use bun_sys::{ - self, CloseOnDrop, Dir, Fd, FdDirExt as _, FdExt as _, File, dir_iterator as DirIterator, -}; +use bun_sys::{self, Dir, Fd, FdDirExt as _, FdExt as _, File, dir_iterator as DirIterator}; // ─────────────────────────────────────────────────────────────────────────── // local shims for upstream-stub gaps @@ -1906,7 +1904,7 @@ fn new_boxed_buffered_file_reader(file: bun_sys::File) -> Box Option<&[u8]> { // pack() // ─────────────────────────────────────────────────────────────────────────── -// Const generics cannot vary the -// return type directly, so both instantiations return an Option that is -// `Some` only when FOR_PUBLISH == true. -pub(crate) type PackReturn<'a, const FOR_PUBLISH: bool> = Option>; - -pub(crate) fn pack( - ctx: &mut Context<'_>, +/// Reads `abs_package_json_path` through the workspace package.json cache; +/// read/parse failures are fatal. Unlike +/// `WorkspacePackageJSONCache::get_with_path_or_exit`, this keeps pack's +/// error wording and ordering (`Output::err` first, then the log printed +/// unconditionally on parse errors), matching the `pack_command.zig` +/// reference. +fn load_package_json_or_exit<'a>( + manager_ptr: *mut PackageManager, abs_package_json_path: &ZStr, -) -> Result, PackError> { - // Note: reshaped for borrowck — - // `ctx`-whole calls (`run_lifecycle_script(ctx, …)`, - // `iterate_bundled_deps(ctx, …)`) overlap the manager borrow. - // Round-trip the field through a raw - // pointer so the long-lived `manager` reborrow is decoupled from `ctx`; - // every interleaved `ctx` access touches disjoint fields (`command_ctx`, - // `bundled_deps`, `stats`) or only reads `manager` via `pm_*` helpers. - let manager_ptr: *mut PackageManager = &raw mut *ctx.manager; - // SAFETY: `ctx.manager` is the sole `&mut PackageManager`; CLI is - // single-threaded and no callee retains a conflicting borrow. - let manager: &mut PackageManager = unsafe { &mut *manager_ptr }; - let log_level = manager.options.log_level; - let bump = pack_bump(); +) -> &'a mut WorkspacePackageJSONCache::MapEntry { // Note: `workspace_package_json_cache` and `log` are disjoint fields on // `PackageManager`; route through raw-pointer field projections so the // two `&mut` borrows don't conflict. - let mut json = match pm_workspace_cache(manager_ptr).get_with_path( + match pm_workspace_cache(manager_ptr).get_with_path( pm_log(manager_ptr), abs_package_json_path.as_bytes(), WorkspacePackageJSONCache::GetJSONOptions { @@ -2011,7 +1997,32 @@ pub(crate) fn pack( Global::crash(); } WorkspacePackageJSONCache::GetResult::Entry(entry) => entry, - }; + } +} + +// Const generics cannot vary the +// return type directly, so both instantiations return an Option that is +// `Some` only when FOR_PUBLISH == true. +pub(crate) type PackReturn<'a, const FOR_PUBLISH: bool> = Option>; + +pub(crate) fn pack( + ctx: &mut Context<'_>, + abs_package_json_path: &ZStr, +) -> Result, PackError> { + // Note: reshaped for borrowck — + // `ctx`-whole calls (`run_lifecycle_script(ctx, …)`, + // `iterate_bundled_deps(ctx, …)`) overlap the manager borrow. + // Round-trip the field through a raw + // pointer so the long-lived `manager` reborrow is decoupled from `ctx`; + // every interleaved `ctx` access touches disjoint fields (`command_ctx`, + // `bundled_deps`, `stats`) or only reads `manager` via `pm_*` helpers. + let manager_ptr: *mut PackageManager = &raw mut *ctx.manager; + // SAFETY: `ctx.manager` is the sole `&mut PackageManager`; CLI is + // single-threaded and no callee retains a conflicting borrow. + let manager: &mut PackageManager = unsafe { &mut *manager_ptr }; + let log_level = manager.options.log_level; + let bump = pack_bump(); + let mut json = load_package_json_or_exit(manager_ptr, abs_package_json_path); if FOR_PUBLISH { if let Some(config) = json.root.get(b"publishConfig") { @@ -2241,33 +2252,7 @@ pub(crate) fn pack( let _ = pm_workspace_cache(manager_ptr).map.remove(cache_key); // Re-read package.json from disk - json = match pm_workspace_cache(manager_ptr).get_with_path( - pm_log(manager_ptr), - abs_package_json_path.as_bytes(), - WorkspacePackageJSONCache::GetJSONOptions { - guess_indentation: true, - ..Default::default() - }, - ) { - WorkspacePackageJSONCache::GetResult::ReadErr(err) => { - Output::err( - err, - "failed to read package.json: {}", - format_args!("{}", bstr::BStr::new(abs_package_json_path.as_bytes())), - ); - Global::crash(); - } - WorkspacePackageJSONCache::GetResult::ParseErr(err) => { - Output::err( - err, - "failed to parse package.json: {}", - format_args!("{}", bstr::BStr::new(abs_package_json_path.as_bytes())), - ); - let _ = pm_log(manager_ptr).print(std::ptr::from_mut(Output::error_writer())); - Global::crash(); - } - WorkspacePackageJSONCache::GetResult::Entry(entry) => entry, - }; + json = load_package_json_or_exit(manager_ptr, abs_package_json_path); // Re-validate private flag after scripts may have modified it. if FOR_PUBLISH { @@ -2664,11 +2649,9 @@ pub(crate) fn pack( node = Some(progress.start(b"", pack_queue.count() + bundled_pack_queue.count() + 1)); node.as_mut().expect("infallible: progress active").unit = Progress::Unit::Files; } - // Note: the loop bodies' only early exits are `continue` - // and `Global::crash()` (never returns, no - // unwinding). `scopeguard` captures of `&mut node` overlap the inline - // uses below, so call `complete_one()` explicitly at every loop-body - // exit and `end()` once after the loops. + // Note: `scopeguard` captures of `&mut node` would overlap the inline + // uses below, so call `complete_one()` explicitly after each archived + // entry and `end()` once after the queues drain. entry = archive_package_json( ctx, @@ -2685,142 +2668,41 @@ pub(crate) fn pack( .complete_one(); } - while let Some(item) = pack_queue.remove_or_null() { - let file = match bun_sys::openat( - Fd::from_std_dir(&root_dir), - &item.path, - bun_sys::O::RDONLY, - 0, - ) { - Ok(f) => f, - Err(err) => { - if item.optional { - ctx.stats.total_files -= 1; - if log_level.show_progress() { - node.as_mut() - .expect("infallible: progress active") - .complete_one(); - } - continue; - } - Output::err( - err, - "failed to open file: \"{}\"", - format_args!("{}", bstr::BStr::new(item.path.as_bytes())), - ); - Global::crash(); - } - }; - - let fd: Fd = match file - .make_lib_uv_owned_for_syscall(bun_sys::Tag::open, bun_sys::ErrorCase::CloseOnFail) - { - Ok(fd) => fd, - Err(err) => { - Output::err( - err, - "failed to open file: \"{}\"", - format_args!("{}", bstr::BStr::new(item.path.as_bytes())), - ); - Global::crash(); - } - }; - - let _close_fd = CloseOnDrop::new(fd); - - let stat = match bun_sys::sys_uv::fstat(fd) { - Ok(s) => s, - Err(err) => { - Output::err( - err, - "failed to stat file: \"{}\"", - format_args!("{}", bstr::BStr::new(item.path.as_bytes())), - ); - Global::crash(); - } - }; - - pack_list.push(PackListEntry { - subpath: ZBox::from_bytes(item.path.as_bytes()), - size: usize::try_from(stat.st_size).expect("int cast"), - }); - - entry = add_archive_entry( - ctx, - fd, - &stat, - &item.path, - &mut read_buf, - &mut file_reader, - // SAFETY: `archive` is the non-null `*mut Archive` returned by - // `Archive::write_new()` above; only this thread accesses it. - unsafe { &mut *archive }, - entry, - &mut print_buf, - &bins, - )?; - - if log_level.show_progress() { - node.as_mut() - .expect("infallible: progress active") - .complete_one(); - } - } - - while let Some(item) = bundled_pack_queue.remove_or_null() { - let file = match root_dir.open_file(&item.path, bun_sys::O::RDONLY, 0) { - Ok(f) => f, - Err(err) => { - if item.optional { - ctx.stats.total_files -= 1; - if log_level.show_progress() { - node.as_mut() - .expect("infallible: progress active") - .complete_one(); - } - continue; - } - Output::err( - err, - "failed to open file: \"{}\"", - format_args!("{}", bstr::BStr::new(item.path.as_bytes())), - ); - Global::crash(); - } - }; - let stat = match file.stat() { - Ok(s) => s, - Err(err) => { - Output::err( - err, - "failed to stat file: \"{}\"", - format_args!("{}", file.handle), - ); - Global::crash(); - } - }; - - entry = add_archive_entry( - ctx, - file.handle, - &stat, - &item.path, - &mut read_buf, - &mut file_reader, - // SAFETY: `archive` is the non-null `*mut Archive` returned by - // `Archive::write_new()` above; only this thread accesses it. - unsafe { &mut *archive }, - entry, - &mut print_buf, - &bins, - )?; + entry = archive_pack_queue( + ctx, + &mut pack_queue, + PackQueueOpenMode::UvOwnedFd, + &root_dir, + Some(&mut pack_list), + &mut read_buf, + &mut file_reader, + // SAFETY: `archive` is the non-null `*mut Archive` returned by + // `Archive::write_new()` above; only this thread accesses it. + unsafe { &mut *archive }, + entry, + &mut print_buf, + &bins, + log_level, + &mut node, + )?; - if log_level.show_progress() { - node.as_mut() - .expect("infallible: progress active") - .complete_one(); - } - } + entry = archive_pack_queue( + ctx, + &mut bundled_pack_queue, + PackQueueOpenMode::PlainFile, + &root_dir, + None, + &mut read_buf, + &mut file_reader, + // SAFETY: `archive` is the non-null `*mut Archive` returned by + // `Archive::write_new()` above; only this thread accesses it. + unsafe { &mut *archive }, + entry, + &mut print_buf, + &bins, + log_level, + &mut node, + )?; if log_level.show_progress() { if let Some(n) = node.as_mut() { @@ -3275,6 +3157,144 @@ fn archive_package_json( Ok(entry.clear()) } +/// How [`archive_pack_queue`] opens each queued file. +#[derive(Clone, Copy, PartialEq, Eq)] +enum PackQueueOpenMode { + /// Open with `bun_sys::openat` and convert to a libuv-owned descriptor + /// stat'd through `sys_uv`. + UvOwnedFd, + /// Open relative to `root_dir` as a plain `File`. + PlainFile, +} + +/// Drains `queue`, archiving each file via [`add_archive_entry`]; see +/// [`PackQueueOpenMode`] for how each file is opened. Each entry is also +/// appended to `pack_list` when provided. +/// +/// The loop body's only early exits are `continue` and `Global::crash()` +/// (never returns, no unwinding), so `node.complete_one()` is called +/// explicitly at every loop-body exit instead of via a scope guard. +fn archive_pack_queue( + ctx: &mut Context<'_>, + queue: &mut PackQueue, + open_mode: PackQueueOpenMode, + root_dir: &Dir, + mut pack_list: Option<&mut PackList>, + read_buf: &mut [u8], + file_reader: &mut BufferedFileReader, + archive: &mut Archive, + mut entry: *mut ArchiveEntry, + print_buf: &mut Vec, + bins: &[BinInfo], + log_level: LogLevel, + node: &mut Option<&mut Progress::Node>, +) -> Result<*mut ArchiveEntry, AllocError> { + let uv_owned_fd = open_mode == PackQueueOpenMode::UvOwnedFd; + while let Some(item) = queue.remove_or_null() { + let opened = if uv_owned_fd { + bun_sys::openat( + Fd::from_std_dir(root_dir), + &item.path, + bun_sys::O::RDONLY, + 0, + ) + .map(File::from_fd) + } else { + root_dir.open_file(&item.path, bun_sys::O::RDONLY, 0) + }; + let file = match opened { + Ok(f) => f, + Err(err) => { + if item.optional { + ctx.stats.total_files -= 1; + if log_level.show_progress() { + node.as_mut() + .expect("infallible: progress active") + .complete_one(); + } + continue; + } + Output::err( + err, + "failed to open file: \"{}\"", + format_args!("{}", bstr::BStr::new(item.path.as_bytes())), + ); + Global::crash(); + } + }; + + let file = if uv_owned_fd { + match file + .into_raw() + .make_lib_uv_owned_for_syscall(bun_sys::Tag::open, bun_sys::ErrorCase::CloseOnFail) + { + Ok(fd) => File::from_fd(fd), + Err(err) => { + Output::err( + err, + "failed to open file: \"{}\"", + format_args!("{}", bstr::BStr::new(item.path.as_bytes())), + ); + Global::crash(); + } + } + } else { + file + }; + + let stat = match if uv_owned_fd { + bun_sys::sys_uv::fstat(file.handle) + } else { + file.stat() + } { + Ok(s) => s, + Err(err) => { + if uv_owned_fd { + Output::err( + err, + "failed to stat file: \"{}\"", + format_args!("{}", bstr::BStr::new(item.path.as_bytes())), + ); + } else { + Output::err( + err, + "failed to stat file: \"{}\"", + format_args!("{}", file.handle), + ); + } + Global::crash(); + } + }; + + if let Some(pack_list) = pack_list.as_deref_mut() { + pack_list.push(PackListEntry { + subpath: ZBox::from_bytes(item.path.as_bytes()), + size: usize::try_from(stat.st_size).expect("int cast"), + }); + } + + entry = add_archive_entry( + ctx, + file.handle, + &stat, + &item.path, + read_buf, + file_reader, + archive, + entry, + print_buf, + bins, + )?; + + if log_level.show_progress() { + node.as_mut() + .expect("infallible: progress active") + .complete_one(); + } + } + Ok(entry) +} + fn add_archive_entry( ctx: &mut Context<'_>, file: Fd, diff --git a/src/runtime/cli/pm_update_package_json.rs b/src/runtime/cli/pm_update_package_json.rs index a19296c5ab90..3c0d47c8fc61 100644 --- a/src/runtime/cli/pm_update_package_json.rs +++ b/src/runtime/cli/pm_update_package_json.rs @@ -17,7 +17,7 @@ use bun_install::package_manager_real::{Subcommand, update_package_json_and_inst use crate::build_command::BuildCommand; use crate::cli::Cli; -use crate::command::{self, Context, ContextData}; +use crate::command::{Context, ContextData}; pub fn update_package_json_and_install_catch_error( ctx: Context, @@ -49,96 +49,111 @@ pub fn update_package_json_and_install(ctx: Context, subcommand: Subcommand) -> // `parse` requires ``, expand to a `match`. let mut cli = CommandLineArguments::parse(subcommand)?; - // The way this works: - // 1. Run the bundler on source files - // 2. Rewrite positional arguments to act identically to the developer - // typing in the dependency names - // 3. Run the install command if cli.analyze { - // `ctx`/`cli` are stored as raw `*mut` because - // `BuildCommand::exec` holds `command::get()` (the same `ContextData`) across - // the `on_fetch` callback, and `DependenciesScanner.entry_points` owns a copy - // of `cli.positionals[1..]` for the duration of the scan; storing `&mut` here - // would assert exclusivity we don't have. - struct Analyzer { - ctx: *mut ContextData, - cli: *mut CommandLineArguments, - subcommand: Subcommand, - } - impl bun_bundler::bundle_v2::OnDependenciesAnalyze for Analyzer { - fn on_analyze( - &mut self, - result: &mut DependenciesScannerResult<'_, '_>, - ) -> Result<(), bun_bundler::Error> { - let this = self; - // TODO: add separate argument that makes it so positionals[1..] is not done and instead the positionals are passed - // - // Process-lifetime storage for the rewritten positionals — - // `Global::exit(0)` follows immediately. `OnceLock` (not - // leaked). - static OWNED_KEYS: std::sync::OnceLock>> = std::sync::OnceLock::new(); - static POSITIONALS: std::sync::OnceLock> = - std::sync::OnceLock::new(); + return analyze_dependencies_and_install(ctx, &mut cli, b"add", &mut |ctx, cli| { + update_package_json_and_install_and_cli(ctx, subcommand, cli).map_err(Into::into) + }); + } + + update_package_json_and_install_and_cli(ctx, subcommand, cli).map_err(Into::into) +} + +/// Shared body of the `cli.analyze` branch of `bun install` / `bun add`: +/// 1. Run the bundler's dependency scanner over the positional entry points +/// 2. Rewrite the positionals to `[verb, ...discovered dependency names]`, +/// acting identically to the developer typing in the dependency names +/// 3. Re-enter the install path via `install` and exit the process +pub(crate) fn analyze_dependencies_and_install( + ctx: &mut ContextData, + cli: &mut CommandLineArguments, + verb: &'static [u8], + install: &mut dyn FnMut(&mut ContextData, CommandLineArguments) -> Result<(), Error>, +) -> Result<(), Error> { + // `ctx`/`cli` are stored as raw `*mut` because `BuildCommand::exec` holds + // the global `Context` (the same `ContextData`) across the `on_analyze` + // callback, and `DependenciesScanner.entry_points` owns a copy of + // `cli.positionals[1..]` for the duration of the scan; storing `&mut` + // here would assert exclusivity we don't have. + struct Analyzer<'a> { + ctx: *mut ContextData, + cli: *mut CommandLineArguments, + verb: &'static [u8], + install: &'a mut dyn FnMut(&mut ContextData, CommandLineArguments) -> Result<(), Error>, + } + impl bun_bundler::bundle_v2::OnDependenciesAnalyze for Analyzer<'_> { + fn on_analyze( + &mut self, + result: &mut DependenciesScannerResult<'_, '_>, + ) -> Result<(), bun_bundler::Error> { + let this = self; + // Process-lifetime storage for the rewritten positionals — + // `Global::exit(0)` follows immediately. + // `OnceLock` (not leaking) per PORTING.md §Forbidden. + static OWNED_KEYS: std::sync::OnceLock>> = std::sync::OnceLock::new(); + static POSITIONALS: std::sync::OnceLock> = + std::sync::OnceLock::new(); - let owned = OWNED_KEYS.get_or_init(|| { - result - .dependencies - .keys() - .iter() - .map(|k| Box::<[u8]>::from(&**k)) - .collect() - }); - let positionals = POSITIONALS.get_or_init(|| { - let mut v: Vec<&'static [u8]> = Vec::with_capacity(owned.len() + 1); - v.push(b"add"); - for k in owned { - v.push(&**k); - } - v - }); + let owned = OWNED_KEYS.get_or_init(|| { + result + .dependencies + .keys() + .iter() + .map(|k| Box::<[u8]>::from(&**k)) + .collect() + }); + let positionals = POSITIONALS.get_or_init(|| { + let mut v: Vec<&'static [u8]> = Vec::with_capacity(owned.len() + 1); + v.push(this.verb); + for k in owned { + v.push(&**k); + } + v + }); - // SAFETY: `this.cli` / `this.ctx` were set from live stack locals in - // `update_package_json_and_install` whose scope encloses the entire - // `BuildCommand::exec` call (and hence this callback). The bundler has - // finished reading `entry_points` before invoking `on_fetch`, and this - // callback never returns (`Global::exit` below), so forming fresh `&mut` - // here is exclusive for the remainder of the process. - let cli = unsafe { &mut *this.cli }; - cli.positionals = positionals.as_slice(); - // SAFETY: `this.ctx` points to the `ctx` stack local in - // `update_package_json_and_install`, whose frame outlives this - // callback; `Global::exit` below makes this `&mut` exclusive for - // the remainder of the process. - let ctx = unsafe { &mut *this.ctx }; + // SAFETY: `this.cli` / `this.ctx` were set from live locals in + // `analyze_dependencies_and_install`'s caller, whose scope + // encloses the entire `BuildCommand::exec` call (and hence this + // callback). The bundler does not touch the global `ContextData` + // between dependency-scan completion and `on_analyze` invocation, + // so forming a fresh `&mut` here is exclusive for the duration of + // the `install` continuation. + let cli = unsafe { &mut *this.cli }; + cli.positionals = positionals.as_slice(); + // SAFETY: see above — same invariant covers `this.ctx`. + let ctx = unsafe { &mut *this.ctx }; - update_package_json_and_install_and_cli(ctx, this.subcommand, cli.clone()) - .map_err(crate::Error::from)?; + (this.install)(ctx, cli.clone()).map_err(bun_bundler::Error::from)?; - Global::exit(0); - } + Global::exit(0); } + } - // Note: `DependenciesScanner.entry_points` is `Box<[Box<[u8]>]>`. - // Clone the argv slices into an owned - // buffer (small one-shot list — no perf concern) so `cli` is not borrowed across - // the `&mut analyzer` setup. - let entry_points: Box<[Box<[u8]>]> = cli.positionals[1..] - .iter() - .map(|s| Box::<[u8]>::from(*s)) - .collect(); - - let mut analyzer = Analyzer { - ctx: std::ptr::from_mut::(ctx), - cli: &raw mut cli, - subcommand, - }; + // `DependenciesScanner.entry_points` is `Box<[Box<[u8]>]>`. Clone the + // argv slices into an owned buffer (small one-shot list — no perf + // concern). Captured *before* raw-ptr aliasing of `cli` below so the + // access goes through the live `&mut cli` borrow. + let entry_points: Box<[Box<[u8]>]> = cli.positionals[1..] + .iter() + .map(|s| Box::<[u8]>::from(*s)) + .collect(); - let fetcher = DependenciesScanner::new(&mut analyzer, entry_points); + // Derive raw pointers from the existing `&mut` borrows; all subsequent + // access to `ctx` / `cli` in this function goes through these. + let ctx_ptr: *mut ContextData = ctx; + let mut analyzer = Analyzer { + ctx: ctx_ptr, + cli, + verb, + install, + }; - // This runs the bundler. - BuildCommand::exec(command::get(), Some(&fetcher))?; - return Ok(()); - } + let fetcher = DependenciesScanner::new(&mut analyzer, entry_points); - update_package_json_and_install_and_cli(ctx, subcommand, cli).map_err(Into::into) + // `Command.get()` resolves to the same `*ContextData` already held in + // `ctx`; reborrow through `ctx_ptr` rather than minting a fresh + // `&'static mut` from the global static (which would alias the + // still-live `ctx` parameter under stacked borrows). + // SAFETY: `ctx_ptr` was just derived from the live `ctx: &mut + // ContextData` parameter; `ctx` is not accessed again in this function. + BuildCommand::exec(unsafe { &mut *ctx_ptr }, Some(&fetcher)) } diff --git a/src/runtime/cli/repl.rs b/src/runtime/cli/repl.rs index dcce6191a6f9..408b77fccee5 100644 --- a/src/runtime/cli/repl.rs +++ b/src/runtime/cli/repl.rs @@ -580,6 +580,16 @@ enum ReplResult { SkipEval, } +/// How `evaluate_to_value` reports promise rejections and interrupts. +/// Mirrors the difference between repl.zig's evaluateAndPrint (sets `_error` +/// on globalThis, prints a newline on interrupt) and evaluateAndCopy (does +/// neither). +#[derive(Clone, Copy, PartialEq, Eq)] +enum ReportMode { + Print, + Copy, +} + fn cmd_help(repl: &mut Repl, _: &[u8]) -> ReplResult { repl.print(format_args!( "\n{}REPL Commands:{}\n", @@ -1255,19 +1265,20 @@ impl<'a> Repl<'a> { // JavaScript Evaluation // ======================================================================== - fn evaluate_and_print(&mut self, code: &[u8]) { - let Some(global) = self.global else { - return; - }; - let Some(vm) = self.vm else { - return; - }; + /// Run `code` through the interactive REPL pipeline: transform_for_repl, + /// evaluate, await any async IIFE promise (with Ctrl+C signal handling), + /// unwrap the `{ value: expr }` wrapper, then store the result and set `_` + /// on globalThis. Returns `None` when the outcome was already reported + /// (errors, interrupts, or raw-evaluation fallback). + fn evaluate_to_value(&mut self, code: &[u8], mode: ReportMode) -> Option { + let global = self.global?; + let vm = self.vm?; // Transform the code using REPL mode (hoists declarations, wraps result in { value: expr }) let Some(transformed_code) = self.transform_for_repl(code) else { // Transform failed, try evaluating raw code (for syntax errors, etc.) self.evaluate_raw(code); - return; + return None; }; // Evaluate the transformed code @@ -1289,7 +1300,7 @@ impl<'a> Repl<'a> { if !exception.is_undefined() && !exception.is_null() { self.set_last_error(exception); self.print_js_error(exception); - return; + return None; } // Handle async IIFE results - wait for promise to resolve @@ -1313,7 +1324,7 @@ impl<'a> Repl<'a> { global.clear_termination_exception(); self.print(format_args!("\n")); self.disable_signals_during_wait(); - return; + return None; } // SAFETY: `vm.jsc_vm` is the live JSC VM handle for this thread. @@ -1326,18 +1337,22 @@ impl<'a> Repl<'a> { PromiseStatus::Rejected => { let rejection = jsc::JSPromise::opaque_mut(promise).result(jsc_vm_ref); self.set_last_error(rejection); - // Set _error on the global object - let global_this = global.to_js_value(); - global_this.put(global, b"_error", rejection); + if mode == ReportMode::Print { + // Set _error on the global object + let global_this = global.to_js_value(); + global_this.put(global, b"_error", rejection); + } self.print_js_error(rejection); self.disable_signals_during_wait(); - return; + return None; } PromiseStatus::Pending => { // Interrupted by signal or timed out - self.print(format_args!("\n")); + if mode == ReportMode::Print { + self.print(format_args!("\n")); + } self.disable_signals_during_wait(); - return; + return None; } } self.disable_signals_during_wait(); @@ -1357,7 +1372,7 @@ impl<'a> Repl<'a> { self.set_last_error(exc); self.print_js_error(exc); vm.as_mut().tick(); - return; + return None; } }; if let Some(value) = maybe_value { @@ -1365,7 +1380,7 @@ impl<'a> Repl<'a> { } } - // Store and print result + // Store the result self.set_last_result(actual_result); // Set _ to the last result (only if not undefined) @@ -1375,6 +1390,14 @@ impl<'a> Repl<'a> { global_this.put(global, b"_", actual_result); } + Some(actual_result) + } + + fn evaluate_and_print(&mut self, code: &[u8]) { + let Some(actual_result) = self.evaluate_to_value(code, ReportMode::Print) else { + return; + }; + if actual_result.is_undefined() { if self.use_colors { self.print(format_args!("{}undefined{}\n", Color::DIM, Color::RESET)); @@ -1386,7 +1409,9 @@ impl<'a> Repl<'a> { } // Tick the event loop to handle any pending work - vm.as_mut().tick(); + if let Some(vm) = self.vm { + vm.as_mut().tick(); + } } /// Evaluate a script from `bun repl -e/--eval` or `-p/--print` non-interactively. @@ -1566,104 +1591,20 @@ impl<'a> Repl<'a> { /// Evaluate code and copy the result to clipboard instead of printing it fn evaluate_and_copy(&mut self, code: &[u8]) { - let Some(global) = self.global else { + let Some(actual_result) = self.evaluate_to_value(code, ReportMode::Copy) else { return; }; - let Some(vm) = self.vm else { - return; - }; - - let Some(transformed_code) = self.transform_for_repl(code) else { - self.evaluate_raw(code); - return; - }; - - let mut exception: JSValue = JSValue::UNDEFINED; - // SAFETY: `global` is a live opaque `JSGlobalObject` handle; slice ptr/len pairs - // are valid for the duration of the call; `exception` is a stack local. - let result = unsafe { - Bun__REPL__evaluate( - global, - transformed_code.as_ptr(), - transformed_code.len(), - b"[repl]".as_ptr(), - b"[repl]".len(), - &raw mut exception, - ) - }; - if !exception.is_undefined() && !exception.is_null() { - self.set_last_error(exception); - self.print_js_error(exception); - return; - } - - let mut resolved_result = result; - if let Some(promise) = result.as_promise() { - // SAFETY: `promise` is a live JSC heap cell; `vm.jsc_vm` is the - // owning JSC VM handle for this thread. - jsc::JSPromise::opaque_mut(promise).set_handled(); - self.enable_signals_during_wait(); - // Note: reshaped for borrowck — disable_signals_during_wait called on each path - vm.as_mut() - .wait_for_promise(jsc::AnyPromise::Normal(promise)); - if vm.jsc_vm().execution_forbidden() { - vm.jsc_vm().set_execution_forbidden(false); - global.clear_termination_exception(); - self.print(format_args!("\n")); - self.disable_signals_during_wait(); - return; - } - let jsc_vm_ref = vm.jsc_vm(); - match jsc::JSPromise::opaque_mut(promise).status() { - PromiseStatus::Fulfilled => { - resolved_result = jsc::JSPromise::opaque_mut(promise).result(jsc_vm_ref) - } - PromiseStatus::Rejected => { - let rejection = jsc::JSPromise::opaque_mut(promise).result(jsc_vm_ref); - self.set_last_error(rejection); - self.print_js_error(rejection); - self.disable_signals_during_wait(); - return; - } - PromiseStatus::Pending => { - self.disable_signals_during_wait(); - return; - } - } - self.disable_signals_during_wait(); - } - - let mut actual_result = resolved_result; - if resolved_result.is_object() { - let maybe_value = - match resolved_result.get_own(global, &bun_core::String::static_("value")) { - Ok(v) => v, - Err(err) => { - let exc = global.take_exception(err); - self.set_last_error(exc); - self.print_js_error(exc); - vm.as_mut().tick(); - return; - } - }; - if let Some(value) = maybe_value { - actual_result = value; + if let Err(err) = self.copy_value_to_clipboard(actual_result) { + if let Some(global) = self.global { + let exc = global.take_exception(err); + self.set_last_error(exc); + self.print_js_error(exc); } } - - self.set_last_result(actual_result); - if !actual_result.is_undefined() { - let global_this = global.to_js_value(); - global_this.put(global, b"_", actual_result); - } - - if let Err(err) = self.copy_value_to_clipboard(actual_result) { - let exc = global.take_exception(err); - self.set_last_error(exc); - self.print_js_error(exc); + if let Some(vm) = self.vm { + vm.as_mut().tick(); } - vm.as_mut().tick(); } /// Format a JS value as a string suitable for clipboard. diff --git a/src/runtime/cli/repl_command.rs b/src/runtime/cli/repl_command.rs index faf25548c8fd..7c392244b2b5 100644 --- a/src/runtime/cli/repl_command.rs +++ b/src/runtime/cli/repl_command.rs @@ -98,29 +98,7 @@ impl ReplCommand { // ReplRunner construction to avoid a move-after-borrow. // Configure bundler options - // `BundleOptions.install` is `Option>` so no - // lifetime-extension cast is needed. - let install_ptr = ctx.install.as_deref().map(core::ptr::NonNull::from); - b.options.install = install_ptr; - b.resolver.opts.install = install_ptr; - b.resolver.opts.global_cache = ctx.debug.global_cache; - b.resolver.opts.prefer_offline_install = ctx - .debug - .offline_mode_setting - .unwrap_or(OfflineMode::Online) - == OfflineMode::Offline; - let prefer_latest = ctx - .debug - .offline_mode_setting - .unwrap_or(OfflineMode::Online) - == OfflineMode::Latest; - // The resolver's `BundleOptions` stub has no `prefer_latest_install` field and the - // resolver never reads it; only the bundler-side mirror carries it (matches - // run_command.rs / production.rs). - b.options.global_cache = b.resolver.opts.global_cache; - b.options.prefer_offline_install = b.resolver.opts.prefer_offline_install; - b.options.prefer_latest_install = prefer_latest; - b.resolver.env_loader = NonNull::new(b.env); + crate::cli::run_command::wire_install_options(b, ctx); b.options.env.behavior = EnvBehavior::LoadAllWithoutInlining; b.options.dead_code_elimination = false; // REPL needs all code @@ -305,4 +283,3 @@ unsafe extern "C" { } use bun_bundler::options::EnvBehavior; -use bun_options_types::offline_mode::OfflineMode; diff --git a/src/runtime/cli/run_command.rs b/src/runtime/cli/run_command.rs index 28ed08bfbaeb..13fdd905b629 100644 --- a/src/runtime/cli/run_command.rs +++ b/src/runtime/cli/run_command.rs @@ -90,6 +90,36 @@ impl Default for ExecCfg { } } +/// Shared ctx→install/global-cache/offline-mode option projection for the +/// transpiler and its resolver, used by the run, repl, and bake-production +/// boot paths. +pub(crate) fn wire_install_options(b: &mut Transpiler<'_>, ctx: &ContextData) { + use bun_options_types::offline_mode::OfflineMode; + + // `BundleOptions::install` is a raw `NonNull` backref into + // the CLI's `Box` (process-lifetime). + // `as_deref` yields `&BunInstall`, which + // `NonNull::from` converts without the lifetime tie. + let install_ptr = ctx.install.as_deref().map(::core::ptr::NonNull::from); + b.options.install = install_ptr; + b.resolver.opts.install = install_ptr; + b.resolver.opts.global_cache = ctx.debug.global_cache; + let offline = ctx + .debug + .offline_mode_setting + .unwrap_or(OfflineMode::Online); + b.resolver.opts.prefer_offline_install = offline == OfflineMode::Offline; + // resolver's forward-decl `BundleOptions` lacks + // `prefer_latest_install`; only the bundler-side mirror carries it. + b.options.global_cache = ctx.debug.global_cache; + b.options.prefer_offline_install = offline == OfflineMode::Offline; + b.options.prefer_latest_install = offline == OfflineMode::Latest; + // Stored as `NonNull` (not `&Loader`): `configure_defines()` later + // reborrows the same allocation as `&mut Loader`, which would alias a + // live `&Loader`. The Loader outlives the resolver. + b.resolver.env_loader = ::core::ptr::NonNull::new(b.env); +} + pub struct RunCommand; impl RunCommand { @@ -782,27 +812,8 @@ Full documentation is available at https://bun.com/docs/cli/run /// [`boot_standalone`]. fn wire_transpiler_from_ctx(b: &mut Transpiler<'_>, ctx: &mut ContextData) { use bun_options_types::context::MacroOptions; - use bun_options_types::offline_mode::OfflineMode; - - // `BundleOptions::install` is a raw `NonNull` backref into - // the CLI's `Box` (process-lifetime). - // `as_deref` yields `&BunInstall`, which - // `NonNull::from` converts without the lifetime tie. - let install_ptr = ctx.install.as_deref().map(::core::ptr::NonNull::from); - b.options.install = install_ptr; - b.resolver.opts.install = install_ptr; - b.resolver.opts.global_cache = ctx.debug.global_cache; - let offline = ctx - .debug - .offline_mode_setting - .unwrap_or(OfflineMode::Online); - b.resolver.opts.prefer_offline_install = offline == OfflineMode::Offline; - // resolver's forward-decl `BundleOptions` lacks - // `prefer_latest_install`; only the bundler-side mirror carries it. - b.options.global_cache = ctx.debug.global_cache; - b.options.prefer_offline_install = offline == OfflineMode::Offline; - b.options.prefer_latest_install = offline == OfflineMode::Latest; - b.resolver.env_loader = ::core::ptr::NonNull::new(b.env); + + wire_install_options(b, ctx); b.options.minify_identifiers = ctx.bundler_options.minify_identifiers; b.options.minify_whitespace = ctx.bundler_options.minify_whitespace; diff --git a/src/runtime/cli/run_processes_shared.rs b/src/runtime/cli/run_processes_shared.rs new file mode 100644 index 000000000000..4bfca5dd252c --- /dev/null +++ b/src/runtime/cli/run_processes_shared.rs @@ -0,0 +1,117 @@ +//! Helpers shared by the multi-process script runners: `filter_run` +//! (`bun run --filter`) and `multi_run` (`bun run --parallel`/`--sequential`). + +use std::sync::atomic::{AtomicBool, Ordering}; + +use crate::api::bun::process::{self as spawn, Process, Rusage, Status}; + +/// Set from a signal handler; polled by the run loops. +pub(crate) static SHOULD_ABORT: AtomicBool = AtomicBool::new(false); + +pub(crate) struct AbortHandler; + +impl AbortHandler { + #[cfg(unix)] + extern "C" fn posix_signal_handler( + _sig: i32, + _info: *const bun_sys::posix::siginfo_t, + _: *const core::ffi::c_void, + ) { + SHOULD_ABORT.store(true, Ordering::SeqCst); + } + + #[cfg(windows)] + extern "system" fn windows_ctrl_handler( + dw_ctrl_type: bun_sys::windows::DWORD, + ) -> bun_sys::windows::BOOL { + if dw_ctrl_type == bun_sys::windows::CTRL_C_EVENT { + SHOULD_ABORT.store(true, Ordering::SeqCst); + return bun_sys::windows::TRUE; + } + bun_sys::windows::FALSE + } + + pub(crate) fn install() { + #[cfg(unix)] + { + // SAFETY: all-zero is a valid `libc::sigaction`; sigemptyset/sigaction are + // FFI calls with no extra preconditions beyond valid pointers. + unsafe { + let mut action: bun_sys::posix::Sigaction = bun_core::ffi::zeroed(); + action.sa_sigaction = Self::posix_signal_handler as *const () as usize; + libc::sigemptyset(&raw mut action.sa_mask); + action.sa_flags = (libc::SA_SIGINFO | libc::SA_RESTART | libc::SA_RESETHAND) as _; + bun_sys::posix::sigaction(libc::SIGINT, &raw const action, core::ptr::null_mut()); + } + } + #[cfg(not(unix))] + { + let res = bun_sys::windows::SetConsoleCtrlHandler( + Some(Self::windows_ctrl_handler), + bun_sys::windows::TRUE, + ); + if res == 0 { + if bun_core::env::IS_DEBUG { + bun_core::warn!("Failed to set abort handler\n"); + } + } + } + } + + pub(crate) fn uninstall() { + // only necessary on Windows, as on posix we pass the SA_RESETHAND flag + #[cfg(windows)] + { + // restores default Ctrl+C behavior + let _ = bun_sys::windows::SetConsoleCtrlHandler(None, bun_sys::windows::FALSE); + } + } +} + +/// `Process::watch_or_reap` with the shared error fallback: if registration +/// fails and the process has not already exited, synthesize an error exit so +/// the run loop still observes a terminal status. +pub(crate) fn watch_or_reap(process: &mut Process) { + if let Err(err) = process.watch_or_reap() { + if !process.has_exited() { + // SAFETY: all-zero is a valid Rusage (POD C struct) + let rusage = bun_core::ffi::zeroed::(); + process.on_exit(Status::Err(err), &rusage); + } + } +} + +/// First non-zero exit code across all spawned handles; signaled/errored +/// processes map to their signal exit code (or 1). 0 when every spawned +/// process exited cleanly. +pub(crate) fn aggregate_exit_code<'h>(statuses: impl Iterator>) -> u8 { + for status in statuses.flatten() { + match status { + Status::Exited(exited) => { + if exited.code != 0 { + return exited.code; + } + } + Status::Signaled(signal) => { + return bun_sys::SignalCode(*signal).to_exit_code().unwrap_or(1); + } + _ => return 1, + } + } + 0 +} + +/// A `Stdio::Buffer` slot for `SpawnOptions`; on Windows this carries a freshly +/// allocated libuv pipe whose ownership moves into the spawn result. +pub(crate) fn buffered_stdio() -> spawn::Stdio { + #[cfg(unix)] + { + spawn::Stdio::Buffer + } + #[cfg(not(unix))] + { + spawn::Stdio::Buffer(bun_core::heap::into_raw(Box::new(bun_core::ffi::zeroed::< + bun_sys::windows::libuv::Pipe, + >()))) + } +} diff --git a/src/runtime/cli/unlink_command.rs b/src/runtime/cli/unlink_command.rs index cfababcd13c5..55001cc46995 100644 --- a/src/runtime/cli/unlink_command.rs +++ b/src/runtime/cli/unlink_command.rs @@ -5,13 +5,11 @@ use bun_core::{Global, Output}; use bun_paths::{AbsPath, PathBuffer, platform, resolve_path}; use bun_sys::{self as sys, Dir, Fd, FdDirExt}; -use bun_install::Features; use bun_install::bin as stub_bin; use bun_install::bin_real as bin; -use bun_install::lockfile_real::{Lockfile, package::Package}; use bun_install::package_manager_real::{ self as pm, CommandLineArguments, Subcommand, attempt_to_create_package_json, - global_link_dir_path, options::LogLevel, package_manager_options, setup_global_dir, + global_link_dir_path, options::LogLevel, }; use crate::command::ContextData; @@ -50,66 +48,11 @@ fn unlink(ctx: &mut ContextData) -> crate::Result<()> { if manager.options.positionals.len() == 1 { // bun unlink - let mut lockfile = Lockfile::default(); - let mut package = Package::default(); - // Step 1. parse the nearest package.json file - { - let package_json_source = match bun_ast::to_source( - manager.original_package_json_path.as_zstr(), - Default::default(), - ) { - Ok(s) => s, - Err(e) => { - Output::err_generic( - "failed to read \"{}\" for unlinking: {}", - ( - BStr::new(manager.original_package_json_path.as_bytes()), - BStr::new(e.name()), - ), - ); - Global::crash(); - } - }; - lockfile.init_empty(); - - let mut resolver: () = (); - // `log_mut()` returns a borrow decoupled from `&self`; disjoint - // storage from `&mut PackageManager` (owned by the CLI `Context`). - let log = manager.log_mut(); - package.parse::<()>( - &mut lockfile, - manager, - log, - &package_json_source, - &mut resolver, - Features::FOLDER, - )?; - let name = lockfile.str(&package.name); - if name.is_empty() { - if manager.options.log_level != LogLevel::Silent { - bun_core::pretty_errorln!( - "error: package.json missing \"name\" in \"{}\"", - BStr::new(package_json_source.path.text), - ); - } - Global::crash(); - } else if !strings::is_npm_package_name(name) { - if manager.options.log_level != LogLevel::Silent { - bun_core::pretty_errorln!( - "error: invalid package.json name \"{}\" in \"{}\"", - BStr::new(name), - BStr::new(package_json_source.path.text), - ); - } - Global::crash(); - } - } + let (lockfile, package) = super::link_command::load_package_for_link(manager, "unlinking")?; - // Reshaped for borrowck — `name` borrows `lockfile`; re-derive - // it after the parse block so its lifetime is decoupled from - // `package_json_source` (dropped above) while remaining a slice into - // `lockfile.buffers.string_bytes`. + // `name` is a slice into `lockfile.buffers.string_bytes`, decoupled + // from the helper-local package.json source. let name = lockfile.str(&package.name); match sys::lstat(resolve_path::join_abs_string_z::( @@ -135,36 +78,7 @@ fn unlink(ctx: &mut ContextData) -> crate::Result<()> { } // Step 2. Setup the global directory - let node_modules: Dir = 'brk: { - bin::Linker::ensure_umask(); - let explicit_global_dir: &[u8] = match &ctx.install { - Some(install_) => install_.global_dir.as_deref().unwrap_or(b""), - None => b"", - }; - manager.global_dir = Some(Dir::from_fd(package_manager_options::open_global_dir( - explicit_global_dir, - )?)); - - setup_global_dir(manager, &&mut *ctx)?; - - match manager - .global_dir - .as_ref() - .unwrap() - .make_open_path(b"node_modules", Default::default()) - { - Ok(d) => break 'brk d, - Err(e) => { - if manager.options.log_level != LogLevel::Silent { - bun_core::pretty_errorln!( - "error: failed to create node_modules in global dir due to error {}", - BStr::new(e.name()), - ); - } - Global::crash(); - } - } - }; + let node_modules: Dir = super::link_command::open_global_node_modules(manager, &mut *ctx)?; // Step 3b. Link any global bins if package.bin.tag != stub_bin::Tag::None { diff --git a/src/runtime/cli/update_interactive_command.rs b/src/runtime/cli/update_interactive_command.rs index 0b39657b04d9..e898f85f1209 100644 --- a/src/runtime/cli/update_interactive_command.rs +++ b/src/runtime/cli/update_interactive_command.rs @@ -8,13 +8,10 @@ use bstr::BStr; use bun_alloc::Arena as Bump; use bun_collections::StringHashMap; use bun_core::{Global, Output}; -use bun_glob as glob; use bun_install::dependency::{self, Behavior}; use bun_install::lockfile::package::PackageColumns as _; -use bun_install::lockfile::{LoadResult, LoadStep}; use bun_install::package_manager::{ - LogLevel, ManifestLoad, ROOT_PACKAGE_JSON_PATH, Subcommand, WorkspaceFilter, - install_with_manager, populate_manifest_cache, + ManifestLoad, ROOT_PACKAGE_JSON_PATH, Subcommand, install_with_manager, populate_manifest_cache, }; use bun_install::{ CommandLineArguments, GetJsonOptions, GetJsonResult, INVALID_PACKAGE_ID, PackageID, @@ -36,6 +33,7 @@ use bun_paths::{self as path, PathBuffer}; use bun_semver::{self as semver, SlicedString}; use crate::Command; +use crate::cli::workspace_helpers; pub(crate) struct TerminalHyperlink<'a> { link: &'a [u8], @@ -474,60 +472,13 @@ impl UpdateInteractiveCommand { original_cwd: &[u8], manager: &mut PackageManager, ) -> crate::Result<()> { - // Reshaped for borrowck — capture `log_level` / `ctx.log` - // before borrowing `&mut manager.lockfile`. - let not_silent = manager.options.log_level != LogLevel::Silent; - let ctx_log_ptr: *mut bun_ast::Log = ctx.log; - - match manager.load_lockfile_from_cwd::() { - LoadResult::NotFound => { - if not_silent { - Output::err_generic("missing lockfile, nothing outdated", ()); - } - Global::crash(); - } - LoadResult::Err(cause) => { - if not_silent { - match cause.step { - LoadStep::OpenFile => Output::err_generic( - "failed to open lockfile: {s}", - (cause.value.name(),), - ), - LoadStep::ParseFile => Output::err_generic( - "failed to parse lockfile: {s}", - (cause.value.name(),), - ), - LoadStep::ReadFile => Output::err_generic( - "failed to read lockfile: {s}", - (cause.value.name(),), - ), - LoadStep::Migrating => Output::err_generic( - "failed to migrate lockfile: {s}", - (cause.value.name(),), - ), - } - // SAFETY: `ctx.log` is set by `Command::create_context_data` - // for every subcommand and is non-null for the command's - // lifetime. - if unsafe { (*ctx_log_ptr).has_errors() } { - manager - .log_mut() - .print(std::ptr::from_mut(Output::error_writer()))?; - } - } - Global::crash(); - } - LoadResult::Ok(_) => { - // `load_lockfile_from_cwd` populates `manager.lockfile` (Box) - // in place, so no reassignment is needed. - } - } + workspace_helpers::load_lockfile_or_crash(ctx, manager); let workspace_pkg_ids: Vec = if !manager.options.filter_patterns.is_empty() { let filters = manager.options.filter_patterns; - Self::find_matching_workspaces(original_cwd, manager, filters) + workspace_helpers::find_matching_workspaces(original_cwd, manager, filters) } else if manager.options.do_.recursive() { - Self::get_all_workspaces(manager) + workspace_helpers::get_all_workspaces(manager) } else { let root_pkg_id = manager .root_package_id @@ -710,113 +661,6 @@ impl UpdateInteractiveCommand { Ok(()) } - fn get_all_workspaces(manager: &PackageManager) -> Vec { - let lockfile = &manager.lockfile; - let packages = lockfile.packages.slice(); - let pkg_resolutions = packages.items_resolution(); - - let mut workspace_pkg_ids: Vec = Vec::new(); - for (pkg_id, resolution) in pkg_resolutions.iter().enumerate() { - if resolution.tag != resolution::Tag::Workspace - && resolution.tag != resolution::Tag::Root - { - continue; - } - workspace_pkg_ids.push(pkg_id as PackageID); - } - workspace_pkg_ids - } - - fn find_matching_workspaces( - original_cwd: &[u8], - manager: &PackageManager, - filters: &[&[u8]], - ) -> Vec { - let lockfile = &manager.lockfile; - let packages = lockfile.packages.slice(); - let pkg_names = packages.items_name(); - let pkg_resolutions = packages.items_resolution(); - let string_buf = lockfile.buffers.string_bytes.as_slice(); - - let mut workspace_pkg_ids: Vec = Vec::new(); - for (pkg_id, resolution) in pkg_resolutions.iter().enumerate() { - if resolution.tag != resolution::Tag::Workspace - && resolution.tag != resolution::Tag::Root - { - continue; - } - workspace_pkg_ids.push(pkg_id as PackageID); - } - - let mut path_buf = PathBuffer::uninit(); - - let converted_filters: Vec = filters - .iter() - .map(|filter| { - WorkspaceFilter::init(filter, original_cwd, &mut path_buf.0).expect("OOM") - }) - .collect(); - // `defer { filter.deinit(allocator); allocator.free(...) }` — implicit via Drop. - - // SAFETY: `FileSystem::init` ran during `PackageManager::init`. - let top_level_dir = FileSystem::get().top_level_dir; - - // move all matched workspaces to front of array - let mut i: usize = 0; - while i < workspace_pkg_ids.len() { - let workspace_pkg_id = workspace_pkg_ids[i]; - - let matched = 'matched: { - for filter in &converted_filters { - match filter { - WorkspaceFilter::Path(pattern) => { - if pattern.is_empty() { - continue; - } - let res = &pkg_resolutions[workspace_pkg_id as usize]; - let res_path: &[u8] = match res.tag { - resolution::Tag::Workspace => res.workspace().slice(string_buf), - resolution::Tag::Root => top_level_dir, - _ => unreachable!(), - }; - - let abs_res_path = path::resolve_path::join_abs_string_buf::< - path::platform::Posix, - >( - top_level_dir, &mut path_buf.0, &[res_path] - ); - - if !glob::r#match( - pattern, - strings::without_trailing_slash(abs_res_path), - ) - .matches() - { - break 'matched false; - } - } - WorkspaceFilter::Name(pattern) => { - let name = pkg_names[workspace_pkg_id as usize].slice(string_buf); - if !glob::r#match(pattern, name).matches() { - break 'matched false; - } - } - WorkspaceFilter::All => {} - } - } - true - }; - - if matched { - i += 1; - } else { - workspace_pkg_ids.swap_remove(i); - } - } - - workspace_pkg_ids - } - fn group_catalog_dependencies( packages: Vec, ) -> crate::Result> { diff --git a/src/runtime/cli/workspace_helpers.rs b/src/runtime/cli/workspace_helpers.rs new file mode 100644 index 000000000000..f91ef5041606 --- /dev/null +++ b/src/runtime/cli/workspace_helpers.rs @@ -0,0 +1,160 @@ +//! Workspace/lockfile helpers shared by `bun outdated` and +//! `bun update --interactive`. + +use bun_core::strings; +use bun_core::{Global, Output}; +use bun_glob as glob; +use bun_install::lockfile::package::PackageColumns as _; +use bun_install::lockfile::{LoadResult, LoadStep}; +use bun_install::package_manager::{LogLevel, WorkspaceFilter}; +use bun_install::{PackageID, PackageManager, resolution}; +use bun_paths::{self as path, PathBuffer}; +use bun_resolver::fs::FileSystem; + +use crate::Command; + +/// Load the lockfile from the current directory, reporting errors and exiting +/// the process on failure. +pub(crate) fn load_lockfile_or_crash(ctx: &Command::ContextData, manager: &mut PackageManager) { + let not_silent = manager.options.log_level != LogLevel::Silent; + match manager.load_lockfile_from_cwd::() { + LoadResult::NotFound => { + if not_silent { + Output::err_generic("missing lockfile, nothing outdated", ()); + } + Global::crash(); + } + LoadResult::Err(cause) => { + if not_silent { + match cause.step { + LoadStep::OpenFile => { + Output::err_generic("failed to open lockfile: {s}", (cause.value.name(),)); + } + LoadStep::ParseFile => { + Output::err_generic("failed to parse lockfile: {s}", (cause.value.name(),)); + } + LoadStep::ReadFile => { + Output::err_generic("failed to read lockfile: {s}", (cause.value.name(),)); + } + LoadStep::Migrating => { + Output::err_generic( + "failed to migrate lockfile: {s}", + (cause.value.name(),), + ); + } + } + if ctx.log_ref().has_errors() { + let _ = manager + .log_mut() + .print(std::ptr::from_mut(Output::error_writer())); + } + } + Global::crash(); + } + LoadResult::Ok(_) => { + // `load_from_cwd(&mut self, ..)` populates the lockfile in place, + // so no reassignment is needed. + } + } +} + +/// Collect the package IDs of the root package and every workspace package. +pub(crate) fn get_all_workspaces(manager: &PackageManager) -> Vec { + let lockfile = &manager.lockfile; + let packages = lockfile.packages.slice(); + let pkg_resolutions = packages.items_resolution(); + + let mut workspace_pkg_ids: Vec = Vec::new(); + for (pkg_id, resolution) in pkg_resolutions.iter().enumerate() { + if resolution.tag != resolution::Tag::Workspace && resolution.tag != resolution::Tag::Root { + continue; + } + workspace_pkg_ids.push(pkg_id as PackageID); + } + workspace_pkg_ids +} + +/// Collect the workspace package IDs matching the `--filter` patterns. +pub(crate) fn find_matching_workspaces( + original_cwd: &[u8], + manager: &PackageManager, + filters: &[&[u8]], +) -> Vec { + let lockfile = &manager.lockfile; + let packages = lockfile.packages.slice(); + let pkg_names = packages.items_name(); + let pkg_resolutions = packages.items_resolution(); + let string_buf = lockfile.buffers.string_bytes.as_slice(); + + let mut workspace_pkg_ids = get_all_workspaces(manager); + + let mut path_buf = PathBuffer::uninit(); + + let converted_filters: Vec = filters + .iter() + .map(|filter| { + bun_core::handle_oom(WorkspaceFilter::init(filter, original_cwd, &mut path_buf.0)) + }) + .collect(); + // `defer { filter.deinit(allocator); allocator.free(...) }` — implicit via Drop. + + // SAFETY: `FileSystem::init` runs during `PackageManager::init` so the + // process-singleton is populated. + let top_level_dir = FileSystem::get().top_level_dir; + + // move all matched workspaces to front of array + let mut i: usize = 0; + while i < workspace_pkg_ids.len() { + let workspace_pkg_id = workspace_pkg_ids[i]; + + let matched = 'matched: { + for filter in &converted_filters { + match filter { + WorkspaceFilter::Path(pattern) => { + if pattern.is_empty() { + continue; + } + let res = &pkg_resolutions[workspace_pkg_id as usize]; + let res_path: &[u8] = match res.tag { + resolution::Tag::Workspace => { + // Borrow the field in-place so the returned slice (which may + // point into the inline small-string storage) stays valid. + res.workspace().slice(string_buf) + } + resolution::Tag::Root => top_level_dir, + _ => unreachable!(), + }; + + let abs_res_path = path::resolve_path::join_abs_string_buf::< + path::platform::Posix, + >( + top_level_dir, &mut path_buf.0, &[res_path] + ); + + if !glob::r#match(pattern, strings::without_trailing_slash(abs_res_path)) + .matches() + { + break 'matched false; + } + } + WorkspaceFilter::Name(pattern) => { + let name = pkg_names[workspace_pkg_id as usize].slice(string_buf); + if !glob::r#match(pattern, name).matches() { + break 'matched false; + } + } + WorkspaceFilter::All => {} + } + } + true + }; + + if matched { + i += 1; + } else { + workspace_pkg_ids.swap_remove(i); + } + } + + workspace_pkg_ids +} diff --git a/src/runtime/test_runner/pretty_format.rs b/src/runtime/test_runner/pretty_format.rs index deee658fe244..4b45284546ab 100644 --- a/src/runtime/test_runner/pretty_format.rs +++ b/src/runtime/test_runner/pretty_format.rs @@ -1,4 +1,4 @@ -use core::cell::{Cell, RefCell}; +use core::cell::RefCell; use crate::test_runner::expect::JSValueTestExt; use core::ffi::c_void; @@ -124,25 +124,6 @@ pub enum MessageLevel { Info = 4, } -#[repr(u32)] -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum MessageType { - Log = 0, - Dir = 1, - DirXML = 2, - Table = 3, - Trace = 4, - StartGroup = 5, - StartGroupCollapsed = 6, - EndGroup = 7, - Clear = 8, - Assert = 9, - Timing = 10, - Profile = 11, - ProfileEnd = 12, - Image = 13, -} - #[derive(Copy, Clone, Default)] pub struct FormatOptions { pub enable_colors: bool, @@ -398,61 +379,6 @@ impl Drop for Formatter<'_> { } } -/// `Display` adapter for formatting a single [`JSValue`]. -/// -/// `Display::fmt` only gives us `&self`, so the -/// mutable handle is parked behind a `Cell` and moved out for the duration of -/// the call — this preserves unique-borrow provenance without the -/// `&shared → *const → *mut` cast that would be UB under Stacked Borrows. -pub struct ZigFormatter<'a, 'b> { - pub formatter: Cell>>, - pub global: &'b JSGlobalObject, - pub value: JSValue, -} - -impl<'a, 'b> ZigFormatter<'a, 'b> { - pub fn new(formatter: &'a mut Formatter<'b>, global: &'b JSGlobalObject, value: JSValue) -> Self { - Self { formatter: Cell::new(Some(formatter)), global, value } - } -} - -#[derive(thiserror::Error, Debug, strum::IntoStaticStr)] -pub enum WriteError { - #[error("UhOh")] - UhOh, -} - -impl core::fmt::Display for ZigFormatter<'_, '_> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - // Move the unique `&mut Formatter` out of the cell for the body; - // re-seat it (and clear `remaining_values`) on the way out so the - // adapter stays reusable. - let formatter: &mut Formatter<'_> = self - .formatter - .take() - .expect("ZigFormatter::fmt re-entered or used after consumption"); - - // Assigning a stack-local slice into `Formatter<'b>` would require `'b: 'local`, - // which borrowck rejects. The single-value path never reads `remaining_values` - // (only `StringPossiblyFormatted` consumes it, and `ZigFormatter` always emits a - // single tag), so leaving it `&[]` is observationally equivalent. - formatter.remaining_values = &[]; - formatter.global_this = self.global; - - let result = (|| { - let tag = Tag::get(self.value, self.global).map_err(|_| core::fmt::Error)?; - let mut adapter = bun_io::FmtAdapter::new(f); - formatter - .format::<_, false>(tag, &mut adapter, self.value, self.global) - .map_err(|_| core::fmt::Error) - })(); - - formatter.remaining_values = &[]; - self.formatter.set(Some(formatter)); - result - } -} - #[repr(u8)] #[derive(Copy, Clone, PartialEq, Eq, core::marker::ConstParamTy)] pub enum Tag { @@ -613,20 +539,7 @@ impl Tag { | JSType::ModuleNamespaceObject | JSType::GlobalObject => Tag::Object, - JSType::ArrayBuffer - | JSType::Int8Array - | JSType::Uint8Array - | JSType::Uint8ClampedArray - | JSType::Int16Array - | JSType::Uint16Array - | JSType::Int32Array - | JSType::Uint32Array - | JSType::Float16Array - | JSType::Float32Array - | JSType::Float64Array - | JSType::BigInt64Array - | JSType::BigUint64Array - | JSType::DataView => Tag::TypedArray, + t if t.is_array_buffer_like() => Tag::TypedArray, JSType::HeapBigInt => Tag::BigInt, @@ -2838,7 +2751,7 @@ impl AsymmetricMatcherFormatter for bun_jsc::console_object::Formatter<'_> { ) -> JsResult<()> { let global = self.global_this; self.format::( - bun_jsc::console_object::formatter::TagResult { tag: tag.into(), cell }, + bun_jsc::console_object::formatter::TagResult { tag, cell, custom: None }, w, v, global, diff --git a/src/semver/Version.rs b/src/semver/Version.rs index 286705e86f84..7e8e2e99615d 100644 --- a/src/semver/Version.rs +++ b/src/semver/Version.rs @@ -103,10 +103,6 @@ impl VersionType { impl VersionType { /// Assumes that there is only one buffer for all the strings - pub fn sort_gt(ctx: &[u8], lhs: Self, rhs: Self) -> bool { - Self::order_fn(ctx, lhs, rhs) == Ordering::Greater - } - pub fn order_fn(ctx: &[u8], lhs: Self, rhs: Self) -> Ordering { lhs.order(rhs, ctx, ctx) } diff --git a/src/semver/lib.rs b/src/semver/lib.rs index 3e0fd6290102..a2701ae3c63f 100644 --- a/src/semver/lib.rs +++ b/src/semver/lib.rs @@ -321,11 +321,35 @@ pub mod semver_string { const MAX_INLINE_LEN_M1: usize = String::MAX_INLINE_LEN - 1; match buf.len() { 0..=MAX_INLINE_LEN_M1 => true, + // A string of exactly MAX_INLINE_LEN bytes is only inlinable when its + // final byte's top bit is clear — that bit distinguishes inline data + // from a packed pointer. Non-ASCII strings of exactly 8 bytes hit this. Self::MAX_INLINE_LEN => buf[Self::MAX_INLINE_LEN - 1] & 0x80 == 0, _ => false, } } + /// Caller must ensure `in_.len() <= MAX_INLINE_LEN`. + /// Unrolled per length so this compiles to direct loads instead of a + /// memcpy libcall — short strings dominate lockfile/install parsing. + #[inline] + fn inline_bytes(in_: &[u8]) -> [u8; Self::MAX_INLINE_LEN] { + match in_.len() { + 0 => [0, 0, 0, 0, 0, 0, 0, 0], + 1 => [in_[0], 0, 0, 0, 0, 0, 0, 0], + 2 => [in_[0], in_[1], 0, 0, 0, 0, 0, 0], + 3 => [in_[0], in_[1], in_[2], 0, 0, 0, 0, 0], + 4 => [in_[0], in_[1], in_[2], in_[3], 0, 0, 0, 0], + 5 => [in_[0], in_[1], in_[2], in_[3], in_[4], 0, 0, 0], + 6 => [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], 0, 0], + 7 => [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], 0], + Self::MAX_INLINE_LEN => [ + in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], in_[7], + ], + _ => unreachable!(), + } + } + #[inline] pub fn is_inline(self) -> bool { self.bytes[Self::MAX_INLINE_LEN - 1] & 0x80 == 0 @@ -347,133 +371,34 @@ pub mod semver_string { // bun_install (or bun_install_types) as inherent helpers there. pub fn init(buf: &[u8], in_: &[u8]) -> String { - match in_.len() { - 0 => String::default(), - 1 => String { - bytes: [in_[0], 0, 0, 0, 0, 0, 0, 0], - }, - 2 => String { - bytes: [in_[0], in_[1], 0, 0, 0, 0, 0, 0], - }, - 3 => String { - bytes: [in_[0], in_[1], in_[2], 0, 0, 0, 0, 0], - }, - 4 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], 0, 0, 0, 0], - }, - 5 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], 0, 0, 0], - }, - 6 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], 0, 0], - }, - 7 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], 0], - }, - Self::MAX_INLINE_LEN => { - // If they use the final bit, then it's a big string. - // This should only happen for non-ascii strings that are exactly 8 bytes. - // so that's an edge-case - if in_[Self::MAX_INLINE_LEN - 1] >= 128 { - let ptr_bits: u64 = Pointer::init(buf, in_).to_bits(); - let packed: u64 = (ptr_bits & MAX_ADDRESSABLE_SPACE_MASK) | (1u64 << 63); - String { - bytes: packed.to_ne_bytes(), - } - } else { - String { - bytes: [ - in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], in_[7], - ], - } - } + if Self::can_inline(in_) { + String { + bytes: Self::inline_bytes(in_), } - _ => { - let ptr_bits: u64 = Pointer::init(buf, in_).to_bits(); - let packed: u64 = (ptr_bits & MAX_ADDRESSABLE_SPACE_MASK) | (1u64 << 63); - String { - bytes: packed.to_ne_bytes(), - } + } else { + let ptr_bits: u64 = Pointer::init(buf, in_).to_bits(); + let packed: u64 = (ptr_bits & MAX_ADDRESSABLE_SPACE_MASK) | (1u64 << 63); + String { + bytes: packed.to_ne_bytes(), } } } pub fn init_inline(in_: &[u8]) -> String { debug_assert!(Self::can_inline(in_)); - match in_.len() { - 0 => String::default(), - 1 => String { - bytes: [in_[0], 0, 0, 0, 0, 0, 0, 0], - }, - 2 => String { - bytes: [in_[0], in_[1], 0, 0, 0, 0, 0, 0], - }, - 3 => String { - bytes: [in_[0], in_[1], in_[2], 0, 0, 0, 0, 0], - }, - 4 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], 0, 0, 0, 0], - }, - 5 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], 0, 0, 0], - }, - 6 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], 0, 0], - }, - 7 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], 0], - }, - 8 => String { - bytes: [ - in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], in_[7], - ], - }, - _ => unreachable!(), + String { + bytes: Self::inline_bytes(in_), } } pub fn init_append_if_needed(buf: &mut Vec, in_: &[u8]) -> Result { - Ok(match in_.len() { - 0 => String::default(), - 1 => String { - bytes: [in_[0], 0, 0, 0, 0, 0, 0, 0], - }, - 2 => String { - bytes: [in_[0], in_[1], 0, 0, 0, 0, 0, 0], - }, - 3 => String { - bytes: [in_[0], in_[1], in_[2], 0, 0, 0, 0, 0], - }, - 4 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], 0, 0, 0, 0], - }, - 5 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], 0, 0, 0], - }, - 6 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], 0, 0], - }, - 7 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], 0], - }, - - Self::MAX_INLINE_LEN => { - // If they use the final bit, then it's a big string. - // This should only happen for non-ascii strings that are exactly 8 bytes. - // so that's an edge-case - if in_[Self::MAX_INLINE_LEN - 1] >= 128 { - Self::init_append(buf, in_)? - } else { - String { - bytes: [ - in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], in_[7], - ], - } - } - } - - _ => Self::init_append(buf, in_)?, - }) + if Self::can_inline(in_) { + Ok(String { + bytes: Self::inline_bytes(in_), + }) + } else { + Self::init_append(buf, in_) + } } pub fn init_append(buf: &mut Vec, in_: &[u8]) -> Result { diff --git a/src/sourcemap/Chunk.rs b/src/sourcemap/Chunk.rs index 7af834e8fa3c..e04e51f815f3 100644 --- a/src/sourcemap/Chunk.rs +++ b/src/sourcemap/Chunk.rs @@ -55,20 +55,6 @@ impl Chunk { unsafe { core::ptr::read(self) } } - pub fn print_source_map_contents( - &self, - source: &Source, - mutable: &mut MutableString, - include_sources_contents: bool, - ) -> Result<(), crate::Error> { - print_source_map_contents_json::( - source, - mutable, - include_sources_contents, - self.buffer.list.as_slice(), - ) - } - /// `chunk.buffer` holds an InternalSourceMap blob (the runtime path). Re-encode /// to a standard VLQ "mappings" string before emitting JSON. pub fn print_source_map_contents_from_internal( diff --git a/src/sourcemap/lib.rs b/src/sourcemap/lib.rs index bd4f20bfd85f..827e4106dfab 100644 --- a/src/sourcemap/lib.rs +++ b/src/sourcemap/lib.rs @@ -1215,22 +1215,3 @@ fn find_source_mapping_url_u16(source: &[u16]) -> Option( - origin: &bun_url::URL<'_>, - source: &bun_ast::Source, - asset_prefix_path: &[u8], - writer: &mut W, -) -> bun_io::Result<()> { - writer.write_all(b"\n//# sourceMappingURL=")?; - writer.write_all(bun_core::strings::without_trailing_slash(origin.href))?; - if !asset_prefix_path.is_empty() { - writer.write_all(asset_prefix_path)?; - } - if !source.path.pretty.is_empty() && source.path.pretty[0] != b'/' { - writer.write_all(b"/")?; - } - writer.write_all(source.path.pretty)?; - writer.write_all(b".map")?; - Ok(()) -} diff --git a/src/threading/WaitGroup.rs b/src/threading/WaitGroup.rs index 584fea64aaa7..256107994e49 100644 --- a/src/threading/WaitGroup.rs +++ b/src/threading/WaitGroup.rs @@ -31,10 +31,6 @@ impl WaitGroup { } } - pub fn add_unsynchronized(&mut self, n: usize) { - *self.raw_count.get_mut() += n; - } - pub fn add(&self, n: usize) { // Not Acquire because we don't need to synchronize with other tasks (each runs independently). // Not Release because there are no side effects that other threads depend on when they see diff --git a/src/threading/channel.rs b/src/threading/channel.rs index 1c6738a8772b..f268378274a8 100644 --- a/src/threading/channel.rs +++ b/src/threading/channel.rs @@ -58,14 +58,6 @@ impl<'a, T: Copy> Channel> { } } -impl Channel> { - #[inline] - pub fn init_dynamic() -> Self { - // No allocator param; this non-AST crate uses the global mimalloc. - Self::with_buffer(LinearFifo::>::init()) - } -} - // `T: Copy` because `LinearFifo::write`/`read` are slice-copy based. All // in-tree channel payloads are POD; revisit if a non-`Copy` T appears. impl> Channel { @@ -89,11 +81,6 @@ impl> Channel { self.getters.broadcast(); } - pub fn try_write_item(&self, item: T) -> Result { - let wrote = self.write(core::slice::from_ref(&item))?; - Ok(wrote == 1) - } - pub fn write_item(&self, item: T) -> Result<(), ChannelError> { self.write_all(core::slice::from_ref(&item)) } diff --git a/src/threading/guarded.rs b/src/threading/guarded.rs index 180b4936ada1..62173867a3bf 100644 --- a/src/threading/guarded.rs +++ b/src/threading/guarded.rs @@ -78,15 +78,6 @@ impl GuardedBy { None } } - - /// Borrow the underlying raw [`Mutex`]. Needed by callers that split - /// `lock()`/`unlock()` across function boundaries (e.g. `Progress.rs` - /// porting `lock_api::RawMutex`) or pair this `Guarded` with a bare - /// [`Condition::wait`](crate::Condition::wait). - #[inline] - pub fn raw_mutex(&self) -> &Mutex { - &self.mutex - } } impl GuardedBy { diff --git a/src/threading/work_pool.rs b/src/threading/work_pool.rs index 8980a8154971..60f4a0c98fe6 100644 --- a/src/threading/work_pool.rs +++ b/src/threading/work_pool.rs @@ -146,10 +146,6 @@ impl WorkPool { POOL.get_or_init(create) } - pub fn schedule_batch(batch: Batch) { - Self::get().schedule(batch); - } - pub fn schedule(task: *mut Task) { Self::get().schedule(Batch::from(task)); } diff --git a/src/url/Cargo.toml b/src/url/Cargo.toml index 3ecf09213d22..5b912414efaf 100644 --- a/src/url/Cargo.toml +++ b/src/url/Cargo.toml @@ -21,6 +21,7 @@ libc.workspace = true bitflags.workspace = true bun_alloc.workspace = true bun_core.workspace = true +bun_opaque.workspace = true bun_collections.workspace = true # TODO(b1): bun_io gated — crate does not compile yet; local Write stub in lib.rs # bun_io.workspace = true diff --git a/src/url/lib.rs b/src/url/lib.rs index 2e588eb83834..fc6feb5ed2f9 100644 --- a/src/url/lib.rs +++ b/src/url/lib.rs @@ -45,11 +45,12 @@ pub mod whatwg { use super::BunString as String; use super::strings; - /// Opaque handle to a heap-allocated WTF::URL (C++). Always behind `*mut URL`. - /// Construct via `from_string`/`from_utf8`; free via `deinit`. - #[repr(C)] - pub struct URL { - _opaque: [u8; 0], + bun_opaque::opaque_ffi! { + /// Opaque handle to a heap-allocated WTF::URL (C++). Always behind `*mut URL`. + /// Construct via `from_string`/`from_utf8`; free via `deinit`. + /// `!Send`/`!Sync` per the macro: WTF::URL holds non-atomically-refcounted + /// WTF::Strings, so the handle must stay on the thread that created it. + pub struct URL; } // Getters take `*const URL` — the C++ side (BunString.cpp) never mutates the @@ -125,12 +126,22 @@ pub mod whatwg { } impl URL { - pub fn from_string(str: &String) -> Option> { - let mut input = *str; + // `from_string`/`from_utf8` return an owned C++ heap pointer that the + // caller must free exactly once via `deinit`/`destroy`. + pub fn from_string(str: String) -> Option> { + let mut input = str; URL__fromString(&mut input) } pub fn from_utf8(input: &[u8]) -> Option> { - Self::from_string(&String::borrow_utf8(input)) + Self::from_string(String::borrow_utf8(input)) + } + /// By-value form of the free [`file_url_from_string`] helper. + pub fn file_url_from_string(str: String) -> String { + file_url_from_string(&str) + } + /// By-value form of the free [`path_from_file_url`] helper. + pub fn path_from_file_url(str: String) -> String { + path_from_file_url(&str) } /// Includes the leading '#'. pub fn hash(&self) -> String { @@ -188,6 +199,15 @@ pub mod whatwg { pub fn deinit(&mut self) { URL__deinit(self) } + /// Raw-pointer form of [`URL::deinit`]. + /// + /// # Safety + /// `this` must be a live heap pointer from `from_string`/`from_utf8` + /// (or the C++ side), freed exactly once. + pub unsafe fn destroy(this: *mut Self) { + // SAFETY: caller guarantees `this` is valid and uniquely owned. + unsafe { URL__deinit(&mut *this) } + } } } // Re-export the free helpers at crate root so lower-tier callers can write diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index 5bf0cca96d9e..917da88a3a9b 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -8257,6 +8257,21 @@ describe("outdated", () => { expect(rest).toMatchSnapshot(); }); } + test("errors without a lockfile", async () => { + await write(packageJson, JSON.stringify({ name: "no-lockfile", version: "1.0.0" })); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "outdated"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const [err, _out, exitCode] = await Promise.all([stderr.text(), stdout.text(), exited]); + expect(err).toContain("error: missing lockfile, nothing outdated"); + expect(exitCode).toBe(1); + }); test("in workspace", async () => { await Promise.all([ write( diff --git a/test/cli/install/bun-link.test.ts b/test/cli/install/bun-link.test.ts index 76438f8dadf1..209aa1b86563 100644 --- a/test/cli/install/bun-link.test.ts +++ b/test/cli/install/bun-link.test.ts @@ -472,3 +472,37 @@ it("should link dependency without crashing", async () => { // This should fail with a non-zero exit code. expect(await exited4).toBe(1); }); + +for (const command of ["link", "unlink"]) { + it(`should error when ${command}ing a package without a name`, async () => { + await writeFile(join(link_dir, "package.json"), JSON.stringify({ version: "0.0.1" })); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), command], + cwd: link_dir, + stdout: "pipe", + stdin: "ignore", + stderr: "pipe", + env, + }); + const [err, _out, exitCode] = await Promise.all([stderr.text(), stdout.text(), exited]); + expect(err).toContain('error: package.json missing "name"'); + expect(exitCode).toBe(1); + }); +} + +it("should error when linking a package with an invalid name", async () => { + await writeFile(join(link_dir, "package.json"), JSON.stringify({ name: "NOT a valid name!", version: "0.0.1" })); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "link"], + cwd: link_dir, + stdout: "pipe", + stdin: "ignore", + stderr: "pipe", + env, + }); + const [err, _out, exitCode] = await Promise.all([stderr.text(), stdout.text(), exited]); + expect(err).toContain('error: invalid package.json name "NOT a valid name!"'); + expect(exitCode).toBe(1); +}); diff --git a/test/cli/install/bun-pack.test.ts b/test/cli/install/bun-pack.test.ts index 370d9c0e1b23..09192597ccd7 100644 --- a/test/cli/install/bun-pack.test.ts +++ b/test/cli/install/bun-pack.test.ts @@ -51,6 +51,13 @@ test("basic", async () => { expect(tarball.entries).toMatchObject([{ "pathname": "package/package.json" }, { "pathname": "package/index.js" }]); }); +test("fails when package.json cannot be parsed", async () => { + await write(join(packageDir, "package.json"), '{"name": "pack-bad-json",'); + + const { err } = await packExpectError(packageDir, bunEnv); + expect(err).toContain(`failed to parse package.json: ${join(packageDir, "package.json")}`); +}); + test("in subdirectory", async () => { await Promise.all([ write( diff --git a/test/cli/update_interactive_install.test.ts b/test/cli/update_interactive_install.test.ts index 8392d7cae5ed..27f2ac4c9aa1 100644 --- a/test/cli/update_interactive_install.test.ts +++ b/test/cli/update_interactive_install.test.ts @@ -201,3 +201,24 @@ describe.concurrent("bun update --interactive actually installs packages", () => } }); }); + +describe.concurrent("bun update --interactive error handling", () => { + test("errors without a lockfile", async () => { + using dir = tempDir("update-interactive-no-lockfile", { + "package.json": JSON.stringify({ name: "no-lockfile", version: "1.0.0" }), + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "update", "--interactive"], + cwd: String(dir), + env: bunEnv, + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + }); + + const [stderr, _stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]); + expect(stderr).toContain("error: missing lockfile, nothing outdated"); + expect(exitCode).toBe(1); + }); +});