Skip to content

Commit 489f570

Browse files
committed
Auto merge of #5037 - Eh2406:conflict_tracking, r=alexcrichton
Conflict tracking This is an alternative implementation of #4834. This is slower but hopefully more flexible and clearer. The idea is to keep a list of `PackageId`'s that have caused us to skip a `Candidate`. Then we can use the list when we are backtracking if any items in our list have not been activated then we will have new `Candidate`'s to try so we should stop backtracking. Or to say that another way; We can continue backtracking as long as all the items in our list is still activated. Next this new framework was used to make the error messages more focused. We only need to list the versions that conflict, as opposed to all previously activated versions. Why is this more flexible? 1. It is not limited to conflicts within the same package. If `RemainingCandidates.next` skips something because of a links attribute, that is easy to record, just add the `PackageId` to the set `conflicting_prev_active`. 2. Arbitrary things can add conflicts to the backtracking. If we fail to activate because some dependency needs a feature that does not exist, that is easy to record, just add the `PackageId` to the set `conflicting_activations`. 3. All things that could cause use to fail will be in the error messages, as the error messages loop over the set. 4. With a simple extension, replacing the `HashSet` with a `HashMap<_, Reason>`, we can customize the error messages to show the nature of the conflict. @alexcrichton, @aidanhs, Does the logic look right? Does this seem clearer to you?
2 parents e2c5d2e + 8899093 commit 489f570

File tree

2 files changed

+115
-62
lines changed

2 files changed

+115
-62
lines changed

src/cargo/core/resolver/mod.rs

Lines changed: 75 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -541,23 +541,33 @@ struct BacktrackFrame<'a> {
541541
#[derive(Clone)]
542542
struct RemainingCandidates {
543543
remaining: RcVecIter<Candidate>,
544+
// note: change to RcList or something if clone is to expensive
545+
conflicting_prev_active: HashSet<PackageId>,
544546
}
545547

546548
impl RemainingCandidates {
547-
fn next(&mut self, prev_active: &[Summary]) -> Option<Candidate> {
549+
fn next(&mut self, prev_active: &[Summary]) -> Result<Candidate, HashSet<PackageId>> {
548550
// Filter the set of candidates based on the previously activated
549551
// versions for this dependency. We can actually use a version if it
550552
// precisely matches an activated version or if it is otherwise
551553
// incompatible with all other activated versions. Note that we
552554
// define "compatible" here in terms of the semver sense where if
553555
// the left-most nonzero digit is the same they're considered
554556
// compatible.
555-
self.remaining.by_ref().map(|p| p.1).find(|b| {
556-
prev_active.iter().any(|a| *a == b.summary) ||
557-
prev_active.iter().all(|a| {
558-
!compatible(a.version(), b.summary.version())
559-
})
560-
})
557+
//
558+
// When we are done we return the set of previously activated
559+
// that conflicted with the ones we tried. If any of these change
560+
// then we would have considered different candidates.
561+
for (_, b) in self.remaining.by_ref() {
562+
if let Some(a) = prev_active.iter().find(|a| compatible(a.version(), b.summary.version())) {
563+
if *a != b.summary {
564+
self.conflicting_prev_active.insert(a.package_id().clone());
565+
continue
566+
}
567+
}
568+
return Ok(b);
569+
}
570+
Err(self.conflicting_prev_active.clone())
561571
}
562572
}
563573

@@ -650,9 +660,10 @@ fn activate_deps_loop<'a>(mut cx: Context<'a>,
650660
dep.name(), prev_active.len());
651661
let mut candidates = RemainingCandidates {
652662
remaining: RcVecIter::new(Rc::clone(&candidates)),
663+
conflicting_prev_active: HashSet::new(),
653664
};
654665
(candidates.next(prev_active),
655-
candidates.clone().next(prev_active).is_some(),
666+
candidates.clone().next(prev_active).is_ok(),
656667
candidates)
657668
};
658669

@@ -669,7 +680,7 @@ fn activate_deps_loop<'a>(mut cx: Context<'a>,
669680
// turn. We could possibly fail to activate each candidate, so we try
670681
// each one in turn.
671682
let candidate = match next {
672-
Some(candidate) => {
683+
Ok(candidate) => {
673684
// We have a candidate. Add an entry to the `backtrack_stack` so
674685
// we can try the next one if this one fails.
675686
if has_another {
@@ -685,7 +696,7 @@ fn activate_deps_loop<'a>(mut cx: Context<'a>,
685696
}
686697
candidate
687698
}
688-
None => {
699+
Err(mut conflicting) => {
689700
// This dependency has no valid candidate. Backtrack until we
690701
// find a dependency that does have a candidate to try, and try
691702
// to activate that one. This resets the `remaining_deps` to
@@ -698,10 +709,11 @@ fn activate_deps_loop<'a>(mut cx: Context<'a>,
698709
&mut parent,
699710
&mut cur,
700711
&mut dep,
701-
&mut features) {
712+
&mut features,
713+
&mut conflicting) {
702714
None => return Err(activation_error(&cx, registry, &parent,
703715
&dep,
704-
cx.prev_active(&dep),
716+
conflicting,
705717
&candidates, config)),
706718
Some(candidate) => candidate,
707719
}
@@ -725,39 +737,44 @@ fn activate_deps_loop<'a>(mut cx: Context<'a>,
725737
Ok(cx)
726738
}
727739

728-
// Looks through the states in `backtrack_stack` for dependencies with
729-
// remaining candidates. For each one, also checks if rolling back
730-
// could change the outcome of the failed resolution that caused backtracking
731-
// in the first place - namely, if we've backtracked past the parent of the
732-
// failed dep, or the previous number of activations of the failed dep has
733-
// changed (possibly relaxing version constraints). If the outcome could differ,
734-
// resets `cx` and `remaining_deps` to that level and returns the
735-
// next candidate. If all candidates have been exhausted, returns None.
736-
// Read https://github.com/rust-lang/cargo/pull/4834#issuecomment-362871537 for
737-
// a more detailed explanation of the logic here.
738-
fn find_candidate<'a>(backtrack_stack: &mut Vec<BacktrackFrame<'a>>,
739-
cx: &mut Context<'a>,
740-
remaining_deps: &mut BinaryHeap<DepsFrame>,
741-
parent: &mut Summary,
742-
cur: &mut usize,
743-
dep: &mut Dependency,
744-
features: &mut Rc<Vec<String>>) -> Option<Candidate> {
745-
let num_dep_prev_active = cx.prev_active(dep).len();
740+
/// Looks through the states in `backtrack_stack` for dependencies with
741+
/// remaining candidates. For each one, also checks if rolling back
742+
/// could change the outcome of the failed resolution that caused backtracking
743+
/// in the first place. Namely, if we've backtracked past the parent of the
744+
/// failed dep, or any of the packages flagged as giving us trouble in conflicting_activations.
745+
/// Read https://github.com/rust-lang/cargo/pull/4834
746+
/// For several more detailed explanations of the logic here.
747+
///
748+
/// If the outcome could differ, resets `cx` and `remaining_deps` to that
749+
/// level and returns the next candidate.
750+
/// If all candidates have been exhausted, returns None.
751+
fn find_candidate<'a>(
752+
backtrack_stack: &mut Vec<BacktrackFrame<'a>>,
753+
cx: &mut Context<'a>,
754+
remaining_deps: &mut BinaryHeap<DepsFrame>,
755+
parent: &mut Summary,
756+
cur: &mut usize,
757+
dep: &mut Dependency,
758+
features: &mut Rc<Vec<String>>,
759+
conflicting_activations: &mut HashSet<PackageId>,
760+
) -> Option<Candidate> {
746761
while let Some(mut frame) = backtrack_stack.pop() {
747762
let (next, has_another) = {
748763
let prev_active = frame.context_backup.prev_active(&frame.dep);
749-
(frame.remaining_candidates.next(prev_active),
750-
frame.remaining_candidates.clone().next(prev_active).is_some())
764+
(
765+
frame.remaining_candidates.next(prev_active),
766+
frame.remaining_candidates.clone().next(prev_active).is_ok(),
767+
)
751768
};
752-
let cur_num_dep_prev_active = frame.context_backup.prev_active(dep).len();
753-
// Activations should monotonically decrease during backtracking
754-
assert!(cur_num_dep_prev_active <= num_dep_prev_active);
755-
let maychange = !frame.context_backup.is_active(parent) ||
756-
cur_num_dep_prev_active != num_dep_prev_active;
757-
if !maychange {
758-
continue
769+
if frame.context_backup.is_active(parent.package_id())
770+
&& conflicting_activations
771+
.iter()
772+
// note: a lot of redundant work in is_active for similar debs
773+
.all(|con| frame.context_backup.is_active(con))
774+
{
775+
continue;
759776
}
760-
if let Some(candidate) = next {
777+
if let Ok(candidate) = next {
761778
*cur = frame.cur;
762779
if has_another {
763780
*cx = frame.context_backup.clone();
@@ -773,7 +790,7 @@ fn find_candidate<'a>(backtrack_stack: &mut Vec<BacktrackFrame<'a>>,
773790
*dep = frame.dep;
774791
*features = frame.features;
775792
}
776-
return Some(candidate)
793+
return Some(candidate);
777794
}
778795
}
779796
None
@@ -783,7 +800,7 @@ fn activation_error(cx: &Context,
783800
registry: &mut Registry,
784801
parent: &Summary,
785802
dep: &Dependency,
786-
prev_active: &[Summary],
803+
conflicting_activations: HashSet<PackageId>,
787804
candidates: &[Candidate],
788805
config: Option<&Config>) -> CargoError {
789806
let graph = cx.graph();
@@ -799,29 +816,31 @@ fn activation_error(cx: &Context,
799816
dep_path_desc
800817
};
801818
if !candidates.is_empty() {
802-
let mut msg = format!("failed to select a version for `{0}`\n\
819+
let mut msg = format!("failed to select a version for `{}`.\n\
803820
all possible versions conflict with \
804-
previously selected versions of `{0}`\n",
821+
previously selected packages.\n",
805822
dep.name());
806823
msg.push_str("required by ");
807824
msg.push_str(&describe_path(parent.package_id()));
808-
for v in prev_active.iter() {
825+
let mut conflicting_activations: Vec<_> = conflicting_activations.iter().collect();
826+
conflicting_activations.sort_unstable();
827+
for v in conflicting_activations.iter().rev() {
809828
msg.push_str("\n previously selected ");
810-
msg.push_str(&describe_path(v.package_id()));
829+
msg.push_str(&describe_path(v));
811830
}
812831

813-
msg.push_str(&format!("\n possible versions to select: {}",
814-
candidates.iter()
815-
.map(|v| v.summary.version())
816-
.map(|v| v.to_string())
817-
.collect::<Vec<_>>()
818-
.join(", ")));
832+
msg.push_str("\n possible versions to select: ");
833+
msg.push_str(&candidates.iter()
834+
.map(|v| v.summary.version())
835+
.map(|v| v.to_string())
836+
.collect::<Vec<_>>()
837+
.join(", "));
819838

820839
return format_err!("{}", msg)
821840
}
822841

823842
// Once we're all the way down here, we're definitely lost in the
824-
// weeds! We didn't actually use any candidates above, so we need to
843+
// weeds! We didn't actually find any candidates, so we need to
825844
// give an error message that nothing was found.
826845
//
827846
// Note that we re-query the registry with a new dependency that
@@ -834,7 +853,7 @@ fn activation_error(cx: &Context,
834853
Ok(candidates) => candidates,
835854
Err(e) => return e,
836855
};
837-
candidates.sort_by(|a, b| {
856+
candidates.sort_unstable_by(|a, b| {
838857
b.version().cmp(a.version())
839858
});
840859

@@ -1172,11 +1191,10 @@ impl<'a> Context<'a> {
11721191
.unwrap_or(&[])
11731192
}
11741193

1175-
fn is_active(&mut self, summary: &Summary) -> bool {
1176-
let id = summary.package_id();
1194+
fn is_active(&mut self, id: &PackageId) -> bool {
11771195
self.activations.get(id.name())
11781196
.and_then(|v| v.get(id.source_id()))
1179-
.map(|v| v.iter().any(|s| s == summary))
1197+
.map(|v| v.iter().any(|s| s.package_id() == id))
11801198
.unwrap_or(false)
11811199
}
11821200

tests/build.rs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,19 +1026,54 @@ fn incompatible_dependencies() {
10261026
assert_that(p.cargo("build"),
10271027
execs().with_status(101)
10281028
.with_stderr_contains("\
1029-
error: failed to select a version for `bad`
1030-
all possible versions conflict with previously selected versions of `bad`
1029+
error: failed to select a version for `bad`.
1030+
all possible versions conflict with previously selected packages.
10311031
required by package `baz v0.1.0`
10321032
... which is depended on by `incompatible_dependencies v0.0.1 ([..])`
1033-
previously selected package `bad v0.1.0`
1034-
... which is depended on by `foo v0.1.0`
1035-
... which is depended on by `incompatible_dependencies v0.0.1 ([..])`
10361033
previously selected package `bad v1.0.0`
10371034
... which is depended on by `bar v0.1.0`
10381035
... which is depended on by `incompatible_dependencies v0.0.1 ([..])`
10391036
possible versions to select: 1.0.2, 1.0.1"));
10401037
}
10411038

1039+
#[test]
1040+
fn incompatible_dependencies_with_multi_semver() {
1041+
Package::new("bad", "1.0.0").publish();
1042+
Package::new("bad", "1.0.1").publish();
1043+
Package::new("bad", "2.0.0").publish();
1044+
Package::new("bad", "2.0.1").publish();
1045+
Package::new("bar", "0.1.0").dep("bad", "=1.0.0").publish();
1046+
Package::new("baz", "0.1.0").dep("bad", ">=2.0.1").publish();
1047+
1048+
let p = project("transitive_load_test")
1049+
.file("Cargo.toml", r#"
1050+
[project]
1051+
name = "incompatible_dependencies"
1052+
version = "0.0.1"
1053+
1054+
[dependencies]
1055+
bar = "0.1.0"
1056+
baz = "0.1.0"
1057+
bad = ">=1.0.1, <=2.0.0"
1058+
"#)
1059+
.file("src/main.rs", "fn main(){}")
1060+
.build();
1061+
1062+
assert_that(p.cargo("build"),
1063+
execs().with_status(101)
1064+
.with_stderr_contains("\
1065+
error: failed to select a version for `bad`.
1066+
all possible versions conflict with previously selected packages.
1067+
required by package `incompatible_dependencies v0.0.1 ([..])`
1068+
previously selected package `bad v2.0.1`
1069+
... which is depended on by `baz v0.1.0`
1070+
... which is depended on by `incompatible_dependencies v0.0.1 ([..])`
1071+
previously selected package `bad v1.0.0`
1072+
... which is depended on by `bar v0.1.0`
1073+
... which is depended on by `incompatible_dependencies v0.0.1 ([..])`
1074+
possible versions to select: 2.0.0, 1.0.1"));
1075+
}
1076+
10421077
#[test]
10431078
fn compile_offline_while_transitive_dep_not_cached() {
10441079
let bar = Package::new("bar", "1.0.0");

0 commit comments

Comments
 (0)