diff --git a/examples/sudoku.rs b/examples/sudoku.rs index 818b4fde..da9b69a5 100644 --- a/examples/sudoku.rs +++ b/examples/sudoku.rs @@ -260,6 +260,8 @@ impl DependencyProvider for SudokuProvider { favored: None, hint_dependencies_available: HintDependenciesAvailable::All, excluded: Vec::new(), + allow_self_conflicts: false, + allow_multiple: false, }) } diff --git a/src/lib.rs b/src/lib.rs index 8a0552b6..c4701e82 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -200,6 +200,18 @@ pub struct Candidates { /// consider these solvables when forming a solution but will use /// them in the error message if no solution could be found. pub excluded: Vec<(S, StringId)>, + + /// When `true`, self-referential constraints (where a package conflicts + /// with something it also provides) are silently ignored rather than + /// marking the package uninstallable. Defaults to `false`. + /// + /// Some ecosystems (e.g. RPM) require this because packages routinely + /// provide and conflict with the same virtual capability. + pub allow_self_conflicts: bool, + /// When `true`, the solver allows multiple solvables of this package to + /// appear in the solution simultaneously. By default only one version of + /// each package can be selected. + pub allow_multiple: bool, } impl Default for Candidates { @@ -210,6 +222,8 @@ impl Default for Candidates { locked: None, hint_dependencies_available: HintDependenciesAvailable::None, excluded: Vec::new(), + allow_self_conflicts: false, + allow_multiple: false, } } } diff --git a/src/snapshot.rs b/src/snapshot.rs index 7389f86b..3a74353c 100644 --- a/src/snapshot.rs +++ b/src/snapshot.rs @@ -531,6 +531,8 @@ impl DependencyProvider for SnapshotProvider<'_> { .filter(|&s| self.solvable(s).hint_dependencies_available) .collect(), ), + allow_self_conflicts: false, + allow_multiple: false, }) } diff --git a/src/solver/encoding.rs b/src/solver/encoding.rs index 44444909..431d1e63 100644 --- a/src/solver/encoding.rs +++ b/src/solver/encoding.rs @@ -1,6 +1,10 @@ use std::{any::Any, collections::VecDeque, future::ready}; -use super::{SolverState, clause::WatchedLiterals, conditions}; +use super::{ + SolverState, + clause::{Clause, WatchedLiterals}, + conditions, +}; use crate::{ Candidates, ConditionId, ConditionalRequirement, DenseIndex, Dependencies, DependencyProvider, SolverCache, StringId, VariableId, VersionSetId, @@ -258,6 +262,13 @@ impl<'a, 'cache, D: DependencyProvider> Encoder<'a, 'cache, D> { self.cache.provider().display_name(name_id) ); + if package_candidates.allow_self_conflicts { + self.state.allow_self_conflicts_names.insert(name_id); + } + if package_candidates.allow_multiple { + self.state.allow_multiple_names.insert(name_id); + } + // If there is a locked solvable, forbid all other candidates if let Some(locked_solvable_id) = package_candidates.locked { self.add_locked_package_clauses(locked_solvable_id, &package_candidates.candidates); @@ -437,6 +448,13 @@ impl<'a, 'cache, D: DependencyProvider> Encoder<'a, 'cache, D> { let variable = self.state.variable_map.intern_solvable_or_root(solvable_id); for &forbidden_candidate in candidates { + if SolvableIdOrRoot::from(forbidden_candidate) == solvable_id { + let name_id = self.cache.provider().solvable_name(forbidden_candidate); + if !self.state.allow_self_conflicts_names.contains(name_id) { + self.add_self_conflict_clause(variable, constraint); + } + continue; + } let forbidden_candidate_var = self.state.variable_map.intern_solvable(forbidden_candidate); let (watched_literals, conflict, kind) = WatchedLiterals::constrains( @@ -500,6 +518,19 @@ impl<'a, 'cache, D: DependencyProvider> Encoder<'a, 'cache, D> { variable } + /// Adds a unary constrains clause that forbids a solvable that conflicts + /// with something it also provides (a self-conflict). + fn add_self_conflict_clause(&mut self, variable: VariableId, constraint: VersionSetId) { + let kind = Clause::Constrains(variable, variable, constraint); + let clause_id = self.state.add_clause(None, kind); + + self.state.negative_assertions.push((variable, clause_id)); + + if self.state.decision_tracker.assigned_value(variable) == Some(true) { + self.conflicting_clauses.push(clause_id); + } + } + /// Enqueues retrieving the dependencies for a solvable. /// /// This method requests the dependencies for the given solvable in an @@ -625,7 +656,11 @@ impl<'a, 'cache, D: DependencyProvider> Encoder<'a, 'cache, D> { /// Record `variable` as a candidate for a forbid-multiple clause under /// `name_id`. Returns silently if the variable has already been registered; /// see [`Self::forbid_seen`] for why globally-deduplicating is safe. + /// Also skips registration for packages that allow multiple versions. fn register_forbid_target(&mut self, name_id: D::NameId, variable: VariableId) { + if self.state.allow_multiple_names.contains(name_id) { + return; + } if self.forbid_seen.insert(variable) { self.pending_forbid_clauses .entry(name_id) diff --git a/src/solver/mod.rs b/src/solver/mod.rs index 35b213a9..46057fb6 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -219,6 +219,8 @@ pub(crate) struct SolverState { clauses_added_for_package: ::Set, clauses_added_for_solvable: WithRootSet, + pub(crate) allow_self_conflicts_names: ::Set, + pub(crate) allow_multiple_names: ::Set, at_most_one_trackers: HashMap>, /// Keeps track of auxiliary variables that are used to encode at-least-one @@ -249,6 +251,8 @@ impl Default for SolverState { disjunctions: Default::default(), clauses_added_for_package: Default::default(), clauses_added_for_solvable: Default::default(), + allow_self_conflicts_names: Default::default(), + allow_multiple_names: Default::default(), at_most_one_trackers: Default::default(), at_least_one_tracker: Default::default(), decision_tracker: Default::default(), diff --git a/tests/solver/bundle_box/mod.rs b/tests/solver/bundle_box/mod.rs index 40402fd6..6d6f3c54 100644 --- a/tests/solver/bundle_box/mod.rs +++ b/tests/solver/bundle_box/mod.rs @@ -56,6 +56,8 @@ pub struct BundleBoxProvider { favored: HashMap, locked: HashMap, excluded: HashMap>, + allow_self_conflicts: HashSet, + allow_multiple: HashSet, cancel_solving: Cell, // TODO: simplify? concurrent_requests: Arc, @@ -166,6 +168,14 @@ impl BundleBoxProvider { .insert(Pack::new(version), reason.into()); } + pub fn set_allow_self_conflicts(&mut self, package_name: &str) { + self.allow_self_conflicts.insert(package_name.to_owned()); + } + + pub fn set_allow_multiple(&mut self, package_name: &str) { + self.allow_multiple.insert(package_name.to_owned()); + } + pub fn set_locked(&mut self, package_name: &str, version: u32) { self.locked .insert(package_name.to_owned(), Pack::new(version)); @@ -343,6 +353,8 @@ impl DependencyProvider for BundleBoxProvider { let mut candidates = Candidates { candidates: Vec::with_capacity(package.len()), + allow_self_conflicts: self.allow_self_conflicts.contains(package_name), + allow_multiple: self.allow_multiple.contains(package_name), ..Candidates::default() }; let favor = self.favored.get(package_name); diff --git a/tests/solver/main.rs b/tests/solver/main.rs index e4273888..58834c92 100644 --- a/tests/solver/main.rs +++ b/tests/solver/main.rs @@ -1370,3 +1370,208 @@ fn test_constrains_multiple_parents() { x=1 "###); } + +/// When `forbid_self_conflicts` is false, a package that constrains itself +/// is silently allowed. Some ecosystems (e.g. RPM) explicitly support this. +/// The real-world examples are structured a bit differently however. +#[test] +fn test_self_conflict_allowed() { + let mut provider = BundleBoxProvider::new(); + // a=1 constrains "a" to [2,100) — version 1 is NOT in that range, + // so a=1 appears as a non-matching candidate for its own constraint + // (i.e. a self-conflict). + provider.add_package("a", 1.into(), &[], &["a 2..100"]); + provider.set_allow_self_conflicts("a"); + + let requirements = provider.requirements(&["a"]); + let mut solver = Solver::new(provider); + let problem = Problem::new().requirements(requirements); + let solved = solver.solve(problem).unwrap(); + let result = transaction_to_string(solver.provider(), &solved); + assert_snapshot!(result, @r" + a=1 + "); +} + +/// When `forbid_self_conflicts` is true (the default), a package that +/// constrains itself is marked uninstallable. +#[test] +fn test_self_conflict_forbidden() { + let mut provider = BundleBoxProvider::new(); + provider.add_package("a", 1.into(), &[], &["a 2..100"]); + + let requirements = provider.requirements(&["a"]); + let mut solver = Solver::new(provider); + let problem = Problem::new().requirements(requirements); + match solver.solve(problem) { + Ok(_) => panic!("expected unsat due to self-conflict"), + Err(UnsolvableOrCancelled::Unsolvable(_)) => {} + Err(UnsolvableOrCancelled::Cancelled(_)) => { + panic!("expected unsolvable, not cancelled") + } + } +} + +/// When `forbid_self_conflicts` is false and there are multiple versions, +/// only the self-conflicting version is skipped — other versions of the +/// same package are still constrained normally. +#[test] +fn test_self_conflict_allowed_multiple_versions() { + let mut provider = BundleBoxProvider::new(); + // a=1 constrains "a" to [2,100) — a=1 itself is outside that range + // (self-conflict, skipped), but a=2 is inside, so no constraint on a=2. + // a=3 constrains "a" to [4,100) — a=3 itself is outside (self-conflict, + // skipped), but a=1 and a=2 are also outside so they ARE forbidden. + provider.add_package("a", 1.into(), &[], &["a 2..100"]); + provider.add_package("a", 2.into(), &[], &[]); + provider.add_package("a", 3.into(), &[], &["a 4..100"]); + provider.set_allow_self_conflicts("a"); + + let requirements = provider.requirements(&["a"]); + let mut solver = Solver::new(provider); + let problem = Problem::new().requirements(requirements); + let solved = solver.solve(problem).unwrap(); + let result = transaction_to_string(solver.provider(), &solved); + // a=3 is the highest version; its self-conflict is ignored, and it + // constrains a=1 and a=2 away normally. + assert_snapshot!(result, @r" + a=3 + "); +} + +/// `allow_self_conflicts` works correctly when the self-conflicting +/// package is a transitive dependency discovered in a later solver pass. +#[test] +fn test_self_conflict_allowed_transitive() { + let mut provider = BundleBoxProvider::new(); + // "a" has a self-conflict and is a transitive dep of "app" via "lib". + provider.add_package("a", 1.into(), &[], &["a 2..100"]); + provider.set_allow_self_conflicts("a"); + provider.add_package("lib", 1.into(), &["a"], &[]); + provider.add_package("app", 1.into(), &["lib"], &[]); + + let requirements = provider.requirements(&["app"]); + let mut solver = Solver::new(provider); + let problem = Problem::new().requirements(requirements); + let solved = solver.solve(problem).unwrap(); + let result = transaction_to_string(solver.provider(), &solved); + assert_snapshot!(result, @r" + a=1 + app=1 + lib=1 + "); +} + +/// When `allow_multiple` is set on a package's candidates, the solver +/// permits multiple versions of that package in the solution. +#[test] +fn test_allow_multiple_versions() { + let mut provider = BundleBoxProvider::new(); + provider.add_package("kernel", 1.into(), &[], &[]); + provider.add_package("kernel", 2.into(), &[], &[]); + provider.add_package("kernel", 3.into(), &[], &[]); + provider.set_allow_multiple("kernel"); + + // Request all three versions as soft requirements. + let k1 = provider.solvable_id("kernel", 1u32); + let k2 = provider.solvable_id("kernel", 2u32); + let k3 = provider.solvable_id("kernel", 3u32); + let requirements = provider.requirements(&["kernel"]); + let mut solver = Solver::new(provider); + let problem = Problem::new() + .requirements(requirements) + .soft_requirements(vec![k1, k2, k3]); + let solved = solver.solve(problem).unwrap(); + let result = transaction_to_string(solver.provider(), &solved); + assert_snapshot!(result, @r" + kernel=1 + kernel=2 + kernel=3 + "); +} + +/// Without `allow_multiple`, only the highest version is selected. +#[test] +fn test_single_version_default() { + let mut provider = BundleBoxProvider::new(); + provider.add_package("kernel", 1.into(), &[], &[]); + provider.add_package("kernel", 2.into(), &[], &[]); + provider.add_package("kernel", 3.into(), &[], &[]); + + let requirements = provider.requirements(&["kernel"]); + let mut solver = Solver::new(provider); + let problem = Problem::new().requirements(requirements); + let solved = solver.solve(problem).unwrap(); + let result = transaction_to_string(solver.provider(), &solved); + assert_snapshot!(result, @r" + kernel=3 + "); +} + +/// `allow_multiple` works correctly when the multiversion package is +/// required by a transitive dependency discovered in a later solver +/// pass (i.e. a different Encoder invocation than the one that first +/// processed the package's candidates). +#[test] +fn test_allow_multiple_transitive() { + let mut provider = BundleBoxProvider::new(); + provider.add_package("kernel", 1.into(), &[], &[]); + provider.add_package("kernel", 2.into(), &[], &[]); + provider.add_package("kernel", 3.into(), &[], &[]); + provider.set_allow_multiple("kernel"); + // "app" depends on kernel and driver; driver also depends on kernel. + // driver's dependency on kernel will be processed in a later encoder + // invocation after driver is selected. + provider.add_package("app", 1.into(), &["kernel 2..100", "driver"], &[]); + provider.add_package("driver", 1.into(), &["kernel 1..100"], &[]); + + let k1 = provider.solvable_id("kernel", 1u32); + let k2 = provider.solvable_id("kernel", 2u32); + let k3 = provider.solvable_id("kernel", 3u32); + let requirements = provider.requirements(&["app"]); + let mut solver = Solver::new(provider); + let problem = Problem::new() + .requirements(requirements) + .soft_requirements(vec![k1, k2, k3]); + let solved = solver.solve(problem).unwrap(); + let result = transaction_to_string(solver.provider(), &solved); + assert_snapshot!(result, @r" + app=1 + driver=1 + kernel=1 + kernel=2 + kernel=3 + "); +} + +/// `allow_multiple` packages still respect dependency constraints +/// from other packages. +#[test] +fn test_allow_multiple_with_constraints() { + let mut provider = BundleBoxProvider::new(); + provider.add_package("kernel", 1.into(), &[], &[]); + provider.add_package("kernel", 2.into(), &[], &[]); + provider.add_package("kernel", 3.into(), &[], &[]); + provider.set_allow_multiple("kernel"); + // "app" depends on kernel >=2 + provider.add_package("app", 1.into(), &["kernel 2..100"], &[]); + + let k1 = provider.solvable_id("kernel", 1u32); + let k2 = provider.solvable_id("kernel", 2u32); + let k3 = provider.solvable_id("kernel", 3u32); + let requirements = provider.requirements(&["app"]); + let mut solver = Solver::new(provider); + let problem = Problem::new() + .requirements(requirements) + .soft_requirements(vec![k1, k2, k3]); + let solved = solver.solve(problem).unwrap(); + let result = transaction_to_string(solver.provider(), &solved); + // All three kernel versions are installed; app's requirement is + // satisfied by kernel=2 and kernel=3. + assert_snapshot!(result, @r" + app=1 + kernel=1 + kernel=2 + kernel=3 + "); +}