diff --git a/crates/uv-warnings/src/lib.rs b/crates/uv-warnings/src/lib.rs index 2b664be8d..a076682ef 100644 --- a/crates/uv-warnings/src/lib.rs +++ b/crates/uv-warnings/src/lib.rs @@ -1,3 +1,5 @@ +use std::error::Error; +use std::iter; use std::sync::atomic::AtomicBool; use std::sync::{LazyLock, Mutex}; @@ -6,6 +8,7 @@ use std::sync::{LazyLock, Mutex}; pub use anstream; #[doc(hidden)] pub use owo_colors; +use owo_colors::{DynColor, OwoColorize}; use rustc_hash::FxHashSet; /// Whether user-facing warnings are enabled. @@ -56,3 +59,41 @@ macro_rules! warn_user_once { } }}; } + +/// Format an error or warning chain. +/// +/// # Example +/// +/// ```text +/// error: Failed to install app +/// Caused By: Failed to install dependency +/// Caused By: Error writing failed `/home/ferris/deps/foo`: Permission denied +/// ``` +/// +/// ```text +/// warning: Failed to create registry entry for Python 3.12 +/// Caused By: Security policy forbids chaining registry entries +/// ``` +pub fn write_error_chain( + err: &dyn Error, + mut stream: impl std::fmt::Write, + level: impl AsRef, + color: impl DynColor + Copy, +) -> std::fmt::Result { + writeln!( + &mut stream, + "{}{} {}", + level.as_ref().color(color).bold(), + ":".bold(), + err.to_string().trim() + )?; + for source in iter::successors(err.source(), |&err| err.source()) { + writeln!( + &mut stream, + " {}: {}", + "Caused by".color(color).bold(), + source.to_string().trim() + )?; + } + Ok(()) +} diff --git a/crates/uv/src/commands/publish.rs b/crates/uv/src/commands/publish.rs index e7f5e00a2..083b6503a 100644 --- a/crates/uv/src/commands/publish.rs +++ b/crates/uv/src/commands/publish.rs @@ -1,11 +1,10 @@ use std::fmt::Write; -use std::iter; use std::sync::Arc; use std::time::Duration; use anyhow::{Context, Result, bail}; use console::Term; -use owo_colors::OwoColorize; +use owo_colors::{AnsiColors, OwoColorize}; use tokio::sync::Semaphore; use tracing::{debug, info}; use uv_auth::Credentials; @@ -17,7 +16,7 @@ use uv_publish::{ CheckUrlClient, TrustedPublishResult, check_trusted_publishing, files_for_publishing, upload, }; use uv_redacted::DisplaySafeUrl; -use uv_warnings::warn_user_once; +use uv_warnings::{warn_user_once, write_error_chain}; use crate::commands::reporters::PublishReporter; use crate::commands::{ExitStatus, human_readable_bytes}; @@ -274,19 +273,15 @@ async fn gather_credentials( fetching the trusted publishing token. If you don't want to use trusted \ publishing, you can ignore this error, but you need to provide credentials." )?; - writeln!( + + write_error_chain( + anyhow::Error::from(err) + .context("Trusted publishing failed") + .as_ref(), printer.stderr(), - "{}: {err}", - "Trusted publishing error".red().bold() + "error", + AnsiColors::Red, )?; - for source in iter::successors(std::error::Error::source(&err), |&err| err.source()) { - writeln!( - printer.stderr(), - " {}: {}", - "Caused by".red().bold(), - source.to_string().trim() - )?; - } } } diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index f2082ce8a..02e4c27e5 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -10,7 +10,7 @@ use futures::StreamExt; use futures::stream::FuturesUnordered; use indexmap::IndexSet; use itertools::{Either, Itertools}; -use owo_colors::OwoColorize; +use owo_colors::{AnsiColors, OwoColorize}; use rustc_hash::{FxHashMap, FxHashSet}; use tracing::{debug, trace}; @@ -30,7 +30,7 @@ use uv_python::{ }; use uv_shell::Shell; use uv_trampoline_builder::{Launcher, LauncherKind}; -use uv_warnings::warn_user; +use uv_warnings::{warn_user, write_error_chain}; use crate::commands::python::{ChangeEvent, ChangeEventKind}; use crate::commands::reporters::PythonDownloadReporter; @@ -139,7 +139,7 @@ impl Changelog { enum InstallErrorKind { DownloadUnpack, Bin, - #[cfg(windows)] + #[cfg_attr(not(windows), allow(dead_code))] Registry, } @@ -667,7 +667,6 @@ pub(crate) async fn install( // to warn let fatal = !errors.iter().all(|(kind, _, _)| match kind { InstallErrorKind::Bin => bin.is_none(), - #[cfg(windows)] InstallErrorKind::Registry => registry.is_none(), InstallErrorKind::DownloadUnpack => false, }); @@ -676,40 +675,45 @@ pub(crate) async fn install( .into_iter() .sorted_unstable_by(|(_, key_a, _), (_, key_b, _)| key_a.cmp(key_b)) { - let (level, verb) = match kind { - InstallErrorKind::DownloadUnpack => ("error".red().bold().to_string(), "install"), + match kind { + InstallErrorKind::DownloadUnpack => { + write_error_chain( + err.context(format!("Failed to install {key}")).as_ref(), + printer.stderr(), + "error", + AnsiColors::Red, + )?; + } InstallErrorKind::Bin => { - let level = match bin { - None => "warning".yellow().bold().to_string(), + let (level, color) = match bin { + None => ("warning", AnsiColors::Yellow), Some(false) => continue, - Some(true) => "error".red().bold().to_string(), + Some(true) => ("error", AnsiColors::Red), }; - (level, "install executable for") - } - #[cfg(windows)] - InstallErrorKind::Registry => { - let level = match registry { - None => "warning".yellow().bold().to_string(), - Some(false) => continue, - Some(true) => "error".red().bold().to_string(), - }; - (level, "install registry entry for") - } - }; - writeln!( - printer.stderr(), - "{level}{} Failed to {verb} {}", - ":".bold(), - key.green() - )?; - for err in err.chain() { - writeln!( - printer.stderr(), - " {}: {}", - "Caused by".red().bold(), - err.to_string().trim() - )?; + write_error_chain( + err.context(format!("Failed to install executable for {key}")) + .as_ref(), + printer.stderr(), + level, + color, + )?; + } + InstallErrorKind::Registry => { + let (level, color) = match registry { + None => ("warning", AnsiColors::Yellow), + Some(false) => continue, + Some(true) => ("error", AnsiColors::Red), + }; + + write_error_chain( + err.context(format!("Failed to create registry entry for {key}")) + .as_ref(), + printer.stderr(), + level, + color, + )?; + } } } diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs index 13e1f2ae3..af42a9eef 100644 --- a/crates/uv/src/commands/tool/upgrade.rs +++ b/crates/uv/src/commands/tool/upgrade.rs @@ -1,6 +1,6 @@ use anyhow::Result; use itertools::Itertools; -use owo_colors::OwoColorize; +use owo_colors::{AnsiColors, OwoColorize}; use std::collections::BTreeMap; use std::fmt::Write; use tracing::debug; @@ -18,6 +18,7 @@ use uv_python::{ use uv_requirements::RequirementsSpecification; use uv_settings::{Combine, PythonInstallMirrors, ResolverInstallerOptions, ToolOptions}; use uv_tool::InstalledTools; +use uv_warnings::write_error_chain; use uv_workspace::WorkspaceCache; use crate::commands::pip::loggers::{ @@ -155,20 +156,13 @@ pub(crate) async fn upgrade( .into_iter() .sorted_unstable_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b)) { - writeln!( + write_error_chain( + err.context(format!("Failed to upgrade {}", name.green())) + .as_ref(), printer.stderr(), - "{}: Failed to upgrade {}", - "error".red().bold(), - name.green() + "error", + AnsiColors::Red, )?; - for err in err.chain() { - writeln!( - printer.stderr(), - " {}: {}", - "Caused by".red().bold(), - err.to_string().trim() - )?; - } } return Ok(ExitStatus::Failure); } diff --git a/crates/uv/tests/it/publish.rs b/crates/uv/tests/it/publish.rs index 0fdf435e8..ba86cf7e9 100644 --- a/crates/uv/tests/it/publish.rs +++ b/crates/uv/tests/it/publish.rs @@ -126,7 +126,7 @@ fn no_credentials() { // Emulate CI .env(EnvVars::GITHUB_ACTIONS, "true") // Just to make sure - .env_remove(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN), @r###" + .env_remove(EnvVars::ACTIONS_ID_TOKEN_REQUEST_TOKEN), @r" success: false exit_code: 2 ----- stdout ----- @@ -134,12 +134,13 @@ fn no_credentials() { ----- stderr ----- Publishing 1 file to https://test.pypi.org/legacy/ Note: Neither credentials nor keyring are configured, and there was an error fetching the trusted publishing token. If you don't want to use trusted publishing, you can ignore this error, but you need to provide credentials. - Trusted publishing error: Environment variable ACTIONS_ID_TOKEN_REQUEST_TOKEN not set, is the `id-token: write` permission missing? + error: Trusted publishing failed + Caused by: Environment variable ACTIONS_ID_TOKEN_REQUEST_TOKEN not set, is the `id-token: write` permission missing? Uploading ok-1.0.0-py3-none-any.whl ([SIZE]) error: Failed to publish `../../scripts/links/ok-1.0.0-py3-none-any.whl` to https://test.pypi.org/legacy/ Caused by: Failed to send POST request Caused by: Missing credentials for https://test.pypi.org/legacy/ - "### + " ); }