1use 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#[derive(Debug)]
22pub struct TaskResult {
23 pub name: String,
25 pub success: bool,
27 pub duration: Duration,
29 pub stdout: String,
31 pub stderr: String,
33 pub cached: bool,
35}
36
37#[derive(Debug)]
39pub struct BuildResult {
40 pub tasks: Vec<TaskResult>,
42 pub total_duration: Duration,
44 pub success: bool,
46}
47
48pub struct Executor {
50 project_dir: std::path::PathBuf,
52 cache: BuildCache,
54 verbose: bool,
56}
57
58impl Executor {
59 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 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 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 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 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 self.cache.save(&self.project_dir)?;
216
217 Ok(BuildResult {
218 tasks: all_results,
219 total_duration,
220 success,
221 })
222 }
223
224 pub fn cache_mut(&mut self) -> &mut BuildCache {
226 &mut self.cache
227 }
228}
229
230async 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 (true, String::new(), String::new())
249 }
250 TaskAction::Composite => {
251 (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
266async fn run_external_command(
268 command: &str,
269 working_dir: &Path,
270 _verbose: bool,
271) -> ForgeResult<(bool, String, String)> {
272 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}