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