cadi_builder/
plan.rs

1//! Build planning for CADI
2
3use cadi_core::{CadiError, CadiResult, Manifest};
4use std::collections::{HashMap, HashSet};
5
6/// A build plan
7#[derive(Debug)]
8pub struct BuildPlan {
9    /// Steps to execute in order
10    pub steps: Vec<BuildStep>,
11    /// Estimated total time in ms
12    pub estimated_time_ms: u64,
13}
14
15/// A single build step
16#[derive(Debug)]
17pub struct BuildStep {
18    /// Step name
19    pub name: String,
20    /// Chunk ID (if known)
21    pub chunk_id: Option<String>,
22    /// Transformation to apply
23    pub transform: super::TransformType,
24    /// Input chunk IDs
25    pub inputs: Vec<super::TransformInput>,
26    /// Dependencies (step names that must complete first)
27    pub depends_on: Vec<String>,
28}
29
30impl BuildPlan {
31    /// Create a build plan from a manifest
32    pub fn from_manifest(manifest: &Manifest, target: &str) -> CadiResult<Self> {
33        let target_config = manifest.find_target(target)
34            .ok_or_else(|| CadiError::BuildFailed(format!("Target '{}' not found", target)))?;
35        
36        let mut steps = Vec::new();
37        let mut visited = HashSet::new();
38        
39        // Build dependency graph
40        let deps = build_dependency_graph(&manifest.build_graph);
41        
42        // Determine nodes needed for this target
43        let nodes_needed: HashSet<_> = if target_config.nodes.is_empty() {
44            manifest.build_graph.nodes.iter().map(|n| n.id.as_str()).collect()
45        } else {
46            target_config.nodes.iter().map(|n| n.id.as_str()).collect()
47        };
48        
49        // Topological sort of nodes
50        for node_id in &nodes_needed {
51            collect_steps(
52                &manifest.build_graph,
53                node_id,
54                &target_config.platform,
55                &deps,
56                &mut visited,
57                &mut steps,
58            )?;
59        }
60        
61        // Estimate time (placeholder)
62        let estimated_time_ms = steps.len() as u64 * 1000;
63        
64        Ok(Self {
65            steps,
66            estimated_time_ms,
67        })
68    }
69
70    /// Check if plan is empty
71    pub fn is_empty(&self) -> bool {
72        self.steps.is_empty()
73    }
74
75    /// Get step count
76    pub fn len(&self) -> usize {
77        self.steps.len()
78    }
79}
80
81/// Build a dependency graph from the build graph edges
82fn build_dependency_graph(build_graph: &cadi_core::BuildGraph) -> HashMap<String, Vec<String>> {
83    let mut deps: HashMap<String, Vec<String>> = HashMap::new();
84    
85    for node in &build_graph.nodes {
86        deps.entry(node.id.clone()).or_default();
87    }
88    
89    for edge in &build_graph.edges {
90        deps.entry(edge.from.clone())
91            .or_default()
92            .push(edge.to.clone());
93    }
94    
95    deps
96}
97
98/// Recursively collect build steps for a node
99fn collect_steps(
100    build_graph: &cadi_core::BuildGraph,
101    node_id: &str,
102    platform: &str,
103    deps: &HashMap<String, Vec<String>>,
104    visited: &mut HashSet<String>,
105    steps: &mut Vec<BuildStep>,
106) -> CadiResult<()> {
107    if visited.contains(node_id) {
108        return Ok(());
109    }
110    
111    // Process dependencies first
112    if let Some(node_deps) = deps.get(node_id) {
113        for dep_id in node_deps {
114            collect_steps(build_graph, dep_id, platform, deps, visited, steps)?;
115        }
116    }
117    
118    visited.insert(node_id.to_string());
119    
120    // Find the node
121    let node = build_graph.nodes.iter()
122        .find(|n| n.id == node_id)
123        .ok_or_else(|| CadiError::BuildFailed(format!("Node '{}' not found", node_id)))?;
124    
125    // Determine the best representation for this platform
126    let repr = select_representation(node, platform);
127    
128    // Create build step
129    let step = BuildStep {
130        name: node_id.to_string(),
131        chunk_id: repr.map(|r| r.chunk.clone()),
132        transform: determine_transform(node, platform),
133        inputs: build_inputs(node, deps),
134        depends_on: deps.get(node_id).cloned()
135            .unwrap_or_default(),
136    };
137    
138    steps.push(step);
139    
140    Ok(())
141}
142
143/// Select the best representation for a platform
144fn select_representation<'a>(
145    node: &'a cadi_core::GraphNode,
146    platform: &str,
147) -> Option<&'a cadi_core::Representation> {
148    // Try to find exact platform match
149    if let Some(r) = node.representations.iter().find(|r| {
150        r.architecture.as_ref().map(|a| a == platform).unwrap_or(false)
151    }) {
152        return Some(r);
153    }
154    
155    // Fall back to any representation
156    node.representations.first()
157}
158
159/// Determine the transformation needed for a node
160fn determine_transform(node: &cadi_core::GraphNode, platform: &str) -> super::TransformType {
161    // If we have source, need to compile
162    if node.source_cadi.is_some() {
163        return super::TransformType::Compile {
164            target: platform.to_string(),
165        };
166    }
167    
168    // If we have IR, need to compile to blob
169    if node.ir_cadi.is_some() {
170        return super::TransformType::Compile {
171            target: platform.to_string(),
172        };
173    }
174    
175    // If we have container, just fetch
176    if node.container_cadi.is_some() {
177        return super::TransformType::Custom {
178            name: "fetch".to_string(),
179            args: Default::default(),
180        };
181    }
182    
183    // Default to bundle
184    super::TransformType::Bundle {
185        format: "default".to_string(),
186    }
187}
188
189/// Build input list for a node
190fn build_inputs(
191    node: &cadi_core::GraphNode,
192    deps: &HashMap<String, Vec<String>>,
193) -> Vec<super::TransformInput> {
194    let mut inputs = Vec::new();
195    
196    // Add main input
197    if let Some(ref source_id) = node.source_cadi {
198        inputs.push(super::TransformInput {
199            chunk_id: source_id.clone(),
200            data: None,
201            role: "main".to_string(),
202            path: None,
203        });
204    } else if let Some(ref ir_id) = node.ir_cadi {
205        inputs.push(super::TransformInput {
206            chunk_id: ir_id.clone(),
207            data: None,
208            role: "main".to_string(),
209            path: None,
210        });
211    } else if let Some(ref blob_id) = node.blob_cadi {
212        inputs.push(super::TransformInput {
213            chunk_id: blob_id.clone(),
214            data: None,
215            role: "main".to_string(),
216            path: None,
217        });
218    }
219    
220    // Add dependency inputs
221    if let Some(node_deps) = deps.get(&node.id) {
222        for dep_id in node_deps {
223            inputs.push(super::TransformInput {
224                chunk_id: format!("pending:{}", dep_id),
225                data: None,
226                role: "dependency".to_string(),
227                path: None,
228            });
229        }
230    }
231    
232    inputs
233}