diff --git a/crates/uv-installer/src/site_packages.rs b/crates/uv-installer/src/site_packages.rs index 08d037e87..efd7e5dab 100644 --- a/crates/uv-installer/src/site_packages.rs +++ b/crates/uv-installer/src/site_packages.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::collections::BTreeSet; use std::iter::Flatten; use std::path::PathBuf; @@ -14,6 +15,7 @@ use uv_distribution_types::{ use uv_fs::Simplified; use uv_normalize::PackageName; use uv_pep440::{Version, VersionSpecifiers}; +use uv_pep508::VersionOrUrl; use uv_pypi_types::{Requirement, ResolverMarkerEnvironment, VerbatimParsedUrl}; use uv_python::{Interpreter, PythonEnvironment}; use uv_types::InstalledPackagesProvider; @@ -281,179 +283,164 @@ impl SitePackages { } /// Returns if the installed packages satisfy the given requirements. - pub fn satisfies( + pub fn satisfies_spec( &self, requirements: &[UnresolvedRequirementSpecification], constraints: &[NameRequirementSpecification], + overrides: &[UnresolvedRequirementSpecification], markers: &ResolverMarkerEnvironment, ) -> Result { - // Collect the constraints, filtering them by their marker environment. - let constraints: FxHashMap<&PackageName, Vec<&Requirement>> = constraints - .iter() - .filter(|constraint| constraint.requirement.evaluate_markers(Some(markers), &[])) - .fold(FxHashMap::default(), |mut constraints, constraint| { + // First, map all unnamed requirements to named requirements. + let requirements = { + let mut named = Vec::with_capacity(requirements.len()); + for requirement in requirements { + match &requirement.requirement { + UnresolvedRequirement::Named(requirement) => { + named.push(Cow::Borrowed(requirement)); + } + UnresolvedRequirement::Unnamed(requirement) => { + match self.get_urls(requirement.url.verbatim.raw()).as_slice() { + [] => { + return Ok(SatisfiesResult::Unsatisfied( + requirement.url.verbatim.raw().to_string(), + )) + } + [distribution] => { + let requirement = uv_pep508::Requirement { + name: distribution.name().clone(), + version_or_url: Some(VersionOrUrl::Url( + requirement.url.clone(), + )), + marker: requirement.marker, + extras: requirement.extras.clone(), + origin: requirement.origin.clone(), + }; + named.push(Cow::Owned(Requirement::from(requirement))); + } + _ => { + return Ok(SatisfiesResult::Unsatisfied( + requirement.url.verbatim.raw().to_string(), + )) + } + } + } + } + } + named + }; + + // Second, map all overrides to named requirements. We assume that all overrides are + // relevant. + let overrides = { + let mut named = Vec::with_capacity(overrides.len()); + for requirement in overrides { + match &requirement.requirement { + UnresolvedRequirement::Named(requirement) => { + named.push(Cow::Borrowed(requirement)); + } + UnresolvedRequirement::Unnamed(requirement) => { + match self.get_urls(requirement.url.verbatim.raw()).as_slice() { + [] => { + return Ok(SatisfiesResult::Unsatisfied( + requirement.url.verbatim.raw().to_string(), + )) + } + [distribution] => { + let requirement = uv_pep508::Requirement { + name: distribution.name().clone(), + version_or_url: Some(VersionOrUrl::Url( + requirement.url.clone(), + )), + marker: requirement.marker, + extras: requirement.extras.clone(), + origin: requirement.origin.clone(), + }; + named.push(Cow::Owned(Requirement::from(requirement))); + } + _ => { + return Ok(SatisfiesResult::Unsatisfied( + requirement.url.verbatim.raw().to_string(), + )) + } + } + } + } + } + named + }; + + self.satisfies_requirements( + requirements.iter().map(Cow::as_ref), + constraints.iter().map(|constraint| &constraint.requirement), + overrides.iter().map(Cow::as_ref), + markers, + ) + } + + /// Like [`SitePackages::satisfies_spec`], but with resolved names for all requirements. + pub fn satisfies_requirements<'a>( + &self, + requirements: impl ExactSizeIterator, + constraints: impl Iterator, + overrides: impl Iterator, + markers: &ResolverMarkerEnvironment, + ) -> Result { + // Collect the constraints and overrides by package name. + let constraints: FxHashMap<&PackageName, Vec<&Requirement>> = + constraints.fold(FxHashMap::default(), |mut constraints, constraint| { constraints - .entry(&constraint.requirement.name) + .entry(&constraint.name) .or_default() - .push(&constraint.requirement); + .push(constraint); constraints }); + let overrides: FxHashMap<&PackageName, Vec<&Requirement>> = + overrides.fold(FxHashMap::default(), |mut overrides, r#override| { + overrides + .entry(&r#override.name) + .or_default() + .push(r#override); + overrides + }); let mut stack = Vec::with_capacity(requirements.len()); let mut seen = FxHashSet::with_capacity_and_hasher(requirements.len(), FxBuildHasher); // Add the direct requirements to the queue. - for entry in requirements { - if entry.requirement.evaluate_markers(Some(markers), &[]) { - if seen.insert(entry.clone()) { - stack.push(entry.clone()); - } - } - } - - // Verify that all non-editable requirements are met. - while let Some(entry) = stack.pop() { - let installed = match &entry.requirement { - UnresolvedRequirement::Named(requirement) => self.get_packages(&requirement.name), - UnresolvedRequirement::Unnamed(requirement) => { - self.get_urls(requirement.url.verbatim.raw()) - } - }; - match installed.as_slice() { - [] => { - // The package isn't installed. - return Ok(SatisfiesResult::Unsatisfied(entry.requirement.to_string())); - } - [distribution] => { - match RequirementSatisfaction::check( - distribution, - entry.requirement.source().as_ref(), - )? { - RequirementSatisfaction::Mismatch | RequirementSatisfaction::OutOfDate => { - return Ok(SatisfiesResult::Unsatisfied(entry.requirement.to_string())) - } - RequirementSatisfaction::Satisfied => {} - } - - // Validate that the installed version satisfies the constraints. - for constraint in constraints.get(&distribution.name()).into_iter().flatten() { - match RequirementSatisfaction::check(distribution, &constraint.source)? { - RequirementSatisfaction::Mismatch - | RequirementSatisfaction::OutOfDate => { - return Ok(SatisfiesResult::Unsatisfied( - entry.requirement.to_string(), - )) - } - RequirementSatisfaction::Satisfied => {} - } - } - - // Recurse into the dependencies. - let metadata = distribution - .metadata() - .with_context(|| format!("Failed to read metadata for: {distribution}"))?; - - // Add the dependencies to the queue. - for dependency in metadata.requires_dist { - if dependency.evaluate_markers(markers, entry.requirement.extras()) { - let dependency = UnresolvedRequirementSpecification { - requirement: UnresolvedRequirement::Named(Requirement::from( - dependency, - )), - hashes: vec![], - }; - if seen.insert(dependency.clone()) { - stack.push(dependency); - } - } - } - } - _ => { - // There are multiple installed distributions for the same package. - return Ok(SatisfiesResult::Unsatisfied(entry.requirement.to_string())); - } - } - } - - Ok(SatisfiesResult::Fresh { - recursive_requirements: seen, - }) - } - - /// Like [`SitePackages::satisfies`], but with resolved names for all requirements. - pub fn satisfies_names( - &self, - requirements: &[NameRequirementSpecification], - constraints: &[NameRequirementSpecification], - overrides: &[NameRequirementSpecification], - markers: &ResolverMarkerEnvironment, - ) -> Result { - // Collect the constraints and overrides by package name. - let constraints: FxHashMap<&PackageName, Vec<&Requirement>> = - constraints - .iter() - .fold(FxHashMap::default(), |mut constraints, constraint| { - constraints - .entry(&constraint.requirement.name) - .or_default() - .push(&constraint.requirement); - constraints - }); - let overrides: FxHashMap<&PackageName, Vec<&Requirement>> = - overrides - .iter() - .fold(FxHashMap::default(), |mut overrides, r#override| { - overrides - .entry(&r#override.requirement.name) - .or_default() - .push(&r#override.requirement); - overrides - }); - - let mut stack = Vec::with_capacity(requirements.len()); - let mut seen = FxHashSet::with_capacity_and_hasher(requirements.len(), FxBuildHasher); - - // Add the direct requirements to the queue. - for entry in requirements { - if let Some(r#overrides) = overrides.get(&entry.requirement.name) { - for r#override in r#overrides { - if r#override.evaluate_markers(Some(markers), &[]) { - let entry = NameRequirementSpecification::from((*r#override).clone()); - if seen.insert(entry.clone()) { - stack.push(entry); + for requirement in requirements { + if let Some(r#overrides) = overrides.get(&requirement.name) { + for dependency in r#overrides { + if dependency.evaluate_markers(Some(markers), &[]) { + if seen.insert((*dependency).clone()) { + stack.push(Cow::Borrowed(*dependency)); } } } } else { - if entry.requirement.evaluate_markers(Some(markers), &[]) { - if seen.insert(entry.clone()) { - stack.push(entry.clone()); + if requirement.evaluate_markers(Some(markers), &[]) { + if seen.insert(requirement.clone()) { + stack.push(Cow::Borrowed(requirement)); } } } } // Verify that all non-editable requirements are met. - while let Some(entry) = stack.pop() { - let name = &entry.requirement.name; + while let Some(requirement) = stack.pop() { + let name = &requirement.name; let installed = self.get_packages(name); match installed.as_slice() { [] => { // The package isn't installed. - return Ok(SatisfiesResult::Unsatisfied(entry.requirement.to_string())); + return Ok(SatisfiesResult::Unsatisfied(requirement.to_string())); } [distribution] => { // Validate that the requirement is satisfied. - if entry.requirement.evaluate_markers(Some(markers), &[]) { - match RequirementSatisfaction::check( - distribution, - &entry.requirement.source, - )? { + if requirement.evaluate_markers(Some(markers), &[]) { + match RequirementSatisfaction::check(distribution, &requirement.source)? { RequirementSatisfaction::Mismatch | RequirementSatisfaction::OutOfDate => { - return Ok(SatisfiesResult::Unsatisfied( - entry.requirement.to_string(), - )) + return Ok(SatisfiesResult::Unsatisfied(requirement.to_string())) } RequirementSatisfaction::Satisfied => {} } @@ -467,7 +454,7 @@ impl SitePackages { RequirementSatisfaction::Mismatch | RequirementSatisfaction::OutOfDate => { return Ok(SatisfiesResult::Unsatisfied( - entry.requirement.to_string(), + requirement.to_string(), )) } RequirementSatisfaction::Satisfied => {} @@ -485,22 +472,16 @@ impl SitePackages { let dependency = Requirement::from(dependency); if let Some(r#overrides) = overrides.get(&dependency.name) { for dependency in r#overrides { - if dependency - .evaluate_markers(Some(markers), &entry.requirement.extras) - { - let dependency = - NameRequirementSpecification::from((*dependency).clone()); - if seen.insert(dependency.clone()) { - stack.push(dependency); + if dependency.evaluate_markers(Some(markers), &requirement.extras) { + if seen.insert((*dependency).clone()) { + stack.push(Cow::Borrowed(*dependency)); } } } } else { - if dependency.evaluate_markers(Some(markers), &entry.requirement.extras) - { - let dependency = NameRequirementSpecification::from(dependency); + if dependency.evaluate_markers(Some(markers), &requirement.extras) { if seen.insert(dependency.clone()) { - stack.push(dependency); + stack.push(Cow::Owned(dependency)); } } } @@ -508,13 +489,13 @@ impl SitePackages { } _ => { // There are multiple installed distributions for the same package. - return Ok(SatisfiesResult::Unsatisfied(entry.requirement.to_string())); + return Ok(SatisfiesResult::Unsatisfied(requirement.to_string())); } } } Ok(SatisfiesResult::Fresh { - recursive_requirements: FxHashSet::default(), + recursive_requirements: seen, }) } } @@ -525,7 +506,7 @@ pub enum SatisfiesResult { /// All requirements are recursively satisfied. Fresh { /// The flattened set (transitive closure) of all requirements checked. - recursive_requirements: FxHashSet, + recursive_requirements: FxHashSet, }, /// We found an unsatisfied requirement. Since we exit early, we only know about the first /// unsatisfied requirement. diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index 07f6a2c41..f9a599cae 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -239,10 +239,9 @@ pub(crate) async fn pip_install( if reinstall.is_none() && upgrade.is_none() && source_trees.is_empty() - && overrides.is_empty() && matches!(modifications, Modifications::Sufficient) { - match site_packages.satisfies(&requirements, &constraints, &marker_env)? { + match site_packages.satisfies_spec(&requirements, &constraints, &overrides, &marker_env)? { // If the requirements are already satisfied, we're done. SatisfiesResult::Fresh { recursive_requirements, @@ -250,7 +249,7 @@ pub(crate) async fn pip_install( if enabled!(Level::DEBUG) { for requirement in recursive_requirements .iter() - .map(|entry| entry.requirement.to_string()) + .map(ToString::to_string) .sorted() { debug!("Requirement satisfied: {requirement}"); diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 0ecdc6e64..89d091614 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -2009,8 +2009,8 @@ pub(crate) async fn update_environment( // Check if the current environment satisfies the requirements let site_packages = SitePackages::from_environment(&venv)?; - if source_trees.is_empty() && reinstall.is_none() && upgrade.is_none() && overrides.is_empty() { - match site_packages.satisfies(&requirements, &constraints, &marker_env)? { + if source_trees.is_empty() && reinstall.is_none() && upgrade.is_none() { + match site_packages.satisfies_spec(&requirements, &constraints, &overrides, &marker_env)? { // If the requirements are already satisfied, we're done. SatisfiesResult::Fresh { recursive_requirements, @@ -2022,7 +2022,7 @@ pub(crate) async fn update_environment( "All requirements satisfied: {}", recursive_requirements .iter() - .map(|entry| entry.requirement.to_string()) + .map(ToString::to_string) .sorted() .join(" | ") ); diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 29ca48300..5df9cbf68 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -1084,9 +1084,10 @@ fn can_skip_ephemeral( return false; } - match site_packages.satisfies( + match site_packages.satisfies_spec( &spec.requirements, &spec.constraints, + &spec.overrides, &base_interpreter.resolver_marker_environment(), ) { // If the requirements are already satisfied, we're done. @@ -1097,7 +1098,7 @@ fn can_skip_ephemeral( "Base environment satisfies requirements: {}", recursive_requirements .iter() - .map(|entry| entry.requirement.to_string()) + .map(ToString::to_string) .sorted() .join(" | ") ); diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 973f5f992..d886487d9 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -814,28 +814,11 @@ async fn get_or_create_environment( { // Check if the installed packages meet the requirements. let site_packages = SitePackages::from_environment(&environment)?; - - let requirements = requirements - .iter() - .cloned() - .map(NameRequirementSpecification::from) - .collect::>(); - let constraints = constraints - .iter() - .cloned() - .map(NameRequirementSpecification::from) - .collect::>(); - let overrides = overrides - .iter() - .cloned() - .map(NameRequirementSpecification::from) - .collect::>(); - if matches!( - site_packages.satisfies_names( - &requirements, - &constraints, - &overrides, + site_packages.satisfies_requirements( + requirements.iter(), + constraints.iter(), + overrides.iter(), &interpreter.resolver_marker_environment() ), Ok(SatisfiesResult::Fresh { .. })