Skip to content

Commit f9aca63

Browse files
committed
Stable order for virtual packages
uv gives priorities to packages by package name, not by virtual package (`PubGrubPackage`). pubgrub otoh when prioritizing order the virtual packages. When the order of virtual packages changes, uv changes its resolutions and error messages. This means uv was depending on implementation details of pubgrub's prioritization caching. This broke with pubgrub-rs/pubgrub#299, which added a tiebreaker term that made pubgrub's sorting deterministic given a deterministic ordering of allocating the packages (which happens the first time pubgrub sees a package). The new custom tiebreaker decreases the difference to upstream pubgrub.
1 parent e65a273 commit f9aca63

File tree

7 files changed

+156
-47
lines changed

7 files changed

+156
-47
lines changed

Cargo.lock

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ petgraph = { version = "0.6.5" }
130130
platform-info = { version = "2.0.3" }
131131
proc-macro2 = { version = "1.0.86" }
132132
procfs = { version = "0.17.0", default-features = false, features = ["flate2"] }
133-
pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "05e8d12cea8d72c6d2d017900e478d0abd28fef4" }
133+
pubgrub = { path = "../pubgrub" }
134+
#pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "05e8d12cea8d72c6d2d017900e478d0abd28fef4" }
134135
quote = { version = "1.0.37" }
135136
rayon = { version = "1.10.0" }
136137
reflink-copy = { version = "0.1.19" }
@@ -175,7 +176,8 @@ unicode-width = { version = "0.1.13" }
175176
unscanny = { version = "0.1.0" }
176177
url = { version = "2.5.2", features = ["serde"] }
177178
urlencoding = { version = "2.1.3" }
178-
version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "05e8d12cea8d72c6d2d017900e478d0abd28fef4" }
179+
version-ranges = { path = "../pubgrub/version-ranges" }
180+
#version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "05e8d12cea8d72c6d2d017900e478d0abd28fef4" }
179181
walkdir = { version = "2.5.0" }
180182
which = { version = "7.0.0", features = ["regex"] }
181183
windows-registry = { version = "0.3.0" }

crates/uv-resolver/src/dependency_provider.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ impl DependencyProvider for UvDependencyProvider {
1717
type V = Version;
1818
type VS = Range<Version>;
1919
type M = UnavailableReason;
20-
type Priority = Option<PubGrubPriority>;
20+
/// Main priority and tiebreak for virtual packages
21+
type Priority = (Option<PubGrubPriority>, u32);
2122
type Err = Infallible;
2223

2324
fn prioritize(

crates/uv-resolver/src/pubgrub/priority.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ use crate::pubgrub::PubGrubPackageInner;
2020
///
2121
/// See: <https://github.com/pypa/pip/blob/ef78c129b1a966dbbbdb8ebfffc43723e89110d1/src/pip/_internal/resolution/resolvelib/provider.py#L120>
2222
#[derive(Clone, Debug, Default)]
23-
pub(crate) struct PubGrubPriorities(FxHashMap<PackageName, PubGrubPriority>);
23+
pub(crate) struct PubGrubPriorities(
24+
FxHashMap<PackageName, PubGrubPriority>,
25+
FxHashMap<PubGrubPackage, u32>,
26+
);
2427

2528
impl PubGrubPriorities {
2629
/// Add a [`PubGrubPackage`] to the priority map.
@@ -30,6 +33,13 @@ impl PubGrubPriorities {
3033
version: &Range<Version>,
3134
urls: &ForkUrls,
3235
) {
36+
if !self.1.contains_key(&package) {
37+
self.1.insert(
38+
package.clone(),
39+
u32::try_from(self.1.len()).expect("Less than 2**32 packages"),
40+
);
41+
}
42+
3343
let next = self.0.len();
3444
// The root package and Python constraints have no explicit priority, the root package is
3545
// always first and the Python version (range) is fixed.
@@ -92,15 +102,17 @@ impl PubGrubPriorities {
92102
}
93103

94104
/// Return the [`PubGrubPriority`] of the given package, if it exists.
95-
pub(crate) fn get(&self, package: &PubGrubPackage) -> Option<PubGrubPriority> {
96-
match &**package {
105+
pub(crate) fn get(&self, package: &PubGrubPackage) -> (Option<PubGrubPriority>, u32) {
106+
let main_priority = match &**package {
97107
PubGrubPackageInner::Root(_) => Some(PubGrubPriority::Root),
98108
PubGrubPackageInner::Python(_) => Some(PubGrubPriority::Root),
99109
PubGrubPackageInner::Marker { name, .. } => self.0.get(name).copied(),
100110
PubGrubPackageInner::Extra { name, .. } => self.0.get(name).copied(),
101111
PubGrubPackageInner::Dev { name, .. } => self.0.get(name).copied(),
102112
PubGrubPackageInner::Package { name, .. } => self.0.get(name).copied(),
103-
}
113+
};
114+
let tiebreaker = self.1.get(&package).copied().unwrap_or_default();
115+
(main_priority, tiebreaker)
104116
}
105117

106118
/// Mark a package as prioritized by setting it to [`PubGrubPriority::ConflictEarly`], if it

crates/uv/tests/it/lock_conflict.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,8 @@ fn extra_basic_three_extras() -> Result<()> {
250250
251251
----- stderr -----
252252
× No solution found when resolving dependencies:
253-
╰─▶ Because project[project3] depends on sortedcontainers==2.4.0 and project[extra1] depends on sortedcontainers==2.2.0, we can conclude that project[extra1] and project[project3] are incompatible.
254-
And because your project requires project[extra1] and project[project3], we can conclude that your project's requirements are unsatisfiable.
253+
╰─▶ Because project[project3] depends on sortedcontainers==2.4.0 and project[extra2] depends on sortedcontainers==2.3.0, we can conclude that project[extra2] and project[project3] are incompatible.
254+
And because your project requires project[extra2] and project[project3], we can conclude that your project's requirements are unsatisfiable.
255255
"###);
256256

257257
// And now with the same extra configuration, we tell uv about
@@ -538,8 +538,8 @@ fn extra_multiple_not_conflicting2() -> Result<()> {
538538
539539
----- stderr -----
540540
× No solution found when resolving dependencies:
541-
╰─▶ Because project[project4] depends on sortedcontainers==2.4.0 and project[extra1] depends on sortedcontainers==2.3.0, we can conclude that project[extra1] and project[project4] are incompatible.
542-
And because your project requires project[extra1] and project[project4], we can conclude that your project's requirements are unsatisfiable.
541+
╰─▶ Because project[project4] depends on sortedcontainers==2.4.0 and project[project3] depends on sortedcontainers==2.3.0, we can conclude that project[project3] and project[project4] are incompatible.
542+
And because your project requires project[project3] and project[project4], we can conclude that your project's requirements are unsatisfiable.
543543
"###);
544544

545545
// If we define extra1/extra2 as conflicting and project3/project4
@@ -1289,10 +1289,8 @@ fn extra_nested_across_workspace() -> Result<()> {
12891289
12901290
----- stderr -----
12911291
× No solution found when resolving dependencies:
1292-
╰─▶ Because dummy[extra2] depends on proxy1[extra2] and only proxy1[extra2]==0.1.0 is available, we can conclude that dummy[extra2] depends on proxy1[extra2]==0.1.0. (1)
1293-
1294-
Because proxy1[extra2]==0.1.0 depends on anyio==4.2.0 and proxy1[extra1]==0.1.0 depends on anyio==4.1.0, we can conclude that proxy1[extra1]==0.1.0 and proxy1[extra2]==0.1.0 are incompatible.
1295-
And because we know from (1) that dummy[extra2] depends on proxy1[extra2]==0.1.0, we can conclude that dummy[extra2] and proxy1[extra1]==0.1.0 are incompatible.
1292+
╰─▶ Because dummy[extra2] depends on proxy1[extra2] and only proxy1[extra2]==0.1.0 is available, we can conclude that dummy[extra2] depends on proxy1[extra2]==0.1.0.
1293+
And because proxy1[extra2]==0.1.0 depends on anyio==4.2.0 and proxy1[extra1]==0.1.0 depends on anyio==4.1.0, we can conclude that proxy1[extra1]==0.1.0 and dummy[extra2] are incompatible.
12961294
And because only proxy1[extra1]==0.1.0 is available and dummysub[extra1] depends on proxy1[extra1], we can conclude that dummysub[extra1] and dummy[extra2] are incompatible.
12971295
And because your workspace requires dummy[extra2] and dummysub[extra1], we can conclude that your workspace's requirements are unsatisfiable.
12981296
"###);
@@ -1795,7 +1793,7 @@ fn mixed() -> Result<()> {
17951793
17961794
----- stderr -----
17971795
× No solution found when resolving dependencies:
1798-
╰─▶ Because project[extra1] depends on sortedcontainers==2.4.0 and project:group1 depends on sortedcontainers==2.3.0, we can conclude that project:group1 and project[extra1] are incompatible.
1796+
╰─▶ Because project:group1 depends on sortedcontainers==2.3.0 and project[extra1] depends on sortedcontainers==2.4.0, we can conclude that project[extra1] and project:group1 are incompatible.
17991797
And because your project requires project[extra1] and project:group1, we can conclude that your project's requirements are unsatisfiable.
18001798
"###);
18011799

0 commit comments

Comments
 (0)