diff --git a/dag/src/lib.rs b/dag/src/lib.rs index fd43480e..2ac5fde2 100644 --- a/dag/src/lib.rs +++ b/dag/src/lib.rs @@ -2,12 +2,102 @@ // // SPDX-License-Identifier: MPL-2.0 -pub mod subgraph; +use petgraph::{ + prelude::DiGraph, + visit::{Dfs, Topo, Walker}, +}; -pub use self::reexport::*; +use self::subgraph::subgraph; -pub mod reexport { - pub use petgraph::algo::{toposort, Cycle}; - pub use petgraph::graph::DiGraph; - pub use petgraph::visit::Dfs; +mod subgraph; + +pub type NodeIndex = petgraph::prelude::NodeIndex; + +#[derive(Debug, Clone)] +pub struct Dag(DiGraph); + +impl Default for Dag { + fn default() -> Self { + Self(DiGraph::default()) + } +} + +impl Dag +where + N: Clone + PartialEq, +{ + pub fn new() -> Self { + Self::default() + } + + /// Adds node N to the graph and retusn it's index. + /// If N already exists, it'll return the index of that node. + pub fn add_node_or_get_index(&mut self, node: N) -> NodeIndex { + if let Some(index) = self.get_index(&node) { + index + } else { + self.0.add_node(node) + } + } + + pub fn node_exists(&self, node: &N) -> bool { + self.get_index(node).is_some() + } + + pub fn remove_node(&mut self, node: &N) -> Option { + if let Some(index) = self.get_index(node) { + self.0.remove_node(index) + } else { + None + } + } + + pub fn add_edge(&mut self, a: NodeIndex, b: NodeIndex) -> bool { + let a_node = &self.0[a]; + + // prevent cycle (b connects to a) + if self.dfs(b).any(|n| n == a_node) { + return false; + } + + // don't add edge if it alread exists + if self.0.find_edge(a, b).is_some() { + return false; + } + + // We're good, add it + self.0.add_edge(a, b, ()); + + true + } + + pub fn iter_nodes(&self) -> impl Iterator { + self.0.node_indices().map(|i| &self.0[i]) + } + + pub fn dfs(&self, start: NodeIndex) -> impl Iterator { + let dfs = Dfs::new(&self.0, start); + + dfs.iter(&self.0).map(|i| &self.0[i]) + } + + pub fn topo(&self) -> impl Iterator { + let topo = Topo::new(&self.0); + + topo.iter(&self.0).map(|i| &self.0[i]) + } + + pub fn transpose(&self) -> Self { + let mut transposed = self.0.clone(); + transposed.reverse(); + Self(transposed) + } + + pub fn subgraph(&self, starting_nodes: &[N]) -> Self { + Self(subgraph(&self.0, starting_nodes)) + } + + pub fn get_index(&self, node: &N) -> Option { + self.0.node_indices().find(|i| self.0[*i] == *node) + } } diff --git a/dag/src/subgraph.rs b/dag/src/subgraph.rs index 52cc1d69..e8cda648 100644 --- a/dag/src/subgraph.rs +++ b/dag/src/subgraph.rs @@ -2,27 +2,47 @@ // // SPDX-License-Identifier: MPL-2.0 -use petgraph::{prelude::GraphMap, visit::Dfs, EdgeType}; +use petgraph::{prelude::Graph, stable_graph::IndexType, visit::Dfs, EdgeType}; -/// Given an input [GraphMap] and the start nodes, construct a subgraph +/// Given an input [`Graph`] and the start nodes, construct a subgraph /// Used largely in transposed form for reverse dependency calculation -pub fn subgraph(graph: &GraphMap, starting_nodes: Vec) -> GraphMap +pub fn subgraph( + graph: &Graph, + starting_nodes: &[N], +) -> Graph where - V: Eq + std::hash::Hash + Ord + Copy, - E: Default, + N: PartialEq + Clone, + E: Clone, + Ix: IndexType, Ty: EdgeType, { - let mut res = GraphMap::default(); + let add_node = |graph: &mut Graph, node| { + if let Some(index) = graph.node_indices().find(|i| graph[*i] == node) { + index + } else { + graph.add_node(node) + } + }; + let mut res = Graph::default(); let mut dfs = Dfs::empty(&graph); + for starting_node in starting_nodes { - dfs.move_to(starting_node); + let Some(starting_node_index) = graph.node_indices().find(|n| graph[*n] == *starting_node) + else { + continue; + }; + + dfs.move_to(starting_node_index); + while let Some(node) = dfs.next(&graph) { - res.extend( - graph - .neighbors_directed(node, petgraph::Direction::Outgoing) - .map(|adj| (node, adj)), - ); + let node_index = add_node(&mut res, graph[node].clone()); + for neighbor in graph.neighbors_directed(node, petgraph::Direction::Outgoing) { + if let Some(edge) = graph.find_edge(node, neighbor) { + let neighbor_index = add_node(&mut res, graph[neighbor].clone()); + res.add_edge(node_index, neighbor_index, graph[edge].clone()); + } + } } } @@ -32,7 +52,8 @@ where #[cfg(test)] mod test { use petgraph::{ - prelude::DiGraphMap, + data::{Element, FromElements}, + prelude::DiGraph, visit::{Reversed, Topo, Walker}, }; @@ -40,34 +61,117 @@ mod test { #[test] fn basic_topo() { - let graph: DiGraphMap = DiGraphMap::from_edges(&[(1, 2), (1, 3), (2, 3)]); - let subg = subgraph(&graph, vec![1]); + let graph: DiGraph = DiGraph::from_elements([ + Element::Node { weight: 1 }, + Element::Node { weight: 2 }, + Element::Node { weight: 3 }, + Element::Node { weight: 4 }, + Element::Edge { + source: 0, + target: 1, + weight: (), + }, + Element::Edge { + source: 0, + target: 2, + weight: (), + }, + Element::Edge { + source: 1, + target: 2, + weight: (), + }, + Element::Edge { + source: 2, + target: 3, + weight: (), + }, + ]); + let subg = subgraph(&graph, &[2]); let topo = Topo::new(&subg); - let order: Vec = topo.iter(&subg).collect(); + let order: Vec = topo.iter(&subg).map(|n| subg[n]).collect(); - assert_eq!(order, vec![1, 2, 3]); + assert_eq!(order, vec![2, 3, 4]); } #[test] fn reverse_topo() { - let graph: DiGraphMap = DiGraphMap::from_edges(&[(1, 2), (1, 3), (2, 3)]); - let items = vec![1]; - let subg = subgraph(&graph, items); + let graph: DiGraph = DiGraph::from_elements([ + Element::Node { weight: 1 }, + Element::Node { weight: 2 }, + Element::Node { weight: 3 }, + Element::Node { weight: 4 }, + Element::Edge { + source: 0, + target: 1, + weight: (), + }, + Element::Edge { + source: 0, + target: 2, + weight: (), + }, + Element::Edge { + source: 1, + target: 2, + weight: (), + }, + Element::Edge { + source: 2, + target: 3, + weight: (), + }, + ]); + let subg = subgraph(&graph, &[2]); let revg = Reversed(&subg); - let removal: Vec = Topo::new(revg).iter(revg).collect(); - assert_eq!(removal, vec![3, 2, 1]); + let removal: Vec = Topo::new(revg).iter(revg).map(|n| subg[n]).collect(); + assert_eq!(removal, vec![4, 3, 2]); } // TODO: break cycles! #[ignore = "cycles breaking needs to be implemented"] #[test] fn cyclic_topo() { - let graph: DiGraphMap = - DiGraphMap::from_edges(&[(1, 2), (1, 3), (2, 4), (2, 5), (3, 5), (4, 1)]); - let items = vec![1, 4]; - let subg = subgraph(&graph, items); + let graph: DiGraph = DiGraph::from_elements([ + Element::Node { weight: 1 }, + Element::Node { weight: 2 }, + Element::Node { weight: 3 }, + Element::Node { weight: 4 }, + Element::Node { weight: 5 }, + Element::Edge { + source: 0, + target: 1, + weight: (), + }, + Element::Edge { + source: 0, + target: 2, + weight: (), + }, + Element::Edge { + source: 1, + target: 3, + weight: (), + }, + Element::Edge { + source: 1, + target: 4, + weight: (), + }, + Element::Edge { + source: 2, + target: 4, + weight: (), + }, + Element::Edge { + source: 3, + target: 0, + weight: (), + }, + ]); + let subg = subgraph(&graph, &[1, 4]); let revg = Reversed(&subg); - let removal: Vec = Topo::new(revg).iter(revg).collect(); + let removal: Vec = Topo::new(revg).iter(revg).map(|n| subg[n]).collect(); assert_eq!(removal, vec![5, 3, 4, 2, 1]); } } diff --git a/moss/src/cli/install.rs b/moss/src/cli/install.rs index e70c4b51..1666757b 100644 --- a/moss/src/cli/install.rs +++ b/moss/src/cli/install.rs @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MPL-2.0 -use std::{path::PathBuf, time::Duration}; +use std::{path::Path, time::Duration}; use clap::{arg, ArgMatches, Command}; use futures::{future::join_all, stream, StreamExt, TryStreamExt}; @@ -45,9 +45,7 @@ async fn find_packages(id: &str, client: &Client) -> Result, Error> } /// Handle execution of `moss install` -pub async fn handle(args: &ArgMatches) -> Result<(), Error> { - let root = args.get_one::("root").unwrap().clone(); - +pub async fn handle(args: &ArgMatches, root: &Path) -> Result<(), Error> { let pkgs = args .get_many::("NAME") .into_iter() @@ -82,7 +80,6 @@ pub async fn handle(args: &ArgMatches) -> Result<(), Error> { // Resolve and map it. Remove any installed items. OK to unwrap here because they're resolved already let results = join_all( tx.finalize() - .iter() .map(|p| async { client.registry.by_id(p).boxed().next().await.unwrap() }), ) .await; diff --git a/moss/src/cli/mod.rs b/moss/src/cli/mod.rs index c03fb22b..a0a2d22f 100644 --- a/moss/src/cli/mod.rs +++ b/moss/src/cli/mod.rs @@ -82,13 +82,14 @@ pub async fn process() -> Result<(), Error> { Some(("extract", args)) => extract::handle(args).await.map_err(Error::Extract), Some(("info", args)) => info::handle(args).await.map_err(Error::Info), Some(("inspect", args)) => inspect::handle(args).await.map_err(Error::Inspect), - Some(("install", args)) => install::handle(args).await.map_err(Error::Install), + Some(("install", args)) => install::handle(args, root).await.map_err(Error::Install), Some(("version", _)) => { version::print(); Ok(()) } - Some(("list", a)) => list::handle(a).await.map_err(Error::List), - Some(("repo", a)) => repo::handle(a, root).await.map_err(Error::Repo), + Some(("list", args)) => list::handle(args).await.map_err(Error::List), + Some(("remove", args)) => remove::handle(args, root).await.map_err(Error::Remove), + Some(("repo", args)) => repo::handle(args, root).await.map_err(Error::Repo), _ => unreachable!(), } } @@ -110,6 +111,9 @@ pub enum Error { #[error("error in extraction: {0}")] Extract(#[from] extract::Error), + #[error("error handling remove: {0}")] + Remove(#[from] remove::Error), + #[error("error handling repo: {0}")] Repo(#[from] repo::Error), } diff --git a/moss/src/cli/remove.rs b/moss/src/cli/remove.rs index 89d4972f..488c7287 100644 --- a/moss/src/cli/remove.rs +++ b/moss/src/cli/remove.rs @@ -2,10 +2,103 @@ // // SPDX-License-Identifier: MPL-2.0 -use clap::Command; +use std::{collections::HashSet, path::Path}; + +use clap::{arg, ArgMatches, Command}; +use futures::{future::join_all, StreamExt}; +use itertools::{Either, Itertools}; +use moss::{ + client::{self, Client}, + package::Flags, + registry::transaction, +}; +use thiserror::Error; +use tui::pretty::print_to_columns; + +use super::name_to_provider; pub fn command() -> Command { Command::new("remove") .about("Remove packages") .long_about("Remove packages by name") + .arg(arg!( ... "packages to install").value_parser(clap::value_parser!(String))) +} + +/// Handle execution of `moss remove` +pub async fn handle(args: &ArgMatches, root: &Path) -> Result<(), Error> { + let pkgs = args + .get_many::("NAME") + .into_iter() + .flatten() + .map(|name| name_to_provider(name)) + .collect::>(); + + // Grab a client for the target, enumerate packages + let client = Client::new_for_root(root).await?; + + let installed = client + .registry + .list_installed(Flags::NONE) + .collect::>() + .await; + let installed_ids = installed.iter().map(|p| &p.id).collect::>(); + + let (for_removal, not_installed): (Vec<_>, Vec<_>) = pkgs.iter().partition_map(|provider| { + installed + .iter() + .find(|i| i.meta.providers.contains(provider)) + .map(|i| Either::Left(i.id.clone())) + .unwrap_or(Either::Right(provider.clone())) + }); + + // TODO: Add error hookups + if !not_installed.is_empty() { + println!("Missing packages in lookup: {:?}", not_installed); + return Err(Error::NotImplemented); + } + + let mut transaction = client.registry.transaction()?; + + // Add all installed packages to transaction + transaction + .add(installed_ids.iter().cloned().cloned().collect()) + .await?; + + // Remove all pkgs for removal + transaction.remove(for_removal).await?; + + // Finalized tx has all reverse deps removed + let finalized = transaction.finalize().collect::>(); + + // Difference resolves to all removed pkgs + let removed = installed_ids.difference(&finalized); + + // Get metadata for all removed pkgs & dedupe + let mut results = join_all( + removed + .into_iter() + .map(|p| async { client.registry.by_id(p).boxed().next().await.unwrap() }), + ) + .await; + results.sort_by_key(|p| p.meta.name.to_string()); + results.dedup_by_key(|p| p.meta.name.to_string()); + + println!("The following package(s) will be removed:"); + println!(); + print_to_columns(&results); + println!(); + + Ok(()) +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("client error")] + Client(#[from] client::Error), + + #[error("not yet implemented")] + NotImplemented, + + #[error("transaction error: {0}")] + Transaction(#[from] transaction::Error), } diff --git a/moss/src/db/meta/mod.rs b/moss/src/db/meta/mod.rs index 5f5dd2a9..aee80710 100644 --- a/moss/src/db/meta/mod.rs +++ b/moss/src/db/meta/mod.rs @@ -10,7 +10,7 @@ use thiserror::Error; use crate::db::Encoding; use crate::package::{self, Meta}; -use crate::Provider; +use crate::{Dependency, Provider}; #[derive(Debug, Clone, Copy)] enum Table { @@ -23,6 +23,7 @@ enum Table { #[derive(Debug)] pub enum Filter { Provider(Provider), + Dependency(Dependency), Name(package::Name), } @@ -50,6 +51,27 @@ impl Filter { .push(")"); } } + Filter::Dependency(d) => { + if let Table::Dependencies = table { + query + .push( + " + where dependency = + ", + ) + .push_bind(d.encode()); + } else { + query + .push( + " + where package in + (select distinct package from meta_dependencies where dependency = + ", + ) + .push_bind(d.encode()) + .push(")"); + } + } Filter::Name(n) => { if let Table::Meta = table { query diff --git a/moss/src/package/render.rs b/moss/src/package/render.rs index 1127b269..1fb06e1d 100644 --- a/moss/src/package/render.rs +++ b/moss/src/package/render.rs @@ -15,6 +15,16 @@ use crate::Package; const COLUMN_PADDING: usize = 4; /// Allow display packages in column form +impl ColumnDisplay for Package { + fn get_display_width(&self) -> usize { + ColumnDisplay::get_display_width(&self) + } + + fn display_column(&self, writer: &mut impl Write, col: Column, width: usize) { + ColumnDisplay::display_column(&self, writer, col, width) + } +} + impl<'a> ColumnDisplay for &'a Package { fn get_display_width(&self) -> usize { self.meta.name.to_string().len() diff --git a/moss/src/registry/transaction.rs b/moss/src/registry/transaction.rs index 28cea3d9..dd0f2dc8 100644 --- a/moss/src/registry/transaction.rs +++ b/moss/src/registry/transaction.rs @@ -2,11 +2,8 @@ // // SPDX-License-Identifier: MPL-2.0 -use std::collections::HashSet; - -use dag::{toposort, Dfs, DiGraph}; +use dag::Dag; use futures::{StreamExt, TryFutureExt}; -use itertools::Itertools; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -36,7 +33,7 @@ pub struct Transaction<'a> { registry: &'a Registry, // unique set of package ids - packages: HashSet, + packages: Dag, } /// Construct a new Transaction wrapped around the underlying Registry @@ -46,31 +43,39 @@ pub(super) fn new(registry: &Registry) -> Result, Error> { Ok(Transaction { id: None, registry, - packages: HashSet::new(), + packages: Dag::default(), }) } impl<'a> Transaction<'a> { /// Add a package to this transaction pub async fn add(&mut self, incoming: Vec) -> Result<(), Error> { - self.packages.extend(self.compute_deps(incoming).await?); - Ok(()) + self.update(incoming).await } - /// Remove a set of packages and reverse dependencies - pub fn remove(&self, id: package::Id) -> Result<(), Error> { - Err(Error::NotImplemented) + /// Remove a set of packages and their reverse dependencies + pub async fn remove(&mut self, packages: Vec) -> Result<(), Error> { + // Get transposed subgraph + let transposed = self.packages.transpose(); + let subgraph = transposed.subgraph(&packages); + + // For each node, remove it from transaction graph + subgraph.iter_nodes().for_each(|package| { + // Remove that package + self.packages.remove_node(package); + }); + + Ok(()) } /// Return the package IDs in the fully baked configuration - pub fn finalize(&self) -> Vec { - self.packages.iter().cloned().collect_vec() + pub fn finalize(&self) -> impl Iterator + '_ { + self.packages.topo() } - /// Return all of the dependencies for input ID - async fn compute_deps(&self, incoming: Vec) -> Result, Error> { - let mut graph = DiGraph::new(); - let mut items = incoming.clone(); + /// Update internal package graph with all incoming packages & their deps + async fn update(&mut self, incoming: Vec) -> Result<(), Error> { + let mut items = incoming; loop { if items.is_empty() { @@ -78,11 +83,8 @@ impl<'a> Transaction<'a> { } let mut next = vec![]; for check_id in items.iter() { - // See if the node exists yet.. - let check_node = graph - .node_indices() - .find(|i| graph[*i] == *check_id) - .unwrap_or_else(|| graph.add_node(check_id.clone())); + // Ensure node is added and get it's index + let check_node = self.packages.add_node_or_get_index(check_id.clone()); // Grab this package in question let matches = self.registry.by_id(check_id).collect::>().await; @@ -98,45 +100,23 @@ impl<'a> Transaction<'a> { // Now get it resolved let search = self.resolve_installation_provider(provider).await?; - // Grab dependency node - let mut need_search = false; - let dep_node = graph - .node_indices() - .find(|i| graph[*i] == search) - .unwrap_or_else(|| { - need_search = true; - graph.add_node(search.clone()) - }); + // Add dependency node + let need_search = !self.packages.node_exists(&search); + let dep_node = self.packages.add_node_or_get_index(search.clone()); // No dag node for it previously if need_search { next.push(search.clone()); } - // Connect w/ edges if non cyclical - let mut dfs = Dfs::new(&graph, dep_node); - let mut add_edge = true; - while let Some(item) = dfs.next(&graph) { - if item == dep_node { - add_edge = false; - break; - } - } - if graph.find_edge_undirected(check_node, dep_node).is_none() && add_edge { - graph.add_edge(check_node, dep_node, 1); - } + // Connect w/ edges (rejects cyclical & duplicate edges) + self.packages.add_edge(check_node, dep_node); } } items = next; } - // topologically sort, returning a mapped cylical error if necessary - // TODO: Handle emission of the cyclical error better and the chain involved - Ok(toposort(&graph, None) - .map_err(|e| Error::Cyclical(graph[e.node_id()].clone()))? - .into_iter() - .map(|i| graph[i].clone()) - .collect_vec()) + Ok(()) } /// Attempt to resolve the filterered provider @@ -162,7 +142,7 @@ impl<'a> Transaction<'a> { .registry .by_provider(&provider, package::Flags::NONE) .filter_map(|f| async { - if self.packages.contains(&f.id) { + if self.packages.node_exists(&f.id) { Some(f) } else { None @@ -199,9 +179,6 @@ pub enum Error { #[error("database error: {0}")] Database(#[from] crate::db::meta::Error), - #[error("cyclical dependencies")] - Cyclical(package::Id), - #[error("no such name: {0}")] NoCandidate(String),