250 lines
8.4 KiB
Rust
250 lines
8.4 KiB
Rust
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<PathBuf>,
|
|
pub deps: BTreeMap<String, ProjectId>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct BuildPlan {
|
|
pub steps: Vec<BuildStep>,
|
|
}
|
|
|
|
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<PathBuf> = 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<PathBuf> = 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<String, ProjectId> = 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<ProjectId> {
|
|
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<ReverseProjectId> = 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<std::cmp::Ordering> {
|
|
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")]);
|
|
}
|
|
}
|