Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3965593
output: report a failed write to stdout instead of claiming success
alii Aug 15, 2026
c7740cb
pm hash-string propagates the write error too; a zero-length write is…
alii Aug 15, 2026
bd5963b
console: stop printing instead of panicking when stdout cannot be wri…
alii Aug 15, 2026
6ce8b11
pm hash: keep output buffering paired when the write fails
alii Aug 15, 2026
0a53911
test: cover pm hash-string and console.log with an unwritable stdout
alii Aug 15, 2026
f8541ce
console: Request and BuildArtifact stop printing like their siblings …
alii Aug 15, 2026
c55b23a
install: a core WriteFailed is install's WriteFailed, so bun.lockb in…
alii Aug 15, 2026
8bca574
bun --filter: printing a child's output is best-effort, finishing it …
alii Aug 15, 2026
1f10bcf
test --coverage: a closed stderr does not skip the lcov/junit output …
alii Aug 15, 2026
6b384de
build: write every output file even when the summary cannot be printed
alii Aug 15, 2026
e56d2ec
install: printing the log or the summary is best-effort once node_mod…
alii Aug 15, 2026
2115988
bun build: free the option snapshots before exit_or_watch so LeakSani…
alii Aug 15, 2026
0803f70
restore .bk.yaml
alii Aug 17, 2026
a411553
console: cover the Request and BuildArtifact printing paths in the cl…
alii Aug 17, 2026
53d1236
test: cover the --parallel, bun build and --coverage paths when stdou…
alii Aug 18, 2026
ea17c9b
console test: drop the BuildArtifact case, its writes cannot fail aft…
alii Aug 18, 2026
4f134ae
install: the remaining log prints to stderr are best-effort too
robobun Aug 19, 2026
804de55
bun build: write a single bundle to stdout unbuffered so a failed wri…
robobun Aug 22, 2026
f107e40
BuildArtifact: restore the formatter indent on every exit of write_fo…
robobun Aug 22, 2026
308f334
Blob: restore the formatter indent on every exit of write_format
robobun Aug 22, 2026
76331b2
install: an allocation failure while printing the summary tree is sti…
robobun Aug 22, 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 @@ -1179,7 +1179,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
6 changes: 3 additions & 3 deletions src/install/PackageManager/PopulateManifestCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,13 +366,13 @@ pub fn populate_manifest_cache(
}

/// Prints the fetch failures a [`Packages::Ids`] pass logged; true when one of them is a required dependency's.
pub fn print_fetch_failures(manager: &PackageManager) -> crate::Result<bool> {
pub fn print_fetch_failures(manager: &PackageManager) -> bool {
let log = manager.log_mut();
let failed_required = log.has_errors();
if !log.msgs.is_empty() {
Output::flush();
log.print(core::ptr::from_mut(Output::error_writer()))?;
let _ = log.print(core::ptr::from_mut(Output::error_writer()));
log.reset();
}
Ok(failed_required)
failed_required
}
76 changes: 35 additions & 41 deletions src/install/PackageManager/install_with_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,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 All @@ -163,7 +163,7 @@ pub fn install_with_manager(
let mut lockfile = Lockfile::default();
let mut maybe_root = lockfile::Package::default();

let source_copy = root_package_json_source(manager, root_package_json_path)?;
let source_copy = root_package_json_source(manager, root_package_json_path);

let mut resolver: () = ();
// `parse` needs `manager`, `manager.log` and a fresh
Expand Down Expand Up @@ -659,9 +659,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()));
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
manager.log_mut().reset();
super::add_catalog::refuse_declared_positionals(manager);

Expand Down Expand Up @@ -801,15 +801,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 @@ -918,9 +914,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 @@ -938,7 +934,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 @@ -992,7 +988,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 @@ -1141,7 +1137,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 @@ -1151,12 +1147,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 @@ -1226,17 +1222,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.
Comment thread
robobun marked this conversation as resolved.
Resolved
#[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 @@ -1262,15 +1258,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 printed = 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 @@ -1279,9 +1275,11 @@ fn print_summary_tree(
unsafe { &mut *mgr },
writer,
log_level,
)?;
)
};
if let Err(crate::Error::Alloc(bun_alloc::AllocError)) = printed {
bun_core::out_of_memory();
}
Ok(())
}

#[cold]
Expand Down Expand Up @@ -1515,7 +1513,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 @@ -1531,9 +1529,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 @@ -1542,7 +1540,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 @@ -1867,23 +1864,20 @@ fn record_updating_package_versions(manager: &mut PackageManager) {
}
}

fn root_package_json_source(
manager: &mut PackageManager,
root_package_json_path: &ZStr,
) -> crate::Result<Source> {
fn root_package_json_source(manager: &mut PackageManager, root_package_json_path: &ZStr) -> Source {
let (verb, err) = match manager.workspace_package_json_cache.get_with_path(
manager.log_mut(),
root_package_json_path.as_bytes(),
Default::default(),
) {
WorkspacePackageJsonCacheResult::Entry(entry) => return Ok(entry.source.clone()),
WorkspacePackageJsonCacheResult::Entry(entry) => return entry.source.clone(),
WorkspacePackageJsonCacheResult::ReadErr(err) => ("read", err),
WorkspacePackageJsonCacheResult::ParseErr(err) => ("parse", err),
};
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()));
}
Output::err(
err,
Expand Down Expand Up @@ -1930,7 +1924,7 @@ fn create_new_lockfile_and_enqueue(
Global::crash();
}

let source_copy = root_package_json_source(manager, root_package_json_path)?;
let source_copy = root_package_json_source(manager, root_package_json_path);

let mut resolver: () = ();
{
Expand Down Expand Up @@ -2202,7 +2196,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 @@ -1039,8 +1039,7 @@ fn run_tasks_erased(
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 @@ -212,7 +212,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 @@ -444,6 +444,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
63 changes: 30 additions & 33 deletions src/install/lockfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1685,7 +1685,7 @@ impl<'a> Printer<'a> {
// `IntoLogWrite` is implemented for `*mut bun_core::io::Writer`,
// not `&mut &mut Writer` — pass the raw vtable pointer.
let ew: *mut bun_core::io::Writer = Output::error_writer();
log.print(ew)?;
let _ = log.print(ew);
}
Global::crash();
}
Expand Down Expand Up @@ -2897,26 +2897,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.
Comment thread
robobun marked this conversation as resolved.
Resolved
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.
Comment thread
robobun marked this conversation as resolved.
Resolved
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 @@ -3046,29 +3065,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
Loading
Loading