From 7b9e911d88695472e8053d9dc6e60ed0db6230e6 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Mon, 10 Jun 2024 16:54:07 +0200 Subject: [PATCH 1/3] feat: add `DependencySnapshot` (#44) * feat: add DependencySnapshot * chore: add more interning methods * fix: tests * fix: missing snapshot --- Cargo.toml | 4 + src/internal/id.rs | 8 + src/internal/mapping.rs | 60 +++- src/lib.rs | 12 + src/snapshot.rs | 448 ++++++++++++++++++++++++++ tests/snapshots/solver__snapshot.snap | 8 + tests/solver.rs | 76 ++++- tools/solve-snapshot/Cargo.toml | 13 + tools/solve-snapshot/src/main.rs | 84 +++++ 9 files changed, 703 insertions(+), 10 deletions(-) create mode 100644 src/snapshot.rs create mode 100644 tests/snapshots/solver__snapshot.snap create mode 100644 tools/solve-snapshot/Cargo.toml create mode 100644 tools/solve-snapshot/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 124701f..5ce1b70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = ["tools/solve-snapshot"] + [package] name = "resolvo" version = "0.6.0" @@ -33,3 +36,4 @@ proptest = "1.2" tracing-test = { version = "0.2.4", features = ["no-env-filter"] } tokio = { version = "1.35.1", features = ["time", "rt"] } resolvo = { path = ".", features = ["tokio"] } +serde_json = "1.0" \ No newline at end of file diff --git a/src/internal/id.rs b/src/internal/id.rs index 535d139..ab84316 100644 --- a/src/internal/id.rs +++ b/src/internal/id.rs @@ -5,6 +5,8 @@ use crate::{internal::arena::ArenaId, Interner}; /// The id associated to a package name #[repr(transparent)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] pub struct NameId(u32); impl ArenaId for NameId { @@ -20,6 +22,8 @@ impl ArenaId for NameId { /// The id associated with a generic string #[repr(transparent)] #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] pub struct StringId(u32); impl ArenaId for StringId { @@ -35,6 +39,8 @@ impl ArenaId for StringId { /// The id associated with a VersionSet. #[repr(transparent)] #[derive(Clone, Default, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] pub struct VersionSetId(u32); impl ArenaId for VersionSetId { @@ -50,6 +56,8 @@ impl ArenaId for VersionSetId { /// The id associated to a solvable #[repr(transparent)] #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] pub struct SolvableId(u32); /// Internally used id for solvables that can also represent root and null. diff --git a/src/internal/mapping.rs b/src/internal/mapping.rs index 14c67c1..34cf4a4 100644 --- a/src/internal/mapping.rs +++ b/src/internal/mapping.rs @@ -1,16 +1,17 @@ +use std::{cmp, iter::FusedIterator, marker::PhantomData}; + use crate::internal::arena::ArenaId; -use std::cmp; -use std::iter::FusedIterator; -use std::marker::PhantomData; const VALUES_PER_CHUNK: usize = 128; -/// A `Mapping` 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. +/// A `Mapping` 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. +#[derive(Clone)] pub struct Mapping { chunks: Vec<[Option; VALUES_PER_CHUNK]>, len: usize, + max: usize, _phantom: PhantomData, } @@ -35,6 +36,7 @@ impl Mapping { Self { chunks, len: 0, + max: 0, _phantom: Default::default(), } } @@ -49,7 +51,8 @@ impl Mapping { /// Insert into the mapping with the specific value pub fn insert(&mut self, id: TId, value: TValue) { - let (chunk, offset) = Self::chunk_and_offset(id.to_usize()); + let idx = id.to_usize(); + let (chunk, offset) = Self::chunk_and_offset(idx); // Resize to fit if needed if chunk >= self.chunks.len() { @@ -58,6 +61,7 @@ impl Mapping { } self.chunks[chunk][offset] = Some(value); self.len += 1; + self.max = self.max.max(idx); } /// Get a specific value in the mapping with bound checks @@ -95,7 +99,9 @@ impl Mapping { /// Get a specific value in the mapping without bound checks /// /// # Safety - /// The caller must uphold most of the safety requirements for `get_unchecked`. i.e. the id having been inserted into the Mapping before. + /// The caller must uphold most of the safety requirements for + /// `get_unchecked`. i.e. the id having been inserted into the Mapping + /// before. pub unsafe fn get_unchecked(&self, id: TId) -> &TValue { let (chunk, offset) = Self::chunk_and_offset(id.to_usize()); self.chunks @@ -108,7 +114,9 @@ impl Mapping { /// Get a specific value in the mapping without bound checks /// /// # Safety - /// The caller must uphold most of the safety requirements for `get_unchecked_mut`. i.e. the id having been inserted into the Mapping before. + /// The caller must uphold most of the safety requirements for + /// `get_unchecked_mut`. i.e. the id having been inserted into the Mapping + /// before. pub unsafe fn get_unchecked_mut(&mut self, id: TId) -> &mut TValue { let (chunk, offset) = Self::chunk_and_offset(id.to_usize()); self.chunks @@ -128,6 +136,11 @@ impl Mapping { self.len == 0 } + /// Returns the maximum id that has been inserted + pub(crate) fn max(&self) -> usize { + self.max + } + /// Defines the number of slots that can be used /// theses slots are not initialized pub fn slots(&self) -> usize { @@ -177,6 +190,35 @@ impl<'a, TId: ArenaId, TValue> Iterator for MappingIter<'a, TId, TValue> { impl<'a, TId: ArenaId, TValue> FusedIterator for MappingIter<'a, TId, TValue> {} +#[cfg(feature = "serde")] +impl serde::Serialize for Mapping { + fn serialize(&self, serializer: S) -> Result { + self.chunks + .iter() + .flatten() + .take(self.max()) + .collect::>() + .serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de, K: ArenaId, V: serde::Deserialize<'de>> serde::Deserialize<'de> for Mapping { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let values = Vec::>::deserialize(deserializer)?; + let mut mapping = Mapping::with_capacity(values.len()); + for (i, value) in values.into_iter().enumerate() { + if let Some(value) = value { + mapping.insert(K::from_usize(i), value); + } + } + Ok(mapping) + } +} + #[cfg(test)] mod tests { use crate::internal::arena::ArenaId; diff --git a/src/lib.rs b/src/lib.rs index c8f9d70..40bac7d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ pub(crate) mod internal; pub mod problem; pub mod runtime; +pub mod snapshot; mod solver; pub mod utils; @@ -171,6 +172,8 @@ pub struct Candidates { /// Holds information about the dependencies of a package. #[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(untagged))] pub enum Dependencies { /// The dependencies are known. Known(KnownDependencies), @@ -184,9 +187,14 @@ pub enum Dependencies { /// Holds information about the dependencies of a package when they are known. #[derive(Default, Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct KnownDependencies { /// Defines which packages should be installed alongside the depending /// package and the constraints applied to the package. + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub requirements: Vec, /// Defines additional constraints on packages that may or may not be part @@ -196,5 +204,9 @@ pub struct KnownDependencies { /// package also added to the solution. /// /// This is often useful to use for optional dependencies. + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] pub constrains: Vec, } diff --git a/src/snapshot.rs b/src/snapshot.rs new file mode 100644 index 0000000..279759d --- /dev/null +++ b/src/snapshot.rs @@ -0,0 +1,448 @@ +//! Provides [`DependencySnapshot`], an object that can capture a snapshot of a +//! dependency provider. This can be very useful to abstract over all the +//! ecosystem specific code and provide a serializable object that can later be +//! reused to solve dependencies. +//! +//! The [`DependencySnapshot`] can be serialized to disk if the `serde` feature +//! is enabled. +//! +//! The [`DependencySnapshot`] implements the [`DependencyProvider`] trait, +//! allowing it to be used as a dependency provider for the solver. + +use std::{any::Any, collections::VecDeque, fmt::Display, time::SystemTime}; + +use ahash::HashSet; +use futures::FutureExt; + +use crate::{ + internal::arena::ArenaId, Candidates, Dependencies, DependencyProvider, Interner, Mapping, + NameId, SolvableId, SolverCache, StringId, VersionSetId, +}; + +/// A single solvable in a [`DependencySnapshot`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Solvable { + /// The string representation of this version set. + pub display: String, + + /// The package name of this solvable. + pub name: NameId, + + /// The order of this solvable compared to other solvables with the same + /// `name`. + pub order: u32, + + /// The dependencies of the solvable + pub dependencies: Dependencies, + + /// Whether the dependencies of this solvable are available right + /// away or if they need to be fetched. + pub hint_dependencies_available: bool, +} + +/// Information about a single version set in a [`DependencySnapshot`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct VersionSet { + /// The package name that this version set references. + pub name: NameId, + + /// The string representation of this version set. + pub display: String, + + /// The candidates that match this version set. + pub matching_candidates: HashSet, +} + +/// A single package in a [`DependencySnapshot`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Package { + /// The name of this package + pub name: String, + + /// All the solvables for this package. + pub solvables: Vec, + + /// Excluded packages + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Vec::is_empty") + )] + pub excluded: Vec<(SolvableId, StringId)>, +} + +/// A snapshot of an object that implements [`DependencyProvider`]. +#[derive(Clone, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct DependencySnapshot { + /// All the solvables in the snapshot + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Mapping::is_empty") + )] + pub solvables: Mapping, + + /// All the requirements in the snapshot + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Mapping::is_empty") + )] + pub requirements: Mapping, + + /// All the packages in the snapshot + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Mapping::is_empty") + )] + pub packages: Mapping, + + /// All the strings in the snapshot + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Mapping::is_empty") + )] + pub strings: Mapping, +} + +impl DependencySnapshot { + /// Construct a new [`DependencySnapshot`] from a [`DependencyProvider`] + /// capturing its entire state. This function will recursively call all + /// methods on the provider with the given `names`, `version_sets`, and + /// `solvables`. + /// + /// This function assumes that the passed in [`DependencyProvider`] does not + /// yield and will block until the snapshot is fully constructed. If you + /// want to construct a snapshot from a provider that might yield, use + /// [`Self::from_provider_async`] instead. + pub fn from_provider( + provider: impl DependencyProvider, + names: impl IntoIterator, + version_sets: impl IntoIterator, + solvables: impl IntoIterator, + ) -> Result> { + Self::from_provider_async(provider, names, version_sets, solvables) + .now_or_never() + .expect( + "the DependencyProvider seems to have yielded. Use `from_provider_async` instead.", + ) + } + + /// Construct a new [`DependencySnapshot`] from a [`DependencyProvider`] + /// capturing its entire state. This function will recursively call all + /// methods on the provider with the given `names`, `version_sets`, and + /// `solvables`. + pub async fn from_provider_async( + provider: impl DependencyProvider, + names: impl IntoIterator, + version_sets: impl IntoIterator, + solvables: impl IntoIterator, + ) -> Result> { + #[derive(Hash, Copy, Clone, Debug, Eq, PartialEq)] + pub enum Element { + Solvable(SolvableId), + VersionSet(VersionSetId), + Package(NameId), + String(StringId), + } + + let cache = SolverCache::new(provider); + + let mut result = Self { + solvables: Mapping::new(), + requirements: Mapping::new(), + packages: Mapping::new(), + strings: Mapping::new(), + }; + + let mut queue = names + .into_iter() + .map(Element::Package) + .chain(version_sets.into_iter().map(Element::VersionSet)) + .chain(solvables.into_iter().map(Element::Solvable)) + .collect::>(); + let mut seen = queue.iter().copied().collect::>(); + let mut available_hints = HashSet::default(); + while let Some(element) = queue.pop_front() { + match element { + Element::Package(name) => { + let display = cache.provider().display_name(name).to_string(); + let candidates = cache.get_or_cache_candidates(name).await?; + for solvable in candidates.candidates.iter() { + if seen.insert(Element::Solvable(*solvable)) { + queue.push_back(Element::Solvable(*solvable)); + } + } + for &(excluded, reason) in &candidates.excluded { + if seen.insert(Element::Solvable(excluded)) { + queue.push_back(Element::Solvable(excluded)); + } + if seen.insert(Element::String(reason)) { + queue.push_back(Element::String(reason)); + } + } + available_hints.extend(candidates.hint_dependencies_available.iter().copied()); + + let package = Package { + name: display, + solvables: candidates.candidates.clone(), + excluded: candidates.excluded.clone(), + }; + + result.packages.insert(name, package); + } + Element::Solvable(solvable_id) => { + let name = cache.provider().solvable_name(solvable_id); + if seen.insert(Element::Package(name)) { + queue.push_back(Element::Package(name)); + }; + + let dependencies = cache.get_or_cache_dependencies(solvable_id).await?; + match &dependencies { + Dependencies::Unknown(reason) => { + if seen.insert(Element::String(*reason)) { + queue.push_back(Element::String(*reason)); + } + } + Dependencies::Known(deps) => { + for &dep in deps.requirements.iter().chain(deps.constrains.iter()) { + if seen.insert(Element::VersionSet(dep)) { + queue.push_back(Element::VersionSet(dep)); + } + } + } + } + + let solvable = Solvable { + display: cache.provider().display_solvable(solvable_id).to_string(), + name, + order: 0, + dependencies: dependencies.clone(), + hint_dependencies_available: cache + .are_dependencies_available_for(solvable_id), + }; + + result.solvables.insert(solvable_id, solvable); + } + Element::String(string_id) => { + let string = cache.provider().display_string(string_id).to_string(); + result.strings.insert(string_id, string); + } + Element::VersionSet(version_set_id) => { + let name = cache.provider().version_set_name(version_set_id); + if seen.insert(Element::Package(name)) { + queue.push_back(Element::Package(name)); + }; + + let display = cache + .provider() + .display_version_set(version_set_id) + .to_string(); + let matching_candidates = cache + .get_or_cache_matching_candidates(version_set_id) + .await?; + + for matching_candidate in matching_candidates.iter() { + if seen.insert(Element::Solvable(*matching_candidate)) { + queue.push_back(Element::Solvable(*matching_candidate)); + } + } + + let version_set = VersionSet { + name, + display, + matching_candidates: matching_candidates.iter().copied().collect(), + }; + + result.requirements.insert(version_set_id, version_set); + } + } + } + + // Compute the order of the solvables + for (_, package) in result.packages.iter() { + let mut solvables = package.solvables.clone(); + cache + .provider() + .sort_candidates(&cache, &mut solvables) + .await; + + for (order, solvable) in solvables.into_iter().enumerate() { + let solvable = result + .solvables + .get_mut(solvable) + .expect("missing solvable"); + solvable.order = order as u32; + } + } + + Ok(result) + } + + /// Returns an object that implements the [`DependencyProvider`] trait for + /// this snapshot. + pub fn provider(&self) -> SnapshotProvider<'_> { + SnapshotProvider::new(self) + } +} + +/// Provides a [`DependencyProvider`] implementation for a +/// [`DependencySnapshot`]. +pub struct SnapshotProvider<'s> { + snapshot: &'s DependencySnapshot, + + additional_version_sets: Vec, + stop_time: Option, +} + +impl<'s> From<&'s DependencySnapshot> for SnapshotProvider<'s> { + fn from(value: &'s DependencySnapshot) -> Self { + Self::new(value) + } +} + +impl<'s> SnapshotProvider<'s> { + /// Create a new [`SnapshotProvider`] from a [`DependencySnapshot`]. + pub fn new(snapshot: &'s DependencySnapshot) -> Self { + Self { + snapshot, + additional_version_sets: Vec::new(), + stop_time: None, + } + } + + /// Adds a timeout to this provider. Solving will stop when the specified + /// time is reached. + pub fn with_timeout(self, stop_time: SystemTime) -> Self { + Self { + stop_time: Some(stop_time), + ..self + } + } + + /// Adds another requirement that matches any version of a package + pub fn add_package_requirement(&mut self, name: NameId) -> VersionSetId { + let id = self.snapshot.requirements.max() + self.additional_version_sets.len(); + + let package = self.package(name); + + let version_set = VersionSet { + name, + display: "*".to_string(), + matching_candidates: package.solvables.iter().copied().collect(), + }; + + self.additional_version_sets.push(version_set); + VersionSetId::from_usize(id) + } + + fn solvable(&self, solvable: SolvableId) -> &Solvable { + self.snapshot + .solvables + .get(solvable) + .expect("missing solvable") + } + + fn package(&self, name_id: NameId) -> &Package { + self.snapshot + .packages + .get(name_id) + .expect("missing package") + } + + fn string(&self, string_id: StringId) -> &String { + self.snapshot + .strings + .get(string_id) + .expect("missing string") + } + + fn version_set(&self, version_set: VersionSetId) -> &VersionSet { + let idx = version_set.to_usize(); + let max_idx = self.snapshot.requirements.max(); + if idx >= max_idx { + &self.additional_version_sets[idx - max_idx] + } else { + self.snapshot + .requirements + .get(version_set) + .expect("missing version set") + } + } +} + +impl<'s> Interner for SnapshotProvider<'s> { + fn display_solvable(&self, solvable: SolvableId) -> impl Display + '_ { + &self.solvable(solvable).display + } + + fn display_name(&self, name: NameId) -> impl Display + '_ { + &self.package(name).name + } + + fn display_version_set(&self, version_set: VersionSetId) -> impl Display + '_ { + &self.version_set(version_set).display + } + + fn display_string(&self, string_id: StringId) -> impl Display + '_ { + self.string(string_id) + } + + fn version_set_name(&self, version_set: VersionSetId) -> NameId { + self.version_set(version_set).name + } + + fn solvable_name(&self, solvable: SolvableId) -> NameId { + self.solvable(solvable).name + } +} + +impl<'s> DependencyProvider for SnapshotProvider<'s> { + async fn filter_candidates( + &self, + candidates: &[SolvableId], + version_set: VersionSetId, + inverse: bool, + ) -> Vec { + let version_set = self.version_set(version_set); + candidates + .iter() + .copied() + .filter(|c| version_set.matching_candidates.contains(c) != inverse) + .collect() + } + + async fn get_candidates(&self, name: NameId) -> Option { + let package = self.package(name); + Some(Candidates { + candidates: package.solvables.clone(), + favored: None, + locked: None, + excluded: package.excluded.clone(), + hint_dependencies_available: package + .solvables + .iter() + .copied() + .filter(|&s| self.solvable(s).hint_dependencies_available) + .collect(), + }) + } + + async fn sort_candidates(&self, _solver: &SolverCache, solvables: &mut [SolvableId]) { + solvables.sort_by_key(|&s| self.solvable(s).order); + } + + async fn get_dependencies(&self, solvable: SolvableId) -> Dependencies { + self.solvable(solvable).dependencies.clone() + } + + fn should_cancel_with_value(&self) -> Option> { + if let Some(stop_time) = &self.stop_time { + if SystemTime::now() > *stop_time { + return Some(Box::new(())); + } + } + None + } +} diff --git a/tests/snapshots/solver__snapshot.snap b/tests/snapshots/solver__snapshot.snap new file mode 100644 index 0000000..224e308 --- /dev/null +++ b/tests/snapshots/solver__snapshot.snap @@ -0,0 +1,8 @@ +--- +source: tests/solver.rs +assertion_line: 1121 +expression: "solve_for_snapshot(snapshot_provider, &[menu_req])" +--- +dropdown=2 +icons=2 +menu=15 diff --git a/tests/solver.rs b/tests/solver.rs index 37a2514..0064255 100644 --- a/tests/solver.rs +++ b/tests/solver.rs @@ -16,8 +16,10 @@ use std::{ use ahash::HashMap; use indexmap::IndexMap; +use insta::assert_snapshot; use itertools::Itertools; use resolvo::{ + snapshot::{DependencySnapshot, SnapshotProvider}, utils::{Pool, Range}, Candidates, Dependencies, DependencyProvider, Interner, KnownDependencies, NameId, SolvableId, Solver, SolverCache, StringId, UnsolvableOrCancelled, VersionSetId, @@ -152,7 +154,7 @@ impl FromStr for Spec { /// This provides sorting functionality for our `BundleBox` packaging system #[derive(Default)] struct BundleBoxProvider { - pool: Rc>>, + pool: Pool>, packages: IndexMap>, favored: HashMap, locked: HashMap, @@ -169,6 +171,7 @@ struct BundleBoxProvider { requested_dependencies: RefCell>, } +#[derive(Debug, Clone)] struct BundleBoxPackageDependencies { dependencies: Vec, constrains: Vec, @@ -179,6 +182,12 @@ impl BundleBoxProvider { Default::default() } + pub fn package_name(&self, name: &str) -> NameId { + self.pool + .lookup_package_name(&name.to_string()) + .expect("package missing") + } + pub fn requirements(&self, requirements: &[&str]) -> Vec { requirements .iter() @@ -223,6 +232,8 @@ impl BundleBoxProvider { dependencies: &[&str], constrains: &[&str], ) { + self.pool.intern_package_name(package_name); + let dependencies = dependencies .iter() .map(|dep| Spec::from_str(dep)) @@ -259,6 +270,15 @@ impl BundleBoxProvider { value } } + + pub fn into_snapshot(self) -> DependencySnapshot { + let name_ids = self + .packages + .keys() + .filter_map(|name| self.pool.lookup_package_name(name)) + .collect::>(); + DependencySnapshot::from_provider(self, name_ids, [], []).unwrap() + } } impl Interner for BundleBoxProvider { @@ -1073,3 +1093,57 @@ fn test_constraints() { b=1 "###); } + +#[test] +fn test_snapshot() { + let provider = BundleBoxProvider::from_packages(&[ + ("menu", 15, vec!["dropdown 2..3"]), + ("menu", 10, vec!["dropdown 1..2"]), + ("dropdown", 2, vec!["icons 2"]), + ("dropdown", 1, vec!["intl 3"]), + ("icons", 2, vec![]), + ("icons", 1, vec![]), + ("intl", 5, vec![]), + ("intl", 3, vec![]), + ]); + + let menu_name_id = provider.package_name("menu"); + + let snapshot = provider.into_snapshot(); + + #[cfg(feature = "serde")] + serialize_snapshot(&snapshot, "snapshot_pubgrub_menu.json"); + + let mut snapshot_provider = snapshot.provider(); + + let menu_req = snapshot_provider.add_package_requirement(menu_name_id); + + assert_snapshot!(solve_for_snapshot(snapshot_provider, &[menu_req])); +} + +#[cfg(feature = "serde")] +fn serialize_snapshot(snapshot: &DependencySnapshot, destination: impl AsRef) { + let file = std::io::BufWriter::new(std::fs::File::create(destination.as_ref()).unwrap()); + serde_json::to_writer_pretty(file, snapshot).unwrap() +} + +fn solve_for_snapshot(provider: SnapshotProvider, root_reqs: &[VersionSetId]) -> String { + let mut solver = Solver::new(provider); + match solver.solve(root_reqs.to_vec(), Vec::new()) { + Ok(solvables) => transaction_to_string(solver.provider(), &solvables), + Err(UnsolvableOrCancelled::Unsolvable(problem)) => { + // Write the problem graphviz to stderr + let graph = problem.graph(&solver); + let mut output = stderr(); + writeln!(output, "UNSOLVABLE:").unwrap(); + graph + .graphviz(&mut output, solver.provider(), true) + .unwrap(); + writeln!(output, "\n").unwrap(); + + // Format a user friendly error message + problem.display_user_friendly(&solver).to_string() + } + Err(UnsolvableOrCancelled::Cancelled(reason)) => *reason.downcast().unwrap(), + } +} diff --git a/tools/solve-snapshot/Cargo.toml b/tools/solve-snapshot/Cargo.toml new file mode 100644 index 0000000..e63fa25 --- /dev/null +++ b/tools/solve-snapshot/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "solve-snapshot" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +resolvo = { path = "../../../resolvo", features = ["serde"] } +clap = { version = "4.5", features = ["derive"] } +csv = "1.3" +serde_json = "1.0" +serde = { version = "1.0.196", features = ["derive"] } \ No newline at end of file diff --git a/tools/solve-snapshot/src/main.rs b/tools/solve-snapshot/src/main.rs new file mode 100644 index 0000000..0ae50d2 --- /dev/null +++ b/tools/solve-snapshot/src/main.rs @@ -0,0 +1,84 @@ +use std::{ + fs::File, + io::BufReader, + ops::Add, + time::{Duration, Instant, SystemTime}, +}; + +use clap::Parser; +use csv::WriterBuilder; +use resolvo::{snapshot::DependencySnapshot, Solver, UnsolvableOrCancelled}; + +#[derive(Parser)] +#[clap(version = "0.1.0", author = "Bas Zalmstra ")] +struct Opts { + snapshot: String, +} + +#[derive(Debug, serde::Serialize)] +struct Record { + package: String, + duration: f64, + error: Option, + records: Option, +} + +fn main() { + let opts: Opts = Opts::parse(); + + eprintln!("Loading snapshot ..."); + let snapshot_file = BufReader::new(File::open(&opts.snapshot).unwrap()); + let snapshot: DependencySnapshot = serde_json::from_reader(snapshot_file).unwrap(); + + let mut writer = WriterBuilder::new() + .has_headers(true) + .from_path("timings.csv") + .unwrap(); + + for (i, (package_name_id, package)) in snapshot.packages.iter().enumerate() { + eprint!( + "solving {} ({i}/{}) ... ", + &package.name, + snapshot.packages.len() + ); + let start = Instant::now(); + + let mut provider = snapshot + .provider() + .with_timeout(SystemTime::now().add(Duration::from_secs(60))); + let package_requirement = provider.add_package_requirement(package_name_id); + let mut solver = Solver::new(provider); + let mut records = None; + let mut error = None; + match solver.solve(vec![package_requirement], vec![]) { + Ok(solution) => { + eprintln!("OK"); + records = Some(solution.len()) + } + Err(UnsolvableOrCancelled::Unsolvable(problem)) => { + eprintln!("FAIL"); + error = Some(problem.display_user_friendly(&solver).to_string()); + } + Err(_) => { + eprintln!("CANCELLED"); + } + } + + let duration = start.elapsed(); + + writer + .serialize(Record { + package: package.name.clone(), + duration: duration.as_secs_f64(), + error, + records, + }) + .unwrap(); + + if i % 100 == 0 { + writer.flush().unwrap(); + } + } + + writer.flush().unwrap(); +} From 62cfe41c7a34c3f0a8fecfaaf96f8859e645b7af Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Mon, 10 Jun 2024 16:57:23 +0200 Subject: [PATCH 2/3] fix: publish state of tool --- tools/solve-snapshot/Cargo.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tools/solve-snapshot/Cargo.toml b/tools/solve-snapshot/Cargo.toml index e63fa25..c8d9d10 100644 --- a/tools/solve-snapshot/Cargo.toml +++ b/tools/solve-snapshot/Cargo.toml @@ -2,12 +2,11 @@ name = "solve-snapshot" version = "0.1.0" edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +publish = false [dependencies] resolvo = { path = "../../../resolvo", features = ["serde"] } clap = { version = "4.5", features = ["derive"] } csv = "1.3" serde_json = "1.0" -serde = { version = "1.0.196", features = ["derive"] } \ No newline at end of file +serde = { version = "1.0.196", features = ["derive"] } From c41d0dfab9fe4794094ebe82d90491eba0c200e1 Mon Sep 17 00:00:00 2001 From: Bas Zalmstra Date: Mon, 10 Jun 2024 17:00:45 +0200 Subject: [PATCH 3/3] chore: release v0.6.1 (#45) --- CHANGELOG.md | 8 ++++++++ Cargo.toml | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbef3eb..26b7466 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.6.1](https://github.com/mamba-org/resolvo/compare/resolvo-v0.6.0...resolvo-v0.6.1) - 2024-06-10 + +### Added +- add `DependencySnapshot` ([#44](https://github.com/mamba-org/resolvo/pull/44)) + +### Fixed +- publish state of tool + ## [0.6.0](https://github.com/mamba-org/resolvo/compare/v0.5.0...v0.6.0) - 2024-06-07 ### Other diff --git a/Cargo.toml b/Cargo.toml index 5ce1b70..09e4710 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["tools/solve-snapshot"] [package] name = "resolvo" -version = "0.6.0" +version = "0.6.1" authors = ["Adolfo OchagavĂ­a ", "Bas Zalmstra ", "Tim de Jager "] description = "Fast package resolver written in Rust (CDCL based SAT solving)" keywords = ["dependency", "solver", "version"] @@ -36,4 +36,4 @@ proptest = "1.2" tracing-test = { version = "0.2.4", features = ["no-env-filter"] } tokio = { version = "1.35.1", features = ["time", "rt"] } resolvo = { path = ".", features = ["tokio"] } -serde_json = "1.0" \ No newline at end of file +serde_json = "1.0"