Skip to main content

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