diff --git a/Cargo.lock b/Cargo.lock index 6f84be9e7..6965c5367 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4585,7 +4585,6 @@ name = "uv-build-frontend" version = "0.0.1" dependencies = [ "anstream", - "anyhow", "fs-err 3.0.0", "indoc", "insta", @@ -4834,6 +4833,7 @@ dependencies = [ "futures", "itertools 0.13.0", "rustc-hash", + "thiserror 2.0.3", "tokio", "tracing", "uv-build-backend", @@ -4847,6 +4847,7 @@ dependencies = [ "uv-git", "uv-install-wheel", "uv-installer", + "uv-platform-tags", "uv-pypi-types", "uv-python", "uv-resolver", diff --git a/crates/uv-build-frontend/Cargo.toml b/crates/uv-build-frontend/Cargo.toml index a0e8fd89a..29e9dff0e 100644 --- a/crates/uv-build-frontend/Cargo.toml +++ b/crates/uv-build-frontend/Cargo.toml @@ -30,7 +30,6 @@ uv-types = { workspace = true } uv-virtualenv = { workspace = true } anstream = { workspace = true } -anyhow = { workspace = true } fs-err = { workspace = true } indoc = { workspace = true } itertools = { workspace = true } diff --git a/crates/uv-build-frontend/src/error.rs b/crates/uv-build-frontend/src/error.rs index 10fdb12b5..81fda9d87 100644 --- a/crates/uv-build-frontend/src/error.rs +++ b/crates/uv-build-frontend/src/error.rs @@ -5,16 +5,17 @@ use std::path::PathBuf; use std::process::ExitStatus; use std::sync::LazyLock; +use crate::PythonRunnerOutput; use owo_colors::OwoColorize; use regex::Regex; use thiserror::Error; use tracing::error; use uv_configuration::BuildOutput; +use uv_distribution_types::IsBuildBackendError; use uv_fs::Simplified; use uv_pep440::Version; use uv_pep508::PackageName; - -use crate::PythonRunnerOutput; +use uv_types::AnyErrorBuild; /// 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(|| { @@ -68,19 +69,47 @@ pub enum Error { #[error("Editable installs with setup.py legacy builds are unsupported, please specify a build backend in pyproject.toml")] EditableSetupPy, #[error("Failed to resolve requirements from {0}")] - RequirementsResolve(&'static str, #[source] anyhow::Error), + RequirementsResolve(&'static str, #[source] AnyErrorBuild), #[error("Failed to install requirements from {0}")] - RequirementsInstall(&'static str, #[source] anyhow::Error), + RequirementsInstall(&'static str, #[source] AnyErrorBuild), #[error("Failed to create temporary virtualenv")] Virtualenv(#[from] uv_virtualenv::Error), + // Build backend errors #[error("Failed to run `{0}`")] CommandFailed(PathBuf, #[source] io::Error), - #[error(transparent)] + #[error("The build backend returned an error")] BuildBackend(#[from] BuildBackendError), - #[error(transparent)] + #[error("The build backend returned an error")] MissingHeader(#[from] MissingHeaderError), #[error("Failed to build PATH for build script")] BuildScriptPath(#[source] env::JoinPathsError), + // For the convenience of typing `setup_build` properly. + #[error("Building source distributions for {0} is disabled")] + NoSourceDistBuild(PackageName), + #[error("Building source distributions is disabled")] + NoSourceDistBuilds, +} + +impl IsBuildBackendError for Error { + fn is_build_backend_error(&self) -> bool { + match self { + Self::Io(_) + | Self::Lowering(_) + | Self::InvalidSourceDist(_) + | Self::InvalidPyprojectTomlSyntax(_) + | Self::InvalidPyprojectTomlSchema(_) + | Self::EditableSetupPy + | Self::RequirementsResolve(_, _) + | Self::RequirementsInstall(_, _) + | Self::Virtualenv(_) + | Self::NoSourceDistBuild(_) + | Self::NoSourceDistBuilds => false, + Self::CommandFailed(_, _) + | Self::BuildBackend(_) + | Self::MissingHeader(_) + | Self::BuildScriptPath(_) => true, + } + } } #[derive(Debug)] @@ -247,6 +276,13 @@ impl Display for BuildBackendError { writeln!(f)?; } + write!( + f, + "\n{}{} This usually indicates a problem with the package or the build environment.", + "hint".bold().cyan(), + ":".bold() + )?; + Ok(()) } } @@ -416,7 +452,10 @@ mod test { assert!(matches!(err, Error::MissingHeader { .. })); // Unix uses exit status, Windows uses exit code. - let formatted = err.to_string().replace("exit status: ", "exit code: "); + let formatted = std::error::Error::source(&err) + .unwrap() + .to_string() + .replace("exit status: ", "exit code: "); let formatted = anstream::adapter::strip_str(&formatted); insta::assert_snapshot!(formatted, @r###" Failed building wheel through setup.py (exit code: 0) @@ -471,7 +510,10 @@ mod test { ); assert!(matches!(err, Error::MissingHeader { .. })); // Unix uses exit status, Windows uses exit code. - let formatted = err.to_string().replace("exit status: ", "exit code: "); + let formatted = std::error::Error::source(&err) + .unwrap() + .to_string() + .replace("exit status: ", "exit code: "); let formatted = anstream::adapter::strip_str(&formatted); insta::assert_snapshot!(formatted, @r###" Failed building wheel through setup.py (exit code: 0) @@ -516,7 +558,10 @@ mod test { ); assert!(matches!(err, Error::MissingHeader { .. })); // Unix uses exit status, Windows uses exit code. - let formatted = err.to_string().replace("exit status: ", "exit code: "); + let formatted = std::error::Error::source(&err) + .unwrap() + .to_string() + .replace("exit status: ", "exit code: "); let formatted = anstream::adapter::strip_str(&formatted); insta::assert_snapshot!(formatted, @r###" Failed building wheel through setup.py (exit code: 0) @@ -559,7 +604,10 @@ mod test { ); assert!(matches!(err, Error::MissingHeader { .. })); // Unix uses exit status, Windows uses exit code. - let formatted = err.to_string().replace("exit status: ", "exit code: "); + let formatted = std::error::Error::source(&err) + .unwrap() + .to_string() + .replace("exit status: ", "exit code: "); let formatted = anstream::adapter::strip_str(&formatted); insta::assert_snapshot!(formatted, @r###" Failed building wheel through setup.py (exit code: 0) diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index 05b1b2cfc..75ea2f638 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -7,7 +7,6 @@ mod error; use fs_err as fs; use indoc::formatdoc; use itertools::Itertools; -use owo_colors::OwoColorize; use rustc_hash::FxHashMap; use serde::de::{value, IntoDeserializer, SeqAccess, Visitor}; use serde::{de, Deserialize, Deserializer}; @@ -36,7 +35,7 @@ use uv_pep508::PackageName; use uv_pypi_types::{Requirement, VerbatimParsedUrl}; use uv_python::{Interpreter, PythonEnvironment}; use uv_static::EnvVars; -use uv_types::{BuildContext, BuildIsolation, SourceBuildTrait}; +use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, SourceBuildTrait}; pub use crate::error::{Error, MissingHeaderCause}; @@ -325,7 +324,7 @@ impl SourceBuild { build_context .install(&resolved_requirements, &venv) .await - .map_err(|err| Error::RequirementsInstall("`build-system.requires`", err))?; + .map_err(|err| Error::RequirementsInstall("`build-system.requires`", err.into()))?; } else { debug!("Proceeding without build isolation"); } @@ -423,7 +422,9 @@ impl SourceBuild { let resolved_requirements = build_context .resolve(&default_backend.requirements) .await - .map_err(|err| Error::RequirementsResolve("`setup.py` build", err))?; + .map_err(|err| { + Error::RequirementsResolve("`setup.py` build", err.into()) + })?; *resolution = Some(resolved_requirements.clone()); resolved_requirements } @@ -431,7 +432,9 @@ impl SourceBuild { build_context .resolve(&pep517_backend.requirements) .await - .map_err(|err| Error::RequirementsResolve("`build-system.requires`", err))? + .map_err(|err| { + Error::RequirementsResolve("`build-system.requires`", err.into()) + })? }, ) } @@ -622,8 +625,8 @@ impl SourceBuild { if !output.status.success() { return Err(Error::from_command_output( format!( - "Build backend failed to determine metadata through `{}`", - format!("prepare_metadata_for_build_{}", self.build_kind).green() + "Call to `{}.prepare_metadata_for_build_{}` failed", + self.pep517_backend.backend, self.build_kind ), &output, self.level, @@ -745,9 +748,8 @@ impl SourceBuild { if !output.status.success() { return Err(Error::from_command_output( format!( - "Build backend failed to build {} through `{}`", - self.build_kind, - format!("build_{}", self.build_kind).green(), + "Call to `{}.build_{}` failed", + pep517_backend.backend, self.build_kind ), &output, self.level, @@ -761,8 +763,8 @@ impl SourceBuild { if !output_dir.join(&distribution_filename).is_file() { return Err(Error::from_command_output( format!( - "Build backend failed to produce {} through `{}`: `{distribution_filename}` not found", - self.build_kind, format!("build_{}", self.build_kind).green(), + "Call to `{}.build_{}` failed", + pep517_backend.backend, self.build_kind ), &output, self.level, @@ -776,11 +778,11 @@ impl SourceBuild { } impl SourceBuildTrait for SourceBuild { - async fn metadata(&mut self) -> anyhow::Result> { + async fn metadata(&mut self) -> Result, AnyErrorBuild> { Ok(self.get_metadata_without_build().await?) } - async fn wheel<'a>(&'a self, wheel_dir: &'a Path) -> anyhow::Result { + async fn wheel<'a>(&'a self, wheel_dir: &'a Path) -> Result { Ok(self.build(wheel_dir).await?) } } @@ -858,8 +860,8 @@ async fn create_pep517_build_environment( if !output.status.success() { return Err(Error::from_command_output( format!( - "Build backend failed to determine requirements with `{}`", - format!("build_{build_kind}()").green() + "Call to `{}.build_{}` failed", + pep517_backend.backend, build_kind ), &output, level, @@ -869,37 +871,27 @@ async fn create_pep517_build_environment( )); } - // Read the requirements from the output file. - let contents = fs_err::read(&outfile).map_err(|err| { - Error::from_command_output( - format!( - "Build backend failed to read requirements from `{}`: {err}", - format!("get_requires_for_build_{build_kind}").green(), - ), - &output, - level, - package_name, - package_version, - version_id, - ) - })?; - - // Deserialize the requirements from the output file. - let extra_requires: Vec> = - serde_json::from_slice::>>(&contents) - .map_err(|err| { - Error::from_command_output( - format!( - "Build backend failed to return requirements from `{}`: {err}", - format!("get_requires_for_build_{build_kind}").green(), - ), - &output, - level, - package_name, - package_version, - version_id, - ) - })?; + // Read and deserialize the requirements from the output file. + let read_requires_result = fs_err::read(&outfile) + .map_err(|err| err.to_string()) + .and_then(|contents| serde_json::from_slice(&contents).map_err(|err| err.to_string())); + let extra_requires: Vec> = match read_requires_result + { + Ok(extra_requires) => extra_requires, + Err(err) => { + return Err(Error::from_command_output( + format!( + "Call to `{}.get_requires_for_build_{}` failed: {}", + pep517_backend.backend, build_kind, err + ), + &output, + level, + package_name, + package_version, + version_id, + )) + } + }; // If necessary, lower the requirements. let extra_requires = match source_strategy { @@ -937,15 +929,16 @@ async fn create_pep517_build_environment( .cloned() .chain(extra_requires) .collect(); - let resolution = build_context - .resolve(&requirements) - .await - .map_err(|err| Error::RequirementsResolve("`build-system.requires`", err))?; + let resolution = build_context.resolve(&requirements).await.map_err(|err| { + Error::RequirementsResolve("`build-system.requires`", AnyErrorBuild::from(err)) + })?; build_context .install(&resolution, venv) .await - .map_err(|err| Error::RequirementsInstall("`build-system.requires`", err))?; + .map_err(|err| { + Error::RequirementsInstall("`build-system.requires`", AnyErrorBuild::from(err)) + })?; } Ok(()) diff --git a/crates/uv-dispatch/Cargo.toml b/crates/uv-dispatch/Cargo.toml index 3750ab24a..07b0e1ab2 100644 --- a/crates/uv-dispatch/Cargo.toml +++ b/crates/uv-dispatch/Cargo.toml @@ -28,6 +28,7 @@ uv-distribution-types = { workspace = true } uv-git = { workspace = true } uv-install-wheel = { workspace = true } uv-installer = { workspace = true } +uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true } uv-resolver = { workspace = true } @@ -38,5 +39,6 @@ anyhow = { workspace = true } futures = { workspace = true } itertools = { workspace = true } rustc-hash = { workspace = true } +thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 862030250..5fe3a7fec 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -5,10 +5,11 @@ use std::ffi::{OsStr, OsString}; use std::path::Path; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result}; use futures::FutureExt; use itertools::Itertools; use rustc_hash::FxHashMap; +use thiserror::Error; use tracing::{debug, instrument, trace}; use uv_build_backend::check_direct_build; use uv_build_frontend::{SourceBuild, SourceBuildContext}; @@ -22,8 +23,8 @@ use uv_configuration::{BuildOutput, Concurrency}; use uv_distribution::DistributionDatabase; use uv_distribution_filename::DistFilename; use uv_distribution_types::{ - CachedDist, DependencyMetadata, IndexCapabilities, IndexLocations, Name, Resolution, - SourceDist, VersionOrUrlRef, + CachedDist, DependencyMetadata, IndexCapabilities, IndexLocations, IsBuildBackendError, Name, + Resolution, SourceDist, VersionOrUrlRef, }; use uv_git::GitResolver; use uv_installer::{Installer, Plan, Planner, Preparer, SitePackages}; @@ -33,7 +34,43 @@ use uv_resolver::{ ExcludeNewer, FlatIndex, Flexibility, InMemoryIndex, Manifest, OptionsBuilder, PythonRequirement, Resolver, ResolverEnvironment, }; -use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight}; +use uv_types::{ + AnyErrorBuild, BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight, +}; + +#[derive(Debug, Error)] +pub enum BuildDispatchError { + #[error(transparent)] + BuildFrontend(#[from] AnyErrorBuild), + + #[error(transparent)] + Tags(#[from] uv_platform_tags::TagsError), + + #[error(transparent)] + Resolve(#[from] uv_resolver::ResolveError), + + #[error(transparent)] + Join(#[from] tokio::task::JoinError), + + #[error(transparent)] + Anyhow(#[from] anyhow::Error), + + #[error(transparent)] + Prepare(#[from] uv_installer::PrepareError), +} + +impl IsBuildBackendError for BuildDispatchError { + fn is_build_backend_error(&self) -> bool { + match self { + BuildDispatchError::Tags(_) + | BuildDispatchError::Resolve(_) + | BuildDispatchError::Join(_) + | BuildDispatchError::Anyhow(_) + | BuildDispatchError::Prepare(_) => false, + BuildDispatchError::BuildFrontend(err) => err.is_build_backend_error(), + } + } +} /// The main implementation of [`BuildContext`], used by the CLI, see [`BuildContext`] /// documentation. @@ -124,6 +161,7 @@ impl<'a> BuildDispatch<'a> { } } +#[allow(refining_impl_trait)] impl<'a> BuildContext for BuildDispatch<'a> { type SourceDistBuilder = SourceBuild; @@ -167,7 +205,10 @@ impl<'a> BuildContext for BuildDispatch<'a> { self.index_locations } - async fn resolve<'data>(&'data self, requirements: &'data [Requirement]) -> Result { + async fn resolve<'data>( + &'data self, + requirements: &'data [Requirement], + ) -> Result { let python_requirement = PythonRequirement::from_interpreter(self.interpreter); let marker_env = self.interpreter.resolver_marker_environment(); let tags = self.interpreter.tags()?; @@ -215,7 +256,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { &'data self, resolution: &'data Resolution, venv: &'data PythonEnvironment, - ) -> Result> { + ) -> Result, BuildDispatchError> { debug!( "Installing in {} in {}", resolution @@ -325,7 +366,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { sources: SourceStrategy, build_kind: BuildKind, build_output: BuildOutput, - ) -> Result { + ) -> Result { let dist_name = dist.map(uv_distribution_types::Name::name); let dist_version = dist .map(uv_distribution_types::DistributionMetadata::version_or_url) @@ -342,13 +383,12 @@ impl<'a> BuildContext for BuildDispatch<'a> { // We always allow editable builds && !matches!(build_kind, BuildKind::Editable) { - if let Some(dist) = dist { - return Err(anyhow!( - "Building source distributions for {} is disabled", - dist.name() - )); - } - return Err(anyhow!("Building source distributions is disabled")); + let err = if let Some(dist) = dist { + uv_build_frontend::Error::NoSourceDistBuild(dist.name().clone()) + } else { + uv_build_frontend::Error::NoSourceDistBuilds + }; + return Err(err); } let builder = SourceBuild::setup( @@ -382,7 +422,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { output_dir: &'data Path, build_kind: BuildKind, version_id: Option<&'data str>, - ) -> Result> { + ) -> Result, BuildDispatchError> { // Direct builds are a preview feature with the uv build backend. if self.preview.is_disabled() { trace!("Preview is disabled, not checking for direct build"); diff --git a/crates/uv-distribution-types/src/dist_error.rs b/crates/uv-distribution-types/src/dist_error.rs index e26462a72..9bfcf8e97 100644 --- a/crates/uv-distribution-types/src/dist_error.rs +++ b/crates/uv-distribution-types/src/dist_error.rs @@ -1,13 +1,19 @@ -use crate::{DistRef, Edge, Name, Node, Resolution, ResolvedDist}; +use crate::{BuiltDist, Dist, DistRef, Edge, Name, Node, Resolution, ResolvedDist, SourceDist}; use petgraph::prelude::EdgeRef; use petgraph::Direction; use rustc_hash::FxHashSet; use std::collections::VecDeque; -use std::fmt::{Display, Formatter}; +use std::fmt::{Debug, Display, Formatter}; use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep440::Version; use version_ranges::Ranges; +/// Inspect whether an error type is a build error. +pub trait IsBuildBackendError: std::error::Error + Send + Sync + 'static { + /// Returns whether the build backend failed to build the package, so it's not a uv error. + fn is_build_backend_error(&self) -> bool; +} + /// The operation(s) that failed when reporting an error with a distribution. #[derive(Debug)] pub enum DistErrorKind { @@ -18,6 +24,29 @@ pub enum DistErrorKind { Read, } +impl DistErrorKind { + pub fn from_dist_and_err(dist: &Dist, err: &impl IsBuildBackendError) -> Self { + if err.is_build_backend_error() { + DistErrorKind::BuildBackend + } else { + match dist { + Dist::Built(BuiltDist::Path(_)) => DistErrorKind::Read, + Dist::Source(SourceDist::Path(_) | SourceDist::Directory(_)) => { + DistErrorKind::Build + } + Dist::Built(_) => DistErrorKind::Download, + Dist::Source(source_dist) => { + if source_dist.is_local() { + DistErrorKind::Build + } else { + DistErrorKind::DownloadAndBuild + } + } + } + } + } +} + impl Display for DistErrorKind { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index 530fe8b16..217682501 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -8,10 +8,12 @@ use zip::result::ZipError; use crate::metadata::MetadataError; use uv_client::WrappedReqwestError; use uv_distribution_filename::WheelFilenameError; +use uv_distribution_types::IsBuildBackendError; use uv_fs::Simplified; use uv_normalize::PackageName; use uv_pep440::{Version, VersionSpecifiers}; use uv_pypi_types::{HashAlgorithm, HashDigest, ParsedUrlError}; +use uv_types::AnyErrorBuild; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -52,7 +54,7 @@ pub enum Error { // Build error #[error(transparent)] - Build(anyhow::Error), + Build(AnyErrorBuild), #[error("Built wheel has an invalid filename")] WheelFilename(#[from] WheelFilenameError), #[error("Package metadata name `{metadata}` does not match given name `{given}`")] @@ -174,6 +176,15 @@ impl From for Error { } } +impl IsBuildBackendError for Error { + fn is_build_backend_error(&self) -> bool { + match self { + Self::Build(err) => err.is_build_backend_error(), + _ => false, + } + } +} + impl Error { /// Construct a hash mismatch error. pub fn hash_mismatch( diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 7e19eeac2..227b2f2fc 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -1879,7 +1879,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Some(&source.to_string()), ) .await - .map_err(Error::Build)? + .map_err(|err| Error::Build(err.into()))? { // In the uv build backend, the normalized filename and the disk filename are the same. name.to_string() @@ -1900,7 +1900,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { BuildOutput::Debug, ) .await - .map_err(Error::Build)? + .map_err(|err| Error::Build(err.into()))? .wheel(temp_dir.path()) .await .map_err(Error::Build)? @@ -1976,7 +1976,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { BuildOutput::Debug, ) .await - .map_err(Error::Build)?; + .map_err(|err| Error::Build(err.into()))?; // Build the metadata. let dist_info = builder.metadata().await.map_err(Error::Build)?; diff --git a/crates/uv-installer/src/preparer.rs b/crates/uv-installer/src/preparer.rs index 2093162ac..fac490570 100644 --- a/crates/uv-installer/src/preparer.rs +++ b/crates/uv-installer/src/preparer.rs @@ -226,20 +226,15 @@ pub enum Error { impl Error { /// Create an [`Error`] from a distribution error. - fn from_dist(dist: Dist, cause: uv_distribution::Error, resolution: &Resolution) -> Self { - let kind = match &dist { - Dist::Built(_) => DistErrorKind::Download, - Dist::Source(dist) => { - if dist.is_local() { - DistErrorKind::Build - } else { - DistErrorKind::DownloadAndBuild - } - } - }; + fn from_dist(dist: Dist, err: uv_distribution::Error, resolution: &Resolution) -> Self { let chain = DerivationChain::from_resolution(resolution, (&dist).into()).unwrap_or_default(); - Self::Dist(kind, Box::new(dist), chain, cause) + Self::Dist( + DistErrorKind::from_dist_and_err(&dist, &err), + Box::new(dist), + chain, + err, + ) } } diff --git a/crates/uv-requirements/src/lib.rs b/crates/uv-requirements/src/lib.rs index 3c2c15f6a..adb342e13 100644 --- a/crates/uv-requirements/src/lib.rs +++ b/crates/uv-requirements/src/lib.rs @@ -34,20 +34,12 @@ pub enum Error { impl Error { /// Create an [`Error`] from a distribution error. - pub(crate) fn from_dist(dist: Dist, cause: uv_distribution::Error) -> Self { - match dist { - Dist::Built(dist) => { - Self::Dist(DistErrorKind::Download, Box::new(Dist::Built(dist)), cause) - } - Dist::Source(dist) => { - let kind = if dist.is_local() { - DistErrorKind::Build - } else { - DistErrorKind::DownloadAndBuild - }; - Self::Dist(kind, Box::new(Dist::Source(dist)), cause) - } - } + pub(crate) fn from_dist(dist: Dist, err: uv_distribution::Error) -> Self { + Self::Dist( + DistErrorKind::from_dist_and_err(&dist, &err), + Box::new(dist), + err, + ) } } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 3657ad3e1..8560aaceb 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -1059,36 +1059,11 @@ impl ResolverState { // TODO(charlie): Add derivation chain for URL dependencies. In practice, this isn't // critical since we fetch URL dependencies _prior_ to invoking the resolver. - let chain = DerivationChain::default(); - let (kind, dist) = match &**dist { - Dist::Built(built_dist @ BuiltDist::Path(_)) => { - (DistErrorKind::Read, Dist::Built(built_dist.clone())) - } - Dist::Source(source_dist @ SourceDist::Path(_)) => { - (DistErrorKind::Build, Dist::Source(source_dist.clone())) - } - Dist::Source(source_dist @ SourceDist::Directory(_)) => { - (DistErrorKind::Build, Dist::Source(source_dist.clone())) - } - Dist::Built(built_dist) => { - (DistErrorKind::Download, Dist::Built(built_dist.clone())) - } - Dist::Source(source_dist) => { - if source_dist.is_local() { - (DistErrorKind::Build, Dist::Source(source_dist.clone())) - } else { - ( - DistErrorKind::DownloadAndBuild, - Dist::Source(source_dist.clone()), - ) - } - } - }; return Err(ResolveError::Dist( - kind, - Box::new(dist), - chain, - (*err).clone(), + DistErrorKind::from_dist_and_err(dist, &**err), + dist.clone(), + DerivationChain::default(), + err.clone(), )); } }; @@ -1446,35 +1421,11 @@ impl ResolverState { let chain = DerivationChainBuilder::from_state(id, version, pubgrub) .unwrap_or_default(); - let (kind, dist) = match &**dist { - Dist::Built(built_dist @ BuiltDist::Path(_)) => { - (DistErrorKind::Read, Dist::Built(built_dist.clone())) - } - Dist::Source(source_dist @ SourceDist::Path(_)) => { - (DistErrorKind::Build, Dist::Source(source_dist.clone())) - } - Dist::Source(source_dist @ SourceDist::Directory(_)) => { - (DistErrorKind::Build, Dist::Source(source_dist.clone())) - } - Dist::Built(built_dist) => { - (DistErrorKind::Download, Dist::Built(built_dist.clone())) - } - Dist::Source(source_dist) => { - if source_dist.is_local() { - (DistErrorKind::Build, Dist::Source(source_dist.clone())) - } else { - ( - DistErrorKind::DownloadAndBuild, - Dist::Source(source_dist.clone()), - ) - } - } - }; return Err(ResolveError::Dist( - kind, - Box::new(dist), + DistErrorKind::from_dist_and_err(dist, &**err), + dist.clone(), chain, - (*err).clone(), + err.clone(), )); } }; @@ -2353,22 +2304,6 @@ impl ForkState { // A dependency from the root package or requirements.txt. debug!("Adding direct dependency: {package}{version}"); - let name = package.name_no_root().unwrap(); - - // Catch cases where we pass a package once by name with extras and then once as - // URL for the specific distribution. - has_url = has_url - || dependencies - .iter() - .filter(|other_dep| *other_dep != dependency) - .filter(|other_dep| { - other_dep - .package - .name() - .is_some_and(|other_name| other_name == name) - }) - .any(|other_dep| other_dep.url.is_some()); - // Warn the user if a direct dependency lacks a lower bound in `--lowest` resolution. let missing_lower_bound = version .bounding_range() @@ -2383,6 +2318,7 @@ impl ForkState { "The direct dependency `{name}` is unpinned. \ Consider setting a lower bound when using `--resolution lowest` \ to avoid using outdated versions.", + name = package.name_no_root().unwrap(), ); } } diff --git a/crates/uv-types/src/traits.rs b/crates/uv-types/src/traits.rs index 68c17258a..fc88cab09 100644 --- a/crates/uv-types/src/traits.rs +++ b/crates/uv-types/src/traits.rs @@ -1,4 +1,6 @@ +use std::fmt::{Debug, Display, Formatter}; use std::future::Future; +use std::ops::Deref; use std::path::{Path, PathBuf}; use uv_distribution_filename::DistFilename; @@ -9,8 +11,8 @@ use uv_configuration::{ BuildKind, BuildOptions, BuildOutput, ConfigSettings, LowerBound, SourceStrategy, }; use uv_distribution_types::{ - CachedDist, DependencyMetadata, IndexCapabilities, IndexLocations, InstalledDist, Resolution, - SourceDist, + CachedDist, DependencyMetadata, IndexCapabilities, IndexLocations, InstalledDist, + IsBuildBackendError, Resolution, SourceDist, }; use uv_git::GitResolver; use uv_pep508::PackageName; @@ -47,7 +49,7 @@ use uv_python::{Interpreter, PythonEnvironment}; /// │ │ │ /// └─────────────┐ │ ┌──────────────┘ /// ┌──┴────┴────┴───┐ -/// │ uv-types │ +/// │ uv-types │ /// └────────────────┘ /// ``` /// @@ -94,7 +96,7 @@ pub trait BuildContext { fn resolve<'a>( &'a self, requirements: &'a [Requirement], - ) -> impl Future> + 'a; + ) -> impl Future> + 'a; /// Install the given set of package versions into the virtual environment. The environment must /// use the same base Python as [`BuildContext::interpreter`] @@ -102,7 +104,7 @@ pub trait BuildContext { &'a self, resolution: &'a Resolution, venv: &'a PythonEnvironment, - ) -> impl Future>> + 'a; + ) -> impl Future, impl IsBuildBackendError>> + 'a; /// Set up a source distribution build by installing the required dependencies. A wrapper for /// `uv_build::SourceBuild::setup`. @@ -121,7 +123,7 @@ pub trait BuildContext { sources: SourceStrategy, build_kind: BuildKind, build_output: BuildOutput, - ) -> impl Future> + 'a; + ) -> impl Future> + 'a; /// Build by calling directly into the uv build backend without PEP 517, if possible. /// @@ -136,7 +138,7 @@ pub trait BuildContext { output_dir: &'a Path, build_kind: BuildKind, version_id: Option<&'a str>, - ) -> impl Future>> + 'a; + ) -> impl Future, impl IsBuildBackendError>> + 'a; } /// A wrapper for `uv_build::SourceBuild` to avoid cyclical crate dependencies. @@ -150,7 +152,7 @@ pub trait SourceBuildTrait { /// /// Returns the metadata directory if we're having a PEP 517 build and the /// `prepare_metadata_for_build_wheel` hook exists - fn metadata(&mut self) -> impl Future>>; + fn metadata(&mut self) -> impl Future, AnyErrorBuild>>; /// A wrapper for `uv_build::SourceBuild::build`. /// @@ -159,7 +161,10 @@ pub trait SourceBuildTrait { /// Returns the filename of the built wheel inside the given `wheel_dir`. The filename is a /// string and not a `WheelFilename` because the on disk filename might not be normalized in the /// same way as uv would. - fn wheel<'a>(&'a self, wheel_dir: &'a Path) -> impl Future> + 'a; + fn wheel<'a>( + &'a self, + wheel_dir: &'a Path, + ) -> impl Future> + 'a; } /// A wrapper for [`uv_installer::SitePackages`] @@ -181,3 +186,61 @@ impl InstalledPackagesProvider for EmptyInstalledPackages { std::iter::empty() } } + +/// `anyhow::Error`-like wrapper type for [`BuildDispatch`] method return values, that also makes +/// `IsBuildBackendError` work as `thiserror` `#[source]`. +/// +/// The errors types have the same problem as [`BuildDispatch`] generally: The `uv-resolver`, +/// `uv-installer` and `uv-build-frontend` error types all reference each other: +/// Resolution and installation may need to build packages, while the build frontend needs to +/// resolve and install for the PEP 517 build environment. +/// +/// Usually, `anyhow::Error` is opaque error type of choice. In this case though, we error type +/// that we can inspect on whether it's a build backend error with [`IsBuildBackendError`], and +/// `anyhow::Error` does not allow attaching more traits. The next choice would be +/// `Box`, but `thiserror` +/// complains about the internal `AsDynError` not being implemented when being used as `#[source]`. +/// This struct is an otherwise transparent error wrapper that thiserror recognizes. +pub struct AnyErrorBuild(Box); + +impl Debug for AnyErrorBuild { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Debug::fmt(&self.0, f) + } +} + +impl Display for AnyErrorBuild { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl std::error::Error for AnyErrorBuild { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.0.source() + } + + #[allow(deprecated)] + fn description(&self) -> &str { + self.0.description() + } + + #[allow(deprecated)] + fn cause(&self) -> Option<&dyn std::error::Error> { + self.0.cause() + } +} + +impl From for AnyErrorBuild { + fn from(err: T) -> Self { + Self(Box::new(err)) + } +} + +impl Deref for AnyErrorBuild { + type Target = dyn IsBuildBackendError; + + fn deref(&self) -> &Self::Target { + &*self.0 + } +} diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 7684c3c71..f764617a9 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -42,7 +42,7 @@ use uv_python::{ use uv_requirements::RequirementsSource; use uv_resolver::{ExcludeNewer, FlatIndex, RequiresPython}; use uv_settings::PythonInstallMirrors; -use uv_types::{BuildContext, BuildIsolation, HashStrategy}; +use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, HashStrategy}; use uv_workspace::{DiscoveryOptions, Workspace, WorkspaceError}; #[derive(Debug, Error)] @@ -66,7 +66,7 @@ enum Error { #[error(transparent)] BuildBackend(#[from] uv_build_backend::Error), #[error(transparent)] - BuildDispatch(anyhow::Error), + BuildDispatch(AnyErrorBuild), #[error(transparent)] BuildFrontend(#[from] uv_build_frontend::Error), #[error("Failed to write message")] @@ -923,7 +923,7 @@ async fn build_sdist( build_output, ) .await - .map_err(Error::BuildDispatch)?; + .map_err(|err| Error::BuildDispatch(err.into()))?; let filename = builder.build(output_dir).await?; BuildMessage::Build { filename: DistFilename::SourceDistFilename( @@ -1020,7 +1020,7 @@ async fn build_wheel( build_output, ) .await - .map_err(Error::BuildDispatch)?; + .map_err(|err| Error::BuildDispatch(err.into()))?; let filename = builder.build(output_dir).await?; BuildMessage::Build { filename: DistFilename::WheelFilename( diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index f9f6b1698..e54f7bccf 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -26,7 +26,7 @@ use uv_python::{ use uv_resolver::{ExcludeNewer, FlatIndex}; use uv_settings::PythonInstallMirrors; use uv_shell::{shlex_posix, shlex_windows, Shell}; -use uv_types::{BuildContext, BuildIsolation, HashStrategy}; +use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, HashStrategy}; use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceError}; @@ -113,7 +113,7 @@ enum VenvError { #[error("Failed to install seed packages")] #[diagnostic(code(uv::venv::seed))] - Seed(#[source] anyhow::Error), + Seed(#[source] AnyErrorBuild), #[error("Failed to extract interpreter tags")] #[diagnostic(code(uv::venv::tags))] @@ -360,11 +360,11 @@ async fn venv_impl( let resolution = build_dispatch .resolve(&requirements) .await - .map_err(VenvError::Seed)?; + .map_err(|err| VenvError::Seed(err.into()))?; let installed = build_dispatch .install(&resolution, &venv) .await - .map_err(VenvError::Seed)?; + .map_err(|err| VenvError::Seed(err.into()))?; let changelog = Changelog::from_installed(installed); DefaultInstallLogger diff --git a/crates/uv/tests/it/build.rs b/crates/uv/tests/it/build.rs index 3765b14d7..76e3345e4 100644 --- a/crates/uv/tests/it/build.rs +++ b/crates/uv/tests/it/build.rs @@ -897,7 +897,9 @@ fn fail() -> Result<()> { from setuptools import setup IndentationError: unexpected indent × Failed to build `[TEMP_DIR]/project` - ╰─▶ Build backend failed to determine requirements with `build_sdist()` (exit status: 1) + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta.build_sdist` failed (exit status: 1) + hint: This usually indicates a problem with the package or the build environment. "###); Ok(()) @@ -1345,7 +1347,9 @@ fn build_all_with_failure() -> Result<()> { Successfully built dist/member_a-0.1.0.tar.gz Successfully built dist/member_a-0.1.0-py3-none-any.whl × Failed to build `member-b @ [TEMP_DIR]/project/packages/member_b` - ╰─▶ Build backend failed to determine requirements with `build_sdist()` (exit status: 1) + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta.build_sdist` failed (exit status: 1) + hint: This usually indicates a problem with the package or the build environment. Successfully built dist/project-0.1.0.tar.gz Successfully built dist/project-0.1.0-py3-none-any.whl "###); diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index cf1a1b7fa..442b15fc2 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -5559,7 +5559,8 @@ fn fail_to_add_revert_project() -> Result<()> { ----- stderr ----- Resolved 3 packages in [TIME] × Failed to build `child @ file://[TEMP_DIR]/child` - ╰─▶ Build backend failed to determine requirements with `build_wheel()` (exit status: 1) + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta.build_wheel` failed (exit status: 1) [stderr] Traceback (most recent call last): @@ -5574,6 +5575,7 @@ fn fail_to_add_revert_project() -> Result<()> { File "", line 1, in ZeroDivisionError: division by zero + hint: This usually indicates a problem with the package or the build environment. help: `child` was included because `parent` (v0.1.0) depends on `child` "###); @@ -5668,7 +5670,8 @@ fn fail_to_edit_revert_project() -> Result<()> { ----- stderr ----- Resolved 3 packages in [TIME] × Failed to build `child @ file://[TEMP_DIR]/child` - ╰─▶ Build backend failed to determine requirements with `build_wheel()` (exit status: 1) + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta.build_wheel` failed (exit status: 1) [stderr] Traceback (most recent call last): @@ -5683,6 +5686,7 @@ fn fail_to_edit_revert_project() -> Result<()> { File "", line 1, in ZeroDivisionError: division by zero + hint: This usually indicates a problem with the package or the build environment. help: `child` was included because `parent` (v0.1.0) depends on `child` "###); diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 0778c497a..6aebec582 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -19497,8 +19497,9 @@ fn lock_derivation_chain_prod() -> Result<()> { ----- stdout ----- ----- stderr ----- - × Failed to download and build `wsgiref==0.1.2` - ╰─▶ Build backend failed to determine requirements with `build_wheel()` (exit status: 1) + × Failed to build `wsgiref==0.1.2` + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1) [stderr] Traceback (most recent call last): @@ -19518,6 +19519,7 @@ fn lock_derivation_chain_prod() -> Result<()> { ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)? + hint: This usually indicates a problem with the package or the build environment. help: `wsgiref` (v0.1.2) was included because `project` (v0.1.0) depends on `wsgiref==0.1.2` "###); @@ -19555,8 +19557,9 @@ fn lock_derivation_chain_extra() -> Result<()> { ----- stdout ----- ----- stderr ----- - × Failed to download and build `wsgiref==0.1.2` - ╰─▶ Build backend failed to determine requirements with `build_wheel()` (exit status: 1) + × Failed to build `wsgiref==0.1.2` + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1) [stderr] Traceback (most recent call last): @@ -19576,6 +19579,7 @@ fn lock_derivation_chain_extra() -> Result<()> { ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)? + hint: This usually indicates a problem with the package or the build environment. help: `wsgiref` (v0.1.2) was included because `project[wsgi]` (v0.1.0) depends on `wsgiref>=0.1` "###); @@ -19615,8 +19619,9 @@ fn lock_derivation_chain_group() -> Result<()> { ----- stdout ----- ----- stderr ----- - × Failed to download and build `wsgiref==0.1.2` - ╰─▶ Build backend failed to determine requirements with `build_wheel()` (exit status: 1) + × Failed to build `wsgiref==0.1.2` + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1) [stderr] Traceback (most recent call last): @@ -19636,6 +19641,7 @@ fn lock_derivation_chain_group() -> Result<()> { ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)? + hint: This usually indicates a problem with the package or the build environment. help: `wsgiref` (v0.1.2) was included because `project:wsgi` (v0.1.0) depends on `wsgiref` "###); @@ -19686,8 +19692,9 @@ fn lock_derivation_chain_extended() -> Result<()> { ----- stdout ----- ----- stderr ----- - × Failed to download and build `wsgiref==0.1.2` - ╰─▶ Build backend failed to determine requirements with `build_wheel()` (exit status: 1) + × Failed to build `wsgiref==0.1.2` + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1) [stderr] Traceback (most recent call last): @@ -19707,6 +19714,7 @@ fn lock_derivation_chain_extended() -> Result<()> { ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)? + hint: This usually indicates a problem with the package or the build environment. help: `wsgiref` (v0.1.2) was included because `project` (v0.1.0) depends on `child` (v0.1.0) which depends on `wsgiref>=0.1, <0.2` "###); diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 864971b68..9d2ed61b3 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -13781,8 +13781,9 @@ fn compile_derivation_chain() -> Result<()> { ----- stdout ----- ----- stderr ----- - × Failed to download and build `wsgiref==0.1.2` - ╰─▶ Build backend failed to determine requirements with `build_wheel()` (exit status: 1) + × Failed to build `wsgiref==0.1.2` + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1) [stderr] Traceback (most recent call last): @@ -13802,6 +13803,7 @@ fn compile_derivation_chain() -> Result<()> { ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)? + hint: This usually indicates a problem with the package or the build environment. help: `wsgiref` (v0.1.2) was included because `child` (v0.1.0) depends on `wsgiref` "### ); diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 5469e4013..b122c8cd6 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -303,7 +303,8 @@ dependencies = ["flask==1.0.x"] ----- stderr ----- × Failed to build `project @ file://[TEMP_DIR]/path_dep` - ╰─▶ Build backend failed to determine requirements with `build_wheel()` (exit status: 1) + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1) [stdout] configuration error: `project.dependencies[0]` must be pep508 @@ -355,6 +356,8 @@ dependencies = ["flask==1.0.x"] raise ValueError(f"{error}/n{summary}") from None ValueError: invalid pyproject.toml config: `project.dependencies[0]`. configuration error: `project.dependencies[0]` must be pep508 + + hint: This usually indicates a problem with the package or the build environment. "### ); @@ -4059,13 +4062,16 @@ fn no_build_isolation() -> Result<()> { ----- stdout ----- ----- stderr ----- - × Failed to download and build `anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz` - ╰─▶ Build backend failed to determine metadata through `prepare_metadata_for_build_wheel` (exit status: 1) + × Failed to build `anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz` + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta.prepare_metadata_for_build_wheel` failed (exit status: 1) [stderr] Traceback (most recent call last): File "", line 8, in ModuleNotFoundError: No module named 'setuptools' + + hint: This usually indicates a problem with the package or the build environment. "### ); @@ -4127,13 +4133,16 @@ fn respect_no_build_isolation_env_var() -> Result<()> { ----- stdout ----- ----- stderr ----- - × Failed to download and build `anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz` - ╰─▶ Build backend failed to determine metadata through `prepare_metadata_for_build_wheel` (exit status: 1) + × Failed to build `anyio @ https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz` + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta.prepare_metadata_for_build_wheel` failed (exit status: 1) [stderr] Traceback (most recent call last): File "", line 8, in ModuleNotFoundError: No module named 'setuptools' + + hint: This usually indicates a problem with the package or the build environment. "### ); @@ -7124,13 +7133,16 @@ fn install_build_isolation_package() -> Result<()> { ----- stdout ----- ----- stderr ----- - × Failed to download and build `iniconfig @ https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz` - ╰─▶ Build backend failed to determine metadata through `prepare_metadata_for_build_wheel` (exit status: 1) + × Failed to build `iniconfig @ https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz` + ├─▶ The build backend returned an error + ╰─▶ Call to `hatchling.build.prepare_metadata_for_build_wheel` failed (exit status: 1) [stderr] Traceback (most recent call last): File "", line 8, in ModuleNotFoundError: No module named 'hatchling' + + hint: This usually indicates a problem with the package or the build environment. "### ); @@ -7383,8 +7395,9 @@ fn sklearn() { ----- stdout ----- ----- stderr ----- - × Failed to download and build `sklearn==0.0.post12` - ╰─▶ Build backend failed to determine requirements with `build_wheel()` (exit status: 1) + × Failed to build `sklearn==0.0.post12` + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1) [stderr] The 'sklearn' PyPI package is deprecated, use 'scikit-learn' @@ -7403,6 +7416,7 @@ fn sklearn() { More information is available at https://github.com/scikit-learn/sklearn-pypi-package + hint: This usually indicates a problem with the package or the build environment. help: `sklearn` is often confused for `scikit-learn` Did you mean to install `scikit-learn` instead? "### ); @@ -7439,8 +7453,9 @@ fn resolve_derivation_chain() -> Result<()> { ----- stdout ----- ----- stderr ----- - × Failed to download and build `wsgiref==0.1.2` - ╰─▶ Build backend failed to determine requirements with `build_wheel()` (exit status: 1) + × Failed to build `wsgiref==0.1.2` + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1) [stderr] Traceback (most recent call last): @@ -7460,6 +7475,7 @@ fn resolve_derivation_chain() -> Result<()> { ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)? + hint: This usually indicates a problem with the package or the build environment. help: `wsgiref` (v0.1.2) was included because `project` (v0.1.0) depends on `wsgiref` "### ); diff --git a/crates/uv/tests/it/pip_sync.rs b/crates/uv/tests/it/pip_sync.rs index fa0ac8a5f..9884ed116 100644 --- a/crates/uv/tests/it/pip_sync.rs +++ b/crates/uv/tests/it/pip_sync.rs @@ -4169,7 +4169,7 @@ fn require_hashes_wheel_path_mismatch() -> Result<()> { ----- stderr ----- Resolved 1 package in [TIME] - × Failed to download `tqdm @ file://[WORKSPACE]/scripts/links/tqdm-1000.0.0-py3-none-any.whl` + × Failed to read `tqdm @ file://[WORKSPACE]/scripts/links/tqdm-1000.0.0-py3-none-any.whl` ╰─▶ Hash mismatch for `tqdm @ file://[WORKSPACE]/scripts/links/tqdm-1000.0.0-py3-none-any.whl` Expected: diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 00ee61580..70dde1a12 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -761,14 +761,16 @@ fn sync_build_isolation_package() -> Result<()> { ----- stderr ----- Resolved 2 packages in [TIME] - × Failed to download and build `source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz` - ╰─▶ Build backend failed to build wheel through `build_wheel` (exit status: 1) + × Failed to build `source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz` + ├─▶ The build backend returned an error + ╰─▶ Call to `hatchling.build.build_wheel` failed (exit status: 1) [stderr] Traceback (most recent call last): File "", line 8, in ModuleNotFoundError: No module named 'hatchling' + hint: This usually indicates a problem with the package or the build environment. help: `source-distribution` was included because `project` (v0.1.0) depends on `source-distribution` "###); @@ -852,14 +854,16 @@ fn sync_build_isolation_extra() -> Result<()> { ----- stderr ----- Resolved [N] packages in [TIME] - × Failed to download and build `source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz` - ╰─▶ Build backend failed to build wheel through `build_wheel` (exit status: 1) + × Failed to build `source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz` + ├─▶ The build backend returned an error + ╰─▶ Call to `hatchling.build.build_wheel` failed (exit status: 1) [stderr] Traceback (most recent call last): File "", line 8, in ModuleNotFoundError: No module named 'hatchling' + hint: This usually indicates a problem with the package or the build environment. help: `source-distribution` was included because `project[compile]` (v0.1.0) depends on `source-distribution` "###); @@ -871,14 +875,16 @@ fn sync_build_isolation_extra() -> Result<()> { ----- stderr ----- Resolved [N] packages in [TIME] - × Failed to download and build `source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz` - ╰─▶ Build backend failed to build wheel through `build_wheel` (exit status: 1) + × Failed to build `source-distribution @ https://files.pythonhosted.org/packages/10/1f/57aa4cce1b1abf6b433106676e15f9fa2c92ed2bd4cf77c3b50a9e9ac773/source_distribution-0.0.1.tar.gz` + ├─▶ The build backend returned an error + ╰─▶ Call to `hatchling.build.build_wheel` failed (exit status: 1) [stderr] Traceback (most recent call last): File "", line 8, in ModuleNotFoundError: No module named 'hatchling' + hint: This usually indicates a problem with the package or the build environment. help: `source-distribution` was included because `project[compile]` (v0.1.0) depends on `source-distribution` "###); @@ -4953,8 +4959,9 @@ fn sync_derivation_chain() -> Result<()> { ----- stderr ----- Resolved 2 packages in [TIME] - × Failed to download and build `wsgiref==0.1.2` - ╰─▶ Build backend failed to determine requirements with `build_wheel()` (exit status: 1) + × Failed to build `wsgiref==0.1.2` + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1) [stderr] Traceback (most recent call last): @@ -4974,6 +4981,7 @@ fn sync_derivation_chain() -> Result<()> { ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)? + hint: This usually indicates a problem with the package or the build environment. help: `wsgiref` (v0.1.2) was included because `project` (v0.1.0) depends on `wsgiref` "###); @@ -5017,8 +5025,9 @@ fn sync_derivation_chain_extra() -> Result<()> { ----- stderr ----- Resolved 2 packages in [TIME] - × Failed to download and build `wsgiref==0.1.2` - ╰─▶ Build backend failed to determine requirements with `build_wheel()` (exit status: 1) + × Failed to build `wsgiref==0.1.2` + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1) [stderr] Traceback (most recent call last): @@ -5038,6 +5047,7 @@ fn sync_derivation_chain_extra() -> Result<()> { ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)? + hint: This usually indicates a problem with the package or the build environment. help: `wsgiref` (v0.1.2) was included because `project[wsgi]` (v0.1.0) depends on `wsgiref` "###); @@ -5083,8 +5093,9 @@ fn sync_derivation_chain_group() -> Result<()> { ----- stderr ----- Resolved 2 packages in [TIME] - × Failed to download and build `wsgiref==0.1.2` - ╰─▶ Build backend failed to determine requirements with `build_wheel()` (exit status: 1) + × Failed to build `wsgiref==0.1.2` + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1) [stderr] Traceback (most recent call last): @@ -5104,6 +5115,7 @@ fn sync_derivation_chain_group() -> Result<()> { ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)? + hint: This usually indicates a problem with the package or the build environment. help: `wsgiref` (v0.1.2) was included because `project:wsgi` (v0.1.0) depends on `wsgiref` "###); diff --git a/crates/uv/tests/it/tool_install.rs b/crates/uv/tests/it/tool_install.rs index 3323d8641..e951cafc2 100644 --- a/crates/uv/tests/it/tool_install.rs +++ b/crates/uv/tests/it/tool_install.rs @@ -1469,8 +1469,9 @@ fn tool_install_uninstallable() { ----- stderr ----- Resolved 1 package in [TIME] - × Failed to download and build `pyenv==0.0.1` - ╰─▶ Build backend failed to build wheel through `build_wheel` (exit status: 1) + × Failed to build `pyenv==0.0.1` + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1) [stdout] running bdist_wheel @@ -1486,6 +1487,9 @@ fn tool_install_uninstallable() { https://github.com/pyenv/pyenv#installation # + + + hint: This usually indicates a problem with the package or the build environment. "###); // Ensure the tool environment is not created. diff --git a/docs/reference/build_failures.md b/docs/reference/build_failures.md index b010e3142..a919cc161 100644 --- a/docs/reference/build_failures.md +++ b/docs/reference/build_failures.md @@ -12,22 +12,23 @@ unsupported version of Python: ```console $ uv pip install -p 3.13 'numpy<1.20' Resolved 1 package in 62ms - × Failed to download and build `numpy==1.19.5` - ├─▶ Build backend failed to determine requirements with `build_wheel()` (exit status: 1) + × Failed to build `numpy==1.19.5` + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel()` failed (exit status: 1) - │ [stderr] - │ Traceback (most recent call last): - │ File "", line 8, in - │ from setuptools.build_meta import __legacy__ as backend - │ File "/Users/example/.cache/uv/builds-v0/.tmp96A0WB/lib/python3.13/site-packages/setuptools/__init__.py", line 9, in - │ import distutils.core - │ ModuleNotFoundError: No module named 'distutils' + [stderr] + Traceback (most recent call last): + File "", line 8, in + from setuptools.build_meta import __legacy__ as backend + File "/home/konsti/.cache/uv/builds-v0/.tmpi4bgKb/lib/python3.13/site-packages/setuptools/__init__.py", line 9, in + import distutils.core + ModuleNotFoundError: No module named 'distutils' - ╰─▶ distutils was removed from the standard library in Python 3.12. Consider adding a constraint - (like `numpy >1.19.5`) to avoid building a version of numpy that depends on distutils. + hint: `distutils` was removed from the standard library in Python 3.12. Consider adding a constraint (like `numpy >1.19.5`) to avoid building a version of `numpy` that depends + on `distutils`. ``` -Notice that the error message is prefaced by "Build backend failed to determine requirements". +Notice that the error message is prefaced by "The build backend returned an error". The build failure includes the `[stderr]` (and `[stdout]`, if present) from the build backend that was used for the build. The error logs are not from uv itself. @@ -118,8 +119,9 @@ If the build error mentions a missing command, for example, `gcc`: ```hl_lines="17" -× Failed to download and build `pysha3==1.0.2` -╰─▶ Build backend failed to build wheel through `build_wheel` (exit status: 1) +× Failed to build `pysha3==1.0.2` +├─▶ The build backend returned an error +╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1) [stdout] running bdist_wheel @@ -130,8 +132,8 @@ If the build error mentions a missing command, for example, `gcc`: running build_ext building '_pysha3' extension creating build/temp.linux-x86_64-cpython-310/Modules/_sha3 - gcc -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -DPY_WITH_KECCAK=1 -I/root/.cache/uv/builds-v0/.tmpxAJdUa/include -I/usr/local/include/python3.10 -c Modules/_sha3/sha3module.c -o - build/temp.linux-x86_64-cpython-310/Modules/_sha3/sha3module.o + gcc -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -DPY_WITH_KECCAK=1 -I/root/.cache/uv/builds-v0/.tmp8V4iEk/include -I/usr/local/include/python3.10 -c + Modules/_sha3/sha3module.c -o build/temp.linux-x86_64-cpython-310/Modules/_sha3/sha3module.o [stderr] error: command 'gcc' failed: No such file or directory @@ -165,8 +167,9 @@ For example, installing `pygraphviz` requires Graphviz to be installed: ```hl_lines="18-19" -× Failed to download and build `pygraphviz==1.14` -╰─▶ Build backend failed to build wheel through `build_wheel` (exit status: 1) +× Failed to build `pygraphviz==1.14` +├─▶ The build backend returned an error +╰─▶ Call to `setuptools.build_meta.build_wheel` failed (exit status: 1) [stdout] running bdist_wheel @@ -215,8 +218,9 @@ dependency: ```hl_lines="7" -× Failed to download and build `chumpy==0.70` -╰─▶ Build backend failed to determine requirements with `build_wheel()` (exit status: 1) + × Failed to build `chumpy==0.70` + ├─▶ The build backend returned an error + ╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1) [stderr] Traceback (most recent call last): @@ -270,8 +274,9 @@ apache-beam<=2.49.0 ```hl_lines="1" -× Failed to download and build `apache-beam==2.0.0` -╰─▶ Build backend failed to determine requirements with `build_wheel()` (exit status: 1) +× Failed to build `apache-beam==2.0.0` +├─▶ The build backend returned an error +╰─▶ Call to `setuptools.build_meta:__legacy__.build_wheel` failed (exit status: 1) [stderr] ...