bootstrap/utils/
exec.rs

1//! Command Execution Module
2//!
3//! Provides a structured interface for executing and managing commands during bootstrap,
4//! with support for controlled failure handling and output management.
5//!
6//! This module defines the [`ExecutionContext`] type, which encapsulates global configuration
7//! relevant to command execution in the bootstrap process. This includes settings such as
8//! dry-run mode, verbosity level, and failure behavior.
9
10use std::collections::HashMap;
11use std::ffi::{OsStr, OsString};
12use std::fmt::{Debug, Formatter};
13use std::fs::File;
14use std::hash::Hash;
15use std::io::{BufWriter, Write};
16use std::panic::Location;
17use std::path::Path;
18use std::process::{
19    Child, ChildStderr, ChildStdout, Command, CommandArgs, CommandEnvs, ExitStatus, Output, Stdio,
20};
21use std::sync::{Arc, Mutex};
22use std::time::{Duration, Instant};
23
24use build_helper::drop_bomb::DropBomb;
25use build_helper::exit;
26
27use crate::core::config::DryRun;
28use crate::{PathBuf, t};
29
30/// What should be done when the command fails.
31#[derive(Debug, Copy, Clone)]
32pub enum BehaviorOnFailure {
33    /// Immediately stop bootstrap.
34    Exit,
35    /// Delay failure until the end of bootstrap invocation.
36    DelayFail,
37    /// Ignore the failure, the command can fail in an expected way.
38    Ignore,
39}
40
41/// How should the output of a specific stream of the command (stdout/stderr) be handled
42/// (whether it should be captured or printed).
43#[derive(Debug, Copy, Clone)]
44pub enum OutputMode {
45    /// Prints the stream by inheriting it from the bootstrap process.
46    Print,
47    /// Captures the stream into memory.
48    Capture,
49}
50
51impl OutputMode {
52    pub fn captures(&self) -> bool {
53        match self {
54            OutputMode::Print => false,
55            OutputMode::Capture => true,
56        }
57    }
58
59    pub fn stdio(&self) -> Stdio {
60        match self {
61            OutputMode::Print => Stdio::inherit(),
62            OutputMode::Capture => Stdio::piped(),
63        }
64    }
65}
66
67#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)]
68pub struct CommandFingerprint {
69    program: OsString,
70    args: Vec<OsString>,
71    envs: Vec<(OsString, Option<OsString>)>,
72    cwd: Option<PathBuf>,
73}
74
75impl CommandFingerprint {
76    #[cfg(feature = "tracing")]
77    pub(crate) fn program_name(&self) -> String {
78        Path::new(&self.program)
79            .file_name()
80            .map(|p| p.to_string_lossy().to_string())
81            .unwrap_or_else(|| "<unknown command>".to_string())
82    }
83
84    /// Helper method to format both Command and BootstrapCommand as a short execution line,
85    /// without all the other details (e.g. environment variables).
86    pub(crate) fn format_short_cmd(&self) -> String {
87        use std::fmt::Write;
88
89        let mut cmd = self.program.to_string_lossy().to_string();
90        for arg in &self.args {
91            let arg = arg.to_string_lossy();
92            if arg.contains(' ') {
93                write!(cmd, " '{arg}'").unwrap();
94            } else {
95                write!(cmd, " {arg}").unwrap();
96            }
97        }
98        if let Some(cwd) = &self.cwd {
99            write!(cmd, " [workdir={}]", cwd.to_string_lossy()).unwrap();
100        }
101        cmd
102    }
103}
104
105#[derive(Default, Clone)]
106pub struct CommandProfile {
107    pub traces: Vec<ExecutionTrace>,
108}
109
110#[derive(Default)]
111pub struct CommandProfiler {
112    stats: Mutex<HashMap<CommandFingerprint, CommandProfile>>,
113}
114
115impl CommandProfiler {
116    pub fn record_execution(&self, key: CommandFingerprint, start_time: Instant) {
117        let mut stats = self.stats.lock().unwrap();
118        let entry = stats.entry(key).or_default();
119        entry.traces.push(ExecutionTrace::Executed { duration: start_time.elapsed() });
120    }
121
122    pub fn record_cache_hit(&self, key: CommandFingerprint) {
123        let mut stats = self.stats.lock().unwrap();
124        let entry = stats.entry(key).or_default();
125        entry.traces.push(ExecutionTrace::CacheHit);
126    }
127
128    /// Report summary of executed commands file at the specified `path`.
129    pub fn report_summary(&self, path: &Path, start_time: Instant) {
130        let file = t!(File::create(path));
131
132        let mut writer = BufWriter::new(file);
133        let stats = self.stats.lock().unwrap();
134
135        let mut entries: Vec<_> = stats
136            .iter()
137            .map(|(key, profile)| {
138                let max_duration = profile
139                    .traces
140                    .iter()
141                    .filter_map(|trace| match trace {
142                        ExecutionTrace::Executed { duration, .. } => Some(*duration),
143                        _ => None,
144                    })
145                    .max();
146
147                (key, profile, max_duration)
148            })
149            .collect();
150
151        entries.sort_by(|a, b| b.2.cmp(&a.2));
152
153        let total_bootstrap_duration = start_time.elapsed();
154
155        let total_fingerprints = entries.len();
156        let mut total_cache_hits = 0;
157        let mut total_execution_duration = Duration::ZERO;
158        let mut total_saved_duration = Duration::ZERO;
159
160        for (key, profile, max_duration) in &entries {
161            writeln!(writer, "Command: {:?}", key.format_short_cmd()).unwrap();
162
163            let mut hits = 0;
164            let mut runs = 0;
165            let mut command_total_duration = Duration::ZERO;
166
167            for trace in &profile.traces {
168                match trace {
169                    ExecutionTrace::CacheHit => {
170                        hits += 1;
171                    }
172                    ExecutionTrace::Executed { duration, .. } => {
173                        runs += 1;
174                        command_total_duration += *duration;
175                    }
176                }
177            }
178
179            total_cache_hits += hits;
180            total_execution_duration += command_total_duration;
181            // This makes sense only in our current setup, where:
182            // - If caching is enabled, we record the timing for the initial execution,
183            //   and all subsequent runs will be cache hits.
184            // - If caching is disabled or unused, there will be no cache hits,
185            //   and we'll record timings for all executions.
186            total_saved_duration += command_total_duration * hits as u32;
187
188            let command_vs_bootstrap = if total_bootstrap_duration > Duration::ZERO {
189                100.0 * command_total_duration.as_secs_f64()
190                    / total_bootstrap_duration.as_secs_f64()
191            } else {
192                0.0
193            };
194
195            let duration_str = match max_duration {
196                Some(d) => format!("{d:.2?}"),
197                None => "-".into(),
198            };
199
200            writeln!(
201                writer,
202                "Summary: {runs} run(s), {hits} hit(s), max_duration={duration_str} total_duration: {command_total_duration:.2?} ({command_vs_bootstrap:.2?}% of total)\n"
203            )
204            .unwrap();
205        }
206
207        let overhead_time = total_bootstrap_duration
208            .checked_sub(total_execution_duration)
209            .unwrap_or(Duration::ZERO);
210
211        writeln!(writer, "\n=== Aggregated Summary ===").unwrap();
212        writeln!(writer, "Total unique commands (fingerprints): {total_fingerprints}").unwrap();
213        writeln!(writer, "Total time spent in command executions: {total_execution_duration:.2?}")
214            .unwrap();
215        writeln!(writer, "Total bootstrap time: {total_bootstrap_duration:.2?}").unwrap();
216        writeln!(writer, "Time spent outside command executions: {overhead_time:.2?}").unwrap();
217        writeln!(writer, "Total cache hits: {total_cache_hits}").unwrap();
218        writeln!(writer, "Estimated time saved due to cache hits: {total_saved_duration:.2?}")
219            .unwrap();
220    }
221}
222
223#[derive(Clone)]
224pub enum ExecutionTrace {
225    CacheHit,
226    Executed { duration: Duration },
227}
228
229/// Wrapper around `std::process::Command`.
230///
231/// By default, the command will exit bootstrap if it fails.
232/// If you want to allow failures, use [allow_failure].
233/// If you want to delay failures until the end of bootstrap, use [delay_failure].
234///
235/// By default, the command will print its stdout/stderr to stdout/stderr of bootstrap ([OutputMode::Print]).
236/// If you want to handle the output programmatically, use [BootstrapCommand::run_capture].
237///
238/// Bootstrap will print a debug log to stdout if the command fails and failure is not allowed.
239///
240/// By default, command executions are cached based on their workdir, program, arguments, and environment variables.
241/// This avoids re-running identical commands unnecessarily, unless caching is explicitly disabled.
242///
243/// [allow_failure]: BootstrapCommand::allow_failure
244/// [delay_failure]: BootstrapCommand::delay_failure
245pub struct BootstrapCommand {
246    command: Command,
247    pub failure_behavior: BehaviorOnFailure,
248    // Run the command even during dry run
249    pub run_in_dry_run: bool,
250    // This field makes sure that each command is executed (or disarmed) before it is dropped,
251    // to avoid forgetting to execute a command.
252    drop_bomb: DropBomb,
253    should_cache: bool,
254}
255
256impl<'a> BootstrapCommand {
257    #[track_caller]
258    pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
259        Command::new(program).into()
260    }
261    pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
262        self.command.arg(arg.as_ref());
263        self
264    }
265
266    /// Cache the command. If it will be executed multiple times with the exact same arguments
267    /// and environment variables in the same bootstrap invocation, the previous result will be
268    /// loaded from memory.
269    pub fn cached(&mut self) -> &mut Self {
270        self.should_cache = true;
271        self
272    }
273
274    pub fn args<I, S>(&mut self, args: I) -> &mut Self
275    where
276        I: IntoIterator<Item = S>,
277        S: AsRef<OsStr>,
278    {
279        self.command.args(args);
280        self
281    }
282
283    pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
284    where
285        K: AsRef<OsStr>,
286        V: AsRef<OsStr>,
287    {
288        self.command.env(key, val);
289        self
290    }
291
292    pub fn get_envs(&self) -> CommandEnvs<'_> {
293        self.command.get_envs()
294    }
295
296    pub fn get_args(&self) -> CommandArgs<'_> {
297        self.command.get_args()
298    }
299
300    pub fn env_remove<K: AsRef<OsStr>>(&mut self, key: K) -> &mut Self {
301        self.command.env_remove(key);
302        self
303    }
304
305    pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
306        self.command.current_dir(dir);
307        self
308    }
309
310    pub fn stdin(&mut self, stdin: std::process::Stdio) -> &mut Self {
311        self.command.stdin(stdin);
312        self
313    }
314
315    #[must_use]
316    pub fn delay_failure(self) -> Self {
317        Self { failure_behavior: BehaviorOnFailure::DelayFail, ..self }
318    }
319
320    pub fn fail_fast(self) -> Self {
321        Self { failure_behavior: BehaviorOnFailure::Exit, ..self }
322    }
323
324    #[must_use]
325    pub fn allow_failure(self) -> Self {
326        Self { failure_behavior: BehaviorOnFailure::Ignore, ..self }
327    }
328
329    pub fn run_in_dry_run(&mut self) -> &mut Self {
330        self.run_in_dry_run = true;
331        self
332    }
333
334    /// Run the command, while printing stdout and stderr.
335    /// Returns true if the command has succeeded.
336    #[track_caller]
337    pub fn run(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> bool {
338        exec_ctx.as_ref().run(self, OutputMode::Print, OutputMode::Print).is_success()
339    }
340
341    /// Run the command, while capturing and returning all its output.
342    #[track_caller]
343    pub fn run_capture(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
344        exec_ctx.as_ref().run(self, OutputMode::Capture, OutputMode::Capture)
345    }
346
347    /// Run the command, while capturing and returning stdout, and printing stderr.
348    #[track_caller]
349    pub fn run_capture_stdout(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
350        exec_ctx.as_ref().run(self, OutputMode::Capture, OutputMode::Print)
351    }
352
353    /// Spawn the command in background, while capturing and returning all its output.
354    #[track_caller]
355    pub fn start_capture(
356        &'a mut self,
357        exec_ctx: impl AsRef<ExecutionContext>,
358    ) -> DeferredCommand<'a> {
359        exec_ctx.as_ref().start(self, OutputMode::Capture, OutputMode::Capture)
360    }
361
362    /// Spawn the command in background, while capturing and returning stdout, and printing stderr.
363    #[track_caller]
364    pub fn start_capture_stdout(
365        &'a mut self,
366        exec_ctx: impl AsRef<ExecutionContext>,
367    ) -> DeferredCommand<'a> {
368        exec_ctx.as_ref().start(self, OutputMode::Capture, OutputMode::Print)
369    }
370
371    /// Spawn the command in background, while capturing and returning stdout, and printing stderr.
372    /// Returns None in dry-mode
373    #[track_caller]
374    pub fn stream_capture_stdout(
375        &'a mut self,
376        exec_ctx: impl AsRef<ExecutionContext>,
377    ) -> Option<StreamingCommand> {
378        exec_ctx.as_ref().stream(self, OutputMode::Capture, OutputMode::Print)
379    }
380
381    /// Mark the command as being executed, disarming the drop bomb.
382    /// If this method is not called before the command is dropped, its drop will panic.
383    pub fn mark_as_executed(&mut self) {
384        self.drop_bomb.defuse();
385    }
386
387    /// Returns the source code location where this command was created.
388    pub fn get_created_location(&self) -> std::panic::Location<'static> {
389        self.drop_bomb.get_created_location()
390    }
391
392    pub fn fingerprint(&self) -> CommandFingerprint {
393        let command = &self.command;
394        CommandFingerprint {
395            program: command.get_program().into(),
396            args: command.get_args().map(OsStr::to_os_string).collect(),
397            envs: command
398                .get_envs()
399                .map(|(k, v)| (k.to_os_string(), v.map(|val| val.to_os_string())))
400                .collect(),
401            cwd: command.get_current_dir().map(Path::to_path_buf),
402        }
403    }
404}
405
406impl Debug for BootstrapCommand {
407    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
408        write!(f, "{:?}", self.command)?;
409        write!(f, " (failure_mode={:?})", self.failure_behavior)
410    }
411}
412
413impl From<Command> for BootstrapCommand {
414    #[track_caller]
415    fn from(command: Command) -> Self {
416        let program = command.get_program().to_owned();
417        Self {
418            should_cache: false,
419            command,
420            failure_behavior: BehaviorOnFailure::Exit,
421            run_in_dry_run: false,
422            drop_bomb: DropBomb::arm(program),
423        }
424    }
425}
426
427/// Represents the current status of `BootstrapCommand`.
428#[derive(Clone, PartialEq)]
429enum CommandStatus {
430    /// The command has started and finished with some status.
431    Finished(ExitStatus),
432    /// It was not even possible to start the command or wait for it to finish.
433    DidNotStartOrFinish,
434}
435
436/// Create a new BootstrapCommand. This is a helper function to make command creation
437/// shorter than `BootstrapCommand::new`.
438#[track_caller]
439#[must_use]
440pub fn command<S: AsRef<OsStr>>(program: S) -> BootstrapCommand {
441    BootstrapCommand::new(program)
442}
443
444/// Represents the output of an executed process.
445#[derive(Clone, PartialEq)]
446pub struct CommandOutput {
447    status: CommandStatus,
448    stdout: Option<Vec<u8>>,
449    stderr: Option<Vec<u8>>,
450}
451
452impl CommandOutput {
453    #[must_use]
454    pub fn not_finished(stdout: OutputMode, stderr: OutputMode) -> Self {
455        Self {
456            status: CommandStatus::DidNotStartOrFinish,
457            stdout: match stdout {
458                OutputMode::Print => None,
459                OutputMode::Capture => Some(vec![]),
460            },
461            stderr: match stderr {
462                OutputMode::Print => None,
463                OutputMode::Capture => Some(vec![]),
464            },
465        }
466    }
467
468    #[must_use]
469    pub fn from_output(output: Output, stdout: OutputMode, stderr: OutputMode) -> Self {
470        Self {
471            status: CommandStatus::Finished(output.status),
472            stdout: match stdout {
473                OutputMode::Print => None,
474                OutputMode::Capture => Some(output.stdout),
475            },
476            stderr: match stderr {
477                OutputMode::Print => None,
478                OutputMode::Capture => Some(output.stderr),
479            },
480        }
481    }
482
483    #[must_use]
484    pub fn is_success(&self) -> bool {
485        match self.status {
486            CommandStatus::Finished(status) => status.success(),
487            CommandStatus::DidNotStartOrFinish => false,
488        }
489    }
490
491    #[must_use]
492    pub fn is_failure(&self) -> bool {
493        !self.is_success()
494    }
495
496    pub fn status(&self) -> Option<ExitStatus> {
497        match self.status {
498            CommandStatus::Finished(status) => Some(status),
499            CommandStatus::DidNotStartOrFinish => None,
500        }
501    }
502
503    #[must_use]
504    pub fn stdout(&self) -> String {
505        String::from_utf8(
506            self.stdout.clone().expect("Accessing stdout of a command that did not capture stdout"),
507        )
508        .expect("Cannot parse process stdout as UTF-8")
509    }
510
511    #[must_use]
512    pub fn stdout_if_present(&self) -> Option<String> {
513        self.stdout.as_ref().and_then(|s| String::from_utf8(s.clone()).ok())
514    }
515
516    #[must_use]
517    pub fn stdout_if_ok(&self) -> Option<String> {
518        if self.is_success() { Some(self.stdout()) } else { None }
519    }
520
521    #[must_use]
522    pub fn stderr(&self) -> String {
523        String::from_utf8(
524            self.stderr.clone().expect("Accessing stderr of a command that did not capture stderr"),
525        )
526        .expect("Cannot parse process stderr as UTF-8")
527    }
528
529    #[must_use]
530    pub fn stderr_if_present(&self) -> Option<String> {
531        self.stderr.as_ref().and_then(|s| String::from_utf8(s.clone()).ok())
532    }
533}
534
535impl Default for CommandOutput {
536    fn default() -> Self {
537        Self {
538            status: CommandStatus::Finished(ExitStatus::default()),
539            stdout: Some(vec![]),
540            stderr: Some(vec![]),
541        }
542    }
543}
544
545#[derive(Clone, Default)]
546pub struct ExecutionContext {
547    dry_run: DryRun,
548    pub verbosity: u8,
549    pub fail_fast: bool,
550    delayed_failures: Arc<Mutex<Vec<String>>>,
551    command_cache: Arc<CommandCache>,
552    profiler: Arc<CommandProfiler>,
553}
554
555#[derive(Default)]
556pub struct CommandCache {
557    cache: Mutex<HashMap<CommandFingerprint, CommandOutput>>,
558}
559
560enum CommandState<'a> {
561    Cached(CommandOutput),
562    Deferred {
563        process: Option<Result<Child, std::io::Error>>,
564        command: &'a mut BootstrapCommand,
565        stdout: OutputMode,
566        stderr: OutputMode,
567        executed_at: &'a Location<'a>,
568        fingerprint: CommandFingerprint,
569        start_time: Instant,
570        #[cfg(feature = "tracing")]
571        _span_guard: tracing::span::EnteredSpan,
572    },
573}
574
575pub struct StreamingCommand {
576    child: Child,
577    pub stdout: Option<ChildStdout>,
578    pub stderr: Option<ChildStderr>,
579    fingerprint: CommandFingerprint,
580    start_time: Instant,
581    #[cfg(feature = "tracing")]
582    _span_guard: tracing::span::EnteredSpan,
583}
584
585#[must_use]
586pub struct DeferredCommand<'a> {
587    state: CommandState<'a>,
588}
589
590impl CommandCache {
591    pub fn get(&self, key: &CommandFingerprint) -> Option<CommandOutput> {
592        self.cache.lock().unwrap().get(key).cloned()
593    }
594
595    pub fn insert(&self, key: CommandFingerprint, output: CommandOutput) {
596        self.cache.lock().unwrap().insert(key, output);
597    }
598}
599
600impl ExecutionContext {
601    pub fn new(verbosity: u8, fail_fast: bool) -> Self {
602        Self { verbosity, fail_fast, ..Default::default() }
603    }
604
605    pub fn dry_run(&self) -> bool {
606        match self.dry_run {
607            DryRun::Disabled => false,
608            DryRun::SelfCheck | DryRun::UserSelected => true,
609        }
610    }
611
612    pub fn profiler(&self) -> &CommandProfiler {
613        &self.profiler
614    }
615
616    pub fn get_dry_run(&self) -> &DryRun {
617        &self.dry_run
618    }
619
620    pub fn do_if_verbose(&self, f: impl Fn()) {
621        if self.is_verbose() {
622            f()
623        }
624    }
625
626    pub fn is_verbose(&self) -> bool {
627        self.verbosity > 0
628    }
629
630    pub fn fail_fast(&self) -> bool {
631        self.fail_fast
632    }
633
634    pub fn set_dry_run(&mut self, value: DryRun) {
635        self.dry_run = value;
636    }
637
638    pub fn set_verbosity(&mut self, value: u8) {
639        self.verbosity = value;
640    }
641
642    pub fn set_fail_fast(&mut self, value: bool) {
643        self.fail_fast = value;
644    }
645
646    pub fn add_to_delay_failure(&self, message: String) {
647        self.delayed_failures.lock().unwrap().push(message);
648    }
649
650    pub fn report_failures_and_exit(&self) {
651        let failures = self.delayed_failures.lock().unwrap();
652        if failures.is_empty() {
653            return;
654        }
655        eprintln!("\n{} command(s) did not execute successfully:\n", failures.len());
656        for failure in &*failures {
657            eprintln!("  - {failure}");
658        }
659        exit!(1);
660    }
661
662    /// Execute a command and return its output.
663    /// Note: Ideally, you should use one of the BootstrapCommand::run* functions to
664    /// execute commands. They internally call this method.
665    #[track_caller]
666    pub fn start<'a>(
667        &self,
668        command: &'a mut BootstrapCommand,
669        stdout: OutputMode,
670        stderr: OutputMode,
671    ) -> DeferredCommand<'a> {
672        let fingerprint = command.fingerprint();
673
674        if let Some(cached_output) = self.command_cache.get(&fingerprint) {
675            command.mark_as_executed();
676            self.do_if_verbose(|| println!("Cache hit: {command:?}"));
677            self.profiler.record_cache_hit(fingerprint);
678            return DeferredCommand { state: CommandState::Cached(cached_output) };
679        }
680
681        #[cfg(feature = "tracing")]
682        let span_guard = crate::utils::tracing::trace_cmd(command);
683
684        let created_at = command.get_created_location();
685        let executed_at = std::panic::Location::caller();
686
687        if self.dry_run() && !command.run_in_dry_run {
688            return DeferredCommand {
689                state: CommandState::Deferred {
690                    process: None,
691                    command,
692                    stdout,
693                    stderr,
694                    executed_at,
695                    fingerprint,
696                    start_time: Instant::now(),
697                    #[cfg(feature = "tracing")]
698                    _span_guard: span_guard,
699                },
700            };
701        }
702
703        self.do_if_verbose(|| {
704            println!("running: {command:?} (created at {created_at}, executed at {executed_at})")
705        });
706
707        let cmd = &mut command.command;
708        cmd.stdout(stdout.stdio());
709        cmd.stderr(stderr.stdio());
710
711        let start_time = Instant::now();
712
713        let child = cmd.spawn();
714
715        DeferredCommand {
716            state: CommandState::Deferred {
717                process: Some(child),
718                command,
719                stdout,
720                stderr,
721                executed_at,
722                fingerprint,
723                start_time,
724                #[cfg(feature = "tracing")]
725                _span_guard: span_guard,
726            },
727        }
728    }
729
730    /// Execute a command and return its output.
731    /// Note: Ideally, you should use one of the BootstrapCommand::run* functions to
732    /// execute commands. They internally call this method.
733    #[track_caller]
734    pub fn run(
735        &self,
736        command: &mut BootstrapCommand,
737        stdout: OutputMode,
738        stderr: OutputMode,
739    ) -> CommandOutput {
740        self.start(command, stdout, stderr).wait_for_output(self)
741    }
742
743    fn fail(&self, message: &str) -> ! {
744        println!("{message}");
745
746        if !self.is_verbose() {
747            println!("Command has failed. Rerun with -v to see more details.");
748        }
749        exit!(1);
750    }
751
752    /// Spawns the command with configured stdout and stderr handling.
753    ///
754    /// Returns None if in dry-run mode or Panics if the command fails to spawn.
755    pub fn stream(
756        &self,
757        command: &mut BootstrapCommand,
758        stdout: OutputMode,
759        stderr: OutputMode,
760    ) -> Option<StreamingCommand> {
761        command.mark_as_executed();
762        if !command.run_in_dry_run && self.dry_run() {
763            return None;
764        }
765
766        #[cfg(feature = "tracing")]
767        let span_guard = crate::utils::tracing::trace_cmd(command);
768
769        let start_time = Instant::now();
770        let fingerprint = command.fingerprint();
771        let cmd = &mut command.command;
772        cmd.stdout(stdout.stdio());
773        cmd.stderr(stderr.stdio());
774        let child = cmd.spawn();
775        let mut child = match child {
776            Ok(child) => child,
777            Err(e) => panic!("failed to execute command: {cmd:?}\nERROR: {e}"),
778        };
779
780        let stdout = child.stdout.take();
781        let stderr = child.stderr.take();
782        Some(StreamingCommand {
783            child,
784            stdout,
785            stderr,
786            fingerprint,
787            start_time,
788            #[cfg(feature = "tracing")]
789            _span_guard: span_guard,
790        })
791    }
792}
793
794impl AsRef<ExecutionContext> for ExecutionContext {
795    fn as_ref(&self) -> &ExecutionContext {
796        self
797    }
798}
799
800impl StreamingCommand {
801    pub fn wait(
802        mut self,
803        exec_ctx: impl AsRef<ExecutionContext>,
804    ) -> Result<ExitStatus, std::io::Error> {
805        let exec_ctx = exec_ctx.as_ref();
806        let output = self.child.wait();
807        exec_ctx.profiler().record_execution(self.fingerprint, self.start_time);
808        output
809    }
810}
811
812impl<'a> DeferredCommand<'a> {
813    pub fn wait_for_output(self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
814        match self.state {
815            CommandState::Cached(output) => output,
816            CommandState::Deferred {
817                process,
818                command,
819                stdout,
820                stderr,
821                executed_at,
822                fingerprint,
823                start_time,
824                #[cfg(feature = "tracing")]
825                _span_guard,
826            } => {
827                let exec_ctx = exec_ctx.as_ref();
828
829                let output =
830                    Self::finish_process(process, command, stdout, stderr, executed_at, exec_ctx);
831
832                #[cfg(feature = "tracing")]
833                drop(_span_guard);
834
835                if (!exec_ctx.dry_run() || command.run_in_dry_run)
836                    && output.status().is_some()
837                    && command.should_cache
838                {
839                    exec_ctx.command_cache.insert(fingerprint.clone(), output.clone());
840                    exec_ctx.profiler.record_execution(fingerprint, start_time);
841                }
842
843                output
844            }
845        }
846    }
847
848    pub fn finish_process(
849        mut process: Option<Result<Child, std::io::Error>>,
850        command: &mut BootstrapCommand,
851        stdout: OutputMode,
852        stderr: OutputMode,
853        executed_at: &'a std::panic::Location<'a>,
854        exec_ctx: &ExecutionContext,
855    ) -> CommandOutput {
856        use std::fmt::Write;
857
858        command.mark_as_executed();
859
860        let process = match process.take() {
861            Some(p) => p,
862            None => return CommandOutput::default(),
863        };
864
865        let created_at = command.get_created_location();
866
867        #[allow(clippy::enum_variant_names)]
868        enum FailureReason {
869            FailedAtRuntime(ExitStatus),
870            FailedToFinish(std::io::Error),
871            FailedToStart(std::io::Error),
872        }
873
874        let (output, fail_reason) = match process {
875            Ok(child) => match child.wait_with_output() {
876                Ok(output) if output.status.success() => {
877                    // Successful execution
878                    (CommandOutput::from_output(output, stdout, stderr), None)
879                }
880                Ok(output) => {
881                    // Command started, but then it failed
882                    let status = output.status;
883                    (
884                        CommandOutput::from_output(output, stdout, stderr),
885                        Some(FailureReason::FailedAtRuntime(status)),
886                    )
887                }
888                Err(e) => {
889                    // Failed to wait for output
890                    (
891                        CommandOutput::not_finished(stdout, stderr),
892                        Some(FailureReason::FailedToFinish(e)),
893                    )
894                }
895            },
896            Err(e) => {
897                // Failed to spawn the command
898                (CommandOutput::not_finished(stdout, stderr), Some(FailureReason::FailedToStart(e)))
899            }
900        };
901
902        if let Some(fail_reason) = fail_reason {
903            let mut error_message = String::new();
904            let command_str = if exec_ctx.is_verbose() {
905                format!("{command:?}")
906            } else {
907                command.fingerprint().format_short_cmd()
908            };
909            let action = match fail_reason {
910                FailureReason::FailedAtRuntime(e) => {
911                    format!("failed with exit code {}", e.code().unwrap_or(1))
912                }
913                FailureReason::FailedToFinish(e) => {
914                    format!("failed to finish: {e:?}")
915                }
916                FailureReason::FailedToStart(e) => {
917                    format!("failed to start: {e:?}")
918                }
919            };
920            writeln!(
921                error_message,
922                r#"Command `{command_str}` {action}
923Created at: {created_at}
924Executed at: {executed_at}"#,
925            )
926            .unwrap();
927            if stdout.captures() {
928                writeln!(error_message, "\n--- STDOUT vvv\n{}", output.stdout().trim()).unwrap();
929            }
930            if stderr.captures() {
931                writeln!(error_message, "\n--- STDERR vvv\n{}", output.stderr().trim()).unwrap();
932            }
933
934            match command.failure_behavior {
935                BehaviorOnFailure::DelayFail => {
936                    if exec_ctx.fail_fast {
937                        exec_ctx.fail(&error_message);
938                    }
939                    exec_ctx.add_to_delay_failure(error_message);
940                }
941                BehaviorOnFailure::Exit => {
942                    exec_ctx.fail(&error_message);
943                }
944                BehaviorOnFailure::Ignore => {
945                    // If failures are allowed, either the error has been printed already
946                    // (OutputMode::Print) or the user used a capture output mode and wants to
947                    // handle the error output on their own.
948                }
949            }
950        }
951
952        output
953    }
954}