Skip to main content

cyrce_forge_core/
executor.rs

1// =============================================================================
2// 🔥 FORGE — Motor Core: Ejecutor Paralelo de Tareas
3// =============================================================================
4// Ejecuta tareas respetando dependencias y paralelizando cuando es posible.
5// Patrón moderno: async/await con tokio, ejecución por niveles del DAG.
6// =============================================================================
7
8use std::path::Path;
9use std::process::Stdio;
10use std::time::{Duration, Instant};
11
12use colored::Colorize;
13use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
14use tokio::process::Command;
15
16use crate::cache::BuildCache;
17use crate::dag::{TaskAction, TaskGraph};
18use crate::error::{ForgeError, ForgeResult};
19
20/// Resultado de la ejecución de una tarea individual.
21#[derive(Debug)]
22pub struct TaskResult {
23    /// Nombre de la tarea
24    pub name: String,
25    /// Si se ejecutó correctamente
26    pub success: bool,
27    /// Tiempo de ejecución
28    pub duration: Duration,
29    /// Salida estándar capturada
30    pub stdout: String,
31    /// Salida de errores capturada
32    pub stderr: String,
33    /// Si se usó caché (no se re-ejecutó)
34    pub cached: bool,
35}
36
37/// Resultado general de un build.
38#[derive(Debug)]
39pub struct BuildResult {
40    /// Resultados de cada tarea individual
41    pub tasks: Vec<TaskResult>,
42    /// Tiempo total
43    pub total_duration: Duration,
44    /// Si el build fue exitoso
45    pub success: bool,
46}
47
48/// Ejecutor de tareas del build system.
49pub struct Executor {
50    /// Directorio raíz del proyecto
51    project_dir: std::path::PathBuf,
52    /// Sistema de caché incremental
53    cache: BuildCache,
54    /// Si se debe mostrar salida verbosa
55    verbose: bool,
56}
57
58impl Executor {
59    /// Crea un nuevo ejecutor.
60    pub fn new(project_dir: &Path, verbose: bool) -> ForgeResult<Self> {
61        let cache = BuildCache::load(project_dir)?;
62        Ok(Self {
63            project_dir: project_dir.to_path_buf(),
64            cache,
65            verbose,
66        })
67    }
68
69    /// Ejecuta todas las tareas del grafo respetando dependencias.
70    /// Las tareas sin dependencias entre sí se ejecutan en paralelo.
71    pub async fn execute(&mut self, graph: &TaskGraph) -> ForgeResult<BuildResult> {
72        let start = Instant::now();
73        let levels = graph.parallel_levels()?;
74        let mut all_results: Vec<TaskResult> = Vec::new();
75        let mut success = true;
76
77        let multi = MultiProgress::new();
78
79        println!(
80            "\n{}",
81            format!("🔥 FORGE v{} — Iniciando build...", env!("CARGO_PKG_VERSION"))
82                .bold()
83                .cyan()
84        );
85        println!(
86            "{}",
87            format!(
88                "   📋 {} tareas en {} niveles de ejecución\n",
89                graph.len(),
90                levels.len()
91            )
92            .dimmed()
93        );
94
95        for (level_idx, level) in levels.iter().enumerate() {
96            if !success {
97                break;
98            }
99
100            if level.len() > 1 {
101                println!(
102                    "{}",
103                    format!("   ⚡ Nivel {} — {} tareas en paralelo", level_idx + 1, level.len())
104                        .yellow()
105                );
106            }
107
108            // Ejecutar tareas del mismo nivel en paralelo
109            let mut handles = Vec::new();
110
111            for task_name in level {
112                let task = graph
113                    .get_task(task_name)
114                    .ok_or_else(|| ForgeError::TaskNotFound {
115                        task_name: task_name.clone(),
116                    })?
117                    .clone();
118
119                let project_dir = self.project_dir.clone();
120                let verbose = self.verbose;
121
122                let pb = multi.add(ProgressBar::new_spinner());
123                pb.set_style(
124                    ProgressStyle::default_spinner()
125                        .template("   {spinner:.cyan} {msg}")
126                        .unwrap(),
127                );
128                pb.set_message(format!("{}", task.name));
129
130                handles.push(tokio::spawn(async move {
131                    let result = execute_single_task(&task, &project_dir, verbose, &pb).await;
132                    pb.finish_and_clear();
133                    result
134                }));
135            }
136
137            // Esperar que todas las tareas del nivel terminen
138            for handle in handles {
139                match handle.await {
140                    Ok(Ok(result)) => {
141                        let status = if result.cached {
142                            "⚡ CACHÉ".dimmed().to_string()
143                        } else if result.success {
144                            "✅ OK".green().to_string()
145                        } else {
146                            success = false;
147                            "❌ FALLÓ".red().to_string()
148                        };
149
150                        let duration_str =
151                            format!("({:.1}ms)", result.duration.as_secs_f64() * 1000.0).dimmed();
152
153                        println!(
154                            "   {} {} {}",
155                            status,
156                            result.name.bold(),
157                            duration_str
158                        );
159
160                        if !result.success && !result.stderr.is_empty() {
161                            println!("\n{}", "   ── Error ──".red().bold());
162                            for line in result.stderr.lines().take(20) {
163                                println!("      {}", line.red());
164                            }
165                            println!();
166                        }
167
168                        if !result.success {
169                            success = false;
170                        }
171
172                        all_results.push(result);
173                    }
174                    Ok(Err(e)) => {
175                        success = false;
176                        println!("   {} {}", "❌ Error:".red().bold(), e);
177                    }
178                    Err(e) => {
179                        success = false;
180                        println!("   {} Tarea panicked: {}", "💀".red(), e);
181                    }
182                }
183            }
184        }
185
186        let total_duration = start.elapsed();
187
188        // Resumen final
189        println!();
190        if success {
191            println!(
192                "{}",
193                format!(
194                    "🔥 BUILD EXITOSO en {:.2}s ({} tareas)",
195                    total_duration.as_secs_f64(),
196                    all_results.len()
197                )
198                .green()
199                .bold()
200            );
201        } else {
202            println!(
203                "{}",
204                format!(
205                    "💀 BUILD FALLIDO en {:.2}s",
206                    total_duration.as_secs_f64()
207                )
208                .red()
209                .bold()
210            );
211        }
212        println!();
213
214        // Guardar caché actualizado
215        self.cache.save(&self.project_dir)?;
216
217        Ok(BuildResult {
218            tasks: all_results,
219            total_duration,
220            success,
221        })
222    }
223
224    /// Devuelve referencia mutable al caché para actualizaciones externas.
225    pub fn cache_mut(&mut self) -> &mut BuildCache {
226        &mut self.cache
227    }
228}
229
230/// Ejecuta una tarea individual.
231async fn execute_single_task(
232    task: &crate::dag::Task,
233    project_dir: &Path,
234    verbose: bool,
235    pb: &ProgressBar,
236) -> ForgeResult<TaskResult> {
237    let start = Instant::now();
238
239    pb.set_message(format!("Ejecutando: {}", task.name));
240
241    let (success, stdout, stderr) = match &task.action {
242        TaskAction::Command(cmd) => {
243            run_external_command(cmd, project_dir, verbose).await?
244        }
245        TaskAction::Internal(_internal) => {
246            // Las tareas internas serán manejadas por los módulos de lenguaje
247            // Por ahora, simplemente se marcan como exitosas
248            (true, String::new(), String::new())
249        }
250        TaskAction::Composite => {
251            // Las tareas compuestas no ejecutan nada, solo agrupan dependencias
252            (true, String::new(), String::new())
253        }
254    };
255
256    Ok(TaskResult {
257        name: task.name.clone(),
258        success,
259        duration: start.elapsed(),
260        stdout,
261        stderr,
262        cached: false,
263    })
264}
265
266/// Ejecuta un comando externo del sistema.
267async fn run_external_command(
268    command: &str,
269    working_dir: &Path,
270    _verbose: bool,
271) -> ForgeResult<(bool, String, String)> {
272    // En Windows usamos cmd /C, en Unix usamos sh -c
273    let output = if cfg!(target_os = "windows") {
274        Command::new("cmd")
275            .args(["/C", command])
276            .current_dir(working_dir)
277            .stdout(Stdio::piped())
278            .stderr(Stdio::piped())
279            .output()
280            .await
281    } else {
282        Command::new("sh")
283            .args(["-c", command])
284            .current_dir(working_dir)
285            .stdout(Stdio::piped())
286            .stderr(Stdio::piped())
287            .output()
288            .await
289    };
290
291    match output {
292        Ok(output) => {
293            let stdout = String::from_utf8_lossy(&output.stdout).to_string();
294            let stderr = String::from_utf8_lossy(&output.stderr).to_string();
295            Ok((output.status.success(), stdout, stderr))
296        }
297        Err(e) => {
298            if e.kind() == std::io::ErrorKind::NotFound {
299                Err(ForgeError::CommandNotFound {
300                    command: command.to_string(),
301                }
302                .into())
303            } else {
304                Err(ForgeError::IoError {
305                    path: working_dir.to_path_buf(),
306                    message: format!("Error al ejecutar '{}': {}", command, e),
307                }
308                .into())
309            }
310        }
311    }
312}