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)
135            .map(|d| d.clone())
136            .unwrap_or_default(),
137    };
138    
139    steps.push(step);
140    
141    Ok(())
142}
143
144/// Select the best representation for a platform
145fn select_representation<'a>(
146    node: &'a cadi_core::GraphNode,
147    platform: &str,
148) -> Option<&'a cadi_core::Representation> {
149    // Try to find exact platform match
150    if let Some(r) = node.representations.iter().find(|r| {
151        r.architecture.as_ref().map(|a| a == platform).unwrap_or(false)
152    }) {
153        return Some(r);
154    }
155    
156    // Fall back to any representation
157    node.representations.first()
158}
159
160/// Determine the transformation needed for a node
161fn determine_transform(node: &cadi_core::GraphNode, platform: &str) -> super::TransformType {
162    // If we have source, need to compile
163    if node.source_cadi.is_some() {
164        return super::TransformType::Compile {
165            target: platform.to_string(),
166        };
167    }
168    
169    // If we have IR, need to compile to blob
170    if node.ir_cadi.is_some() {
171        return super::TransformType::Compile {
172            target: platform.to_string(),
173        };
174    }
175    
176    // If we have container, just fetch
177    if node.container_cadi.is_some() {
178        return super::TransformType::Custom {
179            name: "fetch".to_string(),
180            args: Default::default(),
181        };
182    }
183    
184    // Default to bundle
185    super::TransformType::Bundle {
186        format: "default".to_string(),
187    }
188}
189
190/// Build input list for a node
191fn build_inputs(
192    node: &cadi_core::GraphNode,
193    deps: &HashMap<String, Vec<String>>,
194) -> Vec<super::TransformInput> {
195    let mut inputs = Vec::new();
196    
197    // Add main input
198    if let Some(ref source_id) = node.source_cadi {
199        inputs.push(super::TransformInput {
200            chunk_id: source_id.clone(),
201            data: None,
202            role: "main".to_string(),
203            path: None,
204        });
205    } else if let Some(ref ir_id) = node.ir_cadi {
206        inputs.push(super::TransformInput {
207            chunk_id: ir_id.clone(),
208            data: None,
209            role: "main".to_string(),
210            path: None,
211        });
212    } else if let Some(ref blob_id) = node.blob_cadi {
213        inputs.push(super::TransformInput {
214            chunk_id: blob_id.clone(),
215            data: None,
216            role: "main".to_string(),
217            path: None,
218        });
219    }
220    
221    // Add dependency inputs
222    if let Some(node_deps) = deps.get(&node.id) {
223        for dep_id in node_deps {
224            inputs.push(super::TransformInput {
225                chunk_id: format!("pending:{}", dep_id),
226                data: None,
227                role: "dependency".to_string(),
228                path: None,
229            });
230        }
231    }
232    
233    inputs
234}