bootstrap/utils/
exec.rs

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