use anyhow::{bail, Result};
use petgraph::graph::{DiGraph, NodeIndex};
use std::collections::{HashMap, HashSet, VecDeque};
use std::path::{Path, PathBuf};
use crate::config;
use crate::config::schema::YmConfig;
#[derive(Debug, Clone)]
pub struct PackageNode {
pub name: String,
pub path: PathBuf,
pub config: YmConfig,
}
pub struct WorkspaceGraph {
pub graph: DiGraph<PackageNode, ()>,
pub name_to_index: HashMap<String, NodeIndex>,
}
impl WorkspaceGraph {
pub fn build(workspace_root: &Path) -> Result<Self> {
let cache_dir = workspace_root.join(config::CACHE_DIR);
if let Some(cached) = super::cache::GraphCache::load(&cache_dir) {
if let Ok(ws) = Self::from_cache(&cached) {
return Ok(ws);
}
}
let ws = Self::build_fresh(workspace_root)?;
let cache_data: Vec<(String, PathBuf, Vec<String>)> = ws
.name_to_index
.keys()
.filter_map(|name| {
let pkg = ws.get_package(name)?;
let ws_deps = pkg.config.workspace_module_deps();
Some((name.clone(), pkg.path.clone(), ws_deps))
})
.collect();
let graph_cache = super::cache::GraphCache::build_from_workspace(workspace_root, &cache_data);
let _ = graph_cache.save(&cache_dir);
Ok(ws)
}
fn build_fresh(workspace_root: &Path) -> Result<Self> {
let root_config = config::load_config(&workspace_root.join(config::CONFIG_FILE))?;
let patterns = root_config
.workspaces
.as_ref()
.cloned()
.unwrap_or_default();
let mut graph = DiGraph::new();
let mut name_to_index = HashMap::new();
let mut packages = Vec::new();
for pattern in &patterns {
let full_pattern = workspace_root.join(pattern).join(config::CONFIG_FILE);
let pattern_str = full_pattern.to_string_lossy().to_string();
for config_path in glob::glob(&pattern_str).unwrap_or_else(|_| glob::glob("").unwrap()).flatten() {
match config::load_config(&config_path) {
Ok(cfg) => {
let pkg_dir = config_path.parent().unwrap().to_path_buf();
let node = PackageNode {
name: cfg.name.clone(),
path: pkg_dir,
config: cfg,
};
packages.push(node);
}
Err(e) => {
bail!("Failed to parse {}: {}", config_path.display(), e);
}
}
}
}
for pkg in &packages {
if let Some(&existing_idx) = name_to_index.get(&pkg.name) {
let existing_node: &PackageNode = &graph[existing_idx];
let existing_path = &existing_node.path;
bail!(
"Duplicate module name '{}' found in:\n - {}\n - {}",
pkg.name,
existing_path.display(),
pkg.path.display()
);
}
let idx = graph.add_node(pkg.clone());
name_to_index.insert(pkg.name.clone(), idx);
}
for pkg in &packages {
let ws_deps = pkg.config.workspace_module_deps();
let from = name_to_index[&pkg.name];
for dep_name in &ws_deps {
if let Some(&to) = name_to_index.get(dep_name) {
graph.add_edge(from, to, ());
}
}
}
if let Err(cycle) = petgraph::algo::toposort(&graph, None) {
let cycle_node = cycle.node_id();
let cycle_name = &graph[cycle_node].name;
let mut path = Vec::new();
let mut visited = HashSet::new();
Self::find_cycle_path(&graph, cycle_node, cycle_node, &mut visited, &mut path);
if path.is_empty() {
bail!("Cycle detected involving module '{}'", cycle_name);
} else {
let path_str: Vec<&str> = path.iter().map(|idx| graph[*idx].name.as_str()).collect();
bail!(
"Cycle detected: {} → {}",
path_str.join(" → "),
graph[cycle_node].name
);
}
}
Ok(WorkspaceGraph {
graph,
name_to_index,
})
}
fn find_cycle_path(
graph: &DiGraph<PackageNode, ()>,
current: NodeIndex,
target: NodeIndex,
visited: &mut HashSet<NodeIndex>,
path: &mut Vec<NodeIndex>,
) -> bool {
path.push(current);
visited.insert(current);
for neighbor in graph.neighbors(current) {
if neighbor == target && path.len() > 1 {
return true;
}
if !visited.contains(&neighbor)
&& Self::find_cycle_path(graph, neighbor, target, visited, path)
{
return true;
}
}
path.pop();
false
}
fn from_cache(cached: &super::cache::GraphCache) -> Result<Self> {
let mut graph = DiGraph::new();
let mut name_to_index = HashMap::new();
for cpkg in &cached.packages {
let config_path = PathBuf::from(&cpkg.path).join(config::CONFIG_FILE);
let cfg = config::load_config(&config_path)?;
let node = PackageNode {
name: cpkg.name.clone(),
path: PathBuf::from(&cpkg.path),
config: cfg,
};
let idx = graph.add_node(node);
name_to_index.insert(cpkg.name.clone(), idx);
}
for cpkg in &cached.packages {
if let Some(&from) = name_to_index.get(&cpkg.name) {
for dep_name in &cpkg.workspace_dependencies {
if let Some(&to) = name_to_index.get(dep_name) {
graph.add_edge(from, to, ());
}
}
}
}
Ok(WorkspaceGraph {
graph,
name_to_index,
})
}
pub fn transitive_closure(&self, target: &str) -> Result<Vec<String>> {
let start = match self.name_to_index.get(target) {
Some(&idx) => idx,
None => bail!("Package '{}' not found in workspace", target),
};
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(start);
visited.insert(start);
while let Some(node) = queue.pop_front() {
for neighbor in self.graph.neighbors(node) {
if visited.insert(neighbor) {
queue.push_back(neighbor);
}
}
}
let mut result = Vec::new();
let mut in_degree: HashMap<NodeIndex, usize> = HashMap::new();
let mut reverse_deps: HashMap<NodeIndex, Vec<NodeIndex>> = HashMap::new();
for &idx in &visited {
let ws_deps = self.graph[idx].config.workspace_module_deps();
let mut deg = 0;
for dep_name in &ws_deps {
if let Some(&dep_idx) = self.name_to_index.get(dep_name) {
if visited.contains(&dep_idx) {
deg += 1;
reverse_deps.entry(dep_idx).or_default().push(idx);
}
}
}
in_degree.insert(idx, deg);
}
let mut ready: VecDeque<NodeIndex> = in_degree
.iter()
.filter(|(_, deg)| **deg == 0)
.map(|(idx, _)| *idx)
.collect();
while let Some(node) = ready.pop_front() {
result.push(self.graph[node].name.clone());
if let Some(deps) = reverse_deps.get(&node) {
for &idx in deps {
if let Some(deg) = in_degree.get_mut(&idx) {
*deg -= 1;
if *deg == 0 {
ready.push_back(idx);
}
}
}
}
}
Ok(result)
}
pub fn get_package(&self, name: &str) -> Option<&PackageNode> {
self.name_to_index
.get(name)
.map(|&idx| &self.graph[idx])
}
pub fn all_packages(&self) -> Vec<String> {
self.name_to_index.keys().cloned().collect()
}
pub fn topological_order(&self) -> Vec<String> {
match petgraph::algo::toposort(&self.graph, None) {
Ok(sorted) => sorted.iter().map(|&idx| self.graph[idx].name.clone()).collect(),
Err(_) => self.all_packages(), }
}
pub fn topological_levels(&self) -> Vec<Vec<String>> {
let topo = match petgraph::algo::toposort(&self.graph, None) {
Ok(sorted) => sorted,
Err(_) => return vec![self.all_packages()],
};
let mut level_of: HashMap<NodeIndex, usize> = HashMap::new();
let mut max_level = 0usize;
for &idx in &topo {
let mut my_level = 0;
for neighbor in self.graph.neighbors(idx) {
if let Some(&dep_level) = level_of.get(&neighbor) {
my_level = my_level.max(dep_level + 1);
}
}
level_of.insert(idx, my_level);
max_level = max_level.max(my_level);
}
let mut levels: Vec<Vec<String>> = vec![Vec::new(); max_level + 1];
for (&idx, &level) in &level_of {
levels[level].push(self.graph[idx].name.clone());
}
levels
}
}