1use anstyle::{AnsiColor as TermAnsiColor, Style as TermStyle};
2use std::io::{self, Write};
3use std::process::ExitCode;
4use std::sync::mpsc;
5use std::thread;
6
7use crate::error::command_failed;
8use crate::interpolate::interpolate;
9use crate::process::{OutputChunk, OutputStream};
10use crate::shell::{run_command, run_command_inherit};
11use crate::{EngineError, ExecutionNode, ExecutionPlan};
12
13pub fn run_plan(plan: &ExecutionPlan) -> Result<ExitCode, EngineError> {
21 run_plan_with_options(plan, RuntimeOptions::default())
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub struct RuntimeOptions {
33 pub quiet: bool,
34}
35
36pub fn run_plan_with_options(
45 plan: &ExecutionPlan,
46 options: RuntimeOptions,
47) -> Result<ExitCode, EngineError> {
48 let total_tasks = plan.nodes.len();
49 let mut task_index = 0usize;
50
51 while task_index < total_tasks {
52 let stage = plan.nodes[task_index].stage;
53 let stage_start = task_index;
54 while task_index < total_tasks && plan.nodes[task_index].stage == stage {
55 task_index += 1;
56 }
57 let stage_nodes = &plan.nodes[stage_start..task_index];
58
59 if !options.quiet {
60 for (offset, node) in stage_nodes.iter().enumerate() {
61 eprintln!(
62 "{}",
63 render_task_progress(stage_start + offset + 1, total_tasks, &node.name)
64 );
65 }
66 }
67
68 if stage_nodes.len() == 1 {
69 run_node_inherit(&stage_nodes[0], &plan.working_dir, plan.shell.as_deref())?;
70 } else {
71 execute_stage(stage_nodes, &plan.working_dir, plan.shell.as_deref())?;
72 }
73 }
74
75 Ok(ExitCode::SUCCESS)
76}
77
78fn execute_stage(
79 stage_nodes: &[ExecutionNode],
80 working_dir: &std::path::Path,
81 default_shell: Option<&str>,
82) -> Result<(), EngineError> {
83 let stage_len = stage_nodes.len();
84 let (event_tx, event_rx) = mpsc::channel::<StageEvent>();
85
86 thread::scope(|scope| {
87 let mut handles = Vec::new();
88
89 for (index, node) in stage_nodes.iter().cloned().enumerate() {
90 let working_dir = working_dir.to_path_buf();
91 let shell = default_shell.map(str::to_string);
92 let event_tx = event_tx.clone();
93 handles.push(
94 scope.spawn(move || {
95 run_node(index, &node, &working_dir, shell.as_deref(), event_tx)
96 }),
97 );
98 }
99 drop(event_tx);
100
101 run_ordered_output_event_loop(&event_rx, stage_len)?;
102
103 collect_thread_results(handles)
104 })
105}
106
107fn run_ordered_output_event_loop(
108 event_rx: &mpsc::Receiver<StageEvent>,
109 stage_len: usize,
110) -> Result<(), EngineError> {
111 let mut buffers = vec![Vec::<OutputChunk>::new(); stage_len];
112 let mut finished = vec![false; stage_len];
113 let mut task_errors = (0..stage_len)
114 .map(|_| None)
115 .collect::<Vec<Option<EngineError>>>();
116 let mut current_index = 0usize;
117 let mut finished_count = 0usize;
118 let mut first_error = None;
119
120 while finished_count < stage_len {
121 match event_rx.recv() {
122 Ok(StageEvent::Output { task_index, chunk }) => {
123 if task_index == current_index {
124 print_output_chunk(&chunk)?;
125 } else {
126 buffers[task_index].push(chunk);
127 }
128 }
129 Ok(StageEvent::Finished { task_index, error }) => {
130 finished[task_index] = true;
131 task_errors[task_index] = error;
132 finished_count += 1;
133
134 while current_index < stage_len {
135 flush_task_buffer(&mut buffers[current_index])?;
136 if !finished[current_index] {
137 break;
138 }
139 if first_error.is_none() {
140 first_error = task_errors[current_index].take();
141 }
142 current_index += 1;
143 }
144 }
145 Err(_) => break,
146 }
147 }
148
149 match first_error {
150 Some(error) => Err(error),
151 None => Ok(()),
152 }
153}
154
155fn collect_thread_results(
156 handles: Vec<thread::ScopedJoinHandle<'_, Result<(), EngineError>>>,
157) -> Result<(), EngineError> {
158 let mut first_error = None;
159 for handle in handles {
160 match handle.join() {
161 Ok(Ok(())) => {}
162 Ok(Err(error)) => {
163 if first_error.is_none() {
164 first_error = Some(error);
165 }
166 }
167 Err(payload) => std::panic::resume_unwind(payload),
168 }
169 }
170 match first_error {
171 Some(error) => Err(error),
172 None => Ok(()),
173 }
174}
175
176fn run_node(
177 task_index: usize,
178 node: &ExecutionNode,
179 working_dir: &std::path::Path,
180 default_shell: Option<&str>,
181 event_tx: mpsc::Sender<StageEvent>,
182) -> Result<(), EngineError> {
183 let (output_tx, output_rx) = mpsc::channel::<OutputChunk>();
184 let forwarder = thread::spawn({
185 let event_tx = event_tx.clone();
186 move || -> Result<(), EngineError> {
187 while let Ok(chunk) = output_rx.recv() {
188 event_tx
189 .send(StageEvent::Output { task_index, chunk })
190 .map_err(|_| {
191 EngineError::Runtime("failed to forward task output".to_string())
192 })?;
193 }
194 Ok(())
195 }
196 });
197
198 let total_commands = node.commands.len();
199 let mut task_error = None;
200
201 for (index, command) in node.commands.iter().enumerate() {
202 let rendered = match interpolate(command, &node.params) {
203 Ok(rendered) => rendered,
204 Err(error) => {
205 task_error = Some(error);
206 break;
207 }
208 };
209
210 let shell = node.shell.as_deref().or(default_shell).unwrap_or("deno");
211 let code = match run_command(
212 &rendered,
213 working_dir,
214 shell,
215 node.shell_fallback,
216 output_tx.clone(),
217 ) {
218 Ok(code) => code,
219 Err(error) => {
220 task_error = Some(error);
221 break;
222 }
223 };
224
225 if code != ExitCode::SUCCESS {
226 task_error = Some(command_failed(
227 &node.name,
228 index + 1,
229 total_commands,
230 &rendered,
231 code,
232 ));
233 break;
234 }
235 }
236
237 drop(output_tx);
238 match forwarder.join() {
239 Ok(Ok(())) => {}
240 Ok(Err(error)) => {
241 if task_error.is_none() {
242 task_error = Some(error);
243 }
244 }
245 Err(_) => {
246 if task_error.is_none() {
247 task_error = Some(EngineError::Runtime(
248 "task output forwarder thread panicked".to_string(),
249 ));
250 }
251 }
252 }
253
254 event_tx
255 .send(StageEvent::Finished {
256 task_index,
257 error: task_error,
258 })
259 .map_err(|_| EngineError::Runtime("failed to finalize task output".to_string()))?;
260 Ok(())
261}
262
263fn run_node_inherit(
264 node: &ExecutionNode,
265 working_dir: &std::path::Path,
266 default_shell: Option<&str>,
267) -> Result<(), EngineError> {
268 let total_commands = node.commands.len();
269
270 for (index, command) in node.commands.iter().enumerate() {
271 let rendered = interpolate(command, &node.params)?;
272 let shell = node.shell.as_deref().or(default_shell).unwrap_or("deno");
273 let code = run_command_inherit(&rendered, working_dir, shell, node.shell_fallback)?;
274
275 if code != ExitCode::SUCCESS {
276 return Err(command_failed(
277 &node.name,
278 index + 1,
279 total_commands,
280 &rendered,
281 code,
282 ));
283 }
284 }
285
286 Ok(())
287}
288
289#[derive(Debug)]
290enum StageEvent {
291 Output {
292 task_index: usize,
293 chunk: OutputChunk,
294 },
295 Finished {
296 task_index: usize,
297 error: Option<EngineError>,
298 },
299}
300
301fn flush_task_buffer(buffer: &mut Vec<OutputChunk>) -> Result<(), EngineError> {
302 for chunk in buffer.drain(..) {
303 print_output_chunk(&chunk)?;
304 }
305 Ok(())
306}
307
308fn print_output_chunk(chunk: &OutputChunk) -> Result<(), EngineError> {
309 match chunk.stream {
310 OutputStream::Stdout => write_output(&chunk.text, io::stdout()),
311 OutputStream::Stderr => write_output(&chunk.text, io::stderr()),
312 }
313}
314
315fn write_output(content: &str, mut writer: impl Write) -> Result<(), EngineError> {
316 write!(writer, "{content}")
317 .map_err(|error| EngineError::Runtime(format!("failed to write task output: {error}")))?;
318
319 writer
320 .flush()
321 .map_err(|error| EngineError::Runtime(format!("failed to flush task output: {error}")))?;
322 Ok(())
323}
324
325fn render_task_progress(task_index: usize, total_tasks: usize, task_name: &str) -> String {
326 let label_style = TermStyle::new()
327 .fg_color(Some(TermAnsiColor::BrightGreen.into()))
328 .bold();
329 let task_style = TermStyle::new()
330 .fg_color(Some(TermAnsiColor::BrightCyan.into()))
331 .bold();
332
333 format!(
334 "{}[{}/{}]{} {}{}{}",
335 label_style.render(),
336 task_index,
337 total_tasks,
338 label_style.render_reset(),
339 task_style.render(),
340 task_name,
341 task_style.render_reset()
342 )
343}