diff --git a/Cargo.lock b/Cargo.lock index 226ef0f0e257c..6140db862ad72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4363,7 +4363,6 @@ dependencies = [ "byteorder", "cache-key", "clap", - "console", "ctrlc", "distribution-filename", "distribution-types", @@ -4472,7 +4471,6 @@ dependencies = [ "indoc", "insta", "itertools 0.13.0", - "owo-colors", "pep440_rs", "pep508_rs", "pypi-types", diff --git a/crates/uv-build-frontend/Cargo.toml b/crates/uv-build-frontend/Cargo.toml index cec8d7cf1a95b..23d44d88dbd1f 100644 --- a/crates/uv-build-frontend/Cargo.toml +++ b/crates/uv-build-frontend/Cargo.toml @@ -29,9 +29,7 @@ anyhow = { workspace = true } fs-err = { workspace = true } indoc = { workspace = true } itertools = { workspace = true } -owo-colors = { workspace = true } regex = { workspace = true } -rustc-hash = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tempfile = { workspace = true } @@ -39,6 +37,7 @@ thiserror = { workspace = true } tokio = { workspace = true } toml_edit = { workspace = true } tracing = { workspace = true } +rustc-hash = { workspace = true } [dev-dependencies] insta = { version = "1.40.0" } diff --git a/crates/uv-build-frontend/src/error.rs b/crates/uv-build-frontend/src/error.rs index f68d0fc78c57e..3b37a2b5d989c 100644 --- a/crates/uv-build-frontend/src/error.rs +++ b/crates/uv-build-frontend/src/error.rs @@ -1,37 +1,19 @@ use crate::PythonRunnerOutput; use itertools::Itertools; -use owo_colors::OwoColorize; use pep440_rs::Version; use pep508_rs::PackageName; use regex::Regex; -use rustc_hash::FxHashMap; use std::env; use std::fmt::{Display, Formatter}; use std::io; use std::path::PathBuf; use std::process::ExitStatus; -use std::str::FromStr; use std::sync::LazyLock; use thiserror::Error; use tracing::error; use uv_configuration::BuildOutput; use uv_fs::Simplified; -/// Static map of common package name typos or misconfigurations to their correct package names. -static SUGGESTIONS: LazyLock> = LazyLock::new(|| { - let suggestions: Vec<(String, String)> = - serde_json::from_str(include_str!("suggestions.json")).unwrap(); - suggestions - .iter() - .map(|(k, v)| { - ( - PackageName::from_str(k).unwrap(), - PackageName::from_str(v).unwrap(), - ) - }) - .collect() -}); - /// e.g. `pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory` static MISSING_HEADER_RE_GCC: LazyLock = LazyLock::new(|| { Regex::new( @@ -95,12 +77,13 @@ pub enum Error { stderr: String, }, /// Nudge the user towards installing the missing dev library - #[error("{message} with {exit_code}\n--- stdout:\n{stdout}\n--- stderr:\n{stderr}\n---\n\n{hint}{colon} {missing_header_cause}", hint = "hint".cyan().bold(), colon = ":".bold())] + #[error("{message} with {exit_code}\n--- stdout:\n{stdout}\n--- stderr:\n{stderr}\n---")] MissingHeaderOutput { message: String, exit_code: ExitStatus, stdout: String, stderr: String, + #[source] missing_header_cause: MissingHeaderCause, }, #[error("{message} with {exit_code}")] @@ -108,10 +91,11 @@ pub enum Error { message: String, exit_code: ExitStatus, }, - #[error("{message} with {exit_code}\n\n{hint}{colon} {missing_header_cause}", hint = "hint".cyan().bold(), colon = ":".bold())] + #[error("{message} with {exit_code}")] MissingHeader { message: String, exit_code: ExitStatus, + #[source] missing_header_cause: MissingHeaderCause, }, #[error("Failed to build PATH for build script")] @@ -124,7 +108,6 @@ enum MissingLibrary { Linker(String), BuildDependency(String), DeprecatedModule(String, Version), - SuggestedPackage(String, String), } #[derive(Debug, Error)] @@ -213,15 +196,6 @@ impl Display for MissingHeaderCause { ) } } - MissingLibrary::SuggestedPackage(package, suggestion) => { - write!( - f, - "`{}` is often confused for `{}` Did you mean to install `{}` instead?", - package.cyan(), - suggestion.cyan(), - suggestion.cyan(), - ) - } } } } @@ -236,42 +210,32 @@ impl Error { version: Option<&Version>, version_id: Option<&str>, ) -> Self { - // Check if we failed to install a known package. - let missing_library = if let Some((name, suggestion)) = - name.and_then(|name| SUGGESTIONS.get(name).map(|suggestion| (name, suggestion))) - { - Some(MissingLibrary::SuggestedPackage( - name.to_string(), - suggestion.to_string(), - )) - } else { - // Limit search to the last 10 lines, which typically contains the relevant error. - output.stderr.iter().rev().take(10).find_map(|line| { - if let Some((_, [header])) = MISSING_HEADER_RE_GCC - .captures(line.trim()) - .or(MISSING_HEADER_RE_CLANG.captures(line.trim())) - .or(MISSING_HEADER_RE_MSVC.captures(line.trim())) - .map(|c| c.extract()) - { - Some(MissingLibrary::Header(header.to_string())) - } else if let Some((_, [library])) = - LD_NOT_FOUND_RE.captures(line.trim()).map(|c| c.extract()) - { - Some(MissingLibrary::Linker(library.to_string())) - } else if WHEEL_NOT_FOUND_RE.is_match(line.trim()) { - Some(MissingLibrary::BuildDependency("wheel".to_string())) - } else if TORCH_NOT_FOUND_RE.is_match(line.trim()) { - Some(MissingLibrary::BuildDependency("torch".to_string())) - } else if DISTUTILS_NOT_FOUND_RE.is_match(line.trim()) { - Some(MissingLibrary::DeprecatedModule( - "distutils".to_string(), - Version::new([3, 12]), - )) - } else { - None - } - }) - }; + // In the cases I've seen it was the 5th and 3rd last line (see test case), 10 seems like a reasonable cutoff. + let missing_library = output.stderr.iter().rev().take(10).find_map(|line| { + if let Some((_, [header])) = MISSING_HEADER_RE_GCC + .captures(line.trim()) + .or(MISSING_HEADER_RE_CLANG.captures(line.trim())) + .or(MISSING_HEADER_RE_MSVC.captures(line.trim())) + .map(|c| c.extract()) + { + Some(MissingLibrary::Header(header.to_string())) + } else if let Some((_, [library])) = + LD_NOT_FOUND_RE.captures(line.trim()).map(|c| c.extract()) + { + Some(MissingLibrary::Linker(library.to_string())) + } else if WHEEL_NOT_FOUND_RE.is_match(line.trim()) { + Some(MissingLibrary::BuildDependency("wheel".to_string())) + } else if TORCH_NOT_FOUND_RE.is_match(line.trim()) { + Some(MissingLibrary::BuildDependency("torch".to_string())) + } else if DISTUTILS_NOT_FOUND_RE.is_match(line.trim()) { + Some(MissingLibrary::DeprecatedModule( + "distutils".to_string(), + Version::new([3, 12]), + )) + } else { + None + } + }); if let Some(missing_library) = missing_library { return match level { diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 53b6140cf16b0..cbca02b22121b 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -184,7 +184,10 @@ impl<'a> BuildContext for BuildDispatch<'a> { let graph = resolver.resolve().await.with_context(|| { format!( "No solution found when resolving: {}", - requirements.iter().map(ToString::to_string).join(", "), + requirements + .iter() + .map(|requirement| format!("`{requirement}`")) + .join(", ") ) })?; Ok(Resolution::from(graph)) diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index a5ba044afbd16..2efd2b045a945 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -57,7 +57,6 @@ axoupdater = { workspace = true, features = [ "tokio", ], optional = true } clap = { workspace = true, features = ["derive", "string", "wrap_help"] } -console = { workspace = true } ctrlc = { workspace = true } flate2 = { workspace = true, default-features = false } fs-err = { workspace = true, features = ["tokio"] } diff --git a/crates/uv/src/commands/diagnostics.rs b/crates/uv/src/commands/diagnostics.rs new file mode 100644 index 0000000000000..d6029e7539283 --- /dev/null +++ b/crates/uv/src/commands/diagnostics.rs @@ -0,0 +1,112 @@ +use distribution_types::{Name, SourceDist}; +use owo_colors::OwoColorize; +use rustc_hash::FxHashMap; +use std::str::FromStr; +use std::sync::LazyLock; +use uv_normalize::PackageName; + +/// Static map of common package name typos or misconfigurations to their correct package names. +static SUGGESTIONS: LazyLock> = LazyLock::new(|| { + let suggestions: Vec<(String, String)> = + serde_json::from_str(include_str!("suggestions.json")).unwrap(); + suggestions + .iter() + .map(|(k, v)| { + ( + PackageName::from_str(k).unwrap(), + PackageName::from_str(v).unwrap(), + ) + }) + .collect() +}); + +/// Render a [`uv_resolver::ResolveError::FetchAndBuild`] with a help message. +pub(crate) fn fetch_and_build(sdist: Box, cause: uv_distribution::Error) { + #[derive(Debug, miette::Diagnostic, thiserror::Error)] + #[error("Failed to download and build `{sdist}`")] + #[diagnostic()] + struct Error { + sdist: Box, + #[source] + cause: uv_distribution::Error, + #[help] + help: Option, + } + + let report = miette::Report::new(Error { + help: SUGGESTIONS.get(sdist.name()).map(|suggestion| { + format!( + "`{}` is often confused for `{}` Did you mean to install `{}` instead?", + sdist.name().cyan(), + suggestion.cyan(), + suggestion.cyan(), + ) + }), + sdist, + cause, + }); + anstream::eprint!("{report:?}"); +} + +/// Render a [`uv_resolver::ResolveError::Build`] with a help message. +pub(crate) fn build(sdist: Box, cause: uv_distribution::Error) { + #[derive(Debug, miette::Diagnostic, thiserror::Error)] + #[error("Failed to build `{sdist}`")] + #[diagnostic()] + struct Error { + sdist: Box, + #[source] + cause: uv_distribution::Error, + #[help] + help: Option, + } + + let report = miette::Report::new(Error { + help: SUGGESTIONS.get(sdist.name()).map(|suggestion| { + format!( + "`{}` is often confused for `{}` Did you mean to install `{}` instead?", + sdist.name().cyan(), + suggestion.cyan(), + suggestion.cyan(), + ) + }), + sdist, + cause, + }); + anstream::eprint!("{report:?}"); +} + +/// Render a [`uv_resolver::NoSolutionError`]. +pub(crate) fn no_solution(err: &uv_resolver::NoSolutionError) { + let report = miette::Report::msg(format!("{err}")).context(err.header()); + anstream::eprint!("{report:?}"); +} + +/// Render a [`uv_resolver::NoSolutionError`] with dedicated context. +pub(crate) fn no_solution_context(err: &uv_resolver::NoSolutionError, context: &'static str) { + let report = miette::Report::msg(format!("{err}")).context(err.header().with_context(context)); + anstream::eprint!("{report:?}"); +} + +/// Render a [`uv_resolver::NoSolutionError`] with a help message. +pub(crate) fn no_solution_hint(err: uv_resolver::NoSolutionError, help: String) { + #[derive(Debug, miette::Diagnostic, thiserror::Error)] + #[error("{header}")] + #[diagnostic()] + struct Error { + /// The header to render in the error message. + header: uv_resolver::NoSolutionHeader, + + /// The underlying error. + #[source] + err: uv_resolver::NoSolutionError, + + /// The help message to display. + #[help] + help: String, + } + + let header = err.header(); + let report = miette::Report::new(Error { header, err, help }); + anstream::eprint!("{report:?}"); +} diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index 7a612a53f3156..5af5e663d8423 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -65,6 +65,7 @@ pub(crate) mod build_backend; mod cache_clean; mod cache_dir; mod cache_prune; +mod diagnostics; mod help; pub(crate) mod pip; mod project; diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 60abcaefa6662..d491447d5cb2d 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -1,7 +1,6 @@ use std::env; use std::path::Path; -use anstream::eprint; use anyhow::{anyhow, Result}; use itertools::Itertools; use owo_colors::OwoColorize; @@ -42,7 +41,7 @@ use uv_warnings::warn_user; use crate::commands::pip::loggers::DefaultResolveLogger; use crate::commands::pip::{operations, resolution_environment}; -use crate::commands::{ExitStatus, OutputWriter}; +use crate::commands::{diagnostics, ExitStatus, OutputWriter}; use crate::printer::Printer; /// Resolve a set of requirements into a set of pinned versions. @@ -391,8 +390,15 @@ pub(crate) async fn pip_compile( { Ok(resolution) => resolution, Err(operations::Error::Resolve(uv_resolver::ResolveError::NoSolution(err))) => { - let report = miette::Report::msg(format!("{err}")).context(err.header()); - eprint!("{report:?}"); + diagnostics::no_solution(&err); + return Ok(ExitStatus::Failure); + } + Err(operations::Error::Resolve(uv_resolver::ResolveError::FetchAndBuild(dist, err))) => { + diagnostics::fetch_and_build(dist, err); + return Ok(ExitStatus::Failure); + } + Err(operations::Error::Resolve(uv_resolver::ResolveError::Build(dist, err))) => { + diagnostics::build(dist, err); return Ok(ExitStatus::Failure); } Err(err) => return Err(err.into()), diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index fce3d8879a69b..2e0d683d7b6a7 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -1,6 +1,5 @@ use std::fmt::Write; -use anstream::eprint; use itertools::Itertools; use owo_colors::OwoColorize; use tracing::{debug, enabled, Level}; @@ -36,7 +35,7 @@ use uv_types::{BuildIsolation, HashStrategy}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; use crate::commands::pip::operations::Modifications; use crate::commands::pip::{operations, resolution_markers, resolution_tags}; -use crate::commands::{ExitStatus, SharedState}; +use crate::commands::{diagnostics, ExitStatus, SharedState}; use crate::printer::Printer; /// Install packages into the current environment. @@ -394,8 +393,15 @@ pub(crate) async fn pip_install( { Ok(resolution) => Resolution::from(resolution), Err(operations::Error::Resolve(uv_resolver::ResolveError::NoSolution(err))) => { - let report = miette::Report::msg(format!("{err}")).context(err.header()); - eprint!("{report:?}"); + diagnostics::no_solution(&err); + return Ok(ExitStatus::Failure); + } + Err(operations::Error::Resolve(uv_resolver::ResolveError::FetchAndBuild(dist, err))) => { + diagnostics::fetch_and_build(dist, err); + return Ok(ExitStatus::Failure); + } + Err(operations::Error::Resolve(uv_resolver::ResolveError::Build(dist, err))) => { + diagnostics::build(dist, err); return Ok(ExitStatus::Failure); } Err(err) => return Err(err.into()), diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index 3e8b5c796e2ea..921058ba28859 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -1,6 +1,5 @@ use std::fmt::Write; -use anstream::eprint; use anyhow::Result; use owo_colors::OwoColorize; use tracing::debug; @@ -32,7 +31,7 @@ use uv_types::{BuildIsolation, HashStrategy}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; use crate::commands::pip::operations::Modifications; use crate::commands::pip::{operations, resolution_markers, resolution_tags}; -use crate::commands::{ExitStatus, SharedState}; +use crate::commands::{diagnostics, ExitStatus, SharedState}; use crate::printer::Printer; /// Install a set of locked requirements into the current Python environment. @@ -345,8 +344,15 @@ pub(crate) async fn pip_sync( { Ok(resolution) => Resolution::from(resolution), Err(operations::Error::Resolve(uv_resolver::ResolveError::NoSolution(err))) => { - let report = miette::Report::msg(format!("{err}")).context(err.header()); - eprint!("{report:?}"); + diagnostics::no_solution(&err); + return Ok(ExitStatus::Failure); + } + Err(operations::Error::Resolve(uv_resolver::ResolveError::FetchAndBuild(dist, err))) => { + diagnostics::fetch_and_build(dist, err); + return Ok(ExitStatus::Failure); + } + Err(operations::Error::Resolve(uv_resolver::ResolveError::Build(dist, err))) => { + diagnostics::build(dist, err); return Ok(ExitStatus::Failure); } Err(err) => return Err(err.into()), diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index a3baad6dccfdf..194cb8e265d57 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -43,7 +43,7 @@ use crate::commands::pip::operations::Modifications; use crate::commands::pip::resolution_environment; use crate::commands::project::{script_python_requirement, ProjectError}; use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter}; -use crate::commands::{pip, project, ExitStatus, SharedState}; +use crate::commands::{diagnostics, pip, project, ExitStatus, SharedState}; use crate::printer::Printer; use crate::settings::{ResolverInstallerSettings, ResolverInstallerSettingsRef}; @@ -573,26 +573,39 @@ pub(crate) async fn add( .await { Ok(()) => Ok(ExitStatus::Success), - Err(ProjectError::Operation(pip::operations::Error::Resolve( - uv_resolver::ResolveError::NoSolution(err), - ))) => { - let header = err.header(); - let report = miette::Report::new(WithHelp { header, cause: err, help: Some("If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing.") }); - anstream::eprint!("{report:?}"); - + Err(err) => { // Revert the changes to the `pyproject.toml`, if necessary. if modified { fs_err::write(root.join("pyproject.toml"), &existing)?; } - Ok(ExitStatus::Failure) - } - Err(err) => { - // Revert the changes to the `pyproject.toml`, if necessary. - if modified { - fs_err::write(root.join("pyproject.toml"), &existing)?; + match err { + ProjectError::Operation(pip::operations::Error::Resolve( + uv_resolver::ResolveError::NoSolution(err), + )) => { + diagnostics::no_solution_hint(err, format!("If you want to add the package regardless of the failed resolution, provide the `{}` flag to skip locking and syncing.", "--frozen".green())); + Ok(ExitStatus::Failure) + } + ProjectError::Operation(pip::operations::Error::Resolve( + uv_resolver::ResolveError::FetchAndBuild(dist, err), + )) => { + diagnostics::fetch_and_build(dist, err); + Ok(ExitStatus::Failure) + } + ProjectError::Operation(pip::operations::Error::Resolve( + uv_resolver::ResolveError::Build(dist, err), + )) => { + diagnostics::build(dist, err); + Ok(ExitStatus::Failure) + } + err => { + // Revert the changes to the `pyproject.toml`, if necessary. + if modified { + fs_err::write(root.join("pyproject.toml"), &existing)?; + } + Err(err.into()) + } } - Err(err.into()) } } } @@ -913,20 +926,3 @@ struct DependencyEdit<'a> { source: Option, edit: ArrayEdit, } - -/// Render a [`uv_resolver::NoSolutionError`] with a help message. -#[derive(Debug, miette::Diagnostic, thiserror::Error)] -#[error("{header}")] -#[diagnostic()] -struct WithHelp { - /// The header to render in the error message. - header: uv_resolver::NoSolutionHeader, - - /// The underlying error. - #[source] - cause: uv_resolver::NoSolutionError, - - /// The help message to display. - #[help] - help: Option<&'static str>, -} diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 562c3a28e4353..1875a5d9d75b4 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -19,7 +19,7 @@ use uv_workspace::{DiscoveryOptions, MemberDiscovery, VirtualProject, Workspace} use crate::commands::pip::loggers::DefaultResolveLogger; use crate::commands::project::lock::do_safe_lock; use crate::commands::project::{FoundInterpreter, ProjectError}; -use crate::commands::{pip, ExitStatus, OutputWriter}; +use crate::commands::{diagnostics, pip, ExitStatus, OutputWriter}; use crate::printer::Printer; use crate::settings::ResolverSettings; @@ -107,8 +107,19 @@ pub(crate) async fn export( Err(ProjectError::Operation(pip::operations::Error::Resolve( uv_resolver::ResolveError::NoSolution(err), ))) => { - let report = miette::Report::msg(format!("{err}")).context(err.header()); - anstream::eprint!("{report:?}"); + diagnostics::no_solution(&err); + return Ok(ExitStatus::Failure); + } + Err(ProjectError::Operation(pip::operations::Error::Resolve( + uv_resolver::ResolveError::FetchAndBuild(dist, err), + ))) => { + diagnostics::fetch_and_build(dist, err); + return Ok(ExitStatus::Failure); + } + Err(ProjectError::Operation(pip::operations::Error::Resolve( + uv_resolver::ResolveError::Build(dist, err), + ))) => { + diagnostics::build(dist, err); return Ok(ExitStatus::Failure); } Err(err) => return Err(err.into()), diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 7ae9b50b3bea3..1b69db1806851 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -36,7 +36,7 @@ use uv_workspace::{DiscoveryOptions, Workspace}; use crate::commands::pip::loggers::{DefaultResolveLogger, ResolveLogger, SummaryResolveLogger}; use crate::commands::project::{find_requires_python, FoundInterpreter, ProjectError, SharedState}; -use crate::commands::{pip, ExitStatus}; +use crate::commands::{diagnostics, pip, ExitStatus}; use crate::printer::Printer; use crate::settings::{ResolverSettings, ResolverSettingsRef}; @@ -122,10 +122,22 @@ pub(crate) async fn lock( Err(ProjectError::Operation(pip::operations::Error::Resolve( uv_resolver::ResolveError::NoSolution(err), ))) => { - let report = miette::Report::msg(format!("{err}")).context(err.header()); - eprint!("{report:?}"); + diagnostics::no_solution(&err); Ok(ExitStatus::Failure) } + Err(ProjectError::Operation(pip::operations::Error::Resolve( + uv_resolver::ResolveError::FetchAndBuild(dist, err), + ))) => { + diagnostics::fetch_and_build(dist, err); + Ok(ExitStatus::Failure) + } + Err(ProjectError::Operation(pip::operations::Error::Resolve( + uv_resolver::ResolveError::Build(dist, err), + ))) => { + diagnostics::build(dist, err); + Ok(ExitStatus::Failure) + } + Err(err) => Err(err.into()), } } diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 48f3c2fb72f24..d6b0dadc039cf 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -42,7 +42,7 @@ use crate::commands::project::{ WorkspacePython, }; use crate::commands::reporters::PythonDownloadReporter; -use crate::commands::{project, ExitStatus, SharedState}; +use crate::commands::{diagnostics, project, ExitStatus, SharedState}; use crate::printer::Printer; use crate::settings::ResolverInstallerSettings; @@ -202,7 +202,7 @@ pub(crate) async fn run( script_dir, script_sources, ) - .map_ok(uv_distribution::LoweredRequirement::into_inner) + .map_ok(LoweredRequirement::into_inner) }) .collect::>()?; let spec = RequirementsSpecification::from_requirements(requirements); @@ -234,9 +234,19 @@ pub(crate) async fn run( Err(ProjectError::Operation(operations::Error::Resolve( uv_resolver::ResolveError::NoSolution(err), ))) => { - let report = miette::Report::msg(format!("{err}")) - .context(err.header().with_context("script")); - eprint!("{report:?}"); + diagnostics::no_solution_context(&err, "script"); + return Ok(ExitStatus::Failure); + } + Err(ProjectError::Operation(operations::Error::Resolve( + uv_resolver::ResolveError::FetchAndBuild(dist, err), + ))) => { + diagnostics::fetch_and_build(dist, err); + return Ok(ExitStatus::Failure); + } + Err(ProjectError::Operation(operations::Error::Resolve( + uv_resolver::ResolveError::Build(dist, err), + ))) => { + diagnostics::build(dist, err); return Ok(ExitStatus::Failure); } Err(err) => return Err(err.into()), @@ -504,8 +514,19 @@ pub(crate) async fn run( Err(ProjectError::Operation(operations::Error::Resolve( uv_resolver::ResolveError::NoSolution(err), ))) => { - let report = miette::Report::msg(format!("{err}")).context(err.header()); - eprint!("{report:?}"); + diagnostics::no_solution(&err); + return Ok(ExitStatus::Failure); + } + Err(ProjectError::Operation(operations::Error::Resolve( + uv_resolver::ResolveError::FetchAndBuild(dist, err), + ))) => { + diagnostics::fetch_and_build(dist, err); + return Ok(ExitStatus::Failure); + } + Err(ProjectError::Operation(operations::Error::Resolve( + uv_resolver::ResolveError::Build(dist, err), + ))) => { + diagnostics::build(dist, err); return Ok(ExitStatus::Failure); } Err(err) => return Err(err.into()), @@ -669,9 +690,19 @@ pub(crate) async fn run( Err(ProjectError::Operation(operations::Error::Resolve( uv_resolver::ResolveError::NoSolution(err), ))) => { - let report = miette::Report::msg(format!("{err}")) - .context(err.header().with_context("`--with`")); - eprint!("{report:?}"); + diagnostics::no_solution_context(&err, "`--with`"); + return Ok(ExitStatus::Failure); + } + Err(ProjectError::Operation(operations::Error::Resolve( + uv_resolver::ResolveError::FetchAndBuild(dist, err), + ))) => { + diagnostics::fetch_and_build(dist, err); + return Ok(ExitStatus::Failure); + } + Err(ProjectError::Operation(operations::Error::Resolve( + uv_resolver::ResolveError::Build(dist, err), + ))) => { + diagnostics::build(dist, err); return Ok(ExitStatus::Failure); } Err(ProjectError::Operation(operations::Error::Named(err))) => { diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index a9ee9d8f119fd..3d072a181da58 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -1,8 +1,9 @@ use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; +use crate::commands::pip::operations; use crate::commands::pip::operations::Modifications; use crate::commands::project::lock::do_safe_lock; use crate::commands::project::{ProjectError, SharedState}; -use crate::commands::{pip, project, ExitStatus}; +use crate::commands::{diagnostics, pip, project, ExitStatus}; use crate::printer::Printer; use crate::settings::{InstallerSettingsRef, ResolverInstallerSettings}; use anyhow::{Context, Result}; @@ -118,11 +119,22 @@ pub(crate) async fn sync( .await { Ok(result) => result.into_lock(), - Err(ProjectError::Operation(pip::operations::Error::Resolve( + Err(ProjectError::Operation(operations::Error::Resolve( uv_resolver::ResolveError::NoSolution(err), ))) => { - let report = miette::Report::msg(format!("{err}")).context(err.header()); - anstream::eprint!("{report:?}"); + diagnostics::no_solution(&err); + return Ok(ExitStatus::Failure); + } + Err(ProjectError::Operation(operations::Error::Resolve( + uv_resolver::ResolveError::FetchAndBuild(dist, err), + ))) => { + diagnostics::fetch_and_build(dist, err); + return Ok(ExitStatus::Failure); + } + Err(ProjectError::Operation(operations::Error::Resolve( + uv_resolver::ResolveError::Build(dist, err), + ))) => { + diagnostics::build(dist, err); return Ok(ExitStatus::Failure); } Err(err) => return Err(err.into()), diff --git a/crates/uv-build-frontend/src/suggestions.json b/crates/uv/src/commands/suggestions.json similarity index 100% rename from crates/uv-build-frontend/src/suggestions.json rename to crates/uv/src/commands/suggestions.json diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index c9d8877c398db..94bf96628508a 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -36,7 +36,9 @@ use crate::commands::pip::operations; use crate::commands::project::{resolve_names, EnvironmentSpecification, ProjectError}; use crate::commands::reporters::PythonDownloadReporter; use crate::commands::tool::Target; -use crate::commands::{project::environment::CachedEnvironment, tool::common::matching_packages}; +use crate::commands::{ + diagnostics, project::environment::CachedEnvironment, tool::common::matching_packages, +}; use crate::commands::{ExitStatus, SharedState}; use crate::printer::Printer; use crate::settings::ResolverInstallerSettings; @@ -125,9 +127,7 @@ pub(crate) async fn run( Err(ProjectError::Operation(operations::Error::Resolve( uv_resolver::ResolveError::NoSolution(err), ))) => { - let report = - miette::Report::msg(format!("{err}")).context(err.header().with_context("tool")); - eprint!("{report:?}"); + diagnostics::no_solution_context(&err, "tool"); return Ok(ExitStatus::Failure); } Err(ProjectError::NamedRequirements(err)) => { diff --git a/crates/uv/tests/build.rs b/crates/uv/tests/build.rs index 2c04f920bf5cd..bfaf2ce35dacc 100644 --- a/crates/uv/tests/build.rs +++ b/crates/uv/tests/build.rs @@ -1398,7 +1398,7 @@ fn build_constraints() -> Result<()> { ----- stderr ----- Building source distribution... error: Failed to install requirements from `build-system.requires` (resolve) - Caused by: No solution found when resolving: setuptools>=42 + Caused by: No solution found when resolving: `setuptools>=42` Caused by: Because you require setuptools>=42 and setuptools==0.1.0, we can conclude that your requirements are unsatisfiable. "###); @@ -1586,7 +1586,7 @@ fn sha() -> Result<()> { ----- stderr ----- Building source distribution... error: Failed to install requirements from `build-system.requires` (resolve) - Caused by: No solution found when resolving: setuptools>=42 + Caused by: No solution found when resolving: `setuptools>=42` Caused by: In `--require-hashes` mode, all requirements must be pinned upfront with `==`, but found: `setuptools` "###); diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index d41089f374e2a..bee02f8d6f849 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -11777,14 +11777,14 @@ fn incompatible_build_constraint() -> Result<()> { .arg("--build-constraint") .arg("build_constraints.txt"), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- ----- stderr ----- - error: Failed to download and build `requests==1.2.0` - Caused by: Failed to install requirements from `setup.py` build (resolve) - Caused by: No solution found when resolving: setuptools>=40.8.0 - Caused by: Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that your requirements are unsatisfiable. + × Failed to download and build `requests==1.2.0` + ├─▶ Failed to install requirements from `setup.py` build (resolve) + ├─▶ No solution found when resolving: `setuptools>=40.8.0` + ╰─▶ Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that your requirements are unsatisfiable. "### ); diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 6a0cf855e0fa4..fe45a55641068 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -6754,14 +6754,14 @@ fn incompatible_build_constraint() -> Result<()> { .arg("--build-constraint") .arg("build_constraints.txt"), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- ----- stderr ----- - error: Failed to download and build `requests==1.2.0` - Caused by: Failed to install requirements from `setup.py` build (resolve) - Caused by: No solution found when resolving: setuptools>=40.8.0 - Caused by: Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that your requirements are unsatisfiable. + × Failed to download and build `requests==1.2.0` + ├─▶ Failed to install requirements from `setup.py` build (resolve) + ├─▶ No solution found when resolving: `setuptools>=40.8.0` + ╰─▶ Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that your requirements are unsatisfiable. "### ); @@ -7080,3 +7080,40 @@ fn missing_top_level() { "### ); } + +/// Show a dedicated error when the user attempts to install `sklearn`. +#[test] +fn sklearn() { + let context = TestContext::new("3.12"); + + uv_snapshot!(context.pip_install().arg("sklearn"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × Failed to download and build `sklearn==0.0.post12` + ╰─▶ Build backend failed to determine extra requires with `build_wheel()` with exit status: 1 + --- stdout: + + --- stderr: + The 'sklearn' PyPI package is deprecated, use 'scikit-learn' + rather than 'sklearn' for pip commands. + + Here is how to fix this error in the main use cases: + - use 'pip install scikit-learn' rather than 'pip install sklearn' + - replace 'sklearn' by 'scikit-learn' in your pip requirements files + (requirements.txt, setup.py, setup.cfg, Pipfile, etc ...) + - if the 'sklearn' package is used by one of your dependencies, + it would be great if you take some time to track which package uses + 'sklearn' instead of 'scikit-learn' and report it to their issue tracker + - as a last resort, set the environment variable + SKLEARN_ALLOW_DEPRECATED_SKLEARN_PACKAGE_INSTALL=True to avoid this error + + More information is available at + https://github.com/scikit-learn/sklearn-pypi-package + --- + help: `sklearn` is often confused for `scikit-learn` Did you mean to install `scikit-learn` instead? + "### + ); +} diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index 5f9bad31667ee..fa0c02d048b69 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -3802,18 +3802,18 @@ fn require_hashes_source_url() -> Result<()> { .arg("--reinstall") .arg("--require-hashes"), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- ----- stderr ----- - error: Failed to download and build `source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz` - Caused by: Hash mismatch for `source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz` + × Failed to download and build `source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz` + ╰─▶ Hash mismatch for `source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz` - Expected: - sha256:a7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a + Expected: + sha256:a7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a - Computed: - sha256:1f83ed7498336c7f2ab9b002cf22583d91115ebc624053dc4eb3a45694490106 + Computed: + sha256:1f83ed7498336c7f2ab9b002cf22583d91115ebc624053dc4eb3a45694490106 "### ); @@ -3833,18 +3833,18 @@ fn require_hashes_source_url_mismatch() -> Result<()> { .arg("requirements.txt") .arg("--require-hashes"), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- ----- stderr ----- - error: Failed to download and build `source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz` - Caused by: Hash mismatch for `source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz` + × Failed to download and build `source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz` + ╰─▶ Hash mismatch for `source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz` - Expected: - sha256:a7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a + Expected: + sha256:a7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a - Computed: - sha256:1f83ed7498336c7f2ab9b002cf22583d91115ebc624053dc4eb3a45694490106 + Computed: + sha256:1f83ed7498336c7f2ab9b002cf22583d91115ebc624053dc4eb3a45694490106 "### ); @@ -3992,12 +3992,12 @@ fn require_hashes_git() -> Result<()> { .arg("requirements.txt") .arg("--require-hashes"), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- ----- stderr ----- - error: Failed to download and build `anyio @ git+https://github.com/agronholm/anyio@4a23745badf5bf5ef7928f1e346e9986bd696d82` - Caused by: Hash-checking is not supported for Git repositories: `anyio @ git+https://github.com/agronholm/anyio@4a23745badf5bf5ef7928f1e346e9986bd696d82` + × Failed to download and build `anyio @ git+https://github.com/agronholm/anyio@4a23745badf5bf5ef7928f1e346e9986bd696d82` + ╰─▶ Hash-checking is not supported for Git repositories: `anyio @ git+https://github.com/agronholm/anyio@4a23745badf5bf5ef7928f1e346e9986bd696d82` "### ); @@ -4022,12 +4022,12 @@ fn require_hashes_source_tree() -> Result<()> { .arg("requirements.txt") .arg("--require-hashes"), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- ----- stderr ----- - error: Failed to build `black @ file://[WORKSPACE]/scripts/packages/black_editable` - Caused by: Hash-checking is not supported for local directories: `black @ file://[WORKSPACE]/scripts/packages/black_editable` + × Failed to build `black @ file://[WORKSPACE]/scripts/packages/black_editable` + ╰─▶ Hash-checking is not supported for local directories: `black @ file://[WORKSPACE]/scripts/packages/black_editable` "### ); @@ -4229,18 +4229,18 @@ fn require_hashes_source_path_mismatch() -> Result<()> { .arg("requirements.txt") .arg("--require-hashes"), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- ----- stderr ----- - error: Failed to build `tqdm @ file://[WORKSPACE]/scripts/links/tqdm-999.0.0.tar.gz` - Caused by: Hash mismatch for `tqdm @ file://[WORKSPACE]/scripts/links/tqdm-999.0.0.tar.gz` + × Failed to build `tqdm @ file://[WORKSPACE]/scripts/links/tqdm-999.0.0.tar.gz` + ╰─▶ Hash mismatch for `tqdm @ file://[WORKSPACE]/scripts/links/tqdm-999.0.0.tar.gz` - Expected: - sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + Expected: + sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f - Computed: - sha256:89fa05cffa7f457658373b85de302d24d0c205ceda2819a8739e324b75e9430b + Computed: + sha256:89fa05cffa7f457658373b85de302d24d0c205ceda2819a8739e324b75e9430b "### ); @@ -5500,7 +5500,7 @@ fn incompatible_build_constraint() -> Result<()> { error: Failed to prepare distributions Caused by: Failed to fetch wheel: requests==1.2.0 Caused by: Failed to install requirements from `setup.py` build (resolve) - Caused by: No solution found when resolving: setuptools>=40.8.0 + Caused by: No solution found when resolving: `setuptools>=40.8.0` Caused by: Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that your requirements are unsatisfiable. "### );