bootstrap/utils/
execution_context.rs

1//! Shared execution context for running bootstrap commands.
2//!
3//! This module provides the [`ExecutionContext`] type, which holds global configuration
4//! relevant during the execution of commands in bootstrap. This includes dry-run
5//! mode, verbosity level, and behavior on failure.
6use std::panic::Location;
7use std::process::Child;
8use std::sync::{Arc, Mutex};
9
10use crate::core::config::DryRun;
11#[cfg(feature = "tracing")]
12use crate::trace_cmd;
13use crate::{BehaviorOnFailure, BootstrapCommand, CommandOutput, OutputMode, exit};
14
15#[derive(Clone, Default)]
16pub struct ExecutionContext {
17    dry_run: DryRun,
18    verbose: u8,
19    pub fail_fast: bool,
20    delayed_failures: Arc<Mutex<Vec<String>>>,
21}
22
23impl ExecutionContext {
24    pub fn new() -> Self {
25        ExecutionContext::default()
26    }
27
28    pub fn dry_run(&self) -> bool {
29        match self.dry_run {
30            DryRun::Disabled => false,
31            DryRun::SelfCheck | DryRun::UserSelected => true,
32        }
33    }
34
35    pub fn get_dry_run(&self) -> &DryRun {
36        &self.dry_run
37    }
38
39    pub fn verbose(&self, f: impl Fn()) {
40        if self.is_verbose() {
41            f()
42        }
43    }
44
45    pub fn is_verbose(&self) -> bool {
46        self.verbose > 0
47    }
48
49    pub fn fail_fast(&self) -> bool {
50        self.fail_fast
51    }
52
53    pub fn set_dry_run(&mut self, value: DryRun) {
54        self.dry_run = value;
55    }
56
57    pub fn set_verbose(&mut self, value: u8) {
58        self.verbose = value;
59    }
60
61    pub fn set_fail_fast(&mut self, value: bool) {
62        self.fail_fast = value;
63    }
64
65    pub fn add_to_delay_failure(&self, message: String) {
66        self.delayed_failures.lock().unwrap().push(message);
67    }
68
69    pub fn report_failures_and_exit(&self) {
70        let failures = self.delayed_failures.lock().unwrap();
71        if failures.is_empty() {
72            return;
73        }
74        eprintln!("\n{} command(s) did not execute successfully:\n", failures.len());
75        for failure in &*failures {
76            eprintln!("  - {failure}");
77        }
78        exit!(1);
79    }
80
81    /// Execute a command and return its output.
82    /// Note: Ideally, you should use one of the BootstrapCommand::run* functions to
83    /// execute commands. They internally call this method.
84    #[track_caller]
85    pub fn start<'a>(
86        &self,
87        command: &'a mut BootstrapCommand,
88        stdout: OutputMode,
89        stderr: OutputMode,
90    ) -> DeferredCommand<'a> {
91        command.mark_as_executed();
92
93        let created_at = command.get_created_location();
94        let executed_at = std::panic::Location::caller();
95
96        if self.dry_run() && !command.run_always {
97            return DeferredCommand { process: None, stdout, stderr, command, executed_at };
98        }
99
100        #[cfg(feature = "tracing")]
101        let _run_span = trace_cmd!(command);
102
103        self.verbose(|| {
104            println!("running: {command:?} (created at {created_at}, executed at {executed_at})")
105        });
106
107        let cmd = command.as_command_mut();
108        cmd.stdout(stdout.stdio());
109        cmd.stderr(stderr.stdio());
110
111        let child = cmd.spawn();
112
113        DeferredCommand { process: Some(child), stdout, stderr, command, executed_at }
114    }
115
116    /// Execute a command and return its output.
117    /// Note: Ideally, you should use one of the BootstrapCommand::run* functions to
118    /// execute commands. They internally call this method.
119    #[track_caller]
120    pub fn run(
121        &self,
122        command: &mut BootstrapCommand,
123        stdout: OutputMode,
124        stderr: OutputMode,
125    ) -> CommandOutput {
126        self.start(command, stdout, stderr).wait_for_output(self)
127    }
128
129    fn fail(&self, message: &str, output: CommandOutput) -> ! {
130        if self.is_verbose() {
131            println!("{message}");
132        } else {
133            let (stdout, stderr) = (output.stdout_if_present(), output.stderr_if_present());
134            // If the command captures output, the user would not see any indication that
135            // it has failed. In this case, print a more verbose error, since to provide more
136            // context.
137            if stdout.is_some() || stderr.is_some() {
138                if let Some(stdout) = output.stdout_if_present().take_if(|s| !s.trim().is_empty()) {
139                    println!("STDOUT:\n{stdout}\n");
140                }
141                if let Some(stderr) = output.stderr_if_present().take_if(|s| !s.trim().is_empty()) {
142                    println!("STDERR:\n{stderr}\n");
143                }
144                println!("Command has failed. Rerun with -v to see more details.");
145            } else {
146                println!("Command has failed. Rerun with -v to see more details.");
147            }
148        }
149        exit!(1);
150    }
151}
152
153impl AsRef<ExecutionContext> for ExecutionContext {
154    fn as_ref(&self) -> &ExecutionContext {
155        self
156    }
157}
158
159pub struct DeferredCommand<'a> {
160    process: Option<Result<Child, std::io::Error>>,
161    command: &'a mut BootstrapCommand,
162    stdout: OutputMode,
163    stderr: OutputMode,
164    executed_at: &'a Location<'a>,
165}
166
167impl<'a> DeferredCommand<'a> {
168    pub fn wait_for_output(mut self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
169        let exec_ctx = exec_ctx.as_ref();
170
171        let process = match self.process.take() {
172            Some(p) => p,
173            None => return CommandOutput::default(),
174        };
175
176        let created_at = self.command.get_created_location();
177        let executed_at = self.executed_at;
178
179        let mut message = String::new();
180
181        let output = match process {
182            Ok(child) => match child.wait_with_output() {
183                Ok(result) if result.status.success() => {
184                    // Successful execution
185                    CommandOutput::from_output(result, self.stdout, self.stderr)
186                }
187                Ok(result) => {
188                    // Command ran but failed
189                    use std::fmt::Write;
190
191                    writeln!(
192                        message,
193                        r#"
194Command {:?} did not execute successfully.
195Expected success, got {}
196Created at: {created_at}
197Executed at: {executed_at}"#,
198                        self.command, result.status,
199                    )
200                    .unwrap();
201
202                    let output = CommandOutput::from_output(result, self.stdout, self.stderr);
203
204                    if self.stdout.captures() {
205                        writeln!(message, "\nSTDOUT ----\n{}", output.stdout().trim()).unwrap();
206                    }
207                    if self.stderr.captures() {
208                        writeln!(message, "\nSTDERR ----\n{}", output.stderr().trim()).unwrap();
209                    }
210
211                    output
212                }
213                Err(e) => {
214                    // Failed to wait for output
215                    use std::fmt::Write;
216
217                    writeln!(
218                        message,
219                        "\n\nCommand {:?} did not execute successfully.\
220                        \nIt was not possible to execute the command: {e:?}",
221                        self.command
222                    )
223                    .unwrap();
224
225                    CommandOutput::did_not_start(self.stdout, self.stderr)
226                }
227            },
228            Err(e) => {
229                // Failed to spawn the command
230                use std::fmt::Write;
231
232                writeln!(
233                    message,
234                    "\n\nCommand {:?} did not execute successfully.\
235                    \nIt was not possible to execute the command: {e:?}",
236                    self.command
237                )
238                .unwrap();
239
240                CommandOutput::did_not_start(self.stdout, self.stderr)
241            }
242        };
243
244        if !output.is_success() {
245            match self.command.failure_behavior {
246                BehaviorOnFailure::DelayFail => {
247                    if exec_ctx.fail_fast {
248                        exec_ctx.fail(&message, output);
249                    }
250                    exec_ctx.add_to_delay_failure(message);
251                }
252                BehaviorOnFailure::Exit => {
253                    exec_ctx.fail(&message, output);
254                }
255                BehaviorOnFailure::Ignore => {
256                    // If failures are allowed, either the error has been printed already
257                    // (OutputMode::Print) or the user used a capture output mode and wants to
258                    // handle the error output on their own.
259                }
260            }
261        }
262
263        output
264    }
265}