Show full derivation chain when encountering build failures (#9108)

## Summary

This PR adds context to our error messages to explain _why_ a given
package was included, if we fail to download or build it.

It's quite a large change, but it motivated some good refactors and
improvements along the way.

Closes https://github.com/astral-sh/uv/issues/8962.
This commit is contained in:
Charlie Marsh 2024-11-14 15:48:26 -05:00 committed by GitHub
parent a552f74308
commit fe477c3417
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1147 additions and 172 deletions

View File

@ -29,8 +29,8 @@ use uv_installer::{Installer, Plan, Planner, Preparer, SitePackages};
use uv_pypi_types::{Conflicts, Requirement};
use uv_python::{Interpreter, PythonEnvironment};
use uv_resolver::{
ExcludeNewer, FlatIndex, Flexibility, InMemoryIndex, Manifest, OptionsBuilder,
PythonRequirement, Resolver, ResolverEnvironment,
DerivationChainBuilder, ExcludeNewer, FlatIndex, Flexibility, InMemoryIndex, Manifest,
OptionsBuilder, PythonRequirement, Resolver, ResolverEnvironment,
};
use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
@ -278,7 +278,33 @@ impl<'a> BuildContext for BuildDispatch<'a> {
remote.iter().map(ToString::to_string).join(", ")
);
preparer.prepare(remote, self.in_flight).await?
preparer
.prepare(remote, self.in_flight)
.await
.map_err(|err| match err {
uv_installer::PrepareError::DownloadAndBuild(dist, chain, err) => {
debug_assert!(chain.is_empty());
let chain =
DerivationChainBuilder::from_resolution(resolution, (&*dist).into())
.unwrap_or_default();
uv_installer::PrepareError::DownloadAndBuild(dist, chain, err)
}
uv_installer::PrepareError::Download(dist, chain, err) => {
debug_assert!(chain.is_empty());
let chain =
DerivationChainBuilder::from_resolution(resolution, (&*dist).into())
.unwrap_or_default();
uv_installer::PrepareError::Download(dist, chain, err)
}
uv_installer::PrepareError::Build(dist, chain, err) => {
debug_assert!(chain.is_empty());
let chain =
DerivationChainBuilder::from_resolution(resolution, (&*dist).into())
.unwrap_or_default();
uv_installer::PrepareError::Build(dist, chain, err)
}
_ => err,
})?
};
// Remove any unnecessary packages.

View File

@ -0,0 +1,82 @@
use uv_normalize::PackageName;
use uv_pep440::Version;
/// A chain of derivation steps from the root package to the current package, to explain why a
/// package is included in the resolution.
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct DerivationChain(Vec<DerivationStep>);
impl FromIterator<DerivationStep> for DerivationChain {
fn from_iter<T: IntoIterator<Item = DerivationStep>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
impl DerivationChain {
/// Returns the length of the derivation chain.
pub fn len(&self) -> usize {
self.0.len()
}
/// Returns `true` if the derivation chain is empty.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Returns an iterator over the steps in the derivation chain.
pub fn iter(&self) -> std::slice::Iter<DerivationStep> {
self.0.iter()
}
}
impl std::fmt::Display for DerivationChain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (idx, step) in self.0.iter().enumerate() {
if idx > 0 {
write!(f, " -> ")?;
}
write!(f, "{}=={}", step.name, step.version)?;
}
Ok(())
}
}
impl<'chain> IntoIterator for &'chain DerivationChain {
type Item = &'chain DerivationStep;
type IntoIter = std::slice::Iter<'chain, DerivationStep>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl IntoIterator for DerivationChain {
type Item = DerivationStep;
type IntoIter = std::vec::IntoIter<DerivationStep>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
/// A step in a derivation chain.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DerivationStep {
/// The name of the package.
name: PackageName,
/// The version of the package.
version: Version,
}
impl DerivationStep {
/// Create a [`DerivationStep`] from a package name and version.
pub fn new(name: PackageName, version: Version) -> Self {
Self { name, version }
}
}
impl std::fmt::Display for DerivationStep {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}=={}", self.name, self.version)
}
}

View File

@ -21,7 +21,7 @@ pub enum FileConversionError {
}
/// Internal analog to [`uv_pypi_types::File`].
#[derive(Debug, Clone, Hash, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)]
#[rkyv(derive(Debug))]
pub struct File {
pub dist_info_metadata: bool,
@ -66,7 +66,7 @@ impl File {
}
/// While a registry file is generally a remote URL, it can also be a file if it comes from a directory flat indexes.
#[derive(Debug, Clone, Hash, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)]
#[rkyv(derive(Debug))]
pub enum FileLocation {
/// URL relative to the base URL.

View File

@ -52,6 +52,7 @@ pub use crate::any::*;
pub use crate::buildable::*;
pub use crate::cached::*;
pub use crate::dependency_metadata::*;
pub use crate::derivation::*;
pub use crate::diagnostic::*;
pub use crate::error::*;
pub use crate::file::*;
@ -74,6 +75,7 @@ mod any;
mod buildable;
mod cached;
mod dependency_metadata;
mod derivation;
mod diagnostic;
mod error;
mod file;
@ -166,14 +168,21 @@ impl std::fmt::Display for InstalledVersion<'_> {
/// Either a built distribution, a wheel, or a source distribution that exists at some location.
///
/// The location can be an index, URL or path (wheel), or index, URL, path or Git repository (source distribution).
#[derive(Debug, Clone, Hash)]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Dist {
Built(BuiltDist),
Source(SourceDist),
}
/// A reference to a built or source distribution.
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub enum DistRef<'a> {
Built(&'a BuiltDist),
Source(&'a SourceDist),
}
/// A wheel, with its three possible origins (index, url, path)
#[derive(Debug, Clone, Hash)]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
#[allow(clippy::large_enum_variant)]
pub enum BuiltDist {
Registry(RegistryBuiltDist),
@ -182,7 +191,7 @@ pub enum BuiltDist {
}
/// A source distribution, with its possible origins (index, url, path, git)
#[derive(Debug, Clone, Hash)]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
#[allow(clippy::large_enum_variant)]
pub enum SourceDist {
Registry(RegistrySourceDist),
@ -193,7 +202,7 @@ pub enum SourceDist {
}
/// A built distribution (wheel) that exists in a registry, like `PyPI`.
#[derive(Debug, Clone, Hash)]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct RegistryBuiltWheel {
pub filename: WheelFilename,
pub file: Box<File>,
@ -201,7 +210,7 @@ pub struct RegistryBuiltWheel {
}
/// A built distribution (wheel) that exists in a registry, like `PyPI`.
#[derive(Debug, Clone, Hash)]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct RegistryBuiltDist {
/// All wheels associated with this distribution. It is guaranteed
/// that there is at least one wheel.
@ -231,7 +240,7 @@ pub struct RegistryBuiltDist {
}
/// A built distribution (wheel) that exists at an arbitrary URL.
#[derive(Debug, Clone, Hash)]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct DirectUrlBuiltDist {
/// We require that wheel urls end in the full wheel filename, e.g.
/// `https://example.org/packages/flask-3.0.0-py3-none-any.whl`
@ -243,7 +252,7 @@ pub struct DirectUrlBuiltDist {
}
/// A built distribution (wheel) that exists in a local directory.
#[derive(Debug, Clone, Hash)]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct PathBuiltDist {
pub filename: WheelFilename,
/// The absolute path to the wheel which we use for installing.
@ -253,7 +262,7 @@ pub struct PathBuiltDist {
}
/// A source distribution that exists in a registry, like `PyPI`.
#[derive(Debug, Clone, Hash)]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct RegistrySourceDist {
pub name: PackageName,
pub version: Version,
@ -272,7 +281,7 @@ pub struct RegistrySourceDist {
}
/// A source distribution that exists at an arbitrary URL.
#[derive(Debug, Clone, Hash)]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct DirectUrlSourceDist {
/// Unlike [`DirectUrlBuiltDist`], we can't require a full filename with a version here, people
/// like using e.g. `foo @ https://github.com/org/repo/archive/master.zip`
@ -288,7 +297,7 @@ pub struct DirectUrlSourceDist {
}
/// A source distribution that exists in a Git repository.
#[derive(Debug, Clone, Hash)]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct GitSourceDist {
pub name: PackageName,
/// The URL without the revision and subdirectory fragment.
@ -300,7 +309,7 @@ pub struct GitSourceDist {
}
/// A source distribution that exists in a local archive (e.g., a `.tar.gz` file).
#[derive(Debug, Clone, Hash)]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct PathSourceDist {
pub name: PackageName,
/// The absolute path to the distribution which we use for installing.
@ -312,7 +321,7 @@ pub struct PathSourceDist {
}
/// A source distribution that exists in a local directory.
#[derive(Debug, Clone, Hash)]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct DirectorySourceDist {
pub name: PackageName,
/// The absolute path to the distribution which we use for installing.
@ -512,12 +521,33 @@ impl Dist {
}
}
/// Returns the version of the distribution, if it is known.
pub fn version(&self) -> Option<&Version> {
match self {
Self::Built(wheel) => Some(wheel.version()),
Self::Source(source_dist) => source_dist.version(),
}
}
/// Convert this distribution into a reference.
pub fn as_ref(&self) -> DistRef {
match self {
Self::Built(dist) => DistRef::Built(dist),
Self::Source(dist) => DistRef::Source(dist),
}
}
}
impl<'a> From<&'a SourceDist> for DistRef<'a> {
fn from(dist: &'a SourceDist) -> Self {
DistRef::Source(dist)
}
}
impl<'a> From<&'a BuiltDist> for DistRef<'a> {
fn from(dist: &'a BuiltDist) -> Self {
DistRef::Built(dist)
}
}
impl BuiltDist {

View File

@ -9,29 +9,13 @@ use uv_cache::Cache;
use uv_configuration::BuildOptions;
use uv_distribution::{DistributionDatabase, LocalWheel};
use uv_distribution_types::{
BuildableSource, BuiltDist, CachedDist, Dist, Hashed, Identifier, Name, RemoteSource,
SourceDist,
BuildableSource, BuiltDist, CachedDist, DerivationChain, Dist, Hashed, Identifier, Name,
RemoteSource, SourceDist,
};
use uv_pep508::PackageName;
use uv_platform_tags::Tags;
use uv_types::{BuildContext, HashStrategy, InFlight};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Building source distributions is disabled, but attempted to build `{0}`")]
NoBuild(PackageName),
#[error("Using pre-built wheels is disabled, but attempted to use `{0}`")]
NoBinary(PackageName),
#[error("Failed to download `{0}`")]
Download(Box<BuiltDist>, #[source] uv_distribution::Error),
#[error("Failed to download and build `{0}`")]
DownloadAndBuild(Box<SourceDist>, #[source] uv_distribution::Error),
#[error("Failed to build `{0}`")]
Build(Box<SourceDist>, #[source] uv_distribution::Error),
#[error("Unzip failed in another thread: {0}")]
Thread(String),
}
/// Prepare distributions for installation.
///
/// Downloads, builds, and unzips a set of distributions.
@ -145,16 +129,7 @@ impl<'a, Context: BuildContext> Preparer<'a, Context> {
.database
.get_or_build_wheel(&dist, self.tags, policy)
.boxed_local()
.map_err(|err| match dist.clone() {
Dist::Built(dist) => Error::Download(Box::new(dist), err),
Dist::Source(dist) => {
if dist.is_local() {
Error::Build(Box::new(dist), err)
} else {
Error::DownloadAndBuild(Box::new(dist), err)
}
}
})
.map_err(|err| Error::from_dist(dist.clone(), err))
.await
.and_then(|wheel: LocalWheel| {
if wheel.satisfies(policy) {
@ -165,16 +140,7 @@ impl<'a, Context: BuildContext> Preparer<'a, Context> {
policy.digests(),
wheel.hashes(),
);
Err(match dist {
Dist::Built(dist) => Error::Download(Box::new(dist), err),
Dist::Source(dist) => {
if dist.is_local() {
Error::Build(Box::new(dist), err)
} else {
Error::DownloadAndBuild(Box::new(dist), err)
}
}
})
Err(Error::from_dist(dist, err))
}
})
.map(CachedDist::from);
@ -203,6 +169,50 @@ impl<'a, Context: BuildContext> Preparer<'a, Context> {
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Building source distributions is disabled, but attempted to build `{0}`")]
NoBuild(PackageName),
#[error("Using pre-built wheels is disabled, but attempted to use `{0}`")]
NoBinary(PackageName),
#[error("Failed to download `{0}`")]
Download(
Box<BuiltDist>,
DerivationChain,
#[source] uv_distribution::Error,
),
#[error("Failed to download and build `{0}`")]
DownloadAndBuild(
Box<SourceDist>,
DerivationChain,
#[source] uv_distribution::Error,
),
#[error("Failed to build `{0}`")]
Build(
Box<SourceDist>,
DerivationChain,
#[source] uv_distribution::Error,
),
#[error("Unzip failed in another thread: {0}")]
Thread(String),
}
impl Error {
/// Create an [`Error`] from a distribution error.
fn from_dist(dist: Dist, cause: uv_distribution::Error) -> Self {
match dist {
Dist::Built(dist) => Self::Download(Box::new(dist), DerivationChain::default(), cause),
Dist::Source(dist) => {
if dist.is_local() {
Self::Build(Box::new(dist), DerivationChain::default(), cause)
} else {
Self::DownloadAndBuild(Box::new(dist), DerivationChain::default(), cause)
}
}
}
}
}
pub trait Reporter: Send + Sync {
/// Callback to invoke when a wheel is unzipped. This implies that the wheel was downloaded and,
/// if necessary, built.

View File

@ -3,7 +3,7 @@ use std::sync::Arc;
use futures::{stream::FuturesOrdered, TryStreamExt};
use uv_distribution::{DistributionDatabase, Reporter};
use uv_distribution_types::{Dist, DistributionMetadata};
use uv_distribution_types::DistributionMetadata;
use uv_pypi_types::Requirement;
use uv_resolver::{InMemoryIndex, MetadataResponse};
use uv_types::{BuildContext, HashStrategy};
@ -100,16 +100,7 @@ impl<'a, Context: BuildContext> ExtrasResolver<'a, Context> {
let archive = database
.get_or_build_wheel_metadata(&dist, hasher.get(&dist))
.await
.map_err(|err| match dist {
Dist::Built(built) => Error::Download(Box::new(built), err),
Dist::Source(source) => {
if source.is_local() {
Error::Build(Box::new(source), err)
} else {
Error::DownloadAndBuild(Box::new(source), err)
}
}
})?;
.map_err(|err| Error::from_dist(dist, err))?;
let metadata = archive.metadata.clone();

View File

@ -1,13 +1,12 @@
use uv_distribution_types::{BuiltDist, Dist, GitSourceDist, SourceDist};
use uv_git::GitUrl;
use uv_pypi_types::{Requirement, RequirementSource};
pub use crate::extras::*;
pub use crate::lookahead::*;
pub use crate::source_tree::*;
pub use crate::sources::*;
pub use crate::specification::*;
pub use crate::unnamed::*;
use uv_distribution_types::{BuiltDist, DerivationChain, Dist, GitSourceDist, SourceDist};
use uv_git::GitUrl;
use uv_pypi_types::{Requirement, RequirementSource};
mod extras;
mod lookahead;
@ -20,13 +19,25 @@ pub mod upgrade;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Failed to download `{0}`")]
Download(Box<BuiltDist>, #[source] uv_distribution::Error),
Download(
Box<BuiltDist>,
DerivationChain,
#[source] uv_distribution::Error,
),
#[error("Failed to download and build `{0}`")]
DownloadAndBuild(Box<SourceDist>, #[source] uv_distribution::Error),
DownloadAndBuild(
Box<SourceDist>,
DerivationChain,
#[source] uv_distribution::Error,
),
#[error("Failed to build `{0}`")]
Build(Box<SourceDist>, #[source] uv_distribution::Error),
Build(
Box<SourceDist>,
DerivationChain,
#[source] uv_distribution::Error,
),
#[error(transparent)]
Distribution(#[from] uv_distribution::Error),
@ -38,6 +49,22 @@ pub enum Error {
WheelFilename(#[from] uv_distribution_filename::WheelFilenameError),
}
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::Download(Box::new(dist), DerivationChain::default(), cause),
Dist::Source(dist) => {
if dist.is_local() {
Self::Build(Box::new(dist), DerivationChain::default(), cause)
} else {
Self::DownloadAndBuild(Box::new(dist), DerivationChain::default(), cause)
}
}
}
}
}
/// Convert a [`Requirement`] into a [`Dist`], if it is a direct URL.
pub(crate) fn required_dist(
requirement: &Requirement,

View File

@ -174,16 +174,7 @@ impl<'a, Context: BuildContext> LookaheadResolver<'a, Context> {
.database
.get_or_build_wheel_metadata(&dist, self.hasher.get(&dist))
.await
.map_err(|err| match dist {
Dist::Built(built) => Error::Download(Box::new(built), err),
Dist::Source(source) => {
if source.is_local() {
Error::Build(Box::new(source), err)
} else {
Error::DownloadAndBuild(Box::new(source), err)
}
}
})?;
.map_err(|err| Error::from_dist(dist, err))?;
let metadata = archive.metadata.clone();

View File

@ -10,7 +10,8 @@ use rustc_hash::FxHashMap;
use tracing::trace;
use uv_distribution_types::{
BuiltDist, IndexCapabilities, IndexLocations, IndexUrl, InstalledDist, SourceDist,
BuiltDist, DerivationChain, IndexCapabilities, IndexLocations, IndexUrl, InstalledDist,
SourceDist,
};
use uv_normalize::{ExtraName, PackageName};
use uv_pep440::{LocalVersionSlice, Version};
@ -97,20 +98,36 @@ pub enum ResolveError {
ParsedUrl(#[from] uv_pypi_types::ParsedUrlError),
#[error("Failed to download `{0}`")]
Download(Box<BuiltDist>, #[source] Arc<uv_distribution::Error>),
Download(
Box<BuiltDist>,
DerivationChain,
#[source] Arc<uv_distribution::Error>,
),
#[error("Failed to download and build `{0}`")]
DownloadAndBuild(Box<SourceDist>, #[source] Arc<uv_distribution::Error>),
DownloadAndBuild(
Box<SourceDist>,
DerivationChain,
#[source] Arc<uv_distribution::Error>,
),
#[error("Failed to read `{0}`")]
Read(Box<BuiltDist>, #[source] Arc<uv_distribution::Error>),
Read(
Box<BuiltDist>,
DerivationChain,
#[source] Arc<uv_distribution::Error>,
),
// TODO(zanieb): Use `thiserror` in `InstalledDist` so we can avoid chaining `anyhow`
#[error("Failed to read metadata from installed package `{0}`")]
ReadInstalled(Box<InstalledDist>, #[source] anyhow::Error),
ReadInstalled(Box<InstalledDist>, DerivationChain, #[source] anyhow::Error),
#[error("Failed to build `{0}`")]
Build(Box<SourceDist>, #[source] Arc<uv_distribution::Error>),
Build(
Box<SourceDist>,
DerivationChain,
#[source] Arc<uv_distribution::Error>,
),
#[error(transparent)]
NoSolution(#[from] NoSolutionError),

View File

@ -18,9 +18,9 @@ pub use resolution::{
};
pub use resolution_mode::ResolutionMode;
pub use resolver::{
BuildId, DefaultResolverProvider, InMemoryIndex, MetadataResponse, PackageVersionsResult,
Reporter as ResolverReporter, Resolver, ResolverEnvironment, ResolverProvider,
VersionsResponse, WheelMetadataResult,
BuildId, DefaultResolverProvider, DerivationChainBuilder, InMemoryIndex, MetadataResponse,
PackageVersionsResult, Reporter as ResolverReporter, Resolver, ResolverEnvironment,
ResolverProvider, VersionsResponse, WheelMetadataResult,
};
pub use version_map::VersionMap;
pub use yanks::AllowedYanks;

View File

@ -0,0 +1,137 @@
use std::collections::VecDeque;
use petgraph::Direction;
use pubgrub::{Kind, SelectedDependencies, State};
use rustc_hash::FxHashSet;
use uv_distribution_types::{
DerivationChain, DerivationStep, DistRef, Name, Node, Resolution, ResolvedDist,
};
use uv_pep440::Version;
use crate::dependency_provider::UvDependencyProvider;
use crate::pubgrub::PubGrubPackage;
/// A chain of derivation steps from the root package to the current package, to explain why a
/// package is included in the resolution.
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct DerivationChainBuilder;
impl DerivationChainBuilder {
/// Compute a [`DerivationChain`] from a resolution graph.
///
/// This is used to construct a derivation chain upon install failure in the `uv pip` context,
/// where we don't have a lockfile describing the resolution.
pub fn from_resolution(
resolution: &Resolution,
target: DistRef<'_>,
) -> Option<DerivationChain> {
// Find the target distribution in the resolution graph.
let target = resolution.graph().node_indices().find(|node| {
let Node::Dist {
dist: ResolvedDist::Installable { dist, .. },
..
} = &resolution.graph()[*node]
else {
return false;
};
target == dist.as_ref()
})?;
// Perform a BFS to find the shortest path to the root.
let mut queue = VecDeque::new();
queue.push_back((target, Vec::new()));
// TODO(charlie): Consider respecting markers here.
let mut seen = FxHashSet::default();
while let Some((node, mut path)) = queue.pop_front() {
if !seen.insert(node) {
continue;
}
match &resolution.graph()[node] {
Node::Root => {
path.reverse();
path.pop();
return Some(DerivationChain::from_iter(path));
}
Node::Dist { dist, .. } => {
path.push(DerivationStep::new(
dist.name().clone(),
dist.version().clone(),
));
for neighbor in resolution
.graph()
.neighbors_directed(node, Direction::Incoming)
{
queue.push_back((neighbor, path.clone()));
}
}
}
}
None
}
/// Compute a [`DerivationChain`] from the current PubGrub state.
///
/// This is used to construct a derivation chain upon resolution failure.
pub(crate) fn from_state(
package: &PubGrubPackage,
version: &Version,
state: &State<UvDependencyProvider>,
) -> Option<DerivationChain> {
/// Find a path from the current package to the root package.
fn find_path(
package: &PubGrubPackage,
version: &Version,
state: &State<UvDependencyProvider>,
solution: &SelectedDependencies<UvDependencyProvider>,
path: &mut Vec<DerivationStep>,
) -> bool {
// Retrieve the incompatiblies for the current package.
let Some(incompats) = state.incompatibilities.get(package) else {
return false;
};
for index in incompats {
let incompat = &state.incompatibility_store[*index];
// Find a dependency from a package to the current package.
if let Kind::FromDependencyOf(p1, _v1, p2, v2) = &incompat.kind {
if p2 == package && v2.contains(version) {
if let Some(version) = solution.get(p1) {
if let Some(name) = p1.name() {
// Add to the current path.
path.push(DerivationStep::new(name.clone(), version.clone()));
// Recursively search the next package.
if find_path(p1, version, state, solution, path) {
return true;
}
// Backtrack if the path didn't lead to the root.
path.pop();
} else {
// If we've reached the root, return.
return true;
}
}
}
}
}
false
}
let solution = state.partial_solution.extract_solution();
let path = {
let mut path = vec![];
if !find_path(package, version, state, &solution, &mut path) {
return None;
}
path.reverse();
path.dedup();
path
};
Some(path.into_iter().collect())
}
}

View File

@ -27,9 +27,9 @@ pub(crate) use urls::Urls;
use uv_configuration::{Constraints, Overrides};
use uv_distribution::{ArchiveMetadata, DistributionDatabase};
use uv_distribution_types::{
BuiltDist, CompatibleDist, Dist, DistributionMetadata, IncompatibleDist, IncompatibleSource,
IncompatibleWheel, IndexCapabilities, IndexLocations, IndexUrl, InstalledDist,
PythonRequirementKind, RemoteSource, ResolvedDist, ResolvedDistRef, SourceDist,
BuiltDist, CompatibleDist, DerivationChain, Dist, DistributionMetadata, IncompatibleDist,
IncompatibleSource, IncompatibleWheel, IndexCapabilities, IndexLocations, IndexUrl,
InstalledDist, PythonRequirementKind, RemoteSource, ResolvedDist, ResolvedDistRef, SourceDist,
VersionOrUrlRef,
};
use uv_git::GitResolver;
@ -62,6 +62,8 @@ pub(crate) use crate::resolver::availability::{
IncompletePackage, ResolverVersion, UnavailablePackage, UnavailableReason, UnavailableVersion,
};
use crate::resolver::batch_prefetch::BatchPrefetcher;
pub use crate::resolver::derivation::DerivationChainBuilder;
use crate::resolver::groups::Groups;
pub use crate::resolver::index::InMemoryIndex;
use crate::resolver::indexes::Indexes;
@ -76,6 +78,7 @@ use crate::{marker, DependencyMode, Exclusions, FlatIndex, Options, ResolutionMo
mod availability;
mod batch_prefetch;
mod derivation;
mod environment;
mod fork_map;
mod groups;
@ -387,6 +390,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
resolutions.push(resolution);
continue 'FORK;
};
state.next = highest_priority_pkg;
let url = state.next.name().and_then(|name| state.fork_urls.get(name));
@ -520,9 +524,12 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
&state.fork_urls,
&state.env,
&state.python_requirement,
&state.pubgrub,
)?;
match forked_deps {
ForkedDependencies::Unavailable(reason) => {
// Then here, if we get a reason that we consider unrecoverable, we should
// show the derivation chain.
state
.pubgrub
.add_incompatibility(Incompatibility::custom_version(
@ -945,25 +952,33 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
unreachable!("`requires-python` is only known upfront for registry distributions")
}
MetadataResponse::Error(dist, err) => {
// 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();
return Err(match &**dist {
Dist::Built(built_dist @ BuiltDist::Path(_)) => {
ResolveError::Read(Box::new(built_dist.clone()), (*err).clone())
ResolveError::Read(Box::new(built_dist.clone()), chain, (*err).clone())
}
Dist::Source(source_dist @ SourceDist::Path(_)) => {
ResolveError::Build(Box::new(source_dist.clone()), (*err).clone())
ResolveError::Build(Box::new(source_dist.clone()), chain, (*err).clone())
}
Dist::Source(source_dist @ SourceDist::Directory(_)) => {
ResolveError::Build(Box::new(source_dist.clone()), (*err).clone())
ResolveError::Build(Box::new(source_dist.clone()), chain, (*err).clone())
}
Dist::Built(built_dist) => {
ResolveError::Download(Box::new(built_dist.clone()), (*err).clone())
ResolveError::Download(Box::new(built_dist.clone()), chain, (*err).clone())
}
Dist::Source(source_dist) => {
if source_dist.is_local() {
ResolveError::Build(Box::new(source_dist.clone()), (*err).clone())
ResolveError::Build(
Box::new(source_dist.clone()),
chain,
(*err).clone(),
)
} else {
ResolveError::DownloadAndBuild(
Box::new(source_dist.clone()),
chain,
(*err).clone(),
)
}
@ -1197,8 +1212,16 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
fork_urls: &ForkUrls,
env: &ResolverEnvironment,
python_requirement: &PythonRequirement,
pubgrub: &State<UvDependencyProvider>,
) -> Result<ForkedDependencies, ResolveError> {
let result = self.get_dependencies(package, version, fork_urls, env, python_requirement);
let result = self.get_dependencies(
package,
version,
fork_urls,
env,
python_requirement,
pubgrub,
);
if env.marker_environment().is_some() {
result.map(|deps| match deps {
Dependencies::Available(deps) | Dependencies::Unforkable(deps) => {
@ -1220,6 +1243,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
fork_urls: &ForkUrls,
env: &ResolverEnvironment,
python_requirement: &PythonRequirement,
pubgrub: &State<UvDependencyProvider>,
) -> Result<Dependencies, ResolveError> {
let url = package.name().and_then(|name| fork_urls.get(name));
let dependencies = match &**package {
@ -1357,28 +1381,42 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
));
}
MetadataResponse::Error(dist, err) => {
let chain = DerivationChainBuilder::from_state(package, version, pubgrub)
.unwrap_or_default();
return Err(match &**dist {
Dist::Built(built_dist @ BuiltDist::Path(_)) => {
ResolveError::Read(Box::new(built_dist.clone()), (*err).clone())
}
Dist::Source(source_dist @ SourceDist::Path(_)) => {
ResolveError::Build(Box::new(source_dist.clone()), (*err).clone())
}
Dist::Built(built_dist @ BuiltDist::Path(_)) => ResolveError::Read(
Box::new(built_dist.clone()),
chain,
(*err).clone(),
),
Dist::Source(source_dist @ SourceDist::Path(_)) => ResolveError::Build(
Box::new(source_dist.clone()),
chain,
(*err).clone(),
),
Dist::Source(source_dist @ SourceDist::Directory(_)) => {
ResolveError::Build(Box::new(source_dist.clone()), (*err).clone())
}
Dist::Built(built_dist) => {
ResolveError::Download(Box::new(built_dist.clone()), (*err).clone())
ResolveError::Build(
Box::new(source_dist.clone()),
chain,
(*err).clone(),
)
}
Dist::Built(built_dist) => ResolveError::Download(
Box::new(built_dist.clone()),
chain,
(*err).clone(),
),
Dist::Source(source_dist) => {
if source_dist.is_local() {
ResolveError::Build(
Box::new(source_dist.clone()),
chain,
(*err).clone(),
)
} else {
ResolveError::DownloadAndBuild(
Box::new(source_dist.clone()),
chain,
(*err).clone(),
)
}
@ -1848,9 +1886,14 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
}
Request::Installed(dist) => {
let metadata = dist
.metadata()
.map_err(|err| ResolveError::ReadInstalled(Box::new(dist.clone()), err))?;
// TODO(charlie): This should be return a `MetadataResponse`.
let metadata = dist.metadata().map_err(|err| {
ResolveError::ReadInstalled(
Box::new(dist.clone()),
DerivationChain::default(),
err,
)
})?;
Ok(Some(Response::Installed { dist, metadata }))
}
@ -1964,7 +2007,11 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
}
ResolvedDist::Installed { dist } => {
let metadata = dist.metadata().map_err(|err| {
ResolveError::ReadInstalled(Box::new(dist.clone()), err)
ResolveError::ReadInstalled(
Box::new(dist.clone()),
DerivationChain::default(),
err,
)
})?;
Response::Installed { dist, metadata }
}

View File

@ -4,7 +4,7 @@ use std::sync::LazyLock;
use owo_colors::OwoColorize;
use rustc_hash::FxHashMap;
use uv_distribution_types::{BuiltDist, Name, SourceDist};
use uv_distribution_types::{BuiltDist, DerivationChain, Name, SourceDist};
use uv_normalize::PackageName;
use crate::commands::pip;
@ -42,7 +42,7 @@ impl OperationDiagnostic {
pub(crate) fn with_hint(hint: String) -> Self {
Self {
hint: Some(hint),
context: None,
..Default::default()
}
}
@ -50,8 +50,8 @@ impl OperationDiagnostic {
#[must_use]
pub(crate) fn with_context(context: &'static str) -> Self {
Self {
hint: None,
context: Some(context),
..Default::default()
}
}
@ -74,47 +74,70 @@ impl OperationDiagnostic {
}
pip::operations::Error::Resolve(uv_resolver::ResolveError::DownloadAndBuild(
dist,
chain,
err,
)) => {
download_and_build(dist, Box::new(err));
download_and_build(dist, &chain, Box::new(err));
None
}
pip::operations::Error::Resolve(uv_resolver::ResolveError::Download(dist, err)) => {
download(dist, Box::new(err));
pip::operations::Error::Resolve(uv_resolver::ResolveError::Download(
dist,
chain,
err,
)) => {
download(dist, &chain, Box::new(err));
None
}
pip::operations::Error::Resolve(uv_resolver::ResolveError::Build(dist, err)) => {
build(dist, Box::new(err));
pip::operations::Error::Resolve(uv_resolver::ResolveError::Build(dist, chain, err)) => {
build(dist, &chain, Box::new(err));
None
}
pip::operations::Error::Requirements(uv_requirements::Error::DownloadAndBuild(
dist,
chain,
err,
)) => {
download_and_build(dist, Box::new(err));
download_and_build(dist, &chain, Box::new(err));
None
}
pip::operations::Error::Requirements(uv_requirements::Error::Download(dist, err)) => {
download(dist, Box::new(err));
pip::operations::Error::Requirements(uv_requirements::Error::Download(
dist,
chain,
err,
)) => {
download(dist, &chain, Box::new(err));
None
}
pip::operations::Error::Requirements(uv_requirements::Error::Build(dist, err)) => {
build(dist, Box::new(err));
pip::operations::Error::Requirements(uv_requirements::Error::Build(
dist,
chain,
err,
)) => {
build(dist, &chain, Box::new(err));
None
}
pip::operations::Error::Prepare(uv_installer::PrepareError::DownloadAndBuild(
dist,
chain,
err,
)) => {
download_and_build(dist, Box::new(err));
download_and_build(dist, &chain, Box::new(err));
None
}
pip::operations::Error::Prepare(uv_installer::PrepareError::Download(dist, err)) => {
download(dist, Box::new(err));
pip::operations::Error::Prepare(uv_installer::PrepareError::Download(
dist,
chain,
err,
)) => {
download(dist, &chain, Box::new(err));
None
}
pip::operations::Error::Prepare(uv_installer::PrepareError::Build(dist, err)) => {
build(dist, Box::new(err));
pip::operations::Error::Prepare(uv_installer::PrepareError::Build(
dist,
chain,
err,
)) => {
build(dist, &chain, Box::new(err));
None
}
pip::operations::Error::Requirements(err) => {
@ -133,7 +156,7 @@ impl OperationDiagnostic {
}
/// Render a remote source distribution build failure with a help message.
pub(crate) fn download_and_build(sdist: Box<SourceDist>, cause: Error) {
pub(crate) fn download_and_build(sdist: Box<SourceDist>, chain: &DerivationChain, cause: Error) {
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
#[error("Failed to download and build `{sdist}`")]
#[diagnostic()]
@ -146,14 +169,32 @@ pub(crate) fn download_and_build(sdist: Box<SourceDist>, cause: Error) {
}
let report = miette::Report::new(Diagnostic {
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(),
)
}),
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(),
)
})
.or_else(|| {
if chain.is_empty() {
None
} else {
let mut message = format!("`{}` was included because", sdist.name().cyan());
for (i, step) in chain.iter().enumerate() {
if i == 0 {
message = format!("{message} `{}` depends on", step.cyan());
} else {
message = format!("{message} `{}` which depends on", step.cyan());
}
}
message = format!("{message} `{}`", sdist.name().cyan());
Some(message)
}
}),
sdist,
cause,
});
@ -161,7 +202,7 @@ pub(crate) fn download_and_build(sdist: Box<SourceDist>, cause: Error) {
}
/// Render a remote binary distribution download failure with a help message.
pub(crate) fn download(sdist: Box<BuiltDist>, cause: Error) {
pub(crate) fn download(sdist: Box<BuiltDist>, chain: &DerivationChain, cause: Error) {
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
#[error("Failed to download `{sdist}`")]
#[diagnostic()]
@ -174,14 +215,32 @@ pub(crate) fn download(sdist: Box<BuiltDist>, cause: Error) {
}
let report = miette::Report::new(Diagnostic {
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(),
)
}),
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(),
)
})
.or_else(|| {
if chain.is_empty() {
None
} else {
let mut message = format!("`{}` was included because", sdist.name().cyan());
for (i, step) in chain.iter().enumerate() {
if i == 0 {
message = format!("{message} `{}` depends on", step.cyan());
} else {
message = format!("{message} `{}` which depends on", step.cyan());
}
}
message = format!("{message} `{}`", sdist.name().cyan());
Some(message)
}
}),
sdist,
cause,
});
@ -189,7 +248,7 @@ pub(crate) fn download(sdist: Box<BuiltDist>, cause: Error) {
}
/// Render a local source distribution build failure with a help message.
pub(crate) fn build(sdist: Box<SourceDist>, cause: Error) {
pub(crate) fn build(sdist: Box<SourceDist>, chain: &DerivationChain, cause: Error) {
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
#[error("Failed to build `{sdist}`")]
#[diagnostic()]
@ -202,14 +261,32 @@ pub(crate) fn build(sdist: Box<SourceDist>, cause: Error) {
}
let report = miette::Report::new(Diagnostic {
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(),
)
}),
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(),
)
})
.or_else(|| {
if chain.is_empty() {
None
} else {
let mut message = format!("`{}` was included because", sdist.name().cyan());
for (i, step) in chain.iter().enumerate() {
if i == 0 {
message = format!("{message} `{}` depends on", step.cyan());
} else {
message = format!("{message} `{}` which depends on", step.cyan());
}
}
message = format!("{message} `{}`", sdist.name().cyan());
Some(message)
}
}),
sdist,
cause,
});

View File

@ -412,7 +412,7 @@ pub(crate) async fn pip_install(
)
.await
{
Ok(resolution) => Resolution::from(resolution),
Ok(graph) => Resolution::from(graph),
Err(err) => {
return diagnostics::OperationDiagnostic::default()
.report(err)

View File

@ -36,8 +36,9 @@ use uv_requirements::{
SourceTreeResolver,
};
use uv_resolver::{
DependencyMode, Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, Preference,
Preferences, PythonRequirement, Resolver, ResolverEnvironment, ResolverOutput,
DependencyMode, DerivationChainBuilder, Exclusions, FlatIndex, InMemoryIndex, Manifest,
Options, Preference, Preferences, PythonRequirement, Resolver, ResolverEnvironment,
ResolverOutput,
};
use uv_types::{HashStrategy, InFlight, InstalledPackagesProvider};
use uv_warnings::warn_user;
@ -459,7 +460,37 @@ pub(crate) async fn install(
)
.with_reporter(PrepareReporter::from(printer).with_length(remote.len() as u64));
let wheels = preparer.prepare(remote.clone(), in_flight).await?;
let wheels = preparer
.prepare(remote.clone(), in_flight)
.await
.map_err(Error::from)
.map_err(|err| match err {
// Attach resolution context to the error.
Error::Prepare(uv_installer::PrepareError::Download(dist, chain, err)) => {
debug_assert!(chain.is_empty());
let chain =
DerivationChainBuilder::from_resolution(resolution, (&*dist).into())
.unwrap_or_default();
Error::Prepare(uv_installer::PrepareError::Download(dist, chain, err))
}
Error::Prepare(uv_installer::PrepareError::Build(dist, chain, err)) => {
debug_assert!(chain.is_empty());
let chain =
DerivationChainBuilder::from_resolution(resolution, (&*dist).into())
.unwrap_or_default();
Error::Prepare(uv_installer::PrepareError::Build(dist, chain, err))
}
Error::Prepare(uv_installer::PrepareError::DownloadAndBuild(dist, chain, err)) => {
debug_assert!(chain.is_empty());
let chain =
DerivationChainBuilder::from_resolution(resolution, (&*dist).into())
.unwrap_or_default();
Error::Prepare(uv_installer::PrepareError::DownloadAndBuild(
dist, chain, err,
))
}
_ => err,
})?;
logger.on_prepare(wheels.len(), start, printer)?;

View File

@ -13,7 +13,9 @@ use uv_configuration::{
ExtrasSpecification, HashCheckingMode, InstallOptions, LowerBound, TrustedHost,
};
use uv_dispatch::BuildDispatch;
use uv_distribution_types::{DirectorySourceDist, Dist, Index, ResolvedDist, SourceDist};
use uv_distribution_types::{
DirectorySourceDist, Dist, Index, Resolution, ResolvedDist, SourceDist,
};
use uv_installer::SitePackages;
use uv_normalize::PackageName;
use uv_pep508::{MarkerTree, Requirement, VersionOrUrl};
@ -467,9 +469,7 @@ pub(super) async fn do_sync(
}
/// Filter out any virtual workspace members.
fn apply_no_virtual_project(
resolution: uv_distribution_types::Resolution,
) -> uv_distribution_types::Resolution {
fn apply_no_virtual_project(resolution: Resolution) -> Resolution {
resolution.filter(|dist| {
let ResolvedDist::Installable { dist, .. } = dist else {
return true;
@ -488,10 +488,7 @@ fn apply_no_virtual_project(
}
/// If necessary, convert any editable requirements to non-editable.
fn apply_editable_mode(
resolution: uv_distribution_types::Resolution,
editable: EditableMode,
) -> uv_distribution_types::Resolution {
fn apply_editable_mode(resolution: Resolution, editable: EditableMode) -> Resolution {
match editable {
// No modifications are necessary for editable mode; retain any editable distributions.
EditableMode::Editable => resolution,

View File

@ -5573,6 +5573,8 @@ fn fail_to_add_revert_project() -> Result<()> {
exec(code, locals())
File "<string>", line 1, in <module>
ZeroDivisionError: division by zero
help: `child` was included because `parent==0.1.0` depends on `child`
"###);
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
@ -5680,6 +5682,8 @@ fn fail_to_edit_revert_project() -> Result<()> {
exec(code, locals())
File "<string>", line 1, in <module>
ZeroDivisionError: division by zero
help: `child` was included because `parent==0.1.0` depends on `child`
"###);
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;

View File

@ -7357,6 +7357,7 @@ fn lock_invalid_hash() -> Result<()> {
Computed:
sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
help: `idna` was included because `project==0.1.0` depends on `anyio==3.7.0` which depends on `idna`
"###);
Ok(())
@ -8195,6 +8196,7 @@ fn lock_redact_https() -> Result<()> {
× Failed to download `iniconfig==2.0.0`
Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl`
HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl)
help: `iniconfig` was included because `foo==0.1.0` depends on `iniconfig`
"###);
// Installing from the lockfile should fail without an index.
@ -8207,6 +8209,7 @@ fn lock_redact_https() -> Result<()> {
× Failed to download `iniconfig==2.0.0`
Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl`
HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl)
help: `iniconfig` was included because `foo==0.1.0` depends on `iniconfig`
"###);
// Installing from the lockfile should succeed when credentials are included on the command-line.
@ -8246,6 +8249,7 @@ fn lock_redact_https() -> Result<()> {
× Failed to download `iniconfig==2.0.0`
Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl`
HTTP status client error (401 Unauthorized) for url (https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl)
help: `iniconfig` was included because `foo==0.1.0` depends on `iniconfig`
"###);
// Installing with credentials from with `UV_INDEX_URL` should succeed.
@ -19826,3 +19830,249 @@ fn lock_dynamic_version() -> Result<()> {
Ok(())
}
#[test]
fn lock_derivation_chain() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["wsgiref"]
"#,
)?;
let filters = context
.filters()
.into_iter()
.chain([
(r"exit code: 1", "exit status: 1"),
(r"/.*/src", "/[TMP]/src"),
])
.collect::<Vec<_>>();
uv_snapshot!(filters, context.lock(), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× Failed to download and build `wsgiref==0.1.2`
Build backend failed to determine requirements with `build_wheel()` (exit status: 1)
[stderr]
Traceback (most recent call last):
File "<string>", line 14, in <module>
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel
return self._get_build_requires(config_settings, requirements=['wheel'])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 295, in _get_build_requires
self.run_setup()
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 487, in run_setup
super().run_setup(setup_script=setup_script)
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 311, in run_setup
exec(code, locals())
File "<string>", line 5, in <module>
File "[CACHE_DIR]/[TMP]/src/ez_setup/__init__.py", line 170
print "Setuptools version",version,"or greater has been installed."
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
help: `wsgiref` was included because `project==0.1.0` depends on `wsgiref`
"###);
Ok(())
}
#[test]
fn lock_derivation_chain_extra() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
optional-dependencies = { wsgi = ["wsgiref"] }
"#,
)?;
let filters = context
.filters()
.into_iter()
.chain([
(r"exit code: 1", "exit status: 1"),
(r"/.*/src", "/[TMP]/src"),
])
.collect::<Vec<_>>();
uv_snapshot!(filters, context.lock(), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× Failed to download and build `wsgiref==0.1.2`
Build backend failed to determine requirements with `build_wheel()` (exit status: 1)
[stderr]
Traceback (most recent call last):
File "<string>", line 14, in <module>
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel
return self._get_build_requires(config_settings, requirements=['wheel'])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 295, in _get_build_requires
self.run_setup()
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 487, in run_setup
super().run_setup(setup_script=setup_script)
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 311, in run_setup
exec(code, locals())
File "<string>", line 5, in <module>
File "[CACHE_DIR]/[TMP]/src/ez_setup/__init__.py", line 170
print "Setuptools version",version,"or greater has been installed."
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
help: `wsgiref` was included because `project==0.1.0` depends on `wsgiref`
"###);
Ok(())
}
#[test]
fn lock_derivation_chain_group() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[dependency-groups]
wsgi = ["wsgiref"]
"#,
)?;
let filters = context
.filters()
.into_iter()
.chain([
(r"exit code: 1", "exit status: 1"),
(r"/.*/src", "/[TMP]/src"),
])
.collect::<Vec<_>>();
uv_snapshot!(filters, context.lock(), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× Failed to download and build `wsgiref==0.1.2`
Build backend failed to determine requirements with `build_wheel()` (exit status: 1)
[stderr]
Traceback (most recent call last):
File "<string>", line 14, in <module>
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel
return self._get_build_requires(config_settings, requirements=['wheel'])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 295, in _get_build_requires
self.run_setup()
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 487, in run_setup
super().run_setup(setup_script=setup_script)
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 311, in run_setup
exec(code, locals())
File "<string>", line 5, in <module>
File "[CACHE_DIR]/[TMP]/src/ez_setup/__init__.py", line 170
print "Setuptools version",version,"or greater has been installed."
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
help: `wsgiref` was included because `project==0.1.0` depends on `wsgiref`
"###);
Ok(())
}
#[test]
fn lock_derivation_chain_extended() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["child"]
[tool.uv.sources]
child = { path = "child" }
"#,
)?;
let child = context.temp_dir.child("child");
child.child("pyproject.toml").write_str(
r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["wsgiref"]
"#,
)?;
let filters = context
.filters()
.into_iter()
.chain([
(r"exit code: 1", "exit status: 1"),
(r"/.*/src", "/[TMP]/src"),
])
.collect::<Vec<_>>();
uv_snapshot!(filters, context.lock(), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× Failed to download and build `wsgiref==0.1.2`
Build backend failed to determine requirements with `build_wheel()` (exit status: 1)
[stderr]
Traceback (most recent call last):
File "<string>", line 14, in <module>
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel
return self._get_build_requires(config_settings, requirements=['wheel'])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 295, in _get_build_requires
self.run_setup()
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 487, in run_setup
super().run_setup(setup_script=setup_script)
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 311, in run_setup
exec(code, locals())
File "<string>", line 5, in <module>
File "[CACHE_DIR]/[TMP]/src/ez_setup/__init__.py", line 170
print "Setuptools version",version,"or greater has been installed."
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
help: `wsgiref` was included because `project==0.1.0` depends on `child==0.1.0` which depends on `wsgiref`
"###);
Ok(())
}

View File

@ -7319,3 +7319,62 @@ fn sklearn() {
"###
);
}
#[test]
fn resolve_derivation_chain() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! {r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["wsgiref"]
"#
})?;
let filters = context
.filters()
.into_iter()
.chain([
(r"exit code: 1", "exit status: 1"),
(r"/.*/src", "/[TMP]/src"),
])
.collect::<Vec<_>>();
uv_snapshot!(filters, context.pip_install()
.arg("-e")
.arg("."), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× Failed to download and build `wsgiref==0.1.2`
Build backend failed to determine requirements with `build_wheel()` (exit status: 1)
[stderr]
Traceback (most recent call last):
File "<string>", line 14, in <module>
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel
return self._get_build_requires(config_settings, requirements=['wheel'])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 295, in _get_build_requires
self.run_setup()
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 487, in run_setup
super().run_setup(setup_script=setup_script)
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 311, in run_setup
exec(code, locals())
File "<string>", line 5, in <module>
File "[CACHE_DIR]/[TMP]/src/ez_setup/__init__.py", line 170
print "Setuptools version",version,"or greater has been installed."
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
help: `wsgiref` was included because `project==0.1.0` depends on `wsgiref`
"###
);
Ok(())
}

View File

@ -700,6 +700,8 @@ fn sync_build_isolation_package() -> Result<()> {
Traceback (most recent call last):
File "<string>", line 8, in <module>
ModuleNotFoundError: No module named 'hatchling'
help: `source-distribution` was included because `project==0.1.0` depends on `source-distribution`
"###);
// Install `hatchling` for `source-distribution`.
@ -789,6 +791,8 @@ fn sync_build_isolation_extra() -> Result<()> {
Traceback (most recent call last):
File "<string>", line 8, in <module>
ModuleNotFoundError: No module named 'hatchling'
help: `source-distribution` was included because `project==0.1.0` depends on `source-distribution`
"###);
// Running `uv sync` with `--all-extras` should also fail.
@ -806,6 +810,8 @@ fn sync_build_isolation_extra() -> Result<()> {
Traceback (most recent call last):
File "<string>", line 8, in <module>
ModuleNotFoundError: No module named 'hatchling'
help: `source-distribution` was included because `project==0.1.0` depends on `source-distribution`
"###);
// Install the build dependencies.
@ -4270,3 +4276,196 @@ fn sync_all_groups() -> Result<()> {
Ok(())
}
#[test]
fn sync_derivation_chain() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["wsgiref"]
[[tool.uv.dependency-metadata]]
name = "wsgiref"
version = "0.1.2"
dependencies = []
"#,
)?;
let filters = context
.filters()
.into_iter()
.chain([
(r"exit code: 1", "exit status: 1"),
(r"/.*/src", "/[TMP]/src"),
])
.collect::<Vec<_>>();
uv_snapshot!(filters, context.sync(), @r###"
success: false
exit_code: 1
----- stdout -----
----- 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)
[stderr]
Traceback (most recent call last):
File "<string>", line 14, in <module>
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel
return self._get_build_requires(config_settings, requirements=['wheel'])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 295, in _get_build_requires
self.run_setup()
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 487, in run_setup
super().run_setup(setup_script=setup_script)
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 311, in run_setup
exec(code, locals())
File "<string>", line 5, in <module>
File "[CACHE_DIR]/[TMP]/src/ez_setup/__init__.py", line 170
print "Setuptools version",version,"or greater has been installed."
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
help: `wsgiref` was included because `project==0.1.0` depends on `wsgiref`
"###);
Ok(())
}
#[test]
fn sync_derivation_chain_extra() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
optional-dependencies = { wsgi = ["wsgiref"] }
[[tool.uv.dependency-metadata]]
name = "wsgiref"
version = "0.1.2"
dependencies = []
"#,
)?;
let filters = context
.filters()
.into_iter()
.chain([
(r"exit code: 1", "exit status: 1"),
(r"/.*/src", "/[TMP]/src"),
])
.collect::<Vec<_>>();
uv_snapshot!(filters, context.sync().arg("--extra").arg("wsgi"), @r###"
success: false
exit_code: 1
----- stdout -----
----- 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)
[stderr]
Traceback (most recent call last):
File "<string>", line 14, in <module>
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel
return self._get_build_requires(config_settings, requirements=['wheel'])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 295, in _get_build_requires
self.run_setup()
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 487, in run_setup
super().run_setup(setup_script=setup_script)
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 311, in run_setup
exec(code, locals())
File "<string>", line 5, in <module>
File "[CACHE_DIR]/[TMP]/src/ez_setup/__init__.py", line 170
print "Setuptools version",version,"or greater has been installed."
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
help: `wsgiref` was included because `project==0.1.0` depends on `wsgiref`
"###);
Ok(())
}
#[test]
fn sync_derivation_chain_group() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[dependency-groups]
wsgi = ["wsgiref"]
[[tool.uv.dependency-metadata]]
name = "wsgiref"
version = "0.1.2"
dependencies = []
"#,
)?;
let filters = context
.filters()
.into_iter()
.chain([
(r"exit code: 1", "exit status: 1"),
(r"/.*/src", "/[TMP]/src"),
])
.collect::<Vec<_>>();
uv_snapshot!(filters, context.sync().arg("--group").arg("wsgi"), @r###"
success: false
exit_code: 1
----- stdout -----
----- 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)
[stderr]
Traceback (most recent call last):
File "<string>", line 14, in <module>
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 325, in get_requires_for_build_wheel
return self._get_build_requires(config_settings, requirements=['wheel'])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 295, in _get_build_requires
self.run_setup()
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 487, in run_setup
super().run_setup(setup_script=setup_script)
File "[CACHE_DIR]/builds-v0/[TMP]/build_meta.py", line 311, in run_setup
exec(code, locals())
File "<string>", line 5, in <module>
File "[CACHE_DIR]/[TMP]/src/ez_setup/__init__.py", line 170
print "Setuptools version",version,"or greater has been installed."
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?
help: `wsgiref` was included because `project==0.1.0` depends on `wsgiref`
"###);
Ok(())
}