diff --git a/crates/uv-requirements/src/lookahead.rs b/crates/uv-requirements/src/lookahead.rs index c0f854cf1..1275352da 100644 --- a/crates/uv-requirements/src/lookahead.rs +++ b/crates/uv-requirements/src/lookahead.rs @@ -1,15 +1,17 @@ use std::collections::VecDeque; use anyhow::{Context, Result}; +use cache_key::CanonicalUrl; use futures::stream::FuturesUnordered; use futures::StreamExt; use rustc_hash::FxHashSet; -use distribution_types::{Dist, LocalEditable}; +use distribution_types::{Dist, DistributionMetadata, LocalEditable}; use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl}; use pypi_types::Metadata23; use uv_client::RegistryClient; use uv_distribution::{DistributionDatabase, Reporter}; +use uv_resolver::InMemoryIndex; use uv_types::{BuildContext, Constraints, Overrides, RequestedRequirements}; /// A resolver for resolving lookahead requirements from direct URLs. @@ -37,6 +39,8 @@ pub struct LookaheadResolver<'a, Context: BuildContext + Send + Sync> { overrides: &'a Overrides, /// The editable requirements for the project. editables: &'a [(LocalEditable, Metadata23)], + /// The in-memory index for resolving dependencies. + index: &'a InMemoryIndex, /// The database for fetching and building distributions. database: DistributionDatabase<'a, Context>, } @@ -50,12 +54,14 @@ impl<'a, Context: BuildContext + Send + Sync> LookaheadResolver<'a, Context> { editables: &'a [(LocalEditable, Metadata23)], context: &'a Context, client: &'a RegistryClient, + index: &'a InMemoryIndex, ) -> Self { Self { requirements, constraints, overrides, editables, + index, database: DistributionDatabase::new(client, context), } } @@ -126,15 +132,34 @@ impl<'a, Context: BuildContext + Send + Sync> LookaheadResolver<'a, Context> { // Convert to a buildable distribution. let dist = Dist::from_url(requirement.name, url.clone())?; - // Run the PEP 517 build process to extract metadata from the source distribution. - let (metadata, _precise) = self - .database - .get_or_build_wheel_metadata(&dist) - .await - .with_context(|| match &dist { - Dist::Built(built) => format!("Failed to download: {built}"), - Dist::Source(source) => format!("Failed to download and build: {source}"), - })?; + // Fetch the metadata for the distribution. + let requires_dist = { + // If the metadata is already in the index, return it. + if let Some(metadata) = self.index.get_metadata(&dist.package_id()) { + metadata.requires_dist.clone() + } else { + // Run the PEP 517 build process to extract metadata from the source distribution. + let (metadata, precise) = self + .database + .get_or_build_wheel_metadata(&dist) + .await + .with_context(|| match &dist { + Dist::Built(built) => format!("Failed to download: {built}"), + Dist::Source(source) => format!("Failed to download and build: {source}"), + })?; + + // Insert the metadata into the index. + self.index + .insert_metadata(dist.package_id(), metadata.clone()); + + // Insert the redirect into the index. + if let Some(precise) = precise { + self.index.insert_redirect(CanonicalUrl::new(url), precise); + } + + metadata.requires_dist + } + }; // Consider the dependencies to be "direct" if the requirement is a local source tree. let direct = if let Dist::Source(source_dist) = &dist { @@ -146,7 +171,7 @@ impl<'a, Context: BuildContext + Send + Sync> LookaheadResolver<'a, Context> { // Return the requirements from the metadata. Ok(Some(RequestedRequirements::new( requirement.extras, - metadata.requires_dist, + requires_dist, direct, ))) } diff --git a/crates/uv-resolver/src/resolver/index.rs b/crates/uv-resolver/src/resolver/index.rs index 64692032a..b4eb62132 100644 --- a/crates/uv-resolver/src/resolver/index.rs +++ b/crates/uv-resolver/src/resolver/index.rs @@ -1,5 +1,6 @@ use cache_key::CanonicalUrl; use dashmap::DashMap; +use std::sync::Arc; use url::Url; use distribution_types::PackageId; @@ -24,3 +25,30 @@ pub struct InMemoryIndex { /// `git+https://github.com/pallets/flask.git@c2f65dd1cfff0672b902fd5b30815f0b4137214c`. pub(crate) redirects: DashMap, } + +impl InMemoryIndex { + /// Insert a [`VersionsResponse`] into the index. + pub fn insert_package(&self, package_name: PackageName, metadata: VersionsResponse) { + self.packages.done(package_name, metadata); + } + + /// Insert a [`Metadata23`] into the index. + pub fn insert_metadata(&self, package_id: PackageId, metadata: Metadata23) { + self.distributions.done(package_id, metadata); + } + + /// Insert a redirect from a source URL to a target URL. + pub fn insert_redirect(&self, source: CanonicalUrl, target: Url) { + self.redirects.insert(source, target); + } + + /// Get the [`VersionsResponse`] for a given package name, without waiting. + pub fn get_package(&self, package_name: &PackageName) -> Option> { + self.packages.get(package_name) + } + + /// Get the [`Metadata23`] for a given package ID, without waiting. + pub fn get_metadata(&self, package_id: &PackageId) -> Option> { + self.distributions.get(package_id) + } +} diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index 03400c80f..4c2b793c5 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -345,6 +345,7 @@ pub(crate) async fn pip_compile( &editables, &build_dispatch, &client, + &top_level_index, ) .with_reporter(ResolverReporter::from(printer)) .resolve(&markers) diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 93a800f39..3479a3bc0 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -546,6 +546,7 @@ async fn resolve( &editables, build_dispatch, client, + index, ) .with_reporter(ResolverReporter::from(printer)) .resolve(markers) diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index cce82df56..eb31bf0fd 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -1584,8 +1584,8 @@ fn conflicting_transitive_url_dependency() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because flask==3.0.0 depends on werkzeug>=3.0.0 and only werkzeug<3.0.0 - is available, we can conclude that flask==3.0.0 cannot be used. + ╰─▶ Because only werkzeug<3.0.0 is available and flask==3.0.0 depends on + werkzeug>=3.0.0, we can conclude that flask==3.0.0 cannot be used. And because you require flask==3.0.0, we can conclude that the requirements are unsatisfiable. "###