Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/sudoku.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
}

Expand Down
14 changes: 14 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,18 @@ pub struct Candidates<S = SolvableId> {
/// 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<S> Default for Candidates<S> {
Expand All @@ -210,6 +222,8 @@ impl<S> Default for Candidates<S> {
locked: None,
hint_dependencies_available: HintDependenciesAvailable::None,
excluded: Vec::new(),
allow_self_conflicts: false,
allow_multiple: false,
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,8 @@ impl DependencyProvider for SnapshotProvider<'_> {
.filter(|&s| self.solvable(s).hint_dependencies_available)
.collect(),
),
allow_self_conflicts: false,
allow_multiple: false,
})
}

Expand Down
37 changes: 36 additions & 1 deletion src/solver/encoding.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions src/solver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ pub(crate) struct SolverState<D: DependencyProvider> {

clauses_added_for_package: <D::NameId as SolverId>::Set,
clauses_added_for_solvable: WithRootSet<D::SolvableId>,
pub(crate) allow_self_conflicts_names: <D::NameId as SolverId>::Set,
pub(crate) allow_multiple_names: <D::NameId as SolverId>::Set,
at_most_one_trackers: HashMap<D::NameId, AtMostOnceTracker<VariableId>>,

/// Keeps track of auxiliary variables that are used to encode at-least-one
Expand Down Expand Up @@ -249,6 +251,8 @@ impl<D: DependencyProvider> Default for SolverState<D> {
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(),
Expand Down
12 changes: 12 additions & 0 deletions tests/solver/bundle_box/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ pub struct BundleBoxProvider {
favored: HashMap<String, Pack>,
locked: HashMap<String, Pack>,
excluded: HashMap<String, HashMap<Pack, String>>,
allow_self_conflicts: HashSet<String>,
allow_multiple: HashSet<String>,
cancel_solving: Cell<bool>,
// TODO: simplify?
concurrent_requests: Arc<AtomicUsize>,
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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);
Expand Down
205 changes: 205 additions & 0 deletions tests/solver/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
");
}
Loading