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