mirror of
https://github.com/astral-sh/uv.git
synced 2025-09-26 12:09:12 +00:00
Only respect preferences across the same indexes (#9302)
Some checks are pending
CI / build binary | linux (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on linux (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on opensuse (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | python on macos x86_64 (push) Blocked by required conditions
CI / check system | python3.10 on windows (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / lint (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on linux (push) Blocked by required conditions
CI / check system | conda3.8 on linux (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / check system | conda3.11 on macos (push) Blocked by required conditions
CI / check system | conda3.8 on macos (push) Blocked by required conditions
CI / check system | conda3.11 on windows (push) Blocked by required conditions
CI / check system | conda3.8 on windows (push) Blocked by required conditions
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / typos (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / check system | embedded python3.10 on windows (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
Some checks are pending
CI / build binary | linux (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on linux (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on opensuse (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | python on macos x86_64 (push) Blocked by required conditions
CI / check system | python3.10 on windows (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / lint (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on linux (push) Blocked by required conditions
CI / check system | conda3.8 on linux (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / check system | conda3.11 on macos (push) Blocked by required conditions
CI / check system | conda3.8 on macos (push) Blocked by required conditions
CI / check system | conda3.11 on windows (push) Blocked by required conditions
CI / check system | conda3.8 on windows (push) Blocked by required conditions
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / typos (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / check system | embedded python3.10 on windows (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
## Summary The issue here is fairly complex. Consider the following: ```toml [project] name = "project" version = "0.1.0" requires-python = ">=3.12.0" dependencies = [] [project.optional-dependencies] cpu = [ "torch>=2.5.1", "torchvision>=0.20.1", ] cu124 = [ "torch>=2.5.1", "torchvision>=0.20.1", ] [tool.uv] conflicts = [ [ { extra = "cpu" }, { extra = "cu124" }, ], ] [tool.uv.sources] torch = [ { index = "pytorch-cpu", extra = "cpu", marker = "platform_system != 'Darwin'" }, ] torchvision = [ { index = "pytorch-cpu", extra = "cpu", marker = "platform_system != 'Darwin'" }, ] [[tool.uv.index]] name = "pytorch-cpu" url = "https://download.pytorch.org/whl/cpu" explicit = true ``` When solving this project, we first pick a PyTorch version from PyPI, to solve the `cu124` extra, selecting `2.5.1`. Later, we try to solve the `cpu` extra. In solving that extra, we look at the PyTorch CPU index. Ideally, we'd select `2.5.1+cpu`... But `2.5.1` is already a preference. So we choose that. Now, we only respect preferences for explicit indexes if they came from the same index. Closes https://github.com/astral-sh/uv/issues/9295.
This commit is contained in:
parent
c6482dd038
commit
5e48819dbb
9 changed files with 129 additions and 64 deletions
|
@ -4,7 +4,7 @@ use std::fmt::{Display, Formatter};
|
|||
use tracing::{debug, trace};
|
||||
|
||||
use uv_configuration::IndexStrategy;
|
||||
use uv_distribution_types::{CompatibleDist, IncompatibleDist, IncompatibleSource};
|
||||
use uv_distribution_types::{CompatibleDist, IncompatibleDist, IncompatibleSource, IndexUrl};
|
||||
use uv_distribution_types::{DistributionMetadata, IncompatibleWheel, Name, PrioritizedDist};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::Version;
|
||||
|
@ -80,11 +80,12 @@ impl CandidateSelector {
|
|||
preferences: &'a Preferences,
|
||||
installed_packages: &'a InstalledPackages,
|
||||
exclusions: &'a Exclusions,
|
||||
index: Option<&'a IndexUrl>,
|
||||
env: &ResolverEnvironment,
|
||||
) -> Option<Candidate<'a>> {
|
||||
let is_excluded = exclusions.contains(package_name);
|
||||
|
||||
// Check for a preference from a lockfile or a previous fork that satisfies the range and
|
||||
// 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,
|
||||
|
@ -93,6 +94,7 @@ impl CandidateSelector {
|
|||
preferences,
|
||||
installed_packages,
|
||||
is_excluded,
|
||||
index,
|
||||
env,
|
||||
) {
|
||||
trace!("Using preference {} {}", preferred.name, preferred.version);
|
||||
|
@ -131,23 +133,39 @@ impl CandidateSelector {
|
|||
preferences: &'a Preferences,
|
||||
installed_packages: &'a InstalledPackages,
|
||||
is_excluded: bool,
|
||||
index: Option<&'a IndexUrl>,
|
||||
env: &ResolverEnvironment,
|
||||
) -> 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.
|
||||
let preferences_match = preferences.get(package_name).filter(|(marker, _version)| {
|
||||
// `.unwrap_or(true)` because the universal marker is considered matching.
|
||||
marker
|
||||
.map(|marker| env.included_by_marker(marker))
|
||||
.unwrap_or(true)
|
||||
});
|
||||
let preferences_mismatch = preferences.get(package_name).filter(|(marker, _version)| {
|
||||
marker
|
||||
.map(|marker| !env.included_by_marker(marker))
|
||||
.unwrap_or(false)
|
||||
});
|
||||
let preferences_match =
|
||||
preferences
|
||||
.get(package_name)
|
||||
.filter(|(marker, _index, _version)| {
|
||||
// `.unwrap_or(true)` because the universal marker is considered matching.
|
||||
marker
|
||||
.map(|marker| env.included_by_marker(marker))
|
||||
.unwrap_or(true)
|
||||
});
|
||||
let preferences_mismatch =
|
||||
preferences
|
||||
.get(package_name)
|
||||
.filter(|(marker, _index, _version)| {
|
||||
marker
|
||||
.map(|marker| !env.included_by_marker(marker))
|
||||
.unwrap_or(false)
|
||||
});
|
||||
let preferences = preferences_match.chain(preferences_mismatch).filter_map(
|
||||
|(marker, source, version)| {
|
||||
// If the package is mapped to an explicit index, only consider preferences that
|
||||
// match the index.
|
||||
index
|
||||
.map_or(true, |index| source == Some(index))
|
||||
.then_some((marker, version))
|
||||
},
|
||||
);
|
||||
self.get_preferred_from_iter(
|
||||
preferences_match.chain(preferences_mismatch),
|
||||
preferences,
|
||||
package_name,
|
||||
range,
|
||||
version_maps,
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
use tracing::trace;
|
||||
|
||||
use uv_distribution_types::{InstalledDist, InstalledMetadata, InstalledVersion, Name};
|
||||
use uv_distribution_types::{IndexUrl, InstalledDist, InstalledMetadata, InstalledVersion, Name};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::{Operator, Version};
|
||||
use uv_pep508::{MarkerTree, VersionOrUrl};
|
||||
use uv_pypi_types::{HashDigest, HashError};
|
||||
use uv_requirements_txt::{RequirementEntry, RequirementsTxtRequirement};
|
||||
|
||||
use crate::ResolverEnvironment;
|
||||
use crate::{LockError, ResolverEnvironment};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum PreferenceError {
|
||||
|
@ -25,6 +26,8 @@ pub struct Preference {
|
|||
version: Version,
|
||||
/// The markers on the requirement itself (those after the semicolon).
|
||||
marker: MarkerTree,
|
||||
/// The index URL of the package, if any.
|
||||
index: Option<IndexUrl>,
|
||||
/// If coming from a package with diverging versions, the markers of the forks this preference
|
||||
/// is part of, otherwise `None`.
|
||||
fork_markers: Vec<MarkerTree>,
|
||||
|
@ -60,6 +63,7 @@ impl Preference {
|
|||
marker: requirement.marker,
|
||||
// requirements.txt doesn't have fork annotations.
|
||||
fork_markers: vec![],
|
||||
index: None,
|
||||
hashes: entry
|
||||
.hashes
|
||||
.iter()
|
||||
|
@ -79,6 +83,7 @@ impl Preference {
|
|||
name: dist.name().clone(),
|
||||
version: version.clone(),
|
||||
marker: MarkerTree::TRUE,
|
||||
index: None,
|
||||
// Installed distributions don't have fork annotations.
|
||||
fork_markers: vec![],
|
||||
hashes: Vec::new(),
|
||||
|
@ -86,14 +91,18 @@ impl Preference {
|
|||
}
|
||||
|
||||
/// Create a [`Preference`] from a locked distribution.
|
||||
pub fn from_lock(package: &crate::lock::Package) -> Self {
|
||||
Self {
|
||||
pub fn from_lock(
|
||||
package: &crate::lock::Package,
|
||||
install_path: &Path,
|
||||
) -> Result<Self, LockError> {
|
||||
Ok(Self {
|
||||
name: package.id.name.clone(),
|
||||
version: package.id.version.clone(),
|
||||
marker: MarkerTree::TRUE,
|
||||
index: package.index(install_path)?,
|
||||
fork_markers: package.fork_markers().to_vec(),
|
||||
hashes: Vec::new(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the [`PackageName`] of the package for this [`Preference`].
|
||||
|
@ -107,6 +116,13 @@ impl Preference {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Entry {
|
||||
marker: Option<MarkerTree>,
|
||||
index: Option<IndexUrl>,
|
||||
pin: Pin,
|
||||
}
|
||||
|
||||
/// A set of pinned packages that should be preserved during resolution, if possible.
|
||||
///
|
||||
/// The marker is the marker of the fork that resolved to the pin, if any.
|
||||
|
@ -114,15 +130,15 @@ impl Preference {
|
|||
/// Preferences should be prioritized first by whether their marker matches and then by the order
|
||||
/// they are stored, so that a lockfile has higher precedence than sibling forks.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Preferences(FxHashMap<PackageName, Vec<(Option<MarkerTree>, Pin)>>);
|
||||
pub struct Preferences(FxHashMap<PackageName, Vec<Entry>>);
|
||||
|
||||
impl Preferences {
|
||||
/// Create a map of pinned packages from an iterator of [`Preference`] entries.
|
||||
///
|
||||
/// The provided [`ResolverEnvironment`] will be used to filter the preferences
|
||||
/// to an applicable subset.
|
||||
pub fn from_iter<PreferenceIterator: IntoIterator<Item = Preference>>(
|
||||
preferences: PreferenceIterator,
|
||||
pub fn from_iter(
|
||||
preferences: impl IntoIterator<Item = Preference>,
|
||||
env: &ResolverEnvironment,
|
||||
) -> Self {
|
||||
let mut slf = Self::default();
|
||||
|
@ -152,6 +168,7 @@ impl Preferences {
|
|||
if preference.fork_markers.is_empty() {
|
||||
slf.insert(
|
||||
preference.name,
|
||||
preference.index,
|
||||
None,
|
||||
Pin {
|
||||
version: preference.version,
|
||||
|
@ -162,6 +179,7 @@ impl Preferences {
|
|||
for fork_marker in preference.fork_markers {
|
||||
slf.insert(
|
||||
preference.name.clone(),
|
||||
preference.index.clone(),
|
||||
Some(fork_marker),
|
||||
Pin {
|
||||
version: preference.version.clone(),
|
||||
|
@ -179,13 +197,15 @@ impl Preferences {
|
|||
pub(crate) fn insert(
|
||||
&mut self,
|
||||
package_name: PackageName,
|
||||
index: Option<IndexUrl>,
|
||||
markers: Option<MarkerTree>,
|
||||
pin: impl Into<Pin>,
|
||||
) {
|
||||
self.0
|
||||
.entry(package_name)
|
||||
.or_default()
|
||||
.push((markers, pin.into()));
|
||||
self.0.entry(package_name).or_default().push(Entry {
|
||||
marker: markers,
|
||||
index,
|
||||
pin: pin.into(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns an iterator over the preferences.
|
||||
|
@ -194,15 +214,19 @@ impl Preferences {
|
|||
) -> impl Iterator<
|
||||
Item = (
|
||||
&PackageName,
|
||||
impl Iterator<Item = (Option<&MarkerTree>, &Version)>,
|
||||
impl Iterator<Item = (Option<&MarkerTree>, Option<&IndexUrl>, &Version)>,
|
||||
),
|
||||
> {
|
||||
self.0.iter().map(|(name, preferences)| {
|
||||
(
|
||||
name,
|
||||
preferences
|
||||
.iter()
|
||||
.map(|(markers, pin)| (markers.as_ref(), pin.version())),
|
||||
preferences.iter().map(|entry| {
|
||||
(
|
||||
entry.marker.as_ref(),
|
||||
entry.index.as_ref(),
|
||||
entry.pin.version(),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -211,12 +235,14 @@ impl Preferences {
|
|||
pub(crate) fn get(
|
||||
&self,
|
||||
package_name: &PackageName,
|
||||
) -> impl Iterator<Item = (Option<&MarkerTree>, &Version)> {
|
||||
self.0
|
||||
.get(package_name)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|(markers, pin)| (markers.as_ref(), pin.version()))
|
||||
) -> impl Iterator<Item = (Option<&MarkerTree>, Option<&IndexUrl>, &Version)> {
|
||||
self.0.get(package_name).into_iter().flatten().map(|entry| {
|
||||
(
|
||||
entry.marker.as_ref(),
|
||||
entry.index.as_ref(),
|
||||
entry.pin.version(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the hashes for a package, if the version matches that of the pin.
|
||||
|
@ -229,8 +255,8 @@ impl Preferences {
|
|||
.get(package_name)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.find(|(_markers, pin)| pin.version() == version)
|
||||
.map(|(_markers, pin)| pin.hashes())
|
||||
.find(|entry| entry.pin.version() == version)
|
||||
.map(|entry| entry.pin.hashes())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -381,6 +381,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
for (package, version) in &resolution.nodes {
|
||||
preferences.insert(
|
||||
package.name.clone(),
|
||||
package.index.clone(),
|
||||
resolution.env.try_markers().cloned(),
|
||||
version.clone(),
|
||||
);
|
||||
|
@ -669,14 +670,15 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
diverging_packages: &'a [PackageName],
|
||||
) -> impl Iterator<Item = Result<ForkState, ResolveError>> + 'a {
|
||||
debug!(
|
||||
"Splitting resolution on {}=={} over {} into {} resolution with separate markers",
|
||||
"Splitting resolution on {}=={} over {} into {} resolution{} with separate markers",
|
||||
current_state.next,
|
||||
version,
|
||||
diverging_packages
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.join(", "),
|
||||
forks.len()
|
||||
forks.len(),
|
||||
if forks.len() == 1 { "" } else { "s" }
|
||||
);
|
||||
assert!(forks.len() >= 2);
|
||||
// This is a somewhat tortured technique to ensure
|
||||
|
@ -1075,6 +1077,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
preferences,
|
||||
&self.installed_packages,
|
||||
&self.exclusions,
|
||||
index,
|
||||
env,
|
||||
) else {
|
||||
// Short circuit: we couldn't find _any_ versions for a package.
|
||||
|
@ -1934,6 +1937,7 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
|
|||
&self.preferences,
|
||||
&self.installed_packages,
|
||||
&self.exclusions,
|
||||
None,
|
||||
&env,
|
||||
) else {
|
||||
return Ok(None);
|
||||
|
|
|
@ -45,7 +45,7 @@ impl AllowedYanks {
|
|||
allowed_yanks
|
||||
.entry(name.clone())
|
||||
.or_default()
|
||||
.extend(preferences.map(|(_markers, version)| version.clone()));
|
||||
.extend(preferences.map(|(.., version)| version.clone()));
|
||||
}
|
||||
|
||||
Self(Arc::new(allowed_yanks))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue