use crate::error::{Result, WorkspaceError};
use crate::workspace::WorkspaceInfo;
use petgraph::algo::{toposort, DfsSpace};
use petgraph::graph::{NodeIndex, UnGraph};
use petgraph::Graph;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct DependencyGraph {
graph: Graph<String, (), petgraph::Directed>,
node_map: HashMap<String, NodeIndex>,
index_map: HashMap<NodeIndex, String>,
}
#[derive(Debug, Clone)]
pub struct PublishOrder {
pub tiers: Vec<PublishTier>,
pub total_packages: usize,
}
#[derive(Debug, Clone)]
pub struct PublishTier {
pub packages: Vec<String>,
pub tier_number: usize,
}
impl DependencyGraph {
pub fn build(workspace: &WorkspaceInfo) -> Result<Self> {
let mut graph = Graph::new();
let mut node_map = HashMap::with_capacity(workspace.packages.len());
let mut index_map = HashMap::with_capacity(workspace.packages.len());
for (package_name, package_info) in &workspace.packages {
if let Some(toml::Value::Boolean(false)) = package_info.config.other.get("publish") {
continue;
}
let node_index = graph.add_node(package_name.clone());
node_map.insert(package_name.clone(), node_index);
index_map.insert(node_index, package_name.clone());
}
for (package_name, dependencies) in &workspace.internal_dependencies {
let dependent_index = match node_map.get(package_name) {
Some(idx) => idx,
None => continue, };
for dependency_name in dependencies {
let dependency_index = match node_map.get(dependency_name) {
Some(idx) => idx,
None => continue, };
graph.add_edge(*dependency_index, *dependent_index, ());
}
}
Ok(Self {
graph,
node_map,
index_map,
})
}
pub fn publish_order(&self) -> Result<PublishOrder> {
self.validate_no_cycles()?;
let sorted_indices = toposort(&self.graph, None)
.map_err(|cycle| {
let cycle_packages: Vec<String> = self.index_map.get(&cycle.node_id())
.map(|name| vec![name.clone()])
.unwrap_or_else(|| vec!["unknown".to_string()]);
WorkspaceError::CircularDependency {
packages: cycle_packages,
}
})?;
let ordered_packages: Vec<String> = sorted_indices
.into_iter()
.filter_map(|idx| self.index_map.get(&idx).cloned())
.collect();
let tiers = self.group_into_tiers(&ordered_packages);
let total_packages = ordered_packages.len();
Ok(PublishOrder {
tiers,
total_packages,
})
}
fn group_into_tiers(&self, ordered_packages: &[String]) -> Vec<PublishTier> {
let mut tiers = Vec::new();
let mut remaining_packages: std::collections::VecDeque<_> = ordered_packages.iter().collect();
let mut tier_number = 0;
while !remaining_packages.is_empty() {
let mut current_tier = Vec::new();
let mut indices_to_remove = Vec::new();
for (index, package_name) in remaining_packages.iter().enumerate() {
if self.can_publish_in_current_tier(package_name, &remaining_packages) {
current_tier.push((*package_name).clone());
indices_to_remove.push(index);
}
}
for &index in indices_to_remove.iter().rev() {
remaining_packages.remove(index);
}
if current_tier.is_empty() && !remaining_packages.is_empty() {
if let Some(package) = remaining_packages.pop_front() {
current_tier.push(package.clone());
}
}
if !current_tier.is_empty() {
tiers.push(PublishTier {
packages: current_tier,
tier_number,
});
tier_number += 1;
}
}
tiers
}
fn can_publish_in_current_tier(
&self,
package_name: &str,
remaining_packages: &std::collections::VecDeque<&String>,
) -> bool {
let package_index = match self.node_map.get(package_name) {
Some(idx) => *idx,
None => return false,
};
let neighbors = self.graph.neighbors_directed(package_index, petgraph::Direction::Incoming);
for dependency_index in neighbors {
if let Some(dependency_name) = self.index_map.get(&dependency_index) {
if remaining_packages.iter().any(|&name| name == dependency_name) {
return false; }
}
}
true
}
fn validate_no_cycles(&self) -> Result<()> {
let _undirected: UnGraph<String, ()> = self.graph.clone().into_edge_type();
let mut dfs_space = DfsSpace::new(&self.graph);
for node_index in self.graph.node_indices() {
if let Some(cycle_path) = self.find_cycle_from_node(node_index, &mut dfs_space) {
let cycle_packages: Vec<String> = cycle_path
.into_iter()
.filter_map(|idx| self.index_map.get(&idx).cloned())
.collect();
return Err(WorkspaceError::CircularDependency {
packages: cycle_packages,
}.into());
}
}
Ok(())
}
fn find_cycle_from_node(
&self,
start_node: NodeIndex,
_dfs_space: &mut DfsSpace<NodeIndex, fixedbitset::FixedBitSet>,
) -> Option<Vec<NodeIndex>> {
let mut visited = std::collections::HashSet::new();
let mut recursion_stack = std::collections::HashSet::new();
let mut path = Vec::new();
self.dfs_cycle_detection(start_node, &mut visited, &mut recursion_stack, &mut path)
}
fn dfs_cycle_detection(
&self,
node: NodeIndex,
visited: &mut std::collections::HashSet<NodeIndex>,
recursion_stack: &mut std::collections::HashSet<NodeIndex>,
path: &mut Vec<NodeIndex>,
) -> Option<Vec<NodeIndex>> {
visited.insert(node);
recursion_stack.insert(node);
path.push(node);
let neighbors: Vec<_> = self.graph.neighbors_directed(node, petgraph::Direction::Outgoing).collect();
for neighbor in neighbors {
if !visited.contains(&neighbor) {
if let Some(cycle) = self.dfs_cycle_detection(neighbor, visited, recursion_stack, path) {
return Some(cycle);
}
} else if recursion_stack.contains(&neighbor) {
let cycle_start = path.iter().position(|&n| n == neighbor)?;
return Some(path[cycle_start..].to_vec());
}
}
recursion_stack.remove(&node);
path.pop();
None
}
pub fn dependents(&self, package_name: &str) -> Vec<String> {
let package_index = match self.node_map.get(package_name) {
Some(idx) => *idx,
None => return Vec::new(),
};
self.graph
.neighbors_directed(package_index, petgraph::Direction::Outgoing)
.filter_map(|idx| self.index_map.get(&idx).cloned())
.collect()
}
pub fn dependencies(&self, package_name: &str) -> Vec<String> {
let package_index = match self.node_map.get(package_name) {
Some(idx) => *idx,
None => return Vec::new(),
};
self.graph
.neighbors_directed(package_index, petgraph::Direction::Incoming)
.filter_map(|idx| self.index_map.get(&idx).cloned())
.collect()
}
pub fn depends_on(&self, package_a: &str, package_b: &str) -> bool {
let start_index = match self.node_map.get(package_a) {
Some(idx) => *idx,
None => return false,
};
let target_index = match self.node_map.get(package_b) {
Some(idx) => *idx,
None => return false,
};
petgraph::algo::has_path_connecting(&self.graph, start_index, target_index, None)
}
pub fn dependency_depth(&self, package_name: &str) -> usize {
let package_index = match self.node_map.get(package_name) {
Some(idx) => *idx,
None => return 0,
};
self.calculate_depth_recursive(package_index, &mut std::collections::HashSet::new())
}
fn calculate_depth_recursive(
&self,
node: NodeIndex,
visited: &mut std::collections::HashSet<NodeIndex>,
) -> usize {
if visited.contains(&node) {
return 0; }
visited.insert(node);
let max_dependency_depth = self.graph
.neighbors_directed(node, petgraph::Direction::Incoming)
.map(|dep_idx| self.calculate_depth_recursive(dep_idx, visited))
.max()
.unwrap_or(0);
visited.remove(&node);
max_dependency_depth + 1
}
}
impl PublishOrder {
pub fn tier_for_package(&self, package_name: &str) -> Option<usize> {
self.tiers
.iter()
.find(|tier| tier.packages.contains(&package_name.to_string()))
.map(|tier| tier.tier_number)
}
pub fn packages_in_tier(&self, tier_number: usize) -> Vec<String> {
self.tiers
.get(tier_number)
.map(|tier| tier.packages.clone())
.unwrap_or_default()
}
pub fn tier_count(&self) -> usize {
self.tiers.len()
}
pub fn contains_package(&self, package_name: &str) -> bool {
self.tiers
.iter()
.any(|tier| tier.packages.contains(&package_name.to_string()))
}
pub fn ordered_packages(&self) -> impl Iterator<Item = &String> {
self.tiers.iter().flat_map(|tier| tier.packages.iter())
}
}
impl PublishTier {
pub fn contains(&self, package_name: &str) -> bool {
self.packages.contains(&package_name.to_string())
}
pub fn package_count(&self) -> usize {
self.packages.len()
}
pub fn is_parallel_publishable(&self) -> bool {
self.packages.len() > 1
}
}