Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
d057a95
output: report a failed write to stdout instead of claiming success
alii Aug 15, 2026
3f9b90a
pm hash-string propagates the write error too; a zero-length write is…
alii Aug 15, 2026
8f1061a
console: stop printing instead of panicking when stdout cannot be wri…
alii Aug 15, 2026
349dff5
pm hash: keep output buffering paired when the write fails
alii Aug 15, 2026
a448a94
test: cover pm hash-string and console.log with an unwritable stdout
alii Aug 15, 2026
285395c
console: Request and BuildArtifact stop printing like their siblings …
alii Aug 15, 2026
a6ba44d
install: a core WriteFailed is install's WriteFailed, so bun.lockb in…
alii Aug 15, 2026
7f69374
bun --filter: printing a child's output is best-effort, finishing it …
alii Aug 15, 2026
fc4f9fd
test --coverage: a closed stderr does not skip the lcov/junit output …
alii Aug 15, 2026
ebdb005
build: write every output file even when the summary cannot be printed
alii Aug 15, 2026
ae11bdd
install: printing the log or the summary is best-effort once node_mod…
alii Aug 15, 2026
46710eb
bun build: free the option snapshots before exit_or_watch so LeakSani…
alii Aug 15, 2026
c449419
restore .bk.yaml
alii Aug 17, 2026
2f074c8
console: cover the Request and BuildArtifact printing paths in the cl…
alii Aug 17, 2026
e77fbd3
test: cover the --parallel, bun build and --coverage paths when stdou…
alii Aug 18, 2026
723ea31
console test: drop the BuildArtifact case, its writes cannot fail aft…
alii Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/install/PackageManager/PackageManagerDirectories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1178,7 +1178,6 @@ pub fn save_lockfile(
if this
.lockfile
.has_meta_hash_changed(false, packages_len_before_install)
.unwrap_or(false)
{
Output::panic(format_args!(
"Lockfile metahash non-deterministic after saving"
Expand Down
60 changes: 27 additions & 33 deletions src/install/PackageManager/install_with_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ pub fn install_with_manager(
manager.progress = Default::default();

match &load_result {
lockfile::LoadResult::Err(cause) => report_lockfile_load_error(manager, cause, log_level)?,
lockfile::LoadResult::Err(cause) => report_lockfile_load_error(manager, cause, log_level),
lockfile::LoadResult::Ok(ok) => {
if manager.subcommand == Subcommand::Update {
record_updating_package_versions(manager);
Expand Down Expand Up @@ -647,9 +647,9 @@ pub fn install_with_manager(
print_kept_patched(manager);

let had_errors_before_cleaning_lockfile = manager.log_mut().has_errors();
manager
let _ = manager
.log_mut()
.print(std::ptr::from_mut(Output::error_writer()))?;
.print(std::ptr::from_mut(Output::error_writer()));
manager.log_mut().reset();
super::add_catalog::refuse_declared_positionals(manager);

Expand Down Expand Up @@ -789,15 +789,11 @@ pub fn install_with_manager(
)) {
break 'frozen_lockfile;
}
} else if !(manager
.lockfile
.has_meta_hash_changed(
PackageManager::verbose_install()
|| manager.options.do_.print_meta_hash_string(),
packages_len_before_install,
)
.unwrap_or(false))
{
} else if !manager.lockfile.has_meta_hash_changed(
PackageManager::verbose_install()
|| manager.options.do_.print_meta_hash_string(),
packages_len_before_install,
) {
break 'frozen_lockfile;
}
}
Expand Down Expand Up @@ -906,9 +902,9 @@ pub fn install_with_manager(
};

if log_level != Options::LogLevel::Silent {
manager
let _ = manager
.log_mut()
.print(std::ptr::from_mut(Output::error_writer()))?;
.print(std::ptr::from_mut(Output::error_writer()));
}
if had_errors_before_cleaning_lockfile || manager.log_mut().has_errors() {
Global::crash();
Expand All @@ -926,7 +922,7 @@ pub fn install_with_manager(
manager.lockfile.has_meta_hash_changed(
PackageManager::verbose_install() || manager.options.do_.print_meta_hash_string(),
packages_len_before_install.min(manager.lockfile.packages.len()),
)?
)
};

// It's unnecessary work to re-save the lockfile if there are no changes.
Expand Down Expand Up @@ -980,7 +976,7 @@ pub fn install_with_manager(
did_meta_hash_change,
requests_removed_from_lockfile,
log_level,
)?;
);
}

if install_summary.fail > 0 {
Expand Down Expand Up @@ -1122,7 +1118,7 @@ fn print_install_summary(
did_meta_hash_change: bool,
requests_removed_from_lockfile: u32,
log_level: Options::LogLevel,
) -> crate::Result<()> {
) {
let _flush_guard = Output::flush_guard();

let mut printed_timestamp = false;
Expand All @@ -1132,12 +1128,12 @@ fn print_install_summary(
if install_summary.fail > 0 {
print_summary_failed(install_summary);
}
return Ok(());
return;
}
if this.subcommand == Subcommand::Update && this.options.dry_run {
return Ok(());
return;
}
print_summary_tree(this, install_summary, log_level)?;
print_summary_tree(this, install_summary, log_level);

let print_removed = this.subcommand == Subcommand::Remove
&& this.summary.remove > 0
Expand Down Expand Up @@ -1207,17 +1203,17 @@ fn print_install_summary(
if this.options.do_.summary() && !printed_timestamp {
print_summary_timing_fallback(ctx.start_time);
}

Ok(())
}

/// Best-effort: node_modules and the lockfile are already on disk, so a closed
/// stdout must not turn the install into an error.
#[cold]
#[inline(never)]
fn print_summary_tree(
this: &mut PackageManager,
install_summary: &PackageInstallSummary,
log_level: Options::LogLevel,
) -> crate::Result<()> {
) {
// `Tree::print` reads `manager.{subcommand, workspace_name_hash, update_target_workspaces, updating_packages}` and writes `manager.{track_installed_bin, kept_patched_text, manifests}`, none overlapping the `Printer`'s `lockfile` / `options` / `update_requests` borrows.
let mgr: *mut PackageManager = this;
// `mgr` is the sole provenance root from here through the `Tree::print`
Expand All @@ -1243,15 +1239,15 @@ fn print_summary_tree(
Output::enable_buffering();
let writer = Output::writer_buffered();
// Runtime bool → const-generic dispatch.
if Output::enable_ansi_colors_stdout() {
let _ = if Output::enable_ansi_colors_stdout() {
LockfilePrinter::Tree::print::<_, true>(
&printer,
// SAFETY: `mgr` is the sole provenance root; `Tree::print` writes only fields
// disjoint from `printer`'s shared `lockfile`/`options`/`update_requests` borrows.
unsafe { &mut *mgr },
writer,
log_level,
)?;
)
} else {
LockfilePrinter::Tree::print::<_, false>(
&printer,
Expand All @@ -1260,9 +1256,8 @@ fn print_summary_tree(
unsafe { &mut *mgr },
writer,
log_level,
)?;
}
Ok(())
)
};
}

#[cold]
Expand Down Expand Up @@ -1496,7 +1491,7 @@ fn report_lockfile_load_error(
manager: &mut PackageManager,
cause: &lockfile::LoadResultErr,
log_level: Options::LogLevel,
) -> crate::Result<()> {
) {
if log_level != Options::LogLevel::Silent
&& !crate::migration::reported_unsupported_lockfile_version(cause)
{
Expand All @@ -1512,9 +1507,9 @@ fn report_lockfile_load_error(
}

if manager.log_mut().errors > 0 {
manager
let _ = manager
.log_mut()
.print(std::ptr::from_mut(Output::error_writer()))?;
.print(std::ptr::from_mut(Output::error_writer()));
manager.log_mut().reset();
}
Output::flush();
Expand All @@ -1523,7 +1518,6 @@ fn report_lockfile_load_error(
if manager.options.enable.fail_early() {
Global::crash();
}
Ok(())
}

/// Returns the rows the plan re-resolved so the overrides/catalogs invalidation loops that follow leave them pinned; only tracked when those loops will run.
Expand Down Expand Up @@ -2184,7 +2178,7 @@ fn save_lockfile_only(
manager.lockfile.meta_hash = manager.lockfile.generate_meta_hash(
PackageManager::verbose_install() || manager.options.do_.print_meta_hash_string(),
packages_len_before_install,
)?;
);

let saved = save_lockfile(
manager,
Expand Down
3 changes: 1 addition & 2 deletions src/install/PackageManager/runTasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -932,8 +932,7 @@ pub fn run_tasks<C: RunTasksCallbacks>(
if !task.log.msgs.is_empty() {
// `IntoLogWrite` is implemented for `*mut bun_core::io::Writer`,
// not `&mut Writer` (the underlying `Writer` is the FFI shape).
// Propagate the write error (WriteFailed) out of `run_tasks`.
task.log.print(std::ptr::from_mut(Output::error_writer()))?;
let _ = task.log.print(std::ptr::from_mut(Output::error_writer()));
if task.log.errors > 0 {
manager.any_failed_to_install = true;
}
Expand Down
13 changes: 12 additions & 1 deletion src/install/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ pub enum Error {
#[error(transparent)]
Alloc(#[from] bun_alloc::AllocError),
#[error(transparent)]
Core(#[from] bun_core::Error),
Core(bun_core::Error),
#[error(transparent)]
Resolver(#[from] bun_resolver::Error),
#[error(transparent)]
Expand Down Expand Up @@ -453,6 +453,17 @@ impl From<crate::pnpm::MigratePnpmLockfileError> for Error {
}
}

impl From<bun_core::Error> for Error {
fn from(e: bun_core::Error) -> Self {
match e {
bun_core::Error::Alloc(a) => Self::Alloc(a),
bun_core::Error::WriteFailed => Self::WriteFailed,
bun_core::Error::InvalidCharacter => Self::InvalidCharacter,
other => Self::Core(other),
}
}
}

impl From<Error> for bun_core::Error {
fn from(e: Error) -> Self {
match e {
Expand Down
61 changes: 29 additions & 32 deletions src/install/lockfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2840,26 +2840,45 @@ impl Lockfile {
&mut self,
print_name_version_string: bool,
packages_len: usize,
) -> Result<bool, BunError> {
) -> bool {
let previous_meta_hash = self.meta_hash;
self.meta_hash = self.generate_meta_hash(print_name_version_string, packages_len)?;
Ok(!strings::eql_long(
&previous_meta_hash,
&self.meta_hash,
false,
))
self.meta_hash = self.generate_meta_hash(print_name_version_string, packages_len);
!strings::eql_long(&previous_meta_hash, &self.meta_hash, false)
}

pub(crate) fn generate_meta_hash(
&self,
print_name_version_string: bool,
packages_len: usize,
) -> Result<MetaHash, BunError> {
) -> MetaHash {
if packages_len <= 1 {
return Ok(ZERO_HASH);
return ZERO_HASH;
}

let input = self.meta_hash_input(packages_len);
let input = input.written_slice();
if print_name_version_string {
// The `--verbose` dump is best-effort; `bun pm hash-string` prints
// `meta_hash_input()` itself so it can report a failed write.
Output::flush();
let _buffering = Output::disable_buffering_scope();
let _ = Output::writer().write_all(input);
}

let mut digest = ZERO_HASH;
// SAFETY: engine is null (default).
unsafe { Crypto::SHA512_256::hash(input, &mut digest, core::ptr::null_mut()) };
digest
}

/// The text `generate_meta_hash` digests; empty when the lockfile holds at
/// most the root package.
pub fn meta_hash_input(&self, packages_len: usize) -> bun_core::StringBuilder {
let mut string_builder = bun_core::StringBuilder::default();
if packages_len <= 1 {
return string_builder;
}

let names: &[SemverString] = &self.packages.items_name()[..packages_len];
let resolutions: &[Resolution] = &self.packages.items_resolution()[..packages_len];
let bytes = self.buffers.string_bytes.as_slice();
Expand Down Expand Up @@ -2958,29 +2977,7 @@ impl Lockfile {
}

let _ = string_builder.append(HASH_SUFFIX);

let len = string_builder.len;
let alphabetized_name_version_string = &string_builder.allocated_slice()[..len];
if print_name_version_string {
Output::flush();
Output::disable_buffering();
Output::writer()
.write_all(alphabetized_name_version_string)
.expect("unreachable");
Output::enable_buffering();
}

let mut digest = ZERO_HASH;
// SAFETY: engine is null (default).
unsafe {
Crypto::SHA512_256::hash(
alphabetized_name_version_string,
&mut digest,
core::ptr::null_mut(),
)
};

Ok(digest)
string_builder
}

pub(crate) fn resolve_package_from_name_and_version(
Expand Down
2 changes: 1 addition & 1 deletion src/install/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ fn migrate_npm_lockfile<'a>(
this.verify_data()?;
}

this.meta_hash = this.generate_meta_hash(false, this.packages.len())?;
this.meta_hash = this.generate_meta_hash(false, this.packages.len());

Ok(LoadResult::Ok(LoadResultOk {
lockfile: this,
Expand Down
9 changes: 4 additions & 5 deletions src/install/update_transitive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ pub(crate) fn enqueue_peer_rows(
index_sort::sort_indices_unstable(&mut targets, &mut |a, b| a.cmp(&b));
targets.dedup();
populate_manifest_cache::populate_manifest_cache(manager, Packages::Exact(&targets))?;
print_log(manager)?;
print_log(manager);
for &row in rows {
moved.push((row, manager.lockfile.buffers.resolutions[row as usize]));
reresolve(manager, row)?;
Expand All @@ -463,13 +463,12 @@ pub(crate) fn enqueue_peer_rows(
}

/// Pending log lines go to stderr; `--silent` drops everything but errors.
fn print_log(manager: &PackageManager) -> crate::Result<()> {
fn print_log(manager: &PackageManager) {
let log = manager.log_mut();
if manager.options.log_level != LogLevel::Silent || log.has_errors() {
log.print(core::ptr::from_mut(Output::error_writer()))?;
let _ = log.print(core::ptr::from_mut(Output::error_writer()));
}
log.reset();
Ok(())
}

/// `moved` pairs an invalidated edge with the package it used to resolve to; every other edge still on that package follows it to the edge's new npm resolution when its range allows.
Expand Down Expand Up @@ -1219,7 +1218,7 @@ fn plan_edges(
}
index_sort::sort_vec_unstable_by(&mut unchecked, |a, b| a.cmp(b));
warn_unchecked(manager, msgs_before, &unchecked);
print_log(manager)?;
print_log(manager);

for (range, hash, pre) in pre_strings {
let pre = manager
Expand Down
2 changes: 1 addition & 1 deletion src/install/yarn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1913,7 +1913,7 @@ pub(crate) fn migrate_yarn_lockfile<'a>(
this.verify_data()?;
}

this.meta_hash = this.generate_meta_hash(false, this.packages.len())?;
this.meta_hash = this.generate_meta_hash(false, this.packages.len());

let result = LoadResult::Ok(lockfile::LoadResultOk {
lockfile: this,
Expand Down
Loading