use super::types::{EffectivePlanExplanation, PlannedCompilationUnit};
use anyhow::{bail, Result};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct DagNode {
pub unit: PlannedCompilationUnit,
pub dependencies: Vec<String>,
pub dependents: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CompilationDag {
nodes: HashMap<String, DagNode>,
roots: Vec<String>,
leaves: Vec<String>,
}
impl CompilationDag {
pub fn from_explanation(explanation: &EffectivePlanExplanation) -> Result<Self> {
let known_ids: HashSet<String> = explanation.units.iter().map(|u| u.unit_id()).collect();
let mut nodes: HashMap<String, DagNode> = HashMap::new();
for unit in &explanation.units {
let u_id = unit.unit_id();
let valid_deps: Vec<String> = unit
.dependencies
.iter()
.filter(|d| known_ids.contains(*d))
.cloned()
.collect();
nodes.insert(
u_id,
DagNode {
unit: unit.clone(),
dependencies: valid_deps,
dependents: Vec::new(),
},
);
}
let linking_ids: Vec<String> = nodes
.iter()
.filter(|(_, node)| {
matches!(
node.unit.kind.as_str(),
"bin"
| "test"
| "custom-test"
| "example"
| "bench"
| "cdylib"
| "dylib"
| "proc-macro"
) || super::pipelining::package_has_build_script(&node.unit.manifest_path)
})
.map(|(id, _)| id.clone())
.collect();
for id in linking_ids {
let mut transitive = HashSet::new();
let mut stack = nodes[&id].dependencies.clone();
while let Some(curr) = stack.pop() {
if transitive.insert(curr.clone()) {
if let Some(dep_node) = nodes.get(&curr) {
stack.extend(dep_node.dependencies.clone());
}
}
}
if let Some(node) = nodes.get_mut(&id) {
let mut all_deps: Vec<String> = transitive.into_iter().collect();
all_deps.sort();
node.dependencies = all_deps;
}
}
let node_keys: Vec<String> = nodes.keys().cloned().collect();
for id in &node_keys {
let deps = nodes[id].dependencies.clone();
for dep_id in deps {
if let Some(dep_node) = nodes.get_mut(&dep_id) {
dep_node.dependents.push(id.clone());
}
}
}
let roots: Vec<String> = nodes
.iter()
.filter(|(_, node)| node.dependents.is_empty())
.map(|(id, _)| id.clone())
.collect();
let leaves: Vec<String> = nodes
.iter()
.filter(|(_, node)| node.dependencies.is_empty())
.map(|(id, _)| id.clone())
.collect();
let dag = Self {
nodes,
roots,
leaves,
};
let _ = dag.topological_waves()?;
Ok(dag)
}
pub fn get_node(&self, unit_id: &str) -> Option<&DagNode> {
self.nodes.get(unit_id)
}
pub fn node_ids(&self) -> Vec<String> {
self.nodes.keys().cloned().collect()
}
pub fn root_ids(&self) -> &[String] {
&self.roots
}
pub fn leaf_ids(&self) -> &[String] {
&self.leaves
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn topological_waves(&self) -> Result<Vec<Vec<String>>> {
let mut in_degrees: HashMap<String, usize> = HashMap::new();
for (id, node) in &self.nodes {
in_degrees.insert(id.clone(), node.dependencies.len());
}
let mut waves: Vec<Vec<String>> = Vec::new();
let mut completed: HashSet<String> = HashSet::new();
while completed.len() < self.nodes.len() {
let current_wave: Vec<String> = self
.nodes
.keys()
.filter(|id| !completed.contains(*id) && in_degrees[*id] == 0)
.cloned()
.collect();
if current_wave.is_empty() {
bail!("Cyclic dependency detected in compilation DAG");
}
for id in ¤t_wave {
completed.insert(id.clone());
if let Some(node) = self.nodes.get(id) {
for dep_on_me in &node.dependents {
if let Some(deg) = in_degrees.get_mut(dep_on_me) {
if *deg > 0 {
*deg -= 1;
}
}
}
}
}
waves.push(current_wave);
}
Ok(waves)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::action::ToolchainInfo;
use std::path::PathBuf;
#[test]
fn test_topological_waves_linear() {
let u1 = PlannedCompilationUnit {
package_id: "pkg1".to_string(),
package_name: "pkg1".to_string(),
version: "1.0.0".to_string(),
target_name: "pkg1".to_string(),
kind: "lib".to_string(),
src_path: PathBuf::from("src/lib.rs"),
manifest_path: PathBuf::from("Cargo.toml"),
cache_eligibility: "eligible".to_string(),
is_workspace: true,
is_primary: true,
env_vars: Vec::new(),
dependencies: Vec::new(),
edition: Some("2021".to_string()),
features: Vec::new(),
for_host: false,
workspace_root: None,
};
let u2 = PlannedCompilationUnit {
package_id: "pkg2".to_string(),
package_name: "pkg2".to_string(),
version: "1.0.0".to_string(),
target_name: "pkg2".to_string(),
kind: "bin".to_string(),
src_path: PathBuf::from("src/main.rs"),
manifest_path: PathBuf::from("Cargo.toml"),
cache_eligibility: "eligible".to_string(),
is_workspace: true,
is_primary: true,
env_vars: Vec::new(),
dependencies: vec![u1.unit_id()],
edition: Some("2021".to_string()),
features: Vec::new(),
for_host: false,
workspace_root: None,
};
let explanation = EffectivePlanExplanation {
manifest_path: PathBuf::from("Cargo.toml"),
target_directory: PathBuf::from("target"),
profile: "debug".to_string(),
toolchain: ToolchainInfo {
name: "rustc".to_string(),
version: "1.80.0".to_string(),
target_triple: Some("x86_64".to_string()),
},
units: vec![u2.clone(), u1.clone()],
};
let dag = CompilationDag::from_explanation(&explanation).unwrap();
assert_eq!(dag.len(), 2);
assert_eq!(dag.leaf_ids(), &[u1.unit_id()]);
assert_eq!(dag.root_ids(), &[u2.unit_id()]);
let waves = dag.topological_waves().unwrap();
assert_eq!(waves.len(), 2);
assert_eq!(waves[0], vec![u1.unit_id()]);
assert_eq!(waves[1], vec![u2.unit_id()]);
}
}