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