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