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::hash::Hash;
14use std::panic::Location;
15use std::path::Path;
16use std::process::{
17    Child, ChildStderr, ChildStdout, Command, CommandArgs, CommandEnvs, ExitStatus, Output, Stdio,
18};
19use std::sync::{Arc, Mutex};
20
21use build_helper::ci::CiEnv;
22use build_helper::drop_bomb::DropBomb;
23use build_helper::exit;
24
25use crate::PathBuf;
26use crate::core::config::DryRun;
27#[cfg(feature = "tracing")]
28use crate::trace_cmd;
29
30/// What should be done when the command fails.
31#[derive(Debug, Copy, Clone)]
32pub 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 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 fn captures(&self) -> bool {
53        match self {
54            OutputMode::Print => false,
55            OutputMode::Capture => true,
56        }
57    }
58
59    pub 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 struct CommandCacheKey {
69    program: OsString,
70    args: Vec<OsString>,
71    envs: Vec<(OsString, Option<OsString>)>,
72    cwd: Option<PathBuf>,
73}
74
75/// Wrapper around `std::process::Command`.
76///
77/// By default, the command will exit bootstrap if it fails.
78/// If you want to allow failures, use [allow_failure].
79/// If you want to delay failures until the end of bootstrap, use [delay_failure].
80///
81/// By default, the command will print its stdout/stderr to stdout/stderr of bootstrap ([OutputMode::Print]).
82/// If you want to handle the output programmatically, use [BootstrapCommand::run_capture].
83///
84/// Bootstrap will print a debug log to stdout if the command fails and failure is not allowed.
85///
86/// By default, command executions are cached based on their workdir, program, arguments, and environment variables.
87/// This avoids re-running identical commands unnecessarily, unless caching is explicitly disabled.
88///
89/// [allow_failure]: BootstrapCommand::allow_failure
90/// [delay_failure]: BootstrapCommand::delay_failure
91pub struct BootstrapCommand {
92    command: Command,
93    pub failure_behavior: BehaviorOnFailure,
94    // Run the command even during dry run
95    pub run_in_dry_run: bool,
96    // This field makes sure that each command is executed (or disarmed) before it is dropped,
97    // to avoid forgetting to execute a command.
98    drop_bomb: DropBomb,
99    should_cache: bool,
100}
101
102impl<'a> BootstrapCommand {
103    #[track_caller]
104    pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
105        Command::new(program).into()
106    }
107    pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
108        self.command.arg(arg.as_ref());
109        self
110    }
111
112    pub fn do_not_cache(&mut self) -> &mut Self {
113        self.should_cache = false;
114        self
115    }
116
117    pub fn args<I, S>(&mut self, args: I) -> &mut Self
118    where
119        I: IntoIterator<Item = S>,
120        S: AsRef<OsStr>,
121    {
122        self.command.args(args);
123        self
124    }
125
126    pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
127    where
128        K: AsRef<OsStr>,
129        V: AsRef<OsStr>,
130    {
131        self.command.env(key, val);
132        self
133    }
134
135    pub fn get_envs(&self) -> CommandEnvs<'_> {
136        self.command.get_envs()
137    }
138
139    pub fn get_args(&self) -> CommandArgs<'_> {
140        self.command.get_args()
141    }
142
143    pub fn env_remove<K: AsRef<OsStr>>(&mut self, key: K) -> &mut Self {
144        self.command.env_remove(key);
145        self
146    }
147
148    pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
149        self.command.current_dir(dir);
150        self
151    }
152
153    pub fn stdin(&mut self, stdin: std::process::Stdio) -> &mut Self {
154        self.command.stdin(stdin);
155        self
156    }
157
158    #[must_use]
159    pub fn delay_failure(self) -> Self {
160        Self { failure_behavior: BehaviorOnFailure::DelayFail, ..self }
161    }
162
163    pub fn fail_fast(self) -> Self {
164        Self { failure_behavior: BehaviorOnFailure::Exit, ..self }
165    }
166
167    #[must_use]
168    pub fn allow_failure(self) -> Self {
169        Self { failure_behavior: BehaviorOnFailure::Ignore, ..self }
170    }
171
172    pub fn run_in_dry_run(&mut self) -> &mut Self {
173        self.run_in_dry_run = true;
174        self
175    }
176
177    /// Run the command, while printing stdout and stderr.
178    /// Returns true if the command has succeeded.
179    #[track_caller]
180    pub fn run(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> bool {
181        exec_ctx.as_ref().run(self, OutputMode::Print, OutputMode::Print).is_success()
182    }
183
184    /// Run the command, while capturing and returning all its output.
185    #[track_caller]
186    pub fn run_capture(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
187        exec_ctx.as_ref().run(self, OutputMode::Capture, OutputMode::Capture)
188    }
189
190    /// Run the command, while capturing and returning stdout, and printing stderr.
191    #[track_caller]
192    pub fn run_capture_stdout(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
193        exec_ctx.as_ref().run(self, OutputMode::Capture, OutputMode::Print)
194    }
195
196    /// Spawn the command in background, while capturing and returning all its output.
197    #[track_caller]
198    pub fn start_capture(
199        &'a mut self,
200        exec_ctx: impl AsRef<ExecutionContext>,
201    ) -> DeferredCommand<'a> {
202        exec_ctx.as_ref().start(self, OutputMode::Capture, OutputMode::Capture)
203    }
204
205    /// Spawn the command in background, while capturing and returning stdout, and printing stderr.
206    #[track_caller]
207    pub fn start_capture_stdout(
208        &'a mut self,
209        exec_ctx: impl AsRef<ExecutionContext>,
210    ) -> DeferredCommand<'a> {
211        exec_ctx.as_ref().start(self, OutputMode::Capture, OutputMode::Print)
212    }
213
214    /// Spawn the command in background, while capturing and returning stdout, and printing stderr.
215    /// Returns None in dry-mode
216    #[track_caller]
217    pub fn stream_capture_stdout(
218        &'a mut self,
219        exec_ctx: impl AsRef<ExecutionContext>,
220    ) -> Option<StreamingCommand> {
221        exec_ctx.as_ref().stream(self, OutputMode::Capture, OutputMode::Print)
222    }
223
224    /// Mark the command as being executed, disarming the drop bomb.
225    /// If this method is not called before the command is dropped, its drop will panic.
226    pub fn mark_as_executed(&mut self) {
227        self.drop_bomb.defuse();
228    }
229
230    /// Returns the source code location where this command was created.
231    pub fn get_created_location(&self) -> std::panic::Location<'static> {
232        self.drop_bomb.get_created_location()
233    }
234
235    /// If in a CI environment, forces the command to run with colors.
236    pub fn force_coloring_in_ci(&mut self) {
237        if CiEnv::is_ci() {
238            // Due to use of stamp/docker, the output stream of bootstrap is not
239            // a TTY in CI, so coloring is by-default turned off.
240            // The explicit `TERM=xterm` environment is needed for
241            // `--color always` to actually work. This env var was lost when
242            // compiling through the Makefile. Very strange.
243            self.env("TERM", "xterm").args(["--color", "always"]);
244        }
245    }
246
247    pub fn cache_key(&self) -> Option<CommandCacheKey> {
248        if !self.should_cache {
249            return None;
250        }
251        let command = &self.command;
252        Some(CommandCacheKey {
253            program: command.get_program().into(),
254            args: command.get_args().map(OsStr::to_os_string).collect(),
255            envs: command
256                .get_envs()
257                .map(|(k, v)| (k.to_os_string(), v.map(|val| val.to_os_string())))
258                .collect(),
259            cwd: command.get_current_dir().map(Path::to_path_buf),
260        })
261    }
262}
263
264impl Debug for BootstrapCommand {
265    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
266        write!(f, "{:?}", self.command)?;
267        write!(f, " (failure_mode={:?})", self.failure_behavior)
268    }
269}
270
271impl From<Command> for BootstrapCommand {
272    #[track_caller]
273    fn from(command: Command) -> Self {
274        let program = command.get_program().to_owned();
275        Self {
276            should_cache: true,
277            command,
278            failure_behavior: BehaviorOnFailure::Exit,
279            run_in_dry_run: false,
280            drop_bomb: DropBomb::arm(program),
281        }
282    }
283}
284
285/// Represents the current status of `BootstrapCommand`.
286#[derive(Clone, PartialEq)]
287enum CommandStatus {
288    /// The command has started and finished with some status.
289    Finished(ExitStatus),
290    /// It was not even possible to start the command.
291    DidNotStart,
292}
293
294/// Create a new BootstrapCommand. This is a helper function to make command creation
295/// shorter than `BootstrapCommand::new`.
296#[track_caller]
297#[must_use]
298pub fn command<S: AsRef<OsStr>>(program: S) -> BootstrapCommand {
299    BootstrapCommand::new(program)
300}
301
302/// Represents the output of an executed process.
303#[derive(Clone, PartialEq)]
304pub struct CommandOutput {
305    status: CommandStatus,
306    stdout: Option<Vec<u8>>,
307    stderr: Option<Vec<u8>>,
308}
309
310impl CommandOutput {
311    #[must_use]
312    pub fn did_not_start(stdout: OutputMode, stderr: OutputMode) -> Self {
313        Self {
314            status: CommandStatus::DidNotStart,
315            stdout: match stdout {
316                OutputMode::Print => None,
317                OutputMode::Capture => Some(vec![]),
318            },
319            stderr: match stderr {
320                OutputMode::Print => None,
321                OutputMode::Capture => Some(vec![]),
322            },
323        }
324    }
325
326    #[must_use]
327    pub fn from_output(output: Output, stdout: OutputMode, stderr: OutputMode) -> Self {
328        Self {
329            status: CommandStatus::Finished(output.status),
330            stdout: match stdout {
331                OutputMode::Print => None,
332                OutputMode::Capture => Some(output.stdout),
333            },
334            stderr: match stderr {
335                OutputMode::Print => None,
336                OutputMode::Capture => Some(output.stderr),
337            },
338        }
339    }
340
341    #[must_use]
342    pub fn is_success(&self) -> bool {
343        match self.status {
344            CommandStatus::Finished(status) => status.success(),
345            CommandStatus::DidNotStart => false,
346        }
347    }
348
349    #[must_use]
350    pub fn is_failure(&self) -> bool {
351        !self.is_success()
352    }
353
354    pub fn status(&self) -> Option<ExitStatus> {
355        match self.status {
356            CommandStatus::Finished(status) => Some(status),
357            CommandStatus::DidNotStart => None,
358        }
359    }
360
361    #[must_use]
362    pub fn stdout(&self) -> String {
363        String::from_utf8(
364            self.stdout.clone().expect("Accessing stdout of a command that did not capture stdout"),
365        )
366        .expect("Cannot parse process stdout as UTF-8")
367    }
368
369    #[must_use]
370    pub fn stdout_if_present(&self) -> Option<String> {
371        self.stdout.as_ref().and_then(|s| String::from_utf8(s.clone()).ok())
372    }
373
374    #[must_use]
375    pub fn stdout_if_ok(&self) -> Option<String> {
376        if self.is_success() { Some(self.stdout()) } else { None }
377    }
378
379    #[must_use]
380    pub fn stderr(&self) -> String {
381        String::from_utf8(
382            self.stderr.clone().expect("Accessing stderr of a command that did not capture stderr"),
383        )
384        .expect("Cannot parse process stderr as UTF-8")
385    }
386
387    #[must_use]
388    pub fn stderr_if_present(&self) -> Option<String> {
389        self.stderr.as_ref().and_then(|s| String::from_utf8(s.clone()).ok())
390    }
391}
392
393impl Default for CommandOutput {
394    fn default() -> Self {
395        Self {
396            status: CommandStatus::Finished(ExitStatus::default()),
397            stdout: Some(vec![]),
398            stderr: Some(vec![]),
399        }
400    }
401}
402
403/// Helper trait to format both Command and BootstrapCommand as a short execution line,
404/// without all the other details (e.g. environment variables).
405#[cfg(feature = "tracing")]
406pub trait FormatShortCmd {
407    fn format_short_cmd(&self) -> String;
408}
409
410#[cfg(feature = "tracing")]
411impl FormatShortCmd for BootstrapCommand {
412    fn format_short_cmd(&self) -> String {
413        self.command.format_short_cmd()
414    }
415}
416
417#[cfg(feature = "tracing")]
418impl FormatShortCmd for Command {
419    fn format_short_cmd(&self) -> String {
420        let program = Path::new(self.get_program());
421        let mut line = vec![program.file_name().unwrap().to_str().unwrap()];
422        line.extend(self.get_args().map(|arg| arg.to_str().unwrap()));
423        line.join(" ")
424    }
425}
426
427#[derive(Clone, Default)]
428pub struct ExecutionContext {
429    dry_run: DryRun,
430    verbose: u8,
431    pub fail_fast: bool,
432    delayed_failures: Arc<Mutex<Vec<String>>>,
433    command_cache: Arc<CommandCache>,
434}
435
436#[derive(Default)]
437pub struct CommandCache {
438    cache: Mutex<HashMap<CommandCacheKey, CommandOutput>>,
439}
440
441enum CommandState<'a> {
442    Cached(CommandOutput),
443    Deferred {
444        process: Option<Result<Child, std::io::Error>>,
445        command: &'a mut BootstrapCommand,
446        stdout: OutputMode,
447        stderr: OutputMode,
448        executed_at: &'a Location<'a>,
449        cache_key: Option<CommandCacheKey>,
450    },
451}
452
453pub struct StreamingCommand {
454    child: Child,
455    pub stdout: Option<ChildStdout>,
456    pub stderr: Option<ChildStderr>,
457}
458
459#[must_use]
460pub struct DeferredCommand<'a> {
461    state: CommandState<'a>,
462}
463
464impl CommandCache {
465    pub fn get(&self, key: &CommandCacheKey) -> Option<CommandOutput> {
466        self.cache.lock().unwrap().get(key).cloned()
467    }
468
469    pub fn insert(&self, key: CommandCacheKey, output: CommandOutput) {
470        self.cache.lock().unwrap().insert(key, output);
471    }
472}
473
474impl ExecutionContext {
475    pub fn new() -> Self {
476        ExecutionContext::default()
477    }
478
479    pub fn dry_run(&self) -> bool {
480        match self.dry_run {
481            DryRun::Disabled => false,
482            DryRun::SelfCheck | DryRun::UserSelected => true,
483        }
484    }
485
486    pub fn get_dry_run(&self) -> &DryRun {
487        &self.dry_run
488    }
489
490    pub fn verbose(&self, f: impl Fn()) {
491        if self.is_verbose() {
492            f()
493        }
494    }
495
496    pub fn is_verbose(&self) -> bool {
497        self.verbose > 0
498    }
499
500    pub fn fail_fast(&self) -> bool {
501        self.fail_fast
502    }
503
504    pub fn set_dry_run(&mut self, value: DryRun) {
505        self.dry_run = value;
506    }
507
508    pub fn set_verbose(&mut self, value: u8) {
509        self.verbose = value;
510    }
511
512    pub fn set_fail_fast(&mut self, value: bool) {
513        self.fail_fast = value;
514    }
515
516    pub fn add_to_delay_failure(&self, message: String) {
517        self.delayed_failures.lock().unwrap().push(message);
518    }
519
520    pub fn report_failures_and_exit(&self) {
521        let failures = self.delayed_failures.lock().unwrap();
522        if failures.is_empty() {
523            return;
524        }
525        eprintln!("\n{} command(s) did not execute successfully:\n", failures.len());
526        for failure in &*failures {
527            eprintln!("  - {failure}");
528        }
529        exit!(1);
530    }
531
532    /// Execute a command and return its output.
533    /// Note: Ideally, you should use one of the BootstrapCommand::run* functions to
534    /// execute commands. They internally call this method.
535    #[track_caller]
536    pub fn start<'a>(
537        &self,
538        command: &'a mut BootstrapCommand,
539        stdout: OutputMode,
540        stderr: OutputMode,
541    ) -> DeferredCommand<'a> {
542        let cache_key = command.cache_key();
543
544        if let Some(cached_output) = cache_key.as_ref().and_then(|key| self.command_cache.get(key))
545        {
546            command.mark_as_executed();
547            self.verbose(|| println!("Cache hit: {command:?}"));
548            return DeferredCommand { state: CommandState::Cached(cached_output) };
549        }
550
551        let created_at = command.get_created_location();
552        let executed_at = std::panic::Location::caller();
553
554        if self.dry_run() && !command.run_in_dry_run {
555            return DeferredCommand {
556                state: CommandState::Deferred {
557                    process: None,
558                    command,
559                    stdout,
560                    stderr,
561                    executed_at,
562                    cache_key,
563                },
564            };
565        }
566
567        #[cfg(feature = "tracing")]
568        let _run_span = trace_cmd!(command);
569
570        self.verbose(|| {
571            println!("running: {command:?} (created at {created_at}, executed at {executed_at})")
572        });
573
574        let cmd = &mut command.command;
575        cmd.stdout(stdout.stdio());
576        cmd.stderr(stderr.stdio());
577
578        let child = cmd.spawn();
579
580        DeferredCommand {
581            state: CommandState::Deferred {
582                process: Some(child),
583                command,
584                stdout,
585                stderr,
586                executed_at,
587                cache_key,
588            },
589        }
590    }
591
592    /// Execute a command and return its output.
593    /// Note: Ideally, you should use one of the BootstrapCommand::run* functions to
594    /// execute commands. They internally call this method.
595    #[track_caller]
596    pub fn run(
597        &self,
598        command: &mut BootstrapCommand,
599        stdout: OutputMode,
600        stderr: OutputMode,
601    ) -> CommandOutput {
602        self.start(command, stdout, stderr).wait_for_output(self)
603    }
604
605    fn fail(&self, message: &str, output: CommandOutput) -> ! {
606        if self.is_verbose() {
607            println!("{message}");
608        } else {
609            let (stdout, stderr) = (output.stdout_if_present(), output.stderr_if_present());
610            // If the command captures output, the user would not see any indication that
611            // it has failed. In this case, print a more verbose error, since to provide more
612            // context.
613            if stdout.is_some() || stderr.is_some() {
614                if let Some(stdout) = output.stdout_if_present().take_if(|s| !s.trim().is_empty()) {
615                    println!("STDOUT:\n{stdout}\n");
616                }
617                if let Some(stderr) = output.stderr_if_present().take_if(|s| !s.trim().is_empty()) {
618                    println!("STDERR:\n{stderr}\n");
619                }
620                println!("Command has failed. Rerun with -v to see more details.");
621            } else {
622                println!("Command has failed. Rerun with -v to see more details.");
623            }
624        }
625        exit!(1);
626    }
627
628    /// Spawns the command with configured stdout and stderr handling.
629    ///
630    /// Returns None if in dry-run mode or Panics if the command fails to spawn.
631    pub fn stream(
632        &self,
633        command: &mut BootstrapCommand,
634        stdout: OutputMode,
635        stderr: OutputMode,
636    ) -> Option<StreamingCommand> {
637        command.mark_as_executed();
638        if !command.run_in_dry_run && self.dry_run() {
639            return None;
640        }
641        let cmd = &mut command.command;
642        cmd.stdout(stdout.stdio());
643        cmd.stderr(stderr.stdio());
644        let child = cmd.spawn();
645        let mut child = match child {
646            Ok(child) => child,
647            Err(e) => panic!("failed to execute command: {cmd:?}\nERROR: {e}"),
648        };
649
650        let stdout = child.stdout.take();
651        let stderr = child.stderr.take();
652        Some(StreamingCommand { child, stdout, stderr })
653    }
654}
655
656impl AsRef<ExecutionContext> for ExecutionContext {
657    fn as_ref(&self) -> &ExecutionContext {
658        self
659    }
660}
661
662impl StreamingCommand {
663    pub fn wait(mut self) -> Result<ExitStatus, std::io::Error> {
664        self.child.wait()
665    }
666}
667
668impl<'a> DeferredCommand<'a> {
669    pub fn wait_for_output(self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
670        match self.state {
671            CommandState::Cached(output) => output,
672            CommandState::Deferred { process, command, stdout, stderr, executed_at, cache_key } => {
673                let exec_ctx = exec_ctx.as_ref();
674
675                let output =
676                    Self::finish_process(process, command, stdout, stderr, executed_at, exec_ctx);
677
678                if (!exec_ctx.dry_run() || command.run_in_dry_run)
679                    && let (Some(cache_key), Some(_)) = (&cache_key, output.status())
680                {
681                    exec_ctx.command_cache.insert(cache_key.clone(), output.clone());
682                }
683
684                output
685            }
686        }
687    }
688
689    pub fn finish_process(
690        mut process: Option<Result<Child, std::io::Error>>,
691        command: &mut BootstrapCommand,
692        stdout: OutputMode,
693        stderr: OutputMode,
694        executed_at: &'a std::panic::Location<'a>,
695        exec_ctx: &ExecutionContext,
696    ) -> CommandOutput {
697        command.mark_as_executed();
698
699        let process = match process.take() {
700            Some(p) => p,
701            None => return CommandOutput::default(),
702        };
703
704        let created_at = command.get_created_location();
705
706        let mut message = String::new();
707
708        let output = match process {
709            Ok(child) => match child.wait_with_output() {
710                Ok(result) if result.status.success() => {
711                    // Successful execution
712                    CommandOutput::from_output(result, stdout, stderr)
713                }
714                Ok(result) => {
715                    // Command ran but failed
716                    use std::fmt::Write;
717
718                    writeln!(
719                        message,
720                        r#"
721Command {command:?} did not execute successfully.
722Expected success, got {}
723Created at: {created_at}
724Executed at: {executed_at}"#,
725                        result.status,
726                    )
727                    .unwrap();
728
729                    let output = CommandOutput::from_output(result, stdout, stderr);
730
731                    if stdout.captures() {
732                        writeln!(message, "\nSTDOUT ----\n{}", output.stdout().trim()).unwrap();
733                    }
734                    if stderr.captures() {
735                        writeln!(message, "\nSTDERR ----\n{}", output.stderr().trim()).unwrap();
736                    }
737
738                    output
739                }
740                Err(e) => {
741                    // Failed to wait for output
742                    use std::fmt::Write;
743
744                    writeln!(
745                        message,
746                        "\n\nCommand {command:?} did not execute successfully.\
747                        \nIt was not possible to execute the command: {e:?}"
748                    )
749                    .unwrap();
750
751                    CommandOutput::did_not_start(stdout, stderr)
752                }
753            },
754            Err(e) => {
755                // Failed to spawn the command
756                use std::fmt::Write;
757
758                writeln!(
759                    message,
760                    "\n\nCommand {command:?} did not execute successfully.\
761                    \nIt was not possible to execute the command: {e:?}"
762                )
763                .unwrap();
764
765                CommandOutput::did_not_start(stdout, stderr)
766            }
767        };
768
769        if !output.is_success() {
770            match command.failure_behavior {
771                BehaviorOnFailure::DelayFail => {
772                    if exec_ctx.fail_fast {
773                        exec_ctx.fail(&message, output);
774                    }
775                    exec_ctx.add_to_delay_failure(message);
776                }
777                BehaviorOnFailure::Exit => {
778                    exec_ctx.fail(&message, output);
779                }
780                BehaviorOnFailure::Ignore => {
781                    // If failures are allowed, either the error has been printed already
782                    // (OutputMode::Print) or the user used a capture output mode and wants to
783                    // handle the error output on their own.
784                }
785            }
786        }
787
788        output
789    }
790}