Skip to content

Commit

Permalink
Use fork marker preferences in resolutions
Browse files Browse the repository at this point in the history
  • Loading branch information
konstin committed Jul 26, 2024
1 parent d31a01e commit ff4651c
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 104 deletions.
216 changes: 153 additions & 63 deletions crates/uv-resolver/src/candidate_selector.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use itertools::Itertools;
use pubgrub::range::Range;
use std::fmt::{Display, Formatter};
use tracing::debug;
use tracing::{debug, trace};

use distribution_types::{CompatibleDist, IncompatibleDist, IncompatibleSource};
use distribution_types::{DistributionMetadata, IncompatibleWheel, Name, PrioritizedDist};
use pep440_rs::Version;
use pep508_rs::MarkerEnvironment;
use pep508_rs::{MarkerEnvironment, MarkerTree};
use uv_configuration::IndexStrategy;
use uv_normalize::PackageName;
use uv_types::InstalledPackagesProvider;
Expand Down Expand Up @@ -66,9 +66,7 @@ impl CandidateSelector {
pub(crate) fn index_strategy(&self) -> &IndexStrategy {
&self.index_strategy
}
}

impl CandidateSelector {
/// Select a [`Candidate`] from a set of candidate versions and files.
///
/// Unless present in the provided [`Exclusions`], local distributions from the
Expand All @@ -84,6 +82,8 @@ impl CandidateSelector {
exclusions: &'a Exclusions,
markers: &ResolverMarkers,
) -> Option<Candidate<'a>> {
// Check for a preference from a lockfile or a previous fork that satisfies the range and
// is allowed.
if let Some(preferred) = self.get_preferred(
package_name,
range,
Expand All @@ -93,86 +93,179 @@ impl CandidateSelector {
exclusions,
markers,
) {
trace!("Using preference {} {}", preferred.name, preferred.version,);
return Some(preferred);
}

// Check for a locally installed distribution that satisfies the range and is allowed.
if !exclusions.contains(package_name) {
if let Some(installed) = Self::get_installed(package_name, range, installed_packages) {
trace!(
"Using preference {} {} from installed package",
installed.name,
installed.version,
);
return Some(installed);
}
}

self.select_no_preference(package_name, range, version_maps, markers)
}

/// Check for a preference (e.g., an existing version from an existing lockfile or
/// from a previous fork) that satisfies the current range.
/// If the package has a preference, an existing version from an existing lockfile or a version
/// from a sibling fork, and the preference satisfies the current range, use that.
///
/// We try to find a resolution that, depending on the input, does not diverge from the
/// lockfile or matches a sibling fork. We try an exact match for the current markers (fork
/// or specific) first, to ensure stability with repeated locking. If that doesn't work, we
/// fall back to preferences that don't match in hopes of still resolving different forks into
/// the same version; A solution with less different versions is more desirable than one where
/// we may have more recent version in some cases, but overall more versions.
fn get_preferred<'a, InstalledPackages: InstalledPackagesProvider>(
&self,
&'a self,
package_name: &'a PackageName,
range: &Range<Version>,
version_maps: &'a [VersionMap],
preferences: &'a Preferences,
installed_packages: &'a InstalledPackages,
exclusions: &'a Exclusions,
markers: &ResolverMarkers,
) -> Option<Candidate<'a>> {
let version = preferences.version(package_name)?;

// Respect the version range for this requirement.
if !range.contains(version) {
return None;
exclusions: &Exclusions,
resolver_markers: &ResolverMarkers,
) -> Option<Candidate> {
// In the branches, we "sort" the preferences by marker-matching through an iterator that
// first has the matching half and then the mismatching half.
match resolver_markers {
ResolverMarkers::SpecificEnvironment(env) => {
// We may hit a combination of fork markers preferences with specific environment
// output in the future when adding support for the PEP 665 successor.
let preferences_match =
preferences.get(package_name).filter(|(marker, _version)| {
// `.unwrap_or(true)` because the universal marker is considered matching.
marker
.map(|marker| marker.evaluate(env, &[]))
.unwrap_or(true)
});
let preferences_mismatch =
preferences.get(package_name).filter(|(marker, _version)| {
marker
.map(|marker| !marker.evaluate(env, &[]))
.unwrap_or(false)
});
self.get_preferred_from_iter(
preferences_match.chain(preferences_mismatch),
package_name,
range,
version_maps,
installed_packages,
exclusions,
resolver_markers,
)
}
ResolverMarkers::Universal { .. } => {
// In universal mode, all preferences are matching.
self.get_preferred_from_iter(
preferences.get(package_name),
package_name,
range,
version_maps,
installed_packages,
exclusions,
resolver_markers,
)
}
ResolverMarkers::Fork(fork_markers) => {
let preferences_match =
preferences.get(package_name).filter(|(marker, _version)| {
// `.unwrap_or(true)` because the universal marker is considered matching.
marker.map(|marker| marker == fork_markers).unwrap_or(true)
});
let preferences_mismatch =
preferences.get(package_name).filter(|(marker, _version)| {
marker.map(|marker| marker != fork_markers).unwrap_or(false)
});
self.get_preferred_from_iter(
preferences_match.chain(preferences_mismatch),
package_name,
range,
version_maps,
installed_packages,
exclusions,
resolver_markers,
)
}
}
}

// Check for a locally installed distribution that matches the preferred version.
if !exclusions.contains(package_name) {
let installed_dists = installed_packages.get_packages(package_name);
match installed_dists.as_slice() {
[] => {}
[dist] => {
if dist.version() == version {
debug!("Found installed version of {dist} that satisfies preference in {range}");

return Some(Candidate {
name: package_name,
version,
dist: CandidateDist::Compatible(CompatibleDist::InstalledDist(dist)),
choice_kind: VersionChoiceKind::Preference,
});
/// Return the first preference that satisfies the current range and is allowed.
fn get_preferred_from_iter<'a, InstalledPackages: InstalledPackagesProvider>(
&'a self,
preferences: impl Iterator<Item = (Option<&'a MarkerTree>, &'a Version)>,
package_name: &'a PackageName,
range: &Range<Version>,
version_maps: &'a [VersionMap],
installed_packages: &'a InstalledPackages,
exclusions: &Exclusions,
resolver_markers: &ResolverMarkers,
) -> Option<Candidate<'a>> {
for (_marker, version) in preferences {
// Respect the version range for this requirement.
if !range.contains(version) {
continue;
}

// Check for a locally installed distribution that matches the preferred version.
if !exclusions.contains(package_name) {
let installed_dists = installed_packages.get_packages(package_name);
match installed_dists.as_slice() {
[] => {}
[dist] => {
if dist.version() == version {
debug!("Found installed version of {dist} that satisfies preference in {range}");

return Some(Candidate {
name: package_name,
version,
dist: CandidateDist::Compatible(CompatibleDist::InstalledDist(
dist,
)),
choice_kind: VersionChoiceKind::Preference,
});
}
}
// We do not consider installed distributions with multiple versions because
// during installation these must be reinstalled from the remote
_ => {
debug!("Ignoring installed versions of {package_name}: multiple distributions found");
}
}
// We do not consider installed distributions with multiple versions because
// during installation these must be reinstalled from the remote
_ => {
debug!("Ignoring installed versions of {package_name}: multiple distributions found");
}
}
}

// Respect the pre-release strategy for this fork.
if version.any_prerelease()
&& self.prerelease_strategy.allows(package_name, markers) != AllowPreRelease::Yes
{
return None;
}
// Respect the pre-release strategy for this fork.
if version.any_prerelease()
&& self
.prerelease_strategy
.allows(package_name, resolver_markers)
!= AllowPreRelease::Yes
{
continue;
}

// Check for a remote distribution that matches the preferred version
if let Some(file) = version_maps
.iter()
.find_map(|version_map| version_map.get(version))
{
return Some(Candidate::new(
package_name,
version,
file,
VersionChoiceKind::Preference,
));
// Check for a remote distribution that matches the preferred version
if let Some(file) = version_maps
.iter()
.find_map(|version_map| version_map.get(version))
{
return Some(Candidate::new(
package_name,
version,
file,
VersionChoiceKind::Preference,
));
}
}

None
}

/// Check for a locally installed distribution that satisfies the range.
/// Check for an installed distribution that satisfies the current range and is allowed.
fn get_installed<'a, InstalledPackages: InstalledPackagesProvider>(
package_name: &'a PackageName,
range: &Range<Version>,
Expand Down Expand Up @@ -217,8 +310,8 @@ impl CandidateSelector {
version_maps: &'a [VersionMap],
markers: &ResolverMarkers,
) -> Option<Candidate> {
tracing::trace!(
"selecting candidate for package {package_name} with range {range:?} with {} remote versions",
trace!(
"Selecting candidate for {package_name} with range {range} with {} remote versions",
version_maps.iter().map(VersionMap::len).sum::<usize>(),
);
let highest = self.use_highest_version(package_name);
Expand Down Expand Up @@ -408,12 +501,9 @@ impl CandidateSelector {

return Some(candidate);
}
tracing::trace!(
"exhausted all candidates for package {:?} with range {:?} \
after {} steps",
package_name,
range,
steps,
trace!(
"Exhausted all candidates for package {package_name} with range {range} \
after {steps} steps",
);
match prerelease {
None => None,
Expand Down
4 changes: 4 additions & 0 deletions crates/uv-resolver/src/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,10 @@ impl Distribution {
&self.id.version
}

pub fn fork_markers(&self) -> Option<&BTreeSet<MarkerTree>> {
self.fork_markers.as_ref()
}

/// Returns a [`VersionId`] for this package that can be used for resolution.
fn version_id(&self, workspace_root: &Path) -> Result<VersionId, LockError> {
match &self.id.source {
Expand Down
Loading

0 comments on commit ff4651c

Please sign in to comment.