From f08348f9ff765f613442c2e3fce3bdf7f3c50cc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Tue, 20 Jun 2023 13:34:37 +0200 Subject: [PATCH 01/17] Allow manually specifying platform and virtual packages --- crates/rattler-bin/src/commands/create.rs | 57 ++++++++++++++++++----- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/crates/rattler-bin/src/commands/create.rs b/crates/rattler-bin/src/commands/create.rs index 677c0bb53..9442efb85 100644 --- a/crates/rattler-bin/src/commands/create.rs +++ b/crates/rattler-bin/src/commands/create.rs @@ -8,7 +8,7 @@ use rattler::{ }; use rattler_conda_types::{ Channel, ChannelConfig, GenericVirtualPackage, MatchSpec, Platform, PrefixRecord, - RepoDataRecord, + RepoDataRecord, Version, }; use rattler_networking::{AuthenticatedClient, AuthenticationStorage}; use rattler_repodata_gateway::fetch::{ @@ -36,6 +36,15 @@ pub struct Opt { #[clap(required = true)] specs: Vec, + + #[clap(long)] + dry_run: bool, + + #[clap(long)] + platform: Option, + + #[clap(long)] + virtual_package: Option>, } pub async fn create(opt: Opt) -> anyhow::Result<()> { @@ -43,7 +52,13 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { let target_prefix = env::current_dir()?.join(".prefix"); // Determine the platform we're going to install for - let install_platform = Platform::current(); + let install_platform = if let Some(platform) = opt.platform { + Platform::from_str(&platform)? + } else { + Platform::current() + }; + + println!("installing for platform: {:?}", install_platform); // Parse the specs from the command line. We do this explicitly instead of allow clap to deal // with this because we need to parse the `channel_config` when parsing matchspecs. @@ -75,10 +90,10 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { let channel_urls = channels .iter() .flat_map(|channel| { - channel - .platforms_or_default() - .iter() - .map(move |platform| (channel.clone(), *platform)) + vec![ + (channel.clone(), install_platform.clone()), + (channel.clone(), Platform::NoArch), + ] }) .collect::>(); @@ -147,14 +162,34 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { // system. Some packages depend on these virtual packages to indiciate compability with the // hardware of the system. let virtual_packages = wrap_in_progress("determining virtual packages", move || { - rattler_virtual_packages::VirtualPackage::current().map(|vpkgs| { - vpkgs + if let Some(virtual_packages) = opt.virtual_package { + Ok(virtual_packages .iter() - .map(|vpkg| GenericVirtualPackage::from(vpkg.clone())) - .collect::>() - }) + .map(|virt_pkg| { + let elems = virt_pkg.split("=").collect::>(); + GenericVirtualPackage { + name: elems[0].to_string(), + version: elems + .get(1) + .map(|s| Version::from_str(s)) + .unwrap_or(Version::from_str("0")) + .expect("Could not parse virtual package version"), + build_string: elems.get(2).unwrap_or(&"").to_string(), + } + }) + .collect::>()) + } else { + rattler_virtual_packages::VirtualPackage::current().map(|vpkgs| { + vpkgs + .iter() + .map(|vpkg| GenericVirtualPackage::from(vpkg.clone())) + .collect::>() + }) + } })?; + println!("virtual packages: {:?}", virtual_packages); + // Now that we parsed and downloaded all information, construct the packaging problem that we // need to solve. We do this by constructing a `SolverProblem`. This encapsulates all the // information required to be able to solve the problem. From b2fe311a3b464b558f61b2feb5e7ada89ccba8f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Mon, 5 Jun 2023 14:29:09 +0200 Subject: [PATCH 02/17] First take at porting libsolv --- crates/libsolv_rs/Cargo.toml | 10 + crates/libsolv_rs/src/conda_util.rs | 143 + crates/libsolv_rs/src/decision_map.rs | 42 + crates/libsolv_rs/src/decision_tracker.rs | 86 + crates/libsolv_rs/src/lib.rs | 10 + crates/libsolv_rs/src/pool.rs | 317 ++ crates/libsolv_rs/src/rules.rs | 528 ++++ crates/libsolv_rs/src/solvable.rs | 150 + crates/libsolv_rs/src/solve_jobs.rs | 28 + crates/libsolv_rs/src/solve_problem.rs | 88 + crates/libsolv_rs/src/solver.rs | 1185 +++++++ crates/libsolv_rs/src/watch_map.rs | 61 + crates/rattler-bin/src/commands/create.rs | 15 + crates/rattler_solve/Cargo.toml | 15 +- crates/rattler_solve/build.rs | 47 - crates/rattler_solve/src/lib.rs | 36 +- crates/rattler_solve/src/libsolv/input.rs | 276 +- .../src/libsolv/libc_byte_slice.rs | 42 - crates/rattler_solve/src/libsolv/mod.rs | 119 +- crates/rattler_solve/src/libsolv/output.rs | 57 +- .../rattler_solve/src/libsolv/wrapper/ffi.rs | 2754 ----------------- .../src/libsolv/wrapper/flags.rs | 18 - .../rattler_solve/src/libsolv/wrapper/keys.rs | 16 - .../rattler_solve/src/libsolv/wrapper/mod.rs | 25 - .../rattler_solve/src/libsolv/wrapper/pool.rs | 341 -- .../src/libsolv/wrapper/queue.rs | 95 - .../rattler_solve/src/libsolv/wrapper/repo.rs | 179 -- .../src/libsolv/wrapper/repodata.rs | 110 - .../src/libsolv/wrapper/solvable.rs | 41 - .../src/libsolv/wrapper/solve_goal.rs | 97 - .../src/libsolv/wrapper/solve_problem.rs | 108 - .../src/libsolv/wrapper/solver.rs | 159 - .../src/libsolv/wrapper/transaction.rs | 70 - ...olve__test_libsolv__solve_tensorboard.snap | 83 + 34 files changed, 2869 insertions(+), 4482 deletions(-) create mode 100644 crates/libsolv_rs/Cargo.toml create mode 100644 crates/libsolv_rs/src/conda_util.rs create mode 100644 crates/libsolv_rs/src/decision_map.rs create mode 100644 crates/libsolv_rs/src/decision_tracker.rs create mode 100644 crates/libsolv_rs/src/lib.rs create mode 100644 crates/libsolv_rs/src/pool.rs create mode 100644 crates/libsolv_rs/src/rules.rs create mode 100644 crates/libsolv_rs/src/solvable.rs create mode 100644 crates/libsolv_rs/src/solve_jobs.rs create mode 100644 crates/libsolv_rs/src/solve_problem.rs create mode 100644 crates/libsolv_rs/src/solver.rs create mode 100644 crates/libsolv_rs/src/watch_map.rs delete mode 100644 crates/rattler_solve/build.rs delete mode 100644 crates/rattler_solve/src/libsolv/libc_byte_slice.rs delete mode 100644 crates/rattler_solve/src/libsolv/wrapper/ffi.rs delete mode 100644 crates/rattler_solve/src/libsolv/wrapper/flags.rs delete mode 100644 crates/rattler_solve/src/libsolv/wrapper/keys.rs delete mode 100644 crates/rattler_solve/src/libsolv/wrapper/mod.rs delete mode 100644 crates/rattler_solve/src/libsolv/wrapper/pool.rs delete mode 100644 crates/rattler_solve/src/libsolv/wrapper/queue.rs delete mode 100644 crates/rattler_solve/src/libsolv/wrapper/repo.rs delete mode 100644 crates/rattler_solve/src/libsolv/wrapper/repodata.rs delete mode 100644 crates/rattler_solve/src/libsolv/wrapper/solvable.rs delete mode 100644 crates/rattler_solve/src/libsolv/wrapper/solve_goal.rs delete mode 100644 crates/rattler_solve/src/libsolv/wrapper/solve_problem.rs delete mode 100644 crates/rattler_solve/src/libsolv/wrapper/solver.rs delete mode 100644 crates/rattler_solve/src/libsolv/wrapper/transaction.rs create mode 100644 crates/rattler_solve/src/snapshots/rattler_solve__test_libsolv__solve_tensorboard.snap diff --git a/crates/libsolv_rs/Cargo.toml b/crates/libsolv_rs/Cargo.toml new file mode 100644 index 000000000..02f298118 --- /dev/null +++ b/crates/libsolv_rs/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "libsolv_rs" +version = "0.1.0" +edition = "2021" + +[dependencies] +rattler_conda_types = { version = "0.4.0", path = "../rattler_conda_types" } + +[dev-dependencies] +insta = "1.29.0" diff --git a/crates/libsolv_rs/src/conda_util.rs b/crates/libsolv_rs/src/conda_util.rs new file mode 100644 index 000000000..a1aa4d3c7 --- /dev/null +++ b/crates/libsolv_rs/src/conda_util.rs @@ -0,0 +1,143 @@ +use crate::pool::StringId; +use crate::solvable::{Solvable, SolvableId}; +use rattler_conda_types::{MatchSpec, PackageRecord, Version}; +use std::cmp::Ordering; +use std::collections::HashMap; +use std::str::FromStr; + +/// Returns the order of two candidates based on rules used by conda. +pub(crate) fn compare_candidates( + solvables: &[Solvable], + interned_strings: &HashMap, + packages_by_name: &HashMap>, + a: &PackageRecord, + b: &PackageRecord, +) -> Ordering { + // First compare by "tracked_features". If one of the packages has a tracked feature it is + // sorted below the one that doesn't have the tracked feature. + let a_has_tracked_features = a.track_features.is_empty(); + let b_has_tracked_features = b.track_features.is_empty(); + match b_has_tracked_features.cmp(&a_has_tracked_features) { + Ordering::Less => return Ordering::Less, + Ordering::Greater => return Ordering::Greater, + Ordering::Equal => {} + }; + + // Otherwise, select the variant with the highest version + match a.version.cmp(&b.version) { + Ordering::Less => return Ordering::Greater, + Ordering::Greater => return Ordering::Less, + Ordering::Equal => {} + }; + + // Otherwise, select the variant with the highest build number + match a.build_number.cmp(&b.build_number) { + Ordering::Less => return Ordering::Greater, + Ordering::Greater => return Ordering::Less, + Ordering::Equal => {} + }; + + // Otherwise, compare the dependencies of the variants. If there are similar + // dependencies select the variant that selects the highest version of the dependency. + let a_match_specs: Vec<_> = a + .depends + .iter() + .map(|d| MatchSpec::from_str(d).unwrap()) + .collect(); + let b_match_specs: Vec<_> = b + .depends + .iter() + .map(|d| MatchSpec::from_str(d).unwrap()) + .collect(); + + let b_specs_by_name: HashMap<_, _> = b_match_specs + .iter() + .filter_map(|spec| spec.name.as_ref().map(|name| (name, spec))) + .collect(); + + let a_specs_by_name = a_match_specs + .iter() + .filter_map(|spec| spec.name.as_ref().map(|name| (name, spec))); + + let mut total_score = 0; + for (a_dep_name, a_spec) in a_specs_by_name { + if let Some(b_spec) = b_specs_by_name.get(&a_dep_name) { + if &a_spec == b_spec { + continue; + } + + // Find which of the two specs selects the highest version + let highest_a = + find_highest_version(solvables, interned_strings, packages_by_name, a_spec); + let highest_b = + find_highest_version(solvables, interned_strings, packages_by_name, b_spec); + + // Skip version if no package is selected by either spec + let (a_version, a_tracked_features, b_version, b_tracked_features) = if let ( + Some((a_version, a_tracked_features)), + Some((b_version, b_tracked_features)), + ) = + (highest_a, highest_b) + { + (a_version, a_tracked_features, b_version, b_tracked_features) + } else { + continue; + }; + + // If one of the dependencies only selects versions with tracked features, down- + // weight that variant. + if let Some(score) = match a_tracked_features.cmp(&b_tracked_features) { + Ordering::Less => Some(-100), + Ordering::Greater => Some(100), + Ordering::Equal => None, + } { + total_score += score; + continue; + } + + // Otherwise, down-weigh the version with the lowest selected version. + total_score += match a_version.cmp(&b_version) { + Ordering::Less => 1, + Ordering::Equal => 0, + Ordering::Greater => -1, + }; + } + } + + // If ranking the dependencies provides a score, use that for the sorting. + match total_score.cmp(&0) { + Ordering::Equal => {} + ord => return ord, + }; + + // Otherwise, order by timestamp + b.timestamp.cmp(&a.timestamp) +} + +pub(crate) fn find_highest_version( + solvables: &[Solvable], + interned_strings: &HashMap, + packages_by_name: &HashMap>, + match_spec: &MatchSpec, +) -> Option<(Version, bool)> { + let name = match_spec.name.as_deref().unwrap(); + let name_id = interned_strings[name]; + + // For each record that matches the spec + let candidates = packages_by_name[&name_id] + .iter() + .map(|s| solvables[s.index()].package().record) + .filter(|s| match_spec.matches(s)); + + candidates.fold(None, |init, record| { + Some(init.map_or_else( + || (record.version.clone(), !record.track_features.is_empty()), + |(version, has_tracked_features)| { + ( + version.max(record.version.clone()), + has_tracked_features && record.track_features.is_empty(), + ) + }, + )) + }) +} diff --git a/crates/libsolv_rs/src/decision_map.rs b/crates/libsolv_rs/src/decision_map.rs new file mode 100644 index 000000000..ec6c16c1f --- /dev/null +++ b/crates/libsolv_rs/src/decision_map.rs @@ -0,0 +1,42 @@ +use crate::solvable::SolvableId; +use std::cmp::Ordering; + +/// Map of all available solvables +pub(crate) struct DecisionMap { + /// = 0: undecided + /// > 0: level of decision when installed + /// < 0: level of decision when conflict + map: Vec, +} + +impl DecisionMap { + pub(crate) fn new(nsolvables: u32) -> Self { + Self { + map: vec![0; nsolvables as usize], + } + } + + pub(crate) fn nsolvables(&self) -> u32 { + self.map.len() as u32 + } + + pub(crate) fn reset(&mut self, solvable_id: SolvableId) { + self.map[solvable_id.index()] = 0; + } + + pub(crate) fn set(&mut self, solvable_id: SolvableId, value: bool, level: u32) { + self.map[solvable_id.index()] = if value { level as i64 } else { -(level as i64) }; + } + + pub(crate) fn level(&self, solvable_id: SolvableId) -> u32 { + self.map[solvable_id.index()].unsigned_abs() as u32 + } + + pub(crate) fn value(&self, solvable_id: SolvableId) -> Option { + match self.map[solvable_id.index()].cmp(&0) { + Ordering::Less => Some(false), + Ordering::Equal => None, + Ordering::Greater => Some(true), + } + } +} diff --git a/crates/libsolv_rs/src/decision_tracker.rs b/crates/libsolv_rs/src/decision_tracker.rs new file mode 100644 index 000000000..1de7ab351 --- /dev/null +++ b/crates/libsolv_rs/src/decision_tracker.rs @@ -0,0 +1,86 @@ +use crate::decision_map::DecisionMap; +use crate::solvable::SolvableId; +use crate::solver::Decision; + +pub(crate) struct DecisionTracker { + map: DecisionMap, + stack: Vec, + propagate_index: usize, +} + +impl DecisionTracker { + pub(crate) fn new(nsolvables: u32) -> Self { + Self { + map: DecisionMap::new(nsolvables), + stack: Vec::new(), + propagate_index: 0, + } + } + + pub(crate) fn clear(&mut self) { + *self = Self::new(self.map.nsolvables()); + } + + pub(crate) fn is_empty(&self) -> bool { + self.stack.is_empty() + } + + pub(crate) fn assigned_value(&self, solvable_id: SolvableId) -> Option { + self.map.value(solvable_id) + } + + pub(crate) fn map(&self) -> &DecisionMap { + &self.map + } + + pub(crate) fn stack(&self) -> &[Decision] { + &self.stack + } + + pub(crate) fn level(&self, solvable_id: SolvableId) -> u32 { + self.map.level(solvable_id) + } + + /// Attempts to add a decision + /// + /// Returns true if the solvable was undecided, false if it was already decided to the same value + /// + /// Returns an error if the solvable was decided to a different value (which means there is a conflict) + pub(crate) fn try_add_decision(&mut self, decision: Decision, level: u32) -> Result { + match self.map.value(decision.solvable_id) { + None => { + self.map.set(decision.solvable_id, decision.value, level); + self.stack.push(decision); + Ok(true) + } + Some(value) if value == decision.value => Ok(false), + _ => Err(()), + } + } + + pub(crate) fn undo_until(&mut self, level: u32) { + while let Some(decision) = self.stack.last() { + if self.level(decision.solvable_id) <= level { + break; + } + + self.undo_last(); + } + } + + pub(crate) fn undo_last(&mut self) -> (Decision, u32) { + let decision = self.stack.pop().unwrap(); + self.map.reset(decision.solvable_id); + + self.propagate_index = self.stack.len(); + + let top_decision = self.stack.last().unwrap(); + (decision, self.map.level(top_decision.solvable_id)) + } + + pub(crate) fn next_unpropagated(&mut self) -> Option { + let &decision = self.stack[self.propagate_index..].iter().next()?; + self.propagate_index += 1; + Some(decision) + } +} diff --git a/crates/libsolv_rs/src/lib.rs b/crates/libsolv_rs/src/lib.rs new file mode 100644 index 000000000..7ac7bb139 --- /dev/null +++ b/crates/libsolv_rs/src/lib.rs @@ -0,0 +1,10 @@ +mod conda_util; +mod decision_map; +mod decision_tracker; +pub mod pool; +mod rules; +pub mod solvable; +pub mod solve_jobs; +pub mod solve_problem; +pub mod solver; +mod watch_map; diff --git a/crates/libsolv_rs/src/pool.rs b/crates/libsolv_rs/src/pool.rs new file mode 100644 index 000000000..1299f344d --- /dev/null +++ b/crates/libsolv_rs/src/pool.rs @@ -0,0 +1,317 @@ +use crate::conda_util; +use crate::solvable::{PackageSolvable, Solvable, SolvableId}; +use rattler_conda_types::{MatchSpec, PackageRecord}; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::str::FromStr; + +#[derive(Clone, Copy, Eq, PartialEq, Hash)] +pub struct RepoId(u32); + +impl RepoId { + fn new(id: u32) -> Self { + Self(id) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct StringId { + value: u32, +} + +impl StringId { + pub(crate) fn new(index: usize) -> Self { + Self { + value: index as u32, + } + } + + pub(crate) fn index(self) -> usize { + self.value as usize + } +} + +#[derive(Clone, Copy, Debug)] +pub(crate) struct MatchSpecId(u32); + +impl MatchSpecId { + fn new(index: usize) -> Self { + Self(index as u32) + } + + pub(crate) fn index(self) -> usize { + self.0 as usize + } +} + +pub struct Pool { + pub(crate) solvables: Vec, + + /// The total amount of registered repos + total_repos: u32, + + /// Interned strings + pub(crate) strings_to_ids: HashMap, + strings: Vec, + + /// Interned match specs + match_specs_to_ids: HashMap, + pub(crate) match_specs: Vec, + + /// Cached candidates for each match spec, indexed by their MatchSpecId + pub(crate) match_spec_to_candidates: Vec>>, + + pub(crate) match_spec_to_forbidden: Vec>>, + + // TODO: eventually we could turn this into a Vec, making sure we have a separate interning + // scheme for package names + pub(crate) packages_by_name: HashMap>, +} + +impl Default for Pool { + fn default() -> Self { + Self { + solvables: vec![Solvable::new_root()], + total_repos: 0, + + strings_to_ids: HashMap::new(), + strings: Vec::new(), + + packages_by_name: HashMap::default(), + + match_specs_to_ids: HashMap::default(), + match_specs: Vec::new(), + match_spec_to_candidates: Vec::new(), + match_spec_to_forbidden: Vec::new(), + } + } +} + +impl Pool { + pub fn new() -> Self { + Self::default() + } + + pub fn new_repo(&mut self, _url: impl AsRef) -> RepoId { + let id = RepoId::new(self.total_repos); + self.total_repos += 1; + id + } + + /// Adds a new solvable to a repo + pub fn add_package(&mut self, repo_id: RepoId, record: &'static PackageRecord) -> SolvableId { + assert!(self.solvables.len() <= u32::MAX as usize); + + let name = self.intern_str(&record.name); + + let solvable_id = SolvableId::new(self.solvables.len()); + self.solvables + .push(Solvable::new_package(repo_id, name, record)); + + assert!(repo_id.0 < self.total_repos); + + self.packages_by_name + .entry(name) + .or_insert(Vec::new()) + .push(solvable_id); + + solvable_id + } + + pub(crate) fn intern_matchspec(&mut self, match_spec: String) -> MatchSpecId { + let next_index = self.match_specs.len(); + match self.match_specs_to_ids.entry(match_spec) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + // println!("Interning match_spec: {}", entry.key()); + self.match_specs + .push(MatchSpec::from_str(entry.key()).unwrap()); + self.match_spec_to_candidates.push(None); + self.match_spec_to_forbidden.push(None); + + // Update the entry + let id = MatchSpecId::new(next_index); + entry.insert(id); + + id + } + } + } + + pub fn reset_package( + &mut self, + repo_id: RepoId, + solvable_id: SolvableId, + record: &'static PackageRecord, + ) { + let name = self.intern_str(&record.name); + self.solvables[solvable_id.index()] = Solvable::new_package(repo_id, name, record); + } + + // This function does not take `self`, because otherwise we run into problems with borrowing + // when we want to use it together with other pool functions + pub(crate) fn get_candidates<'a>( + match_specs: &'a [MatchSpec], + strings_to_ids: &'a HashMap, + solvables: &'a [Solvable], + packages_by_name: &'a HashMap>, + match_spec_to_candidates: &'a mut [Option>], + favored_map: &HashMap, + match_spec_id: MatchSpecId, + ) -> &'a [SolvableId] { + let candidates = match_spec_to_candidates[match_spec_id.index()].get_or_insert_with(|| { + let match_spec = &match_specs[match_spec_id.index()]; + let match_spec_name = match_spec + .name + .as_deref() + .expect("match spec without name!"); + let name_id = match strings_to_ids.get(match_spec_name) { + None => return Vec::new(), + Some(name_id) => name_id, + }; + + let mut pkgs: Vec<_> = packages_by_name[name_id] + .iter() + .cloned() + .filter(|solvable| match_spec.matches(solvables[solvable.index()].package().record)) + .collect(); + + pkgs.sort_by(|p1, p2| { + conda_util::compare_candidates( + solvables, + strings_to_ids, + packages_by_name, + solvables[p1.index()].package().record, + solvables[p2.index()].package().record, + ) + }); + + if let Some(&favored_id) = favored_map.get(name_id) { + if let Some(pos) = pkgs.iter().position(|&s| s == favored_id) { + pkgs.swap(0, pos); + } + } + + pkgs + }); + + candidates.as_slice() + } + + // This function does not take `self`, because otherwise we run into problems with borrowing + // when we want to use it together with other pool functions + pub(crate) fn get_forbidden<'a>( + match_specs: &'a [MatchSpec], + strings_to_ids: &'a HashMap, + solvables: &'a [Solvable], + packages_by_name: &'a HashMap>, + match_spec_to_forbidden: &'a mut [Option>], + match_spec_id: MatchSpecId, + ) -> &'a [SolvableId] { + let candidates = match_spec_to_forbidden[match_spec_id.index()].get_or_insert_with(|| { + let match_spec = &match_specs[match_spec_id.index()]; + let match_spec_name = match_spec + .name + .as_deref() + .expect("match spec without name!"); + let name_id = match strings_to_ids.get(match_spec_name) { + None => return Vec::new(), + Some(name_id) => name_id, + }; + + packages_by_name[name_id] + .iter() + .cloned() + .filter(|solvable| { + !match_spec.matches(solvables[solvable.index()].package().record) + }) + .collect() + }); + + candidates.as_slice() + } + + pub fn add_dependency(&mut self, solvable_id: SolvableId, match_spec: String) { + let match_spec_id = self.intern_matchspec(match_spec); + let solvable = self.solvables[solvable_id.index()].package_mut(); + solvable.dependencies.push(match_spec_id); + } + + pub fn add_constrains(&mut self, solvable_id: SolvableId, match_spec: String) { + let match_spec_id = self.intern_matchspec(match_spec); + let solvable = self.solvables[solvable_id.index()].package_mut(); + solvable.constrains.push(match_spec_id); + } + + pub(crate) fn nsolvables(&self) -> u32 { + self.solvables.len() as u32 + } + + /// Interns string like types into a `Pool` returning a `StringId` + pub(crate) fn intern_str>(&mut self, str: T) -> StringId { + let next_id = StringId::new(self.strings_to_ids.len()); + match self.strings_to_ids.entry(str.into()) { + Entry::Occupied(e) => *e.get(), + Entry::Vacant(e) => { + self.strings.push(e.key().clone()); + e.insert(next_id); + next_id + } + } + } + + pub fn resolve_string(&self, string_id: StringId) -> &str { + &self.strings[string_id.index()] + } + + /// Returns a string describing the last error associated to this pool, or "no error" if there + /// were no errors + pub fn last_error(&self) -> String { + // See pool_errstr + "no error".to_string() + } + + /// Resolves the id to a solvable + /// + /// Panics if the solvable is not found in the pool + pub fn resolve_solvable(&self, id: SolvableId) -> &PackageSolvable { + self.resolve_solvable_inner(id).package() + } + + /// Resolves the id to a solvable + /// + /// Panics if the solvable is not found in the pool + pub fn resolve_solvable_mut(&mut self, id: SolvableId) -> &mut PackageSolvable { + self.resolve_solvable_inner_mut(id).package_mut() + } + + /// Resolves the id to a solvable + /// + /// Panics if the solvable is not found in the pool + pub(crate) fn resolve_solvable_inner(&self, id: SolvableId) -> &Solvable { + if id.index() < self.solvables.len() { + &self.solvables[id.index()] + } else { + panic!("invalid solvable id!") + } + } + + /// Resolves the id to a solvable + /// + /// Panics if the solvable is not found in the pool + pub(crate) fn resolve_solvable_inner_mut(&mut self, id: SolvableId) -> &mut Solvable { + if id.index() < self.solvables.len() { + &mut self.solvables[id.index()] + } else { + panic!("invalid solvable id!") + } + } + + pub(crate) fn resolve_match_spec(&self, id: MatchSpecId) -> &MatchSpec { + &self.match_specs[id.index()] + } + + pub(crate) fn root_solvable_mut(&mut self) -> &mut Vec { + self.solvables[0].root_mut() + } +} diff --git a/crates/libsolv_rs/src/rules.rs b/crates/libsolv_rs/src/rules.rs new file mode 100644 index 000000000..dafb0909c --- /dev/null +++ b/crates/libsolv_rs/src/rules.rs @@ -0,0 +1,528 @@ +use crate::decision_map::DecisionMap; +use crate::pool::{MatchSpecId, Pool}; +use crate::solvable::SolvableId; +use crate::solver::RuleId; + +#[derive(Clone)] +pub(crate) struct Rule { + pub watched_literals: [SolvableId; 2], + next_watches: [RuleId; 2], + pub(crate) kind: RuleKind, +} + +impl Rule { + pub fn new(kind: RuleKind, learnt_rules: &[Vec], pool: &Pool) -> Self { + let watched_literals = kind + .initial_watches(learnt_rules, pool) + .unwrap_or([SolvableId::null(), SolvableId::null()]); + + let rule = Self { + watched_literals, + next_watches: [RuleId::null(), RuleId::null()], + kind, + }; + + debug_assert!(!rule.has_watches() || watched_literals[0] != watched_literals[1]); + + rule + } + + pub fn debug(&self, pool: &Pool) { + match self.kind { + RuleKind::InstallRoot => println!("install root"), + RuleKind::Learnt(index) => println!("learnt rule {index}"), + RuleKind::Requires(solvable_id, match_spec_id) => { + let match_spec = pool.resolve_match_spec(match_spec_id).to_string(); + println!( + "{} requires {match_spec}", + pool.resolve_solvable_inner(solvable_id).display() + ) + } + RuleKind::Constrains(s1, s2) => { + println!( + "{} excludes {}", + pool.resolve_solvable_inner(s1).display(), + pool.resolve_solvable_inner(s2).display() + ) + } + RuleKind::Forbids(s1, _) => { + let name = pool + .resolve_solvable_inner(s1) + .package() + .record + .name + .as_str(); + println!("only one {name} allowed") + } + } + } + + pub fn link_to_rule(&mut self, watch_index: usize, linked_rule: RuleId) { + self.next_watches[watch_index] = linked_rule; + } + + pub fn get_linked_rule(&self, watch_index: usize) -> RuleId { + self.next_watches[watch_index] + } + + pub fn unlink_rule( + &mut self, + linked_rule: &Rule, + watched_solvable: SolvableId, + linked_rule_watch_index: usize, + ) { + if self.watched_literals[0] == watched_solvable { + self.next_watches[0] = linked_rule.next_watches[linked_rule_watch_index]; + } else { + debug_assert_eq!(self.watched_literals[1], watched_solvable); + self.next_watches[1] = linked_rule.next_watches[linked_rule_watch_index]; + } + } + + pub fn next_watched_rule(&self, solvable_id: SolvableId) -> RuleId { + if solvable_id == self.watched_literals[0] { + self.next_watches[0] + } else { + debug_assert_eq!(self.watched_literals[1], solvable_id); + self.next_watches[1] + } + } + + // Returns the index of the watch that turned false, if any + pub fn watch_turned_false( + &self, + solvable_id: SolvableId, + decision_map: &DecisionMap, + learnt_rules: &[Vec], + ) -> Option<([Literal; 2], usize)> { + debug_assert!(self.watched_literals.contains(&solvable_id)); + + let literals @ [w1, w2] = self.watched_literals(learnt_rules); + + if solvable_id == w1.solvable_id && w1.eval(decision_map) == Some(false) { + Some((literals, 0)) + } else if solvable_id == w2.solvable_id && w2.eval(decision_map) == Some(false) { + Some((literals, 1)) + } else { + None + } + } + + pub fn has_watches(&self) -> bool { + // If the first watch is not null, the second won't be either + !self.watched_literals[0].is_null() + } + + pub fn watched_literals(&self, learnt_rules: &[Vec]) -> [Literal; 2] { + let literals = |op1: bool, op2: bool| { + [ + Literal { + solvable_id: self.watched_literals[0], + negate: !op1, + }, + Literal { + solvable_id: self.watched_literals[1], + negate: !op2, + }, + ] + }; + + match self.kind { + RuleKind::InstallRoot => unreachable!(), + RuleKind::Learnt(index) => { + // TODO: this is probably not going to cut it for performance + let &w1 = learnt_rules[index] + .iter() + .find(|l| l.solvable_id == self.watched_literals[0]) + .unwrap(); + let &w2 = learnt_rules[index] + .iter() + .find(|l| l.solvable_id == self.watched_literals[1]) + .unwrap(); + [w1, w2] + } + RuleKind::Forbids(_, _) => literals(false, false), + RuleKind::Constrains(_, _) => literals(false, false), + RuleKind::Requires(solvable_id, _) => { + if self.watched_literals[0] == solvable_id { + literals(false, true) + } else if self.watched_literals[1] == solvable_id { + literals(true, false) + } else { + literals(true, true) + } + } + } + } + + pub fn next_unwatched_variable( + &self, + pool: &Pool, + learnt_rules: &[Vec], + decision_map: &DecisionMap, + ) -> Option { + // The next unwatched variable (if available), is a variable that is: + // * Not already being watched + // * Not yet decided, or decided in such a way that the literal yields true + let can_watch = |solvable_lit: Literal| { + !self.watched_literals.contains(&solvable_lit.solvable_id) + && solvable_lit.eval(decision_map).unwrap_or(true) + }; + + match self.kind { + RuleKind::InstallRoot => unreachable!(), + RuleKind::Learnt(index) => learnt_rules[index] + .iter() + .cloned() + .find(|&l| can_watch(l)) + .map(|l| l.solvable_id), + RuleKind::Forbids(_, _) => None, + RuleKind::Constrains(_, _) => None, + RuleKind::Requires(solvable_id, match_spec_id) => { + // The solvable that added this rule + let solvable_lit = Literal { + solvable_id, + negate: true, + }; + if can_watch(solvable_lit) { + return Some(solvable_id); + } + + // The available candidates + for &candidate in pool.match_spec_to_candidates[match_spec_id.index()] + .as_deref() + .unwrap() + { + let lit = Literal { + solvable_id: candidate, + negate: false, + }; + if can_watch(lit) { + return Some(candidate); + } + } + + // No solvable available to watch + None + } + } + } + + /// Returns the list of literals that constitute this rule + pub fn literals(&self, learnt_rules: &[Vec], pool: &Pool) -> Vec { + match self.kind { + RuleKind::InstallRoot => unreachable!(), + RuleKind::Learnt(index) => learnt_rules[index].clone(), + RuleKind::Requires(solvable_id, match_spec_id) => { + // All variables contribute to the conflict + std::iter::once(Literal { + solvable_id, + negate: true, + }) + .chain( + pool.match_spec_to_candidates[match_spec_id.index()] + .as_deref() + .unwrap() + .iter() + .cloned() + .map(|solvable_id| Literal { + solvable_id, + negate: false, + }), + ) + .collect() + } + RuleKind::Forbids(s1, s2) => { + vec![ + Literal { + solvable_id: s1, + negate: true, + }, + Literal { + solvable_id: s2, + negate: true, + }, + ] + } + RuleKind::Constrains(s1, s2) => { + vec![ + Literal { + solvable_id: s1, + negate: true, + }, + Literal { + solvable_id: s2, + negate: true, + }, + ] + } + } + } + + /// Returns the list of variables that imply that the provided solvable should be decided + pub fn conflict_causes( + &self, + variable: SolvableId, + learnt_rules: &[Vec], + pool: &Pool, + ) -> Vec { + match self.kind { + RuleKind::InstallRoot => unreachable!(), + RuleKind::Learnt(index) => learnt_rules[index] + .iter() + .cloned() + .filter(|lit| lit.solvable_id != variable) + .collect(), + RuleKind::Requires(solvable_id, match_spec_id) => { + // All variables contribute to the conflict + std::iter::once(Literal { + solvable_id, + negate: true, + }) + .chain( + pool.match_spec_to_candidates[match_spec_id.index()] + .as_deref() + .unwrap() + .iter() + .cloned() + .map(|solvable_id| Literal { + solvable_id, + negate: false, + }), + ) + .filter(|&l| variable != l.solvable_id) + .collect() + } + RuleKind::Forbids(s1, s2) => { + let cause = if variable == s1 { s2 } else { s1 }; + + vec![Literal { + solvable_id: cause, + negate: true, + }] + } + + RuleKind::Constrains(s1, s2) => { + let cause = if variable == s1 { s2 } else { s1 }; + + vec![Literal { + solvable_id: cause, + negate: true, + }] + } + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub(crate) struct Literal { + pub(crate) solvable_id: SolvableId, + pub(crate) negate: bool, +} + +impl Literal { + pub(crate) fn satisfying_value(self) -> bool { + !self.negate + } + + pub(crate) fn eval(self, decision_map: &DecisionMap) -> Option { + decision_map + .value(self.solvable_id) + .map(|value| self.eval_inner(value)) + } + + fn eval_inner(self, solvable_value: bool) -> bool { + if self.negate { + !solvable_value + } else { + solvable_value + } + } +} + +#[derive(Copy, Clone, Debug)] +pub(crate) enum RuleKind { + InstallRoot, + /// The solvable requires the candidates associated to the match spec + /// + /// In SAT terms: (¬A ∨ B1 ∨ B2 ∨ ... ∨ B99), where B1 to B99 represent the possible candidates + /// for the provided match spec. + Requires(SolvableId, MatchSpecId), + /// The left solvable forbids installing the right solvable + /// + /// Used to ensure only a single version of a package is installed + /// + /// In SAT terms: (¬A ∨ ¬B) + Forbids(SolvableId, SolvableId), + /// Similar to forbid, but created due to a constrains relationship + Constrains(SolvableId, SolvableId), + /// Learnt rule + Learnt(usize), +} + +impl RuleKind { + fn initial_watches( + &self, + learnt_rules: &[Vec], + pool: &Pool, + ) -> Option<[SolvableId; 2]> { + match self { + RuleKind::InstallRoot => None, + RuleKind::Constrains(s1, s2) => Some([*s1, *s2]), + RuleKind::Forbids(s1, s2) => Some([*s1, *s2]), + RuleKind::Learnt(index) => { + let literals = &learnt_rules[*index]; + debug_assert!(!literals.is_empty()); + if literals.len() == 1 { + // No need for watches, since we learned an assertion + None + } else { + Some([ + literals.first().unwrap().solvable_id, + literals.last().unwrap().solvable_id, + ]) + } + } + RuleKind::Requires(id, match_spec) => { + let candidates = pool.match_spec_to_candidates[match_spec.index()] + .as_ref() + .unwrap(); + + if candidates.is_empty() { + None + } else { + Some([*id, candidates[0]]) + } + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + fn rule(next_rules: [RuleId; 2], watched_solvables: [SolvableId; 2]) -> Rule { + Rule { + watched_literals: watched_solvables, + next_watches: next_rules, + + // The kind is irrelevant here + kind: RuleKind::InstallRoot, + } + } + + #[test] + fn test_literal_satisfying_value() { + let lit = Literal { + solvable_id: SolvableId::root(), + negate: true, + }; + assert_eq!(lit.satisfying_value(), false); + + let lit = Literal { + solvable_id: SolvableId::root(), + negate: false, + }; + assert_eq!(lit.satisfying_value(), true); + } + + #[test] + fn test_literal_eval() { + let mut decision_map = DecisionMap::new(10); + + let literal = Literal { + solvable_id: SolvableId::root(), + negate: false, + }; + let negated_literal = Literal { + solvable_id: SolvableId::root(), + negate: true, + }; + + // Undecided + assert_eq!(literal.eval(&decision_map), None); + assert_eq!(negated_literal.eval(&decision_map), None); + + // Decided + decision_map.set(SolvableId::root(), true, 1); + assert_eq!(literal.eval(&decision_map), Some(true)); + assert_eq!(negated_literal.eval(&decision_map), Some(false)); + + decision_map.set(SolvableId::root(), false, 1); + assert_eq!(literal.eval(&decision_map), Some(false)); + assert_eq!(negated_literal.eval(&decision_map), Some(true)); + } + + #[test] + fn test_unlink_rule_different() { + let rule1 = rule( + [RuleId::new(2), RuleId::new(3)], + [SolvableId::new(1596), SolvableId::new(1211)], + ); + let rule2 = rule( + [RuleId::null(), RuleId::new(3)], + [SolvableId::new(1596), SolvableId::new(1208)], + ); + let rule3 = rule( + [RuleId::null(), RuleId::null()], + [SolvableId::new(1211), SolvableId::new(42)], + ); + + // Unlink 0 + { + let mut rule1 = rule1.clone(); + rule1.unlink_rule(&rule2, SolvableId::new(1596), 0); + assert_eq!( + rule1.watched_literals, + [SolvableId::new(1596), SolvableId::new(1211)] + ); + assert_eq!(rule1.next_watches, [RuleId::null(), RuleId::new(3)]) + } + + // Unlink 1 + { + let mut rule1 = rule1.clone(); + rule1.unlink_rule(&rule3, SolvableId::new(1211), 0); + assert_eq!( + rule1.watched_literals, + [SolvableId::new(1596), SolvableId::new(1211)] + ); + assert_eq!(rule1.next_watches, [RuleId::new(2), RuleId::null()]) + } + } + + #[test] + fn test_unlink_rule_same() { + let rule1 = rule( + [RuleId::new(2), RuleId::new(2)], + [SolvableId::new(1596), SolvableId::new(1211)], + ); + let rule2 = rule( + [RuleId::null(), RuleId::null()], + [SolvableId::new(1596), SolvableId::new(1211)], + ); + + // Unlink 0 + { + let mut rule1 = rule1.clone(); + rule1.unlink_rule(&rule2, SolvableId::new(1596), 0); + assert_eq!( + rule1.watched_literals, + [SolvableId::new(1596), SolvableId::new(1211)] + ); + assert_eq!(rule1.next_watches, [RuleId::null(), RuleId::new(2)]) + } + + // Unlink 1 + { + let mut rule1 = rule1.clone(); + rule1.unlink_rule(&rule2, SolvableId::new(1211), 1); + assert_eq!( + rule1.watched_literals, + [SolvableId::new(1596), SolvableId::new(1211)] + ); + assert_eq!(rule1.next_watches, [RuleId::new(2), RuleId::null()]) + } + } +} diff --git a/crates/libsolv_rs/src/solvable.rs b/crates/libsolv_rs/src/solvable.rs new file mode 100644 index 000000000..4e0501bf1 --- /dev/null +++ b/crates/libsolv_rs/src/solvable.rs @@ -0,0 +1,150 @@ +use crate::pool::{MatchSpecId, RepoId, StringId}; +use rattler_conda_types::{PackageRecord, Version}; +use std::fmt::{Display, Formatter}; + +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +pub struct SolvableId(u32); + +impl SolvableId { + pub(crate) fn new(index: usize) -> Self { + Self(index as u32) + } + + pub(crate) fn root() -> Self { + Self(0) + } + + pub(crate) fn null() -> Self { + Self(u32::MAX) + } + + pub(crate) fn is_null(self) -> bool { + self.0 == u32::MAX + } + + pub(crate) fn index(self) -> usize { + self.0 as usize + } +} + +pub(crate) struct SolvableDisplay<'a> { + name: &'a str, + version: Option<&'a Version>, + build: Option<&'a str>, +} + +impl Display for SolvableDisplay<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name)?; + if let Some(version) = self.version { + write!(f, " {}", version)?; + } + + if let Some(build) = self.build { + if !build.is_empty() { + write!(f, " {}", build)?; + } + } + + Ok(()) + } +} + +pub struct PackageSolvable { + pub(crate) repo_id: RepoId, + pub(crate) dependencies: Vec, + pub(crate) constrains: Vec, + pub(crate) record: &'static PackageRecord, + pub(crate) name: StringId, + // pub version: StringId, + pub metadata: SolvableMetadata, +} + +impl PackageSolvable { + pub fn repo_id(&self) -> RepoId { + self.repo_id + } +} + +#[derive(Default)] +pub struct SolvableMetadata { + pub original_index: Option, +} + +pub(crate) struct Solvable { + pub(crate) inner: SolvableInner, +} + +pub(crate) enum SolvableInner { + Root(Vec), + Package(PackageSolvable), +} + +impl Solvable { + pub(crate) fn new_root() -> Self { + Self { + inner: SolvableInner::Root(Vec::new()), + } + } + + pub(crate) fn new_package( + repo_id: RepoId, + name: StringId, + record: &'static PackageRecord, + ) -> Self { + Self { + inner: SolvableInner::Package(PackageSolvable { + repo_id, + record, + name, + dependencies: Vec::new(), + constrains: Vec::new(), + metadata: SolvableMetadata::default(), + }), + } + } + + pub(crate) fn display(&self) -> SolvableDisplay { + match &self.inner { + SolvableInner::Root(_) => SolvableDisplay { + name: "root", + version: None, + build: None, + }, + SolvableInner::Package(p) => SolvableDisplay { + name: &p.record.name, + version: Some(&p.record.version), + build: Some(&p.record.build), + }, + } + } + + pub(crate) fn root_mut(&mut self) -> &mut Vec { + match &mut self.inner { + SolvableInner::Root(match_specs) => match_specs, + _ => panic!("unexpected package solvable!"), + } + } + + pub(crate) fn get_package(&self) -> Option<&PackageSolvable> { + match &self.inner { + SolvableInner::Root(_) => None, + SolvableInner::Package(p) => Some(p), + } + } + + pub(crate) fn get_package_mut(&mut self) -> Option<&mut PackageSolvable> { + match &mut self.inner { + SolvableInner::Root(_) => None, + SolvableInner::Package(p) => Some(p), + } + } + + pub fn package(&self) -> &PackageSolvable { + self.get_package().expect("unexpected root solvable") + } + + pub fn package_mut(&mut self) -> &mut PackageSolvable { + self.get_package_mut().expect("unexpected root solvable") + } +} diff --git a/crates/libsolv_rs/src/solve_jobs.rs b/crates/libsolv_rs/src/solve_jobs.rs new file mode 100644 index 000000000..d00aa5c13 --- /dev/null +++ b/crates/libsolv_rs/src/solve_jobs.rs @@ -0,0 +1,28 @@ +use crate::solvable::SolvableId; +use rattler_conda_types::MatchSpec; + +#[derive(Default)] +pub struct SolveJobs { + pub(crate) install: Vec, + pub(crate) favor: Vec, + pub(crate) lock: Vec, +} + +impl SolveJobs { + /// The specified spec must be installed + pub fn install(&mut self, match_spec: MatchSpec) { + self.install.push(match_spec); + } + + /// Favor the specified solvable over other variants. This doesnt mean this variant will be + /// used. To guarantee a solvable is used (if selected) use the `Self::lock` function. + pub fn favor(&mut self, id: SolvableId) { + self.favor.push(id); + } + + /// Lock the specified solvable over other variants. This implies that not other variant will + /// ever be considered. + pub fn lock(&mut self, id: SolvableId) { + self.lock.push(id); + } +} diff --git a/crates/libsolv_rs/src/solve_problem.rs b/crates/libsolv_rs/src/solve_problem.rs new file mode 100644 index 000000000..de8ce46db --- /dev/null +++ b/crates/libsolv_rs/src/solve_problem.rs @@ -0,0 +1,88 @@ +use crate::pool::StringId; + +#[derive(Debug)] +pub enum SolveProblem { + /// A top level requirement. + /// The difference between JOB and PKG is unknown (possibly unused). + Job { dep: String }, + /// A top level dependency does not exist. + /// Could be a wrong name or missing channel. + JobNothingProvidesDep { dep: String }, + /// A top level dependency does not exist. + /// Could be a wrong name or missing channel. + JobUnknownPackage { dep: String }, + /// A top level requirement. + /// The difference between JOB and PKG is unknown (possibly unused). + Pkg { dep: String }, + /// Looking for a valid solution to the installation satisfiability expand to + /// two solvables of same package that cannot be installed together. This is + /// a partial exaplanation of why one of the solvables (could be any of the + /// parent) cannot be installed. + PkgConflicts { source: StringId, target: StringId }, + /// A constraint (run_constrained) on source is conflicting with target. + /// SOLVER_RULE_PKG_CONSTRAINS has a dep, but it can resolve to nothing. + /// The constraint conflict is actually expressed between the target and + /// a constrains node child of the source. + PkgConstrains { + source: StringId, + target: StringId, + dep: String, + }, + /// A package dependency does not exist. + /// Could be a wrong name or missing channel. + /// This is a partial exaplanation of why a specific solvable (could be any + /// of the parent) cannot be installed. + PkgNothingProvidesDep { source: StringId, dep: String }, + /// Express a dependency on source that is involved in explaining the + /// problem. + /// Not all dependency of package will appear, only enough to explain the + //. problem. It is not a problem in itself, only a part of the graph. + PkgRequires { source: StringId, dep: String }, + /// Package conflict between two solvables of same package name (handled the same as + /// [`SolveProblem::PkgConflicts`]). + PkgSameName { source: StringId, target: StringId }, + /// Encounterd in the problems list from libsolv but unknown. + /// Explicitly ignored until we do something with it. + Update, +} + +// impl SolveProblem { +// pub fn from_raw( +// problem_type: ffi::SolverRuleinfo, +// dep: Option, +// source: Option, +// target: Option, +// ) -> Self { +// match problem_type { +// SOLVER_RULE_JOB => Self::Job { dep: dep.unwrap() }, +// SOLVER_RULE_JOB_NOTHING_PROVIDES_DEP => { +// Self::JobNothingProvidesDep { dep: dep.unwrap() } +// } +// SOLVER_RULE_JOB_UNKNOWN_PACKAGE => Self::JobUnknownPackage { dep: dep.unwrap() }, +// SOLVER_RULE_PKG => Self::Pkg { dep: dep.unwrap() }, +// SOLVER_RULE_SOLVER_RULE_PKG_CONFLICTS => Self::PkgConflicts { +// source: source.unwrap(), +// target: target.unwrap(), +// }, +// SOLVER_RULE_PKG_CONSTRAINS => Self::PkgConstrains { +// source: source.unwrap(), +// target: target.unwrap(), +// dep: dep.unwrap(), +// }, +// SOLVER_RULE_SOLVER_RULE_PKG_NOTHING_PROVIDES_DEP => Self::PkgNothingProvidesDep { +// source: source.unwrap(), +// dep: dep.unwrap(), +// }, +// SOLVER_RULE_PKG_REQUIRES => Self::PkgRequires { +// source: source.unwrap(), +// dep: dep.unwrap(), +// }, +// SOLVER_RULE_SOLVER_RULE_PKG_SAME_NAME => Self::PkgSameName { +// source: source.unwrap(), +// target: target.unwrap(), +// }, +// SOLVER_RULE_SOLVER_RULE_UPDATE => Self::Update, +// _ => panic!("Unknown problem type: {}", problem_type), +// } +// } +// } diff --git a/crates/libsolv_rs/src/solver.rs b/crates/libsolv_rs/src/solver.rs new file mode 100644 index 000000000..3cdc507d6 --- /dev/null +++ b/crates/libsolv_rs/src/solver.rs @@ -0,0 +1,1185 @@ +use crate::pool::{MatchSpecId, Pool, StringId}; +use crate::rules::{Literal, Rule, RuleKind}; +use crate::solvable::SolvableId; +use crate::solve_jobs::SolveJobs; + +use crate::watch_map::WatchMap; + +use crate::decision_tracker::DecisionTracker; +use rattler_conda_types::MatchSpec; +use std::collections::{HashMap, HashSet}; +use std::fmt::{Display, Formatter}; + +#[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Debug)] +pub(crate) struct RuleId(u32); + +impl RuleId { + pub(crate) fn new(index: usize) -> Self { + Self(index as u32) + } + + fn install_root() -> Self { + Self(0) + } + + fn index(self) -> usize { + self.0 as usize + } + + fn is_null(self) -> bool { + self.0 == u32::MAX + } + + pub(crate) fn null() -> RuleId { + RuleId(u32::MAX) + } +} + +#[derive(Copy, Clone, Eq, PartialEq)] +pub(crate) struct Decision { + pub(crate) solvable_id: SolvableId, + pub(crate) value: bool, + pub(crate) derived_from: RuleId, +} + +impl Decision { + pub(crate) fn new(solvable: SolvableId, value: bool, derived_from: RuleId) -> Self { + Self { + solvable_id: solvable, + value, + derived_from, + } + } +} + +pub struct Transaction { + pub steps: Vec<(SolvableId, TransactionKind)>, +} + +#[derive(Copy, Clone, Debug)] +pub enum TransactionKind { + Install, +} + +impl Display for TransactionKind { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +pub struct Solver { + pool: Pool, + + rules: Vec, + watches: WatchMap, + + learnt_rules: Vec>, + learnt_rules_start: RuleId, + + decision_tracker: DecisionTracker, +} + +impl Solver { + /// Create a solver, using the provided pool + pub fn new(pool: Pool) -> Self { + Self { + rules: Vec::new(), + watches: WatchMap::new(), + learnt_rules: Vec::new(), + learnt_rules_start: RuleId(0), + decision_tracker: DecisionTracker::new(pool.nsolvables()), + pool, + } + } + + pub fn pool(&self) -> &Pool { + &self.pool + } + + /// Creates a string for each 'problem' that the solver still has which it encountered while + /// solving the matchspecs. Use this function to print the existing problems to string. + fn solver_problems(&self) -> Vec { + Vec::new() + } + + /// Solves the provided `jobs` and returns a transaction from the found solution. + /// + /// Returns an error if problems remain unsolved. + pub fn solve(&mut self, jobs: SolveJobs) -> Result> { + // TODO: sanity check that solvables inside jobs.lock and jobs.favor are unique + + // Clear state + self.pool.root_solvable_mut().clear(); + self.decision_tracker.clear(); + self.rules = vec![Rule::new(RuleKind::InstallRoot, &[], &self.pool)]; + self.learnt_rules.clear(); + + // Favored map + let mut favored_map = HashMap::new(); + for &favored_id in &jobs.favor { + let name_id = self.pool.resolve_solvable_inner(favored_id).package().name; + favored_map.insert(name_id, favored_id); + } + + // Initialize the root solvable with the requested packages as dependencies + let mut visited_solvables = HashSet::default(); + for match_spec in &jobs.install { + let match_spec_id = self.pool.intern_matchspec(match_spec.to_string()); + let root_solvable = self.pool.root_solvable_mut(); + root_solvable.push(match_spec_id); + + // Recursively add rules for the current dep + self.add_rules_for_root_dep(&mut visited_solvables, &favored_map, match_spec_id); + } + + // Initialize rules ensuring only a single candidate per package name is installed + for candidates in self.pool.packages_by_name.values() { + // Each candidate gets a rule with each other candidate + for (i, &candidate) in candidates.iter().enumerate() { + for &other_candidate in &candidates[i + 1..] { + self.rules.push(Rule::new( + RuleKind::Forbids(candidate, other_candidate), + &self.learnt_rules, + &self.pool, + )); + } + } + } + + // All new rules are learnt after this point + self.learnt_rules_start = RuleId::new(self.rules.len()); + + // Create watches chains + self.make_watches(); + + // Run SAT + self.run_sat(&jobs.install, &jobs.lock); + + let remaining_problems = self.solver_problems(); + if remaining_problems.is_empty() { + let steps = self + .decision_tracker + .stack() + .iter() + .flat_map(|d| { + if d.value && d.solvable_id != SolvableId::root() { + Some((d.solvable_id, TransactionKind::Install)) + } else { + // Ignore things that are set to false + None + } + }) + .collect(); + Ok(Transaction { steps }) + } else { + Err(remaining_problems) + } + } + + fn add_rules_for_root_dep( + &mut self, + visited: &mut HashSet, + favored_map: &HashMap, + dep: MatchSpecId, + ) { + let mut candidate_stack = Vec::new(); + + // Gather direct candidates for the dependency + { + let candidates = Pool::get_candidates( + &self.pool.match_specs, + &self.pool.strings_to_ids, + &self.pool.solvables, + &self.pool.packages_by_name, + &mut self.pool.match_spec_to_candidates, + favored_map, + dep, + ); + for &candidate in candidates { + if visited.insert(candidate) { + candidate_stack.push(candidate); + } + } + } + + // Process candidates, adding their dependencies recursively + while let Some(candidate) = candidate_stack.pop() { + let solvable = self.pool.solvables[candidate.index()].package(); + + // Requires + for &dep in &solvable.dependencies { + // Ensure the candidates have their rules added + let dep_candidates = Pool::get_candidates( + &self.pool.match_specs, + &self.pool.strings_to_ids, + &self.pool.solvables, + &self.pool.packages_by_name, + &mut self.pool.match_spec_to_candidates, + favored_map, + dep, + ); + + for &dep_candidate in dep_candidates { + if visited.insert(dep_candidate) { + candidate_stack.push(dep_candidate); + } + } + + // Create requires rule + self.rules.push(Rule::new( + RuleKind::Requires(candidate, dep), + &self.learnt_rules, + &self.pool, + )); + } + + // Constrains + for &dep in &solvable.constrains { + let dep_forbidden = Pool::get_forbidden( + &self.pool.match_specs, + &self.pool.strings_to_ids, + &self.pool.solvables, + &self.pool.packages_by_name, + &mut self.pool.match_spec_to_forbidden, + dep, + ) + .to_vec(); + + for dep in dep_forbidden { + self.rules.push(Rule::new( + RuleKind::Constrains(candidate, dep), + &self.learnt_rules, + &self.pool, + )); + } + } + } + + // Root has a requirement on this match spec + self.rules.push(Rule::new( + RuleKind::Requires(SolvableId::root(), dep), + &self.learnt_rules, + &self.pool, + )); + } + + fn run_sat(&mut self, top_level_requirements: &[MatchSpec], locked_solvables: &[SolvableId]) { + let level = match self.install_root_solvable() { + Ok(new_level) => new_level, + Err(_) => panic!("install root solvable failed"), + }; + + if self.decide_top_level_assertions(level, locked_solvables).is_err() { + panic!("propagate assertions failed"); + }; + + if self.decide_top_level_exclusions(top_level_requirements).is_err() { + panic!("decide top level exclusions failed"); + } + + if let Err((_, _, cause)) = self.propagate(level) { + self.analyze_unsolvable(cause, false); + panic!("Propagate after installing root failed"); + } + + if self.resolve_dependencies(level).is_err() { + panic!("Resolve dependencies failed"); + } + } + + fn install_root_solvable(&mut self) -> Result { + assert!(self.decision_tracker.is_empty()); + self.decision_tracker + .try_add_decision(Decision::new(SolvableId::root(), true, RuleId::install_root()), 1) + .expect("bug: solvable was already decided!"); + Ok(1) + } + + fn decide_top_level_assertions( + &mut self, + level: u32, + locked_solvables: &[SolvableId], + ) -> Result<(), ()> { + println!("=== Deciding assertions"); + + // Assertions derived from locked dependencies + for &locked_solvable_id in locked_solvables { + // For each locked solvable, decide that other solvables with the same name are + // forbidden + let name = self + .pool + .resolve_solvable_inner(locked_solvable_id) + .package() + .name; + if let Some(other_candidates) = self.pool.packages_by_name.get(&name) { + for &other_candidate in other_candidates { + if other_candidate != locked_solvable_id { + self.decision_tracker.try_add_decision( + Decision::new(other_candidate, false, RuleId::install_root()), + level, + )?; + } + } + } + } + + // Assertions derived from requirements that cannot be fulfilled + for (i, rule) in self.rules.iter().enumerate() { + if let RuleKind::Requires(solvable_id, _) = rule.kind { + if !rule.has_watches() { + // A requires rule without watches means it has a single literal (i.e. + // there are no candidates) + let decided = self.decision_tracker.try_add_decision( + Decision::new(solvable_id, false, RuleId::new(i)), + level, + )?; + + if decided { + println!( + "Set {} = false", + self.pool.resolve_solvable_inner(solvable_id).display() + ); + } + } + } + } + + Ok(()) + } + + fn decide_top_level_exclusions( + &mut self, + top_level_requirements: &[MatchSpec], + ) -> Result<(), ()> { + println!("=== Deciding exclusions"); + for req in top_level_requirements { + let name = req.name.as_deref().unwrap(); + + let Some(candidates) = self.pool.strings_to_ids.get(name).and_then(|name_id| self.pool.packages_by_name.get(name_id)) else { + return Ok(()); + }; + let non_matching = candidates.iter().filter(|solvable_id| { + let solvable = &self.pool.solvables[solvable_id.index()]; + !req.matches(solvable.package().record) + }); + + for &solvable_id in non_matching { + let decided = self + .decision_tracker + .try_add_decision(Decision::new(solvable_id, false, RuleId::new(0)), 1)?; + if decided { + println!( + "{} = false", + self.pool.resolve_solvable_inner(solvable_id).display() + ); + } + } + } + + Ok(()) + } + + /// Resolves all dependencies + fn resolve_dependencies(&mut self, mut level: u32) -> Result { + let mut i = 0; + loop { + if i >= self.rules.len() { + break; + } + + let (required_by, candidate) = { + let rule = &self.rules[i]; + i += 1; + + // We are only interested in requires rules + let RuleKind::Requires(solvable_id, deps) = rule.kind else { + continue; + }; + + // Consider only rules in which we have decided to install the solvable + if self.decision_tracker.assigned_value(solvable_id) != Some(true) { + continue; + } + + // Consider only rules in which no candidates have been installed + let candidates = self.pool.match_spec_to_candidates[deps.index()] + .as_deref() + .unwrap(); + if candidates + .iter() + .any(|&c| self.decision_tracker.assigned_value(c) == Some(true)) + { + continue; + } + + // Get the first candidate that is undecided and should be installed + // + // This assumes that the packages have been provided in the right order when the solvables were created + // (most recent packages first) + ( + solvable_id, + candidates + .iter() + .cloned() + .find(|&c| self.decision_tracker.assigned_value(c).is_none()) + .unwrap(), + ) + }; + + // Assumption: there are multiple candidates, otherwise this would have already been handled + // by unit propagation + self.create_branch(); + level = + self.set_propagate_learn(level, candidate, required_by, true, RuleId::new(i))?; + + // if level < orig_level { + // return Err(level); + // } + + // We have made progress, and should look at all rules in the next iteration + i = 0; + } + + // We just went through all rules and there are no choices left to be made + Ok(level) + } + + fn set_propagate_learn( + &mut self, + mut level: u32, + solvable: SolvableId, + required_by: SolvableId, + disable_rules: bool, + rule_id: RuleId, + ) -> Result { + level += 1; + + println!( + "=== Install {} at level {level} (required by {})", + self.pool.resolve_solvable_inner(solvable).display(), + self.pool.resolve_solvable_inner(required_by).display(), + ); + + self.decision_tracker + .try_add_decision(Decision::new(solvable, true, rule_id), level) + .expect("bug: solvable was already decided!"); + + loop { + let r = self.propagate(level); + let Err((conflicting_solvable, attempted_value, conflicting_rule)) = r else { + // Propagation succeeded + println!("=== Propagation succeeded"); + break; + }; + + { + let solvable = self + .pool + .resolve_solvable_inner(conflicting_solvable) + .display(); + println!( + "=== Propagation conflicted: could not set {solvable} to {attempted_value}" + ); + print!("During unit propagation for rule: "); + self.rules[conflicting_rule.index()].debug(&self.pool); + let decision = self + .decision_tracker + .stack() + .iter() + .find(|d| d.solvable_id == conflicting_solvable) + .unwrap(); + print!( + "Previously decided value: {}. Derived from: ", + !attempted_value + ); + self.rules[decision.derived_from.index()].debug(&self.pool); + } + + if level == 1 { + // Is it really unsolvable if we are back to level 1? + println!("=== UNSOLVABLE"); + for decision in self.decision_tracker.stack() { + let rule = &self.rules[decision.derived_from.index()]; + let level = self.decision_tracker.level(decision.solvable_id); + let action = if decision.value { "install" } else { "forbid" }; + + if let RuleKind::Forbids(_, _) = rule.kind { + // Skip forbids rules, to reduce noise + continue; + } + + print!( + "* ({level}) {action} {}", + self.pool + .resolve_solvable_inner(decision.solvable_id) + .display() + ); + print!(". Reason: "); + rule.debug(&self.pool); + } + self.analyze_unsolvable(conflicting_rule, disable_rules); + return Err(()); + } + + let (new_level, learned_rule_id, literal) = + self.analyze(level, conflicting_solvable, conflicting_rule); + level = new_level; + + println!("=== Backtracked to level {level}"); + + // Optimization: propagate right now, since we know that the rule is a unit clause + let decision = literal.satisfying_value(); + self.decision_tracker + .try_add_decision( + Decision::new(literal.solvable_id, decision, learned_rule_id), + level, + ) + .expect("bug: solvable was already decided!"); + println!( + "=== Propagate after learn: {} = {decision}", + self.pool + .resolve_solvable_inner(literal.solvable_id) + .display() + ); + } + + Ok(level) + } + + fn create_branch(&mut self) { + // TODO: we should probably keep info here for backtracking + } + + fn propagate(&mut self, level: u32) -> Result<(), (SolvableId, bool, RuleId)> { + // Learnt assertions + let learnt_rules_start = self.learnt_rules_start.index(); + for (i, rule) in self.rules[learnt_rules_start..].iter().enumerate() { + let RuleKind::Learnt(learnt_index) = rule.kind else { + unreachable!(); + }; + + let literals = &self.learnt_rules[learnt_index]; + if literals.len() > 1 { + continue; + } + + debug_assert!(!literals.is_empty()); + + let literal = literals[0]; + let decision = literal.satisfying_value(); + let rule_id = RuleId::new(learnt_rules_start + i); + + let decided = self + .decision_tracker + .try_add_decision(Decision::new(literal.solvable_id, decision, rule_id), level) + .map_err(|_| (literal.solvable_id, decision, rule_id))?; + + if decided { + let s = self.pool.resolve_solvable_inner(literal.solvable_id); + println!("Propagate assertion {} = {}", s.display(), decision); + } + } + + // Watched literals + while let Some(decision) = self.decision_tracker.next_unpropagated() { + let pkg = decision.solvable_id; + + // Propagate, iterating through the linked list of rules that watch this solvable + let mut old_predecessor_rule_id: Option; + let mut predecessor_rule_id: Option = None; + let mut rule_id = self.watches.first_rule_watching_solvable(pkg); + while !rule_id.is_null() { + if predecessor_rule_id == Some(rule_id) { + panic!("Linked list is circular!"); + } + + // This is a convoluted way of getting mutable access to the current and the previous rule, + // which is necessary when we have to remove the current rule from the list + let (predecessor_rule, rule) = if let Some(prev_rule_id) = predecessor_rule_id { + if prev_rule_id < rule_id { + let (prev, current) = self.rules.split_at_mut(rule_id.index()); + (Some(&mut prev[prev_rule_id.index()]), &mut current[0]) + } else { + let (current, prev) = self.rules.split_at_mut(prev_rule_id.index()); + (Some(&mut prev[0]), &mut current[rule_id.index()]) + } + } else { + (None, &mut self.rules[rule_id.index()]) + }; + + // Update the prev_rule_id for the next run + old_predecessor_rule_id = predecessor_rule_id; + predecessor_rule_id = Some(rule_id); + + // Configure the next rule to visit + let this_rule_id = rule_id; + rule_id = rule.next_watched_rule(pkg); + + if let Some((watched_literals, watch_index)) = + rule.watch_turned_false(pkg, self.decision_tracker.map(), &self.learnt_rules) + { + // One of the watched literals is now false + if let Some(variable) = rule.next_unwatched_variable( + &self.pool, + &self.learnt_rules, + self.decision_tracker.map(), + ) { + debug_assert!(!rule.watched_literals.contains(&variable)); + + self.watches.update_watched( + predecessor_rule, + rule, + this_rule_id, + watch_index, + pkg, + variable, + ); + + // Make sure the right predecessor is kept for the next iteration (i.e. the + // current rule is no longer a predecessor of the next one; the current + // rule's predecessor is) + predecessor_rule_id = old_predecessor_rule_id; + } else { + // We could not find another literal to watch, which means the remaining + // watched literal can be set to true + let remaining_watch_index = match watch_index { + 0 => 1, + 1 => 0, + _ => unreachable!(), + }; + + let remaining_watch = watched_literals[remaining_watch_index]; + let decided = self + .decision_tracker + .try_add_decision( + Decision::new( + remaining_watch.solvable_id, + remaining_watch.satisfying_value(), + this_rule_id, + ), + level, + ) + .map_err(|_| (remaining_watch.solvable_id, true, this_rule_id))?; + + if decided { + match rule.kind { + RuleKind::InstallRoot + | RuleKind::Requires(_, _) + | RuleKind::Constrains(_, _) + | RuleKind::Learnt(_) => { + print!( + "Propagate {} = {}. ", + self.pool + .resolve_solvable_inner(remaining_watch.solvable_id) + .display(), + remaining_watch.satisfying_value() + ); + rule.debug(&self.pool); + } + // Skip logging for forbids, which is so noisy + RuleKind::Forbids(_, _) => {} + } + } + } + } + } + } + + Ok(()) + } + + fn analyze_unsolvable(&mut self, _rule: RuleId, _disable_rules: bool) { + // todo!() + } + + fn analyze( + &mut self, + mut current_level: u32, + mut s: SolvableId, + mut rule_id: RuleId, + ) -> (u32, RuleId, Literal) { + let mut seen = HashSet::new(); + let mut causes_at_current_level = 0u32; + let mut learnt = Vec::new(); + let mut btlevel = 0; + + // println!("=== ANALYZE"); + + let mut first_iteration = true; + let mut s_value; + loop { + // TODO: we should be able to get rid of the branching, always retrieving the whole list + // of literals, since the hash set will ensure we aren't considering the conflicting + // solvable after the first iteration + let causes = if first_iteration { + first_iteration = false; + self.rules[rule_id.index()].literals(&self.learnt_rules, &self.pool) + } else { + self.rules[rule_id.index()].conflict_causes(s, &self.learnt_rules, &self.pool) + }; + + debug_assert!(!causes.is_empty()); + + // print!("level = {current_level}; rule: "); + // self.rules[rule_id.index()].debug(&self.pool); + + // Collect literals that imply that `s` should be assigned a given value (triggering a conflict) + for cause in causes { + if seen.insert(cause.solvable_id) { + let decision_level = self.decision_tracker.level(cause.solvable_id); + // let decision = self + // .decision_tracker + // .assigned_value(cause.solvable_id) + // .unwrap(); + // println!( + // "- {} = {} (level {decision_level})", + // self.pool.solvables[cause.solvable_id.index()].display(), + // decision + // ); + if decision_level == current_level { + causes_at_current_level += 1; + } else if current_level > 1 { + let learnt_literal = Literal { + solvable_id: cause.solvable_id, + negate: self + .decision_tracker + .assigned_value(cause.solvable_id) + .unwrap(), + }; + learnt.push(learnt_literal); + btlevel = btlevel.max(decision_level); + } else { + // A conflict with a decision at level 1 means the problem is unsatisfiable + // (otherwise we would "learn" that the decision at level 1 was wrong, but + // those decisions are either directly provided by [or derived from] the + // user's input) + panic!("unsolvable"); + } + } + } + + // Select next literal to look at + loop { + let (last_decision, last_decision_level) = self.decision_tracker.undo_last(); + + s = last_decision.solvable_id; + s_value = last_decision.value; + rule_id = last_decision.derived_from; + + current_level = last_decision_level; + + // We are interested in the first literal we come across that caused the conflicting + // assignment + if seen.contains(&s) { + break; + } + } + + causes_at_current_level = causes_at_current_level.saturating_sub(1); + if causes_at_current_level == 0 { + break; + } + } + + let last_literal = Literal { + solvable_id: s, + negate: s_value, + }; + learnt.push(last_literal); + + // Add the rule + let rule_id = RuleId::new(self.rules.len()); + let learnt_index = self.learnt_rules.len(); + self.learnt_rules.push(learnt.clone()); + + let mut rule = Rule::new( + RuleKind::Learnt(learnt_index), + &self.learnt_rules, + &self.pool, + ); + + if rule.has_watches() { + self.watches.start_watching(&mut rule, rule_id); + } + + // Store it + self.rules.push(rule); + + println!("Learnt disjunction:"); + for lit in learnt { + let yes_no = if lit.negate { "NOT " } else { "" }; + println!( + "- {yes_no}{}", + self.pool.resolve_solvable_inner(lit.solvable_id).display() + ); + } + + // println!("Backtracked from {level} to {btlevel}"); + + // print!("Last decision before backtracking: "); + // let decision = self.decision_queue.back().unwrap(); + // self.pool.resolve_solvable(decision.solvable_id).debug(); + // println!(" = {}", decision.value); + + // Should revert at most to the root level + let target_level = btlevel.max(1); + self.decision_tracker.undo_until(target_level); + + (target_level, rule_id, last_literal) + } + + fn make_watches(&mut self) { + self.watches.initialize(self.pool.solvables.len()); + + // Watches are already initialized in the rules themselves, here we build a linked list for + // each package (a rule will be linked to other rules that are watching the same package) + for (i, rule) in self.rules.iter_mut().enumerate() { + if !rule.has_watches() { + // Skip rules without watches + continue; + } + + self.watches.start_watching(rule, RuleId::new(i)); + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use rattler_conda_types::{PackageRecord, Version}; + use std::str::FromStr; + + fn pool(packages: &[(&str, &str, Vec<&str>)]) -> Pool { + let mut pool = Pool::new(); + let repo_id = pool.new_repo(""); + + for (pkg_name, version, deps) in packages { + let pkg_name = *pkg_name; + let version = *version; + let record = Box::new(PackageRecord { + arch: None, + build: "".to_string(), + build_number: 0, + constrains: vec![], + depends: deps.iter().map(|s| s.to_string()).collect(), + features: None, + legacy_bz2_md5: None, + legacy_bz2_size: None, + license: None, + license_family: None, + md5: None, + name: pkg_name.to_string(), + noarch: Default::default(), + platform: None, + sha256: None, + size: None, + subdir: "".to_string(), + timestamp: None, + track_features: vec![], + version: version.parse().unwrap(), + }); + + let solvable_id = pool.add_package(repo_id, Box::leak(record)); + + for &dep in deps { + pool.add_dependency(solvable_id, dep.to_string()); + } + } + + pool + } + + fn install(packages: &[&str]) -> SolveJobs { + let mut jobs = SolveJobs::default(); + for &p in packages { + jobs.install(p.parse().unwrap()); + } + jobs + } + + fn transaction_to_string(pool: &Pool, transaction: &Transaction) -> String { + use std::fmt::Write; + let mut buf = String::new(); + for &(solvable_id, _) in &transaction.steps { + writeln!( + buf, + "{}", + pool.resolve_solvable_inner(solvable_id).display() + ) + .unwrap(); + } + + buf + } + + #[test] + fn test_unit_propagation_1() { + let pool = pool(&[("asdf", "1.2.3", vec![])]); + let mut solver = Solver::new(pool); + let solved = solver.solve(install(&["asdf"])).unwrap(); + + assert_eq!(solved.steps.len(), 1); + + let solvable = solver + .pool + .resolve_solvable_inner(solved.steps[0].0) + .package(); + assert_eq!(solvable.record.name, "asdf"); + assert_eq!(solvable.record.version.to_string(), "1.2.3"); + } + + #[test] + fn test_unit_propagation_nested() { + let pool = pool(&[ + ("asdf", "1.2.3", vec!["efgh"]), + ("efgh", "4.5.6", vec![]), + ("dummy", "42.42.42", vec![]), + ]); + let mut solver = Solver::new(pool); + let solved = solver.solve(install(&["asdf"])).unwrap(); + + assert_eq!(solved.steps.len(), 2); + + let solvable = solver + .pool + .resolve_solvable_inner(solved.steps[0].0) + .package(); + assert_eq!(solvable.record.name, "asdf"); + assert_eq!(solvable.record.version.to_string(), "1.2.3"); + + let solvable = solver + .pool + .resolve_solvable_inner(solved.steps[1].0) + .package(); + assert_eq!(solvable.record.name, "efgh"); + assert_eq!(solvable.record.version.to_string(), "4.5.6"); + } + + #[test] + fn test_resolve_dependencies() { + let pool = pool(&[ + ("asdf", "1.2.4", vec![]), + ("asdf", "1.2.3", vec![]), + ("efgh", "4.5.7", vec![]), + ("efgh", "4.5.6", vec![]), + ]); + let mut solver = Solver::new(pool); + let solved = solver.solve(install(&["asdf", "efgh"])).unwrap(); + + assert_eq!(solved.steps.len(), 2); + + let solvable = solver + .pool + .resolve_solvable_inner(solved.steps[0].0) + .package(); + assert_eq!(solvable.record.name, "asdf"); + assert_eq!(solvable.record.version.to_string(), "1.2.4"); + + let solvable = solver + .pool + .resolve_solvable_inner(solved.steps[1].0) + .package(); + assert_eq!(solvable.record.name, "efgh"); + assert_eq!(solvable.record.version.to_string(), "4.5.7"); + } + + #[test] + fn test_resolve_with_conflict() { + let pool = pool(&[ + ("asdf", "1.2.4", vec!["conflicting=1.0.1"]), + ("asdf", "1.2.3", vec!["conflicting=1.0.0"]), + ("efgh", "4.5.7", vec!["conflicting=1.0.0"]), + ("efgh", "4.5.6", vec!["conflicting=1.0.0"]), + ("conflicting", "1.0.1", vec![]), + ("conflicting", "1.0.0", vec![]), + ]); + let mut solver = Solver::new(pool); + let solved = solver.solve(install(&["asdf", "efgh"])).unwrap(); + + for &(solvable_id, _) in &solved.steps { + let solvable = solver.pool().resolve_solvable_inner(solvable_id).display(); + println!("Install {solvable}"); + } + + assert_eq!(solved.steps.len(), 3); + + let solvable = solver + .pool + .resolve_solvable_inner(solved.steps[0].0) + .package(); + assert_eq!(solvable.record.name, "conflicting"); + assert_eq!(solvable.record.version.to_string(), "1.0.0"); + + let solvable = solver + .pool + .resolve_solvable_inner(solved.steps[1].0) + .package(); + assert_eq!(solvable.record.name, "asdf"); + assert_eq!(solvable.record.version.to_string(), "1.2.3"); + + let solvable = solver + .pool + .resolve_solvable_inner(solved.steps[2].0) + .package(); + assert_eq!(solvable.record.name, "efgh"); + assert_eq!(solvable.record.version.to_string(), "4.5.7"); + } + + #[test] + fn test_resolve_with_nonexisting() { + let pool = pool(&[ + ("asdf", "1.2.4", vec!["b"]), + ("asdf", "1.2.3", vec![]), + ("b", "1.2.3", vec!["idontexist"]), + ]); + let mut solver = Solver::new(pool); + let solved = solver.solve(install(&["asdf"])).unwrap(); + + assert_eq!(solved.steps.len(), 1); + + let solvable = solver + .pool + .resolve_solvable_inner(solved.steps[0].0) + .package(); + assert_eq!(solvable.record.name, "asdf"); + assert_eq!(solvable.record.version.to_string(), "1.2.3"); + } + + #[test] + fn test_resolve_locked_top_level() { + let pool = pool(&[("asdf", "1.2.4", vec![]), ("asdf", "1.2.3", vec![])]); + + let locked = pool + .solvables + .iter() + .position(|s| { + if let Some(package) = s.get_package() { + package.record.version == Version::from_str("1.2.3").unwrap() + } else { + false + } + }) + .unwrap(); + + let locked = SolvableId::new(locked); + + let mut solver = Solver::new(pool); + let mut jobs = install(&["asdf"]); + jobs.lock(locked); + + let solved = solver.solve(jobs).unwrap(); + + assert_eq!(solved.steps.len(), 1); + let solvable_id = solved.steps[0].0; + assert_eq!(solvable_id, locked); + } + + #[test] + fn test_resolve_ignored_locked_top_level() { + let pool = pool(&[ + ("asdf", "1.2.4", vec![]), + ("asdf", "1.2.3", vec!["fgh"]), + ("fgh", "1.0.0", vec![]), + ]); + + let locked = pool + .solvables + .iter() + .position(|s| { + if let Some(package) = s.get_package() { + package.record.version == Version::from_str("1.0.0").unwrap() + } else { + false + } + }) + .unwrap(); + + let locked = SolvableId::new(locked); + + let mut solver = Solver::new(pool); + let mut jobs = install(&["asdf"]); + jobs.lock(locked); + + let solved = solver.solve(jobs).unwrap(); + + assert_eq!(solved.steps.len(), 1); + let solvable = solver + .pool + .resolve_solvable_inner(solved.steps[0].0) + .package(); + assert_eq!(solvable.record.name, "asdf"); + assert_eq!(solvable.record.version, Version::from_str("1.2.4").unwrap()); + } + + #[test] + fn test_resolve_favor_without_conflict() { + let pool = pool(&[ + ("A", "1", vec![]), + ("A", "2", vec![]), + ("B", "1", vec![]), + ("B", "2", vec![]), + ]); + + let mut jobs = install(&["A", "B>=2"]); + + // Already installed: A=1; B=1 + let already_installed = pool + .solvables + .iter() + .enumerate() + .skip(1) // Skip the root solvable + .filter(|(_, s)| s.package().record.version == Version::from_str("1").unwrap()) + .map(|(i, _)| SolvableId::new(i)); + + for solvable_id in already_installed { + jobs.favor(solvable_id); + } + + let mut solver = Solver::new(pool); + let solved = solver.solve(jobs).unwrap(); + + let result = transaction_to_string(&solver.pool, &solved); + insta::assert_snapshot!(result, @r###" + B 2 + A 1 + "###); + } + + #[test] + fn test_resolve_favor_with_conflict() { + let pool = pool(&[ + ("A", "1", vec!["C=1"]), + ("A", "2", vec![]), + ("B", "1", vec!["C=1"]), + ("B", "2", vec!["C=2"]), + ("C", "1", vec![]), + ("C", "2", vec![]), + ]); + + let mut jobs = install(&["A", "B>=2"]); + + // Already installed: A=1; B=1; C=1 + let already_installed = pool + .solvables + .iter() + .enumerate() + .skip(1) // Skip the root solvable + .filter(|(_, s)| s.package().record.version == Version::from_str("1").unwrap()) + .map(|(i, _)| SolvableId::new(i)); + + for solvable_id in already_installed { + jobs.favor(solvable_id); + } + + let mut solver = Solver::new(pool); + let solved = solver.solve(jobs).unwrap(); + + let result = transaction_to_string(&solver.pool, &solved); + insta::assert_snapshot!(result, @r###" + B 2 + C 2 + A 2 + "###); + } +} diff --git a/crates/libsolv_rs/src/watch_map.rs b/crates/libsolv_rs/src/watch_map.rs new file mode 100644 index 000000000..813155913 --- /dev/null +++ b/crates/libsolv_rs/src/watch_map.rs @@ -0,0 +1,61 @@ +use crate::rules::Rule; +use crate::solvable::SolvableId; +use crate::solver::RuleId; + +/// A map from solvables to the rules that are watching them +pub(crate) struct WatchMap { + /// Note: the map is to a single rule, but rules form a linked list, so it is possible to go + /// from one to the next + map: Vec, +} + +impl WatchMap { + pub(crate) fn new() -> Self { + Self { map: Vec::new() } + } + + pub(crate) fn initialize(&mut self, nsolvables: usize) { + self.map = vec![RuleId::null(); nsolvables]; + } + + pub(crate) fn start_watching(&mut self, rule: &mut Rule, rule_id: RuleId) { + for (watch_index, watched_solvable) in rule.watched_literals.into_iter().enumerate() { + let already_watching = self.first_rule_watching_solvable(watched_solvable); + rule.link_to_rule(watch_index, already_watching); + self.watch_solvable(watched_solvable, rule_id); + } + } + + pub(crate) fn update_watched( + &mut self, + predecessor_rule: Option<&mut Rule>, + rule: &mut Rule, + rule_id: RuleId, + watch_index: usize, + previous_watch: SolvableId, + new_watch: SolvableId, + ) { + // Remove this rule from its current place in the linked list, because we + // are no longer watching what brought us here + if let Some(predecessor_rule) = predecessor_rule { + // Unlink the rule + predecessor_rule.unlink_rule(rule, previous_watch, watch_index); + } else { + // This was the first rule in the chain + self.map[previous_watch.index()] = rule.get_linked_rule(watch_index); + } + + // Set the new watch + rule.watched_literals[watch_index] = new_watch; + rule.link_to_rule(watch_index, self.map[new_watch.index()]); + self.map[new_watch.index()] = rule_id; + } + + pub(crate) fn first_rule_watching_solvable(&mut self, watched_solvable: SolvableId) -> RuleId { + self.map[watched_solvable.index()] + } + + pub(crate) fn watch_solvable(&mut self, watched_solvable: SolvableId, id: RuleId) { + self.map[watched_solvable.index()] = id; + } +} diff --git a/crates/rattler-bin/src/commands/create.rs b/crates/rattler-bin/src/commands/create.rs index 9442efb85..d0db00ef0 100644 --- a/crates/rattler-bin/src/commands/create.rs +++ b/crates/rattler-bin/src/commands/create.rs @@ -212,6 +212,21 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> { rattler_solve::LibsolvBackend.solve(solver_task) })?; + if opt.dry_run { + println!("The following operations would be performed:"); + let mut sorted = required_packages.clone(); + sorted.sort_by(|a, b| a.package_record.name.cmp(&b.package_record.name)); + for op in sorted { + println!( + "{: <30} {: >20} {: <20}", + op.package_record.name, + format!("{}", op.package_record.version), + op.package_record.build + ); + } + return Ok(()); + } + // Construct a transaction to let transaction = Transaction::from_current_and_desired( installed_packages, diff --git a/crates/rattler_solve/Cargo.toml b/crates/rattler_solve/Cargo.toml index 4b0a71dba..62e78a7be 100644 --- a/crates/rattler_solve/Cargo.toml +++ b/crates/rattler_solve/Cargo.toml @@ -13,8 +13,7 @@ readme.workspace = true [dependencies] rattler_conda_types = { version = "0.4.0", path = "../rattler_conda_types" } rattler_digest = { version = "0.4.0", path = "../rattler_digest" } -libc = "0.2" -libz-sys = { version = "1.1.9", default-features = false, features = ["static"] } +libsolv_rs = { path = "../libsolv_rs" } anyhow = "1.0.71" thiserror = "1.0.40" tracing = "0.1.37" @@ -25,16 +24,10 @@ tempfile = "3.6.0" cfg-if = "1.0.0" [dev-dependencies] +rattler_repodata_gateway = { path = "../rattler_repodata_gateway", features = [ + "sparse", +] } insta = { version = "1.29.0", features = ["yaml"] } rstest = "0.17.0" serde_json = "1.0.96" url = "2.4.0" - -[build-dependencies] -anyhow = "1.0.71" -cc = "1.0.79" -cmake = "0.1.50" - -[package.metadata.cargo-udeps.ignore] -# libz-sys is required to build libsolv properly -normal = ["libz-sys"] diff --git a/crates/rattler_solve/build.rs b/crates/rattler_solve/build.rs deleted file mode 100644 index 9378f962e..000000000 --- a/crates/rattler_solve/build.rs +++ /dev/null @@ -1,47 +0,0 @@ -use anyhow::{anyhow, Result}; -use std::path::{self, PathBuf}; - -fn build_libsolv() -> Result { - let target = std::env::var("TARGET").unwrap(); - let is_windows = target.contains("windows"); - - let p = path::PathBuf::from("./libsolv/CMakeLists.txt"); - if !p.is_file() { - return Err(anyhow!( - "Bundled libsolv not found, please do `git submodule update --init`." - )); - } - let out = cmake::Config::new(p.parent().unwrap()) - .define("ENABLE_CONDA", "ON") - .define("ENABLE_STATIC", "ON") - .define("DISABLE_SHARED", "ON") - .define("MULTI_SEMANTICS", "ON") - .define("WITHOUT_COOKIEOPEN", "ON") - .register_dep("z") - .target(&std::env::var("CMAKE_TARGET").unwrap_or_else(|_| std::env::var("TARGET").unwrap())) - .build(); - println!( - "cargo:rustc-link-search=native={}", - out.join("lib").display() - ); - println!( - "cargo:rustc-link-search=native={}", - out.join("lib64").display() - ); - - if is_windows { - println!("cargo:rustc-link-lib=static=solv_static"); - println!("cargo:rustc-link-lib=static=solvext_static"); - } else { - println!("cargo:rustc-link-lib=static=solv"); - println!("cargo:rustc-link-lib=static=solvext"); - } - - Ok(out.join("include/solv")) -} - -fn main() -> Result<()> { - build_libsolv()?; - - Ok(()) -} diff --git a/crates/rattler_solve/src/lib.rs b/crates/rattler_solve/src/lib.rs index 13bde9aee..28f1860bd 100644 --- a/crates/rattler_solve/src/lib.rs +++ b/crates/rattler_solve/src/lib.rs @@ -6,9 +6,7 @@ mod libsolv; mod solver_backend; -pub use libsolv::{ - cache_repodata as cache_libsolv_repodata, LibcByteSlice, LibsolvBackend, LibsolvRepoData, -}; +pub use libsolv::{LibsolvBackend, LibsolvRepoData}; pub use solver_backend::SolverBackend; use std::fmt; @@ -85,6 +83,7 @@ mod test_libsolv { }; use std::str::FromStr; use url::Url; + use rattler_repodata_gateway::sparse::SparseRepoData; fn conda_json_path() -> String { format!( @@ -132,6 +131,10 @@ mod test_libsolv { ) } + fn read_sparse_repodata(path: &str) -> SparseRepoData { + SparseRepoData::new(Channel::from_str("dummy", &ChannelConfig::default()).unwrap(), "dummy".to_string(), path).unwrap() + } + fn installed_package( channel: &str, subdir: &str, @@ -169,17 +172,19 @@ mod test_libsolv { } } - #[test] - fn test_solve_python() { + fn solve_real_world(specs: Vec<&str>) -> Vec { + let specs = specs.iter().map(|s| MatchSpec::from_str(s).unwrap()).collect::>(); + let json_file = conda_json_path(); let json_file_noarch = conda_json_path_noarch(); - let repo_data = read_repodata(&json_file); - let repo_data_noarch = read_repodata(&json_file_noarch); - - let available_packages = vec![repo_data, repo_data_noarch]; + let sparse_repo_datas = vec![ + read_sparse_repodata(&json_file), + read_sparse_repodata(&json_file_noarch) + ]; - let specs = vec![MatchSpec::from_str("python=3.9").unwrap()]; + let names = specs.iter().map(|s| s.name.clone().unwrap()); + let available_packages = SparseRepoData::load_records_recursive(&sparse_repo_datas, names).unwrap(); let solver_task = SolverTask { available_packages: available_packages @@ -206,7 +211,18 @@ mod test_libsolv { // The order of packages is nondeterministic, so we sort them to ensure we can compare them // to a previous run pkgs.sort(); + pkgs + } + #[test] + fn test_solve_tensorboard() { + let pkgs = solve_real_world(vec!["tensorboard=2.1.1", "grpc-cpp=1.39.1"]); + insta::assert_yaml_snapshot!(pkgs); + } + + #[test] + fn test_solve_python() { + let pkgs = solve_real_world(vec!["python=3.9"]); insta::assert_yaml_snapshot!(pkgs); } diff --git a/crates/rattler_solve/src/libsolv/input.rs b/crates/rattler_solve/src/libsolv/input.rs index 247891bed..33b8c5737 100644 --- a/crates/rattler_solve/src/libsolv/input.rs +++ b/crates/rattler_solve/src/libsolv/input.rs @@ -1,192 +1,54 @@ //! Contains business logic that loads information into libsolv in order to solve a conda //! environment -use crate::libsolv::c_string; -use crate::libsolv::libc_byte_slice::LibcByteSlice; -use crate::libsolv::wrapper::keys::*; -use crate::libsolv::wrapper::pool::Pool; -use crate::libsolv::wrapper::repo::Repo; -use crate::libsolv::wrapper::repodata::Repodata; -use crate::libsolv::wrapper::solvable::SolvableId; +use libsolv_rs::pool::{Pool, RepoId}; +use libsolv_rs::solvable::SolvableId; use rattler_conda_types::package::ArchiveType; -use rattler_conda_types::{GenericVirtualPackage, RepoDataRecord}; +use rattler_conda_types::{GenericVirtualPackage, PackageRecord, RepoDataRecord}; use std::cmp::Ordering; use std::collections::HashMap; -#[cfg(not(target_family = "unix"))] -/// Adds solvables to a repo from an in-memory .solv file -/// -/// Note: this function relies on primitives that are only available on unix-like operating systems, -/// and will panic if called from another platform (e.g. Windows) -pub fn add_solv_file(_pool: &Pool, _repo: &Repo, _solv_bytes: &LibcByteSlice) { - unimplemented!("this platform does not support in-memory .solv files"); -} - -#[cfg(target_family = "unix")] -/// Adds solvables to a repo from an in-memory .solv file -/// -/// Note: this function relies on primitives that are only available on unix-like operating systems, -/// and will panic if called from another platform (e.g. Windows) -pub fn add_solv_file(pool: &Pool, repo: &Repo, solv_bytes: &LibcByteSlice) { - // Add solv file from memory if available - let mode = c_string("r"); - let file = unsafe { libc::fmemopen(solv_bytes.as_ptr(), solv_bytes.len(), mode.as_ptr()) }; - repo.add_solv(pool, file); - unsafe { libc::fclose(file) }; -} - /// Adds [`RepoDataRecord`] to `repo` /// /// Panics if the repo does not belong to the pool pub fn add_repodata_records( - pool: &Pool, - repo: &Repo, + pool: &mut Pool, + repo_id: RepoId, repo_datas: &[RepoDataRecord], ) -> Vec { - // Sanity check - repo.ensure_belongs_to_pool(pool); - - // Get all the IDs (these strings are internal to libsolv and always present, so we can - // unwrap them at will) - let solvable_buildflavor_id = pool.find_interned_str(SOLVABLE_BUILDFLAVOR).unwrap(); - let solvable_buildtime_id = pool.find_interned_str(SOLVABLE_BUILDTIME).unwrap(); - let solvable_buildversion_id = pool.find_interned_str(SOLVABLE_BUILDVERSION).unwrap(); - let solvable_constraints = pool.find_interned_str(SOLVABLE_CONSTRAINS).unwrap(); - let solvable_download_size_id = pool.find_interned_str(SOLVABLE_DOWNLOADSIZE).unwrap(); - let solvable_license_id = pool.find_interned_str(SOLVABLE_LICENSE).unwrap(); - let solvable_pkg_id = pool.find_interned_str(SOLVABLE_PKGID).unwrap(); - let solvable_checksum = pool.find_interned_str(SOLVABLE_CHECKSUM).unwrap(); - let solvable_track_features = pool.find_interned_str(SOLVABLE_TRACK_FEATURES).unwrap(); - let repo_type_md5 = pool.find_interned_str(REPOKEY_TYPE_MD5).unwrap(); - let repo_type_sha256 = pool.find_interned_str(REPOKEY_TYPE_SHA256).unwrap(); - - // Custom id - let solvable_index_id = pool.intern_str("solvable:repodata_record_index"); - // Keeps a mapping from packages added to the repo to the type and solvable let mut package_to_type: HashMap<&str, (ArchiveType, SolvableId)> = HashMap::new(); - // Through `data` we can manipulate solvables (see the `Repodata` docs for details) - let data = repo.add_repodata(); - let mut solvable_ids = Vec::new(); for (repo_data_index, repo_data) in repo_datas.iter().enumerate() { // Create a solvable for the package let solvable_id = - match add_or_reuse_solvable(pool, repo, &data, &mut package_to_type, repo_data) { + match add_or_reuse_solvable(pool, repo_id, &mut package_to_type, repo_data) { Some(id) => id, None => continue, }; // Store the current index so we can retrieve the original repo data record // from the final transaction - data.set_num(solvable_id, solvable_index_id, repo_data_index as u64); + pool.resolve_solvable_mut(solvable_id) + .metadata + .original_index = Some(repo_data_index); - // Safe because there are no other active references to any solvable (so no aliasing) - let solvable = unsafe { solvable_id.resolve_raw(pool).as_mut() }; let record = &repo_data.package_record; - // Name and version - solvable.name = pool.intern_str(record.name.as_str()).into(); - solvable.evr = pool.intern_str(record.version.to_string()).into(); - let rel_eq = pool.rel_eq(solvable.name, solvable.evr); - repo.add_provides(solvable, rel_eq); - - // Location (filename (fn) and subdir) - data.set_location( - solvable_id, - &c_string(&record.subdir), - &c_string(&repo_data.file_name), - ); - // Dependencies for match_spec in record.depends.iter() { - // Create a reldep id from a matchspec - let match_spec_id = pool.conda_matchspec(&c_string(match_spec)); - - // Add it to the list of requirements of this solvable - repo.add_requires(solvable, match_spec_id); + pool.add_dependency(solvable_id, match_spec.to_string()); } - // Constraints + // Constrains for match_spec in record.constrains.iter() { - // Create a reldep id from a matchspec - let match_spec_id = pool.conda_matchspec(&c_string(match_spec)); - - // Add it to the list of constraints of this solvable - data.add_idarray(solvable_id, solvable_constraints, match_spec_id); - } - - // Track features - for track_features in record.track_features.iter() { - let track_feature = track_features.trim(); - if !track_feature.is_empty() { - data.add_idarray( - solvable_id, - solvable_track_features, - pool.intern_str(track_features.trim()).into(), - ); - } - } - - // Timestamp - if let Some(timestamp) = record.timestamp { - data.set_num( - solvable_id, - solvable_buildtime_id, - timestamp.timestamp() as u64, - ); - } - - // Size - if let Some(size) = record.size { - data.set_num(solvable_id, solvable_download_size_id, size); - } - - // Build string - data.add_poolstr_array( - solvable_id, - solvable_buildflavor_id, - &c_string(&record.build), - ); - - // Build number - data.set_str( - solvable_id, - solvable_buildversion_id, - &c_string(record.build_number.to_string()), - ); - - // License - if let Some(license) = record.license.as_ref() { - data.add_poolstr_array(solvable_id, solvable_license_id, &c_string(license)); - } - - // MD5 hash - if let Some(md5) = record.md5.as_ref() { - data.set_checksum( - solvable_id, - solvable_pkg_id, - repo_type_md5, - &c_string(format!("{:x}", md5)), - ); - } - - // Sha256 hash - if let Some(sha256) = record.sha256.as_ref() { - data.set_checksum( - solvable_id, - solvable_checksum, - repo_type_sha256, - &c_string(format!("{:x}", sha256)), - ); + pool.add_constrains(solvable_id, match_spec.to_string()); } solvable_ids.push(solvable_id) } - repo.internalize(); - solvable_ids } @@ -196,14 +58,15 @@ pub fn add_repodata_records( /// `None`). If no `.conda` version has been added, we create a new solvable (replacing any existing /// solvable for the `.tar.bz` version of the package). fn add_or_reuse_solvable<'a>( - pool: &Pool, - repo: &Repo, - data: &Repodata, + pool: &mut Pool, + repo_id: RepoId, package_to_type: &mut HashMap<&'a str, (ArchiveType, SolvableId)>, repo_data: &'a RepoDataRecord, ) -> Option { // Sometimes we can reuse an existing solvable if let Some((filename, archive_type)) = ArchiveType::split_str(&repo_data.file_name) { + let record_ptr = &repo_data.package_record as *const _; + if let Some(&(other_package_type, old_solvable_id)) = package_to_type.get(filename) { match archive_type.cmp(&other_package_type) { Ordering::Less => { @@ -219,7 +82,7 @@ fn add_or_reuse_solvable<'a>( package_to_type.insert(filename, (archive_type, old_solvable_id)); // Reset and reuse the old solvable - reset_solvable(pool, repo, data, old_solvable_id); + pool.reset_package(repo_id, old_solvable_id, unsafe { &*record_ptr }); return Some(old_solvable_id); } Ordering::Equal => { @@ -227,7 +90,7 @@ fn add_or_reuse_solvable<'a>( } } } else { - let solvable_id = repo.add_solvable(); + let solvable_id = pool.add_package(repo_id, unsafe { &*record_ptr }); package_to_type.insert(filename, (archive_type, solvable_id)); return Some(solvable_id); } @@ -235,81 +98,40 @@ fn add_or_reuse_solvable<'a>( tracing::warn!("unknown package extension: {}", &repo_data.file_name); } - Some(repo.add_solvable()) + let record_ptr = &repo_data.package_record as *const _; + let solvable_id = pool.add_package(repo_id, unsafe { &*record_ptr }); + Some(solvable_id) } -pub fn add_virtual_packages(pool: &Pool, repo: &Repo, packages: &[GenericVirtualPackage]) { - let data = repo.add_repodata(); - - let solvable_buildflavor_id = pool.find_interned_str(SOLVABLE_BUILDFLAVOR).unwrap(); +pub fn add_virtual_packages(pool: &mut Pool, repo_id: RepoId, packages: &[GenericVirtualPackage]) { + let packages: &'static _ = packages + .iter() + .map(|p| PackageRecord { + arch: None, + name: p.name.clone(), + noarch: Default::default(), + platform: None, + sha256: None, + size: None, + subdir: "".to_string(), + timestamp: None, + build_number: 0, + version: p.version.clone(), + build: p.build_string.clone(), + depends: Vec::new(), + features: None, + legacy_bz2_md5: None, + legacy_bz2_size: None, + license: None, + license_family: None, + constrains: vec![], + md5: None, + track_features: vec![], + }) + .collect::>() + .leak(); for package in packages { - // Create a solvable for the package - let solvable_id = repo.add_solvable(); - - // Safe because there are no other references to this solvable_id (we just created it) - let solvable = unsafe { solvable_id.resolve_raw(pool).as_mut() }; - - // Name and version - solvable.name = pool.intern_str(package.name.as_str()).into(); - solvable.evr = pool.intern_str(package.version.to_string()).into(); - let rel_eq = pool.rel_eq(solvable.name, solvable.evr); - repo.add_provides(solvable, rel_eq); - - // Build string - data.add_poolstr_array( - solvable_id, - solvable_buildflavor_id, - &c_string(&package.build_string), - ); + pool.add_package(repo_id, &package); } } - -fn reset_solvable(pool: &Pool, repo: &Repo, data: &Repodata, solvable_id: SolvableId) { - let blank_solvable = repo.add_solvable(); - - // Replace the existing solvable with the blank one - pool.swap_solvables(blank_solvable, solvable_id); - data.swap_attrs(blank_solvable, solvable_id); - - // It is safe to free the blank solvable, because there are no other references to it - // than in this function - unsafe { repo.free_solvable(blank_solvable) }; -} - -/// Caches the repodata as an in-memory `.solv` file -/// -/// Note: this function relies on primitives that are only available on unix-like operating systems, -/// and will panic if called from another platform (e.g. Windows) -#[cfg(not(target_family = "unix"))] -pub fn cache_repodata(_url: String, _data: &[RepoDataRecord]) -> LibcByteSlice { - unimplemented!("this function is only available on unix-like operating systems") -} - -/// Caches the repodata as an in-memory `.solv` file -/// -/// Note: this function relies on primitives that are only available on unix-like operating systems, -/// and will panic if called from another platform (e.g. Windows) -#[cfg(target_family = "unix")] -pub fn cache_repodata(url: String, data: &[RepoDataRecord]) -> LibcByteSlice { - // Add repodata to a new pool + repo - let pool = Pool::default(); - let repo = Repo::new(&pool, url); - add_repodata_records(&pool, &repo, data); - - // Export repo to .solv in memory - let mut stream_ptr = std::ptr::null_mut(); - let mut stream_size = 0; - let file = unsafe { libc::open_memstream(&mut stream_ptr, &mut stream_size) }; - if file.is_null() { - panic!("unable to open memstream"); - } - - repo.write(&pool, file); - unsafe { libc::fclose(file) }; - - let stream_ptr = std::ptr::NonNull::new(stream_ptr).expect("stream_ptr was null"); - - // Safe because we know `stream_ptr` points to an array of bytes of length `stream_size` - unsafe { LibcByteSlice::from_raw_parts(stream_ptr.cast(), stream_size) } -} diff --git a/crates/rattler_solve/src/libsolv/libc_byte_slice.rs b/crates/rattler_solve/src/libsolv/libc_byte_slice.rs deleted file mode 100644 index e4c9a8b38..000000000 --- a/crates/rattler_solve/src/libsolv/libc_byte_slice.rs +++ /dev/null @@ -1,42 +0,0 @@ -#![cfg_attr(not(target_family = "unix"), allow(dead_code))] - -use std::ptr::NonNull; - -/// Represents an owned byte slice that was allocated using [`libc::malloc`] and is deallocated upon -/// drop using [`libc::free`]. -pub struct LibcByteSlice { - ptr: NonNull, - len: usize, -} - -// We can safely implemenet `Send` because LibcByteSlice is immutable -unsafe impl Send for LibcByteSlice {} - -// We can safely implemenet `Send` because LibcByteSlice is immutable -unsafe impl Sync for LibcByteSlice {} - -impl LibcByteSlice { - /// Constructs a `LibcByteSlice` from its raw parts - /// - /// # Safety - /// - /// `ptr` should have been allocated using [`libc::malloc`] and `len` should be the size - /// in bytes of the allocated chunk of memory - pub unsafe fn from_raw_parts(ptr: NonNull, len: usize) -> LibcByteSlice { - LibcByteSlice { ptr, len } - } - - pub(super) fn as_ptr(&self) -> *mut libc::c_void { - self.ptr.as_ptr() - } - - pub(super) fn len(&self) -> usize { - self.len - } -} - -impl Drop for LibcByteSlice { - fn drop(&mut self) { - unsafe { libc::free(self.ptr.as_ptr() as *mut _) } - } -} diff --git a/crates/rattler_solve/src/libsolv/mod.rs b/crates/rattler_solve/src/libsolv/mod.rs index dbb64e89e..966d456e7 100644 --- a/crates/rattler_solve/src/libsolv/mod.rs +++ b/crates/rattler_solve/src/libsolv/mod.rs @@ -1,22 +1,14 @@ use crate::{SolveError, SolverBackend, SolverTask}; -pub use input::cache_repodata; -use input::{add_repodata_records, add_solv_file, add_virtual_packages}; -pub use libc_byte_slice::LibcByteSlice; +use input::{add_repodata_records, add_virtual_packages}; +use libsolv_rs::pool::Pool; +use libsolv_rs::solve_jobs::SolveJobs; +use libsolv_rs::solver::Solver; use output::get_required_packages; use rattler_conda_types::RepoDataRecord; use std::collections::HashMap; -use std::ffi::CString; -use wrapper::{ - flags::SolverFlag, - pool::{Pool, Verbosity}, - repo::Repo, - solve_goal::SolveGoal, -}; mod input; -mod libc_byte_slice; mod output; -mod wrapper; /// Represents the information required to load available packages into libsolv for a single channel /// and platform combination @@ -24,40 +16,13 @@ mod wrapper; pub struct LibsolvRepoData<'a> { /// The actual records after parsing `repodata.json` pub records: &'a [RepoDataRecord], - - /// The in-memory .solv file built from the records (if available) - pub solv_file: Option<&'a LibcByteSlice>, } impl LibsolvRepoData<'_> { /// Constructs a new `LibsolvRepoData` without a corresponding .solv file pub fn from_records(records: &[RepoDataRecord]) -> LibsolvRepoData { - LibsolvRepoData { - records, - solv_file: None, - } - } -} - -/// Convenience method that converts a string reference to a CString, replacing NUL characters with -/// whitespace (` `) -fn c_string>(str: T) -> CString { - let bytes = str.as_ref().as_bytes(); - - let mut vec = Vec::with_capacity(bytes.len() + 1); - vec.extend_from_slice(bytes); - - for byte in &mut vec { - if *byte == 0 { - *byte = b' '; - } + LibsolvRepoData { records } } - - // Trailing 0 - vec.push(0); - - // Safe because the string does is guaranteed to have no NUL bytes other than the trailing one - unsafe { CString::from_vec_with_nul_unchecked(vec) } } /// A [`SolverBackend`] implemented using the `libsolv` library @@ -71,20 +36,11 @@ impl SolverBackend for LibsolvBackend { task: SolverTask, ) -> Result, SolveError> { // Construct a default libsolv pool - let pool = Pool::default(); - - // Setup proper logging for the pool - pool.set_debug_callback(|msg, _flags| { - tracing::event!(tracing::Level::DEBUG, "{}", msg.trim_end()); - }); - pool.set_debug_level(Verbosity::Low); + let mut pool = Pool::new(); // Add virtual packages - let repo = Repo::new(&pool, "virtual_packages"); - add_virtual_packages(&pool, &repo, &task.virtual_packages); - - // Mark the virtual packages as installed. - pool.set_installed(&repo); + let repo_id = pool.new_repo("virtual_packages"); + add_virtual_packages(&mut pool, repo_id, &task.virtual_packages); // Create repos for all channel + platform combinations let mut repo_mapping = HashMap::new(); @@ -95,43 +51,32 @@ impl SolverBackend for LibsolvBackend { } let channel_name = &repodata.records[0].channel; - let repo = Repo::new(&pool, channel_name); - - if let Some(solv_file) = repodata.solv_file { - add_solv_file(&pool, &repo, solv_file); - } else { - add_repodata_records(&pool, &repo, repodata.records); - } + let repo_id = pool.new_repo(channel_name); + add_repodata_records(&mut pool, repo_id, repodata.records); // Keep our own info about repodata_records - repo_mapping.insert(repo.id(), repo_mapping.len()); + repo_mapping.insert(repo_id, repo_mapping.len()); all_repodata_records.push(repodata.records); - - // We dont want to drop the Repo, its stored in the pool anyway, so just forget it. - std::mem::forget(repo); } // Create a special pool for records that are already installed or locked. - let repo = Repo::new(&pool, "locked"); - let installed_solvables = add_repodata_records(&pool, &repo, &task.locked_packages); + let repo_id = pool.new_repo("locked"); + let installed_solvables = add_repodata_records(&mut pool, repo_id, &task.locked_packages); // Also add the installed records to the repodata - repo_mapping.insert(repo.id(), repo_mapping.len()); + repo_mapping.insert(repo_id, repo_mapping.len()); all_repodata_records.push(&task.locked_packages); // Create a special pool for records that are pinned and cannot be changed. - let repo = Repo::new(&pool, "pinned"); - let pinned_solvables = add_repodata_records(&pool, &repo, &task.pinned_packages); + let repo_id = pool.new_repo("pinned"); + let pinned_solvables = add_repodata_records(&mut pool, repo_id, &task.pinned_packages); // Also add the installed records to the repodata - repo_mapping.insert(repo.id(), repo_mapping.len()); + repo_mapping.insert(repo_id, repo_mapping.len()); all_repodata_records.push(&task.pinned_packages); - // Create datastructures for solving - pool.create_whatprovides(); - // Add matchspec to the queue - let mut goal = SolveGoal::default(); + let mut goal = SolveJobs::default(); // Favor the currently installed packages for favor_solvable in installed_solvables { @@ -145,19 +90,15 @@ impl SolverBackend for LibsolvBackend { // Specify the matchspec requests for spec in task.specs { - let id = pool.intern_matchspec(&spec); - goal.install(id, false) + goal.install(spec); } // Construct a solver and solve the problems in the queue - let mut solver = pool.create_solver(); - solver.set_flag(SolverFlag::allow_uninstall(), true); - solver.set_flag(SolverFlag::allow_downgrade(), true); - - let transaction = solver.solve(&mut goal).map_err(SolveError::Unsolvable)?; + let mut solver = Solver::new(pool); + let transaction = solver.solve(goal).map_err(SolveError::Unsolvable)?; let required_records = get_required_packages( - &pool, + solver.pool(), &repo_mapping, &transaction, all_repodata_records.as_slice(), @@ -174,19 +115,3 @@ impl SolverBackend for LibsolvBackend { Ok(required_records) } } - -#[cfg(test)] -mod test { - use crate::libsolv::c_string; - use rstest::rstest; - - #[rstest] - #[case("", "")] - #[case("a\0b\0c\0d\0", "a b c d ")] - #[case("a b c d", "a b c d")] - #[case("😒", "😒")] - fn test_c_string(#[case] input: &str, #[case] expected_output: &str) { - let output = c_string(input); - assert_eq!(output.as_bytes(), expected_output.as_bytes()); - } -} diff --git a/crates/rattler_solve/src/libsolv/output.rs b/crates/rattler_solve/src/libsolv/output.rs index 16113e85b..074b704ab 100644 --- a/crates/rattler_solve/src/libsolv/output.rs +++ b/crates/rattler_solve/src/libsolv/output.rs @@ -1,11 +1,9 @@ //! Contains business logic to retrieve the results from libsolv after attempting to resolve a conda //! environment -use crate::libsolv::wrapper::pool::{Pool, StringId}; -use crate::libsolv::wrapper::repo::RepoId; -use crate::libsolv::wrapper::solvable::SolvableId; -use crate::libsolv::wrapper::transaction::Transaction; -use crate::libsolv::wrapper::{ffi, solvable}; +use libsolv_rs::pool::{Pool, RepoId}; +use libsolv_rs::solvable::SolvableId; +use libsolv_rs::solver::{Transaction, TransactionKind}; use rattler_conda_types::RepoDataRecord; use std::collections::HashMap; @@ -18,31 +16,24 @@ pub fn get_required_packages( repo_mapping: &HashMap, transaction: &Transaction, repodata_records: &[&[RepoDataRecord]], -) -> Result, Vec> { +) -> Result, Vec> { let mut required_packages = Vec::new(); let mut unsupported_operations = Vec::new(); - let solvable_index_id = pool - .find_interned_str("solvable:repodata_record_index") - .unwrap(); - - // Safe because `transaction.steps` is an active queue - let transaction_queue = transaction.get_steps(); - - for id in transaction_queue.iter() { - let transaction_type = transaction.transaction_type(id); - + for &(id, kind) in &transaction.steps { // Retrieve the repodata record corresponding to this solvable - let (repo_index, solvable_index) = - get_solvable_indexes(pool, repo_mapping, solvable_index_id, id); - let repodata_record = &repodata_records[repo_index][solvable_index]; - - match transaction_type as u32 { - ffi::SOLVER_TRANSACTION_INSTALL => { - required_packages.push(repodata_record.clone()); - } - _ => { - unsupported_operations.push(transaction_type); + // + // Note that packages without indexes are virtual and can be ignored + if let Some((repo_index, solvable_index)) = get_solvable_indexes(pool, repo_mapping, id) { + let repodata_record = &repodata_records[repo_index][solvable_index]; + + match kind { + TransactionKind::Install => { + required_packages.push(repodata_record.clone()); + } + _ => { + unsupported_operations.push(kind); + } } } } @@ -57,17 +48,13 @@ pub fn get_required_packages( fn get_solvable_indexes( pool: &Pool, repo_mapping: &HashMap, - solvable_index_id: StringId, id: SolvableId, -) -> (usize, usize) { - let solvable = id.resolve_raw(pool); - let solvable_index = - solvable::lookup_num(solvable.as_ptr(), solvable_index_id).unwrap() as usize; - - // Safe because there are no active mutable borrows of any solvable at this stage - let repo_id = RepoId::from_ffi_solvable(unsafe { solvable.as_ref() }); +) -> Option<(usize, usize)> { + let solvable = pool.resolve_solvable(id); + let solvable_index = solvable.metadata.original_index?; + let repo_id = solvable.repo_id(); let repo_index = repo_mapping[&repo_id]; - (repo_index, solvable_index) + Some((repo_index, solvable_index)) } diff --git a/crates/rattler_solve/src/libsolv/wrapper/ffi.rs b/crates/rattler_solve/src/libsolv/wrapper/ffi.rs deleted file mode 100644 index 5f9ae5ed7..000000000 --- a/crates/rattler_solve/src/libsolv/wrapper/ffi.rs +++ /dev/null @@ -1,2754 +0,0 @@ -//! Generated file, do not edit by hand, see `crate/tools/src` - -#![allow( - non_upper_case_globals, - non_camel_case_types, - non_snake_case, - dead_code, - clippy::upper_case_acronyms -)] - -pub use libc::FILE; - -pub const SOLV_VERSION_0: u32 = 0; -pub const SOLV_VERSION_1: u32 = 1; -pub const SOLV_VERSION_2: u32 = 2; -pub const SOLV_VERSION_3: u32 = 3; -pub const SOLV_VERSION_4: u32 = 4; -pub const SOLV_VERSION_5: u32 = 5; -pub const SOLV_VERSION_6: u32 = 6; -pub const SOLV_VERSION_7: u32 = 7; -pub const SOLV_VERSION_8: u32 = 8; -pub const SOLV_VERSION_9: u32 = 9; -pub const SOLV_FLAG_PREFIX_POOL: u32 = 4; -pub const SOLV_FLAG_SIZE_BYTES: u32 = 8; -pub const SOLV_FLAG_USERDATA: u32 = 16; -pub const SOLV_FLAG_IDARRAYBLOCK: u32 = 32; -pub const DISTTYPE_RPM: u32 = 0; -pub const DISTTYPE_DEB: u32 = 1; -pub const DISTTYPE_ARCH: u32 = 2; -pub const DISTTYPE_HAIKU: u32 = 3; -pub const DISTTYPE_CONDA: u32 = 4; -pub const SOLV_FATAL: u32 = 1; -pub const SOLV_ERROR: u32 = 2; -pub const SOLV_WARN: u32 = 4; -pub const SOLV_DEBUG_STATS: u32 = 8; -pub const SOLV_DEBUG_RULE_CREATION: u32 = 16; -pub const SOLV_DEBUG_PROPAGATE: u32 = 32; -pub const SOLV_DEBUG_ANALYZE: u32 = 64; -pub const SOLV_DEBUG_UNSOLVABLE: u32 = 128; -pub const SOLV_DEBUG_SOLUTIONS: u32 = 256; -pub const SOLV_DEBUG_POLICY: u32 = 512; -pub const SOLV_DEBUG_RESULT: u32 = 1024; -pub const SOLV_DEBUG_JOB: u32 = 2048; -pub const SOLV_DEBUG_SOLVER: u32 = 4096; -pub const SOLV_DEBUG_TRANSACTION: u32 = 8192; -pub const SOLV_DEBUG_WATCHES: u32 = 16384; -pub const SOLV_DEBUG_TO_STDERR: u32 = 1073741824; -pub const POOL_FLAG_PROMOTEEPOCH: u32 = 1; -pub const POOL_FLAG_FORBIDSELFCONFLICTS: u32 = 2; -pub const POOL_FLAG_OBSOLETEUSESPROVIDES: u32 = 3; -pub const POOL_FLAG_IMPLICITOBSOLETEUSESPROVIDES: u32 = 4; -pub const POOL_FLAG_OBSOLETEUSESCOLORS: u32 = 5; -pub const POOL_FLAG_NOINSTALLEDOBSOLETES: u32 = 6; -pub const POOL_FLAG_HAVEDISTEPOCH: u32 = 7; -pub const POOL_FLAG_NOOBSOLETESMULTIVERSION: u32 = 8; -pub const POOL_FLAG_ADDFILEPROVIDESFILTERED: u32 = 9; -pub const POOL_FLAG_IMPLICITOBSOLETEUSESCOLORS: u32 = 10; -pub const POOL_FLAG_NOWHATPROVIDESAUX: u32 = 11; -pub const POOL_FLAG_WHATPROVIDESWITHDISABLED: u32 = 12; -pub const REL_GT: u32 = 1; -pub const REL_EQ: u32 = 2; -pub const REL_LT: u32 = 4; -pub const REL_AND: u32 = 16; -pub const REL_OR: u32 = 17; -pub const REL_WITH: u32 = 18; -pub const REL_NAMESPACE: u32 = 19; -pub const REL_ARCH: u32 = 20; -pub const REL_FILECONFLICT: u32 = 21; -pub const REL_COND: u32 = 22; -pub const REL_COMPAT: u32 = 23; -pub const REL_KIND: u32 = 24; -pub const REL_MULTIARCH: u32 = 25; -pub const REL_ELSE: u32 = 26; -pub const REL_ERROR: u32 = 27; -pub const REL_WITHOUT: u32 = 28; -pub const REL_UNLESS: u32 = 29; -pub const REL_CONDA: u32 = 30; -pub const SEARCH_STRINGMASK: u32 = 15; -pub const SEARCH_STRING: u32 = 1; -pub const SEARCH_STRINGSTART: u32 = 2; -pub const SEARCH_STRINGEND: u32 = 3; -pub const SEARCH_SUBSTRING: u32 = 4; -pub const SEARCH_GLOB: u32 = 5; -pub const SEARCH_REGEX: u32 = 6; -pub const SEARCH_ERROR: u32 = 15; -pub const SEARCH_NOCASE: u32 = 128; -pub const SEARCH_NO_STORAGE_SOLVABLE: u32 = 256; -pub const SEARCH_SUB: u32 = 512; -pub const SEARCH_ARRAYSENTINEL: u32 = 1024; -pub const SEARCH_DISABLED_REPOS: u32 = 2048; -pub const SEARCH_KEEP_TYPE_DELETED: u32 = 4096; -pub const SEARCH_SKIP_KIND: u32 = 65536; -pub const SEARCH_FILES: u32 = 131072; -pub const SEARCH_CHECKSUMS: u32 = 262144; -pub const SEARCH_SUBSCHEMA: u32 = 1073741824; -pub const SEARCH_THISSOLVID: u32 = 2147483648; -pub const SEARCH_COMPLETE_FILELIST: u32 = 0; -pub const SEARCH_NEXT_KEY: u32 = 1; -pub const SEARCH_NEXT_SOLVABLE: u32 = 2; -pub const SEARCH_STOP: u32 = 3; -pub const SEARCH_ENTERSUB: i32 = -1; -pub const SOLVER_TRANSACTION_IGNORE: u32 = 0; -pub const SOLVER_TRANSACTION_ERASE: u32 = 16; -pub const SOLVER_TRANSACTION_REINSTALLED: u32 = 17; -pub const SOLVER_TRANSACTION_DOWNGRADED: u32 = 18; -pub const SOLVER_TRANSACTION_CHANGED: u32 = 19; -pub const SOLVER_TRANSACTION_UPGRADED: u32 = 20; -pub const SOLVER_TRANSACTION_OBSOLETED: u32 = 21; -pub const SOLVER_TRANSACTION_INSTALL: u32 = 32; -pub const SOLVER_TRANSACTION_REINSTALL: u32 = 33; -pub const SOLVER_TRANSACTION_DOWNGRADE: u32 = 34; -pub const SOLVER_TRANSACTION_CHANGE: u32 = 35; -pub const SOLVER_TRANSACTION_UPGRADE: u32 = 36; -pub const SOLVER_TRANSACTION_OBSOLETES: u32 = 37; -pub const SOLVER_TRANSACTION_MULTIINSTALL: u32 = 48; -pub const SOLVER_TRANSACTION_MULTIREINSTALL: u32 = 49; -pub const SOLVER_TRANSACTION_MAXTYPE: u32 = 63; -pub const SOLVER_TRANSACTION_SHOW_ACTIVE: u32 = 1; -pub const SOLVER_TRANSACTION_SHOW_ALL: u32 = 2; -pub const SOLVER_TRANSACTION_SHOW_OBSOLETES: u32 = 4; -pub const SOLVER_TRANSACTION_SHOW_MULTIINSTALL: u32 = 8; -pub const SOLVER_TRANSACTION_CHANGE_IS_REINSTALL: u32 = 16; -pub const SOLVER_TRANSACTION_MERGE_VENDORCHANGES: u32 = 32; -pub const SOLVER_TRANSACTION_MERGE_ARCHCHANGES: u32 = 64; -pub const SOLVER_TRANSACTION_RPM_ONLY: u32 = 128; -pub const SOLVER_TRANSACTION_KEEP_PSEUDO: u32 = 256; -pub const SOLVER_TRANSACTION_OBSOLETE_IS_UPGRADE: u32 = 512; -pub const SOLVER_TRANSACTION_ARCHCHANGE: u32 = 256; -pub const SOLVER_TRANSACTION_VENDORCHANGE: u32 = 257; -pub const SOLVER_TRANSACTION_KEEP_ORDERDATA: u32 = 1; -pub const SOLVER_TRANSACTION_KEEP_ORDERCYCLES: u32 = 2; -pub const SOLVER_TRANSACTION_KEEP_ORDEREDGES: u32 = 4; -pub const SOLVER_ORDERCYCLE_HARMLESS: u32 = 0; -pub const SOLVER_ORDERCYCLE_NORMAL: u32 = 1; -pub const SOLVER_ORDERCYCLE_CRITICAL: u32 = 2; -pub const SOLVER_RULE_TYPEMASK: u32 = 65280; -pub const SOLVER_SOLUTION_JOB: u32 = 0; -pub const SOLVER_SOLUTION_DISTUPGRADE: i32 = -1; -pub const SOLVER_SOLUTION_INFARCH: i32 = -2; -pub const SOLVER_SOLUTION_BEST: i32 = -3; -pub const SOLVER_SOLUTION_POOLJOB: i32 = -4; -pub const SOLVER_SOLUTION_BLACK: i32 = -5; -pub const SOLVER_SOLUTION_STRICTREPOPRIORITY: i32 = -6; -pub const SOLVER_SOLVABLE: u32 = 1; -pub const SOLVER_SOLVABLE_NAME: u32 = 2; -pub const SOLVER_SOLVABLE_PROVIDES: u32 = 3; -pub const SOLVER_SOLVABLE_ONE_OF: u32 = 4; -pub const SOLVER_SOLVABLE_REPO: u32 = 5; -pub const SOLVER_SOLVABLE_ALL: u32 = 6; -pub const SOLVER_SELECTMASK: u32 = 255; -pub const SOLVER_NOOP: u32 = 0; -pub const SOLVER_INSTALL: u32 = 256; -pub const SOLVER_ERASE: u32 = 512; -pub const SOLVER_UPDATE: u32 = 768; -pub const SOLVER_WEAKENDEPS: u32 = 1024; -pub const SOLVER_MULTIVERSION: u32 = 1280; -pub const SOLVER_LOCK: u32 = 1536; -pub const SOLVER_DISTUPGRADE: u32 = 1792; -pub const SOLVER_VERIFY: u32 = 2048; -pub const SOLVER_DROP_ORPHANED: u32 = 2304; -pub const SOLVER_USERINSTALLED: u32 = 2560; -pub const SOLVER_ALLOWUNINSTALL: u32 = 2816; -pub const SOLVER_FAVOR: u32 = 3072; -pub const SOLVER_DISFAVOR: u32 = 3328; -pub const SOLVER_BLACKLIST: u32 = 3584; -pub const SOLVER_EXCLUDEFROMWEAK: u32 = 4096; -pub const SOLVER_JOBMASK: u32 = 65280; -pub const SOLVER_WEAK: u32 = 65536; -pub const SOLVER_ESSENTIAL: u32 = 131072; -pub const SOLVER_CLEANDEPS: u32 = 262144; -pub const SOLVER_ORUPDATE: u32 = 524288; -pub const SOLVER_FORCEBEST: u32 = 1048576; -pub const SOLVER_TARGETED: u32 = 2097152; -pub const SOLVER_NOTBYUSER: u32 = 4194304; -pub const SOLVER_SETEV: u32 = 16777216; -pub const SOLVER_SETEVR: u32 = 33554432; -pub const SOLVER_SETARCH: u32 = 67108864; -pub const SOLVER_SETVENDOR: u32 = 134217728; -pub const SOLVER_SETREPO: u32 = 268435456; -pub const SOLVER_NOAUTOSET: u32 = 536870912; -pub const SOLVER_SETNAME: u32 = 1073741824; -pub const SOLVER_SETMASK: u32 = 2130706432; -pub const SOLVER_NOOBSOLETES: u32 = 1280; -pub const SOLVER_REASON_UNRELATED: u32 = 0; -pub const SOLVER_REASON_UNIT_RULE: u32 = 1; -pub const SOLVER_REASON_KEEP_INSTALLED: u32 = 2; -pub const SOLVER_REASON_RESOLVE_JOB: u32 = 3; -pub const SOLVER_REASON_UPDATE_INSTALLED: u32 = 4; -pub const SOLVER_REASON_CLEANDEPS_ERASE: u32 = 5; -pub const SOLVER_REASON_RESOLVE: u32 = 6; -pub const SOLVER_REASON_WEAKDEP: u32 = 7; -pub const SOLVER_REASON_RESOLVE_ORPHAN: u32 = 8; -pub const SOLVER_REASON_RECOMMENDED: u32 = 16; -pub const SOLVER_REASON_SUPPLEMENTED: u32 = 17; -pub const SOLVER_FLAG_ALLOW_DOWNGRADE: u32 = 1; -pub const SOLVER_FLAG_ALLOW_ARCHCHANGE: u32 = 2; -pub const SOLVER_FLAG_ALLOW_VENDORCHANGE: u32 = 3; -pub const SOLVER_FLAG_ALLOW_UNINSTALL: u32 = 4; -pub const SOLVER_FLAG_NO_UPDATEPROVIDE: u32 = 5; -pub const SOLVER_FLAG_SPLITPROVIDES: u32 = 6; -pub const SOLVER_FLAG_IGNORE_RECOMMENDED: u32 = 7; -pub const SOLVER_FLAG_ADD_ALREADY_RECOMMENDED: u32 = 8; -pub const SOLVER_FLAG_NO_INFARCHCHECK: u32 = 9; -pub const SOLVER_FLAG_ALLOW_NAMECHANGE: u32 = 10; -pub const SOLVER_FLAG_KEEP_EXPLICIT_OBSOLETES: u32 = 11; -pub const SOLVER_FLAG_BEST_OBEY_POLICY: u32 = 12; -pub const SOLVER_FLAG_NO_AUTOTARGET: u32 = 13; -pub const SOLVER_FLAG_DUP_ALLOW_DOWNGRADE: u32 = 14; -pub const SOLVER_FLAG_DUP_ALLOW_ARCHCHANGE: u32 = 15; -pub const SOLVER_FLAG_DUP_ALLOW_VENDORCHANGE: u32 = 16; -pub const SOLVER_FLAG_DUP_ALLOW_NAMECHANGE: u32 = 17; -pub const SOLVER_FLAG_KEEP_ORPHANS: u32 = 18; -pub const SOLVER_FLAG_BREAK_ORPHANS: u32 = 19; -pub const SOLVER_FLAG_FOCUS_INSTALLED: u32 = 20; -pub const SOLVER_FLAG_YUM_OBSOLETES: u32 = 21; -pub const SOLVER_FLAG_NEED_UPDATEPROVIDE: u32 = 22; -pub const SOLVER_FLAG_URPM_REORDER: u32 = 23; -pub const SOLVER_FLAG_FOCUS_BEST: u32 = 24; -pub const SOLVER_FLAG_STRONG_RECOMMENDS: u32 = 25; -pub const SOLVER_FLAG_INSTALL_ALSO_UPDATES: u32 = 26; -pub const SOLVER_FLAG_ONLY_NAMESPACE_RECOMMENDED: u32 = 27; -pub const SOLVER_FLAG_STRICT_REPO_PRIORITY: u32 = 28; -pub const SOLVER_ALTERNATIVE_TYPE_RULE: u32 = 1; -pub const SOLVER_ALTERNATIVE_TYPE_RECOMMENDS: u32 = 2; -pub const SOLVER_ALTERNATIVE_TYPE_SUGGESTS: u32 = 3; -pub const SELECTION_NAME: u32 = 1; -pub const SELECTION_PROVIDES: u32 = 2; -pub const SELECTION_FILELIST: u32 = 4; -pub const SELECTION_CANON: u32 = 8; -pub const SELECTION_DOTARCH: u32 = 16; -pub const SELECTION_REL: u32 = 32; -pub const SELECTION_GLOB: u32 = 512; -pub const SELECTION_NOCASE: u32 = 2048; -pub const SELECTION_FLAT: u32 = 1024; -pub const SELECTION_SKIP_KIND: u32 = 16384; -pub const SELECTION_MATCH_DEPSTR: u32 = 32768; -pub const SELECTION_INSTALLED_ONLY: u32 = 256; -pub const SELECTION_SOURCE_ONLY: u32 = 4096; -pub const SELECTION_WITH_SOURCE: u32 = 8192; -pub const SELECTION_WITH_DISABLED: u32 = 65536; -pub const SELECTION_WITH_BADARCH: u32 = 131072; -pub const SELECTION_WITH_ALL: u32 = 204800; -pub const SELECTION_REPLACE: u32 = 0; -pub const SELECTION_ADD: u32 = 268435456; -pub const SELECTION_SUBTRACT: u32 = 536870912; -pub const SELECTION_FILTER: u32 = 805306368; -pub const SELECTION_FILTER_KEEP_IFEMPTY: u32 = 1073741824; -pub const SELECTION_FILTER_SWAPPED: u32 = 2147483648; -pub const SELECTION_MODEBITS: u32 = 805306368; -pub const SOLV_ADD_NO_STUBS: u32 = 256; -pub const CONDA_ADD_USE_ONLY_TAR_BZ2: u32 = 256; -pub const CONDA_ADD_WITH_SIGNATUREDATA: u32 = 512; -pub type Stringpool = s_Stringpool; -pub type Pool = s_Pool; -pub type Id = libc::c_int; -pub type Offset = libc::c_uint; -pub type Hashval = libc::c_uint; -pub type Hashtable = *mut Id; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_Reldep { - pub name: Id, - pub evr: Id, - pub flags: libc::c_int, -} -pub type Reldep = s_Reldep; -extern "C" { - pub fn pool_str2id(pool: *mut Pool, arg1: *const libc::c_char, arg2: libc::c_int) -> Id; -} -extern "C" { - pub fn pool_strn2id( - pool: *mut Pool, - arg1: *const libc::c_char, - arg2: libc::c_uint, - arg3: libc::c_int, - ) -> Id; -} -extern "C" { - pub fn pool_rel2id( - pool: *mut Pool, - arg1: Id, - arg2: Id, - arg3: libc::c_int, - arg4: libc::c_int, - ) -> Id; -} -extern "C" { - pub fn pool_id2str(pool: *const Pool, arg1: Id) -> *const libc::c_char; -} -extern "C" { - pub fn pool_id2rel(pool: *const Pool, arg1: Id) -> *const libc::c_char; -} -extern "C" { - pub fn pool_id2evr(pool: *const Pool, arg1: Id) -> *const libc::c_char; -} -extern "C" { - pub fn pool_dep2str(pool: *mut Pool, arg1: Id) -> *const libc::c_char; -} -extern "C" { - pub fn pool_shrink_strings(pool: *mut Pool); -} -extern "C" { - pub fn pool_shrink_rels(pool: *mut Pool); -} -extern "C" { - pub fn pool_freeidhashes(pool: *mut Pool); -} -extern "C" { - pub fn pool_resize_rels_hash(pool: *mut Pool, numnew: libc::c_int); -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_Queue { - pub elements: *mut Id, - pub count: libc::c_int, - pub alloc: *mut Id, - pub left: libc::c_int, -} -pub type Queue = s_Queue; -extern "C" { - pub fn queue_alloc_one(q: *mut Queue); -} -extern "C" { - pub fn queue_alloc_one_head(q: *mut Queue); -} -extern "C" { - pub fn queue_init(q: *mut Queue); -} -extern "C" { - pub fn queue_init_buffer(q: *mut Queue, buf: *mut Id, size: libc::c_int); -} -extern "C" { - pub fn queue_init_clone(target: *mut Queue, source: *const Queue); -} -extern "C" { - pub fn queue_free(q: *mut Queue); -} -extern "C" { - pub fn queue_insert(q: *mut Queue, pos: libc::c_int, id: Id); -} -extern "C" { - pub fn queue_insert2(q: *mut Queue, pos: libc::c_int, id1: Id, id2: Id); -} -extern "C" { - pub fn queue_insertn(q: *mut Queue, pos: libc::c_int, n: libc::c_int, elements: *const Id); -} -extern "C" { - pub fn queue_delete(q: *mut Queue, pos: libc::c_int); -} -extern "C" { - pub fn queue_delete2(q: *mut Queue, pos: libc::c_int); -} -extern "C" { - pub fn queue_deleten(q: *mut Queue, pos: libc::c_int, n: libc::c_int); -} -extern "C" { - pub fn queue_prealloc(q: *mut Queue, n: libc::c_int); -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_Map { - pub map: *mut libc::c_uchar, - pub size: libc::c_int, -} -pub type Map = s_Map; -extern "C" { - pub fn map_init(m: *mut Map, n: libc::c_int); -} -extern "C" { - pub fn map_init_clone(target: *mut Map, source: *const Map); -} -extern "C" { - pub fn map_grow(m: *mut Map, n: libc::c_int); -} -extern "C" { - pub fn map_free(m: *mut Map); -} -extern "C" { - pub fn map_and(t: *mut Map, s: *const Map); -} -extern "C" { - pub fn map_or(t: *mut Map, s: *const Map); -} -extern "C" { - pub fn map_subtract(t: *mut Map, s: *const Map); -} -extern "C" { - pub fn map_invertall(m: *mut Map); -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_Solvable { - pub name: Id, - pub arch: Id, - pub evr: Id, - pub vendor: Id, - pub repo: *mut s_Repo, - pub provides: Offset, - pub obsoletes: Offset, - pub conflicts: Offset, - pub requires: Offset, - pub recommends: Offset, - pub suggests: Offset, - pub supplements: Offset, - pub enhances: Offset, -} -pub type Solvable = s_Solvable; -extern "C" { - pub fn solvable_lookup_type(s: *mut Solvable, keyname: Id) -> Id; -} -extern "C" { - pub fn solvable_lookup_id(s: *mut Solvable, keyname: Id) -> Id; -} -extern "C" { - pub fn solvable_lookup_num( - s: *mut Solvable, - keyname: Id, - notfound: libc::c_ulonglong, - ) -> libc::c_ulonglong; -} -extern "C" { - pub fn solvable_lookup_sizek( - s: *mut Solvable, - keyname: Id, - notfound: libc::c_ulonglong, - ) -> libc::c_ulonglong; -} -extern "C" { - pub fn solvable_lookup_str(s: *mut Solvable, keyname: Id) -> *const libc::c_char; -} -extern "C" { - pub fn solvable_lookup_str_poollang(s: *mut Solvable, keyname: Id) -> *const libc::c_char; -} -extern "C" { - pub fn solvable_lookup_str_lang( - s: *mut Solvable, - keyname: Id, - lang: *const libc::c_char, - usebase: libc::c_int, - ) -> *const libc::c_char; -} -extern "C" { - pub fn solvable_lookup_bool(s: *mut Solvable, keyname: Id) -> libc::c_int; -} -extern "C" { - pub fn solvable_lookup_void(s: *mut Solvable, keyname: Id) -> libc::c_int; -} -extern "C" { - pub fn solvable_get_location( - s: *mut Solvable, - medianrp: *mut libc::c_uint, - ) -> *const libc::c_char; -} -extern "C" { - pub fn solvable_lookup_location( - s: *mut Solvable, - medianrp: *mut libc::c_uint, - ) -> *const libc::c_char; -} -extern "C" { - pub fn solvable_lookup_sourcepkg(s: *mut Solvable) -> *const libc::c_char; -} -extern "C" { - pub fn solvable_lookup_bin_checksum( - s: *mut Solvable, - keyname: Id, - typep: *mut Id, - ) -> *const libc::c_uchar; -} -extern "C" { - pub fn solvable_lookup_checksum( - s: *mut Solvable, - keyname: Id, - typep: *mut Id, - ) -> *const libc::c_char; -} -extern "C" { - pub fn solvable_lookup_idarray(s: *mut Solvable, keyname: Id, q: *mut Queue) -> libc::c_int; -} -extern "C" { - pub fn solvable_lookup_deparray( - s: *mut Solvable, - keyname: Id, - q: *mut Queue, - marker: Id, - ) -> libc::c_int; -} -extern "C" { - pub fn solvable_lookup_count(s: *mut Solvable, keyname: Id) -> libc::c_uint; -} -extern "C" { - pub fn solvable_set_id(s: *mut Solvable, keyname: Id, id: Id); -} -extern "C" { - pub fn solvable_set_num(s: *mut Solvable, keyname: Id, num: libc::c_ulonglong); -} -extern "C" { - pub fn solvable_set_str(s: *mut Solvable, keyname: Id, str_: *const libc::c_char); -} -extern "C" { - pub fn solvable_set_poolstr(s: *mut Solvable, keyname: Id, str_: *const libc::c_char); -} -extern "C" { - pub fn solvable_add_poolstr_array(s: *mut Solvable, keyname: Id, str_: *const libc::c_char); -} -extern "C" { - pub fn solvable_add_idarray(s: *mut Solvable, keyname: Id, id: Id); -} -extern "C" { - pub fn solvable_add_deparray(s: *mut Solvable, keyname: Id, dep: Id, marker: Id); -} -extern "C" { - pub fn solvable_set_idarray(s: *mut Solvable, keyname: Id, q: *mut Queue); -} -extern "C" { - pub fn solvable_set_deparray(s: *mut Solvable, keyname: Id, q: *mut Queue, marker: Id); -} -extern "C" { - pub fn solvable_unset(s: *mut Solvable, keyname: Id); -} -extern "C" { - pub fn solvable_identical(s1: *mut Solvable, s2: *mut Solvable) -> libc::c_int; -} -extern "C" { - pub fn solvable_selfprovidedep(s: *mut Solvable) -> Id; -} -extern "C" { - pub fn solvable_matchesdep( - s: *mut Solvable, - keyname: Id, - dep: Id, - marker: libc::c_int, - ) -> libc::c_int; -} -extern "C" { - pub fn solvable_matchessolvable( - s: *mut Solvable, - keyname: Id, - solvid: Id, - depq: *mut Queue, - marker: libc::c_int, - ) -> libc::c_int; -} -extern "C" { - pub fn solvable_matchessolvable_int( - s: *mut Solvable, - keyname: Id, - marker: libc::c_int, - solvid: Id, - solvidmap: *mut Map, - depq: *mut Queue, - missc: *mut Map, - reloff: libc::c_int, - outdepq: *mut Queue, - ) -> libc::c_int; -} -extern "C" { - pub fn solvable_is_irrelevant_patch(s: *mut Solvable, installedmap: *mut Map) -> libc::c_int; -} -extern "C" { - pub fn solvable_trivial_installable_map( - s: *mut Solvable, - installedmap: *mut Map, - conflictsmap: *mut Map, - multiversionmap: *mut Map, - ) -> libc::c_int; -} -extern "C" { - pub fn solvable_trivial_installable_queue( - s: *mut Solvable, - installed: *mut Queue, - multiversionmap: *mut Map, - ) -> libc::c_int; -} -extern "C" { - pub fn solvable_trivial_installable_repo( - s: *mut Solvable, - installed: *mut s_Repo, - multiversionmap: *mut Map, - ) -> libc::c_int; -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_Stringpool { - pub strings: *mut Offset, - pub nstrings: libc::c_int, - pub stringspace: *mut libc::c_char, - pub sstrings: Offset, - pub stringhashtbl: Hashtable, - pub stringhashmask: Hashval, -} -extern "C" { - pub fn stringpool_init(ss: *mut Stringpool, strs: *mut *const libc::c_char); -} -extern "C" { - pub fn stringpool_init_empty(ss: *mut Stringpool); -} -extern "C" { - pub fn stringpool_clone(ss: *mut Stringpool, from: *mut Stringpool); -} -extern "C" { - pub fn stringpool_free(ss: *mut Stringpool); -} -extern "C" { - pub fn stringpool_freehash(ss: *mut Stringpool); -} -extern "C" { - pub fn stringpool_resize_hash(ss: *mut Stringpool, numnew: libc::c_int); -} -extern "C" { - pub fn stringpool_str2id( - ss: *mut Stringpool, - str_: *const libc::c_char, - create: libc::c_int, - ) -> Id; -} -extern "C" { - pub fn stringpool_strn2id( - ss: *mut Stringpool, - str_: *const libc::c_char, - len: libc::c_uint, - create: libc::c_int, - ) -> Id; -} -extern "C" { - pub fn stringpool_shrink(ss: *mut Stringpool); -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_Datapos { - pub repo: *mut s_Repo, - pub solvid: Id, - pub repodataid: Id, - pub schema: Id, - pub dp: Id, -} -pub type Datapos = s_Datapos; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_Pool { - pub appdata: *mut libc::c_void, - pub ss: s_Stringpool, - pub rels: *mut Reldep, - pub nrels: libc::c_int, - pub repos: *mut *mut s_Repo, - pub nrepos: libc::c_int, - pub urepos: libc::c_int, - pub installed: *mut s_Repo, - pub solvables: *mut Solvable, - pub nsolvables: libc::c_int, - pub languages: *mut *const libc::c_char, - pub nlanguages: libc::c_int, - pub disttype: libc::c_int, - pub id2arch: *mut Id, - pub id2color: *mut libc::c_uchar, - pub lastarch: Id, - pub vendormap: Queue, - pub vendorclasses: *mut *const libc::c_char, - pub whatprovides: *mut Offset, - pub whatprovides_rel: *mut Offset, - pub whatprovidesdata: *mut Id, - pub whatprovidesdataoff: Offset, - pub whatprovidesdataleft: libc::c_int, - pub considered: *mut Map, - pub nscallback: ::std::option::Option< - unsafe extern "C" fn(arg1: *mut s_Pool, data: *mut libc::c_void, name: Id, evr: Id) -> Id, - >, - pub nscallbackdata: *mut libc::c_void, - pub debugmask: libc::c_int, - pub debugcallback: ::std::option::Option< - unsafe extern "C" fn( - arg1: *mut s_Pool, - data: *mut libc::c_void, - type_: libc::c_int, - str_: *const libc::c_char, - ), - >, - pub debugcallbackdata: *mut libc::c_void, - pub loadcallback: ::std::option::Option< - unsafe extern "C" fn( - arg1: *mut s_Pool, - arg2: *mut s_Repodata, - arg3: *mut libc::c_void, - ) -> libc::c_int, - >, - pub loadcallbackdata: *mut libc::c_void, - pub pos: Datapos, - pub pooljobs: Queue, -} -extern "C" { - pub fn pool_create() -> *mut Pool; -} -extern "C" { - pub fn pool_free(pool: *mut Pool); -} -extern "C" { - pub fn pool_freeallrepos(pool: *mut Pool, reuseids: libc::c_int); -} -extern "C" { - pub fn pool_setdebuglevel(pool: *mut Pool, level: libc::c_int); -} -extern "C" { - pub fn pool_setdisttype(pool: *mut Pool, disttype: libc::c_int) -> libc::c_int; -} -extern "C" { - pub fn pool_set_flag(pool: *mut Pool, flag: libc::c_int, value: libc::c_int) -> libc::c_int; -} -extern "C" { - pub fn pool_get_flag(pool: *mut Pool, flag: libc::c_int) -> libc::c_int; -} -extern "C" { - pub fn pool_debug(pool: *mut Pool, type_: libc::c_int, format: *const libc::c_char, ...); -} -extern "C" { - pub fn pool_setdebugcallback( - pool: *mut Pool, - debugcallback: ::std::option::Option< - unsafe extern "C" fn( - arg1: *mut s_Pool, - data: *mut libc::c_void, - type_: libc::c_int, - str_: *const libc::c_char, - ), - >, - debugcallbackdata: *mut libc::c_void, - ); -} -extern "C" { - pub fn pool_setdebugmask(pool: *mut Pool, mask: libc::c_int); -} -extern "C" { - pub fn pool_setloadcallback( - pool: *mut Pool, - cb: ::std::option::Option< - unsafe extern "C" fn( - arg1: *mut s_Pool, - arg2: *mut s_Repodata, - arg3: *mut libc::c_void, - ) -> libc::c_int, - >, - loadcbdata: *mut libc::c_void, - ); -} -extern "C" { - pub fn pool_setnamespacecallback( - pool: *mut Pool, - cb: ::std::option::Option< - unsafe extern "C" fn( - arg1: *mut s_Pool, - arg2: *mut libc::c_void, - arg3: Id, - arg4: Id, - ) -> Id, - >, - nscbdata: *mut libc::c_void, - ); -} -extern "C" { - pub fn pool_flush_namespaceproviders(pool: *mut Pool, ns: Id, evr: Id); -} -extern "C" { - pub fn pool_set_custom_vendorcheck( - pool: *mut Pool, - vendorcheck: ::std::option::Option< - unsafe extern "C" fn( - arg1: *mut s_Pool, - arg2: *mut Solvable, - arg3: *mut Solvable, - ) -> libc::c_int, - >, - ); -} -extern "C" { - pub fn pool_get_custom_vendorcheck( - pool: *mut Pool, - ) -> ::std::option::Option< - unsafe extern "C" fn( - pool: *mut s_Pool, - arg1: *mut Solvable, - arg2: *mut Solvable, - ) -> libc::c_int, - >; -} -extern "C" { - pub fn pool_alloctmpspace(pool: *mut Pool, len: libc::c_int) -> *mut libc::c_char; -} -extern "C" { - pub fn pool_freetmpspace(pool: *mut Pool, space: *const libc::c_char); -} -extern "C" { - pub fn pool_tmpjoin( - pool: *mut Pool, - str1: *const libc::c_char, - str2: *const libc::c_char, - str3: *const libc::c_char, - ) -> *mut libc::c_char; -} -extern "C" { - pub fn pool_tmpappend( - pool: *mut Pool, - str1: *const libc::c_char, - str2: *const libc::c_char, - str3: *const libc::c_char, - ) -> *mut libc::c_char; -} -extern "C" { - pub fn pool_bin2hex( - pool: *mut Pool, - buf: *const libc::c_uchar, - len: libc::c_int, - ) -> *const libc::c_char; -} -extern "C" { - pub fn pool_set_installed(pool: *mut Pool, repo: *mut s_Repo); -} -extern "C" { - pub fn pool_error( - pool: *mut Pool, - ret: libc::c_int, - format: *const libc::c_char, - ... - ) -> libc::c_int; -} -extern "C" { - pub fn pool_errstr(pool: *mut Pool) -> *mut libc::c_char; -} -extern "C" { - pub fn pool_set_rootdir(pool: *mut Pool, rootdir: *const libc::c_char); -} -extern "C" { - pub fn pool_get_rootdir(pool: *mut Pool) -> *const libc::c_char; -} -extern "C" { - pub fn pool_prepend_rootdir(pool: *mut Pool, dir: *const libc::c_char) -> *mut libc::c_char; -} -extern "C" { - pub fn pool_prepend_rootdir_tmp( - pool: *mut Pool, - dir: *const libc::c_char, - ) -> *const libc::c_char; -} -extern "C" { - #[doc = " Solvable management"] - pub fn pool_add_solvable(pool: *mut Pool) -> Id; -} -extern "C" { - pub fn pool_add_solvable_block(pool: *mut Pool, count: libc::c_int) -> Id; -} -extern "C" { - pub fn pool_free_solvable_block( - pool: *mut Pool, - start: Id, - count: libc::c_int, - reuseids: libc::c_int, - ); -} -extern "C" { - pub fn pool_solvable2str(pool: *mut Pool, s: *mut Solvable) -> *const libc::c_char; -} -extern "C" { - pub fn pool_set_languages( - pool: *mut Pool, - languages: *mut *const libc::c_char, - nlanguages: libc::c_int, - ); -} -extern "C" { - pub fn pool_id2langid( - pool: *mut Pool, - id: Id, - lang: *const libc::c_char, - create: libc::c_int, - ) -> Id; -} -extern "C" { - pub fn pool_intersect_evrs( - pool: *mut Pool, - pflags: libc::c_int, - pevr: Id, - flags: libc::c_int, - evr: Id, - ) -> libc::c_int; -} -extern "C" { - pub fn pool_match_dep(pool: *mut Pool, d1: Id, d2: Id) -> libc::c_int; -} -extern "C" { - pub fn pool_match_nevr_rel(pool: *mut Pool, s: *mut Solvable, d: Id) -> libc::c_int; -} -extern "C" { - #[doc = " Prepares a pool for solving"] - pub fn pool_createwhatprovides(pool: *mut Pool); -} -extern "C" { - pub fn pool_addfileprovides(pool: *mut Pool); -} -extern "C" { - pub fn pool_addfileprovides_queue(pool: *mut Pool, idq: *mut Queue, idqinst: *mut Queue); -} -extern "C" { - pub fn pool_freewhatprovides(pool: *mut Pool); -} -extern "C" { - pub fn pool_queuetowhatprovides(pool: *mut Pool, q: *mut Queue) -> Id; -} -extern "C" { - pub fn pool_ids2whatprovides(pool: *mut Pool, ids: *mut Id, count: libc::c_int) -> Id; -} -extern "C" { - pub fn pool_searchlazywhatprovidesq(pool: *mut Pool, d: Id) -> Id; -} -extern "C" { - pub fn pool_addrelproviders(pool: *mut Pool, d: Id) -> Id; -} -extern "C" { - pub fn pool_whatmatchesdep( - pool: *mut Pool, - keyname: Id, - dep: Id, - q: *mut Queue, - marker: libc::c_int, - ); -} -extern "C" { - pub fn pool_whatcontainsdep( - pool: *mut Pool, - keyname: Id, - dep: Id, - q: *mut Queue, - marker: libc::c_int, - ); -} -extern "C" { - pub fn pool_whatmatchessolvable( - pool: *mut Pool, - keyname: Id, - solvid: Id, - q: *mut Queue, - marker: libc::c_int, - ); -} -extern "C" { - pub fn pool_set_whatprovides(pool: *mut Pool, id: Id, providers: Id); -} -extern "C" { - pub fn pool_search( - pool: *mut Pool, - p: Id, - key: Id, - match_: *const libc::c_char, - flags: libc::c_int, - callback: ::std::option::Option< - unsafe extern "C" fn( - cbdata: *mut libc::c_void, - s: *mut Solvable, - data: *mut s_Repodata, - key: *mut s_Repokey, - kv: *mut s_KeyValue, - ) -> libc::c_int, - >, - cbdata: *mut libc::c_void, - ); -} -extern "C" { - pub fn pool_clear_pos(pool: *mut Pool); -} -extern "C" { - pub fn pool_lookup_str(pool: *mut Pool, entry: Id, keyname: Id) -> *const libc::c_char; -} -extern "C" { - pub fn pool_lookup_id(pool: *mut Pool, entry: Id, keyname: Id) -> Id; -} -extern "C" { - pub fn pool_lookup_num( - pool: *mut Pool, - entry: Id, - keyname: Id, - notfound: libc::c_ulonglong, - ) -> libc::c_ulonglong; -} -extern "C" { - pub fn pool_lookup_void(pool: *mut Pool, entry: Id, keyname: Id) -> libc::c_int; -} -extern "C" { - pub fn pool_lookup_bin_checksum( - pool: *mut Pool, - entry: Id, - keyname: Id, - typep: *mut Id, - ) -> *const libc::c_uchar; -} -extern "C" { - pub fn pool_lookup_idarray( - pool: *mut Pool, - entry: Id, - keyname: Id, - q: *mut Queue, - ) -> libc::c_int; -} -extern "C" { - pub fn pool_lookup_checksum( - pool: *mut Pool, - entry: Id, - keyname: Id, - typep: *mut Id, - ) -> *const libc::c_char; -} -extern "C" { - pub fn pool_lookup_deltalocation( - pool: *mut Pool, - entry: Id, - medianrp: *mut libc::c_uint, - ) -> *const libc::c_char; -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_DUChanges { - pub path: *const libc::c_char, - pub kbytes: libc::c_longlong, - pub files: libc::c_longlong, - pub flags: libc::c_int, -} -pub type DUChanges = s_DUChanges; -extern "C" { - pub fn pool_create_state_maps( - pool: *mut Pool, - installed: *mut Queue, - installedmap: *mut Map, - conflictsmap: *mut Map, - ); -} -extern "C" { - pub fn pool_calc_duchanges( - pool: *mut Pool, - installedmap: *mut Map, - mps: *mut DUChanges, - nmps: libc::c_int, - ); -} -extern "C" { - pub fn pool_calc_installsizechange(pool: *mut Pool, installedmap: *mut Map) - -> libc::c_longlong; -} -extern "C" { - pub fn pool_add_fileconflicts_deps(pool: *mut Pool, conflicts: *mut Queue); -} -extern "C" { - pub fn pool_trivial_installable_multiversionmap( - pool: *mut Pool, - installedmap: *mut Map, - pkgs: *mut Queue, - res: *mut Queue, - multiversionmap: *mut Map, - ); -} -extern "C" { - pub fn pool_trivial_installable( - pool: *mut Pool, - installedmap: *mut Map, - pkgs: *mut Queue, - res: *mut Queue, - ); -} -extern "C" { - pub fn pool_setarch(arg1: *mut Pool, arg2: *const libc::c_char); -} -extern "C" { - pub fn pool_setarchpolicy(arg1: *mut Pool, arg2: *const libc::c_char); -} -extern "C" { - pub fn pool_arch2color_slow(pool: *mut Pool, arch: Id) -> libc::c_uchar; -} -extern "C" { - #[doc = " malloc\n exits with error message on error"] - pub fn solv_malloc(arg1: usize) -> *mut libc::c_void; -} -extern "C" { - pub fn solv_malloc2(arg1: usize, arg2: usize) -> *mut libc::c_void; -} -extern "C" { - pub fn solv_calloc(arg1: usize, arg2: usize) -> *mut libc::c_void; -} -extern "C" { - pub fn solv_realloc(arg1: *mut libc::c_void, arg2: usize) -> *mut libc::c_void; -} -extern "C" { - pub fn solv_realloc2(arg1: *mut libc::c_void, arg2: usize, arg3: usize) -> *mut libc::c_void; -} -extern "C" { - pub fn solv_extend_realloc( - arg1: *mut libc::c_void, - arg2: usize, - arg3: usize, - arg4: usize, - ) -> *mut libc::c_void; -} -extern "C" { - pub fn solv_free(arg1: *mut libc::c_void) -> *mut libc::c_void; -} -extern "C" { - pub fn solv_strdup(arg1: *const libc::c_char) -> *mut libc::c_char; -} -extern "C" { - pub fn solv_oom(arg1: usize, arg2: usize); -} -extern "C" { - pub fn solv_timems(subtract: libc::c_uint) -> libc::c_uint; -} -extern "C" { - pub fn solv_setcloexec(fd: libc::c_int, state: libc::c_int) -> libc::c_int; -} -extern "C" { - pub fn solv_sort( - base: *mut libc::c_void, - nmemb: usize, - size: usize, - compar: ::std::option::Option< - unsafe extern "C" fn( - arg1: *const libc::c_void, - arg2: *const libc::c_void, - arg3: *mut libc::c_void, - ) -> libc::c_int, - >, - compard: *mut libc::c_void, - ); -} -extern "C" { - pub fn solv_dupjoin( - str1: *const libc::c_char, - str2: *const libc::c_char, - str3: *const libc::c_char, - ) -> *mut libc::c_char; -} -extern "C" { - pub fn solv_dupappend( - str1: *const libc::c_char, - str2: *const libc::c_char, - str3: *const libc::c_char, - ) -> *mut libc::c_char; -} -extern "C" { - pub fn solv_hex2bin( - strp: *mut *const libc::c_char, - buf: *mut libc::c_uchar, - bufl: libc::c_int, - ) -> libc::c_int; -} -extern "C" { - pub fn solv_bin2hex( - buf: *const libc::c_uchar, - l: libc::c_int, - str_: *mut libc::c_char, - ) -> *mut libc::c_char; -} -extern "C" { - pub fn solv_validutf8(buf: *const libc::c_char) -> usize; -} -extern "C" { - pub fn solv_latin1toutf8(buf: *const libc::c_char) -> *mut libc::c_char; -} -extern "C" { - pub fn solv_replacebadutf8( - buf: *const libc::c_char, - replchar: libc::c_int, - ) -> *mut libc::c_char; -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_Dirpool { - pub dirs: *mut Id, - pub ndirs: libc::c_int, - pub dirtraverse: *mut Id, -} -pub type Dirpool = s_Dirpool; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_Repokey { - pub name: Id, - pub type_: Id, - pub size: libc::c_uint, - pub storage: libc::c_uint, -} -pub type Repokey = s_Repokey; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_Repodata { - pub repodataid: Id, - pub repo: *mut s_Repo, - pub state: libc::c_int, - pub loadcallback: ::std::option::Option, - pub start: libc::c_int, - pub end: libc::c_int, - pub keys: *mut Repokey, - pub nkeys: libc::c_int, - pub keybits: [libc::c_uchar; 32usize], - pub schemata: *mut Id, - pub nschemata: libc::c_int, - pub schemadata: *mut Id, - pub spool: Stringpool, - pub localpool: libc::c_int, - pub dirpool: Dirpool, -} -pub type Repodata = s_Repodata; -extern "C" { - pub fn repodata_initdata(data: *mut Repodata, repo: *mut s_Repo, localpool: libc::c_int); -} -extern "C" { - pub fn repodata_freedata(data: *mut Repodata); -} -extern "C" { - pub fn repodata_free(data: *mut Repodata); -} -extern "C" { - pub fn repodata_empty(data: *mut Repodata, localpool: libc::c_int); -} -extern "C" { - pub fn repodata_load(data: *mut Repodata); -} -extern "C" { - pub fn repodata_key2id(data: *mut Repodata, key: *mut Repokey, create: libc::c_int) -> Id; -} -extern "C" { - pub fn repodata_schema2id(data: *mut Repodata, schema: *mut Id, create: libc::c_int) -> Id; -} -extern "C" { - pub fn repodata_free_schemahash(data: *mut Repodata); -} -extern "C" { - pub fn repodata_search( - data: *mut Repodata, - solvid: Id, - keyname: Id, - flags: libc::c_int, - callback: ::std::option::Option< - unsafe extern "C" fn( - cbdata: *mut libc::c_void, - s: *mut Solvable, - data: *mut Repodata, - key: *mut Repokey, - kv: *mut s_KeyValue, - ) -> libc::c_int, - >, - cbdata: *mut libc::c_void, - ); -} -extern "C" { - pub fn repodata_search_keyskip( - data: *mut Repodata, - solvid: Id, - keyname: Id, - flags: libc::c_int, - keyskip: *mut Id, - callback: ::std::option::Option< - unsafe extern "C" fn( - cbdata: *mut libc::c_void, - s: *mut Solvable, - data: *mut Repodata, - key: *mut Repokey, - kv: *mut s_KeyValue, - ) -> libc::c_int, - >, - cbdata: *mut libc::c_void, - ); -} -extern "C" { - pub fn repodata_search_arrayelement( - data: *mut Repodata, - solvid: Id, - keyname: Id, - flags: libc::c_int, - kv: *mut s_KeyValue, - callback: ::std::option::Option< - unsafe extern "C" fn( - cbdata: *mut libc::c_void, - s: *mut Solvable, - data: *mut Repodata, - key: *mut Repokey, - kv: *mut s_KeyValue, - ) -> libc::c_int, - >, - cbdata: *mut libc::c_void, - ); -} -extern "C" { - pub fn repodata_stringify( - pool: *mut Pool, - data: *mut Repodata, - key: *mut Repokey, - kv: *mut s_KeyValue, - flags: libc::c_int, - ) -> *const libc::c_char; -} -extern "C" { - pub fn repodata_set_filelisttype(data: *mut Repodata, filelisttype: libc::c_int); -} -extern "C" { - pub fn repodata_filelistfilter_matches( - data: *mut Repodata, - str_: *const libc::c_char, - ) -> libc::c_int; -} -extern "C" { - pub fn repodata_free_filelistfilter(data: *mut Repodata); -} -extern "C" { - pub fn repodata_lookup_type(data: *mut Repodata, solvid: Id, keyname: Id) -> Id; -} -extern "C" { - pub fn repodata_lookup_id(data: *mut Repodata, solvid: Id, keyname: Id) -> Id; -} -extern "C" { - pub fn repodata_lookup_str(data: *mut Repodata, solvid: Id, keyname: Id) - -> *const libc::c_char; -} -extern "C" { - pub fn repodata_lookup_num( - data: *mut Repodata, - solvid: Id, - keyname: Id, - notfound: libc::c_ulonglong, - ) -> libc::c_ulonglong; -} -extern "C" { - pub fn repodata_lookup_void(data: *mut Repodata, solvid: Id, keyname: Id) -> libc::c_int; -} -extern "C" { - pub fn repodata_lookup_bin_checksum( - data: *mut Repodata, - solvid: Id, - keyname: Id, - typep: *mut Id, - ) -> *const libc::c_uchar; -} -extern "C" { - pub fn repodata_lookup_idarray( - data: *mut Repodata, - solvid: Id, - keyname: Id, - q: *mut Queue, - ) -> libc::c_int; -} -extern "C" { - pub fn repodata_lookup_binary( - data: *mut Repodata, - solvid: Id, - keyname: Id, - lenp: *mut libc::c_int, - ) -> *const libc::c_void; -} -extern "C" { - pub fn repodata_lookup_count(data: *mut Repodata, solvid: Id, keyname: Id) -> libc::c_uint; -} -extern "C" { - pub fn repodata_lookup_packed_dirstrarray( - data: *mut Repodata, - solvid: Id, - keyname: Id, - ) -> *const libc::c_uchar; -} -extern "C" { - pub fn repodata_fill_keyskip(data: *mut Repodata, solvid: Id, keyskip: *mut Id) -> *mut Id; -} -extern "C" { - pub fn repodata_extend(data: *mut Repodata, p: Id); -} -extern "C" { - pub fn repodata_extend_block(data: *mut Repodata, p: Id, num: libc::c_int); -} -extern "C" { - pub fn repodata_shrink(data: *mut Repodata, end: libc::c_int); -} -extern "C" { - pub fn repodata_internalize(data: *mut Repodata); -} -extern "C" { - pub fn repodata_new_handle(data: *mut Repodata) -> Id; -} -extern "C" { - pub fn repodata_set_void(data: *mut Repodata, solvid: Id, keyname: Id); -} -extern "C" { - pub fn repodata_set_num(data: *mut Repodata, solvid: Id, keyname: Id, num: libc::c_ulonglong); -} -extern "C" { - pub fn repodata_set_id(data: *mut Repodata, solvid: Id, keyname: Id, id: Id); -} -extern "C" { - pub fn repodata_set_str( - data: *mut Repodata, - solvid: Id, - keyname: Id, - str_: *const libc::c_char, - ); -} -extern "C" { - pub fn repodata_set_binary( - data: *mut Repodata, - solvid: Id, - keyname: Id, - buf: *mut libc::c_void, - len: libc::c_int, - ); -} -extern "C" { - pub fn repodata_set_poolstr( - data: *mut Repodata, - solvid: Id, - keyname: Id, - str_: *const libc::c_char, - ); -} -extern "C" { - pub fn repodata_set_constant( - data: *mut Repodata, - solvid: Id, - keyname: Id, - constant: libc::c_uint, - ); -} -extern "C" { - pub fn repodata_set_constantid(data: *mut Repodata, solvid: Id, keyname: Id, id: Id); -} -extern "C" { - pub fn repodata_set_bin_checksum( - data: *mut Repodata, - solvid: Id, - keyname: Id, - type_: Id, - buf: *const libc::c_uchar, - ); -} -extern "C" { - pub fn repodata_set_checksum( - data: *mut Repodata, - solvid: Id, - keyname: Id, - type_: Id, - str_: *const libc::c_char, - ); -} -extern "C" { - pub fn repodata_set_idarray(data: *mut Repodata, solvid: Id, keyname: Id, q: *mut Queue); -} -extern "C" { - pub fn repodata_add_dirnumnum( - data: *mut Repodata, - solvid: Id, - keyname: Id, - dir: Id, - num: Id, - num2: Id, - ); -} -extern "C" { - pub fn repodata_add_dirstr( - data: *mut Repodata, - solvid: Id, - keyname: Id, - dir: Id, - str_: *const libc::c_char, - ); -} -extern "C" { - pub fn repodata_free_dircache(data: *mut Repodata); -} -extern "C" { - pub fn repodata_add_idarray(data: *mut Repodata, solvid: Id, keyname: Id, id: Id); -} -extern "C" { - pub fn repodata_add_poolstr_array( - data: *mut Repodata, - solvid: Id, - keyname: Id, - str_: *const libc::c_char, - ); -} -extern "C" { - pub fn repodata_add_fixarray(data: *mut Repodata, solvid: Id, keyname: Id, ghandle: Id); -} -extern "C" { - pub fn repodata_add_flexarray(data: *mut Repodata, solvid: Id, keyname: Id, ghandle: Id); -} -extern "C" { - pub fn repodata_set_kv( - data: *mut Repodata, - solvid: Id, - keyname: Id, - keytype: Id, - kv: *mut s_KeyValue, - ); -} -extern "C" { - pub fn repodata_unset(data: *mut Repodata, solvid: Id, keyname: Id); -} -extern "C" { - pub fn repodata_unset_uninternalized(data: *mut Repodata, solvid: Id, keyname: Id); -} -extern "C" { - pub fn repodata_merge_attrs(data: *mut Repodata, dest: Id, src: Id); -} -extern "C" { - pub fn repodata_merge_some_attrs( - data: *mut Repodata, - dest: Id, - src: Id, - keyidmap: *mut Map, - overwrite: libc::c_int, - ); -} -extern "C" { - pub fn repodata_swap_attrs(data: *mut Repodata, dest: Id, src: Id); -} -extern "C" { - pub fn repodata_create_stubs(data: *mut Repodata) -> *mut Repodata; -} -extern "C" { - pub fn repodata_disable_paging(data: *mut Repodata); -} -extern "C" { - pub fn repodata_globalize_id(data: *mut Repodata, id: Id, create: libc::c_int) -> Id; -} -extern "C" { - pub fn repodata_localize_id(data: *mut Repodata, id: Id, create: libc::c_int) -> Id; -} -extern "C" { - pub fn repodata_translate_id( - data: *mut Repodata, - fromdata: *mut Repodata, - id: Id, - create: libc::c_int, - ) -> Id; -} -extern "C" { - pub fn repodata_translate_dir_slow( - data: *mut Repodata, - fromdata: *mut Repodata, - dir: Id, - create: libc::c_int, - cache: *mut Id, - ) -> Id; -} -extern "C" { - pub fn repodata_str2dir( - data: *mut Repodata, - dir: *const libc::c_char, - create: libc::c_int, - ) -> Id; -} -extern "C" { - pub fn repodata_dir2str( - data: *mut Repodata, - did: Id, - suf: *const libc::c_char, - ) -> *const libc::c_char; -} -extern "C" { - pub fn repodata_chk2str( - data: *mut Repodata, - type_: Id, - buf: *const libc::c_uchar, - ) -> *const libc::c_char; -} -extern "C" { - pub fn repodata_set_location( - data: *mut Repodata, - solvid: Id, - medianr: libc::c_int, - dir: *const libc::c_char, - file: *const libc::c_char, - ); -} -extern "C" { - pub fn repodata_set_deltalocation( - data: *mut Repodata, - handle: Id, - medianr: libc::c_int, - dir: *const libc::c_char, - file: *const libc::c_char, - ); -} -extern "C" { - pub fn repodata_set_sourcepkg(data: *mut Repodata, solvid: Id, sourcepkg: *const libc::c_char); -} -extern "C" { - pub fn repodata_lookup_kv_uninternalized( - data: *mut Repodata, - solvid: Id, - keyname: Id, - kv: *mut s_KeyValue, - ) -> *mut Repokey; -} -extern "C" { - pub fn repodata_search_uninternalized( - data: *mut Repodata, - solvid: Id, - keyname: Id, - flags: libc::c_int, - callback: ::std::option::Option< - unsafe extern "C" fn( - cbdata: *mut libc::c_void, - s: *mut Solvable, - data: *mut Repodata, - key: *mut Repokey, - kv: *mut s_KeyValue, - ) -> libc::c_int, - >, - cbdata: *mut libc::c_void, - ); -} -extern "C" { - pub fn repodata_memused(data: *mut Repodata) -> libc::c_uint; -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_KeyValue { - pub id: Id, - pub str_: *const libc::c_char, - pub num: libc::c_uint, - pub num2: libc::c_uint, - pub entry: libc::c_int, - pub eof: libc::c_int, - pub parent: *mut s_KeyValue, -} -pub type KeyValue = s_KeyValue; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_Datamatcher { - pub flags: libc::c_int, - pub match_: *const libc::c_char, - pub matchdata: *mut libc::c_void, - pub error: libc::c_int, -} -pub type Datamatcher = s_Datamatcher; -extern "C" { - pub fn datamatcher_init( - ma: *mut Datamatcher, - match_: *const libc::c_char, - flags: libc::c_int, - ) -> libc::c_int; -} -extern "C" { - pub fn datamatcher_free(ma: *mut Datamatcher); -} -extern "C" { - pub fn datamatcher_match(ma: *mut Datamatcher, str_: *const libc::c_char) -> libc::c_int; -} -extern "C" { - pub fn datamatcher_checkbasename( - ma: *mut Datamatcher, - str_: *const libc::c_char, - ) -> libc::c_int; -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_Dataiterator { - pub state: libc::c_int, - pub flags: libc::c_int, - pub pool: *mut Pool, - pub repo: *mut s_Repo, - pub data: *mut s_Repodata, - pub dp: *mut libc::c_uchar, - pub ddp: *mut libc::c_uchar, - pub idp: *mut Id, - pub keyp: *mut Id, - pub key: *mut s_Repokey, - pub kv: KeyValue, - pub matcher: Datamatcher, - pub keyname: Id, - pub repodataid: Id, - pub solvid: Id, - pub repoid: Id, - pub keynames: [Id; 4usize], - pub nkeynames: libc::c_int, - pub rootlevel: libc::c_int, - pub parents: [s_Dataiterator_di_parent; 3usize], - pub nparents: libc::c_int, - pub vert_ddp: *mut libc::c_uchar, - pub vert_off: Id, - pub vert_len: Id, - pub vert_storestate: Id, - pub dupstr: *mut libc::c_char, - pub dupstrn: libc::c_int, - pub keyskip: *mut Id, - pub oldkeyskip: *mut Id, -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_Dataiterator_di_parent { - pub kv: KeyValue, - pub dp: *mut libc::c_uchar, - pub keyp: *mut Id, -} -pub type Dataiterator = s_Dataiterator; -extern "C" { - pub fn dataiterator_init( - di: *mut Dataiterator, - pool: *mut Pool, - repo: *mut s_Repo, - p: Id, - keyname: Id, - match_: *const libc::c_char, - flags: libc::c_int, - ) -> libc::c_int; -} -extern "C" { - pub fn dataiterator_init_clone(di: *mut Dataiterator, from: *mut Dataiterator); -} -extern "C" { - pub fn dataiterator_set_search(di: *mut Dataiterator, repo: *mut s_Repo, p: Id); -} -extern "C" { - pub fn dataiterator_set_keyname(di: *mut Dataiterator, keyname: Id); -} -extern "C" { - pub fn dataiterator_set_match( - di: *mut Dataiterator, - match_: *const libc::c_char, - flags: libc::c_int, - ) -> libc::c_int; -} -extern "C" { - pub fn dataiterator_prepend_keyname(di: *mut Dataiterator, keyname: Id); -} -extern "C" { - pub fn dataiterator_free(di: *mut Dataiterator); -} -extern "C" { - pub fn dataiterator_step(di: *mut Dataiterator) -> libc::c_int; -} -extern "C" { - pub fn dataiterator_setpos(di: *mut Dataiterator); -} -extern "C" { - pub fn dataiterator_setpos_parent(di: *mut Dataiterator); -} -extern "C" { - pub fn dataiterator_match(di: *mut Dataiterator, ma: *mut Datamatcher) -> libc::c_int; -} -extern "C" { - pub fn dataiterator_skip_attribute(di: *mut Dataiterator); -} -extern "C" { - pub fn dataiterator_skip_solvable(di: *mut Dataiterator); -} -extern "C" { - pub fn dataiterator_skip_repo(di: *mut Dataiterator); -} -extern "C" { - pub fn dataiterator_jump_to_solvid(di: *mut Dataiterator, solvid: Id); -} -extern "C" { - pub fn dataiterator_jump_to_repo(di: *mut Dataiterator, repo: *mut s_Repo); -} -extern "C" { - pub fn dataiterator_entersub(di: *mut Dataiterator); -} -extern "C" { - pub fn dataiterator_clonepos(di: *mut Dataiterator, from: *mut Dataiterator); -} -extern "C" { - pub fn dataiterator_seek(di: *mut Dataiterator, whence: libc::c_int); -} -extern "C" { - pub fn dataiterator_strdup(di: *mut Dataiterator); -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_Repo { - pub name: *const libc::c_char, - pub repoid: Id, - pub appdata: *mut libc::c_void, - pub pool: *mut Pool, - pub start: libc::c_int, - pub end: libc::c_int, - pub nsolvables: libc::c_int, - pub disabled: libc::c_int, - pub priority: libc::c_int, - pub subpriority: libc::c_int, - pub idarraydata: *mut Id, - pub idarraysize: libc::c_int, - pub nrepodata: libc::c_int, - pub rpmdbid: *mut Id, -} -pub type Repo = s_Repo; -extern "C" { - pub fn repo_create(pool: *mut Pool, name: *const libc::c_char) -> *mut Repo; -} -extern "C" { - pub fn repo_free(repo: *mut Repo, reuseids: libc::c_int); -} -extern "C" { - pub fn repo_empty(repo: *mut Repo, reuseids: libc::c_int); -} -extern "C" { - pub fn repo_freedata(repo: *mut Repo); -} -extern "C" { - pub fn repo_add_solvable(repo: *mut Repo) -> Id; -} -extern "C" { - pub fn repo_add_solvable_block(repo: *mut Repo, count: libc::c_int) -> Id; -} -extern "C" { - pub fn repo_free_solvable(repo: *mut Repo, p: Id, reuseids: libc::c_int); -} -extern "C" { - pub fn repo_free_solvable_block( - repo: *mut Repo, - start: Id, - count: libc::c_int, - reuseids: libc::c_int, - ); -} -extern "C" { - pub fn repo_sidedata_create(repo: *mut Repo, size: usize) -> *mut libc::c_void; -} -extern "C" { - pub fn repo_sidedata_extend( - repo: *mut Repo, - b: *mut libc::c_void, - size: usize, - p: Id, - count: libc::c_int, - ) -> *mut libc::c_void; -} -extern "C" { - pub fn repo_add_solvable_block_before( - repo: *mut Repo, - count: libc::c_int, - beforerepo: *mut Repo, - ) -> Id; -} -extern "C" { - pub fn repo_addid(repo: *mut Repo, olddeps: Offset, id: Id) -> Offset; -} -extern "C" { - pub fn repo_addid_dep(repo: *mut Repo, olddeps: Offset, id: Id, marker: Id) -> Offset; -} -extern "C" { - pub fn repo_reserve_ids(repo: *mut Repo, olddeps: Offset, num: libc::c_int) -> Offset; -} -extern "C" { - pub fn repo_add_repodata(repo: *mut Repo, flags: libc::c_int) -> *mut Repodata; -} -extern "C" { - pub fn repo_id2repodata(repo: *mut Repo, id: Id) -> *mut Repodata; -} -extern "C" { - pub fn repo_last_repodata(repo: *mut Repo) -> *mut Repodata; -} -extern "C" { - pub fn repo_search( - repo: *mut Repo, - p: Id, - key: Id, - match_: *const libc::c_char, - flags: libc::c_int, - callback: ::std::option::Option< - unsafe extern "C" fn( - cbdata: *mut libc::c_void, - s: *mut Solvable, - data: *mut Repodata, - key: *mut Repokey, - kv: *mut KeyValue, - ) -> libc::c_int, - >, - cbdata: *mut libc::c_void, - ); -} -extern "C" { - pub fn repo_lookup_repodata(repo: *mut Repo, entry: Id, keyname: Id) -> *mut Repodata; -} -extern "C" { - pub fn repo_lookup_repodata_opt(repo: *mut Repo, entry: Id, keyname: Id) -> *mut Repodata; -} -extern "C" { - pub fn repo_lookup_filelist_repodata( - repo: *mut Repo, - entry: Id, - matcher: *mut Datamatcher, - ) -> *mut Repodata; -} -extern "C" { - pub fn repo_lookup_type(repo: *mut Repo, entry: Id, keyname: Id) -> Id; -} -extern "C" { - pub fn repo_lookup_str(repo: *mut Repo, entry: Id, keyname: Id) -> *const libc::c_char; -} -extern "C" { - pub fn repo_lookup_num( - repo: *mut Repo, - entry: Id, - keyname: Id, - notfound: libc::c_ulonglong, - ) -> libc::c_ulonglong; -} -extern "C" { - pub fn repo_lookup_id(repo: *mut Repo, entry: Id, keyname: Id) -> Id; -} -extern "C" { - pub fn repo_lookup_idarray( - repo: *mut Repo, - entry: Id, - keyname: Id, - q: *mut Queue, - ) -> libc::c_int; -} -extern "C" { - pub fn repo_lookup_deparray( - repo: *mut Repo, - entry: Id, - keyname: Id, - q: *mut Queue, - marker: Id, - ) -> libc::c_int; -} -extern "C" { - pub fn repo_lookup_void(repo: *mut Repo, entry: Id, keyname: Id) -> libc::c_int; -} -extern "C" { - pub fn repo_lookup_checksum( - repo: *mut Repo, - entry: Id, - keyname: Id, - typep: *mut Id, - ) -> *const libc::c_char; -} -extern "C" { - pub fn repo_lookup_bin_checksum( - repo: *mut Repo, - entry: Id, - keyname: Id, - typep: *mut Id, - ) -> *const libc::c_uchar; -} -extern "C" { - pub fn repo_lookup_binary( - repo: *mut Repo, - entry: Id, - keyname: Id, - lenp: *mut libc::c_int, - ) -> *const libc::c_void; -} -extern "C" { - pub fn repo_lookup_count(repo: *mut Repo, entry: Id, keyname: Id) -> libc::c_uint; -} -extern "C" { - pub fn solv_depmarker(keyname: Id, marker: Id) -> Id; -} -extern "C" { - pub fn repo_set_id(repo: *mut Repo, p: Id, keyname: Id, id: Id); -} -extern "C" { - pub fn repo_set_num(repo: *mut Repo, p: Id, keyname: Id, num: libc::c_ulonglong); -} -extern "C" { - pub fn repo_set_str(repo: *mut Repo, p: Id, keyname: Id, str_: *const libc::c_char); -} -extern "C" { - pub fn repo_set_poolstr(repo: *mut Repo, p: Id, keyname: Id, str_: *const libc::c_char); -} -extern "C" { - pub fn repo_add_poolstr_array(repo: *mut Repo, p: Id, keyname: Id, str_: *const libc::c_char); -} -extern "C" { - pub fn repo_add_idarray(repo: *mut Repo, p: Id, keyname: Id, id: Id); -} -extern "C" { - pub fn repo_add_deparray(repo: *mut Repo, p: Id, keyname: Id, dep: Id, marker: Id); -} -extern "C" { - pub fn repo_set_idarray(repo: *mut Repo, p: Id, keyname: Id, q: *mut Queue); -} -extern "C" { - pub fn repo_set_deparray(repo: *mut Repo, p: Id, keyname: Id, q: *mut Queue, marker: Id); -} -extern "C" { - pub fn repo_unset(repo: *mut Repo, p: Id, keyname: Id); -} -extern "C" { - pub fn repo_internalize(repo: *mut Repo); -} -extern "C" { - pub fn repo_disable_paging(repo: *mut Repo); -} -extern "C" { - pub fn repo_create_keyskip(repo: *mut Repo, entry: Id, oldkeyskip: *mut *mut Id) -> *mut Id; -} -extern "C" { - pub fn repo_fix_supplements( - repo: *mut Repo, - provides: Offset, - supplements: Offset, - freshens: Offset, - ) -> Offset; -} -extern "C" { - pub fn repo_fix_conflicts(repo: *mut Repo, conflicts: Offset) -> Offset; -} -extern "C" { - pub fn repo_rewrite_suse_deps(s: *mut Solvable, freshens: Offset); -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_Transaction { - pub pool: *mut s_Pool, - pub steps: Queue, -} -pub type Transaction = s_Transaction; -extern "C" { - pub fn transaction_create(pool: *mut s_Pool) -> *mut Transaction; -} -extern "C" { - pub fn transaction_create_decisionq( - pool: *mut s_Pool, - decisionq: *mut Queue, - multiversionmap: *mut Map, - ) -> *mut Transaction; -} -extern "C" { - pub fn transaction_create_clone(srctrans: *mut Transaction) -> *mut Transaction; -} -extern "C" { - pub fn transaction_free(trans: *mut Transaction); -} -extern "C" { - pub fn transaction_obs_pkg(trans: *mut Transaction, p: Id) -> Id; -} -extern "C" { - pub fn transaction_all_obs_pkgs(trans: *mut Transaction, p: Id, pkgs: *mut Queue); -} -extern "C" { - pub fn transaction_type(trans: *mut Transaction, p: Id, mode: libc::c_int) -> Id; -} -extern "C" { - pub fn transaction_classify(trans: *mut Transaction, mode: libc::c_int, classes: *mut Queue); -} -extern "C" { - pub fn transaction_classify_pkgs( - trans: *mut Transaction, - mode: libc::c_int, - type_: Id, - from: Id, - to: Id, - pkgs: *mut Queue, - ); -} -extern "C" { - pub fn transaction_installedresult( - trans: *mut Transaction, - installedq: *mut Queue, - ) -> libc::c_int; -} -extern "C" { - pub fn transaction_calc_installsizechange(trans: *mut Transaction) -> libc::c_longlong; -} -extern "C" { - pub fn transaction_calc_duchanges( - trans: *mut Transaction, - mps: *mut s_DUChanges, - nmps: libc::c_int, - ); -} -extern "C" { - pub fn transaction_order(trans: *mut Transaction, flags: libc::c_int); -} -extern "C" { - pub fn transaction_order_add_choices( - trans: *mut Transaction, - chosen: Id, - choices: *mut Queue, - ) -> libc::c_int; -} -extern "C" { - pub fn transaction_add_obsoleted(trans: *mut Transaction); -} -extern "C" { - pub fn transaction_check_order(trans: *mut Transaction); -} -extern "C" { - pub fn transaction_order_get_cycleids( - trans: *mut Transaction, - q: *mut Queue, - minseverity: libc::c_int, - ); -} -extern "C" { - pub fn transaction_order_get_cycle( - trans: *mut Transaction, - cid: Id, - q: *mut Queue, - ) -> libc::c_int; -} -extern "C" { - pub fn transaction_order_get_edges( - trans: *mut Transaction, - p: Id, - q: *mut Queue, - unbroken: libc::c_int, - ); -} -extern "C" { - pub fn transaction_free_orderdata(trans: *mut Transaction); -} -extern "C" { - pub fn transaction_clone_orderdata(trans: *mut Transaction, srctrans: *mut Transaction); -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_Rule { - pub p: Id, - pub d: Id, - pub w1: Id, - pub w2: Id, - pub n1: Id, - pub n2: Id, -} -pub type Rule = s_Rule; -pub const SolverRuleinfo_SOLVER_RULE_UNKNOWN: SolverRuleinfo = 0; -pub const SolverRuleinfo_SOLVER_RULE_PKG: SolverRuleinfo = 256; -pub const SolverRuleinfo_SOLVER_RULE_PKG_NOT_INSTALLABLE: SolverRuleinfo = 257; -pub const SolverRuleinfo_SOLVER_RULE_PKG_NOTHING_PROVIDES_DEP: SolverRuleinfo = 258; -pub const SolverRuleinfo_SOLVER_RULE_PKG_REQUIRES: SolverRuleinfo = 259; -pub const SolverRuleinfo_SOLVER_RULE_PKG_SELF_CONFLICT: SolverRuleinfo = 260; -pub const SolverRuleinfo_SOLVER_RULE_PKG_CONFLICTS: SolverRuleinfo = 261; -pub const SolverRuleinfo_SOLVER_RULE_PKG_SAME_NAME: SolverRuleinfo = 262; -pub const SolverRuleinfo_SOLVER_RULE_PKG_OBSOLETES: SolverRuleinfo = 263; -pub const SolverRuleinfo_SOLVER_RULE_PKG_IMPLICIT_OBSOLETES: SolverRuleinfo = 264; -pub const SolverRuleinfo_SOLVER_RULE_PKG_INSTALLED_OBSOLETES: SolverRuleinfo = 265; -pub const SolverRuleinfo_SOLVER_RULE_PKG_RECOMMENDS: SolverRuleinfo = 266; -pub const SolverRuleinfo_SOLVER_RULE_PKG_CONSTRAINS: SolverRuleinfo = 267; -pub const SolverRuleinfo_SOLVER_RULE_UPDATE: SolverRuleinfo = 512; -pub const SolverRuleinfo_SOLVER_RULE_FEATURE: SolverRuleinfo = 768; -pub const SolverRuleinfo_SOLVER_RULE_JOB: SolverRuleinfo = 1024; -pub const SolverRuleinfo_SOLVER_RULE_JOB_NOTHING_PROVIDES_DEP: SolverRuleinfo = 1025; -pub const SolverRuleinfo_SOLVER_RULE_JOB_PROVIDED_BY_SYSTEM: SolverRuleinfo = 1026; -pub const SolverRuleinfo_SOLVER_RULE_JOB_UNKNOWN_PACKAGE: SolverRuleinfo = 1027; -pub const SolverRuleinfo_SOLVER_RULE_JOB_UNSUPPORTED: SolverRuleinfo = 1028; -pub const SolverRuleinfo_SOLVER_RULE_DISTUPGRADE: SolverRuleinfo = 1280; -pub const SolverRuleinfo_SOLVER_RULE_INFARCH: SolverRuleinfo = 1536; -pub const SolverRuleinfo_SOLVER_RULE_CHOICE: SolverRuleinfo = 1792; -pub const SolverRuleinfo_SOLVER_RULE_LEARNT: SolverRuleinfo = 2048; -pub const SolverRuleinfo_SOLVER_RULE_BEST: SolverRuleinfo = 2304; -pub const SolverRuleinfo_SOLVER_RULE_YUMOBS: SolverRuleinfo = 2560; -pub const SolverRuleinfo_SOLVER_RULE_RECOMMENDS: SolverRuleinfo = 2816; -pub const SolverRuleinfo_SOLVER_RULE_BLACK: SolverRuleinfo = 3072; -pub const SolverRuleinfo_SOLVER_RULE_STRICT_REPO_PRIORITY: SolverRuleinfo = 3328; -cfg_if::cfg_if! { - if #[cfg(all(target_os = "windows", target_env = "msvc"))] { - pub type SolverRuleinfo = libc::c_int; - } else { - pub type SolverRuleinfo = libc::c_uint; - } -} -extern "C" { - pub fn solver_addrule(solv: *mut s_Solver, p: Id, p2: Id, d: Id) -> *mut Rule; -} -extern "C" { - pub fn solver_unifyrules(solv: *mut s_Solver); -} -extern "C" { - pub fn solver_rulecmp(solv: *mut s_Solver, r1: *mut Rule, r2: *mut Rule) -> libc::c_int; -} -extern "C" { - pub fn solver_shrinkrules(solv: *mut s_Solver, nrules: libc::c_int); -} -extern "C" { - pub fn solver_addpkgrulesforsolvable(solv: *mut s_Solver, s: *mut Solvable, m: *mut Map); -} -extern "C" { - pub fn solver_addpkgrulesforweak(solv: *mut s_Solver, m: *mut Map); -} -extern "C" { - pub fn solver_addpkgrulesforlinked(solv: *mut s_Solver, m: *mut Map); -} -extern "C" { - pub fn solver_addpkgrulesforupdaters( - solv: *mut s_Solver, - s: *mut Solvable, - m: *mut Map, - allow_all: libc::c_int, - ); -} -extern "C" { - pub fn solver_addfeaturerule(solv: *mut s_Solver, s: *mut Solvable); -} -extern "C" { - pub fn solver_addupdaterule(solv: *mut s_Solver, s: *mut Solvable); -} -extern "C" { - pub fn solver_addinfarchrules(solv: *mut s_Solver, addedmap: *mut Map); -} -extern "C" { - pub fn solver_createdupmaps(solv: *mut s_Solver); -} -extern "C" { - pub fn solver_freedupmaps(solv: *mut s_Solver); -} -extern "C" { - pub fn solver_addduprules(solv: *mut s_Solver, addedmap: *mut Map); -} -extern "C" { - pub fn solver_addchoicerules(solv: *mut s_Solver); -} -extern "C" { - pub fn solver_disablechoicerules(solv: *mut s_Solver, r: *mut Rule); -} -extern "C" { - pub fn solver_addbestrules( - solv: *mut s_Solver, - havebestinstalljobs: libc::c_int, - haslockjob: libc::c_int, - ); -} -extern "C" { - pub fn solver_addyumobsrules(solv: *mut s_Solver); -} -extern "C" { - pub fn solver_addblackrules(solv: *mut s_Solver); -} -extern "C" { - pub fn solver_addrecommendsrules(solv: *mut s_Solver); -} -extern "C" { - pub fn solver_addstrictrepopriorules(solv: *mut s_Solver, addedmap: *mut Map); -} -extern "C" { - pub fn solver_disablepolicyrules(solv: *mut s_Solver); -} -extern "C" { - pub fn solver_reenablepolicyrules(solv: *mut s_Solver, jobidx: libc::c_int); -} -extern "C" { - pub fn solver_reenablepolicyrules_cleandeps(solv: *mut s_Solver, pkg: Id); -} -extern "C" { - pub fn solver_allruleinfos(solv: *mut s_Solver, rid: Id, rq: *mut Queue) -> libc::c_int; -} -extern "C" { - pub fn solver_ruleinfo( - solv: *mut s_Solver, - rid: Id, - fromp: *mut Id, - top: *mut Id, - depp: *mut Id, - ) -> SolverRuleinfo; -} -extern "C" { - pub fn solver_ruleclass(solv: *mut s_Solver, rid: Id) -> SolverRuleinfo; -} -extern "C" { - pub fn solver_ruleliterals(solv: *mut s_Solver, rid: Id, q: *mut Queue); -} -extern "C" { - pub fn solver_rule2jobidx(solv: *mut s_Solver, rid: Id) -> libc::c_int; -} -extern "C" { - pub fn solver_rule2job(solv: *mut s_Solver, rid: Id, whatp: *mut Id) -> Id; -} -extern "C" { - pub fn solver_rule2solvable(solv: *mut s_Solver, rid: Id) -> Id; -} -extern "C" { - pub fn solver_rule2rules(solv: *mut s_Solver, rid: Id, q: *mut Queue, recursive: libc::c_int); -} -extern "C" { - pub fn solver_rule2pkgrule(solv: *mut s_Solver, rid: Id) -> Id; -} -extern "C" { - pub fn solver_breakorphans(solv: *mut s_Solver); -} -extern "C" { - pub fn solver_check_brokenorphanrules(solv: *mut s_Solver, dq: *mut Queue); -} -extern "C" { - pub fn solver_recordproblem(solv: *mut s_Solver, rid: Id); -} -extern "C" { - pub fn solver_fixproblem(solv: *mut s_Solver, rid: Id); -} -extern "C" { - pub fn solver_autouninstall(solv: *mut s_Solver, start: libc::c_int) -> Id; -} -extern "C" { - pub fn solver_disableproblemset(solv: *mut s_Solver, start: libc::c_int); -} -extern "C" { - pub fn solver_prepare_solutions(solv: *mut s_Solver) -> libc::c_int; -} -extern "C" { - pub fn solver_problem_count(solv: *mut s_Solver) -> libc::c_uint; -} -extern "C" { - pub fn solver_next_problem(solv: *mut s_Solver, problem: Id) -> Id; -} -extern "C" { - pub fn solver_solution_count(solv: *mut s_Solver, problem: Id) -> libc::c_uint; -} -extern "C" { - pub fn solver_next_solution(solv: *mut s_Solver, problem: Id, solution: Id) -> Id; -} -extern "C" { - pub fn solver_solutionelement_count( - solv: *mut s_Solver, - problem: Id, - solution: Id, - ) -> libc::c_uint; -} -extern "C" { - pub fn solver_solutionelement_internalid(solv: *mut s_Solver, problem: Id, solution: Id) -> Id; -} -extern "C" { - pub fn solver_solutionelement_extrajobflags( - solv: *mut s_Solver, - problem: Id, - solution: Id, - ) -> Id; -} -extern "C" { - pub fn solver_next_solutionelement( - solv: *mut s_Solver, - problem: Id, - solution: Id, - element: Id, - p: *mut Id, - rp: *mut Id, - ) -> Id; -} -extern "C" { - pub fn solver_take_solutionelement( - solv: *mut s_Solver, - p: Id, - rp: Id, - extrajobflags: Id, - job: *mut Queue, - ); -} -extern "C" { - pub fn solver_take_solution(solv: *mut s_Solver, problem: Id, solution: Id, job: *mut Queue); -} -extern "C" { - pub fn solver_findproblemrule(solv: *mut s_Solver, problem: Id) -> Id; -} -extern "C" { - pub fn solver_findallproblemrules(solv: *mut s_Solver, problem: Id, rules: *mut Queue); -} -extern "C" { - pub fn solver_problemruleinfo2str( - solv: *mut s_Solver, - type_: SolverRuleinfo, - source: Id, - target: Id, - dep: Id, - ) -> *const libc::c_char; -} -extern "C" { - pub fn solver_problem2str(solv: *mut s_Solver, problem: Id) -> *const libc::c_char; -} -extern "C" { - pub fn solver_solutionelement2str(solv: *mut s_Solver, p: Id, rp: Id) -> *const libc::c_char; -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_Solver { - pub pool: *mut Pool, - pub job: Queue, - pub solution_callback: ::std::option::Option< - unsafe extern "C" fn(solv: *mut s_Solver, data: *mut libc::c_void) -> libc::c_int, - >, - pub solution_callback_data: *mut libc::c_void, - pub pooljobcnt: libc::c_int, -} -pub type Solver = s_Solver; -extern "C" { - pub fn solver_create(pool: *mut Pool) -> *mut Solver; -} -extern "C" { - pub fn solver_free(solv: *mut Solver); -} -extern "C" { - pub fn solver_solve(solv: *mut Solver, job: *mut Queue) -> libc::c_int; -} -extern "C" { - pub fn solver_create_transaction(solv: *mut Solver) -> *mut Transaction; -} -extern "C" { - pub fn solver_set_flag(solv: *mut Solver, flag: libc::c_int, value: libc::c_int) - -> libc::c_int; -} -extern "C" { - pub fn solver_get_flag(solv: *mut Solver, flag: libc::c_int) -> libc::c_int; -} -extern "C" { - pub fn solver_get_decisionlevel(solv: *mut Solver, p: Id) -> libc::c_int; -} -extern "C" { - pub fn solver_get_decisionqueue(solv: *mut Solver, decisionq: *mut Queue); -} -extern "C" { - pub fn solver_get_lastdecisionblocklevel(solv: *mut Solver) -> libc::c_int; -} -extern "C" { - pub fn solver_get_decisionblock(solv: *mut Solver, level: libc::c_int, decisionq: *mut Queue); -} -extern "C" { - pub fn solver_get_orphaned(solv: *mut Solver, orphanedq: *mut Queue); -} -extern "C" { - pub fn solver_get_recommendations( - solv: *mut Solver, - recommendationsq: *mut Queue, - suggestionsq: *mut Queue, - noselected: libc::c_int, - ); -} -extern "C" { - pub fn solver_get_unneeded(solv: *mut Solver, unneededq: *mut Queue, filtered: libc::c_int); -} -extern "C" { - pub fn solver_get_userinstalled(solv: *mut Solver, q: *mut Queue, flags: libc::c_int); -} -extern "C" { - pub fn pool_add_userinstalled_jobs( - pool: *mut Pool, - q: *mut Queue, - job: *mut Queue, - flags: libc::c_int, - ); -} -extern "C" { - pub fn solver_get_cleandeps(solv: *mut Solver, cleandepsq: *mut Queue); -} -extern "C" { - pub fn solver_describe_decision(solv: *mut Solver, p: Id, infop: *mut Id) -> libc::c_int; -} -extern "C" { - pub fn solver_describe_weakdep_decision(solv: *mut Solver, p: Id, whyq: *mut Queue); -} -extern "C" { - pub fn solver_alternatives_count(solv: *mut Solver) -> libc::c_int; -} -extern "C" { - pub fn solver_get_alternative( - solv: *mut Solver, - alternative: Id, - idp: *mut Id, - fromp: *mut Id, - chosenp: *mut Id, - choices: *mut Queue, - levelp: *mut libc::c_int, - ) -> libc::c_int; -} -extern "C" { - pub fn solver_calculate_multiversionmap( - pool: *mut Pool, - job: *mut Queue, - multiversionmap: *mut Map, - ); -} -extern "C" { - pub fn solver_calculate_noobsmap(pool: *mut Pool, job: *mut Queue, multiversionmap: *mut Map); -} -extern "C" { - pub fn solver_create_state_maps( - solv: *mut Solver, - installedmap: *mut Map, - conflictsmap: *mut Map, - ); -} -extern "C" { - pub fn solver_calc_duchanges(solv: *mut Solver, mps: *mut DUChanges, nmps: libc::c_int); -} -extern "C" { - pub fn solver_calc_installsizechange(solv: *mut Solver) -> libc::c_int; -} -extern "C" { - pub fn pool_job2solvables(pool: *mut Pool, pkgs: *mut Queue, how: Id, what: Id); -} -extern "C" { - pub fn pool_isemptyupdatejob(pool: *mut Pool, how: Id, what: Id) -> libc::c_int; -} -extern "C" { - pub fn solver_select2str(pool: *mut Pool, select: Id, what: Id) -> *const libc::c_char; -} -extern "C" { - pub fn pool_job2str(pool: *mut Pool, how: Id, what: Id, flagmask: Id) -> *const libc::c_char; -} -extern "C" { - pub fn solver_alternative2str( - solv: *mut Solver, - type_: libc::c_int, - id: Id, - from: Id, - ) -> *const libc::c_char; -} -extern "C" { - pub fn solver_trivial_installable(solv: *mut Solver, pkgs: *mut Queue, res: *mut Queue); -} -extern "C" { - pub fn solver_printruleelement(solv: *mut Solver, type_: libc::c_int, r: *mut Rule, v: Id); -} -extern "C" { - pub fn solver_printrule(solv: *mut Solver, type_: libc::c_int, r: *mut Rule); -} -extern "C" { - pub fn solver_printruleclass(solv: *mut Solver, type_: libc::c_int, r: *mut Rule); -} -extern "C" { - pub fn solver_printproblem(solv: *mut Solver, v: Id); -} -extern "C" { - pub fn solver_printwatches(solv: *mut Solver, type_: libc::c_int); -} -extern "C" { - pub fn solver_printdecisionq(solv: *mut Solver, type_: libc::c_int); -} -extern "C" { - pub fn solver_printdecisions(solv: *mut Solver); -} -extern "C" { - pub fn solver_printproblemruleinfo(solv: *mut Solver, rule: Id); -} -extern "C" { - pub fn solver_printprobleminfo(solv: *mut Solver, problem: Id); -} -extern "C" { - pub fn solver_printcompleteprobleminfo(solv: *mut Solver, problem: Id); -} -extern "C" { - pub fn solver_printsolution(solv: *mut Solver, problem: Id, solution: Id); -} -extern "C" { - pub fn solver_printallsolutions(solv: *mut Solver); -} -extern "C" { - pub fn transaction_print(trans: *mut Transaction); -} -extern "C" { - pub fn solver_printtrivial(solv: *mut Solver); -} -extern "C" { - pub fn selection_make( - pool: *mut Pool, - selection: *mut Queue, - name: *const libc::c_char, - flags: libc::c_int, - ) -> libc::c_int; -} -extern "C" { - pub fn selection_make_matchdeps( - pool: *mut Pool, - selection: *mut Queue, - name: *const libc::c_char, - flags: libc::c_int, - keyname: libc::c_int, - marker: libc::c_int, - ) -> libc::c_int; -} -extern "C" { - pub fn selection_make_matchdepid( - pool: *mut Pool, - selection: *mut Queue, - dep: Id, - flags: libc::c_int, - keyname: libc::c_int, - marker: libc::c_int, - ) -> libc::c_int; -} -extern "C" { - pub fn selection_make_matchsolvable( - pool: *mut Pool, - selection: *mut Queue, - solvid: Id, - flags: libc::c_int, - keyname: libc::c_int, - marker: libc::c_int, - ) -> libc::c_int; -} -extern "C" { - pub fn selection_make_matchsolvablelist( - pool: *mut Pool, - selection: *mut Queue, - solvidq: *mut Queue, - flags: libc::c_int, - keyname: libc::c_int, - marker: libc::c_int, - ) -> libc::c_int; -} -extern "C" { - pub fn selection_filter(pool: *mut Pool, sel1: *mut Queue, sel2: *mut Queue); -} -extern "C" { - pub fn selection_add(pool: *mut Pool, sel1: *mut Queue, sel2: *mut Queue); -} -extern "C" { - pub fn selection_subtract(pool: *mut Pool, sel1: *mut Queue, sel2: *mut Queue); -} -extern "C" { - pub fn selection_solvables(pool: *mut Pool, selection: *mut Queue, pkgs: *mut Queue); -} -extern "C" { - pub fn pool_selection2str( - pool: *mut Pool, - selection: *mut Queue, - flagmask: Id, - ) -> *const libc::c_char; -} -extern "C" { - pub fn pool_evrcmp_conda( - pool: *const Pool, - evr1: *const libc::c_char, - evr2: *const libc::c_char, - mode: libc::c_int, - ) -> libc::c_int; -} -extern "C" { - pub fn solvable_conda_matchversion( - s: *mut Solvable, - version: *const libc::c_char, - ) -> libc::c_int; -} -extern "C" { - pub fn pool_addrelproviders_conda(pool: *mut Pool, name: Id, evr: Id, plist: *mut Queue) -> Id; -} -extern "C" { - pub fn pool_conda_matchspec(pool: *mut Pool, name: *const libc::c_char) -> Id; -} -extern "C" { - pub fn repo_add_solv(repo: *mut Repo, fp: *mut FILE, flags: libc::c_int) -> libc::c_int; -} -extern "C" { - pub fn solv_read_userdata( - fp: *mut FILE, - datap: *mut *mut libc::c_uchar, - lenp: *mut libc::c_int, - ) -> libc::c_int; -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct s_Repowriter { - pub repo: *mut Repo, - pub flags: libc::c_int, - pub repodatastart: libc::c_int, - pub repodataend: libc::c_int, - pub solvablestart: libc::c_int, - pub solvableend: libc::c_int, - pub keyfilter: ::std::option::Option< - unsafe extern "C" fn( - repo: *mut Repo, - key: *mut Repokey, - kfdata: *mut libc::c_void, - ) -> libc::c_int, - >, - pub kfdata: *mut libc::c_void, - pub keyq: *mut Queue, - pub userdata: *mut libc::c_void, - pub userdatalen: libc::c_int, -} -pub type Repowriter = s_Repowriter; -extern "C" { - pub fn repowriter_create(repo: *mut Repo) -> *mut Repowriter; -} -extern "C" { - pub fn repowriter_free(writer: *mut Repowriter) -> *mut Repowriter; -} -extern "C" { - pub fn repowriter_set_flags(writer: *mut Repowriter, flags: libc::c_int); -} -extern "C" { - pub fn repowriter_set_keyfilter( - writer: *mut Repowriter, - keyfilter: ::std::option::Option< - unsafe extern "C" fn( - repo: *mut Repo, - key: *mut Repokey, - kfdata: *mut libc::c_void, - ) -> libc::c_int, - >, - kfdata: *mut libc::c_void, - ); -} -extern "C" { - pub fn repowriter_set_keyqueue(writer: *mut Repowriter, keyq: *mut Queue); -} -extern "C" { - pub fn repowriter_set_repodatarange( - writer: *mut Repowriter, - repodatastart: libc::c_int, - repodataend: libc::c_int, - ); -} -extern "C" { - pub fn repowriter_set_solvablerange( - writer: *mut Repowriter, - solvablestart: libc::c_int, - solvableend: libc::c_int, - ); -} -extern "C" { - pub fn repowriter_set_userdata( - writer: *mut Repowriter, - data: *const libc::c_void, - len: libc::c_int, - ); -} -extern "C" { - pub fn repowriter_write(writer: *mut Repowriter, fp: *mut FILE) -> libc::c_int; -} -extern "C" { - pub fn repo_write(repo: *mut Repo, fp: *mut FILE) -> libc::c_int; -} -extern "C" { - pub fn repodata_write(data: *mut Repodata, fp: *mut FILE) -> libc::c_int; -} -extern "C" { - pub fn repo_write_stdkeyfilter( - repo: *mut Repo, - key: *mut Repokey, - kfdata: *mut libc::c_void, - ) -> libc::c_int; -} -extern "C" { - pub fn repo_write_filtered( - repo: *mut Repo, - fp: *mut FILE, - keyfilter: ::std::option::Option< - unsafe extern "C" fn( - repo: *mut Repo, - key: *mut Repokey, - kfdata: *mut libc::c_void, - ) -> libc::c_int, - >, - kfdata: *mut libc::c_void, - keyq: *mut Queue, - ) -> libc::c_int; -} -extern "C" { - pub fn repodata_write_filtered( - data: *mut Repodata, - fp: *mut FILE, - keyfilter: ::std::option::Option< - unsafe extern "C" fn( - repo: *mut Repo, - key: *mut Repokey, - kfdata: *mut libc::c_void, - ) -> libc::c_int, - >, - kfdata: *mut libc::c_void, - keyq: *mut Queue, - ) -> libc::c_int; -} -extern "C" { - pub fn repo_add_conda(repo: *mut Repo, fp: *mut FILE, flags: libc::c_int) -> libc::c_int; -} diff --git a/crates/rattler_solve/src/libsolv/wrapper/flags.rs b/crates/rattler_solve/src/libsolv/wrapper/flags.rs deleted file mode 100644 index a908a65a3..000000000 --- a/crates/rattler_solve/src/libsolv/wrapper/flags.rs +++ /dev/null @@ -1,18 +0,0 @@ -use super::ffi::{SOLVER_FLAG_ALLOW_DOWNGRADE, SOLVER_FLAG_ALLOW_UNINSTALL}; - -#[repr(transparent)] -pub struct SolverFlag(u32); - -impl SolverFlag { - pub fn allow_uninstall() -> SolverFlag { - SolverFlag(SOLVER_FLAG_ALLOW_UNINSTALL) - } - - pub fn allow_downgrade() -> SolverFlag { - SolverFlag(SOLVER_FLAG_ALLOW_DOWNGRADE) - } - - pub fn inner(self) -> i32 { - self.0 as i32 - } -} diff --git a/crates/rattler_solve/src/libsolv/wrapper/keys.rs b/crates/rattler_solve/src/libsolv/wrapper/keys.rs deleted file mode 100644 index 88650eab1..000000000 --- a/crates/rattler_solve/src/libsolv/wrapper/keys.rs +++ /dev/null @@ -1,16 +0,0 @@ -// The constants below are taken from -// https://github.com/openSUSE/libsolv/blob/67aaf74844c532129ec8d7c8a7be4209ee4ef78d/src/knownid.h -// -// We have only copied those that are interesting for rattler - -pub const SOLVABLE_LICENSE: &str = "solvable:license"; -pub const SOLVABLE_BUILDTIME: &str = "solvable:buildtime"; -pub const SOLVABLE_DOWNLOADSIZE: &str = "solvable:downloadsize"; -pub const SOLVABLE_CHECKSUM: &str = "solvable:checksum"; -pub const SOLVABLE_PKGID: &str = "solvable:pkgid"; -pub const SOLVABLE_BUILDFLAVOR: &str = "solvable:buildflavor"; -pub const SOLVABLE_BUILDVERSION: &str = "solvable:buildversion"; -pub const REPOKEY_TYPE_MD5: &str = "repokey:type:md5"; -pub const REPOKEY_TYPE_SHA256: &str = "repokey:type:sha256"; -pub const SOLVABLE_CONSTRAINS: &str = "solvable:constrains"; -pub const SOLVABLE_TRACK_FEATURES: &str = "solvable:track_features"; diff --git a/crates/rattler_solve/src/libsolv/wrapper/mod.rs b/crates/rattler_solve/src/libsolv/wrapper/mod.rs deleted file mode 100644 index f958750ef..000000000 --- a/crates/rattler_solve/src/libsolv/wrapper/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! This module provides a mostly safe wrapper around libsolv -//! -//! The design of libsolv makes it difficult to provide a fully-safe wrapper. One of the reasons is -//! that the library is full of doubly-linked data structures (e.g. Pool <-> Repo <-> Solvable), -//! which are the bane of Rust's ownership system. -//! -//! Some types, like [`queue::Queue`] can be modelled using ownership semantics, but we treat most -//! other types as interiorly mutable, meaning that you can perform write operations through shared -//! references (`&`). This works well as long as you mutate libsolv data through its FFI. If you -//! need to mutate libsolv data from Rust, e.g. when setting up solvables, you will need to obtain -//! a `&mut` references to the relevant data, taking special care to ensure there is no aliasing at -//! hand. - -pub mod ffi; -pub mod flags; -pub mod keys; -pub mod pool; -pub mod queue; -pub mod repo; -pub mod repodata; -pub mod solvable; -pub mod solve_goal; -pub mod solve_problem; -pub mod solver; -pub mod transaction; diff --git a/crates/rattler_solve/src/libsolv/wrapper/pool.rs b/crates/rattler_solve/src/libsolv/wrapper/pool.rs deleted file mode 100644 index 62e733c45..000000000 --- a/crates/rattler_solve/src/libsolv/wrapper/pool.rs +++ /dev/null @@ -1,341 +0,0 @@ -use super::{ffi, repo::Repo, solvable::SolvableId, solver::Solver}; -use crate::libsolv::c_string; -use crate::libsolv::wrapper::ffi::Id; -use rattler_conda_types::MatchSpec; -use std::ffi::c_char; -use std::{ - convert::TryInto, - ffi::{CStr, CString}, - os::raw::c_void, - ptr::NonNull, -}; - -/// The type of distribution that the pool is being used for -/// Note: rattler only supports conda -#[repr(u32)] -pub enum DistType { - Rpm = ffi::DISTTYPE_RPM, - Debian = ffi::DISTTYPE_DEB, - Arch = ffi::DISTTYPE_ARCH, - Haiku = ffi::DISTTYPE_HAIKU, - Conda = ffi::DISTTYPE_CONDA, -} - -/// Wrapper for libsolv Pool, the interning datastructure used by libsolv -/// -/// The wrapper functions as an owned pointer, guaranteed to be non-null and freed -/// when the Pool is dropped -/// -/// Additional note: interning reduces memory usage by only storing unique instances of the provided -/// data, which then share the same `Id`. This `Pool` releases memory when explicitly asked and upon -/// destruction -#[repr(transparent)] -pub struct Pool(NonNull); - -impl Default for Pool { - fn default() -> Self { - let pool_ptr = unsafe { ffi::pool_create() }; - let self_obj = Self(NonNull::new(pool_ptr).expect("pool_create returned null")); - self_obj.set_disttype(DistType::Conda); - self_obj - } -} - -/// Destroy c side of things when pool is dropped -impl Drop for Pool { - fn drop(&mut self) { - // Safe because we know that the pool is never freed manually - unsafe { - // Free the registered Rust callback, if present - let ptr = (*self.raw_ptr()).debugcallbackdata; - if !ptr.is_null() { - let _: Box = Box::from_raw(ptr as *mut _); - } - - // Free the pool itself - ffi::pool_free(self.0.as_mut()); - } - } -} - -/// A boxed closure used for log callbacks -type BoxedLogCallback = Box; - -/// The callback that is actually registered on the pool (it must be a function pointer) -#[no_mangle] -extern "C" fn log_callback( - _pool: *mut ffi::Pool, - user_data: *mut c_void, - flags: i32, - str: *const c_char, -) { - unsafe { - // We have previously stored a `BoxedLogCallback` in `user_data`, so now we can retrieve it - // and run it - let closure: &mut BoxedLogCallback = &mut *(user_data as *mut BoxedLogCallback); - let str = CStr::from_ptr(str); - closure(str.to_str().expect("utf-8 error"), flags); - } -} - -/// Logging verbosity for libsolv -pub enum Verbosity { - None, - Low, - Medium, - Extreme, -} - -impl Pool { - /// Returns a raw pointer to the wrapped `ffi::Pool`, to be used for calling ffi functions - /// that require access to the pool (and for nothing else) - pub(super) fn raw_ptr(&self) -> *mut ffi::Pool { - self.0.as_ptr() - } - - /// Returns a reference to the wrapped `ffi::Pool`. - pub fn as_ref(&self) -> &ffi::Pool { - // Safe because the pool is guaranteed to exist until it is dropped - unsafe { self.0.as_ref() } - } - - /// Swaps two solvables inside the libsolv pool - pub fn swap_solvables(&self, s1: SolvableId, s2: SolvableId) { - let pool = self.as_ref(); - - // Safe because `pool.solvables` is the start of an array of length `pool.nsolvables` - let solvables = - unsafe { std::slice::from_raw_parts_mut(pool.solvables, pool.nsolvables as _) }; - - solvables.swap(s1.0 as _, s2.0 as _); - } - - /// Interns a REL_EQ relation between `id1` and `id2` - pub fn rel_eq(&self, id1: Id, id2: Id) -> Id { - unsafe { ffi::pool_rel2id(self.raw_ptr(), id1, id2, ffi::REL_EQ as i32, 1) } - } - - /// Interns the provided matchspec - pub fn conda_matchspec(&self, matchspec: &CStr) -> Id { - unsafe { ffi::pool_conda_matchspec(self.raw_ptr(), matchspec.as_ptr()) } - } - - /// Add debug callback to the pool - pub fn set_debug_callback(&self, callback: F) { - let box_callback: Box = Box::new(Box::new(callback)); - unsafe { - // Sets the debug callback into the pool - // Double box because file because the Box is a fat pointer and have a different - // size compared to c_void - ffi::pool_setdebugcallback( - self.raw_ptr(), - Some(log_callback), - Box::into_raw(box_callback) as *mut _, - ); - } - } - - /// Set debug level for libsolv - pub fn set_debug_level(&self, verbosity: Verbosity) { - let verbosity: libc::c_int = match verbosity { - Verbosity::None => 0, - Verbosity::Low => 1, - Verbosity::Medium => 2, - Verbosity::Extreme => 3, - }; - unsafe { - ffi::pool_setdebuglevel(self.raw_ptr(), verbosity); - } - } - - /// Set the provided repo to be considered as a source of installed packages - /// - /// Panics if the repo does not belong to this pool - pub fn set_installed(&self, repo: &Repo) { - repo.ensure_belongs_to_pool(self); - unsafe { ffi::pool_set_installed(self.raw_ptr(), repo.raw_ptr()) } - } - - /// Set the disttype for this pool. This is used to determine how to interpret the - /// fields of a solvable. Note that rattler currently only supports the conda disttype. - pub fn set_disttype(&self, disttype: DistType) { - unsafe { ffi::pool_setdisttype(self.raw_ptr(), disttype as i32) }; - } - - /// Create the solver - pub fn create_solver(&self) -> Solver { - let solver = NonNull::new(unsafe { ffi::solver_create(self.raw_ptr()) }) - .expect("solver_create returned a nullptr"); - - // Safe because we know the solver ptr is valid - unsafe { Solver::new(self, solver) } - } - - /// Create the whatprovides on the pool which is needed for solving - pub fn create_whatprovides(&self) { - unsafe { - ffi::pool_createwhatprovides(self.raw_ptr()); - } - } - - pub fn intern_matchspec(&self, match_spec: &MatchSpec) -> MatchSpecId { - let c_str = c_string(match_spec.to_string()); - unsafe { MatchSpecId(ffi::pool_conda_matchspec(self.raw_ptr(), c_str.as_ptr())) } - } - - /// Interns string like types into a `Pool` returning a `StringId` - pub fn intern_str>>(&self, str: T) -> StringId { - let c_str = CString::new(str).expect("the provided string contained a NUL byte"); - let length = c_str.as_bytes().len(); - let c_str = c_str.as_c_str(); - - // Safe because the function accepts any string - unsafe { - StringId(ffi::pool_strn2id( - self.raw_ptr(), - c_str.as_ptr(), - length.try_into().expect("string too large"), - 1, - )) - } - } - - /// Finds a previously interned string or returns `None` if it wasn't found - pub fn find_interned_str>(&self, str: T) -> Option { - let c_str = CString::new(str.as_ref()).expect("the provided string contained a NUL byte"); - let length = c_str.as_bytes().len(); - let c_str = c_str.as_c_str(); - - // Safe because the function accepts any string - unsafe { - let id = ffi::pool_strn2id( - self.raw_ptr(), - c_str.as_ptr(), - length.try_into().expect("string too large"), - 0, - ); - (id != 0).then_some(StringId(id)) - } - } - - /// Returns a string describing the last error associated to this pool, or "no error" if there - /// were no errors - pub fn last_error(&self) -> String { - // Safe, because `pool_errstr` is guaranteed to return a valid string even in the absence - // of errors - let err = unsafe { CStr::from_ptr(ffi::pool_errstr(self.raw_ptr())) }; - err.to_string_lossy().into_owned() - } -} - -/// Wrapper for the StringId of libsolv -#[derive(Copy, Clone)] -pub struct StringId(pub(super) Id); - -impl StringId { - /// Resolves the id to the interned string, if present in the pool - /// - /// Note: string ids are basically indexes in an array, so using a [`StringId`] from one pool in - /// a different one will either return `None` (if the id can't be found) or it will return - /// whatever string is found at the index - pub fn resolve<'a>(&self, pool: &'a Pool) -> Option<&'a str> { - if self.0 < pool.as_ref().ss.nstrings { - // Safe because we know the string is in the pool - let c_str = unsafe { ffi::pool_id2str(pool.0.as_ptr(), self.0) }; - let c_str = unsafe { CStr::from_ptr(c_str) } - .to_str() - .expect("utf-8 parse error"); - Some(c_str) - } else { - None - } - } -} - -impl From for Id { - fn from(id: StringId) -> Self { - id.0 - } -} - -/// Wrapper for the StringId of libsolv -#[derive(Copy, Clone)] -pub struct MatchSpecId(Id); - -/// Conversion to [`Id`] -impl From for Id { - fn from(id: MatchSpecId) -> Self { - id.0 - } -} - -#[cfg(test)] -mod test { - use std::{ffi::CString, str::FromStr}; - - use super::super::pool::Pool; - use rattler_conda_types::MatchSpec; - - #[test] - fn test_pool_string_interning() { - let pool = Pool::default(); - let pool2 = Pool::default(); - let to_intern = "foobar"; - // Intern the string - let id = pool.intern_str(to_intern); - // Get it back - let outcome = id.resolve(&pool); - assert_eq!(to_intern, outcome.unwrap()); - - let outcome2 = id.resolve(&pool2); - assert!(outcome2.is_none()); - } - - #[test] - fn test_pool_string_interning_utf8() { - // Some interesting utf-8 strings to test - let strings = [ - "いろはにほへとちりぬるを - わかよたれそつねならむ - うゐのおくやまけふこえて - あさきゆめみしゑひもせす", - "イロハニホヘト チリヌルヲ ワカヨタレソ ツネナラム ウヰノオクヤマ ケフコエテ アサキユメミシ ヱヒモセスン", - "Pchnąć w tę łódź jeża lub ośm skrzyń fig", - "В чащах юга жил бы цитрус? Да, но фальшивый экземпляр!", - "Съешь же ещё этих мягких французских булок да выпей чаю"]; - - let pool = Pool::default(); - for in_s in strings { - let id = pool.intern_str(in_s); - let outcome = id.resolve(&pool); - assert_eq!(in_s, outcome.unwrap()); - } - } - - #[test] - fn test_matchspec_interning() { - // Create a matchspec - let spec = MatchSpec::from_str("foo=1.0=py27_0").unwrap(); - // Intern it into the pool - let pool = Pool::default(); - pool.intern_matchspec(&spec); - // Don't think libsolv has an API to get it back - } - - #[test] - fn test_pool_callback() { - let pool = Pool::default(); - let (tx, rx) = std::sync::mpsc::sync_channel(10); - // Set the debug level - pool.set_debug_level(super::Verbosity::Extreme); - pool.set_debug_callback(move |msg, _level| { - tx.send(msg.to_owned()).unwrap(); - }); - - // Log something in the pool - let msg = CString::new("foo").unwrap(); - unsafe { super::ffi::pool_debug(pool.0.as_ptr(), 1 << 5, msg.as_ptr()) }; - - assert_eq!(rx.recv().unwrap(), "foo"); - } -} diff --git a/crates/rattler_solve/src/libsolv/wrapper/queue.rs b/crates/rattler_solve/src/libsolv/wrapper/queue.rs deleted file mode 100644 index fc5eacb67..000000000 --- a/crates/rattler_solve/src/libsolv/wrapper/queue.rs +++ /dev/null @@ -1,95 +0,0 @@ -use super::ffi; -use crate::libsolv::wrapper::solvable::SolvableId; -use std::marker::PhantomData; - -/// Wrapper for libsolv queue type. This type is used by to gather items of a specific type. This -/// is a type-safe implementation that is coupled to a specific Id type. -pub struct Queue { - queue: ffi::Queue, - // Makes this queue typesafe - _data: PhantomData, -} - -impl Default for Queue { - fn default() -> Self { - let mut queue = ffi::Queue { - elements: std::ptr::null_mut(), - count: 0, - alloc: std::ptr::null_mut(), - left: 0, - }; - - // Create the queue - unsafe { ffi::queue_init(&mut queue as *mut ffi::Queue) }; - - Self { - queue, - _data: PhantomData, - } - } -} - -impl Drop for Queue { - fn drop(&mut self) { - // Safe because we know that the queue is never freed manually - unsafe { - ffi::queue_free(self.raw_ptr()); - } - } -} - -impl Queue { - /// Returns a raw pointer to the wrapped `ffi::Repo`, to be used for calling ffi functions - /// that require access to the repo (and for nothing else) - pub(super) fn raw_ptr(&mut self) -> *mut ffi::Queue { - &mut self.queue as *mut ffi::Queue - } -} - -impl> Queue { - /// Pushes a single id to the back of the queue - #[allow(dead_code)] - pub fn push_id(&mut self, id: T) { - unsafe { - ffi::queue_insert(self.raw_ptr(), self.queue.count, id.into()); - } - } - - /// Returns an iterator over the ids of the queue - pub fn id_iter(&self) -> impl Iterator + '_ { - unsafe { std::slice::from_raw_parts(self.queue.elements as _, self.queue.count as usize) } - .iter() - .copied() - } -} - -/// A read-only reference to a libsolv queue -pub struct QueueRef<'queue>(ffi::Queue, PhantomData<&'queue ffi::Queue>); - -impl QueueRef<'_> { - /// Construct a new `QueueRef` based on the provided `ffi::Queue` - /// - /// Safety: the queue must not have been freed - pub(super) unsafe fn from_ffi_queue(_source: &T, queue: ffi::Queue) -> QueueRef { - QueueRef(queue, PhantomData::default()) - } - - /// Returns an iterator over the ids of the queue - pub fn iter(&self) -> impl Iterator + '_ { - // Safe to dereference, because we are doing so within the bounds of count - (0..self.0.count as usize).map(|index| { - let id = unsafe { *self.0.elements.add(index) }; - SolvableId(id) - }) - } -} - -#[cfg(test)] -mod test { - use super::{super::pool::StringId, Queue}; - - #[test] - fn create_queue() { - let _queue = Queue::::default(); - } -} diff --git a/crates/rattler_solve/src/libsolv/wrapper/repo.rs b/crates/rattler_solve/src/libsolv/wrapper/repo.rs deleted file mode 100644 index 394bcf52c..000000000 --- a/crates/rattler_solve/src/libsolv/wrapper/repo.rs +++ /dev/null @@ -1,179 +0,0 @@ -use super::repodata::Repodata; -use super::{ffi, pool::Pool, solvable::SolvableId}; -use crate::libsolv::c_string; -use std::{marker::PhantomData, ptr::NonNull}; - -/// Wrapper for libsolv repo, which contains package information (in our case, we are creating repos -/// from `repodata.json`, installed package metadata and virtual packages) -/// -/// The wrapper functions as an owned pointer, guaranteed to be non-null and freed -/// when the Repo is dropped. Next to that, it is also tied to the lifetime of the pool that created -/// it, because libsolv requires a pool to outlive its repos. -pub struct Repo<'pool>(NonNull, PhantomData<&'pool ffi::Repo>); - -/// An Id to uniquely identify a Repo -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct RepoId(usize); - -impl RepoId { - pub fn from_ffi_solvable(solvable: &ffi::Solvable) -> RepoId { - RepoId(solvable.repo as usize) - } -} - -impl<'pool> Drop for Repo<'pool> { - fn drop(&mut self) { - // Safe because we know that the repo is never freed manually - unsafe { ffi::repo_free(self.0.as_mut(), 1) } - } -} - -impl<'pool> Repo<'pool> { - /// Constructs a repo in the provided pool, associated to the given url - pub fn new(pool: &Pool, url: impl AsRef) -> Repo { - let c_url = c_string(url); - - unsafe { - let repo_ptr = ffi::repo_create(pool.raw_ptr(), c_url.as_ptr()); - let non_null_ptr = NonNull::new(repo_ptr).expect("repo ptr was null"); - Repo(non_null_ptr, PhantomData::default()) - } - } - - /// Panics if this repo does not belong to the provided pool - pub fn ensure_belongs_to_pool(&self, pool: &Pool) { - let repo_pool_ptr = unsafe { self.0.as_ref().pool }; - assert_eq!( - repo_pool_ptr, - pool.raw_ptr(), - "repo does not belong to the provided pool" - ); - } - - /// Returns the id of the Repo - pub fn id(&self) -> RepoId { - RepoId(self.raw_ptr() as usize) - } - - /// Returns a raw pointer to the wrapped `ffi::Repo`, to be used for calling ffi functions - /// that require access to the repo (and for nothing else) - pub fn raw_ptr(&self) -> *mut ffi::Repo { - self.0.as_ptr() - } - - /// Adds a new repodata to this repo (repodata is a libsolv datastructure, see [`Repodata`] for - /// details) - pub fn add_repodata(&self) -> Repodata { - unsafe { - let repodata_ptr = ffi::repo_add_repodata(self.raw_ptr(), 0); - Repodata::from_ptr(self, repodata_ptr) - } - } - - /// Adds a `.solv` file to the repo - pub fn add_solv(&self, pool: &Pool, file: *mut libc::FILE) { - let result = unsafe { ffi::repo_add_solv(self.raw_ptr(), file as *mut ffi::FILE, 0) }; - if result != 0 { - panic!("add_solv failed: {}", pool.last_error()); - } - } - - /// Adds a new solvable to this repo - pub fn add_solvable(&self) -> SolvableId { - SolvableId(unsafe { ffi::repo_add_solvable(self.raw_ptr()) }) - } - - /// Adds a new "requires" relation to the solvable - pub fn add_requires(&self, solvable: &mut ffi::Solvable, rel_id: ffi::Id) { - solvable.requires = - unsafe { ffi::repo_addid_dep(self.raw_ptr(), solvable.requires, rel_id, 0) }; - } - - /// Adds a new "provides" relation to the solvable - pub fn add_provides(&self, solvable: &mut ffi::Solvable, rel_id: ffi::Id) { - solvable.provides = - unsafe { ffi::repo_addid_dep(self.raw_ptr(), solvable.provides, rel_id, 0) }; - } - - /// Serializes the current repo as a `.solv` file - /// - /// The provided file should have been opened with write access. Closing the file is the - /// responsibility of the caller. - pub fn write(&self, pool: &Pool, file: *mut libc::FILE) { - let result = unsafe { ffi::repo_write(self.raw_ptr(), file as *mut ffi::FILE) }; - if result != 0 { - panic!("repo_write failed: {}", pool.last_error()); - } - } - - /// Wrapper around `repo_internalize` - pub fn internalize(&self) { - unsafe { ffi::repo_internalize(self.raw_ptr()) } - } - - /// Frees the solvable - /// - /// The caller must ensure the solvable referenced by this id will not be used in the future - pub unsafe fn free_solvable(&self, solvable_id: SolvableId) { - ffi::repo_free_solvable(self.raw_ptr(), solvable_id.into(), 1); - } -} - -#[cfg(test)] -mod tests { - use super::super::pool::Pool; - use super::Repo; - use crate::libsolv::c_string; - use crate::libsolv::wrapper::pool::StringId; - - #[test] - fn test_repo_creation() { - let pool = Pool::default(); - let mut _repo = Repo::new(&pool, "conda-forge"); - } - - #[test] - fn test_repo_solv_roundtrip() { - let dir = tempfile::tempdir().unwrap(); - let solv_path = dir.path().join("repo.solv").to_string_lossy().into_owned(); - let solv_path = c_string(solv_path); - - { - // Create a pool and a repo - let pool = Pool::default(); - let repo = Repo::new(&pool, "conda-forge"); - - // Add a solvable with a particular name - let solvable_id = repo.add_solvable(); - let solvable = unsafe { solvable_id.resolve_raw(&pool).as_mut() }; - solvable.name = pool.intern_str("dummy-solvable").into(); - - // Open and write the .solv file - let mode = c_string("wb"); - let file = unsafe { libc::fopen(solv_path.as_ptr(), mode.as_ptr()) }; - repo.write(&pool, file); - unsafe { libc::fclose(file) }; - } - - // Create a clean pool and repo - let pool = Pool::default(); - let repo = Repo::new(&pool, "conda-forge"); - - // Open and read the .solv file - let mode = c_string("rb"); - let file = unsafe { libc::fopen(solv_path.as_ptr(), mode.as_ptr()) }; - repo.add_solv(&pool, file); - unsafe { libc::fclose(file) }; - - // Check that everything was properly loaded - let repo = unsafe { repo.0.as_ref() }; - assert_eq!(repo.nsolvables, 1); - - let ffi_pool = pool.as_ref(); - - // Somehow there are already 2 solvables in the pool, so we check at the third position - let solvable = unsafe { *ffi_pool.solvables.offset(2) }; - let name = StringId(solvable.name).resolve(&pool).unwrap(); - assert_eq!(name, "dummy-solvable"); - } -} diff --git a/crates/rattler_solve/src/libsolv/wrapper/repodata.rs b/crates/rattler_solve/src/libsolv/wrapper/repodata.rs deleted file mode 100644 index 62c434330..000000000 --- a/crates/rattler_solve/src/libsolv/wrapper/repodata.rs +++ /dev/null @@ -1,110 +0,0 @@ -use super::ffi; -use crate::libsolv::wrapper::pool::StringId; -use crate::libsolv::wrapper::repo::Repo; -use crate::libsolv::wrapper::solvable::SolvableId; -use std::ffi::CStr; -use std::marker::PhantomData; -use std::ptr::NonNull; - -/// Wrapper for libsolv repodata, which provides functions to manipulate solvables -/// -/// The wrapper functions as a borrowed pointer, guaranteed to be non-null and living at least as -/// long as the `Repo` it originates from -pub struct Repodata<'repo>(NonNull, PhantomData<&'repo Repo<'repo>>); - -impl Repodata<'_> { - /// Constructs a new repodata from the provided libsolv pointer. The function will ensure that - /// the pointer is non-null, but the caller must ensure the pointer is actually valid - pub(super) unsafe fn from_ptr<'a>( - _repo: &'a Repo<'a>, - ptr: *mut ffi::Repodata, - ) -> Repodata<'a> { - Repodata( - NonNull::new(ptr).expect("repodata ptr was null"), - PhantomData::default(), - ) - } - - /// Returns a raw pointer to the wrapped `ffi::Repodata`, to be used for calling ffi functions - /// that require access to the repodata (and for nothing else) - pub(super) fn raw_ptr(&self) -> *mut ffi::Repodata { - self.0.as_ptr() - } - - /// Calls repodata_set_checksum - pub fn set_checksum( - &self, - solvable_id: SolvableId, - key: StringId, - checksum_type: StringId, - value: &CStr, - ) { - unsafe { - ffi::repodata_set_checksum( - self.raw_ptr(), - solvable_id.into(), - key.into(), - checksum_type.into(), - value.as_ptr(), - ) - } - } - - /// Calls repodata_set_location - pub fn set_location(&self, solvable_id: SolvableId, dir: &CStr, file: &CStr) { - unsafe { - ffi::repodata_set_location( - self.raw_ptr(), - solvable_id.into(), - 0, - dir.as_ptr(), - file.as_ptr(), - ); - } - } - - /// Calls repodata_set_num - pub fn set_num(&self, solvable_id: SolvableId, key: StringId, value: u64) { - unsafe { - ffi::repodata_set_num(self.raw_ptr(), solvable_id.into(), key.into(), value); - } - } - - /// Calls repodata_set_str - pub fn set_str(&self, solvable_id: SolvableId, key: StringId, value: &CStr) { - unsafe { - ffi::repodata_set_str( - self.raw_ptr(), - solvable_id.into(), - key.into(), - value.as_ptr(), - ) - } - } - - /// Calls repodata_add_idarray - pub fn add_idarray(&self, solvable_id: SolvableId, array_key: StringId, id: ffi::Id) { - unsafe { - ffi::repodata_add_idarray(self.raw_ptr(), solvable_id.into(), array_key.into(), id); - } - } - - /// Calls repodata_add_poolstr_array - pub fn add_poolstr_array(&self, solvable_id: SolvableId, key: StringId, value: &CStr) { - unsafe { - ffi::repodata_add_poolstr_array( - self.raw_ptr(), - solvable_id.into(), - key.into(), - value.as_ptr(), - ) - }; - } - - /// Calls repodata_swap_attrs - pub fn swap_attrs(&self, s1: SolvableId, s2: SolvableId) { - unsafe { - ffi::repodata_swap_attrs(self.raw_ptr(), s1.into(), s2.into()); - } - } -} diff --git a/crates/rattler_solve/src/libsolv/wrapper/solvable.rs b/crates/rattler_solve/src/libsolv/wrapper/solvable.rs deleted file mode 100644 index d086cb4b1..000000000 --- a/crates/rattler_solve/src/libsolv/wrapper/solvable.rs +++ /dev/null @@ -1,41 +0,0 @@ -use super::ffi; -use super::pool::{Pool, StringId}; -use std::ptr::NonNull; - -/// Represents a solvable in a [`Repo`] or [`Pool`] -#[derive(Copy, Clone, Debug)] -pub struct SolvableId(pub(super) ffi::Id); - -impl From for ffi::Id { - fn from(s: SolvableId) -> Self { - s.0 - } -} - -impl SolvableId { - /// Resolves the id to a pointer to the solvable - /// - /// Panics if the solvable is not found in the pool - pub fn resolve_raw(self, pool: &Pool) -> NonNull { - let pool = pool.as_ref(); - - // Internally, the id is just an offset to be applied on top of `pool.solvables` - if self.0 < pool.nsolvables { - // Safe because we just checked the offset is within bounds - let solvable_ptr = unsafe { pool.solvables.offset(self.0 as isize) }; - NonNull::new(solvable_ptr).expect("solvable ptr was null") - } else { - panic!("invalid solvable id!") - } - } -} - -/// Gets a number associated to this solvable -pub fn lookup_num(solvable: *mut ffi::Solvable, key: StringId) -> Option { - let value = unsafe { ffi::solvable_lookup_num(solvable as *mut _, key.0, u64::MAX) }; - if value == u64::MAX { - None - } else { - Some(value) - } -} diff --git a/crates/rattler_solve/src/libsolv/wrapper/solve_goal.rs b/crates/rattler_solve/src/libsolv/wrapper/solve_goal.rs deleted file mode 100644 index a308f807a..000000000 --- a/crates/rattler_solve/src/libsolv/wrapper/solve_goal.rs +++ /dev/null @@ -1,97 +0,0 @@ -use super::ffi; -use crate::libsolv::wrapper::ffi::{ - SOLVER_DISFAVOR, SOLVER_ERASE, SOLVER_FAVOR, SOLVER_INSTALL, SOLVER_LOCK, SOLVER_SOLVABLE, - SOLVER_SOLVABLE_PROVIDES, SOLVER_UPDATE, SOLVER_WEAK, -}; -use crate::libsolv::wrapper::pool::MatchSpecId; -use crate::libsolv::wrapper::solvable::SolvableId; -use std::os::raw::c_int; - -/// Wrapper for libsolv queue type that stores jobs for the solver. This type provides a safe -/// wrapper around a goal state for libsolv. -pub struct SolveGoal { - queue: ffi::Queue, -} - -impl Default for SolveGoal { - fn default() -> Self { - // Safe because we know for a fact that the queue exists - unsafe { - // Create a queue pointer and initialize it - let mut queue = ffi::Queue { - elements: std::ptr::null_mut(), - count: 0, - alloc: std::ptr::null_mut(), - left: 0, - }; - // This initializes some internal libsolv stuff - ffi::queue_init(&mut queue as *mut ffi::Queue); - Self { queue } - } - } -} - -/// This drop implementation drops the internal libsolv queue -impl Drop for SolveGoal { - fn drop(&mut self) { - // Safe because we know that the queue is never freed manually - unsafe { - ffi::queue_free(self.raw_ptr()); - } - } -} - -impl SolveGoal { - /// Returns a raw pointer to the wrapped `ffi::Repo`, to be used for calling ffi functions - /// that require access to the repo (and for nothing else) - pub(super) fn raw_ptr(&mut self) -> *mut ffi::Queue { - &mut self.queue as *mut ffi::Queue - } -} - -impl SolveGoal { - /// The specified spec must be installed - pub fn install(&mut self, match_spec: MatchSpecId, optional: bool) { - let action = if optional { - SOLVER_INSTALL | SOLVER_WEAK - } else { - SOLVER_INSTALL - }; - self.push_id_with_flags(match_spec, action | SOLVER_SOLVABLE_PROVIDES); - } - - /// The specified spec must not be installed. - pub fn erase(&mut self, match_spec: MatchSpecId) { - self.push_id_with_flags(match_spec, SOLVER_ERASE | SOLVER_SOLVABLE_PROVIDES); - } - - /// The highest possible spec must be installed - pub fn update(&mut self, match_spec: MatchSpecId) { - self.push_id_with_flags(match_spec, SOLVER_UPDATE | SOLVER_SOLVABLE_PROVIDES); - } - - /// Favor the specified solvable over other variants. This doesnt mean this variant will be - /// used. To guarantee a solvable is used (if selected) use the `Self::lock` function. - pub fn favor(&mut self, solvable: SolvableId) { - self.push_id_with_flags(solvable, SOLVER_SOLVABLE | SOLVER_FAVOR); - } - - /// Lock the specified solvable over other variants. This implies that not other variant will - /// ever be considered. - pub fn lock(&mut self, solvable: SolvableId) { - self.push_id_with_flags(solvable, SOLVER_SOLVABLE | SOLVER_LOCK); - } - - /// Disfavor the specified variant over other variants. This does not mean it will never be - /// selected, but other variants are considered first. - pub fn disfavor(&mut self, solvable: SolvableId) { - self.push_id_with_flags(solvable, SOLVER_SOLVABLE | SOLVER_DISFAVOR); - } - - /// Push and id and flag into the queue - fn push_id_with_flags(&mut self, id: impl Into, flags: u32) { - unsafe { - ffi::queue_insert2(self.raw_ptr(), self.queue.count, flags as c_int, id.into()); - } - } -} diff --git a/crates/rattler_solve/src/libsolv/wrapper/solve_problem.rs b/crates/rattler_solve/src/libsolv/wrapper/solve_problem.rs deleted file mode 100644 index 63849cd31..000000000 --- a/crates/rattler_solve/src/libsolv/wrapper/solve_problem.rs +++ /dev/null @@ -1,108 +0,0 @@ -use super::ffi; -use super::solvable::SolvableId; - -use ffi::{ - SolverRuleinfo_SOLVER_RULE_JOB as SOLVER_RULE_JOB, - SolverRuleinfo_SOLVER_RULE_JOB_NOTHING_PROVIDES_DEP as SOLVER_RULE_JOB_NOTHING_PROVIDES_DEP, - SolverRuleinfo_SOLVER_RULE_JOB_UNKNOWN_PACKAGE as SOLVER_RULE_JOB_UNKNOWN_PACKAGE, - SolverRuleinfo_SOLVER_RULE_PKG as SOLVER_RULE_PKG, - SolverRuleinfo_SOLVER_RULE_PKG_CONFLICTS as SOLVER_RULE_SOLVER_RULE_PKG_CONFLICTS, - SolverRuleinfo_SOLVER_RULE_PKG_CONSTRAINS as SOLVER_RULE_PKG_CONSTRAINS, - SolverRuleinfo_SOLVER_RULE_PKG_NOTHING_PROVIDES_DEP as SOLVER_RULE_SOLVER_RULE_PKG_NOTHING_PROVIDES_DEP, - SolverRuleinfo_SOLVER_RULE_PKG_REQUIRES as SOLVER_RULE_PKG_REQUIRES, - SolverRuleinfo_SOLVER_RULE_PKG_SAME_NAME as SOLVER_RULE_SOLVER_RULE_PKG_SAME_NAME, - SolverRuleinfo_SOLVER_RULE_UPDATE as SOLVER_RULE_SOLVER_RULE_UPDATE, -}; - -#[derive(Debug)] -pub enum SolveProblem { - /// A top level requirement. - /// The difference between JOB and PKG is unknown (possibly unused). - Job { dep: String }, - /// A top level dependency does not exist. - /// Could be a wrong name or missing channel. - JobNothingProvidesDep { dep: String }, - /// A top level dependency does not exist. - /// Could be a wrong name or missing channel. - JobUnknownPackage { dep: String }, - /// A top level requirement. - /// The difference between JOB and PKG is unknown (possibly unused). - Pkg { dep: String }, - /// Looking for a valid solution to the installation satisfiability expand to - /// two solvables of same package that cannot be installed together. This is - /// a partial exaplanation of why one of the solvables (could be any of the - /// parent) cannot be installed. - PkgConflicts { - source: SolvableId, - target: SolvableId, - }, - /// A constraint (run_constrained) on source is conflicting with target. - /// SOLVER_RULE_PKG_CONSTRAINS has a dep, but it can resolve to nothing. - /// The constraint conflict is actually expressed between the target and - /// a constrains node child of the source. - PkgConstrains { - source: SolvableId, - target: SolvableId, - dep: String, - }, - /// A package dependency does not exist. - /// Could be a wrong name or missing channel. - /// This is a partial exaplanation of why a specific solvable (could be any - /// of the parent) cannot be installed. - PkgNothingProvidesDep { source: SolvableId, dep: String }, - /// Express a dependency on source that is involved in explaining the - /// problem. - /// Not all dependency of package will appear, only enough to explain the - //. problem. It is not a problem in itself, only a part of the graph. - PkgRequires { source: SolvableId, dep: String }, - /// Package conflict between two solvables of same package name (handled the same as - /// [`SolveProblem::PkgConflicts`]). - PkgSameName { - source: SolvableId, - target: SolvableId, - }, - /// Encounterd in the problems list from libsolv but unknown. - /// Explicitly ignored until we do something with it. - Update, -} - -impl SolveProblem { - pub fn from_raw( - problem_type: ffi::SolverRuleinfo, - dep: Option, - source: Option, - target: Option, - ) -> Self { - match problem_type { - SOLVER_RULE_JOB => Self::Job { dep: dep.unwrap() }, - SOLVER_RULE_JOB_NOTHING_PROVIDES_DEP => { - Self::JobNothingProvidesDep { dep: dep.unwrap() } - } - SOLVER_RULE_JOB_UNKNOWN_PACKAGE => Self::JobUnknownPackage { dep: dep.unwrap() }, - SOLVER_RULE_PKG => Self::Pkg { dep: dep.unwrap() }, - SOLVER_RULE_SOLVER_RULE_PKG_CONFLICTS => Self::PkgConflicts { - source: source.unwrap(), - target: target.unwrap(), - }, - SOLVER_RULE_PKG_CONSTRAINS => Self::PkgConstrains { - source: source.unwrap(), - target: target.unwrap(), - dep: dep.unwrap(), - }, - SOLVER_RULE_SOLVER_RULE_PKG_NOTHING_PROVIDES_DEP => Self::PkgNothingProvidesDep { - source: source.unwrap(), - dep: dep.unwrap(), - }, - SOLVER_RULE_PKG_REQUIRES => Self::PkgRequires { - source: source.unwrap(), - dep: dep.unwrap(), - }, - SOLVER_RULE_SOLVER_RULE_PKG_SAME_NAME => Self::PkgSameName { - source: source.unwrap(), - target: target.unwrap(), - }, - SOLVER_RULE_SOLVER_RULE_UPDATE => Self::Update, - _ => panic!("Unknown problem type: {}", problem_type), - } - } -} diff --git a/crates/rattler_solve/src/libsolv/wrapper/solver.rs b/crates/rattler_solve/src/libsolv/wrapper/solver.rs deleted file mode 100644 index 137d1f2a3..000000000 --- a/crates/rattler_solve/src/libsolv/wrapper/solver.rs +++ /dev/null @@ -1,159 +0,0 @@ -use std::ffi::CStr; -use std::marker::PhantomData; -use std::ptr::NonNull; - -use crate::libsolv::wrapper::pool::Pool; -use crate::libsolv::wrapper::solve_goal::SolveGoal; -use crate::libsolv::wrapper::solve_problem::SolveProblem; - -use super::ffi; -use super::flags::SolverFlag; -use super::queue::Queue; -use super::solvable::SolvableId; -use super::transaction::Transaction; - -/// Wrapper for libsolv solver, which is used to drive dependency resolution -/// -/// The wrapper functions as an owned pointer, guaranteed to be non-null and freed -/// when the Solver is dropped -pub struct Solver<'pool>(NonNull, PhantomData<&'pool Pool>); - -impl<'pool> Drop for Solver<'pool> { - fn drop(&mut self) { - unsafe { ffi::solver_free(self.0.as_mut()) } - } -} - -impl Solver<'_> { - /// Constructs a new Solver from the provided libsolv pointer. It is the responsibility of the - /// caller to ensure the pointer is actually valid. - pub(super) unsafe fn new(_pool: &Pool, ptr: NonNull) -> Solver { - Solver(ptr, PhantomData::default()) - } - - /// Returns a raw pointer to the wrapped `ffi::Solver`, to be used for calling ffi functions - /// that require access to the pool (and for nothing else) - fn raw_ptr(&self) -> *mut ffi::Solver { - self.0.as_ptr() - } - - /// Returns the amount of problems that are yet to be solved - fn problem_count(&self) -> u32 { - unsafe { ffi::solver_problem_count(self.raw_ptr()) } - } - - /// Returns a user-friendly representation of a problem - /// - /// Safety: the caller must ensure the id is valid - unsafe fn problem2str(&self, id: ffi::Id) -> &CStr { - let problem = ffi::solver_problem2str(self.raw_ptr(), id); - CStr::from_ptr(problem) - } - - /// Creates a string for each 'problem' that the solver still has which it encountered while - /// solving the matchspecs. Use this function to print the existing problems to string. - fn solver_problems(&self) -> Vec { - let mut output = Vec::new(); - - let count = self.problem_count(); - for i in 1..=count { - // Safe because the id valid (between [1, count]) - let problem = unsafe { self.problem2str(i as ffi::Id) }; - - output.push( - problem - .to_str() - .expect("string is invalid UTF8") - .to_string(), - ); - } - output - } - - pub fn all_solver_problems(&self) -> Vec { - let mut problems = Vec::new(); - let mut problem_rules = Queue::::default(); - - let count = self.problem_count(); - for i in 1..=count { - unsafe { - ffi::solver_findallproblemrules( - self.raw_ptr(), - i.try_into().unwrap(), - problem_rules.raw_ptr(), - ) - }; - for r in problem_rules.id_iter() { - if r != 0 { - let mut source_id = 0; - let mut target_id = 0; - let mut dep_id = 0; - - let problem_type = unsafe { - ffi::solver_ruleinfo( - self.raw_ptr(), - r, - &mut source_id, - &mut target_id, - &mut dep_id, - ) - }; - - let pool = unsafe { (*self.0.as_ptr()).pool as *mut ffi::Pool }; - - let nsolvables = unsafe { (*pool).nsolvables }; - - let target = if target_id < 0 || target_id >= nsolvables { - None - } else { - Some(SolvableId(target_id)) - }; - - let source = if source_id < 0 || source_id >= nsolvables { - None - } else { - Some(SolvableId(target_id)) - }; - - let dep = if dep_id == 0 { - None - } else { - let dep = unsafe { ffi::pool_dep2str(pool, dep_id) }; - let dep = unsafe { CStr::from_ptr(dep) }; - let dep = dep.to_str().expect("Invalid UTF8 value").to_string(); - Some(dep) - }; - - problems.push(SolveProblem::from_raw(problem_type, dep, source, target)); - } - } - } - problems - } - - /// Sets a solver flag - pub fn set_flag(&self, flag: SolverFlag, value: bool) { - unsafe { ffi::solver_set_flag(self.raw_ptr(), flag.inner(), i32::from(value)) }; - } - - /// Solves all the problems in the `queue` and returns a transaction from the found solution. - /// Returns an error if problems remain unsolved. - pub fn solve(&mut self, queue: &mut SolveGoal) -> Result> { - let result = unsafe { - // Run the solve method - ffi::solver_solve(self.raw_ptr(), queue.raw_ptr()); - // If there are no problems left then the solver is done - ffi::solver_problem_count(self.raw_ptr()) == 0 - }; - if result { - let transaction = - NonNull::new(unsafe { ffi::solver_create_transaction(self.raw_ptr()) }) - .expect("solver_create_transaction returned a nullptr"); - - // Safe because we know the `transaction` ptr is valid - Ok(unsafe { Transaction::new(self, transaction) }) - } else { - Err(self.solver_problems()) - } - } -} diff --git a/crates/rattler_solve/src/libsolv/wrapper/transaction.rs b/crates/rattler_solve/src/libsolv/wrapper/transaction.rs deleted file mode 100644 index d083ab49a..000000000 --- a/crates/rattler_solve/src/libsolv/wrapper/transaction.rs +++ /dev/null @@ -1,70 +0,0 @@ -use super::ffi; -use super::solvable::SolvableId; -use super::solver::Solver; -use crate::libsolv::wrapper::queue::QueueRef; -use std::marker::PhantomData; -use std::ptr::NonNull; - -/// Wrapper for [`ffi::Transaction`], which is an abstraction over changes that need to be -/// done to satisfy the dependency constraints -/// -/// The wrapper functions as an owned pointer, guaranteed to be non-null and freed -/// when the Transaction is dropped -pub struct Transaction<'solver>( - NonNull, - PhantomData<&'solver Solver<'solver>>, -); - -impl Drop for Transaction<'_> { - fn drop(&mut self) { - // Safe because we know that the transaction is never freed manually - unsafe { ffi::transaction_free(self.0.as_mut()) } - } -} - -impl Transaction<'_> { - /// Constructs a new Transaction from the provided libsolv pointer. It is the responsibility of the - /// caller to ensure the pointer is actually valid. - pub(super) unsafe fn new<'a>( - _solver: &'a Solver<'a>, - ptr: NonNull, - ) -> Transaction<'a> { - Transaction(ptr, PhantomData::default()) - } - - /// Returns a raw pointer to the wrapped `ffi::Transaction`, to be used for calling ffi functions - /// that require access to the repodata (and for nothing else) - fn raw_ptr(&self) -> *mut ffi::Transaction { - self.0.as_ptr() - } - - /// Returns a reference to the wrapped `ffi::Transaction`. - pub fn as_ref(&self) -> &ffi::Transaction { - unsafe { self.0.as_ref() } - } - - /// Returns the transaction type - pub fn transaction_type(&self, solvable_id: SolvableId) -> ffi::Id { - unsafe { - ffi::transaction_type( - self.raw_ptr(), - solvable_id.into(), - ffi::SOLVER_TRANSACTION_SHOW_ALL as std::os::raw::c_int, - ) - } - } - - /// Returns the second solvable associated to the transaction - /// - /// Safety: the caller must ensure the transaction has an associated solvable and that the - /// provided `solvable_id` is valid - pub unsafe fn obs_pkg(&self, solvable_id: SolvableId) -> SolvableId { - SolvableId(unsafe { ffi::transaction_obs_pkg(self.raw_ptr(), solvable_id.into()) }) - } - - /// Returns the transaction's queue, containing a solvable id for each transaction - pub fn get_steps(&self) -> QueueRef { - // Safe because the transaction is live and `transaction.steps` is a queue - unsafe { QueueRef::from_ffi_queue(self, self.as_ref().steps) } - } -} diff --git a/crates/rattler_solve/src/snapshots/rattler_solve__test_libsolv__solve_tensorboard.snap b/crates/rattler_solve/src/snapshots/rattler_solve__test_libsolv__solve_tensorboard.snap new file mode 100644 index 000000000..386a8fd34 --- /dev/null +++ b/crates/rattler_solve/src/snapshots/rattler_solve__test_libsolv__solve_tensorboard.snap @@ -0,0 +1,83 @@ +--- +source: crates/rattler_solve/src/lib.rs +expression: pkgs +--- +- _libgcc_mutex 0.1 conda_forge +- _openmp_mutex 4.5 2_gnu +- abseil-cpp 20210324.2 h9c3ff4c_0 +- absl-py 1.0.0 pyhd8ed1ab_0 +- aiohttp 3.8.1 py39hb9d737c_1 +- aiosignal 1.2.0 pyhd8ed1ab_0 +- async-timeout 4.0.2 pyhd8ed1ab_0 +- attrs 21.4.0 pyhd8ed1ab_0 +- blinker 1.4 py_1 +- brotlipy 0.7.0 py39hb9d737c_1004 +- bzip2 1.0.8 h7f98852_4 +- c-ares 1.18.1 h7f98852_0 +- ca-certificates 2022.6.15 ha878542_0 +- cachetools 5.0.0 pyhd8ed1ab_0 +- certifi 2022.6.15 py39hf3d152e_0 +- cffi 1.15.1 py39he91dace_0 +- charset-normalizer 2.0.11 pyhd8ed1ab_0 +- click 8.1.3 py39hf3d152e_0 +- cryptography 37.0.4 py39hd97740a_0 +- frozenlist 1.3.1 py39hb9d737c_0 +- google-auth 2.6.0 pyh6c4a22f_1 +- google-auth-oauthlib 0.4.1 py_2 +- grpc-cpp 1.39.1 h850795e_1 +- grpcio 1.46.3 py39h0f497a6_0 +- idna 3.3 pyhd8ed1ab_0 +- importlib-metadata 4.11.4 py39hf3d152e_0 +- ld_impl_linux-64 2.36.1 hea4e1c9_2 +- libblas 3.9.0 16_linux64_openblas +- libcblas 3.9.0 16_linux64_openblas +- libffi 3.4.2 h7f98852_5 +- libgcc-ng 12.1.0 h8d9b700_16 +- libgfortran-ng 12.1.0 h69a702a_16 +- libgfortran5 12.1.0 hdcd56e2_16 +- libgomp 12.1.0 h8d9b700_16 +- liblapack 3.9.0 16_linux64_openblas +- libnsl 2.0.0 h7f98852_0 +- libopenblas 0.3.21 pthreads_h78a6416_1 +- libprotobuf 3.16.0 h780b84a_0 +- libsqlite 3.39.2 h753d276_1 +- libstdcxx-ng 12.1.0 ha89aaad_16 +- libuuid 2.32.1 h7f98852_1000 +- libzlib 1.2.12 h166bdaf_2 +- markdown 3.3.6 pyhd8ed1ab_0 +- multidict 6.0.2 py39hb9d737c_1 +- ncurses 6.3 h27087fc_1 +- numpy 1.23.2 py39hba7629e_0 +- oauthlib 3.2.0 pyhd8ed1ab_0 +- openssl 1.1.1q h166bdaf_0 +- protobuf 3.16.0 py39he80948d_0 +- pyasn1 0.4.8 py_0 +- pyasn1-modules 0.2.7 py_0 +- pycparser 2.21 pyhd8ed1ab_0 +- pyjwt 2.3.0 pyhd8ed1ab_1 +- pyopenssl 22.0.0 pyhd8ed1ab_0 +- pysocks 1.7.1 py39hf3d152e_5 +- python 3.9.13 h9a8a25e_0_cpython +- python_abi 3.9 2_cp39 +- pyu2f 0.1.5 pyhd8ed1ab_0 +- re2 2021.09.01 h9c3ff4c_0 +- readline 8.1.2 h0f457ee_0 +- requests 2.27.1 pyhd8ed1ab_0 +- requests-oauthlib 1.3.1 pyhd8ed1ab_0 +- rsa 4.8 pyhd8ed1ab_0 +- setuptools 65.0.2 py39hf3d152e_0 +- six 1.16.0 pyh6c4a22f_0 +- sqlite 3.39.2 h4ff8645_1 +- tensorboard 2.1.1 py_1 +- tk 8.6.12 h27826a3_0 +- typing-extensions 4.0.1 hd8ed1ab_0 +- typing_extensions 4.0.1 pyha770c72_0 +- tzdata 2021e he74cb21_0 +- urllib3 1.26.8 pyhd8ed1ab_1 +- werkzeug 2.0.3 pyhd8ed1ab_1 +- wheel 0.37.1 pyhd8ed1ab_0 +- xz 5.2.6 h166bdaf_0 +- yarl 1.7.2 py39hb9d737c_2 +- zipp 3.7.0 pyhd8ed1ab_1 +- zlib 1.2.12 h166bdaf_2 + From e1957757e825b1fbf753ee4721613e545c7bad9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Wed, 21 Jun 2023 14:52:03 +0200 Subject: [PATCH 03/17] First take at user-friendly error messages --- crates/libsolv_rs/Cargo.toml | 2 + crates/libsolv_rs/src/lib.rs | 2 +- crates/libsolv_rs/src/pool.rs | 4 +- crates/libsolv_rs/src/problem.rs | 567 ++++++++++++++++++ crates/libsolv_rs/src/rules.rs | 84 ++- ...olver__test__unsat_after_backtracking.snap | 14 + ...test__unsat_applies_graph_compression.snap | 14 + ..._solver__test__unsat_bluesky_conflict.snap | 12 + ..._unsat_incompatible_root_requirements.snap | 10 + ...lver__test__unsat_locked_and_excluded.snap | 11 + ...__test__unsat_missing_top_level_dep_1.snap | 7 + ...__test__unsat_missing_top_level_dep_2.snap | 7 + ...test__unsat_no_candidates_for_child_1.snap | 9 + ...test__unsat_no_candidates_for_child_2.snap | 9 + ...__solver__test__unsat_pubgrub_article.snap | 16 + crates/libsolv_rs/src/solvable.rs | 6 +- crates/libsolv_rs/src/solve_problem.rs | 88 --- crates/libsolv_rs/src/solver.rs | 509 +++++++++++----- crates/rattler_solve/src/lib.rs | 19 +- crates/rattler_solve/src/libsolv/mod.rs | 4 +- 20 files changed, 1108 insertions(+), 286 deletions(-) create mode 100644 crates/libsolv_rs/src/problem.rs create mode 100644 crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_after_backtracking.snap create mode 100644 crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_applies_graph_compression.snap create mode 100644 crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_bluesky_conflict.snap create mode 100644 crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_incompatible_root_requirements.snap create mode 100644 crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_locked_and_excluded.snap create mode 100644 crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_1.snap create mode 100644 crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_2.snap create mode 100644 crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_1.snap create mode 100644 crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_2.snap create mode 100644 crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_pubgrub_article.snap delete mode 100644 crates/libsolv_rs/src/solve_problem.rs diff --git a/crates/libsolv_rs/Cargo.toml b/crates/libsolv_rs/Cargo.toml index 02f298118..2561753e6 100644 --- a/crates/libsolv_rs/Cargo.toml +++ b/crates/libsolv_rs/Cargo.toml @@ -4,6 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] +itertools = "0.11.0" +petgraph = "0.6.3" rattler_conda_types = { version = "0.4.0", path = "../rattler_conda_types" } [dev-dependencies] diff --git a/crates/libsolv_rs/src/lib.rs b/crates/libsolv_rs/src/lib.rs index 7ac7bb139..47750f383 100644 --- a/crates/libsolv_rs/src/lib.rs +++ b/crates/libsolv_rs/src/lib.rs @@ -2,9 +2,9 @@ mod conda_util; mod decision_map; mod decision_tracker; pub mod pool; +pub mod problem; mod rules; pub mod solvable; pub mod solve_jobs; -pub mod solve_problem; pub mod solver; mod watch_map; diff --git a/crates/libsolv_rs/src/pool.rs b/crates/libsolv_rs/src/pool.rs index 1299f344d..b588f3ad3 100644 --- a/crates/libsolv_rs/src/pool.rs +++ b/crates/libsolv_rs/src/pool.rs @@ -31,8 +31,8 @@ impl StringId { } } -#[derive(Clone, Copy, Debug)] -pub(crate) struct MatchSpecId(u32); +#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] +pub struct MatchSpecId(u32); impl MatchSpecId { fn new(index: usize) -> Self { diff --git a/crates/libsolv_rs/src/problem.rs b/crates/libsolv_rs/src/problem.rs new file mode 100644 index 000000000..4fe734132 --- /dev/null +++ b/crates/libsolv_rs/src/problem.rs @@ -0,0 +1,567 @@ +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::fmt::Formatter; +use std::rc::Rc; + +use itertools::Itertools; +use petgraph::graph::{DiGraph, EdgeIndex, NodeIndex}; +use petgraph::visit::{Bfs, EdgeRef}; +use petgraph::Direction; + +use crate::pool::{MatchSpecId, Pool}; +use crate::rules::RuleKind; +use crate::solvable::SolvableId; +use crate::solver::{RuleId, Solver}; + +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum ProblemNode { + Solvable(SolvableId), + UnresolvedDependency, +} + +impl ProblemNode { + fn solvable_id(self) -> SolvableId { + match self { + ProblemNode::Solvable(solvable_id) => solvable_id, + ProblemNode::UnresolvedDependency => { + panic!("expected solvable node, found unresolved dependency") + } + } + } +} + +#[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] +pub enum ProblemEdge { + Requires(MatchSpecId), + Conflict(Conflict), +} + +impl ProblemEdge { + fn try_requires(self) -> Option { + match self { + ProblemEdge::Requires(match_spec_id) => Some(match_spec_id), + ProblemEdge::Conflict(_) => None, + } + } + + fn requires(self) -> MatchSpecId { + match self { + ProblemEdge::Requires(match_spec_id) => match_spec_id, + ProblemEdge::Conflict(_) => panic!("expected requires edge, found conflict"), + } + } +} + +#[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] +pub enum Conflict { + Locked(SolvableId), + Constrains(MatchSpecId), + ForbidMultipleInstances, +} + +pub struct MergedProblemNode { + pub ids: Vec, +} + +#[derive(Debug)] +pub struct Problem { + rules: Vec, +} + +impl Problem { + pub(crate) fn default() -> Self { + Self { rules: Vec::new() } + } + + pub(crate) fn add_rule(&mut self, rule_id: RuleId) { + if !self.rules.contains(&rule_id) { + self.rules.push(rule_id); + } + } + + pub fn graph(&self, solver: &Solver) -> ProblemGraph { + println!("=== Build graph"); + let mut graph = DiGraph::::default(); + let mut nodes: HashMap = HashMap::default(); + + let root_node = Self::add_node(&mut graph, &mut nodes, SolvableId::root()); + let unresolved_node = graph.add_node(ProblemNode::UnresolvedDependency); + + for rule_id in &self.rules { + let rule = &solver.rules[rule_id.index()]; + match rule.kind { + RuleKind::InstallRoot => (), + RuleKind::Learnt(..) => unreachable!(), + RuleKind::Requires(package_id, match_spec_id) => { + let package_node = Self::add_node(&mut graph, &mut nodes, package_id); + + let candidates = solver.pool().match_spec_to_candidates[match_spec_id.index()] + .as_deref() + .unwrap(); + if candidates.is_empty() { + println!( + "{package_id:?} requires {match_spec_id:?}, which has no candidates" + ); + graph.add_edge( + package_node, + unresolved_node, + ProblemEdge::Requires(match_spec_id), + ); + } else { + for &candidate_id in candidates { + println!("{package_id:?} requires {candidate_id:?}"); + + let candidate_node = + Self::add_node(&mut graph, &mut nodes, candidate_id); + graph.add_edge( + package_node, + candidate_node, + ProblemEdge::Requires(match_spec_id), + ); + } + } + } + RuleKind::ForbidMultipleInstances(instance1_id, instance2_id) => { + let node1_id = Self::add_node(&mut graph, &mut nodes, instance1_id); + let node2_id = Self::add_node(&mut graph, &mut nodes, instance2_id); + + let conflict = if instance1_id.is_root() { + Conflict::Locked(instance2_id) + } else { + Conflict::ForbidMultipleInstances + }; + graph.add_edge(node1_id, node2_id, ProblemEdge::Conflict(conflict)); + } + RuleKind::Constrains(package_id, dep_id) => { + let package_node = Self::add_node(&mut graph, &mut nodes, package_id); + let dep_node = Self::add_node(&mut graph, &mut nodes, dep_id); + + let package = solver.pool().resolve_solvable(package_id); + let dep = solver.pool().resolve_solvable(dep_id); + let ms_id = package + .constrains + .iter() + .cloned() + .find(|&ms| { + let ms = solver.pool().resolve_match_spec(ms); + ms.name.as_deref().unwrap() == dep.record.name + }) + .unwrap(); + + graph.add_edge( + package_node, + dep_node, + ProblemEdge::Conflict(Conflict::Constrains(ms_id)), + ); + } + } + } + + let unresolved_node = if graph + .edges_directed(unresolved_node, Direction::Incoming) + .next() + .is_none() + { + graph.remove_node(unresolved_node); + None + } else { + Some(unresolved_node) + }; + + // Sanity check: all nodes are reachable from root + let mut visited_nodes = HashSet::new(); + let mut bfs = Bfs::new(&graph, root_node); + while let Some(nx) = bfs.next(&graph) { + visited_nodes.insert(nx); + } + assert_eq!(graph.node_count(), visited_nodes.len()); + + ProblemGraph { + graph, + root_node, + unresolved_dependency_node: unresolved_node, + } + } + + fn add_node( + graph: &mut DiGraph, + nodes: &mut HashMap, + solvable_id: SolvableId, + ) -> NodeIndex { + *nodes + .entry(solvable_id) + .or_insert_with(|| graph.add_node(ProblemNode::Solvable(solvable_id))) + } + + pub fn display_user_friendly<'a>(&self, solver: &'a Solver) -> DisplayUnsat<'a> { + let graph = self.graph(solver); + + // TODO: remove + graph.graphviz(solver.pool()); + + DisplayUnsat::new(graph, solver.pool()) + } +} + +pub struct ProblemGraph { + graph: DiGraph, + root_node: NodeIndex, + unresolved_dependency_node: Option, +} + +impl ProblemGraph { + fn graphviz(&self, pool: &Pool) { + let graph = &self.graph; + + println!("digraph {{"); + let mut bfs = Bfs::new(&graph, self.root_node); + while let Some(nx) = bfs.next(&graph) { + match graph.node_weight(nx).as_ref().unwrap() { + ProblemNode::Solvable(id) => { + let solvable = pool.resolve_solvable_inner(*id); + for edge in graph.edges_directed(nx, Direction::Outgoing) { + let target = *graph.node_weight(edge.target()).unwrap(); + + let color = match edge.weight() { + ProblemEdge::Requires(_) + if target != ProblemNode::UnresolvedDependency => + { + "black" + } + _ => "red", + }; + + let label = match edge.weight() { + ProblemEdge::Requires(match_spec_id) + | ProblemEdge::Conflict(Conflict::Constrains(match_spec_id)) => { + pool.resolve_match_spec(*match_spec_id).to_string() + } + ProblemEdge::Conflict(Conflict::ForbidMultipleInstances) + | ProblemEdge::Conflict(Conflict::Locked(_)) => { + "already installed".to_string() + } + }; + + let target = match target { + ProblemNode::Solvable(solvable_2) => pool + .resolve_solvable_inner(solvable_2) + .display() + .to_string(), + ProblemNode::UnresolvedDependency => "unresolved".to_string(), + }; + + println!( + "\"{}\" -> \"{}\"[color={color}, label=\"{label}\"];", + solvable.display(), + target + ); + } + } + ProblemNode::UnresolvedDependency => {} + } + } + println!("}}"); + } + + fn simplify(&self, pool: &Pool) -> HashMap> { + let graph = &self.graph; + + // Gather information about nodes that can be merged + let mut maybe_merge = HashMap::new(); + for node_id in graph.node_indices() { + let candidate = match graph[node_id] { + ProblemNode::UnresolvedDependency => continue, + ProblemNode::Solvable(solvable_id) => { + if solvable_id.is_root() { + continue; + } else { + solvable_id + } + } + }; + + if graph + .edges_directed(node_id, Direction::Incoming) + .any(|e| matches!(e.weight(), ProblemEdge::Conflict(..))) + { + // Nodes that are the target of a conflict should never be merged + continue; + } + + let predecessors: Vec<_> = graph + .edges_directed(node_id, Direction::Incoming) + .map(|e| e.source()) + .sorted_unstable() + .collect(); + let successors: Vec<_> = graph + .edges(node_id) + .map(|e| (e.target(), *e.weight())) + .sorted_unstable() + .collect(); + + let name = pool.resolve_solvable(candidate).name; + + let entry = maybe_merge + .entry((name, predecessors, successors)) + .or_insert(Vec::new()); + + entry.push((node_id, candidate)); + } + + let mut merged_candidates = HashMap::default(); + for mut m in maybe_merge.into_values() { + if m.len() > 1 { + m.sort_unstable_by_key(|&(_, id)| &pool.resolve_solvable(id).record.version); + let m = Rc::new(MergedProblemNode { + ids: m.into_iter().map(|(_, snd)| snd).collect(), + }); + for &id in &m.ids { + merged_candidates.insert(id, m.clone()); + } + } + } + + merged_candidates + } + + fn get_installable_set(&self) -> HashSet { + let mut non_installable: HashSet = HashSet::new(); + + // Definition: a package is installable if all paths from it to the graph's leaves pass + // through non-conflicting edges (i.e. each dependency, and each dependency's dependencies, + // etc, can be installed) + + // Gather the starting set of conflicting edges: + // * Edges into the unresolved dependency node + // * Edges equal to `ProblemEdge::Conflict` + let mut conflicting_edges = Vec::new(); + + if let Some(unresolved_nx) = self.unresolved_dependency_node { + conflicting_edges.extend( + self.graph + .edges_directed(unresolved_nx, Direction::Incoming), + ); + } + + conflicting_edges.extend( + self.graph + .edge_references() + .filter(|e| matches!(e.weight(), ProblemEdge::Conflict(..))), + ); + + // Propagate conflicts up the graph + while let Some(edge) = conflicting_edges.pop() { + let source = edge.source(); + if non_installable.insert(source) { + // Visited for the first time, so make sure the predecessors are also marked as non-installable + conflicting_edges.extend(self.graph.edges_directed(source, Direction::Incoming)); + } + } + + // Installable packages are all nodes that were not marked as non-installable + self.graph + .node_indices() + .filter(|nx| !non_installable.contains(nx)) + .collect() + } +} + +pub struct DisplayUnsat<'a> { + graph: ProblemGraph, + merged_candidates: HashMap>, + installable_set: HashSet, + pool: &'a Pool, +} + +impl<'a> DisplayUnsat<'a> { + pub fn new(graph: ProblemGraph, pool: &'a Pool) -> Self { + let merged_candidates = graph.simplify(pool); + let installable_set = graph.get_installable_set(); + + Self { + graph, + merged_candidates, + installable_set, + pool, + } + } +} + +impl fmt::Display for DisplayUnsat<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let graph = &self.graph.graph; + let installable_nodes = &self.installable_set; + let mut reported: HashSet = HashSet::new(); + + pub enum DisplayOp { + Requirement(MatchSpecId, Vec), + Candidate(NodeIndex), + } + + writeln!(f, "The following packages are incompatible")?; + + // Note: we are only interested in requires edges here + let mut stack = graph + .edges(self.graph.root_node) + .filter(|e| e.weight().try_requires().is_some()) + .group_by(|e| e.weight().requires()) + .into_iter() + .map(|(match_spec_id, group)| { + let edges: Vec<_> = group.map(|e| e.id()).collect(); + (match_spec_id, edges) + }) + .sorted_by_key(|(_match_spec_id, edges)| { + edges + .iter() + .any(|&edge| installable_nodes.contains(&graph.edge_endpoints(edge).unwrap().1)) + }) + .map(|(match_spec_id, edges)| (DisplayOp::Requirement(match_spec_id, edges), 0)) + .collect::>(); + while let Some((node, depth)) = stack.pop() { + let indent = " ".repeat(depth * 4); + + match node { + DisplayOp::Requirement(match_spec_id, edges) => { + debug_assert!(!edges.is_empty()); + + let installable = edges.iter().any(|&e| { + let (_, target) = graph.edge_endpoints(e).unwrap(); + installable_nodes.contains(&target) + }); + + let req = self.pool.resolve_match_spec(match_spec_id).to_string(); + let target_nx = graph.edge_endpoints(edges[0]).unwrap().1; + let missing = + edges.len() == 1 && graph[target_nx] == ProblemNode::UnresolvedDependency; + if missing { + // No candidates for requirement + if depth == 0 { + writeln!(f, "{indent}|-- No candidates where found for {req}.")?; + } else { + writeln!(f, "{indent}|-- {req}, for which no candidates where found.",)?; + } + } else if installable { + // Package can be installed (only mentioned for top-level requirements) + if depth == 0 { + writeln!( + f, + "|-- {req} can be installed with any of the following options:" + )?; + } else { + writeln!(f, "{indent}|-- {req}, which can be installed with any of the following options:")?; + } + + stack.extend( + edges + .iter() + .filter(|&&e| { + installable_nodes.contains(&graph.edge_endpoints(e).unwrap().1) + }) + .map(|&e| { + ( + DisplayOp::Candidate(graph.edge_endpoints(e).unwrap().1), + depth + 1, + ) + }), + ); + } else { + // Package cannot be installed (the conflicting requirement is further down the tree) + if depth == 0 { + writeln!(f, "|-- {req} cannot be installed because there are no viable options:")?; + } else { + writeln!(f, "{indent}|-- {req}, which cannot be installed because there are no viable options:")?; + } + + stack.extend(edges.iter().map(|&e| { + ( + DisplayOp::Candidate(graph.edge_endpoints(e).unwrap().1), + depth + 1, + ) + })); + } + } + DisplayOp::Candidate(candidate) => { + let solvable_id = graph[candidate].solvable_id(); + + if reported.contains(&solvable_id) { + continue; + } + + let solvable = self.pool.resolve_solvable(solvable_id); + let version = if let Some(merged) = self.merged_candidates.get(&solvable_id) { + reported.extend(merged.ids.iter().cloned()); + merged + .ids + .iter() + .map(|&id| self.pool.resolve_solvable(id).record.version.to_string()) + .join(" | ") + } else { + solvable.record.version.to_string() + }; + + let is_conflict_source = graph + .edges(candidate) + .any(|e| e.weight().try_requires().is_none()); + let is_leaf = graph + .edges(candidate) + .next() + .is_none(); + + if is_conflict_source { + writeln!(f, "{indent}|-- {} {version}, which conflicts with the versions reported above.", solvable.record.name)?; + } else if is_leaf { + writeln!(f, "{indent}|-- {} {version}", solvable.record.name)?; + } else { + writeln!( + f, + "{indent}|-- {} {version} would require", + solvable.record.name + )?; + let requirements = graph + .edges(candidate) + .group_by(|e| e.weight().requires()) + .into_iter() + .map(|(match_spec_id, group)| { + let edges: Vec<_> = group.map(|e| e.id()).collect(); + (match_spec_id, edges) + }) + .sorted_by_key(|(_match_spec_id, edges)| { + edges.iter().any(|&edge| { + installable_nodes + .contains(&graph.edge_endpoints(edge).unwrap().1) + }) + }) + .map(|(match_spec_id, edges)| { + (DisplayOp::Requirement(match_spec_id, edges), depth + 1) + }); + + stack.extend(requirements); + } + } + } + } + + // Report conflicts caused by locked dependencies + for e in graph.edges(self.graph.root_node) { + let conflict = match e.weight() { + ProblemEdge::Requires(_) => continue, + ProblemEdge::Conflict(conflict) => conflict, + }; + + // The only possible conflict at the root level is a Locked conflict + let locked_id = match conflict { + Conflict::Constrains(_) | Conflict::ForbidMultipleInstances => unreachable!(), + &Conflict::Locked(solvable_id) => solvable_id, + }; + + let locked = self.pool.resolve_solvable(locked_id); + writeln!( + f, + "|-- {} {} is locked, but another version is required as reported above", + locked.record.name, locked.record.version + )?; + } + + Ok(()) + } +} diff --git a/crates/libsolv_rs/src/rules.rs b/crates/libsolv_rs/src/rules.rs index dafb0909c..1f095b598 100644 --- a/crates/libsolv_rs/src/rules.rs +++ b/crates/libsolv_rs/src/rules.rs @@ -2,6 +2,47 @@ use crate::decision_map::DecisionMap; use crate::pool::{MatchSpecId, Pool}; use crate::solvable::SolvableId; use crate::solver::RuleId; +use std::fmt::{Debug, Formatter}; + +pub(crate) struct RuleDebug<'a> { + kind: RuleKind, + pool: &'a Pool, +} + +impl Debug for RuleDebug<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self.kind { + RuleKind::InstallRoot => write!(f, "install root"), + RuleKind::Learnt(index) => write!(f, "learnt rule {index}"), + RuleKind::Requires(solvable_id, match_spec_id) => { + let match_spec = self.pool.resolve_match_spec(match_spec_id).to_string(); + write!( + f, + "{} requires {match_spec}", + self.pool.resolve_solvable_inner(solvable_id).display() + ) + } + RuleKind::Constrains(s1, s2) => { + write!( + f, + "{} excludes {}", + self.pool.resolve_solvable_inner(s1).display(), + self.pool.resolve_solvable_inner(s2).display() + ) + } + RuleKind::ForbidMultipleInstances(s1, _) => { + let name = self + .pool + .resolve_solvable_inner(s1) + .package() + .record + .name + .as_str(); + write!(f, "only one {name} allowed") + } + } + } +} #[derive(Clone)] pub(crate) struct Rule { @@ -27,33 +68,10 @@ impl Rule { rule } - pub fn debug(&self, pool: &Pool) { - match self.kind { - RuleKind::InstallRoot => println!("install root"), - RuleKind::Learnt(index) => println!("learnt rule {index}"), - RuleKind::Requires(solvable_id, match_spec_id) => { - let match_spec = pool.resolve_match_spec(match_spec_id).to_string(); - println!( - "{} requires {match_spec}", - pool.resolve_solvable_inner(solvable_id).display() - ) - } - RuleKind::Constrains(s1, s2) => { - println!( - "{} excludes {}", - pool.resolve_solvable_inner(s1).display(), - pool.resolve_solvable_inner(s2).display() - ) - } - RuleKind::Forbids(s1, _) => { - let name = pool - .resolve_solvable_inner(s1) - .package() - .record - .name - .as_str(); - println!("only one {name} allowed") - } + pub fn debug<'a>(&self, pool: &'a Pool) -> RuleDebug<'a> { + RuleDebug { + kind: self.kind, + pool, } } @@ -141,7 +159,7 @@ impl Rule { .unwrap(); [w1, w2] } - RuleKind::Forbids(_, _) => literals(false, false), + RuleKind::ForbidMultipleInstances(_, _) => literals(false, false), RuleKind::Constrains(_, _) => literals(false, false), RuleKind::Requires(solvable_id, _) => { if self.watched_literals[0] == solvable_id { @@ -176,7 +194,7 @@ impl Rule { .cloned() .find(|&l| can_watch(l)) .map(|l| l.solvable_id), - RuleKind::Forbids(_, _) => None, + RuleKind::ForbidMultipleInstances(_, _) => None, RuleKind::Constrains(_, _) => None, RuleKind::Requires(solvable_id, match_spec_id) => { // The solvable that added this rule @@ -232,7 +250,7 @@ impl Rule { ) .collect() } - RuleKind::Forbids(s1, s2) => { + RuleKind::ForbidMultipleInstances(s1, s2) => { vec![ Literal { solvable_id: s1, @@ -293,7 +311,7 @@ impl Rule { .filter(|&l| variable != l.solvable_id) .collect() } - RuleKind::Forbids(s1, s2) => { + RuleKind::ForbidMultipleInstances(s1, s2) => { let cause = if variable == s1 { s2 } else { s1 }; vec![Literal { @@ -353,7 +371,7 @@ pub(crate) enum RuleKind { /// Used to ensure only a single version of a package is installed /// /// In SAT terms: (¬A ∨ ¬B) - Forbids(SolvableId, SolvableId), + ForbidMultipleInstances(SolvableId, SolvableId), /// Similar to forbid, but created due to a constrains relationship Constrains(SolvableId, SolvableId), /// Learnt rule @@ -369,7 +387,7 @@ impl RuleKind { match self { RuleKind::InstallRoot => None, RuleKind::Constrains(s1, s2) => Some([*s1, *s2]), - RuleKind::Forbids(s1, s2) => Some([*s1, *s2]), + RuleKind::ForbidMultipleInstances(s1, s2) => Some([*s1, *s2]), RuleKind::Learnt(index) => { let literals = &learnt_rules[*index]; debug_assert!(!literals.is_empty()); diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_after_backtracking.snap b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_after_backtracking.snap new file mode 100644 index 000000000..ee21a49c6 --- /dev/null +++ b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_after_backtracking.snap @@ -0,0 +1,14 @@ +--- +source: crates/libsolv_rs/src/solver.rs +expression: error +--- +The following packages are incompatible +|-- B can be installed with any of the following options: + |-- B 4.5.6 | 4.5.7 would require + |-- D 1.*, which can be installed with any of the following options: + |-- D 1.0.0 +|-- C cannot be installed because there are no viable options: + |-- C 1.0.0 | 1.0.1 would require + |-- D 2.*, which cannot be installed because there are no viable options: + |-- D 2.0.0, which conflicts with the versions reported above. + diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_applies_graph_compression.snap b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_applies_graph_compression.snap new file mode 100644 index 000000000..b43babe7d --- /dev/null +++ b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_applies_graph_compression.snap @@ -0,0 +1,14 @@ +--- +source: crates/libsolv_rs/src/solver.rs +expression: error +--- +The following packages are incompatible +|-- A can be installed with any of the following options: + |-- A 9 | 10 would require + |-- B, which can be installed with any of the following options: + |-- B 42 | 100 would require + |-- C <100, which can be installed with any of the following options: + |-- C 99 +|-- C >100 cannot be installed because there are no viable options: + |-- C 101 | 103, which conflicts with the versions reported above. + diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_bluesky_conflict.snap b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_bluesky_conflict.snap new file mode 100644 index 000000000..94bb34a96 --- /dev/null +++ b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_bluesky_conflict.snap @@ -0,0 +1,12 @@ +--- +source: crates/libsolv_rs/src/solver.rs +expression: error +--- +The following packages are incompatible +|-- bluesky-widgets <100 can be installed with any of the following options: + |-- bluesky-widgets 42 would require + |-- suitcase-utils <54, which can be installed with any of the following options: + |-- suitcase-utils 53 +|-- suitcase-utils >=54,<100 cannot be installed because there are no viable options: + |-- suitcase-utils 54, which conflicts with the versions reported above. + diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_incompatible_root_requirements.snap b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_incompatible_root_requirements.snap new file mode 100644 index 000000000..ec1817884 --- /dev/null +++ b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_incompatible_root_requirements.snap @@ -0,0 +1,10 @@ +--- +source: crates/libsolv_rs/src/solver.rs +expression: error +--- +The following packages are incompatible +|-- A >=5,<10 can be installed with any of the following options: + |-- A 5 +|-- A <4 cannot be installed because there are no viable options: + |-- A 2, which conflicts with the versions reported above. + diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_locked_and_excluded.snap b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_locked_and_excluded.snap new file mode 100644 index 000000000..bfff00300 --- /dev/null +++ b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_locked_and_excluded.snap @@ -0,0 +1,11 @@ +--- +source: crates/libsolv_rs/src/solver.rs +expression: error +--- +The following packages are incompatible +|-- asdf can be installed with any of the following options: + |-- asdf 1.2.3 would require + |-- C >1, which can be installed with any of the following options: + |-- C 2.0.0 +|-- C 2.0.0 is locked, but another version is required as reported above + diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_1.snap b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_1.snap new file mode 100644 index 000000000..c5751bf6e --- /dev/null +++ b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_1.snap @@ -0,0 +1,7 @@ +--- +source: crates/libsolv_rs/src/solver.rs +expression: error +--- +The following packages are incompatible +|-- No candidates where found for fghj. + diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_2.snap b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_2.snap new file mode 100644 index 000000000..d6f09b808 --- /dev/null +++ b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_2.snap @@ -0,0 +1,7 @@ +--- +source: crates/libsolv_rs/src/solver.rs +expression: error +--- +The following packages are incompatible +|-- No candidates where found for B 14.*. + diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_1.snap b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_1.snap new file mode 100644 index 000000000..c55e00733 --- /dev/null +++ b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_1.snap @@ -0,0 +1,9 @@ +--- +source: crates/libsolv_rs/src/solver.rs +expression: error +--- +The following packages are incompatible +|-- asdf cannot be installed because there are no viable options: + |-- asdf 1.2.3 would require + |-- C >1, for which no candidates where found. + diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_2.snap b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_2.snap new file mode 100644 index 000000000..2a10f80ed --- /dev/null +++ b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_2.snap @@ -0,0 +1,9 @@ +--- +source: crates/libsolv_rs/src/solver.rs +expression: error +--- +The following packages are incompatible +|-- A <1000 cannot be installed because there are no viable options: + |-- A 41 would require + |-- B <20, for which no candidates where found. + diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_pubgrub_article.snap b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_pubgrub_article.snap new file mode 100644 index 000000000..cdf09cdec --- /dev/null +++ b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_pubgrub_article.snap @@ -0,0 +1,16 @@ +--- +source: crates/libsolv_rs/src/solver.rs +expression: error +--- +The following packages are incompatible +|-- menu can be installed with any of the following options: + |-- menu 1.0.0 would require + |-- dropdown >=1.8.0,<2.0.0, which can be installed with any of the following options: + |-- dropdown 1.8.0 would require + |-- intl 3.0.0.*, which can be installed with any of the following options: + |-- intl 3.0.0 +|-- icons 1.0.0.* can be installed with any of the following options: + |-- icons 1.0.0 +|-- intl 5.0.0.* cannot be installed because there are no viable options: + |-- intl 5.0.0, which conflicts with the versions reported above. + diff --git a/crates/libsolv_rs/src/solvable.rs b/crates/libsolv_rs/src/solvable.rs index 4e0501bf1..4a3e7e6e3 100644 --- a/crates/libsolv_rs/src/solvable.rs +++ b/crates/libsolv_rs/src/solvable.rs @@ -2,7 +2,7 @@ use crate::pool::{MatchSpecId, RepoId, StringId}; use rattler_conda_types::{PackageRecord, Version}; use std::fmt::{Display, Formatter}; -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)] pub struct SolvableId(u32); impl SolvableId { @@ -14,6 +14,10 @@ impl SolvableId { Self(0) } + pub(crate) fn is_root(self) -> bool { + self.0 == 0 + } + pub(crate) fn null() -> Self { Self(u32::MAX) } diff --git a/crates/libsolv_rs/src/solve_problem.rs b/crates/libsolv_rs/src/solve_problem.rs deleted file mode 100644 index de8ce46db..000000000 --- a/crates/libsolv_rs/src/solve_problem.rs +++ /dev/null @@ -1,88 +0,0 @@ -use crate::pool::StringId; - -#[derive(Debug)] -pub enum SolveProblem { - /// A top level requirement. - /// The difference between JOB and PKG is unknown (possibly unused). - Job { dep: String }, - /// A top level dependency does not exist. - /// Could be a wrong name or missing channel. - JobNothingProvidesDep { dep: String }, - /// A top level dependency does not exist. - /// Could be a wrong name or missing channel. - JobUnknownPackage { dep: String }, - /// A top level requirement. - /// The difference between JOB and PKG is unknown (possibly unused). - Pkg { dep: String }, - /// Looking for a valid solution to the installation satisfiability expand to - /// two solvables of same package that cannot be installed together. This is - /// a partial exaplanation of why one of the solvables (could be any of the - /// parent) cannot be installed. - PkgConflicts { source: StringId, target: StringId }, - /// A constraint (run_constrained) on source is conflicting with target. - /// SOLVER_RULE_PKG_CONSTRAINS has a dep, but it can resolve to nothing. - /// The constraint conflict is actually expressed between the target and - /// a constrains node child of the source. - PkgConstrains { - source: StringId, - target: StringId, - dep: String, - }, - /// A package dependency does not exist. - /// Could be a wrong name or missing channel. - /// This is a partial exaplanation of why a specific solvable (could be any - /// of the parent) cannot be installed. - PkgNothingProvidesDep { source: StringId, dep: String }, - /// Express a dependency on source that is involved in explaining the - /// problem. - /// Not all dependency of package will appear, only enough to explain the - //. problem. It is not a problem in itself, only a part of the graph. - PkgRequires { source: StringId, dep: String }, - /// Package conflict between two solvables of same package name (handled the same as - /// [`SolveProblem::PkgConflicts`]). - PkgSameName { source: StringId, target: StringId }, - /// Encounterd in the problems list from libsolv but unknown. - /// Explicitly ignored until we do something with it. - Update, -} - -// impl SolveProblem { -// pub fn from_raw( -// problem_type: ffi::SolverRuleinfo, -// dep: Option, -// source: Option, -// target: Option, -// ) -> Self { -// match problem_type { -// SOLVER_RULE_JOB => Self::Job { dep: dep.unwrap() }, -// SOLVER_RULE_JOB_NOTHING_PROVIDES_DEP => { -// Self::JobNothingProvidesDep { dep: dep.unwrap() } -// } -// SOLVER_RULE_JOB_UNKNOWN_PACKAGE => Self::JobUnknownPackage { dep: dep.unwrap() }, -// SOLVER_RULE_PKG => Self::Pkg { dep: dep.unwrap() }, -// SOLVER_RULE_SOLVER_RULE_PKG_CONFLICTS => Self::PkgConflicts { -// source: source.unwrap(), -// target: target.unwrap(), -// }, -// SOLVER_RULE_PKG_CONSTRAINS => Self::PkgConstrains { -// source: source.unwrap(), -// target: target.unwrap(), -// dep: dep.unwrap(), -// }, -// SOLVER_RULE_SOLVER_RULE_PKG_NOTHING_PROVIDES_DEP => Self::PkgNothingProvidesDep { -// source: source.unwrap(), -// dep: dep.unwrap(), -// }, -// SOLVER_RULE_PKG_REQUIRES => Self::PkgRequires { -// source: source.unwrap(), -// dep: dep.unwrap(), -// }, -// SOLVER_RULE_SOLVER_RULE_PKG_SAME_NAME => Self::PkgSameName { -// source: source.unwrap(), -// target: target.unwrap(), -// }, -// SOLVER_RULE_SOLVER_RULE_UPDATE => Self::Update, -// _ => panic!("Unknown problem type: {}", problem_type), -// } -// } -// } diff --git a/crates/libsolv_rs/src/solver.rs b/crates/libsolv_rs/src/solver.rs index 3cdc507d6..16c6260ab 100644 --- a/crates/libsolv_rs/src/solver.rs +++ b/crates/libsolv_rs/src/solver.rs @@ -1,16 +1,16 @@ +use crate::decision_tracker::DecisionTracker; use crate::pool::{MatchSpecId, Pool, StringId}; +use crate::problem::Problem; use crate::rules::{Literal, Rule, RuleKind}; use crate::solvable::SolvableId; use crate::solve_jobs::SolveJobs; - use crate::watch_map::WatchMap; -use crate::decision_tracker::DecisionTracker; use rattler_conda_types::MatchSpec; use std::collections::{HashMap, HashSet}; use std::fmt::{Display, Formatter}; -#[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Debug)] +#[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Debug, Hash)] pub(crate) struct RuleId(u32); impl RuleId { @@ -18,11 +18,11 @@ impl RuleId { Self(index as u32) } - fn install_root() -> Self { + pub(crate) fn install_root() -> Self { Self(0) } - fn index(self) -> usize { + pub(crate) fn index(self) -> usize { self.0 as usize } @@ -70,11 +70,12 @@ impl Display for TransactionKind { pub struct Solver { pool: Pool, - rules: Vec, + pub(crate) rules: Vec, watches: WatchMap, learnt_rules: Vec>, learnt_rules_start: RuleId, + learnt_why: Vec>, decision_tracker: DecisionTracker, } @@ -87,6 +88,7 @@ impl Solver { watches: WatchMap::new(), learnt_rules: Vec::new(), learnt_rules_start: RuleId(0), + learnt_why: Vec::new(), decision_tracker: DecisionTracker::new(pool.nsolvables()), pool, } @@ -96,23 +98,19 @@ impl Solver { &self.pool } - /// Creates a string for each 'problem' that the solver still has which it encountered while - /// solving the matchspecs. Use this function to print the existing problems to string. - fn solver_problems(&self) -> Vec { - Vec::new() - } - - /// Solves the provided `jobs` and returns a transaction from the found solution. + /// Solves the provided `jobs` and returns a transaction from the found solution /// - /// Returns an error if problems remain unsolved. - pub fn solve(&mut self, jobs: SolveJobs) -> Result> { - // TODO: sanity check that solvables inside jobs.lock and jobs.favor are unique + /// Returns a [`Problem`] if problems remain unsolved, which provides ways to inspect the causes + /// and report them to the user. + pub fn solve(&mut self, jobs: SolveJobs) -> Result { + // TODO: sanity check that solvables inside jobs.favor are unique? // Clear state self.pool.root_solvable_mut().clear(); self.decision_tracker.clear(); self.rules = vec![Rule::new(RuleKind::InstallRoot, &[], &self.pool)]; self.learnt_rules.clear(); + self.learnt_why.clear(); // Favored map let mut favored_map = HashMap::new(); @@ -138,7 +136,7 @@ impl Solver { for (i, &candidate) in candidates.iter().enumerate() { for &other_candidate in &candidates[i + 1..] { self.rules.push(Rule::new( - RuleKind::Forbids(candidate, other_candidate), + RuleKind::ForbidMultipleInstances(candidate, other_candidate), &self.learnt_rules, &self.pool, )); @@ -146,6 +144,23 @@ impl Solver { } } + // Initialize rules for the locked solvable + for &locked_solvable_id in &jobs.lock { + // For each locked solvable, forbid other solvables with the same name + let name = self.pool.resolve_solvable(locked_solvable_id).name; + if let Some(other_candidates) = self.pool.packages_by_name.get(&name) { + for &other_candidate in other_candidates { + if other_candidate != locked_solvable_id { + self.rules.push(Rule::new( + RuleKind::ForbidMultipleInstances(SolvableId::root(), other_candidate), + &self.learnt_rules, + &self.pool, + )); + } + } + } + } + // All new rules are learnt after this point self.learnt_rules_start = RuleId::new(self.rules.len()); @@ -153,27 +168,22 @@ impl Solver { self.make_watches(); // Run SAT - self.run_sat(&jobs.install, &jobs.lock); + self.run_sat(&jobs.install, &jobs.lock)?; - let remaining_problems = self.solver_problems(); - if remaining_problems.is_empty() { - let steps = self - .decision_tracker - .stack() - .iter() - .flat_map(|d| { - if d.value && d.solvable_id != SolvableId::root() { - Some((d.solvable_id, TransactionKind::Install)) - } else { - // Ignore things that are set to false - None - } - }) - .collect(); - Ok(Transaction { steps }) - } else { - Err(remaining_problems) - } + let steps = self + .decision_tracker + .stack() + .iter() + .flat_map(|d| { + if d.value && d.solvable_id != SolvableId::root() { + Some((d.solvable_id, TransactionKind::Install)) + } else { + // Ignore things that are set to false + None + } + }) + .collect(); + Ok(Transaction { steps }) } fn add_rules_for_root_dep( @@ -263,76 +273,56 @@ impl Solver { )); } - fn run_sat(&mut self, top_level_requirements: &[MatchSpec], locked_solvables: &[SolvableId]) { - let level = match self.install_root_solvable() { - Ok(new_level) => new_level, - Err(_) => panic!("install root solvable failed"), - }; + fn run_sat( + &mut self, + top_level_requirements: &[MatchSpec], + locked_solvables: &[SolvableId], + ) -> Result<(), Problem> { + let level = self.install_root_solvable(); - if self.decide_top_level_assertions(level, locked_solvables).is_err() { - panic!("propagate assertions failed"); - }; + self.decide_top_level_assertions(level, locked_solvables, top_level_requirements) + .map_err(|cause| self.analyze_unsolvable(cause))?; - if self.decide_top_level_exclusions(top_level_requirements).is_err() { - panic!("decide top level exclusions failed"); - } + self.propagate(level) + .map_err(|(_, _, cause)| self.analyze_unsolvable(cause))?; - if let Err((_, _, cause)) = self.propagate(level) { - self.analyze_unsolvable(cause, false); - panic!("Propagate after installing root failed"); - } + self.resolve_dependencies(level)?; - if self.resolve_dependencies(level).is_err() { - panic!("Resolve dependencies failed"); - } + Ok(()) } - fn install_root_solvable(&mut self) -> Result { + fn install_root_solvable(&mut self) -> u32 { assert!(self.decision_tracker.is_empty()); self.decision_tracker - .try_add_decision(Decision::new(SolvableId::root(), true, RuleId::install_root()), 1) + .try_add_decision( + Decision::new(SolvableId::root(), true, RuleId::install_root()), + 1, + ) .expect("bug: solvable was already decided!"); - Ok(1) + + // The root solvable is installed at level 1 + 1 } fn decide_top_level_assertions( &mut self, level: u32, - locked_solvables: &[SolvableId], - ) -> Result<(), ()> { + _locked_solvables: &[SolvableId], + _top_level_requirements: &[MatchSpec], + ) -> Result<(), RuleId> { println!("=== Deciding assertions"); - // Assertions derived from locked dependencies - for &locked_solvable_id in locked_solvables { - // For each locked solvable, decide that other solvables with the same name are - // forbidden - let name = self - .pool - .resolve_solvable_inner(locked_solvable_id) - .package() - .name; - if let Some(other_candidates) = self.pool.packages_by_name.get(&name) { - for &other_candidate in other_candidates { - if other_candidate != locked_solvable_id { - self.decision_tracker.try_add_decision( - Decision::new(other_candidate, false, RuleId::install_root()), - level, - )?; - } - } - } - } - // Assertions derived from requirements that cannot be fulfilled for (i, rule) in self.rules.iter().enumerate() { if let RuleKind::Requires(solvable_id, _) = rule.kind { if !rule.has_watches() { // A requires rule without watches means it has a single literal (i.e. // there are no candidates) - let decided = self.decision_tracker.try_add_decision( - Decision::new(solvable_id, false, RuleId::new(i)), - level, - )?; + let rule_id = RuleId::new(i); + let decided = self + .decision_tracker + .try_add_decision(Decision::new(solvable_id, false, rule_id), level) + .map_err(|_| rule_id)?; if decided { println!( @@ -347,40 +337,8 @@ impl Solver { Ok(()) } - fn decide_top_level_exclusions( - &mut self, - top_level_requirements: &[MatchSpec], - ) -> Result<(), ()> { - println!("=== Deciding exclusions"); - for req in top_level_requirements { - let name = req.name.as_deref().unwrap(); - - let Some(candidates) = self.pool.strings_to_ids.get(name).and_then(|name_id| self.pool.packages_by_name.get(name_id)) else { - return Ok(()); - }; - let non_matching = candidates.iter().filter(|solvable_id| { - let solvable = &self.pool.solvables[solvable_id.index()]; - !req.matches(solvable.package().record) - }); - - for &solvable_id in non_matching { - let decided = self - .decision_tracker - .try_add_decision(Decision::new(solvable_id, false, RuleId::new(0)), 1)?; - if decided { - println!( - "{} = false", - self.pool.resolve_solvable_inner(solvable_id).display() - ); - } - } - } - - Ok(()) - } - /// Resolves all dependencies - fn resolve_dependencies(&mut self, mut level: u32) -> Result { + fn resolve_dependencies(&mut self, mut level: u32) -> Result { let mut i = 0; loop { if i >= self.rules.len() { @@ -426,15 +384,7 @@ impl Solver { ) }; - // Assumption: there are multiple candidates, otherwise this would have already been handled - // by unit propagation - self.create_branch(); - level = - self.set_propagate_learn(level, candidate, required_by, true, RuleId::new(i))?; - - // if level < orig_level { - // return Err(level); - // } + level = self.set_propagate_learn(level, candidate, required_by, RuleId::new(i))?; // We have made progress, and should look at all rules in the next iteration i = 0; @@ -449,9 +399,8 @@ impl Solver { mut level: u32, solvable: SolvableId, required_by: SolvableId, - disable_rules: bool, rule_id: RuleId, - ) -> Result { + ) -> Result { level += 1; println!( @@ -480,45 +429,46 @@ impl Solver { println!( "=== Propagation conflicted: could not set {solvable} to {attempted_value}" ); - print!("During unit propagation for rule: "); - self.rules[conflicting_rule.index()].debug(&self.pool); + println!( + "During unit propagation for rule: {:?}", + self.rules[conflicting_rule.index()].debug(&self.pool) + ); + let decision = self .decision_tracker .stack() .iter() .find(|d| d.solvable_id == conflicting_solvable) .unwrap(); - print!( - "Previously decided value: {}. Derived from: ", - !attempted_value + println!( + "Previously decided value: {}. Derived from: {:?}", + !attempted_value, + self.rules[decision.derived_from.index()].debug(&self.pool), ); - self.rules[decision.derived_from.index()].debug(&self.pool); } if level == 1 { - // Is it really unsolvable if we are back to level 1? println!("=== UNSOLVABLE"); for decision in self.decision_tracker.stack() { let rule = &self.rules[decision.derived_from.index()]; let level = self.decision_tracker.level(decision.solvable_id); let action = if decision.value { "install" } else { "forbid" }; - if let RuleKind::Forbids(_, _) = rule.kind { + if let RuleKind::ForbidMultipleInstances(..) = rule.kind { // Skip forbids rules, to reduce noise continue; } - print!( - "* ({level}) {action} {}", + println!( + "* ({level}) {action} {}. Reason: {:?}", self.pool .resolve_solvable_inner(decision.solvable_id) - .display() + .display(), + rule.debug(&self.pool), ); - print!(". Reason: "); - rule.debug(&self.pool); } - self.analyze_unsolvable(conflicting_rule, disable_rules); - return Err(()); + + return Err(self.analyze_unsolvable(conflicting_rule)); } let (new_level, learned_rule_id, literal) = @@ -546,10 +496,6 @@ impl Solver { Ok(level) } - fn create_branch(&mut self) { - // TODO: we should probably keep info here for backtracking - } - fn propagate(&mut self, level: u32) -> Result<(), (SolvableId, bool, RuleId)> { // Learnt assertions let learnt_rules_start = self.learnt_rules_start.index(); @@ -667,17 +613,17 @@ impl Solver { | RuleKind::Requires(_, _) | RuleKind::Constrains(_, _) | RuleKind::Learnt(_) => { - print!( - "Propagate {} = {}. ", + println!( + "Propagate {} = {}. {:?}", self.pool .resolve_solvable_inner(remaining_watch.solvable_id) .display(), - remaining_watch.satisfying_value() + remaining_watch.satisfying_value(), + rule.debug(&self.pool), ); - rule.debug(&self.pool); } // Skip logging for forbids, which is so noisy - RuleKind::Forbids(_, _) => {} + RuleKind::ForbidMultipleInstances(..) => {} } } } @@ -688,8 +634,96 @@ impl Solver { Ok(()) } - fn analyze_unsolvable(&mut self, _rule: RuleId, _disable_rules: bool) { - // todo!() + fn analyze_unsolvable_rule( + rules: &[Rule], + learnt_why: &[Vec], + learnt_rules_start: RuleId, + rule_id: RuleId, + problem: &mut Problem, + seen: &mut HashSet, + ) { + let rule = &rules[rule_id.index()]; + match rule.kind { + RuleKind::Learnt(..) => { + if !seen.insert(rule_id) { + return; + } + + for &cause in &learnt_why[rule_id.index() - learnt_rules_start.index()] { + Self::analyze_unsolvable_rule( + rules, + learnt_why, + learnt_rules_start, + cause, + problem, + seen, + ); + } + } + _ => problem.add_rule(rule_id), + } + } + + fn analyze_unsolvable(&mut self, rule_id: RuleId) -> Problem { + let last_decision = self.decision_tracker.stack().last().unwrap(); + let highest_level = self.decision_tracker.level(last_decision.solvable_id); + debug_assert_eq!(highest_level, 1); + + let mut problem = Problem::default(); + + println!("=== ANALYZE UNSOLVABLE"); + + let mut involved = HashSet::new(); + involved.extend( + self.rules[rule_id.index()] + .literals(&self.learnt_rules, &self.pool) + .iter() + .map(|l| l.solvable_id), + ); + + let mut seen = HashSet::new(); + Self::analyze_unsolvable_rule( + &self.rules, + &self.learnt_why, + self.learnt_rules_start, + rule_id, + &mut problem, + &mut seen, + ); + + for decision in self.decision_tracker.stack()[1..].iter().rev() { + if decision.solvable_id == SolvableId::root() { + panic!("unexpected root solvable") + } + + let why = decision.derived_from; + + if !involved.contains(&decision.solvable_id) { + continue; + } + + assert_ne!(why, RuleId::install_root()); + + Self::analyze_unsolvable_rule( + &self.rules, + &self.learnt_why, + self.learnt_rules_start, + why, + &mut problem, + &mut seen, + ); + + for literal in self.rules[why.index()].literals(&self.learnt_rules, &self.pool) { + if literal.eval(self.decision_tracker.map()) == Some(true) { + assert_eq!(literal.solvable_id, decision.solvable_id); + continue; + } + + involved.insert(literal.solvable_id); + } + } + + problem } fn analyze( @@ -707,7 +741,11 @@ impl Solver { let mut first_iteration = true; let mut s_value; + + let mut learnt_why = Vec::new(); loop { + learnt_why.push(rule_id); + // TODO: we should be able to get rid of the branching, always retrieving the whole list // of literals, since the hash set will ensure we aren't considering the conflicting // solvable after the first iteration @@ -791,6 +829,7 @@ impl Solver { let rule_id = RuleId::new(self.rules.len()); let learnt_index = self.learnt_rules.len(); self.learnt_rules.push(learnt.clone()); + self.learnt_why.push(learnt_why); let mut rule = Rule::new( RuleKind::Learnt(learnt_index), @@ -913,6 +952,14 @@ mod test { buf } + fn solve_unsat(pool: Pool, jobs: SolveJobs) -> String { + let mut solver = Solver::new(pool); + match solver.solve(jobs) { + Ok(_) => panic!("expected unsat, but a solution was found"), + Err(problem) => problem.display_user_friendly(&solver).to_string(), + } + } + #[test] fn test_unit_propagation_1() { let pool = pool(&[("asdf", "1.2.3", vec![])]); @@ -1182,4 +1229,156 @@ mod test { A 2 "###); } + + #[test] + fn test_resolve_cyclic() { + let pool = pool(&[("A", "2", vec!["B<=10"]), ("B", "5", vec!["A>=2,<=4"])]); + let jobs = install(&["A<100"]); + let mut solver = Solver::new(pool); + let solved = solver.solve(jobs).unwrap(); + + let result = transaction_to_string(&solver.pool, &solved); + insta::assert_snapshot!(result, @r###" + A 2 + B 5 + "###); + } + + #[test] + fn test_unsat_locked_and_excluded() { + let pool = pool(&[ + ("asdf", "1.2.3", vec!["C>1"]), + ("C", "2.0.0", vec![]), + ("C", "1.0.0", vec![]), + ]); + let mut job = install(&["asdf"]); + job.lock(SolvableId::new(3)); // C 1.0.0 + + let error = solve_unsat(pool, job); + insta::assert_snapshot!(error); + } + + #[test] + fn test_unsat_no_candidates_for_child_1() { + let pool = pool(&[("asdf", "1.2.3", vec!["C>1"]), ("C", "1.0.0", vec![])]); + let error = solve_unsat(pool, install(&["asdf"])); + insta::assert_snapshot!(error); + } + + #[test] + fn test_unsat_no_candidates_for_child_2() { + let pool = pool(&[("A", "41", vec!["B<20"])]); + let jobs = install(&["A<1000"]); + + let error = solve_unsat(pool, jobs); + insta::assert_snapshot!(error); + } + + #[test] + fn test_unsat_missing_top_level_dep_1() { + let pool = pool(&[("asdf", "1.2.3", vec![])]); + let error = solve_unsat(pool, install(&["fghj"])); + insta::assert_snapshot!(error); + } + + #[test] + fn test_unsat_missing_top_level_dep_2() { + let pool = pool(&[("A", "41", vec!["B=15"]), ("B", "15", vec![])]); + let jobs = install(&["A=41", "B=14"]); + + let error = solve_unsat(pool, jobs); + insta::assert_snapshot!(error); + } + + #[test] + fn test_unsat_after_backtracking() { + let pool = pool(&[ + ("B", "4.5.7", vec!["D=1"]), + ("B", "4.5.6", vec!["D=1"]), + ("C", "1.0.1", vec!["D=2"]), + ("C", "1.0.0", vec!["D=2"]), + ("D", "2.0.0", vec![]), + ("D", "1.0.0", vec![]), + ("E", "1.0.0", vec![]), + ("E", "1.0.1", vec![]), + ]); + + let error = solve_unsat(pool, install(&["B", "C", "E"])); + insta::assert_snapshot!(error); + } + + #[test] + fn test_unsat_incompatible_root_requirements() { + let pool = pool(&[("A", "2", vec![]), ("A", "5", vec![])]); + let jobs = install(&["A<4", "A>=5,<10"]); + + let error = solve_unsat(pool, jobs); + insta::assert_snapshot!(error); + } + + #[test] + fn test_unsat_bluesky_conflict() { + let pool = pool(&[ + ("suitcase-utils", "54", vec![]), + ("suitcase-utils", "53", vec![]), + ( + "bluesky-widgets", + "42", + vec![ + "bluesky-live<10", + "numpy<10", + "python<10", + "suitcase-utils<54", + ], + ), + ("bluesky-live", "1", vec![]), + ("numpy", "1", vec![]), + ("python", "1", vec![]), + ]); + + let jobs = install(&["bluesky-widgets<100", "suitcase-utils>=54,<100"]); + + let error = solve_unsat(pool, jobs); + insta::assert_snapshot!(error); + } + + #[test] + fn test_unsat_pubgrub_article() { + // Taken from the pubgrub article: https://nex3.medium.com/pubgrub-2fb6470504f + let pool = pool(&[ + ("menu", "1.5.0", vec!["dropdown>=2.0.0,<=2.3.0"]), + ("menu", "1.0.0", vec!["dropdown>=1.8.0,<2.0.0"]), + ("dropdown", "2.3.0", vec!["icons=2.0.0"]), + ("dropdown", "1.8.0", vec!["intl=3.0.0"]), + ("icons", "2.0.0", vec![]), + ("icons", "1.0.0", vec![]), + ("intl", "5.0.0", vec![]), + ("intl", "3.0.0", vec![]), + ]); + + let jobs = install(&["menu", "icons=1.0.0", "intl=5.0.0"]); + + let error = solve_unsat(pool, jobs); + insta::assert_snapshot!(error); + } + + // TODO: this isn't testing for compression yet! + #[test] + fn test_unsat_applies_graph_compression() { + let pool = pool(&[ + ("A", "10", vec!["B"]), + ("A", "9", vec!["B"]), + ("B", "100", vec!["C<100"]), + ("B", "42", vec!["C<100"]), + ("C", "103", vec![]), + ("C", "101", vec![]), + ("C", "100", vec![]), + ("C", "99", vec![]), + ]); + + let jobs = install(&["A", "C>100"]); + + let error = solve_unsat(pool, jobs); + insta::assert_snapshot!(error); + } } diff --git a/crates/rattler_solve/src/lib.rs b/crates/rattler_solve/src/lib.rs index 28f1860bd..bfd62893a 100644 --- a/crates/rattler_solve/src/lib.rs +++ b/crates/rattler_solve/src/lib.rs @@ -81,9 +81,9 @@ mod test_libsolv { Channel, ChannelConfig, GenericVirtualPackage, MatchSpec, NoArchType, PackageRecord, RepoData, RepoDataRecord, Version, }; + use rattler_repodata_gateway::sparse::SparseRepoData; use std::str::FromStr; use url::Url; - use rattler_repodata_gateway::sparse::SparseRepoData; fn conda_json_path() -> String { format!( @@ -132,7 +132,12 @@ mod test_libsolv { } fn read_sparse_repodata(path: &str) -> SparseRepoData { - SparseRepoData::new(Channel::from_str("dummy", &ChannelConfig::default()).unwrap(), "dummy".to_string(), path).unwrap() + SparseRepoData::new( + Channel::from_str("dummy", &ChannelConfig::default()).unwrap(), + "dummy".to_string(), + path, + ) + .unwrap() } fn installed_package( @@ -173,18 +178,22 @@ mod test_libsolv { } fn solve_real_world(specs: Vec<&str>) -> Vec { - let specs = specs.iter().map(|s| MatchSpec::from_str(s).unwrap()).collect::>(); + let specs = specs + .iter() + .map(|s| MatchSpec::from_str(s).unwrap()) + .collect::>(); let json_file = conda_json_path(); let json_file_noarch = conda_json_path_noarch(); let sparse_repo_datas = vec![ read_sparse_repodata(&json_file), - read_sparse_repodata(&json_file_noarch) + read_sparse_repodata(&json_file_noarch), ]; let names = specs.iter().map(|s| s.name.clone().unwrap()); - let available_packages = SparseRepoData::load_records_recursive(&sparse_repo_datas, names).unwrap(); + let available_packages = + SparseRepoData::load_records_recursive(&sparse_repo_datas, names).unwrap(); let solver_task = SolverTask { available_packages: available_packages diff --git a/crates/rattler_solve/src/libsolv/mod.rs b/crates/rattler_solve/src/libsolv/mod.rs index 966d456e7..58f123054 100644 --- a/crates/rattler_solve/src/libsolv/mod.rs +++ b/crates/rattler_solve/src/libsolv/mod.rs @@ -95,7 +95,9 @@ impl SolverBackend for LibsolvBackend { // Construct a solver and solve the problems in the queue let mut solver = Solver::new(pool); - let transaction = solver.solve(goal).map_err(SolveError::Unsolvable)?; + let transaction = solver.solve(goal).map_err(|problem| { + SolveError::Unsolvable(vec![problem.display_user_friendly(&solver).to_string()]) + })?; let required_records = get_required_packages( solver.pool(), From 4486ac51cc3f33b7172da38a4c07c1b939615cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Tue, 27 Jun 2023 16:19:47 +0200 Subject: [PATCH 04/17] Polishing libsolv-rs port --- crates/libsolv_rs/src/conda_util.rs | 14 +- crates/libsolv_rs/src/id.rs | 92 +++ crates/libsolv_rs/src/lib.rs | 6 +- crates/libsolv_rs/src/pool.rs | 215 +++---- crates/libsolv_rs/src/problem.rs | 542 +++++++++++------- ...__test__unsat_missing_top_level_dep_1.snap | 7 - ...__test__unsat_missing_top_level_dep_2.snap | 7 - ...test__unsat_no_candidates_for_child_1.snap | 9 - ...test__unsat_no_candidates_for_child_2.snap | 9 - crates/libsolv_rs/src/solvable.rs | 114 ++-- crates/libsolv_rs/src/solve_jobs.rs | 2 +- crates/libsolv_rs/src/solver/decision.rs | 19 + .../src/{ => solver}/decision_map.rs | 8 +- .../src/{ => solver}/decision_tracker.rs | 11 +- .../src/{solver.rs => solver/mod.rs} | 447 ++++++--------- .../src/{rules.rs => solver/rule.rs} | 458 ++++++++------- ...__solver__test__resolve_with_conflict.snap | 8 + ...olver__test__unsat_after_backtracking.snap | 2 +- ...test__unsat_applies_graph_compression.snap | 2 +- ..._solver__test__unsat_bluesky_conflict.snap | 2 +- ...lv_rs__solver__test__unsat_constrains.snap | 13 + ..._unsat_incompatible_root_requirements.snap | 2 +- ...lver__test__unsat_locked_and_excluded.snap | 4 +- ...__test__unsat_missing_top_level_dep_1.snap | 6 + ...__test__unsat_missing_top_level_dep_2.snap | 6 + ...test__unsat_no_candidates_for_child_1.snap | 8 + ...test__unsat_no_candidates_for_child_2.snap | 8 + ...__solver__test__unsat_pubgrub_article.snap | 2 +- .../libsolv_rs/src/{ => solver}/watch_map.rs | 12 +- crates/libsolv_rs/src/transaction.rs | 20 + crates/rattler_solve/src/libsolv/input.rs | 22 +- crates/rattler_solve/src/libsolv/output.rs | 8 +- 32 files changed, 1142 insertions(+), 943 deletions(-) create mode 100644 crates/libsolv_rs/src/id.rs delete mode 100644 crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_1.snap delete mode 100644 crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_2.snap delete mode 100644 crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_1.snap delete mode 100644 crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_2.snap create mode 100644 crates/libsolv_rs/src/solver/decision.rs rename crates/libsolv_rs/src/{ => solver}/decision_map.rs (83%) rename crates/libsolv_rs/src/{ => solver}/decision_tracker.rs (84%) rename crates/libsolv_rs/src/{solver.rs => solver/mod.rs} (78%) rename crates/libsolv_rs/src/{rules.rs => solver/rule.rs} (61%) create mode 100644 crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__resolve_with_conflict.snap rename crates/libsolv_rs/src/{ => solver}/snapshots/libsolv_rs__solver__test__unsat_after_backtracking.snap (92%) rename crates/libsolv_rs/src/{ => solver}/snapshots/libsolv_rs__solver__test__unsat_applies_graph_compression.snap (92%) rename crates/libsolv_rs/src/{ => solver}/snapshots/libsolv_rs__solver__test__unsat_bluesky_conflict.snap (91%) create mode 100644 crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_constrains.snap rename crates/libsolv_rs/src/{ => solver}/snapshots/libsolv_rs__solver__test__unsat_incompatible_root_requirements.snap (86%) rename crates/libsolv_rs/src/{ => solver}/snapshots/libsolv_rs__solver__test__unsat_locked_and_excluded.snap (71%) create mode 100644 crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_1.snap create mode 100644 crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_2.snap create mode 100644 crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_1.snap create mode 100644 crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_2.snap rename crates/libsolv_rs/src/{ => solver}/snapshots/libsolv_rs__solver__test__unsat_pubgrub_article.snap (93%) rename crates/libsolv_rs/src/{ => solver}/watch_map.rs (88%) create mode 100644 crates/libsolv_rs/src/transaction.rs diff --git a/crates/libsolv_rs/src/conda_util.rs b/crates/libsolv_rs/src/conda_util.rs index a1aa4d3c7..76bb3bf72 100644 --- a/crates/libsolv_rs/src/conda_util.rs +++ b/crates/libsolv_rs/src/conda_util.rs @@ -1,5 +1,5 @@ -use crate::pool::StringId; -use crate::solvable::{Solvable, SolvableId}; +use crate::id::{NameId, SolvableId}; +use crate::solvable::Solvable; use rattler_conda_types::{MatchSpec, PackageRecord, Version}; use std::cmp::Ordering; use std::collections::HashMap; @@ -8,8 +8,8 @@ use std::str::FromStr; /// Returns the order of two candidates based on rules used by conda. pub(crate) fn compare_candidates( solvables: &[Solvable], - interned_strings: &HashMap, - packages_by_name: &HashMap>, + interned_strings: &HashMap, + packages_by_name: &[Vec], a: &PackageRecord, b: &PackageRecord, ) -> Ordering { @@ -116,15 +116,15 @@ pub(crate) fn compare_candidates( pub(crate) fn find_highest_version( solvables: &[Solvable], - interned_strings: &HashMap, - packages_by_name: &HashMap>, + interned_strings: &HashMap, + packages_by_name: &[Vec], match_spec: &MatchSpec, ) -> Option<(Version, bool)> { let name = match_spec.name.as_deref().unwrap(); let name_id = interned_strings[name]; // For each record that matches the spec - let candidates = packages_by_name[&name_id] + let candidates = packages_by_name[name_id.index()] .iter() .map(|s| solvables[s.index()].package().record) .filter(|s| match_spec.matches(s)); diff --git a/crates/libsolv_rs/src/id.rs b/crates/libsolv_rs/src/id.rs new file mode 100644 index 000000000..7ed287554 --- /dev/null +++ b/crates/libsolv_rs/src/id.rs @@ -0,0 +1,92 @@ +#[derive(Clone, Copy, Eq, PartialEq, Hash)] +pub struct RepoId(u32); + +impl RepoId { + pub(crate) fn new(id: u32) -> Self { + Self(id) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct NameId { + value: u32, +} + +impl NameId { + pub(crate) fn new(index: usize) -> Self { + Self { + value: index as u32, + } + } + + pub(crate) fn index(self) -> usize { + self.value as usize + } +} + +#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] +pub struct MatchSpecId(u32); + +impl MatchSpecId { + pub(crate) fn new(index: usize) -> Self { + Self(index as u32) + } + + pub(crate) fn index(self) -> usize { + self.0 as usize + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)] +pub struct SolvableId(u32); + +impl SolvableId { + pub(crate) fn new(index: usize) -> Self { + Self(index as u32) + } + + pub(crate) fn root() -> Self { + Self(0) + } + + pub(crate) fn is_root(self) -> bool { + self.0 == 0 + } + + pub(crate) fn null() -> Self { + Self(u32::MAX) + } + + pub(crate) fn is_null(self) -> bool { + self.0 == u32::MAX + } + + pub(crate) fn index(self) -> usize { + self.0 as usize + } +} + +#[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Debug, Hash)] +pub(crate) struct RuleId(u32); + +impl RuleId { + pub(crate) fn new(index: usize) -> Self { + Self(index as u32) + } + + pub(crate) fn install_root() -> Self { + Self(0) + } + + pub(crate) fn index(self) -> usize { + self.0 as usize + } + + pub(crate) fn is_null(self) -> bool { + self.0 == u32::MAX + } + + pub(crate) fn null() -> RuleId { + RuleId(u32::MAX) + } +} diff --git a/crates/libsolv_rs/src/lib.rs b/crates/libsolv_rs/src/lib.rs index 47750f383..0e2148ffa 100644 --- a/crates/libsolv_rs/src/lib.rs +++ b/crates/libsolv_rs/src/lib.rs @@ -1,10 +1,8 @@ mod conda_util; -mod decision_map; -mod decision_tracker; +pub mod id; pub mod pool; pub mod problem; -mod rules; pub mod solvable; pub mod solve_jobs; pub mod solver; -mod watch_map; +pub mod transaction; diff --git a/crates/libsolv_rs/src/pool.rs b/crates/libsolv_rs/src/pool.rs index b588f3ad3..28fb1fdf6 100644 --- a/crates/libsolv_rs/src/pool.rs +++ b/crates/libsolv_rs/src/pool.rs @@ -1,83 +1,50 @@ use crate::conda_util; -use crate::solvable::{PackageSolvable, Solvable, SolvableId}; +use crate::id::{MatchSpecId, NameId, RepoId, SolvableId}; +use crate::solvable::{PackageSolvable, Solvable}; use rattler_conda_types::{MatchSpec, PackageRecord}; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::str::FromStr; -#[derive(Clone, Copy, Eq, PartialEq, Hash)] -pub struct RepoId(u32); - -impl RepoId { - fn new(id: u32) -> Self { - Self(id) - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct StringId { - value: u32, -} - -impl StringId { - pub(crate) fn new(index: usize) -> Self { - Self { - value: index as u32, - } - } - - pub(crate) fn index(self) -> usize { - self.value as usize - } -} - -#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] -pub struct MatchSpecId(u32); - -impl MatchSpecId { - fn new(index: usize) -> Self { - Self(index as u32) - } - - pub(crate) fn index(self) -> usize { - self.0 as usize - } -} - -pub struct Pool { - pub(crate) solvables: Vec, +/// A pool for interning data related to the available packages +pub struct Pool<'a> { + /// All the solvables that have been registered + pub(crate) solvables: Vec>, /// The total amount of registered repos total_repos: u32, - /// Interned strings - pub(crate) strings_to_ids: HashMap, - strings: Vec, + /// Interned package names + package_names: Vec, + + /// Map from package names to the id of their interned counterpart + pub(crate) names_to_ids: HashMap, + + /// Map from interned package names to the solvables that have that name + pub(crate) packages_by_name: Vec>, /// Interned match specs - match_specs_to_ids: HashMap, pub(crate) match_specs: Vec, + /// Map from match spec strings to the id of their interned counterpart + match_specs_to_ids: HashMap, + /// Cached candidates for each match spec, indexed by their MatchSpecId pub(crate) match_spec_to_candidates: Vec>>, + /// Cached forbidden solvables for each match spec, indexed by their MatchSpecId pub(crate) match_spec_to_forbidden: Vec>>, - - // TODO: eventually we could turn this into a Vec, making sure we have a separate interning - // scheme for package names - pub(crate) packages_by_name: HashMap>, } -impl Default for Pool { +impl<'a> Default for Pool<'a> { fn default() -> Self { Self { solvables: vec![Solvable::new_root()], total_repos: 0, - strings_to_ids: HashMap::new(), - strings: Vec::new(), - - packages_by_name: HashMap::default(), + names_to_ids: HashMap::new(), + package_names: Vec::new(), + packages_by_name: Vec::new(), match_specs_to_ids: HashMap::default(), match_specs: Vec::new(), @@ -87,37 +54,59 @@ impl Default for Pool { } } -impl Pool { +impl<'a> Pool<'a> { + /// Creates a new [`Pool`] pub fn new() -> Self { Self::default() } - pub fn new_repo(&mut self, _url: impl AsRef) -> RepoId { + /// Registers a new repo (i.e. a source of packages) + pub fn new_repo(&mut self) -> RepoId { let id = RepoId::new(self.total_repos); self.total_repos += 1; id } - /// Adds a new solvable to a repo - pub fn add_package(&mut self, repo_id: RepoId, record: &'static PackageRecord) -> SolvableId { + /// Adds a new solvable to a repo and returns it's [`SolvableId`] + pub fn add_package(&mut self, repo_id: RepoId, record: &'a PackageRecord) -> SolvableId { assert!(self.solvables.len() <= u32::MAX as usize); - let name = self.intern_str(&record.name); + let name = self.intern_package_name(&record.name); let solvable_id = SolvableId::new(self.solvables.len()); self.solvables .push(Solvable::new_package(repo_id, name, record)); - assert!(repo_id.0 < self.total_repos); - - self.packages_by_name - .entry(name) - .or_insert(Vec::new()) - .push(solvable_id); + self.packages_by_name[name.index()].push(solvable_id); solvable_id } + /// Resets the solvable associated to the id, and assigns the provided package to it + pub fn reset_package( + &mut self, + repo_id: RepoId, + solvable_id: SolvableId, + record: &'a PackageRecord, + ) { + let name = self.intern_package_name(&record.name); + self.solvables[solvable_id.index()] = Solvable::new_package(repo_id, name, record); + } + + /// Registers a dependency for the provided solvable + pub fn add_dependency(&mut self, solvable_id: SolvableId, match_spec: String) { + let match_spec_id = self.intern_matchspec(match_spec); + let solvable = self.solvables[solvable_id.index()].package_mut(); + solvable.dependencies.push(match_spec_id); + } + + /// Registers a constrains for the provided solvable + pub fn add_constrains(&mut self, solvable_id: SolvableId, match_spec: String) { + let match_spec_id = self.intern_matchspec(match_spec); + let solvable = self.solvables[solvable_id.index()].package_mut(); + solvable.constrains.push(match_spec_id); + } + pub(crate) fn intern_matchspec(&mut self, match_spec: String) -> MatchSpecId { let next_index = self.match_specs.len(); match self.match_specs_to_ids.entry(match_spec) { @@ -138,27 +127,17 @@ impl Pool { } } - pub fn reset_package( - &mut self, - repo_id: RepoId, - solvable_id: SolvableId, - record: &'static PackageRecord, - ) { - let name = self.intern_str(&record.name); - self.solvables[solvable_id.index()] = Solvable::new_package(repo_id, name, record); - } - // This function does not take `self`, because otherwise we run into problems with borrowing // when we want to use it together with other pool functions - pub(crate) fn get_candidates<'a>( - match_specs: &'a [MatchSpec], - strings_to_ids: &'a HashMap, - solvables: &'a [Solvable], - packages_by_name: &'a HashMap>, - match_spec_to_candidates: &'a mut [Option>], - favored_map: &HashMap, + pub(crate) fn get_candidates<'b>( + match_specs: &[MatchSpec], + strings_to_ids: &HashMap, + solvables: &[Solvable], + packages_by_name: &[Vec], + match_spec_to_candidates: &'b mut [Option>], + favored_map: &HashMap, match_spec_id: MatchSpecId, - ) -> &'a [SolvableId] { + ) -> &'b [SolvableId] { let candidates = match_spec_to_candidates[match_spec_id.index()].get_or_insert_with(|| { let match_spec = &match_specs[match_spec_id.index()]; let match_spec_name = match_spec @@ -170,7 +149,7 @@ impl Pool { Some(name_id) => name_id, }; - let mut pkgs: Vec<_> = packages_by_name[name_id] + let mut pkgs: Vec<_> = packages_by_name[name_id.index()] .iter() .cloned() .filter(|solvable| match_spec.matches(solvables[solvable.index()].package().record)) @@ -188,7 +167,8 @@ impl Pool { if let Some(&favored_id) = favored_map.get(name_id) { if let Some(pos) = pkgs.iter().position(|&s| s == favored_id) { - pkgs.swap(0, pos); + let removed = pkgs.remove(pos); + pkgs.insert(0, removed); } } @@ -200,14 +180,14 @@ impl Pool { // This function does not take `self`, because otherwise we run into problems with borrowing // when we want to use it together with other pool functions - pub(crate) fn get_forbidden<'a>( - match_specs: &'a [MatchSpec], - strings_to_ids: &'a HashMap, - solvables: &'a [Solvable], - packages_by_name: &'a HashMap>, - match_spec_to_forbidden: &'a mut [Option>], + pub(crate) fn get_forbidden<'b>( + match_specs: &[MatchSpec], + strings_to_ids: &HashMap, + solvables: &[Solvable], + packages_by_name: &[Vec], + match_spec_to_forbidden: &'b mut [Option>], match_spec_id: MatchSpecId, - ) -> &'a [SolvableId] { + ) -> &'b [SolvableId] { let candidates = match_spec_to_forbidden[match_spec_id.index()].get_or_insert_with(|| { let match_spec = &match_specs[match_spec_id.index()]; let match_spec_name = match_spec @@ -219,7 +199,7 @@ impl Pool { Some(name_id) => name_id, }; - packages_by_name[name_id] + packages_by_name[name_id.index()] .iter() .cloned() .filter(|solvable| { @@ -231,44 +211,25 @@ impl Pool { candidates.as_slice() } - pub fn add_dependency(&mut self, solvable_id: SolvableId, match_spec: String) { - let match_spec_id = self.intern_matchspec(match_spec); - let solvable = self.solvables[solvable_id.index()].package_mut(); - solvable.dependencies.push(match_spec_id); - } - - pub fn add_constrains(&mut self, solvable_id: SolvableId, match_spec: String) { - let match_spec_id = self.intern_matchspec(match_spec); - let solvable = self.solvables[solvable_id.index()].package_mut(); - solvable.constrains.push(match_spec_id); - } - - pub(crate) fn nsolvables(&self) -> u32 { - self.solvables.len() as u32 - } - - /// Interns string like types into a `Pool` returning a `StringId` - pub(crate) fn intern_str>(&mut self, str: T) -> StringId { - let next_id = StringId::new(self.strings_to_ids.len()); - match self.strings_to_ids.entry(str.into()) { + /// Interns package names into a `Pool` returning a `NameId` + fn intern_package_name>(&mut self, str: T) -> NameId { + let next_id = NameId::new(self.names_to_ids.len()); + match self.names_to_ids.entry(str.into()) { Entry::Occupied(e) => *e.get(), Entry::Vacant(e) => { - self.strings.push(e.key().clone()); + self.package_names.push(e.key().clone()); + self.packages_by_name.push(Vec::new()); e.insert(next_id); next_id } } } - pub fn resolve_string(&self, string_id: StringId) -> &str { - &self.strings[string_id.index()] - } - - /// Returns a string describing the last error associated to this pool, or "no error" if there - /// were no errors - pub fn last_error(&self) -> String { - // See pool_errstr - "no error".to_string() + /// Returns the package name corresponding to the provided id + /// + /// Panics if the package name is not found in the pool + pub fn resolve_package_name(&self, name_id: NameId) -> &str { + &self.package_names[name_id.index()] } /// Resolves the id to a solvable @@ -281,7 +242,7 @@ impl Pool { /// Resolves the id to a solvable /// /// Panics if the solvable is not found in the pool - pub fn resolve_solvable_mut(&mut self, id: SolvableId) -> &mut PackageSolvable { + pub fn resolve_solvable_mut(&mut self, id: SolvableId) -> &mut PackageSolvable<'a> { self.resolve_solvable_inner_mut(id).package_mut() } @@ -299,7 +260,7 @@ impl Pool { /// Resolves the id to a solvable /// /// Panics if the solvable is not found in the pool - pub(crate) fn resolve_solvable_inner_mut(&mut self, id: SolvableId) -> &mut Solvable { + pub(crate) fn resolve_solvable_inner_mut(&mut self, id: SolvableId) -> &mut Solvable<'a> { if id.index() < self.solvables.len() { &mut self.solvables[id.index()] } else { @@ -307,7 +268,7 @@ impl Pool { } } - pub(crate) fn resolve_match_spec(&self, id: MatchSpecId) -> &MatchSpec { + pub fn resolve_match_spec(&self, id: MatchSpecId) -> &MatchSpec { &self.match_specs[id.index()] } diff --git a/crates/libsolv_rs/src/problem.rs b/crates/libsolv_rs/src/problem.rs index 4fe734132..806ab6594 100644 --- a/crates/libsolv_rs/src/problem.rs +++ b/crates/libsolv_rs/src/problem.rs @@ -4,67 +4,19 @@ use std::fmt::Formatter; use std::rc::Rc; use itertools::Itertools; -use petgraph::graph::{DiGraph, EdgeIndex, NodeIndex}; -use petgraph::visit::{Bfs, EdgeRef}; +use petgraph::graph::{DiGraph, EdgeIndex, EdgeReference, NodeIndex}; +use petgraph::visit::{Bfs, DfsPostOrder, EdgeRef}; use petgraph::Direction; -use crate::pool::{MatchSpecId, Pool}; -use crate::rules::RuleKind; -use crate::solvable::SolvableId; -use crate::solver::{RuleId, Solver}; - -#[derive(Copy, Clone, Eq, PartialEq)] -pub enum ProblemNode { - Solvable(SolvableId), - UnresolvedDependency, -} - -impl ProblemNode { - fn solvable_id(self) -> SolvableId { - match self { - ProblemNode::Solvable(solvable_id) => solvable_id, - ProblemNode::UnresolvedDependency => { - panic!("expected solvable node, found unresolved dependency") - } - } - } -} - -#[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] -pub enum ProblemEdge { - Requires(MatchSpecId), - Conflict(Conflict), -} - -impl ProblemEdge { - fn try_requires(self) -> Option { - match self { - ProblemEdge::Requires(match_spec_id) => Some(match_spec_id), - ProblemEdge::Conflict(_) => None, - } - } - - fn requires(self) -> MatchSpecId { - match self { - ProblemEdge::Requires(match_spec_id) => match_spec_id, - ProblemEdge::Conflict(_) => panic!("expected requires edge, found conflict"), - } - } -} - -#[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] -pub enum Conflict { - Locked(SolvableId), - Constrains(MatchSpecId), - ForbidMultipleInstances, -} - -pub struct MergedProblemNode { - pub ids: Vec, -} +use crate::id::{MatchSpecId, RuleId, SolvableId}; +use crate::pool::Pool; +use crate::solver::rule::Rule; +use crate::solver::Solver; +/// Represents the cause of the solver being unable to find a solution #[derive(Debug)] pub struct Problem { + /// The rules involved in an unsatisfiable conflict rules: Vec, } @@ -79,8 +31,8 @@ impl Problem { } } + /// Generates a graph representation of the problem (see [`ProblemGraph`] for details) pub fn graph(&self, solver: &Solver) -> ProblemGraph { - println!("=== Build graph"); let mut graph = DiGraph::::default(); let mut nodes: HashMap = HashMap::default(); @@ -90,9 +42,9 @@ impl Problem { for rule_id in &self.rules { let rule = &solver.rules[rule_id.index()]; match rule.kind { - RuleKind::InstallRoot => (), - RuleKind::Learnt(..) => unreachable!(), - RuleKind::Requires(package_id, match_spec_id) => { + Rule::InstallRoot => (), + Rule::Learnt(..) => unreachable!(), + Rule::Requires(package_id, match_spec_id) => { let package_node = Self::add_node(&mut graph, &mut nodes, package_id); let candidates = solver.pool().match_spec_to_candidates[match_spec_id.index()] @@ -121,18 +73,19 @@ impl Problem { } } } - RuleKind::ForbidMultipleInstances(instance1_id, instance2_id) => { + Rule::Lock(locked, forbidden) => { + let node2_id = Self::add_node(&mut graph, &mut nodes, forbidden); + let conflict = Conflict::Locked(locked); + graph.add_edge(root_node, node2_id, ProblemEdge::Conflict(conflict)); + } + Rule::ForbidMultipleInstances(instance1_id, instance2_id) => { let node1_id = Self::add_node(&mut graph, &mut nodes, instance1_id); let node2_id = Self::add_node(&mut graph, &mut nodes, instance2_id); - let conflict = if instance1_id.is_root() { - Conflict::Locked(instance2_id) - } else { - Conflict::ForbidMultipleInstances - }; + let conflict = Conflict::ForbidMultipleInstances; graph.add_edge(node1_id, node2_id, ProblemEdge::Conflict(conflict)); } - RuleKind::Constrains(package_id, dep_id) => { + Rule::Constrains(package_id, dep_id) => { let package_node = Self::add_node(&mut graph, &mut nodes, package_id); let dep_node = Self::add_node(&mut graph, &mut nodes, dep_id); @@ -179,7 +132,7 @@ impl Problem { ProblemGraph { graph, root_node, - unresolved_dependency_node: unresolved_node, + unresolved_node, } } @@ -193,71 +146,155 @@ impl Problem { .or_insert_with(|| graph.add_node(ProblemNode::Solvable(solvable_id))) } + /// Display a user-friendly error explaining the problem pub fn display_user_friendly<'a>(&self, solver: &'a Solver) -> DisplayUnsat<'a> { let graph = self.graph(solver); + DisplayUnsat::new(graph, solver.pool()) + } +} - // TODO: remove - graph.graphviz(solver.pool()); +/// A node in the graph representation of a [`Problem`] +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum ProblemNode { + Solvable(SolvableId), + UnresolvedDependency, +} - DisplayUnsat::new(graph, solver.pool()) +impl ProblemNode { + fn solvable_id(self) -> SolvableId { + match self { + ProblemNode::Solvable(solvable_id) => solvable_id, + ProblemNode::UnresolvedDependency => { + panic!("expected solvable node, found unresolved dependency") + } + } + } +} + +/// An edge in the graph representation of a [`Problem`] +#[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] +pub enum ProblemEdge { + Requires(MatchSpecId), + Conflict(Conflict), +} + +impl ProblemEdge { + fn try_requires(self) -> Option { + match self { + ProblemEdge::Requires(match_spec_id) => Some(match_spec_id), + ProblemEdge::Conflict(_) => None, + } + } + + fn requires(self) -> MatchSpecId { + match self { + ProblemEdge::Requires(match_spec_id) => match_spec_id, + ProblemEdge::Conflict(_) => panic!("expected requires edge, found conflict"), + } } } +#[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] +pub enum Conflict { + Locked(SolvableId), + Constrains(MatchSpecId), + ForbidMultipleInstances, +} + +/// Represents a node that has been merged with others +/// +/// Merging is done to simplify error messages, and happens when a group of nodes satisfies the +/// following criteria: +/// +/// - They all have the same name +/// - They all have the same predecessor nodes +/// - They all have the same successor nodes +/// - None of them have incoming conflicting edges +pub struct MergedProblemNode { + pub ids: Vec, +} + +/// Graph representation of [`Problem`] +/// +/// The root of the graph is the "root solvable". Note that not all the solvable's requirements are +/// included in the graph, only those that are directly or indirectly involved in the conflict. See +/// [`ProblemNode`] and [`ProblemEdge`] for the kinds of nodes and edges that make up the graph. pub struct ProblemGraph { graph: DiGraph, root_node: NodeIndex, - unresolved_dependency_node: Option, + unresolved_node: Option, } impl ProblemGraph { - fn graphviz(&self, pool: &Pool) { + /// Returns a graph in its graphviz representation + pub fn graphviz(&self, pool: &Pool, simplify: bool) { let graph = &self.graph; + let merged_nodes = if simplify { + self.simplify(pool) + } else { + HashMap::new() + }; + println!("digraph {{"); - let mut bfs = Bfs::new(&graph, self.root_node); - while let Some(nx) = bfs.next(&graph) { - match graph.node_weight(nx).as_ref().unwrap() { - ProblemNode::Solvable(id) => { - let solvable = pool.resolve_solvable_inner(*id); - for edge in graph.edges_directed(nx, Direction::Outgoing) { - let target = *graph.node_weight(edge.target()).unwrap(); - - let color = match edge.weight() { - ProblemEdge::Requires(_) - if target != ProblemNode::UnresolvedDependency => - { - "black" - } - _ => "red", - }; + for nx in graph.node_indices() { + let id = match graph.node_weight(nx).as_ref().unwrap() { + ProblemNode::Solvable(id) => *id, + _ => continue, + }; - let label = match edge.weight() { - ProblemEdge::Requires(match_spec_id) - | ProblemEdge::Conflict(Conflict::Constrains(match_spec_id)) => { - pool.resolve_match_spec(*match_spec_id).to_string() - } - ProblemEdge::Conflict(Conflict::ForbidMultipleInstances) - | ProblemEdge::Conflict(Conflict::Locked(_)) => { - "already installed".to_string() - } - }; + // If this is a merged node, skip it unless it is the first one in the group + if let Some(merged) = merged_nodes.get(&id) { + if id != merged.ids[0] { + continue; + } + } - let target = match target { - ProblemNode::Solvable(solvable_2) => pool - .resolve_solvable_inner(solvable_2) - .display() - .to_string(), - ProblemNode::UnresolvedDependency => "unresolved".to_string(), - }; + let solvable = pool.resolve_solvable_inner(id); + let mut added_edges = HashSet::new(); + for edge in graph.edges_directed(nx, Direction::Outgoing) { + let target = *graph.node_weight(edge.target()).unwrap(); - println!( - "\"{}\" -> \"{}\"[color={color}, label=\"{label}\"];", - solvable.display(), - target - ); + let color = match edge.weight() { + ProblemEdge::Requires(_) if target != ProblemNode::UnresolvedDependency => { + "black" } - } - ProblemNode::UnresolvedDependency => {} + _ => "red", + }; + + let label = match edge.weight() { + ProblemEdge::Requires(match_spec_id) + | ProblemEdge::Conflict(Conflict::Constrains(match_spec_id)) => { + pool.resolve_match_spec(*match_spec_id).to_string() + } + ProblemEdge::Conflict(Conflict::ForbidMultipleInstances) + | ProblemEdge::Conflict(Conflict::Locked(_)) => "already installed".to_string(), + }; + + let target = match target { + ProblemNode::Solvable(mut solvable_2) => { + // If the target node has been merged, replace it by the first id in the group + if let Some(merged) = merged_nodes.get(&solvable_2) { + solvable_2 = merged.ids[0]; + + // Skip the edge if we would be adding a duplicate + if !added_edges.insert(solvable_2) { + continue; + } + } + + pool.resolve_solvable_inner(solvable_2) + .display() + .to_string() + } + ProblemNode::UnresolvedDependency => "unresolved".to_string(), + }; + + println!( + "\"{}\" -> \"{}\"[color={color}, label=\"{label}\"];", + solvable.display(), + target + ); } } println!("}}"); @@ -295,7 +332,7 @@ impl ProblemGraph { .collect(); let successors: Vec<_> = graph .edges(node_id) - .map(|e| (e.target(), *e.weight())) + .map(|e| e.target()) .sorted_unstable() .collect(); @@ -325,84 +362,153 @@ impl ProblemGraph { } fn get_installable_set(&self) -> HashSet { - let mut non_installable: HashSet = HashSet::new(); - - // Definition: a package is installable if all paths from it to the graph's leaves pass - // through non-conflicting edges (i.e. each dependency, and each dependency's dependencies, - // etc, can be installed) - - // Gather the starting set of conflicting edges: - // * Edges into the unresolved dependency node - // * Edges equal to `ProblemEdge::Conflict` - let mut conflicting_edges = Vec::new(); - - if let Some(unresolved_nx) = self.unresolved_dependency_node { - conflicting_edges.extend( - self.graph - .edges_directed(unresolved_nx, Direction::Incoming), - ); + let mut installable = HashSet::new(); + + // Definition: a package is installable if it does not have any outgoing conflicting edges + // and if each of its dependencies has at least one installable option. + + // Algorithm: propagate installability bottom-up + let mut dfs = DfsPostOrder::new(&self.graph, self.root_node); + 'outer_loop: while let Some(nx) = dfs.next(&self.graph) { + if self.unresolved_node == Some(nx) { + // The unresolved node isn't installable + continue; + } + + let outgoing_conflicts = self + .graph + .edges_directed(nx, Direction::Outgoing) + .any(|e| matches!(e.weight(), ProblemEdge::Conflict(_))); + if outgoing_conflicts { + // Nodes with outgoing conflicts aren't installable + continue; + } + + // Edges grouped by dependency + let dependencies = self + .graph + .edges_directed(nx, Direction::Outgoing) + .map(|e| match e.weight() { + ProblemEdge::Requires(match_spec_id) => (match_spec_id, e.target()), + ProblemEdge::Conflict(_) => unreachable!(), + }) + .group_by(|(&match_spec_id, _)| match_spec_id); + + for (_, mut deps) in &dependencies { + if deps.all(|(_, target)| !installable.contains(&target)) { + // No installable options for this dep + continue 'outer_loop; + } + } + + // The package is installable! + installable.insert(nx); } - conflicting_edges.extend( - self.graph - .edge_references() - .filter(|e| matches!(e.weight(), ProblemEdge::Conflict(..))), - ); - - // Propagate conflicts up the graph - while let Some(edge) = conflicting_edges.pop() { - let source = edge.source(); - if non_installable.insert(source) { - // Visited for the first time, so make sure the predecessors are also marked as non-installable - conflicting_edges.extend(self.graph.edges_directed(source, Direction::Incoming)); + installable + } + + fn get_missing_set(&self) -> HashSet { + // Definition: a package is missing if it is not involved in any conflicts, yet it is not + // installable + + let mut missing = HashSet::new(); + match self.unresolved_node { + None => return missing, + Some(nx) => missing.insert(nx), + }; + + // Algorithm: propagate missing bottom-up + let mut dfs = DfsPostOrder::new(&self.graph, self.root_node); + while let Some(nx) = dfs.next(&self.graph) { + let outgoing_conflicts = self + .graph + .edges_directed(nx, Direction::Outgoing) + .any(|e| matches!(e.weight(), ProblemEdge::Conflict(_))); + if outgoing_conflicts { + // Nodes with outgoing conflicts aren't missing + continue; + } + + // Edges grouped by dependency + let dependencies = self + .graph + .edges_directed(nx, Direction::Outgoing) + .map(|e| match e.weight() { + ProblemEdge::Requires(match_spec_id) => (match_spec_id, e.target()), + ProblemEdge::Conflict(_) => unreachable!(), + }) + .group_by(|(&match_spec_id, _)| match_spec_id); + + // Missing if at least one dependency is missing + if dependencies + .into_iter() + .any(|(_, mut deps)| deps.all(|(_, target)| missing.contains(&target))) + { + missing.insert(nx); } } - // Installable packages are all nodes that were not marked as non-installable - self.graph - .node_indices() - .filter(|nx| !non_installable.contains(nx)) - .collect() + missing } } +/// A struct implementing [`fmt::Display`] that generates a user-friendly representation of a +/// problem graph pub struct DisplayUnsat<'a> { graph: ProblemGraph, merged_candidates: HashMap>, installable_set: HashSet, - pool: &'a Pool, + missing_set: HashSet, + pool: &'a Pool<'a>, } impl<'a> DisplayUnsat<'a> { pub fn new(graph: ProblemGraph, pool: &'a Pool) -> Self { let merged_candidates = graph.simplify(pool); let installable_set = graph.get_installable_set(); + let missing_set = graph.get_missing_set(); Self { graph, merged_candidates, installable_set, + missing_set, pool, } } -} -impl fmt::Display for DisplayUnsat<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let graph = &self.graph.graph; - let installable_nodes = &self.installable_set; - let mut reported: HashSet = HashSet::new(); + fn get_indent(depth: usize, top_level_indent: bool) -> String { + let depth_correction = if depth > 0 && !top_level_indent { 1 } else { 0 }; + + let mut indent = " ".repeat((depth - depth_correction) * 4); + let display_tree_char = depth != 0 || top_level_indent; + if display_tree_char { + indent.push_str("|-- "); + } + + indent + } + + fn fmt_graph( + &self, + f: &mut Formatter<'_>, + top_level_edges: &[EdgeReference], + top_level_indent: bool, + ) -> fmt::Result { pub enum DisplayOp { Requirement(MatchSpecId, Vec), Candidate(NodeIndex), } - writeln!(f, "The following packages are incompatible")?; + let graph = &self.graph.graph; + let installable_nodes = &self.installable_set; + let mut reported: HashSet = HashSet::new(); // Note: we are only interested in requires edges here - let mut stack = graph - .edges(self.graph.root_node) + let mut stack = top_level_edges + .iter() .filter(|e| e.weight().try_requires().is_some()) .group_by(|e| e.weight().requires()) .into_iter() @@ -418,7 +524,7 @@ impl fmt::Display for DisplayUnsat<'_> { .map(|(match_spec_id, edges)| (DisplayOp::Requirement(match_spec_id, edges), 0)) .collect::>(); while let Some((node, depth)) = stack.pop() { - let indent = " ".repeat(depth * 4); + let indent = Self::get_indent(depth, top_level_indent); match node { DisplayOp::Requirement(match_spec_id, edges) => { @@ -436,19 +542,19 @@ impl fmt::Display for DisplayUnsat<'_> { if missing { // No candidates for requirement if depth == 0 { - writeln!(f, "{indent}|-- No candidates where found for {req}.")?; + writeln!(f, "{indent}No candidates where found for {req}.")?; } else { - writeln!(f, "{indent}|-- {req}, for which no candidates where found.",)?; + writeln!(f, "{indent}{req}, for which no candidates where found.",)?; } } else if installable { // Package can be installed (only mentioned for top-level requirements) if depth == 0 { writeln!( f, - "|-- {req} can be installed with any of the following options:" + "{indent}{req} can be installed with any of the following options:" )?; } else { - writeln!(f, "{indent}|-- {req}, which can be installed with any of the following options:")?; + writeln!(f, "{indent}{req}, which can be installed with any of the following options:")?; } stack.extend( @@ -467,9 +573,9 @@ impl fmt::Display for DisplayUnsat<'_> { } else { // Package cannot be installed (the conflicting requirement is further down the tree) if depth == 0 { - writeln!(f, "|-- {req} cannot be installed because there are no viable options:")?; + writeln!(f, "{indent}{req} cannot be installed because there are no viable options:")?; } else { - writeln!(f, "{indent}|-- {req}, which cannot be installed because there are no viable options:")?; + writeln!(f, "{indent}{req}, which cannot be installed because there are no viable options:")?; } stack.extend(edges.iter().map(|&e| { @@ -499,22 +605,48 @@ impl fmt::Display for DisplayUnsat<'_> { solvable.record.version.to_string() }; - let is_conflict_source = graph - .edges(candidate) - .any(|e| e.weight().try_requires().is_none()); - let is_leaf = graph - .edges(candidate) - .next() - .is_none(); - - if is_conflict_source { - writeln!(f, "{indent}|-- {} {version}, which conflicts with the versions reported above.", solvable.record.name)?; - } else if is_leaf { - writeln!(f, "{indent}|-- {} {version}", solvable.record.name)?; + let already_installed = graph.edges(candidate).any(|e| { + e.weight() == &ProblemEdge::Conflict(Conflict::ForbidMultipleInstances) + }); + let constrains_conflict = graph.edges(candidate).any(|e| { + matches!(e.weight(), ProblemEdge::Conflict(Conflict::Constrains(_))) + }); + let is_leaf = graph.edges(candidate).next().is_none(); + + if is_leaf { + writeln!(f, "{indent}{} {version}", solvable.record.name)?; + } else if already_installed { + writeln!(f, "{indent}{} {version}, which conflicts with the versions reported above.", solvable.record.name)?; + } else if constrains_conflict { + let match_specs = graph + .edges(candidate) + .flat_map(|e| match e.weight() { + ProblemEdge::Conflict(Conflict::Constrains(match_spec_id)) => { + Some(match_spec_id) + } + _ => None, + }) + .dedup(); + + writeln!( + f, + "{indent}{} {version} would constrain", + solvable.record.name + )?; + + let indent = Self::get_indent(depth + 1, top_level_indent); + for &match_spec_id in match_specs { + let match_spec = self.pool.resolve_match_spec(match_spec_id); + writeln!( + f, + "{indent}{} , which conflicts with any installable versions previously reported", + match_spec + )?; + } } else { writeln!( f, - "{indent}|-- {} {version} would require", + "{indent}{} {version} would require", solvable.record.name )?; let requirements = graph @@ -541,25 +673,47 @@ impl fmt::Display for DisplayUnsat<'_> { } } - // Report conflicts caused by locked dependencies - for e in graph.edges(self.graph.root_node) { - let conflict = match e.weight() { - ProblemEdge::Requires(_) => continue, - ProblemEdge::Conflict(conflict) => conflict, - }; + Ok(()) + } +} - // The only possible conflict at the root level is a Locked conflict - let locked_id = match conflict { - Conflict::Constrains(_) | Conflict::ForbidMultipleInstances => unreachable!(), - &Conflict::Locked(solvable_id) => solvable_id, - }; +impl fmt::Display for DisplayUnsat<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let (top_level_missing, top_level_conflicts): (Vec<_>, _) = self + .graph + .graph + .edges(self.graph.root_node) + .partition(|e| self.missing_set.contains(&e.target())); - let locked = self.pool.resolve_solvable(locked_id); - writeln!( - f, - "|-- {} {} is locked, but another version is required as reported above", - locked.record.name, locked.record.version - )?; + if !top_level_missing.is_empty() { + self.fmt_graph(f, &top_level_missing, false)?; + } + + if !top_level_conflicts.is_empty() { + writeln!(f, "The following packages are incompatible")?; + self.fmt_graph(f, &top_level_conflicts, true)?; + + // Conflicts caused by locked dependencies + let indent = Self::get_indent(0, true); + for e in self.graph.graph.edges(self.graph.root_node) { + let conflict = match e.weight() { + ProblemEdge::Requires(_) => continue, + ProblemEdge::Conflict(conflict) => conflict, + }; + + // The only possible conflict at the root level is a Locked conflict + let locked_id = match conflict { + Conflict::Constrains(_) | Conflict::ForbidMultipleInstances => unreachable!(), + &Conflict::Locked(solvable_id) => solvable_id, + }; + + let locked = self.pool.resolve_solvable(locked_id); + writeln!( + f, + "{indent}{} {} is locked, but another version is required as reported above", + locked.record.name, locked.record.version + )?; + } } Ok(()) diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_1.snap b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_1.snap deleted file mode 100644 index c5751bf6e..000000000 --- a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_1.snap +++ /dev/null @@ -1,7 +0,0 @@ ---- -source: crates/libsolv_rs/src/solver.rs -expression: error ---- -The following packages are incompatible -|-- No candidates where found for fghj. - diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_2.snap b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_2.snap deleted file mode 100644 index d6f09b808..000000000 --- a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_2.snap +++ /dev/null @@ -1,7 +0,0 @@ ---- -source: crates/libsolv_rs/src/solver.rs -expression: error ---- -The following packages are incompatible -|-- No candidates where found for B 14.*. - diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_1.snap b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_1.snap deleted file mode 100644 index c55e00733..000000000 --- a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_1.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: crates/libsolv_rs/src/solver.rs -expression: error ---- -The following packages are incompatible -|-- asdf cannot be installed because there are no viable options: - |-- asdf 1.2.3 would require - |-- C >1, for which no candidates where found. - diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_2.snap b/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_2.snap deleted file mode 100644 index 2a10f80ed..000000000 --- a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_2.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: crates/libsolv_rs/src/solver.rs -expression: error ---- -The following packages are incompatible -|-- A <1000 cannot be installed because there are no viable options: - |-- A 41 would require - |-- B <20, for which no candidates where found. - diff --git a/crates/libsolv_rs/src/solvable.rs b/crates/libsolv_rs/src/solvable.rs index 4a3e7e6e3..629c07764 100644 --- a/crates/libsolv_rs/src/solvable.rs +++ b/crates/libsolv_rs/src/solvable.rs @@ -1,70 +1,19 @@ -use crate::pool::{MatchSpecId, RepoId, StringId}; +use crate::id::MatchSpecId; +use crate::id::{NameId, RepoId}; use rattler_conda_types::{PackageRecord, Version}; use std::fmt::{Display, Formatter}; -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)] -pub struct SolvableId(u32); - -impl SolvableId { - pub(crate) fn new(index: usize) -> Self { - Self(index as u32) - } - - pub(crate) fn root() -> Self { - Self(0) - } - - pub(crate) fn is_root(self) -> bool { - self.0 == 0 - } - - pub(crate) fn null() -> Self { - Self(u32::MAX) - } - - pub(crate) fn is_null(self) -> bool { - self.0 == u32::MAX - } - - pub(crate) fn index(self) -> usize { - self.0 as usize - } -} - -pub(crate) struct SolvableDisplay<'a> { - name: &'a str, - version: Option<&'a Version>, - build: Option<&'a str>, -} - -impl Display for SolvableDisplay<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.name)?; - if let Some(version) = self.version { - write!(f, " {}", version)?; - } - - if let Some(build) = self.build { - if !build.is_empty() { - write!(f, " {}", build)?; - } - } - - Ok(()) - } -} - -pub struct PackageSolvable { +/// A solvable that was derived from a Conda package +pub struct PackageSolvable<'a> { pub(crate) repo_id: RepoId, pub(crate) dependencies: Vec, pub(crate) constrains: Vec, - pub(crate) record: &'static PackageRecord, - pub(crate) name: StringId, - // pub version: StringId, + pub(crate) record: &'a PackageRecord, + pub(crate) name: NameId, pub metadata: SolvableMetadata, } -impl PackageSolvable { +impl PackageSolvable<'_> { pub fn repo_id(&self) -> RepoId { self.repo_id } @@ -75,27 +24,25 @@ pub struct SolvableMetadata { pub original_index: Option, } -pub(crate) struct Solvable { - pub(crate) inner: SolvableInner, +/// Represents a package that can be installed +pub(crate) struct Solvable<'a> { + pub(crate) inner: SolvableInner<'a>, } -pub(crate) enum SolvableInner { +/// The inner representation of a solvable, which can be either a Conda package or the root solvable +pub(crate) enum SolvableInner<'a> { Root(Vec), - Package(PackageSolvable), + Package(PackageSolvable<'a>), } -impl Solvable { - pub(crate) fn new_root() -> Self { - Self { +impl<'a> Solvable<'a> { + pub(crate) fn new_root() -> Solvable<'static> { + Solvable { inner: SolvableInner::Root(Vec::new()), } } - pub(crate) fn new_package( - repo_id: RepoId, - name: StringId, - record: &'static PackageRecord, - ) -> Self { + pub(crate) fn new_package(repo_id: RepoId, name: NameId, record: &'a PackageRecord) -> Self { Self { inner: SolvableInner::Package(PackageSolvable { repo_id, @@ -137,7 +84,7 @@ impl Solvable { } } - pub(crate) fn get_package_mut(&mut self) -> Option<&mut PackageSolvable> { + pub(crate) fn get_package_mut<'b>(&'b mut self) -> Option<&'b mut PackageSolvable<'a>> { match &mut self.inner { SolvableInner::Root(_) => None, SolvableInner::Package(p) => Some(p), @@ -148,7 +95,30 @@ impl Solvable { self.get_package().expect("unexpected root solvable") } - pub fn package_mut(&mut self) -> &mut PackageSolvable { + pub fn package_mut<'b>(&'b mut self) -> &'b mut PackageSolvable<'a> { self.get_package_mut().expect("unexpected root solvable") } } + +pub(crate) struct SolvableDisplay<'a> { + name: &'a str, + version: Option<&'a Version>, + build: Option<&'a str>, +} + +impl Display for SolvableDisplay<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name)?; + if let Some(version) = self.version { + write!(f, " {}", version)?; + } + + if let Some(build) = self.build { + if !build.is_empty() { + write!(f, " {}", build)?; + } + } + + Ok(()) + } +} diff --git a/crates/libsolv_rs/src/solve_jobs.rs b/crates/libsolv_rs/src/solve_jobs.rs index d00aa5c13..b9bd05cf3 100644 --- a/crates/libsolv_rs/src/solve_jobs.rs +++ b/crates/libsolv_rs/src/solve_jobs.rs @@ -1,4 +1,4 @@ -use crate::solvable::SolvableId; +use crate::id::SolvableId; use rattler_conda_types::MatchSpec; #[derive(Default)] diff --git a/crates/libsolv_rs/src/solver/decision.rs b/crates/libsolv_rs/src/solver/decision.rs new file mode 100644 index 000000000..e9b9af735 --- /dev/null +++ b/crates/libsolv_rs/src/solver/decision.rs @@ -0,0 +1,19 @@ +use crate::id::{RuleId, SolvableId}; + +/// Represents an assignment to a variable +#[derive(Copy, Clone, Eq, PartialEq)] +pub(crate) struct Decision { + pub(crate) solvable_id: SolvableId, + pub(crate) value: bool, + pub(crate) derived_from: RuleId, +} + +impl Decision { + pub(crate) fn new(solvable: SolvableId, value: bool, derived_from: RuleId) -> Self { + Self { + solvable_id: solvable, + value, + derived_from, + } + } +} diff --git a/crates/libsolv_rs/src/decision_map.rs b/crates/libsolv_rs/src/solver/decision_map.rs similarity index 83% rename from crates/libsolv_rs/src/decision_map.rs rename to crates/libsolv_rs/src/solver/decision_map.rs index ec6c16c1f..62cc2e760 100644 --- a/crates/libsolv_rs/src/decision_map.rs +++ b/crates/libsolv_rs/src/solver/decision_map.rs @@ -1,11 +1,11 @@ -use crate::solvable::SolvableId; +use crate::id::SolvableId; use std::cmp::Ordering; -/// Map of all available solvables +/// A map of the assignments to all solvables pub(crate) struct DecisionMap { /// = 0: undecided - /// > 0: level of decision when installed - /// < 0: level of decision when conflict + /// > 0: level of decision when the solvable is set to true + /// < 0: level of decision when the solvable is set to false map: Vec, } diff --git a/crates/libsolv_rs/src/decision_tracker.rs b/crates/libsolv_rs/src/solver/decision_tracker.rs similarity index 84% rename from crates/libsolv_rs/src/decision_tracker.rs rename to crates/libsolv_rs/src/solver/decision_tracker.rs index 1de7ab351..c1a950359 100644 --- a/crates/libsolv_rs/src/decision_tracker.rs +++ b/crates/libsolv_rs/src/solver/decision_tracker.rs @@ -1,7 +1,9 @@ -use crate::decision_map::DecisionMap; -use crate::solvable::SolvableId; -use crate::solver::Decision; +use crate::id::SolvableId; +use crate::solver::decision::Decision; +use crate::solver::decision_map::DecisionMap; +/// Tracks the assignments to solvables, keeping a log that can be used to backtrack, and a map that +/// can be used to query the current value assigned pub(crate) struct DecisionTracker { map: DecisionMap, stack: Vec, @@ -78,6 +80,9 @@ impl DecisionTracker { (decision, self.map.level(top_decision.solvable_id)) } + /// Returns the next decision in the log for which unit propagation still needs to run + /// + /// Side-effect: the decision will be marked as propagated pub(crate) fn next_unpropagated(&mut self) -> Option { let &decision = self.stack[self.propagate_index..].iter().next()?; self.propagate_index += 1; diff --git a/crates/libsolv_rs/src/solver.rs b/crates/libsolv_rs/src/solver/mod.rs similarity index 78% rename from crates/libsolv_rs/src/solver.rs rename to crates/libsolv_rs/src/solver/mod.rs index 16c6260ab..c9bbee724 100644 --- a/crates/libsolv_rs/src/solver.rs +++ b/crates/libsolv_rs/src/solver/mod.rs @@ -1,76 +1,30 @@ -use crate::decision_tracker::DecisionTracker; -use crate::pool::{MatchSpecId, Pool, StringId}; +use crate::id::NameId; +use crate::id::{RuleId, SolvableId}; +use crate::pool::Pool; use crate::problem::Problem; -use crate::rules::{Literal, Rule, RuleKind}; -use crate::solvable::SolvableId; +use crate::solvable::SolvableInner; use crate::solve_jobs::SolveJobs; -use crate::watch_map::WatchMap; +use crate::transaction::{Transaction, TransactionKind}; use rattler_conda_types::MatchSpec; -use std::collections::{HashMap, HashSet}; -use std::fmt::{Display, Formatter}; +use std::collections::{HashMap, HashSet, VecDeque}; -#[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Debug, Hash)] -pub(crate) struct RuleId(u32); +use decision::Decision; +use decision_tracker::DecisionTracker; +use rule::{Literal, Rule, RuleState}; +use watch_map::WatchMap; -impl RuleId { - pub(crate) fn new(index: usize) -> Self { - Self(index as u32) - } - - pub(crate) fn install_root() -> Self { - Self(0) - } - - pub(crate) fn index(self) -> usize { - self.0 as usize - } - - fn is_null(self) -> bool { - self.0 == u32::MAX - } - - pub(crate) fn null() -> RuleId { - RuleId(u32::MAX) - } -} - -#[derive(Copy, Clone, Eq, PartialEq)] -pub(crate) struct Decision { - pub(crate) solvable_id: SolvableId, - pub(crate) value: bool, - pub(crate) derived_from: RuleId, -} - -impl Decision { - pub(crate) fn new(solvable: SolvableId, value: bool, derived_from: RuleId) -> Self { - Self { - solvable_id: solvable, - value, - derived_from, - } - } -} - -pub struct Transaction { - pub steps: Vec<(SolvableId, TransactionKind)>, -} - -#[derive(Copy, Clone, Debug)] -pub enum TransactionKind { - Install, -} +// TODO: remove pub from this +mod decision; +pub(crate) mod decision_map; +mod decision_tracker; +pub(crate) mod rule; +mod watch_map; -impl Display for TransactionKind { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } -} +pub struct Solver<'a> { + pool: Pool<'a>, -pub struct Solver { - pool: Pool, - - pub(crate) rules: Vec, + pub(crate) rules: Vec, watches: WatchMap, learnt_rules: Vec>, @@ -80,16 +34,16 @@ pub struct Solver { decision_tracker: DecisionTracker, } -impl Solver { +impl<'a> Solver<'a> { /// Create a solver, using the provided pool - pub fn new(pool: Pool) -> Self { + pub fn new(pool: Pool<'a>) -> Self { Self { rules: Vec::new(), watches: WatchMap::new(), learnt_rules: Vec::new(), - learnt_rules_start: RuleId(0), + learnt_rules_start: RuleId::null(), learnt_why: Vec::new(), - decision_tracker: DecisionTracker::new(pool.nsolvables()), + decision_tracker: DecisionTracker::new(pool.solvables.len() as u32), pool, } } @@ -108,7 +62,7 @@ impl Solver { // Clear state self.pool.root_solvable_mut().clear(); self.decision_tracker.clear(); - self.rules = vec![Rule::new(RuleKind::InstallRoot, &[], &self.pool)]; + self.rules = vec![RuleState::new(Rule::InstallRoot, &[], &self.pool)]; self.learnt_rules.clear(); self.learnt_why.clear(); @@ -119,24 +73,22 @@ impl Solver { favored_map.insert(name_id, favored_id); } - // Initialize the root solvable with the requested packages as dependencies - let mut visited_solvables = HashSet::default(); + // Populate the root solvable with the requested packages for match_spec in &jobs.install { let match_spec_id = self.pool.intern_matchspec(match_spec.to_string()); - let root_solvable = self.pool.root_solvable_mut(); - root_solvable.push(match_spec_id); - - // Recursively add rules for the current dep - self.add_rules_for_root_dep(&mut visited_solvables, &favored_map, match_spec_id); + self.pool.root_solvable_mut().push(match_spec_id); } + // Create rules for root's dependencies, and their dependencies, and so forth + self.add_rules_for_root_dep(&favored_map); + // Initialize rules ensuring only a single candidate per package name is installed - for candidates in self.pool.packages_by_name.values() { + for candidates in &self.pool.packages_by_name { // Each candidate gets a rule with each other candidate for (i, &candidate) in candidates.iter().enumerate() { for &other_candidate in &candidates[i + 1..] { - self.rules.push(Rule::new( - RuleKind::ForbidMultipleInstances(candidate, other_candidate), + self.rules.push(RuleState::new( + Rule::ForbidMultipleInstances(candidate, other_candidate), &self.learnt_rules, &self.pool, )); @@ -148,15 +100,13 @@ impl Solver { for &locked_solvable_id in &jobs.lock { // For each locked solvable, forbid other solvables with the same name let name = self.pool.resolve_solvable(locked_solvable_id).name; - if let Some(other_candidates) = self.pool.packages_by_name.get(&name) { - for &other_candidate in other_candidates { - if other_candidate != locked_solvable_id { - self.rules.push(Rule::new( - RuleKind::ForbidMultipleInstances(SolvableId::root(), other_candidate), - &self.learnt_rules, - &self.pool, - )); - } + for &other_candidate in &self.pool.packages_by_name[name.index()] { + if other_candidate != locked_solvable_id { + self.rules.push(RuleState::new( + Rule::Lock(locked_solvable_id, other_candidate), + &self.learnt_rules, + &self.pool, + )); } } } @@ -186,42 +136,23 @@ impl Solver { Ok(Transaction { steps }) } - fn add_rules_for_root_dep( - &mut self, - visited: &mut HashSet, - favored_map: &HashMap, - dep: MatchSpecId, - ) { - let mut candidate_stack = Vec::new(); - - // Gather direct candidates for the dependency - { - let candidates = Pool::get_candidates( - &self.pool.match_specs, - &self.pool.strings_to_ids, - &self.pool.solvables, - &self.pool.packages_by_name, - &mut self.pool.match_spec_to_candidates, - favored_map, - dep, - ); - for &candidate in candidates { - if visited.insert(candidate) { - candidate_stack.push(candidate); - } - } - } + fn add_rules_for_root_dep(&mut self, favored_map: &HashMap) { + let mut visited = HashSet::new(); + let mut queue = VecDeque::new(); - // Process candidates, adding their dependencies recursively - while let Some(candidate) = candidate_stack.pop() { - let solvable = self.pool.solvables[candidate.index()].package(); + queue.push_back(SolvableId::root()); - // Requires - for &dep in &solvable.dependencies { - // Ensure the candidates have their rules added - let dep_candidates = Pool::get_candidates( + while let Some(solvable_id) = queue.pop_front() { + let (deps, constrains) = match &self.pool.solvables[solvable_id.index()].inner { + SolvableInner::Root(deps) => (deps, &[] as &[_]), + SolvableInner::Package(pkg) => (&pkg.dependencies, pkg.constrains.as_slice()), + }; + + // Enqueue the candidates of the dependencies + for &dep in deps { + let candidates = Pool::get_candidates( &self.pool.match_specs, - &self.pool.strings_to_ids, + &self.pool.names_to_ids, &self.pool.solvables, &self.pool.packages_by_name, &mut self.pool.match_spec_to_candidates, @@ -229,25 +160,28 @@ impl Solver { dep, ); - for &dep_candidate in dep_candidates { - if visited.insert(dep_candidate) { - candidate_stack.push(dep_candidate); + for &candidate in candidates { + // Note: we skip candidates we have already seen + if visited.insert(candidate) { + queue.push_back(candidate); } } + } - // Create requires rule - self.rules.push(Rule::new( - RuleKind::Requires(candidate, dep), + // Requires + for &dep in deps { + self.rules.push(RuleState::new( + Rule::Requires(solvable_id, dep), &self.learnt_rules, &self.pool, )); } // Constrains - for &dep in &solvable.constrains { + for &dep in constrains { let dep_forbidden = Pool::get_forbidden( &self.pool.match_specs, - &self.pool.strings_to_ids, + &self.pool.names_to_ids, &self.pool.solvables, &self.pool.packages_by_name, &mut self.pool.match_spec_to_forbidden, @@ -256,21 +190,14 @@ impl Solver { .to_vec(); for dep in dep_forbidden { - self.rules.push(Rule::new( - RuleKind::Constrains(candidate, dep), + self.rules.push(RuleState::new( + Rule::Constrains(solvable_id, dep), &self.learnt_rules, &self.pool, )); } } } - - // Root has a requirement on this match spec - self.rules.push(Rule::new( - RuleKind::Requires(SolvableId::root(), dep), - &self.learnt_rules, - &self.pool, - )); } fn run_sat( @@ -314,7 +241,7 @@ impl Solver { // Assertions derived from requirements that cannot be fulfilled for (i, rule) in self.rules.iter().enumerate() { - if let RuleKind::Requires(solvable_id, _) = rule.kind { + if let Rule::Requires(solvable_id, _) = rule.kind { if !rule.has_watches() { // A requires rule without watches means it has a single literal (i.e. // there are no candidates) @@ -350,7 +277,7 @@ impl Solver { i += 1; // We are only interested in requires rules - let RuleKind::Requires(solvable_id, deps) = rule.kind else { + let Rule::Requires(solvable_id, deps) = rule.kind else { continue; }; @@ -454,7 +381,7 @@ impl Solver { let level = self.decision_tracker.level(decision.solvable_id); let action = if decision.value { "install" } else { "forbid" }; - if let RuleKind::ForbidMultipleInstances(..) = rule.kind { + if let Rule::ForbidMultipleInstances(..) = rule.kind { // Skip forbids rules, to reduce noise continue; } @@ -500,7 +427,7 @@ impl Solver { // Learnt assertions let learnt_rules_start = self.learnt_rules_start.index(); for (i, rule) in self.rules[learnt_rules_start..].iter().enumerate() { - let RuleKind::Learnt(learnt_index) = rule.kind else { + let Rule::Learnt(learnt_index) = rule.kind else { unreachable!(); }; @@ -609,10 +536,9 @@ impl Solver { if decided { match rule.kind { - RuleKind::InstallRoot - | RuleKind::Requires(_, _) - | RuleKind::Constrains(_, _) - | RuleKind::Learnt(_) => { + // Skip logging for ForbidMultipleInstances, which is so noisy + Rule::ForbidMultipleInstances(..) => {} + _ => { println!( "Propagate {} = {}. {:?}", self.pool @@ -622,8 +548,6 @@ impl Solver { rule.debug(&self.pool), ); } - // Skip logging for forbids, which is so noisy - RuleKind::ForbidMultipleInstances(..) => {} } } } @@ -635,7 +559,7 @@ impl Solver { } fn analyze_unsolvable_rule( - rules: &[Rule], + rules: &[RuleState], learnt_why: &[Vec], learnt_rules_start: RuleId, rule_id: RuleId, @@ -644,7 +568,7 @@ impl Solver { ) { let rule = &rules[rule_id.index()]; match rule.kind { - RuleKind::Learnt(..) => { + Rule::Learnt(..) => { if !seen.insert(rule_id) { return; } @@ -674,11 +598,13 @@ impl Solver { println!("=== ANALYZE UNSOLVABLE"); let mut involved = HashSet::new(); - involved.extend( - self.rules[rule_id.index()] - .literals(&self.learnt_rules, &self.pool) - .iter() - .map(|l| l.solvable_id), + self.rules[rule_id.index()].kind.visit_literals( + &self.learnt_rules, + &self.pool, + |literal| { + involved.insert(literal.solvable_id); + true + }, ); let mut seen = HashSet::new(); @@ -713,14 +639,19 @@ impl Solver { &mut seen, ); - for literal in self.rules[why.index()].literals(&self.learnt_rules, &self.pool) { - if literal.eval(self.decision_tracker.map()) == Some(true) { - assert_eq!(literal.solvable_id, decision.solvable_id); - continue; - } + self.rules[why.index()].kind.visit_literals( + &self.learnt_rules, + &self.pool, + |literal| { + if literal.eval(self.decision_tracker.map()) == Some(true) { + assert_eq!(literal.solvable_id, decision.solvable_id); + } else { + involved.insert(literal.solvable_id); + } - involved.insert(literal.solvable_id); - } + true + }, + ); } problem @@ -739,62 +670,49 @@ impl Solver { // println!("=== ANALYZE"); - let mut first_iteration = true; let mut s_value; - let mut learnt_why = Vec::new(); + let mut first_iteration = true; loop { learnt_why.push(rule_id); - // TODO: we should be able to get rid of the branching, always retrieving the whole list - // of literals, since the hash set will ensure we aren't considering the conflicting - // solvable after the first iteration - let causes = if first_iteration { - first_iteration = false; - self.rules[rule_id.index()].literals(&self.learnt_rules, &self.pool) - } else { - self.rules[rule_id.index()].conflict_causes(s, &self.learnt_rules, &self.pool) - }; + self.rules[rule_id.index()].kind.visit_literals( + &self.learnt_rules, + &self.pool, + |literal| { + if !first_iteration && literal.solvable_id == s { + // We are only interested in the causes of the conflict, so we ignore the + // solvable whose value was propagated + return true; + } + + if !seen.insert(literal.solvable_id) { + // Skip literals we have already seen + return true; + } - debug_assert!(!causes.is_empty()); - - // print!("level = {current_level}; rule: "); - // self.rules[rule_id.index()].debug(&self.pool); - - // Collect literals that imply that `s` should be assigned a given value (triggering a conflict) - for cause in causes { - if seen.insert(cause.solvable_id) { - let decision_level = self.decision_tracker.level(cause.solvable_id); - // let decision = self - // .decision_tracker - // .assigned_value(cause.solvable_id) - // .unwrap(); - // println!( - // "- {} = {} (level {decision_level})", - // self.pool.solvables[cause.solvable_id.index()].display(), - // decision - // ); + let decision_level = self.decision_tracker.level(literal.solvable_id); if decision_level == current_level { causes_at_current_level += 1; } else if current_level > 1 { let learnt_literal = Literal { - solvable_id: cause.solvable_id, + solvable_id: literal.solvable_id, negate: self .decision_tracker - .assigned_value(cause.solvable_id) + .assigned_value(literal.solvable_id) .unwrap(), }; learnt.push(learnt_literal); btlevel = btlevel.max(decision_level); } else { - // A conflict with a decision at level 1 means the problem is unsatisfiable - // (otherwise we would "learn" that the decision at level 1 was wrong, but - // those decisions are either directly provided by [or derived from] the - // user's input) - panic!("unsolvable"); + unreachable!(); } - } - } + + true + }, + ); + + first_iteration = false; // Select next literal to look at loop { @@ -808,7 +726,7 @@ impl Solver { // We are interested in the first literal we come across that caused the conflicting // assignment - if seen.contains(&s) { + if seen.contains(&last_decision.solvable_id) { break; } } @@ -831,11 +749,7 @@ impl Solver { self.learnt_rules.push(learnt.clone()); self.learnt_why.push(learnt_why); - let mut rule = Rule::new( - RuleKind::Learnt(learnt_index), - &self.learnt_rules, - &self.pool, - ); + let mut rule = RuleState::new(Rule::Learnt(learnt_index), &self.learnt_rules, &self.pool); if rule.has_watches() { self.watches.start_watching(&mut rule, rule_id); @@ -886,44 +800,55 @@ impl Solver { #[cfg(test)] mod test { use super::*; + use crate::id::RepoId; use rattler_conda_types::{PackageRecord, Version}; use std::str::FromStr; - fn pool(packages: &[(&str, &str, Vec<&str>)]) -> Pool { - let mut pool = Pool::new(); - let repo_id = pool.new_repo(""); + fn package(name: &str, version: &str, deps: &[&str], constrains: &[&str]) -> PackageRecord { + PackageRecord { + arch: None, + build: "".to_string(), + build_number: 0, + constrains: constrains.iter().map(|s| s.to_string()).collect(), + depends: deps.iter().map(|s| s.to_string()).collect(), + features: None, + legacy_bz2_md5: None, + legacy_bz2_size: None, + license: None, + license_family: None, + md5: None, + name: name.to_string(), + noarch: Default::default(), + platform: None, + sha256: None, + size: None, + subdir: "".to_string(), + timestamp: None, + track_features: vec![], + version: version.parse().unwrap(), + } + } + + fn add_package(pool: &mut Pool, record: PackageRecord) { + let record = Box::leak(Box::new(record)); + let solvable_id = pool.add_package(RepoId::new(0), record); + + for dep in &record.depends { + pool.add_dependency(solvable_id, dep.to_string()); + } + for constrain in &record.constrains { + pool.add_constrains(solvable_id, constrain.to_string()); + } + } + + fn pool(packages: &[(&str, &str, Vec<&str>)]) -> Pool<'static> { + let mut pool = Pool::new(); for (pkg_name, version, deps) in packages { let pkg_name = *pkg_name; let version = *version; - let record = Box::new(PackageRecord { - arch: None, - build: "".to_string(), - build_number: 0, - constrains: vec![], - depends: deps.iter().map(|s| s.to_string()).collect(), - features: None, - legacy_bz2_md5: None, - legacy_bz2_size: None, - license: None, - license_family: None, - md5: None, - name: pkg_name.to_string(), - noarch: Default::default(), - platform: None, - sha256: None, - size: None, - subdir: "".to_string(), - timestamp: None, - track_features: vec![], - version: version.parse().unwrap(), - }); - - let solvable_id = pool.add_package(repo_id, Box::leak(record)); - - for &dep in deps { - pool.add_dependency(solvable_id, dep.to_string()); - } + let record = package(pkg_name, version, deps, &[]); + add_package(&mut pool, record); } pool @@ -1044,33 +969,14 @@ mod test { let mut solver = Solver::new(pool); let solved = solver.solve(install(&["asdf", "efgh"])).unwrap(); + use std::fmt::Write; + let mut display_result = String::new(); for &(solvable_id, _) in &solved.steps { let solvable = solver.pool().resolve_solvable_inner(solvable_id).display(); - println!("Install {solvable}"); + writeln!(display_result, "{solvable}").unwrap(); } - assert_eq!(solved.steps.len(), 3); - - let solvable = solver - .pool - .resolve_solvable_inner(solved.steps[0].0) - .package(); - assert_eq!(solvable.record.name, "conflicting"); - assert_eq!(solvable.record.version.to_string(), "1.0.0"); - - let solvable = solver - .pool - .resolve_solvable_inner(solved.steps[1].0) - .package(); - assert_eq!(solvable.record.name, "asdf"); - assert_eq!(solvable.record.version.to_string(), "1.2.3"); - - let solvable = solver - .pool - .resolve_solvable_inner(solved.steps[2].0) - .package(); - assert_eq!(solvable.record.name, "efgh"); - assert_eq!(solvable.record.version.to_string(), "4.5.7"); + insta::assert_snapshot!(display_result); } #[test] @@ -1362,7 +1268,6 @@ mod test { insta::assert_snapshot!(error); } - // TODO: this isn't testing for compression yet! #[test] fn test_unsat_applies_graph_compression() { let pool = pool(&[ @@ -1381,4 +1286,22 @@ mod test { let error = solve_unsat(pool, jobs); insta::assert_snapshot!(error); } + + #[test] + fn test_unsat_constrains() { + let mut pool = pool(&[ + ("A", "10", vec!["B>=50"]), + ("A", "9", vec!["B>=50"]), + ("B", "50", vec![]), + ("B", "42", vec![]), + ]); + + add_package(&mut pool, package("C", "10", &[], &["B<50"])); + add_package(&mut pool, package("C", "8", &[], &["B<50"])); + + let jobs = install(&["A", "C"]); + + let error = solve_unsat(pool, jobs); + insta::assert_snapshot!(error); + } } diff --git a/crates/libsolv_rs/src/rules.rs b/crates/libsolv_rs/src/solver/rule.rs similarity index 61% rename from crates/libsolv_rs/src/rules.rs rename to crates/libsolv_rs/src/solver/rule.rs index 1f095b598..93c7485fd 100644 --- a/crates/libsolv_rs/src/rules.rs +++ b/crates/libsolv_rs/src/solver/rule.rs @@ -1,58 +1,199 @@ -use crate::decision_map::DecisionMap; -use crate::pool::{MatchSpecId, Pool}; -use crate::solvable::SolvableId; -use crate::solver::RuleId; +use crate::id::MatchSpecId; +use crate::id::RuleId; +use crate::id::SolvableId; +use crate::pool::Pool; +use crate::solver::decision_map::DecisionMap; use std::fmt::{Debug, Formatter}; -pub(crate) struct RuleDebug<'a> { - kind: RuleKind, - pool: &'a Pool, +/// Represents a single clause in the SAT problem +/// +/// # SAT terminology +/// +/// Clauses consist of disjunctions of literals (i.e. a non-empty list of variables, potentially +/// negated, joined by the logical "or" operator). Here are some examples: +/// +/// - (¬A ∨ ¬B) +/// - (¬A ∨ ¬B ∨ ¬C ∨ ¬D) +/// - (¬A ∨ B ∨ C) +/// - (root) +/// +/// For additional clarity: if `(¬A ∨ ¬B)` is a clause, `¬A` and `¬B` are its literals, and `A` and +/// `B` are variables. In our implementation, variables are represented by [`SolvableId`], and +/// assignments are tracked in the [`DecisionMap`]. +/// +/// The solver will attempt to assign values to the variables involved in the problem in such a way +/// that all clauses become true. If that turns out to be impossible, the problem is unsatisfiable. +/// +/// Since we are not interested in general-purpose SAT solving, but are targeting the specific +/// use-case of dependency resolution, we only support a limited set of clauses. There are thousands +/// of rules for a particular dependency resolution problem, and we try to keep the [`Rule`] enum +/// small. A naive implementation would store a `Vec`. +#[derive(Copy, Clone, Debug)] +pub(crate) enum Rule { + /// An assertion that the root solvable must be installed + /// + /// In SAT terms: (root) + InstallRoot, + /// The solvable requires the candidates associated to the match spec + /// + /// In SAT terms: (¬A ∨ B1 ∨ B2 ∨ ... ∨ B99), where B1 to B99 represent the possible candidates + /// for the provided match spec + Requires(SolvableId, MatchSpecId), + /// Ensures only a single version of a package is installed + /// + /// Usage: generate one [`Rule::ForbidMultipleInstances`] rule for each possible combination of + /// packages under the same name. The rule itself forbids two solvables from being installed at + /// the same time. + /// + /// In SAT terms: (¬A ∨ ¬B) + ForbidMultipleInstances(SolvableId, SolvableId), + /// Forbids packages that do not satisfy a solvable's constrains + /// + /// Usage: for each constrains relationship in a package, determine all the candidates that do + /// _not_ satisfy it, and create one [`Rule::Constrains`]. The rule itself forbids two solvables + /// from being installed at the same time, just as [`Rule::ForbidMultipleInstances`], but it + /// pays off to have a separate variant for user-friendly error messages. + /// + /// In SAT terms: (¬A ∨ ¬B) + Constrains(SolvableId, SolvableId), + /// Forbids the package on the right-hand side + /// + /// Note that the package on the left-hand side is not part of the rule, but just context to + /// know which exact package was locked (necessary for user-friendly error messages) + /// + /// In SAT terms: (¬root ∨ ¬B). Note that we could encode this as an assertion (¬B), but that + /// would require additional logic in the solver. + Lock(SolvableId, SolvableId), + /// A rule learnt during solving + /// + /// The `usize` is an index that can be used to retrieve the rule's literals, which are stored + /// elsewhere to prevent the size of [`Rule`] from blowing up + Learnt(usize), } -impl Debug for RuleDebug<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self.kind { - RuleKind::InstallRoot => write!(f, "install root"), - RuleKind::Learnt(index) => write!(f, "learnt rule {index}"), - RuleKind::Requires(solvable_id, match_spec_id) => { - let match_spec = self.pool.resolve_match_spec(match_spec_id).to_string(); - write!( - f, - "{} requires {match_spec}", - self.pool.resolve_solvable_inner(solvable_id).display() - ) +impl Rule { + /// Returns the ids of the solvables that will be watched right after the rule is created + fn initial_watches( + &self, + learnt_rules: &[Vec], + pool: &Pool, + ) -> Option<[SolvableId; 2]> { + match self { + Rule::InstallRoot => None, + Rule::Constrains(s1, s2) | Rule::ForbidMultipleInstances(s1, s2) => Some([*s1, *s2]), + Rule::Lock(_, s) => Some([SolvableId::root(), *s]), + Rule::Learnt(index) => { + let literals = &learnt_rules[*index]; + debug_assert!(!literals.is_empty()); + if literals.len() == 1 { + // No need for watches, since we learned an assertion + None + } else { + Some([ + literals.first().unwrap().solvable_id, + literals.last().unwrap().solvable_id, + ]) + } } - RuleKind::Constrains(s1, s2) => { - write!( - f, - "{} excludes {}", - self.pool.resolve_solvable_inner(s1).display(), - self.pool.resolve_solvable_inner(s2).display() - ) + Rule::Requires(id, match_spec) => { + let candidates = pool.match_spec_to_candidates[match_spec.index()] + .as_ref() + .unwrap(); + + if candidates.is_empty() { + None + } else { + Some([*id, candidates[0]]) + } } - RuleKind::ForbidMultipleInstances(s1, _) => { - let name = self - .pool - .resolve_solvable_inner(s1) - .package() - .record - .name - .as_str(); - write!(f, "only one {name} allowed") + } + } + + /// Visits each literal in the rule + pub fn visit_literals( + &self, + learnt_rules: &[Vec], + pool: &Pool, + mut visit: impl FnMut(Literal) -> bool, + ) { + match *self { + Rule::InstallRoot => unreachable!(), + Rule::Learnt(index) => { + for &literal in &learnt_rules[index] { + if !visit(literal) { + return; + } + } + } + Rule::Requires(solvable_id, match_spec_id) => { + if !visit(Literal { + solvable_id, + negate: true, + }) { + return; + } + + for &solvable_id in pool.match_spec_to_candidates[match_spec_id.index()] + .as_deref() + .unwrap() + { + if !visit(Literal { + solvable_id, + negate: false, + }) { + return; + } + } + } + Rule::Constrains(s1, s2) | Rule::ForbidMultipleInstances(s1, s2) => { + if !visit(Literal { + solvable_id: s1, + negate: true, + }) { + return; + } + + visit(Literal { + solvable_id: s2, + negate: true, + }); + } + Rule::Lock(_, s) => { + if !visit(Literal { + solvable_id: SolvableId::root(), + negate: true, + }) { + return; + } + + visit(Literal { + solvable_id: s, + negate: true, + }); } } } } +/// Keeps track of the literals watched by a [`Rule`] and the state associated to two linked lists +/// this rule is part of +/// +/// In our SAT implementation, each rule tracks two literals present in its clause, to be notified +/// when the value assigned to the variable has changed (this technique is known as _watches_). +/// Rules that are tracking the same variable are grouped together in a linked list, so it becomes +/// easy to notify them all. #[derive(Clone)] -pub(crate) struct Rule { +pub(crate) struct RuleState { + // The ids of the solvables this rule is watching pub watched_literals: [SolvableId; 2], + // The ids of the next rule in each linked list that this rule is part of next_watches: [RuleId; 2], - pub(crate) kind: RuleKind, + // The clause itself + pub(crate) kind: Rule, } -impl Rule { - pub fn new(kind: RuleKind, learnt_rules: &[Vec], pool: &Pool) -> Self { +impl RuleState { + pub fn new(kind: Rule, learnt_rules: &[Vec], pool: &Pool) -> Self { let watched_literals = kind .initial_watches(learnt_rules, pool) .unwrap_or([SolvableId::null(), SolvableId::null()]); @@ -85,7 +226,7 @@ impl Rule { pub fn unlink_rule( &mut self, - linked_rule: &Rule, + linked_rule: &RuleState, watched_solvable: SolvableId, linked_rule_watch_index: usize, ) { @@ -146,9 +287,11 @@ impl Rule { }; match self.kind { - RuleKind::InstallRoot => unreachable!(), - RuleKind::Learnt(index) => { - // TODO: this is probably not going to cut it for performance + Rule::InstallRoot => unreachable!(), + Rule::Learnt(index) => { + // TODO: we might want to do something else for performance, like keeping the whole + // literal in `self.watched_literals`, to avoid lookups... But first we should + // benchmark! let &w1 = learnt_rules[index] .iter() .find(|l| l.solvable_id == self.watched_literals[0]) @@ -159,9 +302,10 @@ impl Rule { .unwrap(); [w1, w2] } - RuleKind::ForbidMultipleInstances(_, _) => literals(false, false), - RuleKind::Constrains(_, _) => literals(false, false), - RuleKind::Requires(solvable_id, _) => { + Rule::Constrains(..) | Rule::ForbidMultipleInstances(..) | Rule::Lock(..) => { + literals(false, false) + } + Rule::Requires(solvable_id, _) => { if self.watched_literals[0] == solvable_id { literals(false, true) } else if self.watched_literals[1] == solvable_id { @@ -188,15 +332,14 @@ impl Rule { }; match self.kind { - RuleKind::InstallRoot => unreachable!(), - RuleKind::Learnt(index) => learnt_rules[index] + Rule::InstallRoot => unreachable!(), + Rule::Learnt(index) => learnt_rules[index] .iter() .cloned() .find(|&l| can_watch(l)) .map(|l| l.solvable_id), - RuleKind::ForbidMultipleInstances(_, _) => None, - RuleKind::Constrains(_, _) => None, - RuleKind::Requires(solvable_id, match_spec_id) => { + Rule::Constrains(..) | Rule::ForbidMultipleInstances(..) | Rule::Lock(..) => None, + Rule::Requires(solvable_id, match_spec_id) => { // The solvable that added this rule let solvable_lit = Literal { solvable_id, @@ -225,113 +368,9 @@ impl Rule { } } } - - /// Returns the list of literals that constitute this rule - pub fn literals(&self, learnt_rules: &[Vec], pool: &Pool) -> Vec { - match self.kind { - RuleKind::InstallRoot => unreachable!(), - RuleKind::Learnt(index) => learnt_rules[index].clone(), - RuleKind::Requires(solvable_id, match_spec_id) => { - // All variables contribute to the conflict - std::iter::once(Literal { - solvable_id, - negate: true, - }) - .chain( - pool.match_spec_to_candidates[match_spec_id.index()] - .as_deref() - .unwrap() - .iter() - .cloned() - .map(|solvable_id| Literal { - solvable_id, - negate: false, - }), - ) - .collect() - } - RuleKind::ForbidMultipleInstances(s1, s2) => { - vec![ - Literal { - solvable_id: s1, - negate: true, - }, - Literal { - solvable_id: s2, - negate: true, - }, - ] - } - RuleKind::Constrains(s1, s2) => { - vec![ - Literal { - solvable_id: s1, - negate: true, - }, - Literal { - solvable_id: s2, - negate: true, - }, - ] - } - } - } - - /// Returns the list of variables that imply that the provided solvable should be decided - pub fn conflict_causes( - &self, - variable: SolvableId, - learnt_rules: &[Vec], - pool: &Pool, - ) -> Vec { - match self.kind { - RuleKind::InstallRoot => unreachable!(), - RuleKind::Learnt(index) => learnt_rules[index] - .iter() - .cloned() - .filter(|lit| lit.solvable_id != variable) - .collect(), - RuleKind::Requires(solvable_id, match_spec_id) => { - // All variables contribute to the conflict - std::iter::once(Literal { - solvable_id, - negate: true, - }) - .chain( - pool.match_spec_to_candidates[match_spec_id.index()] - .as_deref() - .unwrap() - .iter() - .cloned() - .map(|solvable_id| Literal { - solvable_id, - negate: false, - }), - ) - .filter(|&l| variable != l.solvable_id) - .collect() - } - RuleKind::ForbidMultipleInstances(s1, s2) => { - let cause = if variable == s1 { s2 } else { s1 }; - - vec![Literal { - solvable_id: cause, - negate: true, - }] - } - - RuleKind::Constrains(s1, s2) => { - let cause = if variable == s1 { s2 } else { s1 }; - - vec![Literal { - solvable_id: cause, - negate: true, - }] - } - } - } } +/// Represents a literal in a SAT clause (i.e. either A or ¬A) #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub(crate) struct Literal { pub(crate) solvable_id: SolvableId, @@ -339,10 +378,12 @@ pub(crate) struct Literal { } impl Literal { + /// Returns the value that would make the literal evaluate to true if assigned to the literal's solvable pub(crate) fn satisfying_value(self) -> bool { !self.negate } + /// Evaluates the literal, or returns `None` if no value has been assigned to the solvable pub(crate) fn eval(self, decision_map: &DecisionMap) -> Option { decision_map .value(self.solvable_id) @@ -358,59 +399,50 @@ impl Literal { } } -#[derive(Copy, Clone, Debug)] -pub(crate) enum RuleKind { - InstallRoot, - /// The solvable requires the candidates associated to the match spec - /// - /// In SAT terms: (¬A ∨ B1 ∨ B2 ∨ ... ∨ B99), where B1 to B99 represent the possible candidates - /// for the provided match spec. - Requires(SolvableId, MatchSpecId), - /// The left solvable forbids installing the right solvable - /// - /// Used to ensure only a single version of a package is installed - /// - /// In SAT terms: (¬A ∨ ¬B) - ForbidMultipleInstances(SolvableId, SolvableId), - /// Similar to forbid, but created due to a constrains relationship - Constrains(SolvableId, SolvableId), - /// Learnt rule - Learnt(usize), +/// A representation of a rule that implements [`Debug`] +pub(crate) struct RuleDebug<'a> { + kind: Rule, + pool: &'a Pool<'a>, } -impl RuleKind { - fn initial_watches( - &self, - learnt_rules: &[Vec], - pool: &Pool, - ) -> Option<[SolvableId; 2]> { - match self { - RuleKind::InstallRoot => None, - RuleKind::Constrains(s1, s2) => Some([*s1, *s2]), - RuleKind::ForbidMultipleInstances(s1, s2) => Some([*s1, *s2]), - RuleKind::Learnt(index) => { - let literals = &learnt_rules[*index]; - debug_assert!(!literals.is_empty()); - if literals.len() == 1 { - // No need for watches, since we learned an assertion - None - } else { - Some([ - literals.first().unwrap().solvable_id, - literals.last().unwrap().solvable_id, - ]) - } +impl Debug for RuleDebug<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self.kind { + Rule::InstallRoot => write!(f, "install root"), + Rule::Learnt(index) => write!(f, "learnt rule {index}"), + Rule::Requires(solvable_id, match_spec_id) => { + let match_spec = self.pool.resolve_match_spec(match_spec_id).to_string(); + write!( + f, + "{} requires {match_spec}", + self.pool.resolve_solvable_inner(solvable_id).display() + ) } - RuleKind::Requires(id, match_spec) => { - let candidates = pool.match_spec_to_candidates[match_spec.index()] - .as_ref() - .unwrap(); - - if candidates.is_empty() { - None - } else { - Some([*id, candidates[0]]) - } + Rule::Constrains(s1, s2) => { + write!( + f, + "{} excludes {}", + self.pool.resolve_solvable_inner(s1).display(), + self.pool.resolve_solvable_inner(s2).display() + ) + } + Rule::Lock(locked, forbidden) => { + write!( + f, + "{} is locked, so {} is forbidden", + self.pool.resolve_solvable_inner(locked).display(), + self.pool.resolve_solvable_inner(forbidden).display() + ) + } + Rule::ForbidMultipleInstances(s1, _) => { + let name = self + .pool + .resolve_solvable_inner(s1) + .package() + .record + .name + .as_str(); + write!(f, "only one {name} allowed") } } } @@ -420,13 +452,13 @@ impl RuleKind { mod test { use super::*; - fn rule(next_rules: [RuleId; 2], watched_solvables: [SolvableId; 2]) -> Rule { - Rule { + fn rule(next_rules: [RuleId; 2], watched_solvables: [SolvableId; 2]) -> RuleState { + RuleState { watched_literals: watched_solvables, next_watches: next_rules, // The kind is irrelevant here - kind: RuleKind::InstallRoot, + kind: Rule::InstallRoot, } } @@ -543,4 +575,12 @@ mod test { assert_eq!(rule1.next_watches, [RuleId::new(2), RuleId::null()]) } } + + #[test] + fn test_rule_size() { + // This test is here to ensure we don't increase the size of `Rule` by accident, as we are + // creating thousands of instances. Note: libsolv manages to bring down the size to 24, so + // there is probably room for improvement. + assert_eq!(std::mem::size_of::(), 32); + } } diff --git a/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__resolve_with_conflict.snap b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__resolve_with_conflict.snap new file mode 100644 index 000000000..b9cf91dc0 --- /dev/null +++ b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__resolve_with_conflict.snap @@ -0,0 +1,8 @@ +--- +source: crates/libsolv_rs/src/solver/mod.rs +expression: display_result +--- +conflicting 1.0.0 +asdf 1.2.3 +efgh 4.5.7 + diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_after_backtracking.snap b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_after_backtracking.snap similarity index 92% rename from crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_after_backtracking.snap rename to crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_after_backtracking.snap index ee21a49c6..c47ead8e1 100644 --- a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_after_backtracking.snap +++ b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_after_backtracking.snap @@ -1,5 +1,5 @@ --- -source: crates/libsolv_rs/src/solver.rs +source: crates/libsolv_rs/src/solver/mod.rs expression: error --- The following packages are incompatible diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_applies_graph_compression.snap b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_applies_graph_compression.snap similarity index 92% rename from crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_applies_graph_compression.snap rename to crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_applies_graph_compression.snap index b43babe7d..6d2574715 100644 --- a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_applies_graph_compression.snap +++ b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_applies_graph_compression.snap @@ -1,5 +1,5 @@ --- -source: crates/libsolv_rs/src/solver.rs +source: crates/libsolv_rs/src/solver/mod.rs expression: error --- The following packages are incompatible diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_bluesky_conflict.snap b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_bluesky_conflict.snap similarity index 91% rename from crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_bluesky_conflict.snap rename to crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_bluesky_conflict.snap index 94bb34a96..017f0be9f 100644 --- a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_bluesky_conflict.snap +++ b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_bluesky_conflict.snap @@ -1,5 +1,5 @@ --- -source: crates/libsolv_rs/src/solver.rs +source: crates/libsolv_rs/src/solver/mod.rs expression: error --- The following packages are incompatible diff --git a/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_constrains.snap b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_constrains.snap new file mode 100644 index 000000000..6934b9c6b --- /dev/null +++ b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_constrains.snap @@ -0,0 +1,13 @@ +--- +source: crates/libsolv_rs/src/solver/mod.rs +expression: error +--- +The following packages are incompatible +|-- A can be installed with any of the following options: + |-- A 9 | 10 would require + |-- B >=50, which can be installed with any of the following options: + |-- B 50 +|-- C cannot be installed because there are no viable options: + |-- C 8 | 10 would constrain + |-- B <50 , which conflicts with any installable versions previously reported + diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_incompatible_root_requirements.snap b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_incompatible_root_requirements.snap similarity index 86% rename from crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_incompatible_root_requirements.snap rename to crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_incompatible_root_requirements.snap index ec1817884..3eb36a6d0 100644 --- a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_incompatible_root_requirements.snap +++ b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_incompatible_root_requirements.snap @@ -1,5 +1,5 @@ --- -source: crates/libsolv_rs/src/solver.rs +source: crates/libsolv_rs/src/solver/mod.rs expression: error --- The following packages are incompatible diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_locked_and_excluded.snap b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_locked_and_excluded.snap similarity index 71% rename from crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_locked_and_excluded.snap rename to crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_locked_and_excluded.snap index bfff00300..ca9a796af 100644 --- a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_locked_and_excluded.snap +++ b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_locked_and_excluded.snap @@ -1,5 +1,5 @@ --- -source: crates/libsolv_rs/src/solver.rs +source: crates/libsolv_rs/src/solver/mod.rs expression: error --- The following packages are incompatible @@ -7,5 +7,5 @@ The following packages are incompatible |-- asdf 1.2.3 would require |-- C >1, which can be installed with any of the following options: |-- C 2.0.0 -|-- C 2.0.0 is locked, but another version is required as reported above +|-- C 1.0.0 is locked, but another version is required as reported above diff --git a/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_1.snap b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_1.snap new file mode 100644 index 000000000..79b43a759 --- /dev/null +++ b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_1.snap @@ -0,0 +1,6 @@ +--- +source: crates/libsolv_rs/src/solver/mod.rs +expression: error +--- +No candidates where found for fghj. + diff --git a/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_2.snap b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_2.snap new file mode 100644 index 000000000..1b25afa35 --- /dev/null +++ b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_missing_top_level_dep_2.snap @@ -0,0 +1,6 @@ +--- +source: crates/libsolv_rs/src/solver/mod.rs +expression: error +--- +No candidates where found for B 14.*. + diff --git a/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_1.snap b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_1.snap new file mode 100644 index 000000000..794b8a2e4 --- /dev/null +++ b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_1.snap @@ -0,0 +1,8 @@ +--- +source: crates/libsolv_rs/src/solver/mod.rs +expression: error +--- +asdf cannot be installed because there are no viable options: +|-- asdf 1.2.3 would require + |-- C >1, for which no candidates where found. + diff --git a/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_2.snap b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_2.snap new file mode 100644 index 000000000..ce0b01467 --- /dev/null +++ b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_no_candidates_for_child_2.snap @@ -0,0 +1,8 @@ +--- +source: crates/libsolv_rs/src/solver/mod.rs +expression: error +--- +A <1000 cannot be installed because there are no viable options: +|-- A 41 would require + |-- B <20, for which no candidates where found. + diff --git a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_pubgrub_article.snap b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_pubgrub_article.snap similarity index 93% rename from crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_pubgrub_article.snap rename to crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_pubgrub_article.snap index cdf09cdec..f2cc22d60 100644 --- a/crates/libsolv_rs/src/snapshots/libsolv_rs__solver__test__unsat_pubgrub_article.snap +++ b/crates/libsolv_rs/src/solver/snapshots/libsolv_rs__solver__test__unsat_pubgrub_article.snap @@ -1,5 +1,5 @@ --- -source: crates/libsolv_rs/src/solver.rs +source: crates/libsolv_rs/src/solver/mod.rs expression: error --- The following packages are incompatible diff --git a/crates/libsolv_rs/src/watch_map.rs b/crates/libsolv_rs/src/solver/watch_map.rs similarity index 88% rename from crates/libsolv_rs/src/watch_map.rs rename to crates/libsolv_rs/src/solver/watch_map.rs index 813155913..bc09dc784 100644 --- a/crates/libsolv_rs/src/watch_map.rs +++ b/crates/libsolv_rs/src/solver/watch_map.rs @@ -1,6 +1,6 @@ -use crate::rules::Rule; -use crate::solvable::SolvableId; -use crate::solver::RuleId; +use crate::id::RuleId; +use crate::id::SolvableId; +use crate::solver::rule::RuleState; /// A map from solvables to the rules that are watching them pub(crate) struct WatchMap { @@ -18,7 +18,7 @@ impl WatchMap { self.map = vec![RuleId::null(); nsolvables]; } - pub(crate) fn start_watching(&mut self, rule: &mut Rule, rule_id: RuleId) { + pub(crate) fn start_watching(&mut self, rule: &mut RuleState, rule_id: RuleId) { for (watch_index, watched_solvable) in rule.watched_literals.into_iter().enumerate() { let already_watching = self.first_rule_watching_solvable(watched_solvable); rule.link_to_rule(watch_index, already_watching); @@ -28,8 +28,8 @@ impl WatchMap { pub(crate) fn update_watched( &mut self, - predecessor_rule: Option<&mut Rule>, - rule: &mut Rule, + predecessor_rule: Option<&mut RuleState>, + rule: &mut RuleState, rule_id: RuleId, watch_index: usize, previous_watch: SolvableId, diff --git a/crates/libsolv_rs/src/transaction.rs b/crates/libsolv_rs/src/transaction.rs new file mode 100644 index 000000000..c455c26b2 --- /dev/null +++ b/crates/libsolv_rs/src/transaction.rs @@ -0,0 +1,20 @@ +use crate::id::SolvableId; +use std::fmt::{Display, Formatter}; + +/// Represents the operations that should be performed to achieve the desired state, based on the +/// jobs provided to the solver and their solution +pub struct Transaction { + pub steps: Vec<(SolvableId, TransactionKind)>, +} + +#[derive(Copy, Clone, Debug)] +#[non_exhaustive] +pub enum TransactionKind { + Install, +} + +impl Display for TransactionKind { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/crates/rattler_solve/src/libsolv/input.rs b/crates/rattler_solve/src/libsolv/input.rs index 33b8c5737..f0c595485 100644 --- a/crates/rattler_solve/src/libsolv/input.rs +++ b/crates/rattler_solve/src/libsolv/input.rs @@ -1,8 +1,9 @@ //! Contains business logic that loads information into libsolv in order to solve a conda //! environment -use libsolv_rs::pool::{Pool, RepoId}; -use libsolv_rs::solvable::SolvableId; +use libsolv_rs::id::RepoId; +use libsolv_rs::id::SolvableId; +use libsolv_rs::pool::Pool; use rattler_conda_types::package::ArchiveType; use rattler_conda_types::{GenericVirtualPackage, PackageRecord, RepoDataRecord}; use std::cmp::Ordering; @@ -11,10 +12,10 @@ use std::collections::HashMap; /// Adds [`RepoDataRecord`] to `repo` /// /// Panics if the repo does not belong to the pool -pub fn add_repodata_records( - pool: &mut Pool, +pub fn add_repodata_records<'a>( + pool: &mut Pool<'a>, repo_id: RepoId, - repo_datas: &[RepoDataRecord], + repo_datas: &'a [RepoDataRecord], ) -> Vec { // Keeps a mapping from packages added to the repo to the type and solvable let mut package_to_type: HashMap<&str, (ArchiveType, SolvableId)> = HashMap::new(); @@ -58,15 +59,13 @@ pub fn add_repodata_records( /// `None`). If no `.conda` version has been added, we create a new solvable (replacing any existing /// solvable for the `.tar.bz` version of the package). fn add_or_reuse_solvable<'a>( - pool: &mut Pool, + pool: &mut Pool<'a>, repo_id: RepoId, package_to_type: &mut HashMap<&'a str, (ArchiveType, SolvableId)>, repo_data: &'a RepoDataRecord, ) -> Option { // Sometimes we can reuse an existing solvable if let Some((filename, archive_type)) = ArchiveType::split_str(&repo_data.file_name) { - let record_ptr = &repo_data.package_record as *const _; - if let Some(&(other_package_type, old_solvable_id)) = package_to_type.get(filename) { match archive_type.cmp(&other_package_type) { Ordering::Less => { @@ -82,7 +81,7 @@ fn add_or_reuse_solvable<'a>( package_to_type.insert(filename, (archive_type, old_solvable_id)); // Reset and reuse the old solvable - pool.reset_package(repo_id, old_solvable_id, unsafe { &*record_ptr }); + pool.reset_package(repo_id, old_solvable_id, &repo_data.package_record); return Some(old_solvable_id); } Ordering::Equal => { @@ -90,7 +89,7 @@ fn add_or_reuse_solvable<'a>( } } } else { - let solvable_id = pool.add_package(repo_id, unsafe { &*record_ptr }); + let solvable_id = pool.add_package(repo_id, &repo_data.package_record); package_to_type.insert(filename, (archive_type, solvable_id)); return Some(solvable_id); } @@ -98,8 +97,7 @@ fn add_or_reuse_solvable<'a>( tracing::warn!("unknown package extension: {}", &repo_data.file_name); } - let record_ptr = &repo_data.package_record as *const _; - let solvable_id = pool.add_package(repo_id, unsafe { &*record_ptr }); + let solvable_id = pool.add_package(repo_id, &repo_data.package_record); Some(solvable_id) } diff --git a/crates/rattler_solve/src/libsolv/output.rs b/crates/rattler_solve/src/libsolv/output.rs index 074b704ab..1982c8a3c 100644 --- a/crates/rattler_solve/src/libsolv/output.rs +++ b/crates/rattler_solve/src/libsolv/output.rs @@ -1,11 +1,13 @@ //! Contains business logic to retrieve the results from libsolv after attempting to resolve a conda //! environment -use libsolv_rs::pool::{Pool, RepoId}; -use libsolv_rs::solvable::SolvableId; -use libsolv_rs::solver::{Transaction, TransactionKind}; +use libsolv_rs::id::RepoId; +use libsolv_rs::id::SolvableId; +use libsolv_rs::pool::Pool; +use libsolv_rs::transaction::TransactionKind; use rattler_conda_types::RepoDataRecord; use std::collections::HashMap; +use libsolv_rs::transaction::Transaction; /// Returns which packages should be installed in the environment /// From cc82574190846bbdaba595a71f4bfff4e3aa49cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Thu, 29 Jun 2023 12:48:22 +0200 Subject: [PATCH 05/17] More comments --- crates/libsolv_rs/src/pool.rs | 70 +++++++++++++++---------- crates/rattler_solve/src/libsolv/mod.rs | 9 ++-- 2 files changed, 45 insertions(+), 34 deletions(-) diff --git a/crates/libsolv_rs/src/pool.rs b/crates/libsolv_rs/src/pool.rs index 28fb1fdf6..d9052e7e5 100644 --- a/crates/libsolv_rs/src/pool.rs +++ b/crates/libsolv_rs/src/pool.rs @@ -6,7 +6,7 @@ use std::collections::hash_map::Entry; use std::collections::HashMap; use std::str::FromStr; -/// A pool for interning data related to the available packages +/// A pool that stores data related to the available packages pub struct Pool<'a> { /// All the solvables that have been registered pub(crate) solvables: Vec>, @@ -67,7 +67,7 @@ impl<'a> Pool<'a> { id } - /// Adds a new solvable to a repo and returns it's [`SolvableId`] + /// Adds a package to a repo and returns it's [`SolvableId`] pub fn add_package(&mut self, repo_id: RepoId, record: &'a PackageRecord) -> SolvableId { assert!(self.solvables.len() <= u32::MAX as usize); @@ -82,14 +82,21 @@ impl<'a> Pool<'a> { solvable_id } - /// Resets the solvable associated to the id, and assigns the provided package to it + /// Resets the package associated to the id, as though it had just been created using + /// [`Pool::add_package`] + /// + /// Panics if the new package has a different name than the existing package pub fn reset_package( &mut self, repo_id: RepoId, solvable_id: SolvableId, record: &'a PackageRecord, ) { + assert!(!solvable_id.is_root()); + let name = self.intern_package_name(&record.name); + assert_ne!(self.solvables[solvable_id.index()].package().name, name); + self.solvables[solvable_id.index()] = Solvable::new_package(repo_id, name, record); } @@ -107,26 +114,6 @@ impl<'a> Pool<'a> { solvable.constrains.push(match_spec_id); } - pub(crate) fn intern_matchspec(&mut self, match_spec: String) -> MatchSpecId { - let next_index = self.match_specs.len(); - match self.match_specs_to_ids.entry(match_spec) { - Entry::Occupied(entry) => *entry.get(), - Entry::Vacant(entry) => { - // println!("Interning match_spec: {}", entry.key()); - self.match_specs - .push(MatchSpec::from_str(entry.key()).unwrap()); - self.match_spec_to_candidates.push(None); - self.match_spec_to_forbidden.push(None); - - // Update the entry - let id = MatchSpecId::new(next_index); - entry.insert(id); - - id - } - } - } - // This function does not take `self`, because otherwise we run into problems with borrowing // when we want to use it together with other pool functions pub(crate) fn get_candidates<'b>( @@ -211,7 +198,35 @@ impl<'a> Pool<'a> { candidates.as_slice() } - /// Interns package names into a `Pool` returning a `NameId` + /// Interns a match spec into the `Pool`, returning its `MatchSpecId` + pub(crate) fn intern_matchspec(&mut self, match_spec: String) -> MatchSpecId { + let next_index = self.match_specs.len(); + match self.match_specs_to_ids.entry(match_spec) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + // println!("Interning match_spec: {}", entry.key()); + self.match_specs + .push(MatchSpec::from_str(entry.key()).unwrap()); + self.match_spec_to_candidates.push(None); + self.match_spec_to_forbidden.push(None); + + // Update the entry + let id = MatchSpecId::new(next_index); + entry.insert(id); + + id + } + } + } + + /// Resolves the id to a match spec + /// + /// Panics if the match spec is not found in the pool + pub fn resolve_match_spec(&self, id: MatchSpecId) -> &MatchSpec { + &self.match_specs[id.index()] + } + + /// Interns a package name into the `Pool`, returning its `NameId` fn intern_package_name>(&mut self, str: T) -> NameId { let next_id = NameId::new(self.names_to_ids.len()); match self.names_to_ids.entry(str.into()) { @@ -225,7 +240,7 @@ impl<'a> Pool<'a> { } } - /// Returns the package name corresponding to the provided id + /// Resolves the id to a package name /// /// Panics if the package name is not found in the pool pub fn resolve_package_name(&self, name_id: NameId) -> &str { @@ -268,10 +283,7 @@ impl<'a> Pool<'a> { } } - pub fn resolve_match_spec(&self, id: MatchSpecId) -> &MatchSpec { - &self.match_specs[id.index()] - } - + /// Returns the dependencies associated to the root solvable pub(crate) fn root_solvable_mut(&mut self) -> &mut Vec { self.solvables[0].root_mut() } diff --git a/crates/rattler_solve/src/libsolv/mod.rs b/crates/rattler_solve/src/libsolv/mod.rs index 58f123054..fd79fefa4 100644 --- a/crates/rattler_solve/src/libsolv/mod.rs +++ b/crates/rattler_solve/src/libsolv/mod.rs @@ -39,7 +39,7 @@ impl SolverBackend for LibsolvBackend { let mut pool = Pool::new(); // Add virtual packages - let repo_id = pool.new_repo("virtual_packages"); + let repo_id = pool.new_repo(); add_virtual_packages(&mut pool, repo_id, &task.virtual_packages); // Create repos for all channel + platform combinations @@ -50,8 +50,7 @@ impl SolverBackend for LibsolvBackend { continue; } - let channel_name = &repodata.records[0].channel; - let repo_id = pool.new_repo(channel_name); + let repo_id = pool.new_repo(); add_repodata_records(&mut pool, repo_id, repodata.records); // Keep our own info about repodata_records @@ -60,7 +59,7 @@ impl SolverBackend for LibsolvBackend { } // Create a special pool for records that are already installed or locked. - let repo_id = pool.new_repo("locked"); + let repo_id = pool.new_repo(); let installed_solvables = add_repodata_records(&mut pool, repo_id, &task.locked_packages); // Also add the installed records to the repodata @@ -68,7 +67,7 @@ impl SolverBackend for LibsolvBackend { all_repodata_records.push(&task.locked_packages); // Create a special pool for records that are pinned and cannot be changed. - let repo_id = pool.new_repo("pinned"); + let repo_id = pool.new_repo(); let pinned_solvables = add_repodata_records(&mut pool, repo_id, &task.pinned_packages); // Also add the installed records to the repodata From 8bdd35a0a65f67281bc4e5d1f072fd78a33ff824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Thu, 29 Jun 2023 13:20:15 +0200 Subject: [PATCH 06/17] More comments --- crates/libsolv_rs/src/pool.rs | 4 ++-- crates/rattler_solve/src/libsolv/input.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/libsolv_rs/src/pool.rs b/crates/libsolv_rs/src/pool.rs index d9052e7e5..80513d08c 100644 --- a/crates/libsolv_rs/src/pool.rs +++ b/crates/libsolv_rs/src/pool.rs @@ -82,11 +82,11 @@ impl<'a> Pool<'a> { solvable_id } - /// Resets the package associated to the id, as though it had just been created using + /// Overwrites the package associated to the id, as though it had just been created using /// [`Pool::add_package`] /// /// Panics if the new package has a different name than the existing package - pub fn reset_package( + pub fn overwrite_package( &mut self, repo_id: RepoId, solvable_id: SolvableId, diff --git a/crates/rattler_solve/src/libsolv/input.rs b/crates/rattler_solve/src/libsolv/input.rs index f0c595485..f9be56f58 100644 --- a/crates/rattler_solve/src/libsolv/input.rs +++ b/crates/rattler_solve/src/libsolv/input.rs @@ -80,8 +80,8 @@ fn add_or_reuse_solvable<'a>( // Update the package to the new type mapping package_to_type.insert(filename, (archive_type, old_solvable_id)); - // Reset and reuse the old solvable - pool.reset_package(repo_id, old_solvable_id, &repo_data.package_record); + // Reuse the old solvable + pool.overwrite_package(repo_id, old_solvable_id, &repo_data.package_record); return Some(old_solvable_id); } Ordering::Equal => { From cc3789335b7a69dd2105e855deb63fc9f38bd873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Thu, 29 Jun 2023 13:53:04 +0200 Subject: [PATCH 07/17] PR feedback --- crates/libsolv_rs/src/conda_util.rs | 6 +++--- crates/libsolv_rs/src/id.rs | 21 ++++++++++++++------- crates/libsolv_rs/src/pool.rs | 15 +++++++++------ crates/libsolv_rs/src/solvable.rs | 7 +++++++ 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/crates/libsolv_rs/src/conda_util.rs b/crates/libsolv_rs/src/conda_util.rs index 76bb3bf72..03a255830 100644 --- a/crates/libsolv_rs/src/conda_util.rs +++ b/crates/libsolv_rs/src/conda_util.rs @@ -15,9 +15,9 @@ pub(crate) fn compare_candidates( ) -> Ordering { // First compare by "tracked_features". If one of the packages has a tracked feature it is // sorted below the one that doesn't have the tracked feature. - let a_has_tracked_features = a.track_features.is_empty(); - let b_has_tracked_features = b.track_features.is_empty(); - match b_has_tracked_features.cmp(&a_has_tracked_features) { + let a_has_tracked_features = !a.track_features.is_empty(); + let b_has_tracked_features = !b.track_features.is_empty(); + match a_has_tracked_features.cmp(&b_has_tracked_features) { Ordering::Less => return Ordering::Less, Ordering::Greater => return Ordering::Greater, Ordering::Equal => {} diff --git a/crates/libsolv_rs/src/id.rs b/crates/libsolv_rs/src/id.rs index 7ed287554..5ef49e0a5 100644 --- a/crates/libsolv_rs/src/id.rs +++ b/crates/libsolv_rs/src/id.rs @@ -1,3 +1,4 @@ +#[repr(transparent)] #[derive(Clone, Copy, Eq, PartialEq, Hash)] pub struct RepoId(u32); @@ -7,23 +8,21 @@ impl RepoId { } } +#[repr(transparent)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct NameId { - value: u32, -} +pub struct NameId(u32); impl NameId { pub(crate) fn new(index: usize) -> Self { - Self { - value: index as u32, - } + Self(index as u32) } pub(crate) fn index(self) -> usize { - self.value as usize + self.0 as usize } } +#[repr(transparent)] #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] pub struct MatchSpecId(u32); @@ -37,11 +36,15 @@ impl MatchSpecId { } } +#[repr(transparent)] #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)] pub struct SolvableId(u32); impl SolvableId { pub(crate) fn new(index: usize) -> Self { + debug_assert_ne!(index, 0); + debug_assert_ne!(index, u32::MAX as usize); + Self(index as u32) } @@ -66,11 +69,15 @@ impl SolvableId { } } +#[repr(transparent)] #[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Debug, Hash)] pub(crate) struct RuleId(u32); impl RuleId { pub(crate) fn new(index: usize) -> Self { + debug_assert_ne!(index, 0); + debug_assert_ne!(index, u32::MAX as usize); + Self(index as u32) } diff --git a/crates/libsolv_rs/src/pool.rs b/crates/libsolv_rs/src/pool.rs index 80513d08c..a2ce25832 100644 --- a/crates/libsolv_rs/src/pool.rs +++ b/crates/libsolv_rs/src/pool.rs @@ -7,6 +7,9 @@ use std::collections::HashMap; use std::str::FromStr; /// A pool that stores data related to the available packages +/// +/// Because it stores solvables, it contains references to `PackageRecord`s (the `'a` lifetime comes +/// from the original `PackageRecord`s) pub struct Pool<'a> { /// All the solvables that have been registered pub(crate) solvables: Vec>, @@ -219,7 +222,7 @@ impl<'a> Pool<'a> { } } - /// Resolves the id to a match spec + /// Returns the match spec associated to the provided id /// /// Panics if the match spec is not found in the pool pub fn resolve_match_spec(&self, id: MatchSpecId) -> &MatchSpec { @@ -240,28 +243,28 @@ impl<'a> Pool<'a> { } } - /// Resolves the id to a package name + /// Returns the package name associated to the provided id /// /// Panics if the package name is not found in the pool pub fn resolve_package_name(&self, name_id: NameId) -> &str { &self.package_names[name_id.index()] } - /// Resolves the id to a solvable + /// Returns the solvable associated to the provided id /// /// Panics if the solvable is not found in the pool pub fn resolve_solvable(&self, id: SolvableId) -> &PackageSolvable { self.resolve_solvable_inner(id).package() } - /// Resolves the id to a solvable + /// Returns the solvable associated to the provided id /// /// Panics if the solvable is not found in the pool pub fn resolve_solvable_mut(&mut self, id: SolvableId) -> &mut PackageSolvable<'a> { self.resolve_solvable_inner_mut(id).package_mut() } - /// Resolves the id to a solvable + /// Returns the solvable associated to the provided id /// /// Panics if the solvable is not found in the pool pub(crate) fn resolve_solvable_inner(&self, id: SolvableId) -> &Solvable { @@ -272,7 +275,7 @@ impl<'a> Pool<'a> { } } - /// Resolves the id to a solvable + /// Returns the solvable associated to the provided id /// /// Panics if the solvable is not found in the pool pub(crate) fn resolve_solvable_inner_mut(&mut self, id: SolvableId) -> &mut Solvable<'a> { diff --git a/crates/libsolv_rs/src/solvable.rs b/crates/libsolv_rs/src/solvable.rs index 629c07764..4b3e577e6 100644 --- a/crates/libsolv_rs/src/solvable.rs +++ b/crates/libsolv_rs/src/solvable.rs @@ -4,6 +4,9 @@ use rattler_conda_types::{PackageRecord, Version}; use std::fmt::{Display, Formatter}; /// A solvable that was derived from a Conda package +/// +/// Contains a reference to the `PackageRecord` that corresponds to the solvable (the `'a` lifetime +/// comes from the original `PackageRecord`) pub struct PackageSolvable<'a> { pub(crate) repo_id: RepoId, pub(crate) dependencies: Vec, @@ -21,6 +24,10 @@ impl PackageSolvable<'_> { #[derive(Default)] pub struct SolvableMetadata { + /// The original index of the package in the repository that loaded it + /// + /// Note: this is highly rattler-specific, and is used to retrieve the original `RepoDataRecord` + /// of the solvables after a solution is found pub original_index: Option, } From 2616a435f25524d28892b81bd2dd2227e8c7f862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Thu, 29 Jun 2023 14:01:37 +0200 Subject: [PATCH 08/17] More feedback --- crates/libsolv_rs/src/solver/decision_map.rs | 54 +++++++++++++------ .../libsolv_rs/src/solver/decision_tracker.rs | 6 +-- crates/rattler_solve/src/libsolv/input.rs | 2 +- crates/rattler_solve/src/libsolv/output.rs | 2 +- 4 files changed, 44 insertions(+), 20 deletions(-) diff --git a/crates/libsolv_rs/src/solver/decision_map.rs b/crates/libsolv_rs/src/solver/decision_map.rs index 62cc2e760..cd30c3463 100644 --- a/crates/libsolv_rs/src/solver/decision_map.rs +++ b/crates/libsolv_rs/src/solver/decision_map.rs @@ -1,42 +1,66 @@ use crate::id::SolvableId; use std::cmp::Ordering; +/// Represents a decision (i.e. an assignment to a solvable) and the level at which it was made +/// +/// = 0: undecided +/// > 0: level of decision when the solvable is set to true +/// < 0: level of decision when the solvable is set to false +#[repr(transparent)] +#[derive(Copy, Clone)] +struct DecisionAndLevel(i64); + +impl DecisionAndLevel { + fn undecided() -> DecisionAndLevel { + DecisionAndLevel(0) + } + + fn set(&mut self, value: bool, level: u32) { + self.0 = if value { level as i64 } else { -(level as i64) }; + } + + fn value(self) -> Option { + match self.0.cmp(&0) { + Ordering::Less => Some(false), + Ordering::Equal => None, + Ordering::Greater => Some(true), + } + } + + fn level(self) -> u32 { + self.0.unsigned_abs() as u32 + } +} + /// A map of the assignments to all solvables pub(crate) struct DecisionMap { - /// = 0: undecided - /// > 0: level of decision when the solvable is set to true - /// < 0: level of decision when the solvable is set to false - map: Vec, + map: Vec, } impl DecisionMap { - pub(crate) fn new(nsolvables: u32) -> Self { + pub(crate) fn new(solvable_count: u32) -> Self { Self { - map: vec![0; nsolvables as usize], + map: vec![DecisionAndLevel::undecided(); solvable_count as usize], } } - pub(crate) fn nsolvables(&self) -> u32 { + pub(crate) fn solvable_count(&self) -> u32 { self.map.len() as u32 } pub(crate) fn reset(&mut self, solvable_id: SolvableId) { - self.map[solvable_id.index()] = 0; + self.map[solvable_id.index()] = DecisionAndLevel::undecided(); } pub(crate) fn set(&mut self, solvable_id: SolvableId, value: bool, level: u32) { - self.map[solvable_id.index()] = if value { level as i64 } else { -(level as i64) }; + self.map[solvable_id.index()].set(value, level); } pub(crate) fn level(&self, solvable_id: SolvableId) -> u32 { - self.map[solvable_id.index()].unsigned_abs() as u32 + self.map[solvable_id.index()].level() } pub(crate) fn value(&self, solvable_id: SolvableId) -> Option { - match self.map[solvable_id.index()].cmp(&0) { - Ordering::Less => Some(false), - Ordering::Equal => None, - Ordering::Greater => Some(true), - } + self.map[solvable_id.index()].value() } } diff --git a/crates/libsolv_rs/src/solver/decision_tracker.rs b/crates/libsolv_rs/src/solver/decision_tracker.rs index c1a950359..65b597cce 100644 --- a/crates/libsolv_rs/src/solver/decision_tracker.rs +++ b/crates/libsolv_rs/src/solver/decision_tracker.rs @@ -11,16 +11,16 @@ pub(crate) struct DecisionTracker { } impl DecisionTracker { - pub(crate) fn new(nsolvables: u32) -> Self { + pub(crate) fn new(solvable_count: u32) -> Self { Self { - map: DecisionMap::new(nsolvables), + map: DecisionMap::new(solvable_count), stack: Vec::new(), propagate_index: 0, } } pub(crate) fn clear(&mut self) { - *self = Self::new(self.map.nsolvables()); + *self = Self::new(self.map.solvable_count()); } pub(crate) fn is_empty(&self) -> bool { diff --git a/crates/rattler_solve/src/libsolv/input.rs b/crates/rattler_solve/src/libsolv/input.rs index f9be56f58..6470ec975 100644 --- a/crates/rattler_solve/src/libsolv/input.rs +++ b/crates/rattler_solve/src/libsolv/input.rs @@ -130,6 +130,6 @@ pub fn add_virtual_packages(pool: &mut Pool, repo_id: RepoId, packages: &[Generi .leak(); for package in packages { - pool.add_package(repo_id, &package); + pool.add_package(repo_id, package); } } diff --git a/crates/rattler_solve/src/libsolv/output.rs b/crates/rattler_solve/src/libsolv/output.rs index 1982c8a3c..07a13c561 100644 --- a/crates/rattler_solve/src/libsolv/output.rs +++ b/crates/rattler_solve/src/libsolv/output.rs @@ -4,10 +4,10 @@ use libsolv_rs::id::RepoId; use libsolv_rs::id::SolvableId; use libsolv_rs::pool::Pool; +use libsolv_rs::transaction::Transaction; use libsolv_rs::transaction::TransactionKind; use rattler_conda_types::RepoDataRecord; use std::collections::HashMap; -use libsolv_rs::transaction::Transaction; /// Returns which packages should be installed in the environment /// From c8d10d311c4a403fd1fd20d64aa66bc54d61f847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Thu, 29 Jun 2023 14:32:21 +0200 Subject: [PATCH 09/17] Generate rules using DFS after all --- crates/libsolv_rs/src/solver/mod.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/libsolv_rs/src/solver/mod.rs b/crates/libsolv_rs/src/solver/mod.rs index c9bbee724..15fcf6662 100644 --- a/crates/libsolv_rs/src/solver/mod.rs +++ b/crates/libsolv_rs/src/solver/mod.rs @@ -7,16 +7,15 @@ use crate::solve_jobs::SolveJobs; use crate::transaction::{Transaction, TransactionKind}; use rattler_conda_types::MatchSpec; -use std::collections::{HashMap, HashSet, VecDeque}; +use std::collections::{HashMap, HashSet}; use decision::Decision; use decision_tracker::DecisionTracker; use rule::{Literal, Rule, RuleState}; use watch_map::WatchMap; -// TODO: remove pub from this mod decision; -pub(crate) mod decision_map; +mod decision_map; mod decision_tracker; pub(crate) mod rule; mod watch_map; @@ -57,8 +56,6 @@ impl<'a> Solver<'a> { /// Returns a [`Problem`] if problems remain unsolved, which provides ways to inspect the causes /// and report them to the user. pub fn solve(&mut self, jobs: SolveJobs) -> Result { - // TODO: sanity check that solvables inside jobs.favor are unique? - // Clear state self.pool.root_solvable_mut().clear(); self.decision_tracker.clear(); @@ -138,11 +135,11 @@ impl<'a> Solver<'a> { fn add_rules_for_root_dep(&mut self, favored_map: &HashMap) { let mut visited = HashSet::new(); - let mut queue = VecDeque::new(); + let mut stack = Vec::new(); - queue.push_back(SolvableId::root()); + stack.push(SolvableId::root()); - while let Some(solvable_id) = queue.pop_front() { + while let Some(solvable_id) = stack.pop() { let (deps, constrains) = match &self.pool.solvables[solvable_id.index()].inner { SolvableInner::Root(deps) => (deps, &[] as &[_]), SolvableInner::Package(pkg) => (&pkg.dependencies, pkg.constrains.as_slice()), @@ -163,7 +160,7 @@ impl<'a> Solver<'a> { for &candidate in candidates { // Note: we skip candidates we have already seen if visited.insert(candidate) { - queue.push_back(candidate); + stack.push(candidate); } } } From 74535b2037fd7209c49b665720485cdef0e60dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Thu, 29 Jun 2023 15:02:14 +0200 Subject: [PATCH 10/17] Simplify pool functions --- crates/libsolv_rs/src/pool.rs | 147 ++++++++++++--------------- crates/libsolv_rs/src/problem.rs | 4 +- crates/libsolv_rs/src/solver/mod.rs | 69 +++++++------ crates/libsolv_rs/src/solver/rule.rs | 25 ++--- 4 files changed, 116 insertions(+), 129 deletions(-) diff --git a/crates/libsolv_rs/src/pool.rs b/crates/libsolv_rs/src/pool.rs index a2ce25832..34ef2527e 100644 --- a/crates/libsolv_rs/src/pool.rs +++ b/crates/libsolv_rs/src/pool.rs @@ -33,10 +33,10 @@ pub struct Pool<'a> { match_specs_to_ids: HashMap, /// Cached candidates for each match spec, indexed by their MatchSpecId - pub(crate) match_spec_to_candidates: Vec>>, + pub(crate) match_spec_to_candidates: Vec>, /// Cached forbidden solvables for each match spec, indexed by their MatchSpecId - pub(crate) match_spec_to_forbidden: Vec>>, + pub(crate) match_spec_to_forbidden: Vec>, } impl<'a> Default for Pool<'a> { @@ -98,7 +98,7 @@ impl<'a> Pool<'a> { assert!(!solvable_id.is_root()); let name = self.intern_package_name(&record.name); - assert_ne!(self.solvables[solvable_id.index()].package().name, name); + assert_eq!(self.solvables[solvable_id.index()].package().name, name); self.solvables[solvable_id.index()] = Solvable::new_package(repo_id, name, record); } @@ -117,88 +117,74 @@ impl<'a> Pool<'a> { solvable.constrains.push(match_spec_id); } - // This function does not take `self`, because otherwise we run into problems with borrowing - // when we want to use it together with other pool functions - pub(crate) fn get_candidates<'b>( - match_specs: &[MatchSpec], - strings_to_ids: &HashMap, - solvables: &[Solvable], - packages_by_name: &[Vec], - match_spec_to_candidates: &'b mut [Option>], - favored_map: &HashMap, + /// Populates the list of candidates for the provided match spec + pub(crate) fn populate_candidates( + &self, match_spec_id: MatchSpecId, - ) -> &'b [SolvableId] { - let candidates = match_spec_to_candidates[match_spec_id.index()].get_or_insert_with(|| { - let match_spec = &match_specs[match_spec_id.index()]; - let match_spec_name = match_spec - .name - .as_deref() - .expect("match spec without name!"); - let name_id = match strings_to_ids.get(match_spec_name) { - None => return Vec::new(), - Some(name_id) => name_id, - }; - - let mut pkgs: Vec<_> = packages_by_name[name_id.index()] - .iter() - .cloned() - .filter(|solvable| match_spec.matches(solvables[solvable.index()].package().record)) - .collect(); - - pkgs.sort_by(|p1, p2| { - conda_util::compare_candidates( - solvables, - strings_to_ids, - packages_by_name, - solvables[p1.index()].package().record, - solvables[p2.index()].package().record, - ) - }); - - if let Some(&favored_id) = favored_map.get(name_id) { - if let Some(pos) = pkgs.iter().position(|&s| s == favored_id) { - let removed = pkgs.remove(pos); - pkgs.insert(0, removed); - } - } - - pkgs + favored_map: &HashMap, + match_spec_to_candidates: &mut [Vec], + ) { + let match_spec = &self.match_specs[match_spec_id.index()]; + let match_spec_name = match_spec + .name + .as_deref() + .expect("match spec without name!"); + let name_id = match self.names_to_ids.get(match_spec_name) { + None => return, + Some(name_id) => name_id, + }; + + let mut pkgs: Vec<_> = self.packages_by_name[name_id.index()] + .iter() + .cloned() + .filter(|solvable| { + match_spec.matches(self.solvables[solvable.index()].package().record) + }) + .collect(); + + pkgs.sort_by(|p1, p2| { + conda_util::compare_candidates( + &self.solvables, + &self.names_to_ids, + &self.packages_by_name, + self.solvables[p1.index()].package().record, + self.solvables[p2.index()].package().record, + ) }); - candidates.as_slice() + if let Some(&favored_id) = favored_map.get(name_id) { + if let Some(pos) = pkgs.iter().position(|&s| s == favored_id) { + let removed = pkgs.remove(pos); + pkgs.insert(0, removed); + } + } + + match_spec_to_candidates[match_spec_id.index()] = pkgs; } - // This function does not take `self`, because otherwise we run into problems with borrowing - // when we want to use it together with other pool functions - pub(crate) fn get_forbidden<'b>( - match_specs: &[MatchSpec], - strings_to_ids: &HashMap, - solvables: &[Solvable], - packages_by_name: &[Vec], - match_spec_to_forbidden: &'b mut [Option>], + /// Populates the list of forbidden packages for the provided match spec + pub(crate) fn populate_forbidden( + &self, match_spec_id: MatchSpecId, - ) -> &'b [SolvableId] { - let candidates = match_spec_to_forbidden[match_spec_id.index()].get_or_insert_with(|| { - let match_spec = &match_specs[match_spec_id.index()]; - let match_spec_name = match_spec - .name - .as_deref() - .expect("match spec without name!"); - let name_id = match strings_to_ids.get(match_spec_name) { - None => return Vec::new(), - Some(name_id) => name_id, - }; - - packages_by_name[name_id.index()] - .iter() - .cloned() - .filter(|solvable| { - !match_spec.matches(solvables[solvable.index()].package().record) - }) - .collect() - }); - - candidates.as_slice() + match_spec_to_forbidden: &mut [Vec], + ) { + let match_spec = &self.match_specs[match_spec_id.index()]; + let match_spec_name = match_spec + .name + .as_deref() + .expect("match spec without name!"); + let name_id = match self.names_to_ids.get(match_spec_name) { + None => return, + Some(name_id) => name_id, + }; + + match_spec_to_forbidden[match_spec_id.index()] = self.packages_by_name[name_id.index()] + .iter() + .cloned() + .filter(|solvable| { + !match_spec.matches(self.solvables[solvable.index()].package().record) + }) + .collect(); } /// Interns a match spec into the `Pool`, returning its `MatchSpecId` @@ -207,11 +193,8 @@ impl<'a> Pool<'a> { match self.match_specs_to_ids.entry(match_spec) { Entry::Occupied(entry) => *entry.get(), Entry::Vacant(entry) => { - // println!("Interning match_spec: {}", entry.key()); self.match_specs .push(MatchSpec::from_str(entry.key()).unwrap()); - self.match_spec_to_candidates.push(None); - self.match_spec_to_forbidden.push(None); // Update the entry let id = MatchSpecId::new(next_index); diff --git a/crates/libsolv_rs/src/problem.rs b/crates/libsolv_rs/src/problem.rs index 806ab6594..5341eac1f 100644 --- a/crates/libsolv_rs/src/problem.rs +++ b/crates/libsolv_rs/src/problem.rs @@ -47,9 +47,7 @@ impl Problem { Rule::Requires(package_id, match_spec_id) => { let package_node = Self::add_node(&mut graph, &mut nodes, package_id); - let candidates = solver.pool().match_spec_to_candidates[match_spec_id.index()] - .as_deref() - .unwrap(); + let candidates = &solver.pool().match_spec_to_candidates[match_spec_id.index()]; if candidates.is_empty() { println!( "{package_id:?} requires {match_spec_id:?}, which has no candidates" diff --git a/crates/libsolv_rs/src/solver/mod.rs b/crates/libsolv_rs/src/solver/mod.rs index 15fcf6662..b1f121b89 100644 --- a/crates/libsolv_rs/src/solver/mod.rs +++ b/crates/libsolv_rs/src/solver/mod.rs @@ -59,7 +59,11 @@ impl<'a> Solver<'a> { // Clear state self.pool.root_solvable_mut().clear(); self.decision_tracker.clear(); - self.rules = vec![RuleState::new(Rule::InstallRoot, &[], &self.pool)]; + self.rules = vec![RuleState::new( + Rule::InstallRoot, + &[], + &self.pool.match_spec_to_candidates, + )]; self.learnt_rules.clear(); self.learnt_why.clear(); @@ -87,7 +91,7 @@ impl<'a> Solver<'a> { self.rules.push(RuleState::new( Rule::ForbidMultipleInstances(candidate, other_candidate), &self.learnt_rules, - &self.pool, + &self.pool.match_spec_to_candidates, )); } } @@ -102,7 +106,7 @@ impl<'a> Solver<'a> { self.rules.push(RuleState::new( Rule::Lock(locked_solvable_id, other_candidate), &self.learnt_rules, - &self.pool, + &self.pool.match_spec_to_candidates, )); } } @@ -139,6 +143,12 @@ impl<'a> Solver<'a> { stack.push(SolvableId::root()); + let mut match_spec_to_candidates = vec![Vec::new(); self.pool.match_specs.len()]; + let mut match_spec_to_forbidden = vec![Vec::new(); self.pool.match_specs.len()]; + let mut seen_requires = HashSet::new(); + let mut seen_forbidden = HashSet::new(); + let empty_vec = Vec::new(); + while let Some(solvable_id) = stack.pop() { let (deps, constrains) = match &self.pool.solvables[solvable_id.index()].inner { SolvableInner::Root(deps) => (deps, &[] as &[_]), @@ -147,17 +157,15 @@ impl<'a> Solver<'a> { // Enqueue the candidates of the dependencies for &dep in deps { - let candidates = Pool::get_candidates( - &self.pool.match_specs, - &self.pool.names_to_ids, - &self.pool.solvables, - &self.pool.packages_by_name, - &mut self.pool.match_spec_to_candidates, - favored_map, - dep, - ); + if seen_requires.insert(dep) { + self.pool + .populate_candidates(dep, favored_map, &mut match_spec_to_candidates); + } - for &candidate in candidates { + for &candidate in match_spec_to_candidates + .get(dep.index()) + .unwrap_or(&empty_vec) + { // Note: we skip candidates we have already seen if visited.insert(candidate) { stack.push(candidate); @@ -170,31 +178,32 @@ impl<'a> Solver<'a> { self.rules.push(RuleState::new( Rule::Requires(solvable_id, dep), &self.learnt_rules, - &self.pool, + &match_spec_to_candidates, )); } // Constrains for &dep in constrains { - let dep_forbidden = Pool::get_forbidden( - &self.pool.match_specs, - &self.pool.names_to_ids, - &self.pool.solvables, - &self.pool.packages_by_name, - &mut self.pool.match_spec_to_forbidden, - dep, - ) - .to_vec(); + if seen_forbidden.insert(dep) { + self.pool + .populate_forbidden(dep, &mut match_spec_to_forbidden); + } - for dep in dep_forbidden { + for &dep in match_spec_to_forbidden + .get(dep.index()) + .unwrap_or(&empty_vec) + { self.rules.push(RuleState::new( Rule::Constrains(solvable_id, dep), &self.learnt_rules, - &self.pool, + &match_spec_to_candidates, )); } } } + + self.pool.match_spec_to_candidates = match_spec_to_candidates; + self.pool.match_spec_to_forbidden = match_spec_to_forbidden; } fn run_sat( @@ -284,9 +293,7 @@ impl<'a> Solver<'a> { } // Consider only rules in which no candidates have been installed - let candidates = self.pool.match_spec_to_candidates[deps.index()] - .as_deref() - .unwrap(); + let candidates = &self.pool.match_spec_to_candidates[deps.index()]; if candidates .iter() .any(|&c| self.decision_tracker.assigned_value(c) == Some(true)) @@ -746,7 +753,11 @@ impl<'a> Solver<'a> { self.learnt_rules.push(learnt.clone()); self.learnt_why.push(learnt_why); - let mut rule = RuleState::new(Rule::Learnt(learnt_index), &self.learnt_rules, &self.pool); + let mut rule = RuleState::new( + Rule::Learnt(learnt_index), + &self.learnt_rules, + &self.pool.match_spec_to_candidates, + ); if rule.has_watches() { self.watches.start_watching(&mut rule, rule_id); diff --git a/crates/libsolv_rs/src/solver/rule.rs b/crates/libsolv_rs/src/solver/rule.rs index 93c7485fd..a97087996 100644 --- a/crates/libsolv_rs/src/solver/rule.rs +++ b/crates/libsolv_rs/src/solver/rule.rs @@ -76,7 +76,7 @@ impl Rule { fn initial_watches( &self, learnt_rules: &[Vec], - pool: &Pool, + match_spec_to_candidates: &[Vec], ) -> Option<[SolvableId; 2]> { match self { Rule::InstallRoot => None, @@ -96,10 +96,7 @@ impl Rule { } } Rule::Requires(id, match_spec) => { - let candidates = pool.match_spec_to_candidates[match_spec.index()] - .as_ref() - .unwrap(); - + let candidates = &match_spec_to_candidates[match_spec.index()]; if candidates.is_empty() { None } else { @@ -133,10 +130,7 @@ impl Rule { return; } - for &solvable_id in pool.match_spec_to_candidates[match_spec_id.index()] - .as_deref() - .unwrap() - { + for &solvable_id in &pool.match_spec_to_candidates[match_spec_id.index()] { if !visit(Literal { solvable_id, negate: false, @@ -193,9 +187,13 @@ pub(crate) struct RuleState { } impl RuleState { - pub fn new(kind: Rule, learnt_rules: &[Vec], pool: &Pool) -> Self { + pub fn new( + kind: Rule, + learnt_rules: &[Vec], + match_spec_to_candidates: &[Vec], + ) -> Self { let watched_literals = kind - .initial_watches(learnt_rules, pool) + .initial_watches(learnt_rules, match_spec_to_candidates) .unwrap_or([SolvableId::null(), SolvableId::null()]); let rule = Self { @@ -350,10 +348,7 @@ impl RuleState { } // The available candidates - for &candidate in pool.match_spec_to_candidates[match_spec_id.index()] - .as_deref() - .unwrap() - { + for &candidate in &pool.match_spec_to_candidates[match_spec_id.index()] { let lit = Literal { solvable_id: candidate, negate: false, From 330580c8e6fa6e644fbde260443d107b4e01e427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Thu, 29 Jun 2023 16:12:21 +0200 Subject: [PATCH 11/17] Introduce Arena --- crates/libsolv_rs/src/arena.rs | 51 +++++++++++++++++++++++++++++ crates/libsolv_rs/src/conda_util.rs | 7 ++-- crates/libsolv_rs/src/id.rs | 14 ++++++-- crates/libsolv_rs/src/lib.rs | 1 + crates/libsolv_rs/src/pool.rs | 50 ++++++++++++---------------- crates/libsolv_rs/src/solver/mod.rs | 6 +++- 6 files changed, 94 insertions(+), 35 deletions(-) create mode 100644 crates/libsolv_rs/src/arena.rs diff --git a/crates/libsolv_rs/src/arena.rs b/crates/libsolv_rs/src/arena.rs new file mode 100644 index 000000000..de90868e4 --- /dev/null +++ b/crates/libsolv_rs/src/arena.rs @@ -0,0 +1,51 @@ +use std::marker::PhantomData; +use std::ops::{Index, IndexMut}; + +pub(crate) trait ArenaId { + fn from_usize(x: usize) -> Self; + fn to_usize(self) -> usize; +} + +pub(crate) struct Arena { + data: Vec, + phantom: PhantomData, +} + +impl Arena { + pub(crate) fn new() -> Self { + Self { + data: Vec::new(), + phantom: PhantomData::default(), + } + } + + pub(crate) fn alloc(&mut self, value: TValue) -> TId { + let id = TId::from_usize(self.data.len()); + self.data.push(value); + id + } + + // TODO: all places where we are using len, are places where we should introduce mappings + pub(crate) fn len(&self) -> usize { + self.data.len() + } + + #[cfg(test)] + pub(crate) fn as_slice(&self) -> &[TValue] { + &self.data + } +} + +impl Index for Arena { + type Output = TValue; + + fn index(&self, index: TId) -> &Self::Output { + &self.data[index.to_usize()] + } +} + +impl IndexMut for Arena { + fn index_mut(&mut self, index: TId) -> &mut Self::Output { + &mut self.data[index.to_usize()] + } +} diff --git a/crates/libsolv_rs/src/conda_util.rs b/crates/libsolv_rs/src/conda_util.rs index 03a255830..c26ad20cd 100644 --- a/crates/libsolv_rs/src/conda_util.rs +++ b/crates/libsolv_rs/src/conda_util.rs @@ -1,3 +1,4 @@ +use crate::arena::Arena; use crate::id::{NameId, SolvableId}; use crate::solvable::Solvable; use rattler_conda_types::{MatchSpec, PackageRecord, Version}; @@ -7,7 +8,7 @@ use std::str::FromStr; /// Returns the order of two candidates based on rules used by conda. pub(crate) fn compare_candidates( - solvables: &[Solvable], + solvables: &Arena, interned_strings: &HashMap, packages_by_name: &[Vec], a: &PackageRecord, @@ -115,7 +116,7 @@ pub(crate) fn compare_candidates( } pub(crate) fn find_highest_version( - solvables: &[Solvable], + solvables: &Arena, interned_strings: &HashMap, packages_by_name: &[Vec], match_spec: &MatchSpec, @@ -126,7 +127,7 @@ pub(crate) fn find_highest_version( // For each record that matches the spec let candidates = packages_by_name[name_id.index()] .iter() - .map(|s| solvables[s.index()].package().record) + .map(|&s| solvables[s].package().record) .filter(|s| match_spec.matches(s)); candidates.fold(None, |init, record| { diff --git a/crates/libsolv_rs/src/id.rs b/crates/libsolv_rs/src/id.rs index 5ef49e0a5..f6d14a0ee 100644 --- a/crates/libsolv_rs/src/id.rs +++ b/crates/libsolv_rs/src/id.rs @@ -1,3 +1,5 @@ +use crate::arena::ArenaId; + #[repr(transparent)] #[derive(Clone, Copy, Eq, PartialEq, Hash)] pub struct RepoId(u32); @@ -42,9 +44,7 @@ pub struct SolvableId(u32); impl SolvableId { pub(crate) fn new(index: usize) -> Self { - debug_assert_ne!(index, 0); debug_assert_ne!(index, u32::MAX as usize); - Self(index as u32) } @@ -69,6 +69,16 @@ impl SolvableId { } } +impl ArenaId for SolvableId { + fn from_usize(x: usize) -> Self { + SolvableId::new(x) + } + + fn to_usize(self) -> usize { + self.index() + } +} + #[repr(transparent)] #[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Debug, Hash)] pub(crate) struct RuleId(u32); diff --git a/crates/libsolv_rs/src/lib.rs b/crates/libsolv_rs/src/lib.rs index 0e2148ffa..f01596390 100644 --- a/crates/libsolv_rs/src/lib.rs +++ b/crates/libsolv_rs/src/lib.rs @@ -1,3 +1,4 @@ +mod arena; mod conda_util; pub mod id; pub mod pool; diff --git a/crates/libsolv_rs/src/pool.rs b/crates/libsolv_rs/src/pool.rs index 34ef2527e..9f7d43ef8 100644 --- a/crates/libsolv_rs/src/pool.rs +++ b/crates/libsolv_rs/src/pool.rs @@ -1,3 +1,4 @@ +use crate::arena::Arena; use crate::conda_util; use crate::id::{MatchSpecId, NameId, RepoId, SolvableId}; use crate::solvable::{PackageSolvable, Solvable}; @@ -12,7 +13,7 @@ use std::str::FromStr; /// from the original `PackageRecord`s) pub struct Pool<'a> { /// All the solvables that have been registered - pub(crate) solvables: Vec>, + pub(crate) solvables: Arena>, /// The total amount of registered repos total_repos: u32, @@ -41,8 +42,11 @@ pub struct Pool<'a> { impl<'a> Default for Pool<'a> { fn default() -> Self { + let mut solvables = Arena::new(); + solvables.alloc(Solvable::new_root()); + Self { - solvables: vec![Solvable::new_root()], + solvables, total_repos: 0, names_to_ids: HashMap::new(), @@ -76,9 +80,9 @@ impl<'a> Pool<'a> { let name = self.intern_package_name(&record.name); - let solvable_id = SolvableId::new(self.solvables.len()); - self.solvables - .push(Solvable::new_package(repo_id, name, record)); + let solvable_id = self + .solvables + .alloc(Solvable::new_package(repo_id, name, record)); self.packages_by_name[name.index()].push(solvable_id); @@ -98,22 +102,22 @@ impl<'a> Pool<'a> { assert!(!solvable_id.is_root()); let name = self.intern_package_name(&record.name); - assert_eq!(self.solvables[solvable_id.index()].package().name, name); + assert_eq!(self.solvables[solvable_id].package().name, name); - self.solvables[solvable_id.index()] = Solvable::new_package(repo_id, name, record); + self.solvables[solvable_id] = Solvable::new_package(repo_id, name, record); } /// Registers a dependency for the provided solvable pub fn add_dependency(&mut self, solvable_id: SolvableId, match_spec: String) { let match_spec_id = self.intern_matchspec(match_spec); - let solvable = self.solvables[solvable_id.index()].package_mut(); + let solvable = self.solvables[solvable_id].package_mut(); solvable.dependencies.push(match_spec_id); } /// Registers a constrains for the provided solvable pub fn add_constrains(&mut self, solvable_id: SolvableId, match_spec: String) { let match_spec_id = self.intern_matchspec(match_spec); - let solvable = self.solvables[solvable_id.index()].package_mut(); + let solvable = self.solvables[solvable_id].package_mut(); solvable.constrains.push(match_spec_id); } @@ -137,18 +141,16 @@ impl<'a> Pool<'a> { let mut pkgs: Vec<_> = self.packages_by_name[name_id.index()] .iter() .cloned() - .filter(|solvable| { - match_spec.matches(self.solvables[solvable.index()].package().record) - }) + .filter(|&solvable| match_spec.matches(self.solvables[solvable].package().record)) .collect(); - pkgs.sort_by(|p1, p2| { + pkgs.sort_by(|&p1, &p2| { conda_util::compare_candidates( &self.solvables, &self.names_to_ids, &self.packages_by_name, - self.solvables[p1.index()].package().record, - self.solvables[p2.index()].package().record, + self.solvables[p1].package().record, + self.solvables[p2].package().record, ) }); @@ -181,9 +183,7 @@ impl<'a> Pool<'a> { match_spec_to_forbidden[match_spec_id.index()] = self.packages_by_name[name_id.index()] .iter() .cloned() - .filter(|solvable| { - !match_spec.matches(self.solvables[solvable.index()].package().record) - }) + .filter(|&solvable| !match_spec.matches(self.solvables[solvable].package().record)) .collect(); } @@ -251,26 +251,18 @@ impl<'a> Pool<'a> { /// /// Panics if the solvable is not found in the pool pub(crate) fn resolve_solvable_inner(&self, id: SolvableId) -> &Solvable { - if id.index() < self.solvables.len() { - &self.solvables[id.index()] - } else { - panic!("invalid solvable id!") - } + &self.solvables[id] } /// Returns the solvable associated to the provided id /// /// Panics if the solvable is not found in the pool pub(crate) fn resolve_solvable_inner_mut(&mut self, id: SolvableId) -> &mut Solvable<'a> { - if id.index() < self.solvables.len() { - &mut self.solvables[id.index()] - } else { - panic!("invalid solvable id!") - } + &mut self.solvables[id] } /// Returns the dependencies associated to the root solvable pub(crate) fn root_solvable_mut(&mut self) -> &mut Vec { - self.solvables[0].root_mut() + self.solvables[SolvableId::root()].root_mut() } } diff --git a/crates/libsolv_rs/src/solver/mod.rs b/crates/libsolv_rs/src/solver/mod.rs index b1f121b89..70450bba2 100644 --- a/crates/libsolv_rs/src/solver/mod.rs +++ b/crates/libsolv_rs/src/solver/mod.rs @@ -150,7 +150,7 @@ impl<'a> Solver<'a> { let empty_vec = Vec::new(); while let Some(solvable_id) = stack.pop() { - let (deps, constrains) = match &self.pool.solvables[solvable_id.index()].inner { + let (deps, constrains) = match &self.pool.solvables[solvable_id].inner { SolvableInner::Root(deps) => (deps, &[] as &[_]), SolvableInner::Package(pkg) => (&pkg.dependencies, pkg.constrains.as_slice()), }; @@ -1013,6 +1013,7 @@ mod test { let locked = pool .solvables + .as_slice() .iter() .position(|s| { if let Some(package) = s.get_package() { @@ -1046,6 +1047,7 @@ mod test { let locked = pool .solvables + .as_slice() .iter() .position(|s| { if let Some(package) = s.get_package() { @@ -1087,6 +1089,7 @@ mod test { // Already installed: A=1; B=1 let already_installed = pool .solvables + .as_slice() .iter() .enumerate() .skip(1) // Skip the root solvable @@ -1123,6 +1126,7 @@ mod test { // Already installed: A=1; B=1; C=1 let already_installed = pool .solvables + .as_slice() .iter() .enumerate() .skip(1) // Skip the root solvable From ea40d00b340f70f47505059591e0a1157259baa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Thu, 29 Jun 2023 17:16:10 +0200 Subject: [PATCH 12/17] More arenas and mappings --- crates/libsolv_rs/src/conda_util.rs | 7 ++-- crates/libsolv_rs/src/id.rs | 20 +++++++++ crates/libsolv_rs/src/lib.rs | 1 + crates/libsolv_rs/src/mapping.rs | 43 +++++++++++++++++++ crates/libsolv_rs/src/pool.rs | 62 ++++++++++++++-------------- crates/libsolv_rs/src/problem.rs | 2 +- crates/libsolv_rs/src/solver/mod.rs | 23 +++++------ crates/libsolv_rs/src/solver/rule.rs | 19 +++++---- 8 files changed, 121 insertions(+), 56 deletions(-) create mode 100644 crates/libsolv_rs/src/mapping.rs diff --git a/crates/libsolv_rs/src/conda_util.rs b/crates/libsolv_rs/src/conda_util.rs index c26ad20cd..84e969d41 100644 --- a/crates/libsolv_rs/src/conda_util.rs +++ b/crates/libsolv_rs/src/conda_util.rs @@ -1,5 +1,6 @@ use crate::arena::Arena; use crate::id::{NameId, SolvableId}; +use crate::mapping::Mapping; use crate::solvable::Solvable; use rattler_conda_types::{MatchSpec, PackageRecord, Version}; use std::cmp::Ordering; @@ -10,7 +11,7 @@ use std::str::FromStr; pub(crate) fn compare_candidates( solvables: &Arena, interned_strings: &HashMap, - packages_by_name: &[Vec], + packages_by_name: &Mapping>, a: &PackageRecord, b: &PackageRecord, ) -> Ordering { @@ -118,14 +119,14 @@ pub(crate) fn compare_candidates( pub(crate) fn find_highest_version( solvables: &Arena, interned_strings: &HashMap, - packages_by_name: &[Vec], + packages_by_name: &Mapping>, match_spec: &MatchSpec, ) -> Option<(Version, bool)> { let name = match_spec.name.as_deref().unwrap(); let name_id = interned_strings[name]; // For each record that matches the spec - let candidates = packages_by_name[name_id.index()] + let candidates = packages_by_name[name_id] .iter() .map(|&s| solvables[s].package().record) .filter(|s| match_spec.matches(s)); diff --git a/crates/libsolv_rs/src/id.rs b/crates/libsolv_rs/src/id.rs index f6d14a0ee..22fa601f8 100644 --- a/crates/libsolv_rs/src/id.rs +++ b/crates/libsolv_rs/src/id.rs @@ -24,6 +24,16 @@ impl NameId { } } +impl ArenaId for NameId { + fn from_usize(x: usize) -> Self { + NameId::new(x) + } + + fn to_usize(self) -> usize { + self.index() + } +} + #[repr(transparent)] #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] pub struct MatchSpecId(u32); @@ -38,6 +48,16 @@ impl MatchSpecId { } } +impl ArenaId for MatchSpecId { + fn from_usize(x: usize) -> Self { + MatchSpecId::new(x) + } + + fn to_usize(self) -> usize { + self.index() + } +} + #[repr(transparent)] #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)] pub struct SolvableId(u32); diff --git a/crates/libsolv_rs/src/lib.rs b/crates/libsolv_rs/src/lib.rs index f01596390..9d1d65195 100644 --- a/crates/libsolv_rs/src/lib.rs +++ b/crates/libsolv_rs/src/lib.rs @@ -1,6 +1,7 @@ mod arena; mod conda_util; pub mod id; +mod mapping; pub mod pool; pub mod problem; pub mod solvable; diff --git a/crates/libsolv_rs/src/mapping.rs b/crates/libsolv_rs/src/mapping.rs new file mode 100644 index 000000000..655a6ccc3 --- /dev/null +++ b/crates/libsolv_rs/src/mapping.rs @@ -0,0 +1,43 @@ +use crate::arena::ArenaId; +use std::marker::PhantomData; +use std::ops::{Index, IndexMut}; + +pub(crate) struct Mapping { + data: Vec, + phantom: PhantomData, +} + +impl Mapping { + pub(crate) fn new(data: Vec) -> Self { + Self { + data, + phantom: PhantomData::default(), + } + } + + pub(crate) fn get(&self, id: TId) -> Option<&TValue> { + self.data.get(id.to_usize()) + } + + pub(crate) fn extend(&mut self, value: TValue) { + self.data.push(value); + } + + pub(crate) fn values(&self) -> impl Iterator { + self.data.iter() + } +} + +impl Index for Mapping { + type Output = TValue; + + fn index(&self, index: TId) -> &Self::Output { + &self.data[index.to_usize()] + } +} + +impl IndexMut for Mapping { + fn index_mut(&mut self, index: TId) -> &mut Self::Output { + &mut self.data[index.to_usize()] + } +} diff --git a/crates/libsolv_rs/src/pool.rs b/crates/libsolv_rs/src/pool.rs index 9f7d43ef8..c4a74ebf1 100644 --- a/crates/libsolv_rs/src/pool.rs +++ b/crates/libsolv_rs/src/pool.rs @@ -1,6 +1,7 @@ use crate::arena::Arena; use crate::conda_util; use crate::id::{MatchSpecId, NameId, RepoId, SolvableId}; +use crate::mapping::Mapping; use crate::solvable::{PackageSolvable, Solvable}; use rattler_conda_types::{MatchSpec, PackageRecord}; use std::collections::hash_map::Entry; @@ -19,25 +20,25 @@ pub struct Pool<'a> { total_repos: u32, /// Interned package names - package_names: Vec, + package_names: Arena, /// Map from package names to the id of their interned counterpart pub(crate) names_to_ids: HashMap, /// Map from interned package names to the solvables that have that name - pub(crate) packages_by_name: Vec>, + pub(crate) packages_by_name: Mapping>, /// Interned match specs - pub(crate) match_specs: Vec, + pub(crate) match_specs: Arena, /// Map from match spec strings to the id of their interned counterpart match_specs_to_ids: HashMap, /// Cached candidates for each match spec, indexed by their MatchSpecId - pub(crate) match_spec_to_candidates: Vec>, + pub(crate) match_spec_to_candidates: Mapping>, /// Cached forbidden solvables for each match spec, indexed by their MatchSpecId - pub(crate) match_spec_to_forbidden: Vec>, + pub(crate) match_spec_to_forbidden: Mapping>, } impl<'a> Default for Pool<'a> { @@ -50,13 +51,13 @@ impl<'a> Default for Pool<'a> { total_repos: 0, names_to_ids: HashMap::new(), - package_names: Vec::new(), - packages_by_name: Vec::new(), + package_names: Arena::new(), + packages_by_name: Mapping::new(Vec::new()), match_specs_to_ids: HashMap::default(), - match_specs: Vec::new(), - match_spec_to_candidates: Vec::new(), - match_spec_to_forbidden: Vec::new(), + match_specs: Arena::new(), + match_spec_to_candidates: Mapping::new(Vec::new()), + match_spec_to_forbidden: Mapping::new(Vec::new()), } } } @@ -84,7 +85,7 @@ impl<'a> Pool<'a> { .solvables .alloc(Solvable::new_package(repo_id, name, record)); - self.packages_by_name[name.index()].push(solvable_id); + self.packages_by_name[name].push(solvable_id); solvable_id } @@ -126,19 +127,19 @@ impl<'a> Pool<'a> { &self, match_spec_id: MatchSpecId, favored_map: &HashMap, - match_spec_to_candidates: &mut [Vec], + match_spec_to_candidates: &mut Mapping>, ) { - let match_spec = &self.match_specs[match_spec_id.index()]; + let match_spec = &self.match_specs[match_spec_id]; let match_spec_name = match_spec .name .as_deref() .expect("match spec without name!"); let name_id = match self.names_to_ids.get(match_spec_name) { None => return, - Some(name_id) => name_id, + Some(&name_id) => name_id, }; - let mut pkgs: Vec<_> = self.packages_by_name[name_id.index()] + let mut pkgs: Vec<_> = self.packages_by_name[name_id] .iter() .cloned() .filter(|&solvable| match_spec.matches(self.solvables[solvable].package().record)) @@ -154,33 +155,33 @@ impl<'a> Pool<'a> { ) }); - if let Some(&favored_id) = favored_map.get(name_id) { + if let Some(&favored_id) = favored_map.get(&name_id) { if let Some(pos) = pkgs.iter().position(|&s| s == favored_id) { let removed = pkgs.remove(pos); pkgs.insert(0, removed); } } - match_spec_to_candidates[match_spec_id.index()] = pkgs; + match_spec_to_candidates[match_spec_id] = pkgs; } /// Populates the list of forbidden packages for the provided match spec pub(crate) fn populate_forbidden( &self, match_spec_id: MatchSpecId, - match_spec_to_forbidden: &mut [Vec], + match_spec_to_forbidden: &mut Mapping>, ) { - let match_spec = &self.match_specs[match_spec_id.index()]; + let match_spec = &self.match_specs[match_spec_id]; let match_spec_name = match_spec .name .as_deref() .expect("match spec without name!"); let name_id = match self.names_to_ids.get(match_spec_name) { None => return, - Some(name_id) => name_id, + Some(&name_id) => name_id, }; - match_spec_to_forbidden[match_spec_id.index()] = self.packages_by_name[name_id.index()] + match_spec_to_forbidden[match_spec_id] = self.packages_by_name[name_id] .iter() .cloned() .filter(|&solvable| !match_spec.matches(self.solvables[solvable].package().record)) @@ -189,15 +190,14 @@ impl<'a> Pool<'a> { /// Interns a match spec into the `Pool`, returning its `MatchSpecId` pub(crate) fn intern_matchspec(&mut self, match_spec: String) -> MatchSpecId { - let next_index = self.match_specs.len(); match self.match_specs_to_ids.entry(match_spec) { Entry::Occupied(entry) => *entry.get(), Entry::Vacant(entry) => { - self.match_specs - .push(MatchSpec::from_str(entry.key()).unwrap()); + let id = self + .match_specs + .alloc(MatchSpec::from_str(entry.key()).unwrap()); // Update the entry - let id = MatchSpecId::new(next_index); entry.insert(id); id @@ -209,17 +209,19 @@ impl<'a> Pool<'a> { /// /// Panics if the match spec is not found in the pool pub fn resolve_match_spec(&self, id: MatchSpecId) -> &MatchSpec { - &self.match_specs[id.index()] + &self.match_specs[id] } /// Interns a package name into the `Pool`, returning its `NameId` fn intern_package_name>(&mut self, str: T) -> NameId { - let next_id = NameId::new(self.names_to_ids.len()); match self.names_to_ids.entry(str.into()) { Entry::Occupied(e) => *e.get(), Entry::Vacant(e) => { - self.package_names.push(e.key().clone()); - self.packages_by_name.push(Vec::new()); + let next_id = self.package_names.alloc(e.key().clone()); + + // Keep the mapping in sync + self.packages_by_name.extend(Vec::new()); + e.insert(next_id); next_id } @@ -230,7 +232,7 @@ impl<'a> Pool<'a> { /// /// Panics if the package name is not found in the pool pub fn resolve_package_name(&self, name_id: NameId) -> &str { - &self.package_names[name_id.index()] + &self.package_names[name_id] } /// Returns the solvable associated to the provided id diff --git a/crates/libsolv_rs/src/problem.rs b/crates/libsolv_rs/src/problem.rs index 5341eac1f..ab243e127 100644 --- a/crates/libsolv_rs/src/problem.rs +++ b/crates/libsolv_rs/src/problem.rs @@ -47,7 +47,7 @@ impl Problem { Rule::Requires(package_id, match_spec_id) => { let package_node = Self::add_node(&mut graph, &mut nodes, package_id); - let candidates = &solver.pool().match_spec_to_candidates[match_spec_id.index()]; + let candidates = &solver.pool().match_spec_to_candidates[match_spec_id]; if candidates.is_empty() { println!( "{package_id:?} requires {match_spec_id:?}, which has no candidates" diff --git a/crates/libsolv_rs/src/solver/mod.rs b/crates/libsolv_rs/src/solver/mod.rs index 70450bba2..0cb9d3825 100644 --- a/crates/libsolv_rs/src/solver/mod.rs +++ b/crates/libsolv_rs/src/solver/mod.rs @@ -9,6 +9,7 @@ use crate::transaction::{Transaction, TransactionKind}; use rattler_conda_types::MatchSpec; use std::collections::{HashMap, HashSet}; +use crate::mapping::Mapping; use decision::Decision; use decision_tracker::DecisionTracker; use rule::{Literal, Rule, RuleState}; @@ -84,7 +85,7 @@ impl<'a> Solver<'a> { self.add_rules_for_root_dep(&favored_map); // Initialize rules ensuring only a single candidate per package name is installed - for candidates in &self.pool.packages_by_name { + for candidates in self.pool.packages_by_name.values() { // Each candidate gets a rule with each other candidate for (i, &candidate) in candidates.iter().enumerate() { for &other_candidate in &candidates[i + 1..] { @@ -101,7 +102,7 @@ impl<'a> Solver<'a> { for &locked_solvable_id in &jobs.lock { // For each locked solvable, forbid other solvables with the same name let name = self.pool.resolve_solvable(locked_solvable_id).name; - for &other_candidate in &self.pool.packages_by_name[name.index()] { + for &other_candidate in &self.pool.packages_by_name[name] { if other_candidate != locked_solvable_id { self.rules.push(RuleState::new( Rule::Lock(locked_solvable_id, other_candidate), @@ -143,8 +144,10 @@ impl<'a> Solver<'a> { stack.push(SolvableId::root()); - let mut match_spec_to_candidates = vec![Vec::new(); self.pool.match_specs.len()]; - let mut match_spec_to_forbidden = vec![Vec::new(); self.pool.match_specs.len()]; + let mut match_spec_to_candidates = + Mapping::new(vec![Vec::new(); self.pool.match_specs.len()]); + let mut match_spec_to_forbidden = + Mapping::new(vec![Vec::new(); self.pool.match_specs.len()]); let mut seen_requires = HashSet::new(); let mut seen_forbidden = HashSet::new(); let empty_vec = Vec::new(); @@ -162,10 +165,7 @@ impl<'a> Solver<'a> { .populate_candidates(dep, favored_map, &mut match_spec_to_candidates); } - for &candidate in match_spec_to_candidates - .get(dep.index()) - .unwrap_or(&empty_vec) - { + for &candidate in match_spec_to_candidates.get(dep).unwrap_or(&empty_vec) { // Note: we skip candidates we have already seen if visited.insert(candidate) { stack.push(candidate); @@ -189,10 +189,7 @@ impl<'a> Solver<'a> { .populate_forbidden(dep, &mut match_spec_to_forbidden); } - for &dep in match_spec_to_forbidden - .get(dep.index()) - .unwrap_or(&empty_vec) - { + for &dep in match_spec_to_forbidden.get(dep).unwrap_or(&empty_vec) { self.rules.push(RuleState::new( Rule::Constrains(solvable_id, dep), &self.learnt_rules, @@ -293,7 +290,7 @@ impl<'a> Solver<'a> { } // Consider only rules in which no candidates have been installed - let candidates = &self.pool.match_spec_to_candidates[deps.index()]; + let candidates = &self.pool.match_spec_to_candidates[deps]; if candidates .iter() .any(|&c| self.decision_tracker.assigned_value(c) == Some(true)) diff --git a/crates/libsolv_rs/src/solver/rule.rs b/crates/libsolv_rs/src/solver/rule.rs index a97087996..7db37f05a 100644 --- a/crates/libsolv_rs/src/solver/rule.rs +++ b/crates/libsolv_rs/src/solver/rule.rs @@ -1,6 +1,7 @@ use crate::id::MatchSpecId; use crate::id::RuleId; use crate::id::SolvableId; +use crate::mapping::Mapping; use crate::pool::Pool; use crate::solver::decision_map::DecisionMap; use std::fmt::{Debug, Formatter}; @@ -76,14 +77,14 @@ impl Rule { fn initial_watches( &self, learnt_rules: &[Vec], - match_spec_to_candidates: &[Vec], + match_spec_to_candidates: &Mapping>, ) -> Option<[SolvableId; 2]> { match self { Rule::InstallRoot => None, Rule::Constrains(s1, s2) | Rule::ForbidMultipleInstances(s1, s2) => Some([*s1, *s2]), Rule::Lock(_, s) => Some([SolvableId::root(), *s]), - Rule::Learnt(index) => { - let literals = &learnt_rules[*index]; + &Rule::Learnt(index) => { + let literals = &learnt_rules[index]; debug_assert!(!literals.is_empty()); if literals.len() == 1 { // No need for watches, since we learned an assertion @@ -95,12 +96,12 @@ impl Rule { ]) } } - Rule::Requires(id, match_spec) => { - let candidates = &match_spec_to_candidates[match_spec.index()]; + &Rule::Requires(id, match_spec) => { + let candidates = &match_spec_to_candidates[match_spec]; if candidates.is_empty() { None } else { - Some([*id, candidates[0]]) + Some([id, candidates[0]]) } } } @@ -130,7 +131,7 @@ impl Rule { return; } - for &solvable_id in &pool.match_spec_to_candidates[match_spec_id.index()] { + for &solvable_id in &pool.match_spec_to_candidates[match_spec_id] { if !visit(Literal { solvable_id, negate: false, @@ -190,7 +191,7 @@ impl RuleState { pub fn new( kind: Rule, learnt_rules: &[Vec], - match_spec_to_candidates: &[Vec], + match_spec_to_candidates: &Mapping>, ) -> Self { let watched_literals = kind .initial_watches(learnt_rules, match_spec_to_candidates) @@ -348,7 +349,7 @@ impl RuleState { } // The available candidates - for &candidate in &pool.match_spec_to_candidates[match_spec_id.index()] { + for &candidate in &pool.match_spec_to_candidates[match_spec_id] { let lit = Literal { solvable_id: candidate, negate: false, From 807d87a659b549f89a1870dd7a7291e37a32ebc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Thu, 29 Jun 2023 17:24:03 +0200 Subject: [PATCH 13/17] Use arena for learnt rules --- crates/libsolv_rs/src/arena.rs | 4 +++ crates/libsolv_rs/src/id.rs | 13 +++++++++ crates/libsolv_rs/src/solver/mod.rs | 20 ++++++------- crates/libsolv_rs/src/solver/rule.rs | 42 +++++++++++++++------------- 4 files changed, 50 insertions(+), 29 deletions(-) diff --git a/crates/libsolv_rs/src/arena.rs b/crates/libsolv_rs/src/arena.rs index de90868e4..2bca8e8b7 100644 --- a/crates/libsolv_rs/src/arena.rs +++ b/crates/libsolv_rs/src/arena.rs @@ -19,6 +19,10 @@ impl Arena { } } + pub(crate) fn clear(&mut self) { + self.data.clear(); + } + pub(crate) fn alloc(&mut self, value: TValue) -> TId { let id = TId::from_usize(self.data.len()); self.data.push(value); diff --git a/crates/libsolv_rs/src/id.rs b/crates/libsolv_rs/src/id.rs index 22fa601f8..a759c5049 100644 --- a/crates/libsolv_rs/src/id.rs +++ b/crates/libsolv_rs/src/id.rs @@ -127,3 +127,16 @@ impl RuleId { RuleId(u32::MAX) } } + +#[derive(Copy, Clone, Debug)] +pub(crate) struct LearntRuleId(u32); + +impl ArenaId for LearntRuleId { + fn from_usize(x: usize) -> Self { + Self(x as u32) + } + + fn to_usize(self) -> usize { + self.0 as usize + } +} diff --git a/crates/libsolv_rs/src/solver/mod.rs b/crates/libsolv_rs/src/solver/mod.rs index 0cb9d3825..ba48b6f2b 100644 --- a/crates/libsolv_rs/src/solver/mod.rs +++ b/crates/libsolv_rs/src/solver/mod.rs @@ -1,5 +1,7 @@ -use crate::id::NameId; +use crate::arena::Arena; +use crate::id::{LearntRuleId, NameId}; use crate::id::{RuleId, SolvableId}; +use crate::mapping::Mapping; use crate::pool::Pool; use crate::problem::Problem; use crate::solvable::SolvableInner; @@ -9,7 +11,6 @@ use crate::transaction::{Transaction, TransactionKind}; use rattler_conda_types::MatchSpec; use std::collections::{HashMap, HashSet}; -use crate::mapping::Mapping; use decision::Decision; use decision_tracker::DecisionTracker; use rule::{Literal, Rule, RuleState}; @@ -27,7 +28,7 @@ pub struct Solver<'a> { pub(crate) rules: Vec, watches: WatchMap, - learnt_rules: Vec>, + learnt_rules: Arena>, learnt_rules_start: RuleId, learnt_why: Vec>, @@ -40,7 +41,7 @@ impl<'a> Solver<'a> { Self { rules: Vec::new(), watches: WatchMap::new(), - learnt_rules: Vec::new(), + learnt_rules: Arena::new(), learnt_rules_start: RuleId::null(), learnt_why: Vec::new(), decision_tracker: DecisionTracker::new(pool.solvables.len() as u32), @@ -60,13 +61,13 @@ impl<'a> Solver<'a> { // Clear state self.pool.root_solvable_mut().clear(); self.decision_tracker.clear(); + self.learnt_rules.clear(); + self.learnt_why.clear(); self.rules = vec![RuleState::new( Rule::InstallRoot, - &[], + &self.learnt_rules, &self.pool.match_spec_to_candidates, )]; - self.learnt_rules.clear(); - self.learnt_why.clear(); // Favored map let mut favored_map = HashMap::new(); @@ -746,12 +747,11 @@ impl<'a> Solver<'a> { // Add the rule let rule_id = RuleId::new(self.rules.len()); - let learnt_index = self.learnt_rules.len(); - self.learnt_rules.push(learnt.clone()); + let learnt_id = self.learnt_rules.alloc(learnt.clone()); self.learnt_why.push(learnt_why); let mut rule = RuleState::new( - Rule::Learnt(learnt_index), + Rule::Learnt(learnt_id), &self.learnt_rules, &self.pool.match_spec_to_candidates, ); diff --git a/crates/libsolv_rs/src/solver/rule.rs b/crates/libsolv_rs/src/solver/rule.rs index 7db37f05a..be2a78dfc 100644 --- a/crates/libsolv_rs/src/solver/rule.rs +++ b/crates/libsolv_rs/src/solver/rule.rs @@ -1,6 +1,7 @@ -use crate::id::MatchSpecId; +use crate::arena::Arena; use crate::id::RuleId; use crate::id::SolvableId; +use crate::id::{LearntRuleId, MatchSpecId}; use crate::mapping::Mapping; use crate::pool::Pool; use crate::solver::decision_map::DecisionMap; @@ -67,24 +68,24 @@ pub(crate) enum Rule { Lock(SolvableId, SolvableId), /// A rule learnt during solving /// - /// The `usize` is an index that can be used to retrieve the rule's literals, which are stored + /// The learnt rule id can be used to retrieve the rule's literals, which are stored /// elsewhere to prevent the size of [`Rule`] from blowing up - Learnt(usize), + Learnt(LearntRuleId), } impl Rule { /// Returns the ids of the solvables that will be watched right after the rule is created fn initial_watches( &self, - learnt_rules: &[Vec], + learnt_rules: &Arena>, match_spec_to_candidates: &Mapping>, ) -> Option<[SolvableId; 2]> { match self { Rule::InstallRoot => None, Rule::Constrains(s1, s2) | Rule::ForbidMultipleInstances(s1, s2) => Some([*s1, *s2]), Rule::Lock(_, s) => Some([SolvableId::root(), *s]), - &Rule::Learnt(index) => { - let literals = &learnt_rules[index]; + &Rule::Learnt(learnt_id) => { + let literals = &learnt_rules[learnt_id]; debug_assert!(!literals.is_empty()); if literals.len() == 1 { // No need for watches, since we learned an assertion @@ -110,14 +111,14 @@ impl Rule { /// Visits each literal in the rule pub fn visit_literals( &self, - learnt_rules: &[Vec], + learnt_rules: &Arena>, pool: &Pool, mut visit: impl FnMut(Literal) -> bool, ) { match *self { Rule::InstallRoot => unreachable!(), - Rule::Learnt(index) => { - for &literal in &learnt_rules[index] { + Rule::Learnt(learnt_id) => { + for &literal in &learnt_rules[learnt_id] { if !visit(literal) { return; } @@ -190,7 +191,7 @@ pub(crate) struct RuleState { impl RuleState { pub fn new( kind: Rule, - learnt_rules: &[Vec], + learnt_rules: &Arena>, match_spec_to_candidates: &Mapping>, ) -> Self { let watched_literals = kind @@ -251,7 +252,7 @@ impl RuleState { &self, solvable_id: SolvableId, decision_map: &DecisionMap, - learnt_rules: &[Vec], + learnt_rules: &Arena>, ) -> Option<([Literal; 2], usize)> { debug_assert!(self.watched_literals.contains(&solvable_id)); @@ -271,7 +272,10 @@ impl RuleState { !self.watched_literals[0].is_null() } - pub fn watched_literals(&self, learnt_rules: &[Vec]) -> [Literal; 2] { + pub fn watched_literals( + &self, + learnt_rules: &Arena>, + ) -> [Literal; 2] { let literals = |op1: bool, op2: bool| { [ Literal { @@ -287,15 +291,15 @@ impl RuleState { match self.kind { Rule::InstallRoot => unreachable!(), - Rule::Learnt(index) => { + Rule::Learnt(learnt_id) => { // TODO: we might want to do something else for performance, like keeping the whole // literal in `self.watched_literals`, to avoid lookups... But first we should // benchmark! - let &w1 = learnt_rules[index] + let &w1 = learnt_rules[learnt_id] .iter() .find(|l| l.solvable_id == self.watched_literals[0]) .unwrap(); - let &w2 = learnt_rules[index] + let &w2 = learnt_rules[learnt_id] .iter() .find(|l| l.solvable_id == self.watched_literals[1]) .unwrap(); @@ -319,7 +323,7 @@ impl RuleState { pub fn next_unwatched_variable( &self, pool: &Pool, - learnt_rules: &[Vec], + learnt_rules: &Arena>, decision_map: &DecisionMap, ) -> Option { // The next unwatched variable (if available), is a variable that is: @@ -332,7 +336,7 @@ impl RuleState { match self.kind { Rule::InstallRoot => unreachable!(), - Rule::Learnt(index) => learnt_rules[index] + Rule::Learnt(learnt_id) => learnt_rules[learnt_id] .iter() .cloned() .find(|&l| can_watch(l)) @@ -405,7 +409,7 @@ impl Debug for RuleDebug<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self.kind { Rule::InstallRoot => write!(f, "install root"), - Rule::Learnt(index) => write!(f, "learnt rule {index}"), + Rule::Learnt(learnt_id) => write!(f, "learnt rule {learnt_id:?}"), Rule::Requires(solvable_id, match_spec_id) => { let match_spec = self.pool.resolve_match_spec(match_spec_id).to_string(); write!( @@ -577,6 +581,6 @@ mod test { // This test is here to ensure we don't increase the size of `Rule` by accident, as we are // creating thousands of instances. Note: libsolv manages to bring down the size to 24, so // there is probably room for improvement. - assert_eq!(std::mem::size_of::(), 32); + assert_eq!(std::mem::size_of::(), 28); } } From 9b6cc2e5f9eda3443519314cf5d4aca90846b010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Thu, 29 Jun 2023 17:26:17 +0200 Subject: [PATCH 14/17] Fix failing test --- crates/libsolv_rs/src/pool.rs | 4 ++-- crates/rattler_solve/src/lib.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/libsolv_rs/src/pool.rs b/crates/libsolv_rs/src/pool.rs index c4a74ebf1..c9f14d31b 100644 --- a/crates/libsolv_rs/src/pool.rs +++ b/crates/libsolv_rs/src/pool.rs @@ -34,10 +34,10 @@ pub struct Pool<'a> { /// Map from match spec strings to the id of their interned counterpart match_specs_to_ids: HashMap, - /// Cached candidates for each match spec, indexed by their MatchSpecId + /// Cached candidates for each match spec pub(crate) match_spec_to_candidates: Mapping>, - /// Cached forbidden solvables for each match spec, indexed by their MatchSpecId + /// Cached forbidden solvables for each match spec pub(crate) match_spec_to_forbidden: Mapping>, } diff --git a/crates/rattler_solve/src/lib.rs b/crates/rattler_solve/src/lib.rs index bfd62893a..7a574ace9 100644 --- a/crates/rattler_solve/src/lib.rs +++ b/crates/rattler_solve/src/lib.rs @@ -250,7 +250,7 @@ mod test_libsolv { let err = result.err().unwrap(); match err { SolveError::Unsolvable(errors) => { - assert_eq!(errors, vec!["nothing provides requested asdfasdf"]) + assert_eq!(errors, vec!["No candidates where found for asdfasdf.\n"]) } _ => panic!("Unexpected error: {err:?}"), } From 6ad4df4d96bb1c7a01e3f8db996a8d03b09c929c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Thu, 29 Jun 2023 17:29:37 +0200 Subject: [PATCH 15/17] Fix clippy --- crates/libsolv_rs/src/solver/rule.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/libsolv_rs/src/solver/rule.rs b/crates/libsolv_rs/src/solver/rule.rs index be2a78dfc..9ed73e44e 100644 --- a/crates/libsolv_rs/src/solver/rule.rs +++ b/crates/libsolv_rs/src/solver/rule.rs @@ -463,6 +463,7 @@ mod test { } #[test] + #[allow(clippy::bool_assert_comparison)] fn test_literal_satisfying_value() { let lit = Literal { solvable_id: SolvableId::root(), @@ -532,7 +533,7 @@ mod test { // Unlink 1 { - let mut rule1 = rule1.clone(); + let mut rule1 = rule1; rule1.unlink_rule(&rule3, SolvableId::new(1211), 0); assert_eq!( rule1.watched_literals, @@ -566,7 +567,7 @@ mod test { // Unlink 1 { - let mut rule1 = rule1.clone(); + let mut rule1 = rule1; rule1.unlink_rule(&rule2, SolvableId::new(1211), 1); assert_eq!( rule1.watched_literals, From f5da6cf1f03ecd247985a0fe8073a41328191059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Thu, 29 Jun 2023 17:37:49 +0200 Subject: [PATCH 16/17] Use mapping in watch map --- crates/libsolv_rs/src/arena.rs | 15 +++++++++------ crates/libsolv_rs/src/mapping.rs | 9 ++++++++- crates/libsolv_rs/src/pool.rs | 6 +++--- crates/libsolv_rs/src/solver/mod.rs | 16 +++++++++------- crates/libsolv_rs/src/solver/watch_map.rs | 21 ++++++++++++--------- 5 files changed, 41 insertions(+), 26 deletions(-) diff --git a/crates/libsolv_rs/src/arena.rs b/crates/libsolv_rs/src/arena.rs index 2bca8e8b7..6fe781498 100644 --- a/crates/libsolv_rs/src/arena.rs +++ b/crates/libsolv_rs/src/arena.rs @@ -1,11 +1,9 @@ use std::marker::PhantomData; use std::ops::{Index, IndexMut}; -pub(crate) trait ArenaId { - fn from_usize(x: usize) -> Self; - fn to_usize(self) -> usize; -} - +/// An `Arena` holds a collection of `TValue`s but allocates persistent `TId`s that are used +/// to refer to an element in the arena. When adding an item to an `Arena` it returns a `TId` that +/// can be used to index into the arena. pub(crate) struct Arena { data: Vec, phantom: PhantomData, @@ -29,7 +27,6 @@ impl Arena { id } - // TODO: all places where we are using len, are places where we should introduce mappings pub(crate) fn len(&self) -> usize { self.data.len() } @@ -53,3 +50,9 @@ impl IndexMut for Arena { &mut self.data[index.to_usize()] } } + +/// A trait indicating that the type can be transformed to `usize` and back +pub(crate) trait ArenaId { + fn from_usize(x: usize) -> Self; + fn to_usize(self) -> usize; +} diff --git a/crates/libsolv_rs/src/mapping.rs b/crates/libsolv_rs/src/mapping.rs index 655a6ccc3..bac989fea 100644 --- a/crates/libsolv_rs/src/mapping.rs +++ b/crates/libsolv_rs/src/mapping.rs @@ -2,12 +2,19 @@ use crate::arena::ArenaId; use std::marker::PhantomData; use std::ops::{Index, IndexMut}; +/// An `Arena` holds a collection of `TValue`s that can be addressed by `TId`s. You can +/// think of it as a HashMap, optimized for the case in which we know the `TId`s are +/// contiguous. pub(crate) struct Mapping { data: Vec, phantom: PhantomData, } -impl Mapping { +impl Mapping { + pub(crate) fn empty() -> Self { + Self::new(Vec::new()) + } + pub(crate) fn new(data: Vec) -> Self { Self { data, diff --git a/crates/libsolv_rs/src/pool.rs b/crates/libsolv_rs/src/pool.rs index c9f14d31b..361d4cefe 100644 --- a/crates/libsolv_rs/src/pool.rs +++ b/crates/libsolv_rs/src/pool.rs @@ -52,12 +52,12 @@ impl<'a> Default for Pool<'a> { names_to_ids: HashMap::new(), package_names: Arena::new(), - packages_by_name: Mapping::new(Vec::new()), + packages_by_name: Mapping::empty(), match_specs_to_ids: HashMap::default(), match_specs: Arena::new(), - match_spec_to_candidates: Mapping::new(Vec::new()), - match_spec_to_forbidden: Mapping::new(Vec::new()), + match_spec_to_candidates: Mapping::empty(), + match_spec_to_forbidden: Mapping::empty(), } } } diff --git a/crates/libsolv_rs/src/solver/mod.rs b/crates/libsolv_rs/src/solver/mod.rs index ba48b6f2b..e6fe6900e 100644 --- a/crates/libsolv_rs/src/solver/mod.rs +++ b/crates/libsolv_rs/src/solver/mod.rs @@ -1,4 +1,4 @@ -use crate::arena::Arena; +use crate::arena::{Arena, ArenaId}; use crate::id::{LearntRuleId, NameId}; use crate::id::{RuleId, SolvableId}; use crate::mapping::Mapping; @@ -30,7 +30,7 @@ pub struct Solver<'a> { learnt_rules: Arena>, learnt_rules_start: RuleId, - learnt_why: Vec>, + learnt_why: Mapping>, decision_tracker: DecisionTracker, } @@ -43,7 +43,7 @@ impl<'a> Solver<'a> { watches: WatchMap::new(), learnt_rules: Arena::new(), learnt_rules_start: RuleId::null(), - learnt_why: Vec::new(), + learnt_why: Mapping::empty(), decision_tracker: DecisionTracker::new(pool.solvables.len() as u32), pool, } @@ -62,7 +62,7 @@ impl<'a> Solver<'a> { self.pool.root_solvable_mut().clear(); self.decision_tracker.clear(); self.learnt_rules.clear(); - self.learnt_why.clear(); + self.learnt_why = Mapping::empty(); self.rules = vec![RuleState::new( Rule::InstallRoot, &self.learnt_rules, @@ -562,7 +562,7 @@ impl<'a> Solver<'a> { fn analyze_unsolvable_rule( rules: &[RuleState], - learnt_why: &[Vec], + learnt_why: &Mapping>, learnt_rules_start: RuleId, rule_id: RuleId, problem: &mut Problem, @@ -575,7 +575,9 @@ impl<'a> Solver<'a> { return; } - for &cause in &learnt_why[rule_id.index() - learnt_rules_start.index()] { + let rule_id = + LearntRuleId::from_usize(rule_id.index() - learnt_rules_start.index()); + for &cause in &learnt_why[rule_id] { Self::analyze_unsolvable_rule( rules, learnt_why, @@ -748,7 +750,7 @@ impl<'a> Solver<'a> { // Add the rule let rule_id = RuleId::new(self.rules.len()); let learnt_id = self.learnt_rules.alloc(learnt.clone()); - self.learnt_why.push(learnt_why); + self.learnt_why.extend(learnt_why); let mut rule = RuleState::new( Rule::Learnt(learnt_id), diff --git a/crates/libsolv_rs/src/solver/watch_map.rs b/crates/libsolv_rs/src/solver/watch_map.rs index bc09dc784..b27d593b8 100644 --- a/crates/libsolv_rs/src/solver/watch_map.rs +++ b/crates/libsolv_rs/src/solver/watch_map.rs @@ -1,21 +1,24 @@ use crate::id::RuleId; use crate::id::SolvableId; +use crate::mapping::Mapping; use crate::solver::rule::RuleState; /// A map from solvables to the rules that are watching them pub(crate) struct WatchMap { /// Note: the map is to a single rule, but rules form a linked list, so it is possible to go /// from one to the next - map: Vec, + map: Mapping, } impl WatchMap { pub(crate) fn new() -> Self { - Self { map: Vec::new() } + Self { + map: Mapping::empty(), + } } - pub(crate) fn initialize(&mut self, nsolvables: usize) { - self.map = vec![RuleId::null(); nsolvables]; + pub(crate) fn initialize(&mut self, solvable_count: usize) { + self.map = Mapping::new(vec![RuleId::null(); solvable_count]); } pub(crate) fn start_watching(&mut self, rule: &mut RuleState, rule_id: RuleId) { @@ -42,20 +45,20 @@ impl WatchMap { predecessor_rule.unlink_rule(rule, previous_watch, watch_index); } else { // This was the first rule in the chain - self.map[previous_watch.index()] = rule.get_linked_rule(watch_index); + self.map[previous_watch] = rule.get_linked_rule(watch_index); } // Set the new watch rule.watched_literals[watch_index] = new_watch; - rule.link_to_rule(watch_index, self.map[new_watch.index()]); - self.map[new_watch.index()] = rule_id; + rule.link_to_rule(watch_index, self.map[new_watch]); + self.map[new_watch] = rule_id; } pub(crate) fn first_rule_watching_solvable(&mut self, watched_solvable: SolvableId) -> RuleId { - self.map[watched_solvable.index()] + self.map[watched_solvable] } pub(crate) fn watch_solvable(&mut self, watched_solvable: SolvableId, id: RuleId) { - self.map[watched_solvable.index()] = id; + self.map[watched_solvable] = id; } } From a2a3cc0358a8b2f01fd67f4bab641af4238373cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Ochagav=C3=ADa?= Date: Mon, 3 Jul 2023 11:24:39 +0200 Subject: [PATCH 17/17] Add docs for solver --- crates/libsolv_rs/src/solver/mod.rs | 160 +++++++++++++++++++++------- 1 file changed, 120 insertions(+), 40 deletions(-) diff --git a/crates/libsolv_rs/src/solver/mod.rs b/crates/libsolv_rs/src/solver/mod.rs index e6fe6900e..32c65ffef 100644 --- a/crates/libsolv_rs/src/solver/mod.rs +++ b/crates/libsolv_rs/src/solver/mod.rs @@ -22,14 +22,25 @@ mod decision_tracker; pub(crate) mod rule; mod watch_map; +/// Drives the SAT solving process +/// +/// Keeps solvables in a `Pool`, which contains references to `PackageRecord`s (the `'a` lifetime +/// comes from the original `PackageRecord`s) +/// +/// The implemented algorithm is CDCL (conflict-driven clause learning), which is masterly explained +/// in [An Extensible SAT-solver](http://minisat.se/downloads/MiniSat.pdf). Regarding the data +/// structures used, we mostly follow the approach taken by +/// [libsolv](https://github.com/openSUSE/libsolv). The code of libsolv is, however, very low level +/// C, so if you are looking for an introduction to CDCL, you are encouraged to look at the paper +/// instead or to keep reading through this codebase and its comments. pub struct Solver<'a> { pool: Pool<'a>, pub(crate) rules: Vec, watches: WatchMap, - learnt_rules: Arena>, learnt_rules_start: RuleId, + learnt_rules: Arena>, learnt_why: Mapping>, decision_tracker: DecisionTracker, @@ -49,13 +60,14 @@ impl<'a> Solver<'a> { } } + /// Returns a reference to the pool used by the solver pub fn pool(&self) -> &Pool { &self.pool } /// Solves the provided `jobs` and returns a transaction from the found solution /// - /// Returns a [`Problem`] if problems remain unsolved, which provides ways to inspect the causes + /// Returns a [`Problem`] if no solution was found, which provides ways to inspect the causes /// and report them to the user. pub fn solve(&mut self, jobs: SolveJobs) -> Result { // Clear state @@ -83,7 +95,7 @@ impl<'a> Solver<'a> { } // Create rules for root's dependencies, and their dependencies, and so forth - self.add_rules_for_root_dep(&favored_map); + self.add_rules_for_root_deps(&favored_map); // Initialize rules ensuring only a single candidate per package name is installed for candidates in self.pool.packages_by_name.values() { @@ -139,7 +151,18 @@ impl<'a> Solver<'a> { Ok(Transaction { steps }) } - fn add_rules_for_root_dep(&mut self, favored_map: &HashMap) { + /// Adds rules for root's dependencies, their dependencies, and so forth + /// + /// This function makes sure we only generate rules for the solvables involved in the problem, + /// traversing the graph of requirements and ignoring unrelated packages. The graph is + /// traversed depth-first. + /// + /// A side effect of this function is that candidates for all involved match specs (in the + /// dependencies or constrains part of the package record) are fetched and cached for future + /// use. The `favored_map` parameter influences the order in which the candidates for a + /// dependency are sorted, giving preference to the favored package (i.e. placing it at the + /// front). + fn add_rules_for_root_deps(&mut self, favored_map: &HashMap) { let mut visited = HashSet::new(); let mut stack = Vec::new(); @@ -204,46 +227,69 @@ impl<'a> Solver<'a> { self.pool.match_spec_to_forbidden = match_spec_to_forbidden; } + /// Run the CDCL algorithm to solve the SAT problem + /// + /// The CDCL algorithm's job is to find a valid assignment to the variables involved in the + /// provided clauses. It works in the following steps: + /// + /// 1. __Set__: Assign a value to a variable that hasn't been assigned yet. An assignment in + /// this step starts a new "level" (the first one being level 1). If all variables have been + /// assigned, then we are done. + /// 2. __Propagate__: Perform [unit + /// propagation](https://en.wikipedia.org/wiki/Unit_propagation). Assignments in this step + /// are associated to the same "level" as the decision that triggered them. This "level" + /// metadata is useful when it comes to handling conflicts. See [`Solver::propagate`] for the + /// implementation of this step. + /// 3. __Learn__: If propagation finishes without conflicts, go back to 1. Otherwise find the + /// combination of assignments that caused the conflict and add a new clause to the solver to + /// forbid that combination of assignments (i.e. learn from this mistake so it is not + /// repeated in the future). Then backtrack and go back to step 1 or, if the learnt clause is + /// in conflict with existing clauses, declare the problem to be unsolvable. See + /// [`Solver::analyze`] for the implementation of this step. + /// + /// The solver loop can be found in [`Solver::resolve_dependencies`]. fn run_sat( &mut self, top_level_requirements: &[MatchSpec], locked_solvables: &[SolvableId], ) -> Result<(), Problem> { - let level = self.install_root_solvable(); + assert!(self.decision_tracker.is_empty()); - self.decide_top_level_assertions(level, locked_solvables, top_level_requirements) + // Assign `true` to the root solvable + let level = 1; + self.decision_tracker + .try_add_decision( + Decision::new(SolvableId::root(), true, RuleId::install_root()), + 1, + ) + .expect("bug: solvable was already decided!"); + + // Forbid packages that rely on dependencies without candidates + self.decide_requires_without_candidates(level, locked_solvables, top_level_requirements) .map_err(|cause| self.analyze_unsolvable(cause))?; + // Propagate after the assignments above self.propagate(level) .map_err(|(_, _, cause)| self.analyze_unsolvable(cause))?; + // Enter the solver loop self.resolve_dependencies(level)?; Ok(()) } - fn install_root_solvable(&mut self) -> u32 { - assert!(self.decision_tracker.is_empty()); - self.decision_tracker - .try_add_decision( - Decision::new(SolvableId::root(), true, RuleId::install_root()), - 1, - ) - .expect("bug: solvable was already decided!"); - - // The root solvable is installed at level 1 - 1 - } - - fn decide_top_level_assertions( + /// Forbid packages that rely on dependencies without candidates + /// + /// Since a requires clause is represented as (¬A ∨ candidate_1 ∨ ... ∨ candidate_n), + /// a dependency without candidates becomes (¬A), which means that A should always be false. + fn decide_requires_without_candidates( &mut self, level: u32, _locked_solvables: &[SolvableId], _top_level_requirements: &[MatchSpec], ) -> Result<(), RuleId> { - println!("=== Deciding assertions"); + println!("=== Deciding assertions for requires without candidates"); - // Assertions derived from requirements that cannot be fulfilled for (i, rule) in self.rules.iter().enumerate() { if let Rule::Requires(solvable_id, _) = rule.kind { if !rule.has_watches() { @@ -269,6 +315,14 @@ impl<'a> Solver<'a> { } /// Resolves all dependencies + /// + /// Repeatedly chooses the next variable to assign, and calls [`Solver::set_propagate_learn`] to + /// drive the solving process (as you can see from the name, the method executes the set, + /// propagate and learn steps described in the [`Solver::run_sat`] docs). + /// + /// The next variable to assign is obtained by finding the next dependency for which no concrete + /// package has been picked yet. Then we pick the highest possible version for that package, or + /// the favored version if it was provided by the user, and set its value to true. fn resolve_dependencies(&mut self, mut level: u32) -> Result { let mut i = 0; loop { @@ -323,6 +377,17 @@ impl<'a> Solver<'a> { Ok(level) } + /// Executes one iteration of the CDCL loop + /// + /// A set-propagate-learn round is always initiated by a requirement clause (i.e. + /// [`Rule::Requires`]). The parameters include the variable associated to the candidate for the + /// dependency (`solvable`), the package that originates the dependency (`required_by`), and the + /// id of the requires clause (`rule_id`). + /// + /// Refer to the documentation of [`Solver::run_sat`] for details on the CDCL algorithm. + /// + /// Returns the new level after this set-propagate-learn round, or a [`Problem`] if we + /// discovered that the requested jobs are unsatisfiable. fn set_propagate_learn( &mut self, mut level: u32, @@ -425,8 +490,16 @@ impl<'a> Solver<'a> { Ok(level) } + /// The propagate step of the CDCL algorithm + /// + /// Propagation is implemented by means of watches: each rule that has two or more literals is + /// "subscribed" to changes in the values of two solvables that appear in the rule. When a value + /// is assigned to a solvable, each of the rules tracking that solvable will be notified. That + /// way, the rule can check whether the literal that is using the solvable has become false, in + /// which case it picks a new solvable to watch (if available) or triggers an assignment. fn propagate(&mut self, level: u32) -> Result<(), (SolvableId, bool, RuleId)> { - // Learnt assertions + // Learnt assertions (assertions are clauses that consist of a single literal, and therefore + // do not have watches) let learnt_rules_start = self.learnt_rules_start.index(); for (i, rule) in self.rules[learnt_rules_start..].iter().enumerate() { let Rule::Learnt(learnt_index) = rule.kind else { @@ -455,7 +528,7 @@ impl<'a> Solver<'a> { } } - // Watched literals + // Watched solvables while let Some(decision) = self.decision_tracker.next_unpropagated() { let pkg = decision.solvable_id; @@ -560,6 +633,10 @@ impl<'a> Solver<'a> { Ok(()) } + /// Adds the rule with `rule_id` to the current `Problem` + /// + /// Because learnt rules are not relevant for the user, they are not added to the `Problem`. + /// Instead, we report the rules that caused them. fn analyze_unsolvable_rule( rules: &[RuleState], learnt_why: &Mapping>, @@ -592,6 +669,7 @@ impl<'a> Solver<'a> { } } + /// Create a [`Problem`] based on the id of the rule that triggered an unrecoverable conflict fn analyze_unsolvable(&mut self, rule_id: RuleId) -> Problem { let last_decision = self.decision_tracker.stack().last().unwrap(); let highest_level = self.decision_tracker.level(last_decision.solvable_id); @@ -661,18 +739,27 @@ impl<'a> Solver<'a> { problem } + /// Analyze the causes of the conflict and learn from it + /// + /// This function finds the combination of assignments that caused the conflict and adds a new + /// clause to the solver to forbid that combination of assignments (i.e. learn from this mistake + /// so it is not repeated in the future). It corresponds to the `Solver.analyze` function from + /// the MiniSAT paper. + /// + /// Returns the level to which we should backtrack, the id of the learnt rule and the literal + /// that should be assigned (by definition, when we learn a rule, all its literals except one + /// evaluate to false, so the value of the remaining literal must be assigned to make the clause + /// become true) fn analyze( &mut self, mut current_level: u32, - mut s: SolvableId, + mut conflicting_solvable: SolvableId, mut rule_id: RuleId, ) -> (u32, RuleId, Literal) { let mut seen = HashSet::new(); let mut causes_at_current_level = 0u32; let mut learnt = Vec::new(); - let mut btlevel = 0; - - // println!("=== ANALYZE"); + let mut back_track_to = 0; let mut s_value; let mut learnt_why = Vec::new(); @@ -684,7 +771,7 @@ impl<'a> Solver<'a> { &self.learnt_rules, &self.pool, |literal| { - if !first_iteration && literal.solvable_id == s { + if !first_iteration && literal.solvable_id == conflicting_solvable { // We are only interested in the causes of the conflict, so we ignore the // solvable whose value was propagated return true; @@ -707,7 +794,7 @@ impl<'a> Solver<'a> { .unwrap(), }; learnt.push(learnt_literal); - btlevel = btlevel.max(decision_level); + back_track_to = back_track_to.max(decision_level); } else { unreachable!(); } @@ -722,7 +809,7 @@ impl<'a> Solver<'a> { loop { let (last_decision, last_decision_level) = self.decision_tracker.undo_last(); - s = last_decision.solvable_id; + conflicting_solvable = last_decision.solvable_id; s_value = last_decision.value; rule_id = last_decision.derived_from; @@ -742,7 +829,7 @@ impl<'a> Solver<'a> { } let last_literal = Literal { - solvable_id: s, + solvable_id: conflicting_solvable, negate: s_value, }; learnt.push(last_literal); @@ -774,15 +861,8 @@ impl<'a> Solver<'a> { ); } - // println!("Backtracked from {level} to {btlevel}"); - - // print!("Last decision before backtracking: "); - // let decision = self.decision_queue.back().unwrap(); - // self.pool.resolve_solvable(decision.solvable_id).debug(); - // println!(" = {}", decision.value); - // Should revert at most to the root level - let target_level = btlevel.max(1); + let target_level = back_track_to.max(1); self.decision_tracker.undo_until(target_level); (target_level, rule_id, last_literal)