Skip to content

Commit d35479f

Browse files
committed
uv-resolver: account for conflicting extras when forking
This commit implements the actual logic for creating new forks when there are dependencies matching declared conflicts.
1 parent c7add58 commit d35479f

File tree

2 files changed

+214
-19
lines changed

2 files changed

+214
-19
lines changed

crates/uv-resolver/src/resolver/environment.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
use std::sync::Arc;
22

3+
use rustc_hash::{FxHashMap, FxHashSet};
34
use uv_normalize::{ExtraName, PackageName};
45
use uv_pep508::{MarkerEnvironment, MarkerTree};
5-
use uv_pypi_types::{ConflictingGroupList, ResolverMarkerEnvironment};
6+
use uv_pypi_types::{
7+
ConflictingGroup, ConflictingGroupList, ConflictingGroupRef, ResolverMarkerEnvironment,
8+
};
69

710
use crate::pubgrub::{PubGrubDependency, PubGrubPackage};
811
use crate::requires_python::RequiresPythonRange;
@@ -95,6 +98,8 @@ enum Kind {
9598
initial_forks: Arc<[MarkerTree]>,
9699
/// The markers associated with this resolver fork.
97100
markers: MarkerTree,
101+
/// Conflicting group exclusions.
102+
exclude: Arc<FxHashMap<PackageName, FxHashSet<ExtraName>>>,
98103
},
99104
}
100105

@@ -132,6 +137,7 @@ impl ResolverEnvironment {
132137
let kind = Kind::Universal {
133138
initial_forks: initial_forks.into(),
134139
markers: MarkerTree::TRUE,
140+
exclude: Arc::new(FxHashMap::default()),
135141
};
136142
ResolverEnvironment { kind }
137143
}
@@ -157,6 +163,18 @@ impl ResolverEnvironment {
157163
}
158164
}
159165

166+
/// Returns true if the dependency represented by this forker may be
167+
/// included in the given resolver environment.
168+
pub(crate) fn included_by_group(&self, group: ConflictingGroupRef<'_>) -> bool {
169+
match self.kind {
170+
Kind::Specific { .. } => true,
171+
Kind::Universal { ref exclude, .. } => !exclude
172+
.get(group.package())
173+
.map(|set| set.contains(group.extra()))
174+
.unwrap_or(false),
175+
}
176+
}
177+
160178
/// Returns the bounding Python versions that can satisfy this
161179
/// resolver environment's marker, if it's constrained.
162180
pub(crate) fn requires_python(&self) -> Option<RequiresPythonRange> {
@@ -183,12 +201,56 @@ impl ResolverEnvironment {
183201
Kind::Universal {
184202
ref initial_forks,
185203
markers: ref lhs,
204+
ref exclude,
186205
} => {
187206
let mut markers = lhs.clone();
188207
markers.and(rhs);
189208
let kind = Kind::Universal {
190209
initial_forks: Arc::clone(initial_forks),
191210
markers,
211+
exclude: Arc::clone(exclude),
212+
};
213+
ResolverEnvironment { kind }
214+
}
215+
}
216+
}
217+
218+
/// Returns a new resolver environment with the given groups excluded from
219+
/// it.
220+
///
221+
/// When a group is excluded from a resolver environment,
222+
/// `ResolverEnvironment::included_by_group` will return false. The idea
223+
/// is that a dependency with a corresponding group should be excluded by
224+
/// forks in the resolver with this environment.
225+
///
226+
/// # Panics
227+
///
228+
/// This panics if the resolver environment corresponds to one and only one
229+
/// specific marker environment. i.e., "pip"-style resolution.
230+
pub(crate) fn exclude_by_group(
231+
&self,
232+
groups: impl IntoIterator<Item = ConflictingGroup>,
233+
) -> ResolverEnvironment {
234+
match self.kind {
235+
Kind::Specific { .. } => {
236+
unreachable!("environment narrowing only happens in universal resolution")
237+
}
238+
Kind::Universal {
239+
ref initial_forks,
240+
ref markers,
241+
ref exclude,
242+
} => {
243+
let mut exclude: FxHashMap<_, _> = (**exclude).clone();
244+
for group in groups {
245+
exclude
246+
.entry(group.package().clone())
247+
.or_default()
248+
.insert(group.extra().clone());
249+
}
250+
let kind = Kind::Universal {
251+
initial_forks: Arc::clone(initial_forks),
252+
markers: markers.clone(),
253+
exclude: Arc::new(exclude),
192254
};
193255
ResolverEnvironment { kind }
194256
}
@@ -207,6 +269,7 @@ impl ResolverEnvironment {
207269
let Kind::Universal {
208270
ref initial_forks,
209271
markers: ref _markers,
272+
exclude: ref _exclude,
210273
} = self.kind
211274
else {
212275
return vec![init];

crates/uv-resolver/src/resolver/mod.rs

Lines changed: 150 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ use uv_normalize::{ExtraName, GroupName, PackageName};
3737
use uv_pep440::{release_specifiers_to_ranges, Version, MIN_VERSION};
3838
use uv_pep508::MarkerTree;
3939
use uv_platform_tags::Tags;
40-
use uv_pypi_types::{ConflictingGroupList, Requirement, ResolutionMetadata, VerbatimParsedUrl};
40+
use uv_pypi_types::{
41+
ConflictingGroup, ConflictingGroupList, ConflictingGroupRef, Requirement, ResolutionMetadata,
42+
VerbatimParsedUrl,
43+
};
4144
use uv_types::{BuildContext, HashStrategy, InstalledPackagesProvider};
4245
use uv_warnings::warn_user_once;
4346

@@ -1560,6 +1563,11 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
15601563
) {
15611564
return None;
15621565
}
1566+
if !env.included_by_group(
1567+
ConflictingGroupRef::from((&requirement.name, source_extra)),
1568+
) {
1569+
return None;
1570+
}
15631571
}
15641572
None => {
15651573
if !requirement.evaluate_markers(env.marker_environment(), &[]) {
@@ -1648,6 +1656,11 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
16481656
) {
16491657
return None;
16501658
}
1659+
if !env.included_by_group(
1660+
ConflictingGroupRef::from((&requirement.name, source_extra)),
1661+
) {
1662+
return None;
1663+
}
16511664
}
16521665
None => {
16531666
if !constraint.evaluate_markers(env.marker_environment(), &[]) {
@@ -2740,10 +2753,7 @@ impl Forks {
27402753
) -> Forks {
27412754
let python_marker = python_requirement.to_marker_tree();
27422755

2743-
let mut forks = vec![Fork {
2744-
dependencies: vec![],
2745-
env: env.clone(),
2746-
}];
2756+
let mut forks = vec![Fork::new(env.clone())];
27472757
let mut diverging_packages = BTreeSet::new();
27482758
for (name, mut deps) in name_to_deps {
27492759
assert!(!deps.is_empty(), "every name has at least one dependency");
@@ -2775,7 +2785,7 @@ impl Forks {
27752785
let markers = dep.package.marker().cloned().unwrap_or(MarkerTree::TRUE);
27762786
for fork in &mut forks {
27772787
if fork.env.included_by_marker(&markers) {
2778-
fork.dependencies.push(dep.clone());
2788+
fork.add_dependency(dep.clone());
27792789
}
27802790
}
27812791
continue;
@@ -2793,7 +2803,7 @@ impl Forks {
27932803
// Or, if the markers are always true, then we just
27942804
// add the dependency to every fork unconditionally.
27952805
for fork in &mut forks {
2796-
fork.dependencies.push(dep.clone());
2806+
fork.add_dependency(dep.clone());
27972807
}
27982808
continue;
27992809
}
@@ -2818,7 +2828,7 @@ impl Forks {
28182828
// specifically created to exclude this dependency,
28192829
// so this isn't always true!
28202830
if forker.included(&new_fork.env) {
2821-
new_fork.dependencies.push(dep.clone());
2831+
new_fork.add_dependency(dep.clone());
28222832
}
28232833
// Filter out any forks we created that are disjoint with our
28242834
// Python requirement.
@@ -2830,6 +2840,62 @@ impl Forks {
28302840
forks = new;
28312841
}
28322842
}
2843+
// When there is a conflicting group configuration, we need
2844+
// to potentially add more forks. Each fork added contains an
2845+
// exclusion list of conflicting groups where dependencies with
2846+
// the corresponding package and extra name are forcefully
2847+
// excluded from that group.
2848+
//
2849+
// We specifically iterate on conflicting groups and
2850+
// potentially re-generate all forks for each one. We do it
2851+
// this way in case there are multiple sets of conflicting
2852+
// groups that impact the forks here.
2853+
//
2854+
// For example, if we have conflicting groups {x1, x2} and {x3,
2855+
// x4}, we need to make sure the forks generated from one set
2856+
// also account for the other set.
2857+
for groups in conflicting_groups.iter() {
2858+
let mut new = vec![];
2859+
for fork in std::mem::take(&mut forks) {
2860+
let mut has_conflicting_dependency = false;
2861+
for group in groups.iter() {
2862+
if fork.contains_conflicting_group(group.as_ref()) {
2863+
has_conflicting_dependency = true;
2864+
break;
2865+
}
2866+
}
2867+
if !has_conflicting_dependency {
2868+
new.push(fork);
2869+
continue;
2870+
}
2871+
2872+
// Create a fork that excludes ALL extras.
2873+
let mut fork_none = fork.clone();
2874+
for group in groups.iter() {
2875+
fork_none = fork_none.exclude([group.clone()]);
2876+
}
2877+
new.push(fork_none);
2878+
2879+
// Now create a fork for each conflicting group, where
2880+
// that fork excludes every *other* conflicting group.
2881+
//
2882+
// So if we have conflicting extras foo, bar and baz,
2883+
// then this creates three forks: one that excludes
2884+
// {foo, bar}, one that excludes {foo, baz} and one
2885+
// that excludes {bar, baz}.
2886+
for (i, _) in groups.iter().enumerate() {
2887+
let fork_allows_group = fork.clone().exclude(
2888+
groups
2889+
.iter()
2890+
.enumerate()
2891+
.filter(|&(j, _)| i != j)
2892+
.map(|(_, group)| group.clone()),
2893+
);
2894+
new.push(fork_allows_group);
2895+
}
2896+
}
2897+
forks = new;
2898+
}
28332899
Forks {
28342900
forks,
28352901
diverging_packages,
@@ -2846,7 +2912,7 @@ impl Forks {
28462912
/// have the same name and because the marker expressions are disjoint,
28472913
/// a fork occurs. One fork will contain `a<2` but not `a>=2`, while
28482914
/// the other fork will contain `a>=2` but not `a<2`.
2849-
#[derive(Clone, Debug, Eq, PartialEq)]
2915+
#[derive(Clone, Debug)]
28502916
struct Fork {
28512917
/// The list of dependencies for this fork, guaranteed to be conflict
28522918
/// free. (i.e., There are no two packages with the same name with
@@ -2857,6 +2923,12 @@ struct Fork {
28572923
/// it should be impossible for a package with a marker expression that is
28582924
/// disjoint from the marker expression on this fork to be added.
28592925
dependencies: Vec<PubGrubDependency>,
2926+
/// The conflicting groups in this fork.
2927+
///
2928+
/// This exists to make some access patterns more efficient. Namely,
2929+
/// it makes it easy to check whether there's a dependency with a
2930+
/// particular conflicting group in this fork.
2931+
conflicting_groups: FxHashMap<PackageName, FxHashSet<ExtraName>>,
28602932
/// The resolver environment for this fork.
28612933
///
28622934
/// Principally, this corresponds to the markers in this for. So in the
@@ -2872,26 +2944,86 @@ struct Fork {
28722944
}
28732945

28742946
impl Fork {
2875-
/*
2876-
fn intersect(&mut self, markers: MarkerTree) {
2877-
self.markers.and(markers);
2947+
/// Create a new fork with no dependencies with the given resolver
2948+
/// environment.
2949+
fn new(env: ResolverEnvironment) -> Fork {
2950+
Fork {
2951+
dependencies: vec![],
2952+
conflicting_groups: FxHashMap::default(),
2953+
env,
2954+
}
2955+
}
2956+
2957+
/// Add a dependency to this fork.
2958+
fn add_dependency(&mut self, dep: PubGrubDependency) {
2959+
if let Some(conflicting_group) = dep.package.conflicting_group() {
2960+
self.conflicting_groups
2961+
.entry(conflicting_group.package().clone())
2962+
.or_default()
2963+
.insert(conflicting_group.extra().clone());
2964+
}
2965+
self.dependencies.push(dep);
2966+
}
2967+
2968+
/// Sets the resolver environment to the one given.
2969+
///
2970+
/// Any dependency in this fork that does not satisfy the given environment
2971+
/// is removed.
2972+
fn set_env(&mut self, env: ResolverEnvironment) {
2973+
self.env = env;
28782974
self.dependencies.retain(|dep| {
28792975
let Some(markers) = dep.package.marker() else {
28802976
return true;
28812977
};
2882-
!self.markers.is_disjoint(markers)
2978+
if self.env.included_by_marker(markers) {
2979+
return true;
2980+
}
2981+
if let Some(conflicting_group) = dep.package.conflicting_group() {
2982+
if let Some(set) = self.conflicting_groups.get_mut(conflicting_group.package()) {
2983+
set.remove(conflicting_group.extra());
2984+
}
2985+
}
2986+
false
28832987
});
28842988
}
2885-
*/
28862989

2887-
fn set_env(&mut self, env: ResolverEnvironment) {
2888-
self.env = env;
2990+
/// Returns true if any of the dependencies in this fork contain a
2991+
/// dependency with the given package and extra values.
2992+
fn contains_conflicting_group(&self, group: ConflictingGroupRef<'_>) -> bool {
2993+
self.conflicting_groups
2994+
.get(group.package())
2995+
.map(|set| set.contains(group.extra()))
2996+
.unwrap_or(false)
2997+
}
2998+
2999+
/// Exclude the given groups from this fork.
3000+
///
3001+
/// This removes all dependencies matching the given conflicting groups.
3002+
fn exclude(mut self, groups: impl IntoIterator<Item = ConflictingGroup>) -> Fork {
3003+
self.env = self.env.exclude_by_group(groups);
28893004
self.dependencies.retain(|dep| {
2890-
let Some(markers) = dep.package.marker() else {
3005+
let Some(conflicting_group) = dep.package.conflicting_group() else {
28913006
return true;
28923007
};
2893-
self.env.included(markers)
3008+
if self.env.included_by_group(conflicting_group) {
3009+
return true;
3010+
}
3011+
if let Some(conflicting_group) = dep.package.conflicting_group() {
3012+
if let Some(set) = self.conflicting_groups.get_mut(conflicting_group.package()) {
3013+
set.remove(conflicting_group.extra());
3014+
}
3015+
}
3016+
false
28943017
});
3018+
self
3019+
}
3020+
}
3021+
3022+
impl Eq for Fork {}
3023+
3024+
impl PartialEq for Fork {
3025+
fn eq(&self, other: &Fork) -> bool {
3026+
self.dependencies == other.dependencies && self.env == other.env
28953027
}
28963028
}
28973029

0 commit comments

Comments
 (0)