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    /// Cache the command. If it will be executed multiple times with the exact same arguments
268    /// and environment variables in the same bootstrap invocation, the previous result will be
269    /// loaded from memory.
270    pub fn cached(&mut self) -> &mut Self {
271        self.should_cache = true;
272        self
273    }
274
275    pub fn args<I, S>(&mut self, args: I) -> &mut Self
276    where
277        I: IntoIterator<Item = S>,
278        S: AsRef<OsStr>,
279    {
280        self.command.args(args);
281        self
282    }
283
284    pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
285    where
286        K: AsRef<OsStr>,
287        V: AsRef<OsStr>,
288    {
289        self.command.env(key, val);
290        self
291    }
292
293    pub fn get_envs(&self) -> CommandEnvs<'_> {
294        self.command.get_envs()
295    }
296
297    pub fn get_args(&self) -> CommandArgs<'_> {
298        self.command.get_args()
299    }
300
301    pub fn env_remove<K: AsRef<OsStr>>(&mut self, key: K) -> &mut Self {
302        self.command.env_remove(key);
303        self
304    }
305
306    pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
307        self.command.current_dir(dir);
308        self
309    }
310
311    pub fn stdin(&mut self, stdin: std::process::Stdio) -> &mut Self {
312        self.command.stdin(stdin);
313        self
314    }
315
316    #[must_use]
317    pub fn delay_failure(self) -> Self {
318        Self { failure_behavior: BehaviorOnFailure::DelayFail, ..self }
319    }
320
321    pub fn fail_fast(self) -> Self {
322        Self { failure_behavior: BehaviorOnFailure::Exit, ..self }
323    }
324
325    #[must_use]
326    pub fn allow_failure(self) -> Self {
327        Self { failure_behavior: BehaviorOnFailure::Ignore, ..self }
328    }
329
330    pub fn run_in_dry_run(&mut self) -> &mut Self {
331        self.run_in_dry_run = true;
332        self
333    }
334
335    /// Run the command, while printing stdout and stderr.
336    /// Returns true if the command has succeeded.
337    #[track_caller]
338    pub fn run(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> bool {
339        exec_ctx.as_ref().run(self, OutputMode::Print, OutputMode::Print).is_success()
340    }
341
342    /// Run the command, while capturing and returning all its output.
343    #[track_caller]
344    pub fn run_capture(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
345        exec_ctx.as_ref().run(self, OutputMode::Capture, OutputMode::Capture)
346    }
347
348    /// Run the command, while capturing and returning stdout, and printing stderr.
349    #[track_caller]
350    pub fn run_capture_stdout(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
351        exec_ctx.as_ref().run(self, OutputMode::Capture, OutputMode::Print)
352    }
353
354    /// Spawn the command in background, while capturing and returning all its output.
355    #[track_caller]
356    pub fn start_capture(
357        &'a mut self,
358        exec_ctx: impl AsRef<ExecutionContext>,
359    ) -> DeferredCommand<'a> {
360        exec_ctx.as_ref().start(self, OutputMode::Capture, OutputMode::Capture)
361    }
362
363    /// Spawn the command in background, while capturing and returning stdout, and printing stderr.
364    #[track_caller]
365    pub fn start_capture_stdout(
366        &'a mut self,
367        exec_ctx: impl AsRef<ExecutionContext>,
368    ) -> DeferredCommand<'a> {
369        exec_ctx.as_ref().start(self, OutputMode::Capture, OutputMode::Print)
370    }
371
372    /// Spawn the command in background, while capturing and returning stdout, and printing stderr.
373    /// Returns None in dry-mode
374    #[track_caller]
375    pub fn stream_capture_stdout(
376        &'a mut self,
377        exec_ctx: impl AsRef<ExecutionContext>,
378    ) -> Option<StreamingCommand> {
379        exec_ctx.as_ref().stream(self, OutputMode::Capture, OutputMode::Print)
380    }
381
382    /// Mark the command as being executed, disarming the drop bomb.
383    /// If this method is not called before the command is dropped, its drop will panic.
384    pub fn mark_as_executed(&mut self) {
385        self.drop_bomb.defuse();
386    }
387
388    /// Returns the source code location where this command was created.
389    pub fn get_created_location(&self) -> std::panic::Location<'static> {
390        self.drop_bomb.get_created_location()
391    }
392
393    /// If in a CI environment, forces the command to run with colors.
394    pub fn force_coloring_in_ci(&mut self) {
395        if CiEnv::is_ci() {
396            // Due to use of stamp/docker, the output stream of bootstrap is not
397            // a TTY in CI, so coloring is by-default turned off.
398            // The explicit `TERM=xterm` environment is needed for
399            // `--color always` to actually work. This env var was lost when
400            // compiling through the Makefile. Very strange.
401            self.env("TERM", "xterm").args(["--color", "always"]);
402        }
403    }
404
405    pub fn fingerprint(&self) -> CommandFingerprint {
406        let command = &self.command;
407        CommandFingerprint {
408            program: command.get_program().into(),
409            args: command.get_args().map(OsStr::to_os_string).collect(),
410            envs: command
411                .get_envs()
412                .map(|(k, v)| (k.to_os_string(), v.map(|val| val.to_os_string())))
413                .collect(),
414            cwd: command.get_current_dir().map(Path::to_path_buf),
415        }
416    }
417}
418
419impl Debug for BootstrapCommand {
420    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
421        write!(f, "{:?}", self.command)?;
422        write!(f, " (failure_mode={:?})", self.failure_behavior)
423    }
424}
425
426impl From<Command> for BootstrapCommand {
427    #[track_caller]
428    fn from(command: Command) -> Self {
429        let program = command.get_program().to_owned();
430        Self {
431            should_cache: false,
432            command,
433            failure_behavior: BehaviorOnFailure::Exit,
434            run_in_dry_run: false,
435            drop_bomb: DropBomb::arm(program),
436        }
437    }
438}
439
440/// Represents the current status of `BootstrapCommand`.
441#[derive(Clone, PartialEq)]
442enum CommandStatus {
443    /// The command has started and finished with some status.
444    Finished(ExitStatus),
445    /// It was not even possible to start the command or wait for it to finish.
446    DidNotStartOrFinish,
447}
448
449/// Create a new BootstrapCommand. This is a helper function to make command creation
450/// shorter than `BootstrapCommand::new`.
451#[track_caller]
452#[must_use]
453pub fn command<S: AsRef<OsStr>>(program: S) -> BootstrapCommand {
454    BootstrapCommand::new(program)
455}
456
457/// Represents the output of an executed process.
458#[derive(Clone, PartialEq)]
459pub struct CommandOutput {
460    status: CommandStatus,
461    stdout: Option<Vec<u8>>,
462    stderr: Option<Vec<u8>>,
463}
464
465impl CommandOutput {
466    #[must_use]
467    pub fn not_finished(stdout: OutputMode, stderr: OutputMode) -> Self {
468        Self {
469            status: CommandStatus::DidNotStartOrFinish,
470            stdout: match stdout {
471                OutputMode::Print => None,
472                OutputMode::Capture => Some(vec![]),
473            },
474            stderr: match stderr {
475                OutputMode::Print => None,
476                OutputMode::Capture => Some(vec![]),
477            },
478        }
479    }
480
481    #[must_use]
482    pub fn from_output(output: Output, stdout: OutputMode, stderr: OutputMode) -> Self {
483        Self {
484            status: CommandStatus::Finished(output.status),
485            stdout: match stdout {
486                OutputMode::Print => None,
487                OutputMode::Capture => Some(output.stdout),
488            },
489            stderr: match stderr {
490                OutputMode::Print => None,
491                OutputMode::Capture => Some(output.stderr),
492            },
493        }
494    }
495
496    #[must_use]
497    pub fn is_success(&self) -> bool {
498        match self.status {
499            CommandStatus::Finished(status) => status.success(),
500            CommandStatus::DidNotStartOrFinish => false,
501        }
502    }
503
504    #[must_use]
505    pub fn is_failure(&self) -> bool {
506        !self.is_success()
507    }
508
509    pub fn status(&self) -> Option<ExitStatus> {
510        match self.status {
511            CommandStatus::Finished(status) => Some(status),
512            CommandStatus::DidNotStartOrFinish => None,
513        }
514    }
515
516    #[must_use]
517    pub fn stdout(&self) -> String {
518        String::from_utf8(
519            self.stdout.clone().expect("Accessing stdout of a command that did not capture stdout"),
520        )
521        .expect("Cannot parse process stdout as UTF-8")
522    }
523
524    #[must_use]
525    pub fn stdout_if_present(&self) -> Option<String> {
526        self.stdout.as_ref().and_then(|s| String::from_utf8(s.clone()).ok())
527    }
528
529    #[must_use]
530    pub fn stdout_if_ok(&self) -> Option<String> {
531        if self.is_success() { Some(self.stdout()) } else { None }
532    }
533
534    #[must_use]
535    pub fn stderr(&self) -> String {
536        String::from_utf8(
537            self.stderr.clone().expect("Accessing stderr of a command that did not capture stderr"),
538        )
539        .expect("Cannot parse process stderr as UTF-8")
540    }
541
542    #[must_use]
543    pub fn stderr_if_present(&self) -> Option<String> {
544        self.stderr.as_ref().and_then(|s| String::from_utf8(s.clone()).ok())
545    }
546}
547
548impl Default for CommandOutput {
549    fn default() -> Self {
550        Self {
551            status: CommandStatus::Finished(ExitStatus::default()),
552            stdout: Some(vec![]),
553            stderr: Some(vec![]),
554        }
555    }
556}
557
558#[derive(Clone, Default)]
559pub struct ExecutionContext {
560    dry_run: DryRun,
561    pub verbosity: u8,
562    pub fail_fast: bool,
563    delayed_failures: Arc<Mutex<Vec<String>>>,
564    command_cache: Arc<CommandCache>,
565    profiler: Arc<CommandProfiler>,
566}
567
568#[derive(Default)]
569pub struct CommandCache {
570    cache: Mutex<HashMap<CommandFingerprint, CommandOutput>>,
571}
572
573enum CommandState<'a> {
574    Cached(CommandOutput),
575    Deferred {
576        process: Option<Result<Child, std::io::Error>>,
577        command: &'a mut BootstrapCommand,
578        stdout: OutputMode,
579        stderr: OutputMode,
580        executed_at: &'a Location<'a>,
581        fingerprint: CommandFingerprint,
582        start_time: Instant,
583        #[cfg(feature = "tracing")]
584        _span_guard: tracing::span::EnteredSpan,
585    },
586}
587
588pub struct StreamingCommand {
589    child: Child,
590    pub stdout: Option<ChildStdout>,
591    pub stderr: Option<ChildStderr>,
592    fingerprint: CommandFingerprint,
593    start_time: Instant,
594    #[cfg(feature = "tracing")]
595    _span_guard: tracing::span::EnteredSpan,
596}
597
598#[must_use]
599pub struct DeferredCommand<'a> {
600    state: CommandState<'a>,
601}
602
603impl CommandCache {
604    pub fn get(&self, key: &CommandFingerprint) -> Option<CommandOutput> {
605        self.cache.lock().unwrap().get(key).cloned()
606    }
607
608    pub fn insert(&self, key: CommandFingerprint, output: CommandOutput) {
609        self.cache.lock().unwrap().insert(key, output);
610    }
611}
612
613impl ExecutionContext {
614    pub fn new(verbosity: u8, fail_fast: bool) -> Self {
615        Self { verbosity, fail_fast, ..Default::default() }
616    }
617
618    pub fn dry_run(&self) -> bool {
619        match self.dry_run {
620            DryRun::Disabled => false,
621            DryRun::SelfCheck | DryRun::UserSelected => true,
622        }
623    }
624
625    pub fn profiler(&self) -> &CommandProfiler {
626        &self.profiler
627    }
628
629    pub fn get_dry_run(&self) -> &DryRun {
630        &self.dry_run
631    }
632
633    pub fn verbose(&self, f: impl Fn()) {
634        if self.is_verbose() {
635            f()
636        }
637    }
638
639    pub fn is_verbose(&self) -> bool {
640        self.verbosity > 0
641    }
642
643    pub fn fail_fast(&self) -> bool {
644        self.fail_fast
645    }
646
647    pub fn set_dry_run(&mut self, value: DryRun) {
648        self.dry_run = value;
649    }
650
651    pub fn set_verbosity(&mut self, value: u8) {
652        self.verbosity = value;
653    }
654
655    pub fn set_fail_fast(&mut self, value: bool) {
656        self.fail_fast = value;
657    }
658
659    pub fn add_to_delay_failure(&self, message: String) {
660        self.delayed_failures.lock().unwrap().push(message);
661    }
662
663    pub fn report_failures_and_exit(&self) {
664        let failures = self.delayed_failures.lock().unwrap();
665        if failures.is_empty() {
666            return;
667        }
668        eprintln!("\n{} command(s) did not execute successfully:\n", failures.len());
669        for failure in &*failures {
670            eprintln!("  - {failure}");
671        }
672        exit!(1);
673    }
674
675    /// Execute a command and return its output.
676    /// Note: Ideally, you should use one of the BootstrapCommand::run* functions to
677    /// execute commands. They internally call this method.
678    #[track_caller]
679    pub fn start<'a>(
680        &self,
681        command: &'a mut BootstrapCommand,
682        stdout: OutputMode,
683        stderr: OutputMode,
684    ) -> DeferredCommand<'a> {
685        let fingerprint = command.fingerprint();
686
687        if let Some(cached_output) = self.command_cache.get(&fingerprint) {
688            command.mark_as_executed();
689            self.verbose(|| println!("Cache hit: {command:?}"));
690            self.profiler.record_cache_hit(fingerprint);
691            return DeferredCommand { state: CommandState::Cached(cached_output) };
692        }
693
694        #[cfg(feature = "tracing")]
695        let span_guard = crate::utils::tracing::trace_cmd(command);
696
697        let created_at = command.get_created_location();
698        let executed_at = std::panic::Location::caller();
699
700        if self.dry_run() && !command.run_in_dry_run {
701            return DeferredCommand {
702                state: CommandState::Deferred {
703                    process: None,
704                    command,
705                    stdout,
706                    stderr,
707                    executed_at,
708                    fingerprint,
709                    start_time: Instant::now(),
710                    #[cfg(feature = "tracing")]
711                    _span_guard: span_guard,
712                },
713            };
714        }
715
716        self.verbose(|| {
717            println!("running: {command:?} (created at {created_at}, executed at {executed_at})")
718        });
719
720        let cmd = &mut command.command;
721        cmd.stdout(stdout.stdio());
722        cmd.stderr(stderr.stdio());
723
724        let start_time = Instant::now();
725
726        let child = cmd.spawn();
727
728        DeferredCommand {
729            state: CommandState::Deferred {
730                process: Some(child),
731                command,
732                stdout,
733                stderr,
734                executed_at,
735                fingerprint,
736                start_time,
737                #[cfg(feature = "tracing")]
738                _span_guard: span_guard,
739            },
740        }
741    }
742
743    /// Execute a command and return its output.
744    /// Note: Ideally, you should use one of the BootstrapCommand::run* functions to
745    /// execute commands. They internally call this method.
746    #[track_caller]
747    pub fn run(
748        &self,
749        command: &mut BootstrapCommand,
750        stdout: OutputMode,
751        stderr: OutputMode,
752    ) -> CommandOutput {
753        self.start(command, stdout, stderr).wait_for_output(self)
754    }
755
756    fn fail(&self, message: &str) -> ! {
757        println!("{message}");
758
759        if !self.is_verbose() {
760            println!("Command has failed. Rerun with -v to see more details.");
761        }
762        exit!(1);
763    }
764
765    /// Spawns the command with configured stdout and stderr handling.
766    ///
767    /// Returns None if in dry-run mode or Panics if the command fails to spawn.
768    pub fn stream(
769        &self,
770        command: &mut BootstrapCommand,
771        stdout: OutputMode,
772        stderr: OutputMode,
773    ) -> Option<StreamingCommand> {
774        command.mark_as_executed();
775        if !command.run_in_dry_run && self.dry_run() {
776            return None;
777        }
778
779        #[cfg(feature = "tracing")]
780        let span_guard = crate::utils::tracing::trace_cmd(command);
781
782        let start_time = Instant::now();
783        let fingerprint = command.fingerprint();
784        let cmd = &mut command.command;
785        cmd.stdout(stdout.stdio());
786        cmd.stderr(stderr.stdio());
787        let child = cmd.spawn();
788        let mut child = match child {
789            Ok(child) => child,
790            Err(e) => panic!("failed to execute command: {cmd:?}\nERROR: {e}"),
791        };
792
793        let stdout = child.stdout.take();
794        let stderr = child.stderr.take();
795        Some(StreamingCommand {
796            child,
797            stdout,
798            stderr,
799            fingerprint,
800            start_time,
801            #[cfg(feature = "tracing")]
802            _span_guard: span_guard,
803        })
804    }
805}
806
807impl AsRef<ExecutionContext> for ExecutionContext {
808    fn as_ref(&self) -> &ExecutionContext {
809        self
810    }
811}
812
813impl StreamingCommand {
814    pub fn wait(
815        mut self,
816        exec_ctx: impl AsRef<ExecutionContext>,
817    ) -> Result<ExitStatus, std::io::Error> {
818        let exec_ctx = exec_ctx.as_ref();
819        let output = self.child.wait();
820        exec_ctx.profiler().record_execution(self.fingerprint, self.start_time);
821        output
822    }
823}
824
825impl<'a> DeferredCommand<'a> {
826    pub fn wait_for_output(self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
827        match self.state {
828            CommandState::Cached(output) => output,
829            CommandState::Deferred {
830                process,
831                command,
832                stdout,
833                stderr,
834                executed_at,
835                fingerprint,
836                start_time,
837                #[cfg(feature = "tracing")]
838                _span_guard,
839            } => {
840                let exec_ctx = exec_ctx.as_ref();
841
842                let output =
843                    Self::finish_process(process, command, stdout, stderr, executed_at, exec_ctx);
844
845                #[cfg(feature = "tracing")]
846                drop(_span_guard);
847
848                if (!exec_ctx.dry_run() || command.run_in_dry_run)
849                    && output.status().is_some()
850                    && command.should_cache
851                {
852                    exec_ctx.command_cache.insert(fingerprint.clone(), output.clone());
853                    exec_ctx.profiler.record_execution(fingerprint, start_time);
854                }
855
856                output
857            }
858        }
859    }
860
861    pub fn finish_process(
862        mut process: Option<Result<Child, std::io::Error>>,
863        command: &mut BootstrapCommand,
864        stdout: OutputMode,
865        stderr: OutputMode,
866        executed_at: &'a std::panic::Location<'a>,
867        exec_ctx: &ExecutionContext,
868    ) -> CommandOutput {
869        use std::fmt::Write;
870
871        command.mark_as_executed();
872
873        let process = match process.take() {
874            Some(p) => p,
875            None => return CommandOutput::default(),
876        };
877
878        let created_at = command.get_created_location();
879
880        #[allow(clippy::enum_variant_names)]
881        enum FailureReason {
882            FailedAtRuntime(ExitStatus),
883            FailedToFinish(std::io::Error),
884            FailedToStart(std::io::Error),
885        }
886
887        let (output, fail_reason) = match process {
888            Ok(child) => match child.wait_with_output() {
889                Ok(output) if output.status.success() => {
890                    // Successful execution
891                    (CommandOutput::from_output(output, stdout, stderr), None)
892                }
893                Ok(output) => {
894                    // Command started, but then it failed
895                    let status = output.status;
896                    (
897                        CommandOutput::from_output(output, stdout, stderr),
898                        Some(FailureReason::FailedAtRuntime(status)),
899                    )
900                }
901                Err(e) => {
902                    // Failed to wait for output
903                    (
904                        CommandOutput::not_finished(stdout, stderr),
905                        Some(FailureReason::FailedToFinish(e)),
906                    )
907                }
908            },
909            Err(e) => {
910                // Failed to spawn the command
911                (CommandOutput::not_finished(stdout, stderr), Some(FailureReason::FailedToStart(e)))
912            }
913        };
914
915        if let Some(fail_reason) = fail_reason {
916            let mut error_message = String::new();
917            let command_str = if exec_ctx.is_verbose() {
918                format!("{command:?}")
919            } else {
920                command.fingerprint().format_short_cmd()
921            };
922            let action = match fail_reason {
923                FailureReason::FailedAtRuntime(e) => {
924                    format!("failed with exit code {}", e.code().unwrap_or(1))
925                }
926                FailureReason::FailedToFinish(e) => {
927                    format!("failed to finish: {e:?}")
928                }
929                FailureReason::FailedToStart(e) => {
930                    format!("failed to start: {e:?}")
931                }
932            };
933            writeln!(
934                error_message,
935                r#"Command `{command_str}` {action}
936Created at: {created_at}
937Executed at: {executed_at}"#,
938            )
939            .unwrap();
940            if stdout.captures() {
941                writeln!(error_message, "\n--- STDOUT vvv\n{}", output.stdout().trim()).unwrap();
942            }
943            if stderr.captures() {
944                writeln!(error_message, "\n--- STDERR vvv\n{}", output.stderr().trim()).unwrap();
945            }
946
947            match command.failure_behavior {
948                BehaviorOnFailure::DelayFail => {
949                    if exec_ctx.fail_fast {
950                        exec_ctx.fail(&error_message);
951                    }
952                    exec_ctx.add_to_delay_failure(error_message);
953                }
954                BehaviorOnFailure::Exit => {
955                    exec_ctx.fail(&error_message);
956                }
957                BehaviorOnFailure::Ignore => {
958                    // If failures are allowed, either the error has been printed already
959                    // (OutputMode::Print) or the user used a capture output mode and wants to
960                    // handle the error output on their own.
961                }
962            }
963        }
964
965        output
966    }
967}