use crate::deps::resolver::{ProjectKey, ResolvedGraph}; use prometeu_analysis::ids::ProjectId; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; use std::path::PathBuf; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum BuildTarget { Main, Test, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BuildStep { pub project_id: ProjectId, pub project_key: ProjectKey, pub project_dir: PathBuf, pub target: BuildTarget, pub sources: Vec, pub deps: BTreeMap, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BuildPlan { pub steps: Vec, } impl BuildPlan { pub fn from_graph(graph: &ResolvedGraph, target: BuildTarget) -> Self { let mut steps = Vec::new(); let sorted_ids = topological_sort(graph); for id in sorted_ids { if let Some(node) = graph.nodes.get(&id) { let sources_list: Vec = match target { BuildTarget::Main => node.sources.files.clone(), BuildTarget::Test => node.sources.test_files.clone(), }; // Normalize to relative paths and sort lexicographically let mut sources: Vec = sources_list .into_iter() .map(|p| { p.strip_prefix(&node.path) .map(|rp| rp.to_path_buf()) .unwrap_or(p) }) .collect(); sources.sort(); let mut deps: BTreeMap = BTreeMap::new(); if let Some(edges) = graph.edges.get(&id) { for edge in edges { deps.insert(edge.alias.clone(), edge.to); } } steps.push(BuildStep { project_id: id, project_key: node.key.clone(), project_dir: node.path.clone(), target, sources, deps, }); } } Self { steps } } } fn topological_sort(graph: &ResolvedGraph) -> Vec { let mut in_degree = HashMap::new(); let mut adj = HashMap::new(); for id in graph.nodes.keys() { in_degree.insert(id.clone(), 0); adj.insert(id.clone(), Vec::new()); } for (from, edges) in &graph.edges { for edge in edges { // from depends on edge.to // so edge.to must be built BEFORE from // edge.to -> from adj.get_mut(&edge.to).unwrap().push(from.clone()); *in_degree.get_mut(from).unwrap() += 1; } } let mut ready: std::collections::BinaryHeap = graph.nodes.keys() .filter(|id| *in_degree.get(id).unwrap() == 0) .map(|id| ReverseProjectId(*id)) .collect(); let mut result = Vec::new(); while let Some(ReverseProjectId(u)) = ready.pop() { result.push(u); if let Some(neighbors) = adj.get(&u) { for v in neighbors { let degree = in_degree.get_mut(v).unwrap(); *degree -= 1; if *degree == 0 { ready.push(ReverseProjectId(*v)); } } } } result } #[derive(Eq, PartialEq, Copy, Clone)] struct ReverseProjectId(ProjectId); impl Ord for ReverseProjectId { fn cmp(&self, other: &Self) -> std::cmp::Ordering { // BinaryHeap is a max-heap. We want min-heap with stable numeric order. // So we reverse the comparison on the numeric id. other.0.as_u32().cmp(&self.0.as_u32()) } } impl PartialOrd for ReverseProjectId { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } #[cfg(test)] mod tests { use super::*; use crate::deps::resolver::{ProjectKey, ResolvedEdge, ResolvedGraph, ResolvedNode}; use crate::manifest::Manifest; use crate::sources::ProjectSources; use std::collections::BTreeMap; fn mock_node(id: ProjectId, name: &str, version: &str) -> (ProjectId, ResolvedNode) { let node = ResolvedNode { id, key: ProjectKey { name: name.to_string(), version: version.to_string() }, path: PathBuf::from(format!("/{}", name)), manifest: Manifest { name: name.to_string(), version: version.to_string(), kind: crate::manifest::ManifestKind::Lib, dependencies: BTreeMap::new(), }, sources: ProjectSources { main: None, files: vec![PathBuf::from("b.pbs"), PathBuf::from("a.pbs")], test_files: vec![PathBuf::from("test_b.pbs"), PathBuf::from("test_a.pbs")], }, }; (id, node) } #[test] fn test_topo_sort_stability() { let mut graph = ResolvedGraph::default(); let (a_id, a) = mock_node(ProjectId(0), "a", "1.0.0"); let (b_id, b) = mock_node(ProjectId(1), "b", "1.0.0"); let (c_id, c) = mock_node(ProjectId(2), "c", "1.0.0"); graph.nodes.insert(a_id, a); graph.nodes.insert(b_id, b); graph.nodes.insert(c_id, c); // No edges, order by numeric id: a(0), b(1), c(2) let plan = BuildPlan::from_graph(&graph, BuildTarget::Main); assert_eq!(plan.steps[0].project_key.name, "a"); assert_eq!(plan.steps[1].project_key.name, "b"); assert_eq!(plan.steps[2].project_key.name, "c"); } #[test] fn test_topo_sort_dependencies() { let mut graph = ResolvedGraph::default(); let (a_id, a) = mock_node(ProjectId(0), "a", "1.0.0"); let (b_id, b) = mock_node(ProjectId(1), "b", "1.0.0"); let (c_id, c) = mock_node(ProjectId(2), "c", "1.0.0"); graph.nodes.insert(a_id, a.clone()); graph.nodes.insert(b_id, b.clone()); graph.nodes.insert(c_id, c.clone()); // c depends on b, b depends on a // Sort should be: a, b, c graph.edges.insert(c_id, vec![ResolvedEdge { alias: "b_alias".to_string(), to: b_id }]); graph.edges.insert(b_id, vec![ResolvedEdge { alias: "a_alias".to_string(), to: a_id }]); let plan = BuildPlan::from_graph(&graph, BuildTarget::Main); assert_eq!(plan.steps.len(), 3); assert_eq!(plan.steps[0].project_key.name, "a"); assert_eq!(plan.steps[1].project_key.name, "b"); assert_eq!(plan.steps[2].project_key.name, "c"); assert_eq!(plan.steps[2].deps.get("b_alias").copied(), Some(b_id)); } #[test] fn test_topo_sort_complex() { let mut graph = ResolvedGraph::default(); // d -> b, c // b -> a // c -> a // a let (a_id, a) = mock_node(ProjectId(0), "a", "1.0.0"); let (b_id, b) = mock_node(ProjectId(1), "b", "1.0.0"); let (c_id, c) = mock_node(ProjectId(2), "c", "1.0.0"); let (d_id, d) = mock_node(ProjectId(3), "d", "1.0.0"); graph.nodes.insert(a_id, a.clone()); graph.nodes.insert(b_id, b.clone()); graph.nodes.insert(c_id, c.clone()); graph.nodes.insert(d_id, d.clone()); graph.edges.insert(d_id, vec![ ResolvedEdge { alias: "b".to_string(), to: b_id }, ResolvedEdge { alias: "c".to_string(), to: c_id }, ]); graph.edges.insert(b_id, vec![ResolvedEdge { alias: "a".to_string(), to: a_id }]); graph.edges.insert(c_id, vec![ResolvedEdge { alias: "a".to_string(), to: a_id }]); let plan = BuildPlan::from_graph(&graph, BuildTarget::Main); let names: Vec<_> = plan.steps.iter().map(|s| s.project_key.name.as_str()).collect(); assert_eq!(names, vec!["a", "b", "c", "d"]); } #[test] fn test_sources_sorting() { let mut graph = ResolvedGraph::default(); let (a_id, a) = mock_node(ProjectId(0), "a", "1.0.0"); graph.nodes.insert(a_id, a); let plan = BuildPlan::from_graph(&graph, BuildTarget::Main); assert_eq!(plan.steps[0].sources, vec![PathBuf::from("a.pbs"), PathBuf::from("b.pbs")]); let plan_test = BuildPlan::from_graph(&graph, BuildTarget::Test); assert_eq!(plan_test.steps[0].sources, vec![PathBuf::from("test_a.pbs"), PathBuf::from("test_b.pbs")]); } }