Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
98 changes: 94 additions & 4 deletions crates/uv-warnings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,63 @@ pub fn disable() {
ENABLED.store(false, std::sync::atomic::Ordering::Relaxed);
}

/// A callback function for printing warnings.
type PrinterCallback = Box<dyn Fn(&str) + Send + Sync>;

/// A global printer callback that, when set, is used to print warnings instead of writing
/// directly to stderr. This allows coordinating warning output with indicatif's progress bar
/// system via `MultiProgress::suspend()`.
static PRINTER: Mutex<Option<PrinterCallback>> = Mutex::new(None);

/// Set a global printer callback for warning output.
///
/// When set, all warnings will be routed through this callback instead of writing directly
/// to stderr. This is used to coordinate with indicatif progress bars.
///
/// Note: only one printer callback can be active at a time. If multiple reporters call
/// `set_printer` concurrently, the last one wins and `clear_printer` from an earlier
/// reporter will remove the later reporter's callback. Callers should ensure only one
/// reporter is active at a time.
pub fn set_printer(callback: Box<dyn Fn(&str) + Send + Sync>) {
if let Ok(mut printer) = PRINTER.lock() {
*printer = Some(callback);
}
}

/// Clear the global printer callback, restoring direct stderr output for warnings.
pub fn clear_printer() {
if let Ok(mut printer) = PRINTER.lock() {
*printer = None;
}
}

/// Print a warning line, routing through the global printer callback if set,
/// or falling back to `anstream::eprintln!`.
///
/// Uses `try_lock()` instead of `lock()` to avoid deadlocking if the callback
/// (or anything it transitively calls) triggers another warning on the same thread.
#[doc(hidden)]
pub fn print_warning(line: &str) {
if let Ok(printer) = PRINTER.try_lock() {
if let Some(callback) = printer.as_ref() {
callback(line);
return;
}
}
anstream::eprintln!("{line}");
}

/// Warn a user, if warnings are enabled.
#[macro_export]
macro_rules! warn_user {
($($arg:tt)*) => {{
use $crate::anstream::eprintln;
use $crate::owo_colors::OwoColorize;

if $crate::ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
let message = format!("{}", format_args!($($arg)*));
let formatted = message.bold();
eprintln!("{}{} {formatted}", "warning".yellow().bold(), ":".bold());
let line = format!("{}{} {formatted}", "warning".yellow().bold(), ":".bold());
$crate::print_warning(&line);
}
}};
}
Expand All @@ -46,14 +92,14 @@ pub static WARNINGS: LazyLock<Mutex<FxHashSet<String>>> = LazyLock::new(Mutex::d
#[macro_export]
macro_rules! warn_user_once {
($($arg:tt)*) => {{
use $crate::anstream::eprintln;
use $crate::owo_colors::OwoColorize;

if $crate::ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
if let Ok(mut states) = $crate::WARNINGS.lock() {
let message = format!("{}", format_args!($($arg)*));
if states.insert(message.clone()) {
eprintln!("{}{} {}", "warning".yellow().bold(), ":".bold(), message.bold());
let line = format!("{}{} {}", "warning".yellow().bold(), ":".bold(), message.bold());
$crate::print_warning(&line);
}
}
}
Expand Down Expand Up @@ -126,12 +172,56 @@ pub fn write_error_chain(

#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};

use crate::write_error_chain;
use crate::{clear_printer, print_warning, set_printer};
use anyhow::anyhow;
use indoc::indoc;
use insta::assert_snapshot;
use owo_colors::AnsiColors;

#[test]
fn set_printer_routes_warnings_through_callback() {
let captured: Arc<Mutex<Vec<String>>> = Arc::default();
let captured_clone = captured.clone();
set_printer(Box::new(move |line| {
captured_clone.lock().unwrap().push(line.to_string());
}));

print_warning("test warning message");

let messages = captured.lock().unwrap();
assert_eq!(messages.len(), 1);
assert_eq!(messages[0], "test warning message");

clear_printer();
}

#[test]
fn clear_printer_restores_direct_stderr() {
let captured: Arc<Mutex<Vec<String>>> = Arc::default();
let captured_clone = captured.clone();
set_printer(Box::new(move |line| {
captured_clone.lock().unwrap().push(line.to_string());
}));

clear_printer();
// After clearing, this should go to stderr, not the callback.
print_warning("after clear");

let messages = captured.lock().unwrap();
assert!(messages.is_empty());
}

#[test]
fn print_warning_falls_through_when_no_printer_set() {
// Ensure no printer is set.
clear_printer();
// Should write to stderr without panicking.
print_warning("fallthrough warning");
}

#[test]
fn format_multiline_message() {
let err_middle = indoc! {"Failed to fetch https://example.com/upload/python3.13.tar.zst
Expand Down
21 changes: 21 additions & 0 deletions crates/uv/src/commands/reporters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,17 @@ impl ProgressReporter {
// See: https://github.com/astral-sh/uv/issues/3887.
ProgressMode::Single
} else {
// Route warning output through `MultiProgress::suspend()` to avoid
// indicatif's line management from truncating warning messages.
// See: https://github.com/astral-sh/uv/issues/18626.
if !multi_progress.is_hidden() {
let mp = multi_progress.clone();
uv_warnings::set_printer(Box::new(move |line| {
mp.suspend(|| {
anstream::eprintln!("{line}");
});
}));
}
ProgressMode::Multi {
state: Arc::default(),
multi_progress,
Expand Down Expand Up @@ -422,6 +433,16 @@ impl ProgressReporter {
}
}

impl Drop for ProgressReporter {
fn drop(&mut self) {
if let ProgressMode::Multi { multi_progress, .. } = &self.mode {
if !multi_progress.is_hidden() {
uv_warnings::clear_printer();
}
}
}
}

#[derive(Debug)]
pub(crate) struct PrepareReporter {
reporter: ProgressReporter,
Expand Down
Loading