├── .gitignore ├── Cargo.toml ├── readme.md ├── src ├── error.rs ├── explored_space.rs ├── lib.rs ├── provider.rs ├── reporter.rs └── resolver.rs └── tests ├── in_memory_provider.rs └── snapshots ├── in_memory_provider__error_reporting_bluesky_conflict.snap ├── in_memory_provider__error_reporting_cyclic.snap ├── in_memory_provider__error_reporting_graph_compression_simple.snap ├── in_memory_provider__error_reporting_missing_1.snap ├── in_memory_provider__error_reporting_missing_2.snap ├── in_memory_provider__error_reporting_pubgrub_article.snap └── in_memory_provider__error_reporting_root_conflict.snap /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | .idea 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "resolvelib-rs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.71" 10 | petgraph = "0.6.3" 11 | rustc-hash = "1.1.0" 12 | thiserror = "1.0.40" 13 | itertools = "0.10.5" 14 | 15 | [dev-dependencies] 16 | insta = "1.29.0" 17 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # resovelib-rs 2 | 3 | Rust port of [resolvelib](https://github.com/sarugaku/resolvelib). Props to the authors who wrote 4 | such clear and concise code! 5 | 6 | This library provides two data types: 7 | 8 | * `Resolver`: a struct that drives dependency resolution 9 | * `Provider`: a trait with methods to retrieve information about dependencies 10 | 11 | Note that this library is a low-level building block. You will need to create a custom provider to 12 | use it in a real-world scenario. Check out [in_memory_provider.rs](tests/in_memory_provider.rs) for 13 | an example. 14 | 15 | ## Terminology 16 | 17 | * Candidate: a concrete description of a package that can be installed (e.g. libfoo 2.3) 18 | * Requirement: an abstract description of a package that should be resolved (e.g. any version of libfoo) 19 | * Identifier: the name that identifies both a requirement and the candidate (e.g. libfoo) 20 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::explored_space::ExploredSpace; 2 | use crate::RequirementInformation; 3 | use thiserror::Error; 4 | 5 | #[derive(Error)] 6 | pub enum ResolutionError { 7 | #[error("resolution impossible")] 8 | ResolutionImpossible(ResolutionImpossible), 9 | #[error("resolution too deep")] 10 | ResolutionTooDeep(u64), 11 | } 12 | 13 | pub struct ResolutionImpossible { 14 | graph: ExploredSpace, 15 | unsatisfied: Vec>, 16 | } 17 | 18 | impl ResolutionImpossible { 19 | pub(crate) fn new( 20 | graph: ExploredSpace, 21 | unsatisfied: Vec>, 22 | ) -> Self { 23 | Self { graph, unsatisfied } 24 | } 25 | 26 | pub fn unsatisfied_requirements(&self) -> &[RequirementInformation] { 27 | &self.unsatisfied 28 | } 29 | 30 | pub fn graph(&self) -> &ExploredSpace { 31 | &self.graph 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/explored_space.rs: -------------------------------------------------------------------------------- 1 | use crate::RequirementKind; 2 | 3 | use itertools::Itertools; 4 | use petgraph::graph::{DiGraph, EdgeReference, NodeIndex}; 5 | use petgraph::prelude::{Bfs, EdgeRef}; 6 | use petgraph::visit::DfsPostOrder; 7 | use petgraph::Direction; 8 | use rustc_hash::{FxHashMap, FxHashSet}; 9 | use std::collections::{HashMap, HashSet}; 10 | use std::fmt::Formatter; 11 | use std::hash::Hash; 12 | use std::rc::Rc; 13 | 14 | #[derive(Clone, Hash, PartialEq, Eq)] 15 | pub enum Node { 16 | Root, 17 | Candidate(TCandidate), 18 | NotFound, 19 | } 20 | 21 | #[derive(Clone)] 22 | struct Edge { 23 | requirement: TRequirement, 24 | kind: RequirementKind, 25 | status: EdgeStatus, 26 | } 27 | 28 | impl Edge { 29 | fn healthy(requirement: TRequirement, kind: RequirementKind) -> Self { 30 | Self { 31 | requirement, 32 | kind, 33 | status: EdgeStatus::Healthy, 34 | } 35 | } 36 | 37 | fn conflict(requirement: TRequirement, kind: RequirementKind) -> Self { 38 | Self { 39 | requirement, 40 | kind, 41 | status: EdgeStatus::Conflict, 42 | } 43 | } 44 | } 45 | 46 | #[derive(Clone, PartialEq, Eq)] 47 | enum EdgeStatus { 48 | Conflict, 49 | Healthy, 50 | } 51 | 52 | pub struct DisplayRequirement { 53 | name: String, 54 | candidates: DisplayCandidates, 55 | installable: bool, 56 | } 57 | 58 | enum DisplayCandidates { 59 | Candidates(Vec), 60 | Conflict, 61 | Missing, 62 | } 63 | 64 | impl DisplayRequirement { 65 | fn new(name: String, candidates: DisplayCandidates) -> Self { 66 | let installable = match &candidates { 67 | DisplayCandidates::Candidates(candidates) 68 | if candidates.iter().any(|c| c.installable) => 69 | { 70 | true 71 | } 72 | _ => false, 73 | }; 74 | 75 | Self { 76 | name, 77 | installable, 78 | candidates, 79 | } 80 | } 81 | } 82 | 83 | pub struct DisplayCandidate { 84 | name: String, 85 | version: String, 86 | node_id: NodeIndex, 87 | requirements: Vec, 88 | installable: bool, 89 | } 90 | 91 | struct MergedCandidate { 92 | versions: Vec, 93 | ids: Vec, 94 | } 95 | 96 | pub struct DisplayError { 97 | root_requirements: Vec, 98 | merged_candidates: FxHashMap>, 99 | } 100 | 101 | impl std::fmt::Display for DisplayError { 102 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 103 | let mut reported: FxHashSet = HashSet::default(); 104 | 105 | pub enum DisplayOp<'a> { 106 | Requirement(&'a DisplayRequirement), 107 | Candidate(&'a DisplayCandidate), 108 | } 109 | 110 | writeln!(f, "The following packages are incompatible")?; 111 | 112 | let mut stack = self 113 | .root_requirements 114 | .iter() 115 | .sorted_by_key(|r| r.installable) 116 | .map(|r| (DisplayOp::Requirement(r), 0)) 117 | .collect::>(); 118 | while let Some((node, depth)) = stack.pop() { 119 | let indent = " ".repeat(depth * 4); 120 | 121 | match node { 122 | DisplayOp::Requirement(requirement) => { 123 | let installable = requirement.installable; 124 | let req = &requirement.name; 125 | 126 | if let DisplayCandidates::Missing = requirement.candidates { 127 | // No candidates for requirement 128 | if depth == 0 { 129 | writeln!( 130 | f, 131 | "{indent}|-- No candidates where found for {}.", 132 | requirement.name 133 | )?; 134 | } else { 135 | writeln!( 136 | f, 137 | "{indent}|-- {}, for which no candidates where found.", 138 | requirement.name 139 | )?; 140 | } 141 | } else if installable { 142 | // Package can be installed (only mentioned for top-level requirements) 143 | if depth == 0 { 144 | writeln!(f, "|-- {req} will be installed;")?; 145 | } 146 | } else { 147 | // Package cannot be installed 148 | match &requirement.candidates { 149 | DisplayCandidates::Candidates(candidates) => { 150 | // The conflicting requirement is further down the tree 151 | if depth == 0 { 152 | writeln!(f, "|-- {req} cannot be installed because:")?; 153 | } else { 154 | writeln!( 155 | f, 156 | "{indent}|-- {req}, which cannot be installed because:" 157 | )?; 158 | } 159 | 160 | stack.extend( 161 | candidates 162 | .iter() 163 | .map(|c| (DisplayOp::Candidate(c), depth + 1)), 164 | ); 165 | } 166 | DisplayCandidates::Conflict => { 167 | // We have reached the conflicting requirement 168 | if depth == 0 { 169 | writeln!(f, "|-- {req} cannot be installed, because it conflicts with the versions reported above.")?; 170 | } else { 171 | writeln!(f, "{indent}|-- {req}, which conflicts with the already selected version.")?; 172 | } 173 | } 174 | DisplayCandidates::Missing => unreachable!(), 175 | } 176 | } 177 | } 178 | DisplayOp::Candidate(candidate) if !reported.contains(&candidate.node_id) => { 179 | let version = 180 | if let Some(merged) = self.merged_candidates.get(&candidate.node_id) { 181 | reported.extend(&merged.ids); 182 | merged.versions.join(" | ") 183 | } else { 184 | candidate.version.clone() 185 | }; 186 | 187 | if candidate.requirements.is_empty() { 188 | writeln!(f, "{indent}|-- {} {version}", candidate.name)?; 189 | } else { 190 | writeln!(f, "{indent}|-- {} {version} would require", candidate.name)?; 191 | } 192 | 193 | stack.extend( 194 | candidate 195 | .requirements 196 | .iter() 197 | .map(|r| (DisplayOp::Requirement(r), depth + 1)), 198 | ); 199 | } 200 | _ => {} 201 | } 202 | } 203 | 204 | Ok(()) 205 | } 206 | } 207 | 208 | enum GetDisplayCandidateResult { 209 | Missing, 210 | Conflict, 211 | Candidate(DisplayCandidate), 212 | } 213 | 214 | impl GetDisplayCandidateResult { 215 | fn installable(&self) -> bool { 216 | match self { 217 | GetDisplayCandidateResult::Missing | GetDisplayCandidateResult::Conflict => false, 218 | GetDisplayCandidateResult::Candidate(c) => c.installable, 219 | } 220 | } 221 | } 222 | 223 | #[derive(Clone)] 224 | pub struct ExploredSpace { 225 | node_ids: FxHashMap, NodeIndex>, 226 | graph: DiGraph, Edge>, 227 | } 228 | 229 | impl ExploredSpace 230 | where 231 | TRequirement: Hash + Eq + Clone, 232 | TCandidate: Hash + Eq + Clone, 233 | { 234 | pub(crate) fn new() -> Self { 235 | Self { 236 | node_ids: HashMap::default(), 237 | graph: DiGraph::new(), 238 | } 239 | } 240 | 241 | pub(crate) fn get_or_add_node(&mut self, node: Node) -> NodeIndex { 242 | *self 243 | .node_ids 244 | .entry(node.clone()) 245 | .or_insert_with(|| self.graph.add_node(node)) 246 | } 247 | 248 | pub(crate) fn track_requirement( 249 | &mut self, 250 | node1: NodeIndex, 251 | node2: NodeIndex, 252 | requirement: TRequirement, 253 | kind: RequirementKind, 254 | ) { 255 | self.graph 256 | .add_edge(node1, node2, Edge::healthy(requirement, kind)); 257 | } 258 | 259 | pub(crate) fn track_conflict( 260 | &mut self, 261 | node1: NodeIndex, 262 | node2: NodeIndex, 263 | requirement: TRequirement, 264 | kind: RequirementKind, 265 | ) { 266 | self.graph 267 | .add_edge(node1, node2, Edge::conflict(requirement, kind)); 268 | } 269 | 270 | pub(crate) fn track_missing( 271 | &mut self, 272 | node1: NodeIndex, 273 | requirement: TRequirement, 274 | kind: RequirementKind, 275 | ) { 276 | let node2 = self.get_or_add_node(Node::NotFound); 277 | self.graph 278 | .add_edge(node1, node2, Edge::healthy(requirement, kind)); 279 | } 280 | 281 | pub fn print_user_friendly_error( 282 | &self, 283 | display_candidate_name: impl Fn(TCandidate) -> String, 284 | display_candidate_version: impl Fn(TCandidate) -> String, 285 | sort_candidate_version: impl Fn(&TCandidate) -> K1, 286 | display_requirement: impl Fn(TRequirement) -> String, 287 | sort_requirement: impl Fn(&TRequirement) -> K2, 288 | ) -> DisplayError { 289 | // Build a tree from the root requirements to the conflicts 290 | let root_node = self.node_ids[&Node::Root]; 291 | let mut root_requirements = Vec::new(); 292 | let top_level_edges = self 293 | .graph 294 | .edges(root_node) 295 | .group_by(|e| e.weight().requirement.clone()); 296 | 297 | let mut path = FxHashSet::default(); 298 | for (requirement, candidates) in top_level_edges.into_iter() { 299 | let req = self.get_display_requirement( 300 | &mut path, 301 | &display_candidate_name, 302 | &display_candidate_version, 303 | &display_requirement, 304 | display_requirement(requirement), 305 | candidates.collect(), 306 | ); 307 | root_requirements.push(req); 308 | } 309 | 310 | // Gather information about nodes that can be merged 311 | let mut maybe_merge = FxHashMap::default(); 312 | for node_id in self.graph.node_indices() { 313 | let candidate = match &self.graph[node_id] { 314 | Node::Root | Node::NotFound => continue, 315 | Node::Candidate(c) => c, 316 | }; 317 | 318 | if self 319 | .graph 320 | .edges_directed(node_id, Direction::Incoming) 321 | .any(|e| e.weight().status == EdgeStatus::Conflict) 322 | { 323 | // Nodes that are the target of a conflict should never be merged 324 | continue; 325 | } 326 | 327 | let predecessors: Vec<_> = self 328 | .graph 329 | .edges_directed(node_id, Direction::Incoming) 330 | .map(|e| e.source()) 331 | .sorted_unstable() 332 | .collect(); 333 | let successors: Vec<_> = self 334 | .graph 335 | .edges(node_id) 336 | .map(|e| (e.target(), &e.weight().requirement)) 337 | .sorted_unstable_by_key(|&(target_node, requirement)| { 338 | (target_node, sort_requirement(requirement)) 339 | }) 340 | .collect(); 341 | 342 | let name = display_candidate_name(candidate.clone()); 343 | 344 | let entry = maybe_merge 345 | .entry((name.clone(), predecessors, successors)) 346 | .or_insert((Vec::new(), Vec::new())); 347 | 348 | entry.0.push(node_id); 349 | entry.1.push(candidate.clone()); 350 | } 351 | 352 | let mut merged_candidates = HashMap::default(); 353 | for mut m in maybe_merge.into_values() { 354 | if m.0.len() > 1 { 355 | m.1.sort_unstable_by_key(&sort_candidate_version); 356 | let m = Rc::new(MergedCandidate { 357 | ids: m.0, 358 | versions: m 359 | .1 360 | .into_iter() 361 | .map(|c| display_candidate_version(c)) 362 | .collect(), 363 | }); 364 | for id in &m.ids { 365 | merged_candidates.insert(id.clone(), m.clone()); 366 | } 367 | } 368 | } 369 | 370 | DisplayError { 371 | root_requirements, 372 | merged_candidates, 373 | } 374 | } 375 | 376 | fn get_display_requirement( 377 | &self, 378 | path: &mut FxHashSet, 379 | display_candidate_name: &impl Fn(TCandidate) -> String, 380 | display_candidate_version: &impl Fn(TCandidate) -> String, 381 | display_requirement: &impl Fn(TRequirement) -> String, 382 | name: String, 383 | candidate_edges: Vec>>, 384 | ) -> DisplayRequirement { 385 | let mut candidates = candidate_edges 386 | .into_iter() 387 | .map(|edge| { 388 | self.get_display_candidate( 389 | path, 390 | display_candidate_name, 391 | display_candidate_version, 392 | display_requirement, 393 | edge, 394 | ) 395 | }) 396 | .collect::>(); 397 | 398 | candidates.sort_by_key(|c| c.installable()); 399 | 400 | let candidates = if candidates 401 | .iter() 402 | .all(|c| matches!(c, GetDisplayCandidateResult::Missing)) 403 | { 404 | DisplayCandidates::Missing 405 | } else if candidates 406 | .iter() 407 | .all(|c| matches!(c, GetDisplayCandidateResult::Conflict)) 408 | { 409 | DisplayCandidates::Conflict 410 | } else { 411 | DisplayCandidates::Candidates( 412 | candidates 413 | .into_iter() 414 | .flat_map(|c| match c { 415 | GetDisplayCandidateResult::Missing 416 | | GetDisplayCandidateResult::Conflict => None, 417 | GetDisplayCandidateResult::Candidate(c) => Some(c), 418 | }) 419 | .collect(), 420 | ) 421 | }; 422 | 423 | DisplayRequirement::new(name, candidates) 424 | } 425 | 426 | fn get_display_candidate( 427 | &self, 428 | path: &mut FxHashSet, 429 | display_candidate_name: &impl Fn(TCandidate) -> String, 430 | display_candidate_version: &impl Fn(TCandidate) -> String, 431 | display_requirement: &impl Fn(TRequirement) -> String, 432 | edge_to_candidate: EdgeReference>, 433 | ) -> GetDisplayCandidateResult { 434 | if edge_to_candidate.weight().status == EdgeStatus::Conflict { 435 | return GetDisplayCandidateResult::Conflict; 436 | } 437 | 438 | match &self.graph[edge_to_candidate.target()] { 439 | Node::Candidate(c) => { 440 | let name = display_candidate_name(c.clone()); 441 | let version = display_candidate_version(c.clone()); 442 | let node_id = edge_to_candidate.target(); 443 | 444 | // If already visited, return the same candidate, but without requirements 445 | if path.contains(&node_id) { 446 | return GetDisplayCandidateResult::Candidate(DisplayCandidate { 447 | name, 448 | version, 449 | node_id, 450 | requirements: vec![], 451 | installable: true, 452 | }); 453 | } 454 | 455 | path.insert(node_id); 456 | 457 | let candidate_dependencies = self 458 | .graph 459 | .edges(edge_to_candidate.target()) 460 | .group_by(|e| e.weight().requirement.clone()); 461 | 462 | let mut reqs = Vec::new(); 463 | for (requirement, edges) in candidate_dependencies.into_iter() { 464 | reqs.push(self.get_display_requirement( 465 | path, 466 | display_candidate_name, 467 | display_candidate_version, 468 | display_requirement, 469 | display_requirement(requirement), 470 | edges.collect(), 471 | )); 472 | } 473 | 474 | path.remove(&node_id); 475 | 476 | GetDisplayCandidateResult::Candidate(DisplayCandidate { 477 | name, 478 | version, 479 | node_id, 480 | installable: edge_to_candidate.weight().status == EdgeStatus::Healthy 481 | && reqs.iter().all(|r| r.installable), 482 | requirements: reqs, 483 | }) 484 | } 485 | Node::NotFound => GetDisplayCandidateResult::Missing, 486 | _ => unreachable!(), 487 | } 488 | } 489 | 490 | pub fn graphviz( 491 | &self, 492 | display_candidate: impl Fn(TCandidate) -> String, 493 | display_requirement: impl Fn(TRequirement) -> String, 494 | only_conflicting_paths: bool, 495 | ) -> String { 496 | let root_node = self.node_ids[&Node::Root]; 497 | let mut bfs = Bfs::new(&self.graph, root_node); 498 | 499 | let mut interesting_nodes: FxHashSet = FxHashSet::default(); 500 | if only_conflicting_paths { 501 | let mut dfs = DfsPostOrder::new(&self.graph, root_node); 502 | while let Some(nx) = dfs.next(&self.graph) { 503 | // Two kinds of interesting nodes: 504 | // * Nodes that have an edge to an existing interesting node 505 | // * Nodes that have incoming conflict edges 506 | if self 507 | .graph 508 | .edges(nx) 509 | .any(|e| interesting_nodes.contains(&e.target())) 510 | || self 511 | .graph 512 | .edges_directed(nx, Direction::Incoming) 513 | .any(|e| e.weight().status == EdgeStatus::Conflict) 514 | { 515 | interesting_nodes.insert(nx); 516 | } 517 | } 518 | if let Some(nx) = self.node_ids.get(&Node::NotFound) { 519 | interesting_nodes.insert(nx.clone()); 520 | } 521 | } 522 | 523 | let mut buf = String::new(); 524 | buf.push_str("digraph {\n"); 525 | while let Some(nx) = bfs.next(&self.graph) { 526 | if only_conflicting_paths && !interesting_nodes.contains(&nx) { 527 | continue; 528 | } 529 | 530 | // The node itself 531 | let node1 = self.graph.node_weight(nx).unwrap(); 532 | let node1_name = match node1 { 533 | Node::Root => "root".to_string(), 534 | Node::Candidate(c) => (display_candidate)(c.clone()), 535 | Node::NotFound => "not_found".to_string(), 536 | }; 537 | 538 | for edge in self.graph.edges(nx) { 539 | let neighbor = edge.target(); 540 | 541 | if only_conflicting_paths && !interesting_nodes.contains(&neighbor) { 542 | continue; 543 | } 544 | 545 | let node2 = self.graph.node_weight(neighbor).unwrap(); 546 | let node2_name = match node2 { 547 | Node::Root => "root".to_string(), 548 | Node::Candidate(c) => (display_candidate)(c.clone()), 549 | Node::NotFound => "not_found".to_string(), 550 | }; 551 | 552 | let label = (display_requirement)(edge.weight().requirement.clone()); 553 | let color = 554 | if edge.weight().status == EdgeStatus::Conflict || node2_name == "not_found" { 555 | "red" 556 | } else { 557 | "black" 558 | }; 559 | 560 | let line = 561 | format!(r#""{node1_name}" -> "{node2_name}"[color={color}, label="{label}"];"#); 562 | buf.push_str(&line); 563 | buf.push('\n'); 564 | } 565 | } 566 | buf.push('}'); 567 | 568 | buf 569 | } 570 | } 571 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | mod explored_space; 3 | mod provider; 4 | mod reporter; 5 | mod resolver; 6 | 7 | pub use error::{ResolutionError, ResolutionImpossible}; 8 | pub use provider::Provider; 9 | pub use reporter::{NoOpReporter, Reporter}; 10 | pub use resolver::{ 11 | Criterion, RequirementInformation, RequirementKind, ResolutionResult, Resolver, 12 | }; 13 | -------------------------------------------------------------------------------- /src/provider.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::hash::Hash; 3 | 4 | use crate::resolver::{Criterion, RequirementInformation}; 5 | 6 | pub trait Provider { 7 | type Candidate: Copy; 8 | type Requirement: Copy; 9 | type Identifier: Copy + Hash + Eq; 10 | 11 | /// Retrieve the identifier of a candidate 12 | fn identify_candidate(&self, candidate: Self::Candidate) -> Self::Identifier; 13 | 14 | /// Retrieve the identifier of a requirement 15 | fn identify_requirement(&self, requirement: Self::Requirement) -> Self::Identifier; 16 | 17 | /// Produce a sort key for the given requirement (identified by `identifier`). 18 | /// 19 | /// The lower the return value, the more preferred the requirement is (i.e. it will be resolved 20 | /// before less-preferred requirements). 21 | /// 22 | /// This method provides loads of information, in case you need it to determine the 23 | /// requirement's preference. There is no need to actually use all information, though. In fact, 24 | /// the default implementation determines preference purely based on the amount of candidates 25 | /// for the requirement. 26 | fn get_preference( 27 | &self, 28 | identifier: Self::Identifier, 29 | _resolutions: &HashMap, 30 | criteria: &HashMap>, 31 | _backtrack_causes: &[RequirementInformation], 32 | ) -> u64 { 33 | criteria[&identifier].candidates.len() as u64 34 | } 35 | 36 | /// Produce a vector of candidates that should be considered when resolving the given 37 | /// requirement (identified by `identifier`). 38 | /// 39 | /// This method provides loads of information, in case you need it to determine the 40 | /// requirement's candidates. There is no need to actually use all information, though. It is 41 | /// often enough to have a look only at the requirements and incompatibilities associated to the 42 | /// provided identifier (e.g. `requirements[&identifier]` and `incompatibilities[&identifier]`), 43 | /// without taking the rest into account. 44 | fn find_matches( 45 | &self, 46 | identifier: Self::Identifier, 47 | requirements: &HashMap>, 48 | incompatibilities: &HashMap>, 49 | ) -> Vec; 50 | 51 | /// Whether the candidate satisfies the requirement 52 | fn is_satisfied_by(&self, requirement: Self::Requirement, candidate: Self::Candidate) -> bool; 53 | 54 | /// Produce a vector of requirements that represent a candidate's dependencies 55 | fn get_dependencies(&self, candidate: Self::Candidate) -> Vec; 56 | 57 | /// Produce a vector of requirements that represent a candidate's constraints 58 | fn get_constraints(&self, candidate: Self::Candidate) -> Vec; 59 | 60 | /// Called when the provider produces an inconsistent candidate (i.e. a candidate that does not 61 | /// satisfy the requirements). 62 | /// 63 | /// Inconsistent candidates always trigger a panic, since they are an unrecoverable error as a 64 | /// consequence of a buggy provider. This function can be used to investigate. 65 | fn on_inconsistent_candidate( 66 | &self, 67 | _candidate: Self::Candidate, 68 | _requirements: Vec, 69 | ) { 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/reporter.rs: -------------------------------------------------------------------------------- 1 | use crate::resolver::ResolutionState; 2 | use crate::RequirementInformation; 3 | use std::marker::PhantomData; 4 | 5 | pub trait Reporter { 6 | type Requirement; 7 | type Candidate; 8 | type Identifier; 9 | 10 | /// Called before the resolution starts 11 | fn starting(&self) {} 12 | 13 | /// Called before each resolution round 14 | fn starting_round(&self, _index: u64) {} 15 | 16 | /// Called after each resolution round, except in the last round (use [`Reporter::ending`] for that) 17 | fn ending_round(&self, _index: u64) {} 18 | 19 | /// Called after resolution ends successfully 20 | fn ending( 21 | &self, 22 | _state: &ResolutionState, 23 | ) { 24 | } 25 | 26 | /// Called when adding a new requirement to the resolve criteria 27 | fn adding_requirement( 28 | &self, 29 | _requirement: &RequirementInformation, 30 | ) { 31 | } 32 | 33 | /// Called when starting an attempt at resolving conflicts 34 | fn resolving_conflicts( 35 | &self, 36 | _causes: &[RequirementInformation], 37 | ) { 38 | } 39 | 40 | fn backtracked(&self, _steps: u64) {} 41 | 42 | /// Called when adding a candidate to the potential solution 43 | fn pinning(&self, _candidate: Self::Candidate) {} 44 | } 45 | 46 | pub struct NoOpReporter { 47 | phantom: PhantomData<(TRequirement, TCandidate, TIdentifier)>, 48 | } 49 | 50 | impl Default 51 | for NoOpReporter 52 | { 53 | fn default() -> Self { 54 | Self { 55 | phantom: PhantomData::default(), 56 | } 57 | } 58 | } 59 | 60 | impl Reporter 61 | for NoOpReporter 62 | { 63 | type Requirement = TRequirement; 64 | type Candidate = TCandidate; 65 | type Identifier = TIdentifier; 66 | } 67 | -------------------------------------------------------------------------------- /src/resolver.rs: -------------------------------------------------------------------------------- 1 | use petgraph::graphmap::DiGraphMap; 2 | use rustc_hash::FxHashSet; 3 | use std::collections::{HashMap, HashSet}; 4 | use std::hash::Hash; 5 | 6 | use crate::error::{ResolutionError, ResolutionImpossible}; 7 | use crate::explored_space::{ExploredSpace, Node}; 8 | use crate::provider::Provider; 9 | use crate::Reporter; 10 | 11 | pub struct ResolutionResult { 12 | pub mapping: HashMap, 13 | pub graph: DiGraphMap, ()>, 14 | pub criteria: HashMap>, 15 | } 16 | 17 | #[derive(Clone, Debug)] 18 | pub struct Criterion { 19 | pub candidates: Vec, 20 | pub information: Vec>, 21 | pub incompatibilities: Vec, 22 | } 23 | 24 | impl Default for Criterion { 25 | fn default() -> Self { 26 | Self { 27 | candidates: Vec::new(), 28 | information: Vec::new(), 29 | incompatibilities: Vec::new(), 30 | } 31 | } 32 | } 33 | 34 | impl Criterion 35 | where 36 | TRequirement: Copy, 37 | TCandidate: Copy, 38 | { 39 | fn iter_requirement(&self) -> impl Iterator + '_ { 40 | self.information.iter().map(|i| i.requirement) 41 | } 42 | 43 | fn iter_parent(&self) -> impl Iterator> + '_ { 44 | self.information.iter().map(|i| i.parent) 45 | } 46 | } 47 | 48 | #[derive(Default, Clone)] 49 | pub struct ResolutionState { 50 | mapping: HashMap, 51 | pinned_candidate_stack: Vec, 52 | criteria: HashMap>, 53 | constraints: HashMap>>, 54 | backtrack_causes: Vec>, 55 | } 56 | 57 | impl ResolutionState 58 | where 59 | TRequirement: Copy, 60 | TCandidate: Copy, 61 | TIdentifier: Copy + Hash + Ord, 62 | { 63 | fn build_result

( 64 | mut self, 65 | provider: &P, 66 | ) -> ResolutionResult 67 | where 68 | P: Provider, 69 | { 70 | let mut graph = DiGraphMap::new(); 71 | graph.add_node(None); 72 | 73 | // It looks like each criterion represents a single resolved package 74 | let mut connected = HashSet::new(); 75 | connected.insert(None); 76 | 77 | for (&key, criterion) in &self.criteria { 78 | // Skip the criterion if it cannot be traced back to the root 79 | if !Self::has_route_to_root(provider, &self.criteria, Some(key), &mut connected) { 80 | continue; 81 | } 82 | 83 | // Add the current node if it isn't part of the graph yet 84 | if !graph.contains_node(Some(key)) { 85 | graph.add_node(Some(key)); 86 | } 87 | 88 | // Add the parents of the node 89 | for p in criterion.iter_parent() { 90 | let p_id = p.map(|p| provider.identify_candidate(p)); 91 | if !graph.contains_node(p_id) { 92 | graph.add_node(p_id); 93 | } 94 | 95 | graph.add_edge(p_id, Some(key), ()); 96 | } 97 | } 98 | 99 | self.mapping.retain(|&k, _| connected.contains(&Some(k))); 100 | 101 | ResolutionResult { 102 | mapping: self.mapping, 103 | graph, 104 | criteria: self.criteria, 105 | } 106 | } 107 | 108 | fn has_route_to_root

( 109 | provider: &P, 110 | criteria: &HashMap>, 111 | key: Option, 112 | connected: &mut HashSet>, 113 | ) -> bool 114 | where 115 | P: Provider, 116 | { 117 | if connected.contains(&key) { 118 | return true; 119 | } 120 | 121 | // If the key was None, it would be considered connected 122 | let key = key.unwrap(); 123 | 124 | if let Some(criterion) = criteria.get(&key) { 125 | for p in criterion.iter_parent() { 126 | let parent_id = p.map(|parent| provider.identify_candidate(parent)); 127 | if connected.contains(&parent_id) 128 | || Self::has_route_to_root(provider, criteria, parent_id, connected) 129 | { 130 | connected.insert(Some(key)); 131 | return true; 132 | } 133 | } 134 | } 135 | 136 | false 137 | } 138 | } 139 | 140 | struct Resolution<'a, P: Provider, R: Reporter> { 141 | state: ResolutionState, 142 | states: Vec>, 143 | graph: ExploredSpace, 144 | provider: &'a P, 145 | reporter: &'a R, 146 | } 147 | 148 | impl<'a, P: Provider, R> Resolution<'a, P, R> 149 | where 150 | P::Requirement: Copy + Hash + Eq, 151 | P::Candidate: Copy + Hash + Eq, 152 | P::Identifier: Copy + Hash + Eq, 153 | R: Reporter, 154 | { 155 | fn new(provider: &'a P, reporter: &'a R) -> Self { 156 | Self { 157 | state: ResolutionState { 158 | mapping: HashMap::new(), 159 | criteria: HashMap::new(), 160 | constraints: HashMap::new(), 161 | backtrack_causes: Vec::new(), 162 | pinned_candidate_stack: Vec::new(), 163 | }, 164 | states: Vec::new(), 165 | graph: ExploredSpace::new(), 166 | provider, 167 | reporter, 168 | } 169 | } 170 | 171 | fn resolve( 172 | mut self, 173 | requirements: Vec, 174 | max_rounds: u64, 175 | ) -> Result< 176 | ResolutionState, 177 | ResolutionError, 178 | > { 179 | self.reporter.starting(); 180 | 181 | // Initialize the root state 182 | for r in requirements { 183 | let req_info = RequirementInformation { 184 | requirement: r, 185 | parent: None, 186 | kind: RequirementKind::Dependency, 187 | }; 188 | let update_result = Resolution::create_or_update_criterion( 189 | self.provider, 190 | self.reporter, 191 | &mut self.graph, 192 | &mut self.state.criteria, 193 | req_info, 194 | ); 195 | 196 | if let Err(criterion) = update_result { 197 | return Err(ResolutionError::ResolutionImpossible( 198 | ResolutionImpossible::new(self.graph, criterion.information), 199 | )); 200 | } 201 | } 202 | 203 | // The root state is saved as a sentinel so the first ever pin can have 204 | // something to backtrack to if it fails. The root state is basically 205 | // pinning the virtual "root" package in the graph. 206 | self.push_new_state(); 207 | 208 | for i in 0..max_rounds { 209 | self.reporter.starting_round(i); 210 | 211 | // Note: using FxHashSet here for determinism 212 | let unsatisfied_names: FxHashSet<_> = self 213 | .state 214 | .criteria 215 | .iter() 216 | .filter(|(id, criterion)| !self.is_current_pin_satisfying(id, criterion)) 217 | .map(|(&id, _)| id) 218 | .collect(); 219 | 220 | // All criteria are accounted for. Nothing more to pin, we are done! 221 | if unsatisfied_names.is_empty() { 222 | self.reporter.ending(&self.state); 223 | return Ok(self.state); 224 | } 225 | 226 | // Keep track of satisfied names to calculate diff after pinning 227 | let satisfied_names = self 228 | .state 229 | .criteria 230 | .keys() 231 | .cloned() 232 | .filter(|name| !unsatisfied_names.contains(name)) 233 | .collect::>(); 234 | 235 | // Choose the most preferred unpinned criterion to try. 236 | let &name = unsatisfied_names 237 | .iter() 238 | .min_by_key(|&&x| { 239 | self.provider.get_preference( 240 | x, 241 | &self.state.mapping, 242 | &self.state.criteria, 243 | &self.state.backtrack_causes, 244 | ) 245 | }) 246 | .unwrap(); 247 | 248 | let result = self.attempt_to_pin_candidate(name); 249 | if let Err(failure_causes) = result { 250 | let causes: Vec<_> = failure_causes 251 | .iter() 252 | .flat_map(|c| &c.information) 253 | .cloned() 254 | .collect(); 255 | 256 | self.reporter.resolving_conflicts(&causes); 257 | 258 | // Backjump if pinning fails. The backjump process puts us in 259 | // an unpinned state, so we can work on it in the next round. 260 | // It will return an error if there are dead ends everywhere, 261 | // in which case we give up. 262 | let success = self.backjump(&causes)?; 263 | self.state.backtrack_causes = causes; 264 | 265 | if !success { 266 | return Err(ResolutionError::ResolutionImpossible( 267 | ResolutionImpossible::new(self.graph, self.state.backtrack_causes.clone()), 268 | )); 269 | } 270 | } else { 271 | // We just pinned a new candidate, and updated the criteria with the candidate's 272 | // dependencies and constraints. The resulting criteria must have at least one 273 | // candidate, but it is not required that existing mappings are still satisfied. In 274 | // fact, since we have more requirements now, there is always a chance that an 275 | // existing mapping becomes invalidated (i.e. is no longer satisfiable) 276 | let invalidated_names = self 277 | .state 278 | .criteria 279 | .iter() 280 | .filter(|(k, v)| { 281 | satisfied_names.contains(k) && !self.is_current_pin_satisfying(k, v) 282 | }) 283 | .map(|(&k, _)| k) 284 | .collect(); 285 | 286 | // Remove the requirements contributed by invalidated mappings 287 | self.remove_information_from_criteria(&invalidated_names); 288 | 289 | // Pinning was successful. Push a new state to do another pin. 290 | self.push_new_state() 291 | } 292 | 293 | self.reporter.ending_round(i); 294 | } 295 | 296 | Err(ResolutionError::ResolutionTooDeep(max_rounds)) 297 | } 298 | 299 | /// Remove requirements contributed by the specified parents 300 | fn remove_information_from_criteria(&mut self, parents: &HashSet) { 301 | if parents.is_empty() { 302 | return; 303 | } 304 | 305 | for criterion in self.state.criteria.values_mut() { 306 | criterion.information.retain(|information| { 307 | information.parent.map_or(true, |parent| { 308 | let id = self.provider.identify_candidate(parent); 309 | !parents.contains(&id) 310 | }) 311 | }) 312 | } 313 | } 314 | 315 | /// Adds the provided requirement to the criteria 316 | /// 317 | /// If a criterion already exists for the package identified by the requirement, it will be 318 | /// updated to include the new requirement. If no criterion exists yet, it will be created. 319 | /// 320 | /// The candidate list of the criterion becomes the result of [`Provider::find_matches`] 321 | fn create_or_update_criterion( 322 | provider: &P, 323 | reporter: &R, 324 | graph: &mut ExploredSpace, 325 | criteria: &mut HashMap>, 326 | req_info: RequirementInformation, 327 | ) -> Result<(), Criterion> { 328 | reporter.adding_requirement(&req_info); 329 | 330 | let requirement = req_info.requirement; 331 | let identifier = provider.identify_requirement(req_info.requirement); 332 | 333 | let mut all_requirements: HashMap<_, _> = criteria 334 | .iter() 335 | .map(|(&id, criterion)| (id, criterion.iter_requirement().collect())) 336 | .collect(); 337 | all_requirements 338 | .entry(identifier) 339 | .or_insert(Vec::new()) 340 | .push(requirement); 341 | 342 | let mut all_incompatibilities: HashMap<_, _> = criteria 343 | .iter() 344 | .map(|(&id, criterion)| (id, criterion.incompatibilities.clone())) 345 | .collect(); 346 | all_incompatibilities 347 | .entry(identifier) 348 | .or_insert(Vec::new()); 349 | 350 | // Update the criterion in the map, with the new req_info and candidates 351 | let criterion = criteria.entry(identifier).or_insert(Criterion::default()); 352 | let original_req_count = criterion.information.len(); 353 | criterion.information.push(req_info.clone()); 354 | 355 | let parent = req_info.parent.map_or(Node::Root, Node::Candidate); 356 | let parent = graph.get_or_add_node(parent); 357 | let candidates = 358 | provider.find_matches(identifier, &all_requirements, &all_incompatibilities); 359 | if candidates.is_empty() { 360 | if original_req_count != 0 { 361 | // The criterion used to have candidates, but after adding a new requirement there are 362 | // no candidates anymore. This means we have reached a conflict. 363 | for &candidate in &criterion.candidates { 364 | let child = graph.get_or_add_node(Node::Candidate(candidate)); 365 | graph.track_conflict(parent, child, req_info.requirement, req_info.kind); 366 | } 367 | } else if original_req_count == 0 { 368 | // This is the first requirement we add to the criterion, and it yields no candidates, 369 | // which means the dependency is missing 370 | graph.track_missing(parent, req_info.requirement, req_info.kind); 371 | } 372 | 373 | criterion.candidates.clear(); 374 | Err(criterion.clone()) 375 | } else { 376 | // Track candidates reached from this node 377 | for &candidate in &candidates { 378 | let child = graph.get_or_add_node(Node::Candidate(candidate)); 379 | graph.track_requirement(parent, child, req_info.requirement, req_info.kind); 380 | } 381 | 382 | criterion.candidates = candidates; 383 | Ok(()) 384 | } 385 | } 386 | 387 | /// Push a new state into history 388 | /// 389 | /// This new state will be used to hold resolution results of the next coming round 390 | fn push_new_state(&mut self) { 391 | // The new state is based off the current one 392 | let new_state = self.state.clone(); 393 | 394 | // Push the current state into history (the new state will now be the working state) 395 | let old_state = std::mem::replace(&mut self.state, new_state); 396 | self.states.push(old_state); 397 | } 398 | 399 | /// Restore a state from history 400 | fn restore_state(&mut self) { 401 | self.state = self.states.last().unwrap().clone(); 402 | } 403 | 404 | fn is_current_pin_satisfying( 405 | &self, 406 | id: &P::Identifier, 407 | criterion: &Criterion, 408 | ) -> bool { 409 | if let Some(¤t_pin) = self.state.mapping.get(id) { 410 | criterion 411 | .iter_requirement() 412 | .all(|r| self.provider.is_satisfied_by(r, current_pin)) 413 | } else { 414 | false 415 | } 416 | } 417 | 418 | /// Attempts to find a suitable candidate for the package identified by `id` 419 | /// 420 | /// If a candidate is found, update the state and return `Ok`. Otherwise, return 421 | /// an `Err` with the criteria that caused candidates to be discarded 422 | fn attempt_to_pin_candidate( 423 | &mut self, 424 | id: P::Identifier, 425 | ) -> Result<(), Vec>> { 426 | let criterion = &self.state.criteria[&id]; 427 | 428 | let mut causes = Vec::new(); 429 | for &candidate in &criterion.candidates { 430 | let mut updated_criteria = self.state.criteria.clone(); 431 | 432 | // Update constraints with those from the candidate we are attempting to pin 433 | let mut updated_constraints = self.state.constraints.clone(); 434 | let result = Resolution::update_constraints( 435 | self.provider, 436 | self.reporter, 437 | &mut self.graph, 438 | &mut updated_criteria, 439 | &mut updated_constraints, 440 | candidate, 441 | ); 442 | if let Err(e) = result { 443 | causes.push(e); 444 | continue; 445 | } 446 | 447 | // Update the criteria 448 | let result = Resolution::update_requirements( 449 | self.provider, 450 | self.reporter, 451 | &mut self.graph, 452 | &mut updated_criteria, 453 | &updated_constraints, 454 | candidate, 455 | ); 456 | if let Err(e) = result { 457 | causes.push(e); 458 | continue; 459 | } 460 | 461 | // Check the newly-pinned candidate actually works. This should 462 | // always pass under normal circumstances, but in the case of a 463 | // faulty provider, we will raise an error to notify the implementer 464 | // to fix find_matches() and/or is_satisfied_by(). 465 | let mut unsatisfied = Vec::new(); 466 | for r in criterion.iter_requirement() { 467 | if !self.provider.is_satisfied_by(r, candidate) { 468 | unsatisfied.push(r); 469 | } 470 | } 471 | if !unsatisfied.is_empty() { 472 | self.provider 473 | .on_inconsistent_candidate(candidate, unsatisfied); 474 | panic!("inconsistent candidate"); 475 | } 476 | 477 | self.reporter.pinning(candidate); 478 | 479 | // Add/update criteria 480 | for (id, criterion) in updated_criteria { 481 | self.state.criteria.insert(id, criterion); 482 | } 483 | 484 | // Put newly-pinned candidate at the end. This is essential because 485 | // backtracking looks at this mapping to get the last pin. 486 | self.state.pinned_candidate_stack.push(id); 487 | 488 | // Keep track of the chosen candidates 489 | self.state.mapping.insert(id, candidate); 490 | 491 | return Ok(()); 492 | } 493 | 494 | Err(causes) 495 | } 496 | 497 | /// Updates the criteria to satisfy the candidate's dependencies and constraints 498 | /// 499 | /// If the result is unsatisfiable, returns an `Err` containing the first criterion that had no 500 | /// candidates left after being updated 501 | fn update_requirements( 502 | provider: &P, 503 | reporter: &R, 504 | graph: &mut ExploredSpace, 505 | criteria: &mut HashMap>, 506 | constraints: &HashMap< 507 | P::Identifier, 508 | Vec>, 509 | >, 510 | candidate: P::Candidate, 511 | ) -> Result<(), Criterion> { 512 | for requirement in provider.get_dependencies(candidate) { 513 | let identifier = provider.identify_requirement(requirement); 514 | if !criteria.contains_key(&identifier) { 515 | // If there is no criterion for this package, we will need to create it with the 516 | // relevant constraints 517 | if let Some(constraints) = constraints.get(&identifier) { 518 | for constraint in constraints { 519 | Resolution::create_or_update_criterion( 520 | provider, 521 | reporter, 522 | graph, 523 | criteria, 524 | constraint.clone(), 525 | )?; 526 | } 527 | } 528 | } 529 | 530 | let req_info = RequirementInformation { 531 | requirement, 532 | parent: Some(candidate), 533 | kind: RequirementKind::Dependency, 534 | }; 535 | 536 | // Update the criterion 537 | Resolution::create_or_update_criterion(provider, reporter, graph, criteria, req_info)?; 538 | } 539 | 540 | Ok(()) 541 | } 542 | 543 | /// Tracks constraints contributed by the candidate, and updates each constraint's corresponding 544 | /// criterion, if it exists 545 | /// 546 | /// If the result is unsatisfiable, returns an `Err` containing the first criterion that had no 547 | /// candidates left after being constrained 548 | fn update_constraints( 549 | provider: &P, 550 | reporter: &R, 551 | graph: &mut ExploredSpace, 552 | criteria: &mut HashMap>, 553 | constraints: &mut HashMap< 554 | P::Identifier, 555 | Vec>, 556 | >, 557 | candidate: P::Candidate, 558 | ) -> Result<(), Criterion> { 559 | for requirement in provider.get_constraints(candidate) { 560 | let req_info = RequirementInformation { 561 | requirement, 562 | parent: Some(candidate), 563 | kind: RequirementKind::Constraint, 564 | }; 565 | 566 | // Track the constraint 567 | let identifier = provider.identify_requirement(requirement); 568 | constraints 569 | .entry(identifier) 570 | .or_insert(Vec::new()) 571 | .push(req_info.clone()); 572 | 573 | // Update the constraint's criterion, if it exists 574 | if criteria.contains_key(&identifier) { 575 | Resolution::create_or_update_criterion( 576 | provider, reporter, graph, criteria, req_info, 577 | )?; 578 | } 579 | } 580 | 581 | Ok(()) 582 | } 583 | 584 | fn backjump( 585 | &mut self, 586 | causes: &[RequirementInformation], 587 | ) -> Result> { 588 | // When we enter here, the stack is like this:: 589 | // 590 | // [ state Z ] 591 | // [ state Y ] 592 | // [ state X ] 593 | // .... earlier states are irrelevant. 594 | // 595 | // 1. No pins worked for Z, so it does not have a pin. 596 | // 2. We want to reset state Y to unpinned, and pin another candidate. 597 | // 3. State X holds what state Y was before the pin, but does not 598 | // have the incompatibility information gathered in state Y. 599 | // 600 | // Each iteration of the loop will: 601 | // 602 | // 1. Identify Z. The incompatibility is not always caused by the latest 603 | // state. For example, given three requirements A, B and C, with 604 | // dependencies A1, B1 and C1, where A1 and B1 are incompatible: the 605 | // last state might be related to C, so we want to discard the 606 | // previous state. 607 | // 2. Discard Z. 608 | // 3. Discard Y but remember its incompatibility information gathered 609 | // previously, and the failure we're dealing with right now. 610 | // 4. Push a new state Y' based on X, and apply the incompatibility 611 | // information from Y to Y'. 612 | // 5a. If this causes Y' to conflict, we need to backtrack again. Make Y' 613 | // the new Z and go back to step 2. 614 | // 5b. If the incompatibilities apply cleanly, end backtracking. 615 | let incompatible_candidates = causes 616 | .iter() 617 | .flat_map(|c| c.parent) 618 | .map(|c| self.provider.identify_candidate(c)); 619 | 620 | let incompatible_reqs = causes 621 | .iter() 622 | .map(|c| self.provider.identify_requirement(c.requirement)); 623 | 624 | let incompatible_deps: HashSet<_> = 625 | incompatible_candidates.chain(incompatible_reqs).collect(); 626 | 627 | let mut i = 1; 628 | while self.states.len() >= 2 { 629 | i += 1; 630 | 631 | // Ensure to backtrack to a state that caused the incompatibility 632 | let (broken_state, candidate_id, broken_candidate) = loop { 633 | // Retrieve the last candidate pin and known incompatibilities. 634 | if let Some(mut broken_state) = self.states.pop() { 635 | if let Some(candidate_id) = broken_state.pinned_candidate_stack.pop() { 636 | let candidate = broken_state.mapping[&candidate_id]; 637 | let mut current_dependencies = self 638 | .provider 639 | .get_dependencies(candidate) 640 | .into_iter() 641 | .map(|dep| self.provider.identify_requirement(dep)); 642 | 643 | let incompatible_state = 644 | current_dependencies.any(|dep| incompatible_deps.contains(&dep)); 645 | if incompatible_state { 646 | break (broken_state, candidate_id, candidate); 647 | } 648 | 649 | continue; 650 | } 651 | } 652 | 653 | // Unable to backtrack anymore 654 | return Err(ResolutionError::ResolutionImpossible( 655 | ResolutionImpossible::new(self.graph.clone(), causes.to_vec()), 656 | )); 657 | }; 658 | 659 | let mut incompatibilities_from_broken: HashMap<_, _> = broken_state 660 | .criteria 661 | .into_iter() 662 | .map(|(key, value)| (key, value.incompatibilities)) 663 | .collect(); 664 | incompatibilities_from_broken 665 | .entry(candidate_id) 666 | .or_insert(Vec::new()) 667 | .push(broken_candidate); 668 | 669 | self.restore_state(); 670 | 671 | let success = self.patch_criteria(&incompatibilities_from_broken); 672 | 673 | // It works! Let's work on this new state. 674 | if success { 675 | self.reporter.backtracked(i); 676 | return Ok(true); 677 | } 678 | } 679 | 680 | Ok(false) 681 | } 682 | 683 | fn patch_criteria( 684 | &mut self, 685 | incompatibilities_from_broken: &HashMap>, 686 | ) -> bool { 687 | for (&k, incompatibilities) in incompatibilities_from_broken { 688 | if incompatibilities.is_empty() { 689 | continue; 690 | } 691 | 692 | let criterion = match self.state.criteria.get(&k) { 693 | Some(c) => c, 694 | None => continue, 695 | }; 696 | 697 | // TODO: can we call find_matches without allocating? 698 | let requirements = self 699 | .state 700 | .criteria 701 | .iter() 702 | .map(|(&id, criterion)| (id, criterion.iter_requirement().collect::>())) 703 | .collect(); 704 | 705 | let mut all_incompatibilities: HashMap<_, _> = self 706 | .state 707 | .criteria 708 | .iter() 709 | .map(|(&id, criterion)| (id, criterion.incompatibilities.clone())) 710 | .collect(); 711 | all_incompatibilities 712 | .entry(k) 713 | .or_insert(Vec::new()) 714 | .extend(incompatibilities); 715 | 716 | let candidates = self 717 | .provider 718 | .find_matches(k, &requirements, &all_incompatibilities); 719 | 720 | if candidates.is_empty() { 721 | return false; 722 | } 723 | 724 | let incompatibilities = incompatibilities 725 | .iter() 726 | .cloned() 727 | .chain(criterion.incompatibilities.iter().cloned()) 728 | .collect(); 729 | 730 | // Now update the criterion with relevant incompatibilities and the resulting set of 731 | // candidates 732 | let criterion = self.state.criteria.get_mut(&k).unwrap(); 733 | criterion.candidates = candidates; 734 | criterion.incompatibilities = incompatibilities; 735 | } 736 | 737 | true 738 | } 739 | } 740 | 741 | pub struct Resolver<'a, P: Provider, R: Reporter> { 742 | provider: &'a P, 743 | reporter: &'a R, 744 | } 745 | 746 | impl<'a, P: Provider, R: Reporter> Resolver<'a, P, R> 747 | where 748 | P::Requirement: Copy + Hash + Eq, 749 | P::Candidate: Copy + Hash + Eq, 750 | P::Identifier: Copy + Hash + Eq + Ord, 751 | R: Reporter, 752 | { 753 | pub fn new(provider: &'a P, reporter: &'a R) -> Self { 754 | Self { provider, reporter } 755 | } 756 | 757 | pub fn resolve( 758 | self, 759 | requirements: Vec, 760 | ) -> Result< 761 | ResolutionResult, 762 | ResolutionError, 763 | > { 764 | self.resolve_bounded(requirements, 100) 765 | } 766 | 767 | pub fn resolve_bounded( 768 | self, 769 | requirements: Vec, 770 | max_rounds: u64, 771 | ) -> Result< 772 | ResolutionResult, 773 | ResolutionError, 774 | > { 775 | let resolution = Resolution::new(self.provider, self.reporter); 776 | let state = resolution.resolve(requirements, max_rounds)?; 777 | Ok(state.build_result(self.provider)) 778 | } 779 | } 780 | 781 | #[derive(Clone, Debug, Hash, PartialEq, Eq)] 782 | pub struct RequirementInformation { 783 | pub requirement: TRequirement, 784 | pub parent: Option, 785 | pub kind: RequirementKind, 786 | } 787 | 788 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 789 | pub enum RequirementKind { 790 | Dependency, 791 | Constraint, 792 | } 793 | -------------------------------------------------------------------------------- /tests/in_memory_provider.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::{HashMap, HashSet}; 3 | use std::fmt::Display; 4 | use std::ops::Range; 5 | 6 | use resolvelib_rs::{ 7 | Provider, Reporter, ResolutionError, ResolutionImpossible, ResolutionResult, Resolver, 8 | }; 9 | 10 | #[derive(Debug, PartialEq, Eq, Hash)] 11 | struct Candidate { 12 | package_name: String, 13 | version: u64, 14 | deps: Vec, 15 | constraints: Vec, 16 | } 17 | 18 | #[derive(Debug, PartialEq, Eq, Hash)] 19 | struct Requirement { 20 | package_name: String, 21 | specifier: Range, 22 | } 23 | 24 | fn pkg(package_name: &str, version: u64, deps: Vec) -> Candidate { 25 | Candidate { 26 | package_name: package_name.to_string(), 27 | version, 28 | deps, 29 | constraints: Vec::new(), 30 | } 31 | } 32 | 33 | fn pkg2( 34 | package_name: &str, 35 | version: u64, 36 | deps: Vec, 37 | constraints: Vec, 38 | ) -> Candidate { 39 | Candidate { 40 | package_name: package_name.to_string(), 41 | version, 42 | deps, 43 | constraints, 44 | } 45 | } 46 | 47 | fn req(package_name: &str, specifier: Range) -> Requirement { 48 | Requirement { 49 | package_name: package_name.to_string(), 50 | specifier, 51 | } 52 | } 53 | 54 | struct TrackingReporter<'a> { 55 | operations: RefCell>>, 56 | } 57 | 58 | impl TrackingReporter<'_> { 59 | fn new() -> Self { 60 | Self { 61 | operations: RefCell::new(Vec::new()), 62 | } 63 | } 64 | } 65 | 66 | impl<'a> Reporter for TrackingReporter<'a> { 67 | type Requirement = &'a Requirement; 68 | type Candidate = &'a Candidate; 69 | type Identifier = &'a str; 70 | 71 | fn backtracked(&self, steps: u64) { 72 | self.operations 73 | .borrow_mut() 74 | .push(Operation::Backtrack(steps)); 75 | } 76 | 77 | fn pinning(&self, candidate: Self::Candidate) { 78 | self.operations 79 | .borrow_mut() 80 | .push(Operation::PinCandidate(candidate)); 81 | } 82 | } 83 | 84 | #[derive(Debug)] 85 | enum Operation<'a> { 86 | PinCandidate(&'a Candidate), 87 | Backtrack(u64), 88 | } 89 | 90 | impl<'a> ToString for Operation<'a> { 91 | fn to_string(&self) -> String { 92 | use Operation::*; 93 | match self { 94 | Backtrack(steps) => format!("backtrack {steps}"), 95 | PinCandidate(candidate) => { 96 | format!("pin {}={}", candidate.package_name, candidate.version) 97 | } 98 | } 99 | } 100 | } 101 | 102 | #[derive(Default)] 103 | struct InMemoryProvider<'a> { 104 | candidates: HashMap<(&'a str, u64), &'a Candidate>, 105 | } 106 | 107 | impl<'a> InMemoryProvider<'a> { 108 | fn from_candidates(candidates: &'a [Candidate]) -> Self { 109 | InMemoryProvider { 110 | candidates: candidates 111 | .iter() 112 | .map(|c| ((c.package_name.as_str(), c.version), c)) 113 | .collect(), 114 | } 115 | } 116 | } 117 | 118 | impl<'a> Provider for InMemoryProvider<'a> { 119 | type Candidate = &'a Candidate; 120 | type Requirement = &'a Requirement; 121 | type Identifier = &'a str; 122 | 123 | fn identify_candidate(&self, candidate: Self::Candidate) -> Self::Identifier { 124 | &candidate.package_name 125 | } 126 | 127 | fn identify_requirement(&self, requirement: Self::Requirement) -> Self::Identifier { 128 | &requirement.package_name 129 | } 130 | 131 | fn find_matches( 132 | &self, 133 | identifier: Self::Identifier, 134 | requirements: &HashMap>, 135 | incompatibilities: &HashMap>, 136 | ) -> Vec { 137 | // Find all possible candidates that satisfy the given constraints 138 | let requirements = &requirements[&identifier]; 139 | 140 | // For each requirement, derive candidates 141 | let mut all_candidates = HashSet::new(); 142 | 143 | for (i, requirement) in requirements.into_iter().enumerate() { 144 | let incompatibilities = &incompatibilities[requirement.package_name.as_str()]; 145 | let incompatible_versions: HashSet<_> = 146 | incompatibilities.iter().map(|i| i.version).collect(); 147 | 148 | // Consider only candidates that actually exist and that are not incompatible 149 | let new_candidates: HashSet<_> = requirement 150 | .specifier 151 | .clone() 152 | .rev() // Highest versions come first so they are preferred (the returned candidates should be ordered by preference) 153 | .filter_map(|version| { 154 | self.candidates 155 | .get(&(requirement.package_name.as_str(), version)) 156 | }) 157 | .filter(|candidate| !incompatible_versions.contains(&candidate.version)) 158 | .cloned() 159 | .collect(); 160 | 161 | if i == 0 { 162 | all_candidates = new_candidates; 163 | if all_candidates.is_empty() { 164 | assert_eq!(requirements.len(), 1); 165 | break; 166 | } 167 | } else { 168 | all_candidates.retain(|c| new_candidates.contains(c)); 169 | } 170 | } 171 | 172 | let mut all_candidates: Vec<_> = all_candidates.into_iter().collect(); 173 | all_candidates.sort_by(|c1, c2| c2.version.cmp(&c1.version)); 174 | all_candidates 175 | } 176 | 177 | fn is_satisfied_by(&self, requirement: Self::Requirement, candidate: Self::Candidate) -> bool { 178 | // The candidate is guaranteed to have been generated from the requirement, so we 179 | // only need to check the version specifier 180 | assert_eq!(requirement.package_name, candidate.package_name); 181 | requirement.specifier.contains(&candidate.version) 182 | } 183 | 184 | fn get_dependencies(&self, candidate: Self::Candidate) -> Vec { 185 | candidate.deps.iter().collect() 186 | } 187 | 188 | fn get_constraints(&self, candidate: Self::Candidate) -> Vec { 189 | candidate.constraints.iter().collect() 190 | } 191 | 192 | fn on_inconsistent_candidate( 193 | &self, 194 | candidate: Self::Candidate, 195 | requirements: Vec, 196 | ) { 197 | panic!("Inconsistent candidate: {candidate:?} does not satisfy {requirements:?}"); 198 | } 199 | } 200 | 201 | fn resolve<'a>( 202 | reqs: &'a [Requirement], 203 | pkgs: &'a [Candidate], 204 | ) -> ( 205 | ResolutionResult<&'a Requirement, &'a Candidate, &'a str>, 206 | Vec>, 207 | ) { 208 | let (result, operations) = try_resolve_and_report(reqs, pkgs); 209 | (result.ok().unwrap(), operations) 210 | } 211 | 212 | fn resolve_fail<'a>( 213 | reqs: &'a [Requirement], 214 | pkgs: &'a [Candidate], 215 | ) -> ResolutionImpossible<&'a Requirement, &'a Candidate> { 216 | let result = try_resolve(&reqs, &pkgs); 217 | match result { 218 | Ok(_) => panic!("Expected error, but dependency resolution was successful!"), 219 | Err(ResolutionError::ResolutionImpossible(err)) => err, 220 | Err(_) => panic!("Unexpected error kind!"), 221 | } 222 | } 223 | 224 | fn try_resolve<'a>( 225 | reqs: &'a [Requirement], 226 | pkgs: &'a [Candidate], 227 | ) -> Result< 228 | ResolutionResult<&'a Requirement, &'a Candidate, &'a str>, 229 | ResolutionError<&'a Requirement, &'a Candidate>, 230 | > { 231 | let (result, _) = try_resolve_and_report(reqs, pkgs); 232 | result 233 | } 234 | 235 | fn try_resolve_and_report<'a>( 236 | reqs: &'a [Requirement], 237 | pkgs: &'a [Candidate], 238 | ) -> ( 239 | Result< 240 | ResolutionResult<&'a Requirement, &'a Candidate, &'a str>, 241 | ResolutionError<&'a Requirement, &'a Candidate>, 242 | >, 243 | Vec>, 244 | ) { 245 | let p = InMemoryProvider::from_candidates(pkgs); 246 | let r = TrackingReporter::new(); 247 | let resolver = Resolver::new(&p, &r); 248 | let result = resolver.resolve(reqs.iter().collect()); 249 | (result, r.operations.into_inner()) 250 | } 251 | 252 | fn user_friendly_error<'a>( 253 | err: &ResolutionImpossible<&'a Requirement, &'a Candidate>, 254 | ) -> impl Display { 255 | err.graph().print_user_friendly_error( 256 | |c| format!("{}", c.package_name), 257 | |c| format!("{}", c.version), 258 | |c| c.version, 259 | |r| format!("{} {:?}", r.package_name, r.specifier), 260 | |&r| (&r.package_name, r.specifier.start, r.specifier.end), 261 | ) 262 | } 263 | 264 | #[test] 265 | fn resolve_empty() { 266 | let (result, ops) = resolve(&[], &[]); 267 | 268 | assert_eq!(ops.len(), 0); 269 | assert_eq!(result.mapping.len(), 0); 270 | assert_eq!(result.criteria.len(), 0); 271 | assert_eq!(result.graph.node_count(), 1); 272 | } 273 | 274 | #[test] 275 | fn resolve_single() -> anyhow::Result<()> { 276 | let reqs = vec![req("python", 5..10)]; 277 | let pkgs = vec![pkg("python", 9, vec![]), pkg("python", 10, vec![])]; 278 | 279 | let (result, ops) = resolve(&reqs, &pkgs); 280 | 281 | // Operations 282 | check_ops( 283 | &ops, 284 | r" 285 | pin python=9 286 | ", 287 | ); 288 | 289 | // Outcome 290 | assert_eq!(result.mapping.len(), 1); 291 | 292 | let found_candidate = result.mapping["python"]; 293 | assert_eq!(found_candidate.package_name, "python"); 294 | assert_eq!(found_candidate.version, 9); 295 | 296 | Ok(()) 297 | } 298 | 299 | #[test] 300 | fn resolve_non_existent() { 301 | let reqs = vec![req("python", 0..10)]; 302 | let err = resolve_fail(&reqs, &[]); 303 | 304 | let unsatisfied = err.unsatisfied_requirements(); 305 | assert_eq!(unsatisfied.len(), 1); 306 | assert_eq!(unsatisfied[0].parent, None); 307 | assert_eq!(unsatisfied[0].requirement.package_name, "python"); 308 | assert_eq!(unsatisfied[0].requirement.specifier, 0..10); 309 | } 310 | 311 | #[test] 312 | fn resolve_unsatisfiable_root() { 313 | let reqs = vec![req("python", 0..10)]; 314 | let pkgs = vec![pkg("python", 42, vec![])]; 315 | let err = resolve_fail(&reqs, &pkgs); 316 | 317 | let unsatisfied = err.unsatisfied_requirements(); 318 | assert_eq!(unsatisfied.len(), 1); 319 | assert_eq!(unsatisfied[0].parent, None); 320 | assert_eq!(unsatisfied[0].requirement.package_name, "python"); 321 | assert_eq!(unsatisfied[0].requirement.specifier, 0..10); 322 | } 323 | 324 | #[test] 325 | fn resolve_unsatisfiable_dep() { 326 | let reqs = vec![req("python", 0..10)]; 327 | let pkgs = vec![pkg("python", 8, vec![req("foo", 2..4)])]; 328 | let err = resolve_fail(&reqs, &pkgs); 329 | 330 | let unsatisfied = err.unsatisfied_requirements(); 331 | assert_eq!(unsatisfied.len(), 1); 332 | assert_eq!(unsatisfied[0].parent.unwrap(), &pkgs[0]); 333 | assert_eq!(unsatisfied[0].requirement.package_name, "foo"); 334 | assert_eq!(unsatisfied[0].requirement.specifier, 2..4); 335 | } 336 | 337 | #[test] 338 | fn resolve_complex() { 339 | let reqs = vec![req("python", 0..10), req("some-lib", 12..15)]; 340 | 341 | let pkgs = vec![ 342 | // Available versions of python 343 | pkg("python", 6, vec![req("foo", 2..3)]), 344 | pkg("python", 8, vec![req("foo", 2..4)]), 345 | // Available versions of foo 346 | pkg("foo", 2, vec![]), 347 | pkg("foo", 3, vec![]), 348 | // Available versions of some-lib 349 | pkg("some-lib", 12, vec![req("python", 5..7)]), 350 | pkg("some-lib", 15, vec![req("python", 8..10)]), 351 | ]; 352 | 353 | let (result, ops) = resolve(&reqs, &pkgs); 354 | check_ops( 355 | &ops, 356 | r" 357 | pin some-lib=12 358 | pin python=6 359 | pin foo=2 360 | ", 361 | ); 362 | 363 | assert_eq!(result.mapping.len(), 3); 364 | assert_eq!(result.criteria.len(), 3); 365 | assert_eq!(result.graph.node_count(), 4); 366 | 367 | // Expected mappings 368 | assert_eq!(result.mapping["python"].version, 6); 369 | assert_eq!(result.mapping["foo"].version, 2); 370 | assert_eq!(result.mapping["some-lib"].version, 12); 371 | 372 | // Python criterion 373 | let python_c = &result.criteria["python"]; 374 | assert_eq!(python_c.candidates.len(), 1); 375 | assert_eq!(python_c.information.len(), 2); 376 | assert_eq!( 377 | python_c.information[0] 378 | .parent 379 | .map(|p| p.package_name.as_str()), 380 | None 381 | ); 382 | assert_eq!( 383 | python_c.information[1] 384 | .parent 385 | .map(|p| p.package_name.as_str()), 386 | Some("some-lib") 387 | ); 388 | assert_eq!(python_c.incompatibilities.len(), 0); 389 | 390 | // Graph, topologically sorted (installation order would be from right to left) 391 | let topo_sorted = petgraph::algo::toposort(&result.graph, None).unwrap(); 392 | assert_eq!( 393 | topo_sorted, 394 | &[None, Some("some-lib"), Some("python"), Some("foo")] 395 | ); 396 | } 397 | 398 | #[test] 399 | fn resolve_with_inactive_constraints() { 400 | let reqs = vec![req("A", 0..10)]; 401 | 402 | let pkgs = vec![ 403 | pkg("A", 5, vec![req("B", 0..10)]), 404 | pkg2("B", 2, vec![], vec![req("C", 0..10)]), 405 | pkg("C", 42, vec![]), 406 | ]; 407 | 408 | // Package C is not required, so it won't be resolved 409 | let (result, _) = resolve(&reqs, &pkgs); 410 | assert_eq!(result.mapping["A"].version, 5); 411 | assert_eq!(result.mapping["B"].version, 2); 412 | assert!(!result.mapping.contains_key("C")); 413 | } 414 | 415 | #[test] 416 | fn resolve_with_active_constraints() { 417 | let reqs = vec![req("A", 0..10)]; 418 | 419 | let pkgs = vec![ 420 | pkg("A", 5, vec![req("B", 0..10), req("C", 9..15)]), 421 | pkg2("B", 2, vec![], vec![req("C", 0..10)]), 422 | pkg("C", 12, vec![]), 423 | pkg("C", 9, vec![]), 424 | ]; 425 | 426 | // Package C is required, so it will be constrained to 0..10 427 | let (result, _) = resolve(&reqs, &pkgs); 428 | assert_eq!(result.mapping["A"].version, 5); 429 | assert_eq!(result.mapping["B"].version, 2); 430 | assert_eq!(result.mapping["C"].version, 9); 431 | } 432 | 433 | #[test] 434 | fn resolve_backtrack() { 435 | // This is the dependency tree: 436 | // 437 | // A 438 | // / \ 439 | // B C 440 | // | | 441 | // E E 442 | // 443 | // B prefers the latest version of E, which will be picked first 444 | // C requires an older version of E, so it will cause a backtrack 445 | 446 | let reqs = vec![req("A", 0..10)]; 447 | 448 | let packages = vec![ 449 | // A 450 | pkg("A", 6, vec![req("B", 0..10), req("C", 0..10)]), 451 | // B 452 | pkg("B", 9, vec![req("E", 9..10)]), 453 | pkg("B", 8, vec![req("E", 8..9)]), 454 | // C 455 | pkg("C", 9, vec![req("E", 0..9)]), 456 | pkg("C", 8, vec![req("E", 0..9)]), 457 | pkg("C", 7, vec![req("E", 0..9)]), 458 | // E 459 | pkg("E", 9, vec![]), 460 | pkg("E", 8, vec![]), 461 | ]; 462 | 463 | let (solution, ops) = resolve(&reqs, &packages); 464 | 465 | // Operations 466 | check_ops( 467 | &ops, 468 | r" 469 | pin A=6 470 | pin B=9 471 | pin E=9 472 | backtrack 2 473 | pin B=8 474 | pin E=8 475 | pin C=9 476 | ", 477 | ); 478 | 479 | // Solution 480 | assert_eq!(solution.mapping["A"].version, 6); 481 | assert_eq!(solution.mapping["B"].version, 8); 482 | assert_eq!(solution.mapping["C"].version, 9); 483 | assert_eq!(solution.mapping["E"].version, 8); 484 | } 485 | 486 | #[test] 487 | fn resolve_cyclic() { 488 | let pkgs = vec![ 489 | pkg("A", 2, vec![req("B", 0..10)]), 490 | pkg("B", 5, vec![req("A", 2..4)]), 491 | ]; 492 | let reqs = vec![req("A", 0..99)]; 493 | 494 | let (result, _) = resolve(&reqs, &pkgs); 495 | assert_eq!(result.mapping.len(), 2); 496 | assert_eq!(result.mapping["A"].version, 2); 497 | assert_eq!(result.mapping["B"].version, 5); 498 | } 499 | 500 | #[test] 501 | fn error_reporting_root_conflict() { 502 | let pkgs = vec![pkg("A", 2, vec![]), pkg("A", 5, vec![])]; 503 | let reqs = vec![req("A", 0..4), req("A", 5..10)]; 504 | 505 | let err = resolve_fail(&reqs, &pkgs); 506 | insta::assert_display_snapshot!(user_friendly_error(&err)); 507 | } 508 | 509 | #[test] 510 | fn error_reporting_missing_1() { 511 | let pkgs = vec![pkg("A", 41, vec![req("B", 15..16)]), pkg("B", 15, vec![])]; 512 | let reqs = vec![req("A", 41..42), req("B", 14..15)]; 513 | 514 | let err = resolve_fail(&reqs, &pkgs); 515 | insta::assert_display_snapshot!(user_friendly_error(&err)); 516 | } 517 | 518 | #[test] 519 | fn error_reporting_missing_2() { 520 | let pkgs = vec![pkg("A", 41, vec![req("B", 0..20)])]; 521 | let reqs = vec![req("A", 0..999)]; 522 | 523 | let err = resolve_fail(&reqs, &pkgs); 524 | insta::assert_display_snapshot!(user_friendly_error(&err)); 525 | } 526 | 527 | #[test] 528 | fn error_reporting_bluesky_conflict() { 529 | let pkgs = vec![ 530 | pkg("suitcase-utils", 54, vec![]), 531 | pkg("suitcase-utils", 53, vec![]), 532 | pkg( 533 | "bluesky-widgets", 534 | 42, 535 | vec![ 536 | req("bluesky-live", 0..10), 537 | req("numpy", 0..10), 538 | req("python", 0..10), 539 | req("suitcase-utils", 0..54), 540 | ], 541 | ), 542 | pkg("bluesky-live", 1, vec![]), 543 | pkg("numpy", 1, vec![]), 544 | pkg("python", 1, vec![]), 545 | ]; 546 | 547 | let reqs = vec![req("bluesky-widgets", 0..99), req("suitcase-utils", 54..99)]; 548 | 549 | let err = resolve_fail(&reqs, &pkgs); 550 | insta::assert_display_snapshot!(user_friendly_error(&err)); 551 | } 552 | 553 | #[test] 554 | fn error_reporting_pubgrub_article() { 555 | // Taken from the pubgrub article: https://nex3.medium.com/pubgrub-2fb6470504f 556 | let pkgs = vec![ 557 | pkg("menu", 150, vec![req("dropdown", 200..231)]), 558 | pkg("menu", 100, vec![req("dropdown", 180..200)]), 559 | pkg("dropdown", 230, vec![req("icons", 200..201)]), 560 | pkg("dropdown", 180, vec![req("intl", 300..301)]), 561 | pkg("icons", 200, vec![]), 562 | pkg("icons", 100, vec![]), 563 | pkg("intl", 500, vec![]), 564 | pkg("intl", 300, vec![]), 565 | ]; 566 | 567 | let reqs = vec![ 568 | req("menu", 0..999), 569 | req("icons", 100..101), 570 | req("intl", 500..501), 571 | ]; 572 | 573 | let err = resolve_fail(&reqs, &pkgs); 574 | insta::assert_display_snapshot!(user_friendly_error(&err)); 575 | } 576 | 577 | #[test] 578 | fn error_reporting_graph_compression_simple() { 579 | let pkgs = vec![ 580 | pkg("A", 10, vec![req("B", 0..99)]), 581 | pkg("A", 9, vec![req("B", 0..99)]), 582 | pkg("B", 100, vec![]), 583 | pkg("B", 42, vec![]), 584 | ]; 585 | 586 | let reqs = vec![req("A", 0..99), req("B", 100..999)]; 587 | 588 | let err = resolve_fail(&reqs, &pkgs); 589 | insta::assert_display_snapshot!(user_friendly_error(&err)); 590 | } 591 | 592 | #[test] 593 | fn error_reporting_cyclic() { 594 | let pkgs = vec![ 595 | pkg("A", 5, vec![req("B", 10..20)]), 596 | pkg("B", 10, vec![req("A", 2..4)]), 597 | pkg("C", 50, vec![req("A", 5..10)]), 598 | ]; 599 | let reqs = vec![req("C", 50..55)]; 600 | 601 | let err = resolve_fail(&reqs, &pkgs); 602 | insta::assert_display_snapshot!(user_friendly_error(&err)); 603 | } 604 | 605 | fn check_ops(ops: &[Operation], expected: &str) { 606 | let expected: Vec<_> = expected 607 | .lines() 608 | .map(|l| l.trim()) 609 | .filter(|l| !l.is_empty()) 610 | .collect(); 611 | for (op, &line) in ops.into_iter().zip(&expected) { 612 | let op_str = op.to_string(); 613 | assert_eq!(op_str, line); 614 | } 615 | 616 | if expected.len() > ops.len() { 617 | panic!( 618 | "Expected {}, but found {} actual operations!", 619 | expected.len(), 620 | ops.len() 621 | ); 622 | } else if expected.len() < ops.len() { 623 | panic!( 624 | "Operations match, but there are {} more actual operations. The next one is {}", 625 | ops.len() - expected.len(), 626 | ops[expected.len()].to_string() 627 | ); 628 | } 629 | } 630 | -------------------------------------------------------------------------------- /tests/snapshots/in_memory_provider__error_reporting_bluesky_conflict.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/in_memory_provider.rs 3 | expression: error 4 | --- 5 | The following packages are incompatible 6 | |-- suitcase-utils 54..99 will be installed; 7 | |-- bluesky-widgets 0..99 cannot be installed because: 8 | |-- bluesky-widgets 42 would require 9 | |-- suitcase-utils 0..54, which conflicts with the already selected version. 10 | 11 | -------------------------------------------------------------------------------- /tests/snapshots/in_memory_provider__error_reporting_cyclic.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/in_memory_provider.rs 3 | expression: user_friendly_error(&err) 4 | --- 5 | The following packages are incompatible 6 | |-- C 50..55 cannot be installed because: 7 | |-- C 50 would require 8 | |-- A 5..10, which cannot be installed because: 9 | |-- A 5 would require 10 | |-- B 10..20, which cannot be installed because: 11 | |-- B 10 would require 12 | |-- A 2..4, which conflicts with the already selected version. 13 | 14 | -------------------------------------------------------------------------------- /tests/snapshots/in_memory_provider__error_reporting_graph_compression_simple.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/in_memory_provider.rs 3 | expression: user_friendly_error(&err) 4 | --- 5 | The following packages are incompatible 6 | |-- B 100..999 will be installed; 7 | |-- A 0..99 cannot be installed because: 8 | |-- A 9 | 10 would require 9 | |-- B 0..99, which conflicts with the already selected version. 10 | 11 | -------------------------------------------------------------------------------- /tests/snapshots/in_memory_provider__error_reporting_missing_1.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/in_memory_provider.rs 3 | expression: error 4 | --- 5 | The following packages are incompatible 6 | |-- A 41..42 will be installed; 7 | |-- No candidates where found for B 14..15. 8 | 9 | -------------------------------------------------------------------------------- /tests/snapshots/in_memory_provider__error_reporting_missing_2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/in_memory_provider.rs 3 | expression: error 4 | --- 5 | The following packages are incompatible 6 | |-- A 0..999 cannot be installed because: 7 | |-- A 41 would require 8 | |-- B 0..20, for which no candidates where found. 9 | 10 | -------------------------------------------------------------------------------- /tests/snapshots/in_memory_provider__error_reporting_pubgrub_article.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/in_memory_provider.rs 3 | expression: error 4 | --- 5 | The following packages are incompatible 6 | |-- icons 100..101 will be installed; 7 | |-- intl 500..501 will be installed; 8 | |-- menu 0..999 cannot be installed because: 9 | |-- menu 150 would require 10 | |-- dropdown 200..231, which cannot be installed because: 11 | |-- dropdown 230 would require 12 | |-- icons 200..201, which conflicts with the already selected version. 13 | |-- menu 100 would require 14 | |-- dropdown 180..200, which cannot be installed because: 15 | |-- dropdown 180 would require 16 | |-- intl 300..301, which conflicts with the already selected version. 17 | 18 | -------------------------------------------------------------------------------- /tests/snapshots/in_memory_provider__error_reporting_root_conflict.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/in_memory_provider.rs 3 | expression: error 4 | --- 5 | The following packages are incompatible 6 | |-- A 0..4 will be installed; 7 | |-- A 5..10 cannot be installed, because it conflicts with the versions reported above. 8 | 9 | --------------------------------------------------------------------------------