bootstrap/utils/
exec.rs

1//! Command Execution Module
2//!
3//! This module provides a structured way to execute and manage commands efficiently,
4//! ensuring controlled failure handling and output management.
5use std::ffi::OsStr;
6use std::fmt::{Debug, Formatter};
7use std::path::Path;
8use std::process::{Command, CommandArgs, CommandEnvs, ExitStatus, Output, Stdio};
9
10use build_helper::ci::CiEnv;
11use build_helper::drop_bomb::DropBomb;
12
13use super::execution_context::{DeferredCommand, ExecutionContext};
14
15/// What should be done when the command fails.
16#[derive(Debug, Copy, Clone)]
17pub enum BehaviorOnFailure {
18    /// Immediately stop bootstrap.
19    Exit,
20    /// Delay failure until the end of bootstrap invocation.
21    DelayFail,
22    /// Ignore the failure, the command can fail in an expected way.
23    Ignore,
24}
25
26/// How should the output of a specific stream of the command (stdout/stderr) be handled
27/// (whether it should be captured or printed).
28#[derive(Debug, Copy, Clone)]
29pub enum OutputMode {
30    /// Prints the stream by inheriting it from the bootstrap process.
31    Print,
32    /// Captures the stream into memory.
33    Capture,
34}
35
36impl OutputMode {
37    pub fn captures(&self) -> bool {
38        match self {
39            OutputMode::Print => false,
40            OutputMode::Capture => true,
41        }
42    }
43
44    pub fn stdio(&self) -> Stdio {
45        match self {
46            OutputMode::Print => Stdio::inherit(),
47            OutputMode::Capture => Stdio::piped(),
48        }
49    }
50}
51
52/// Wrapper around `std::process::Command`.
53///
54/// By default, the command will exit bootstrap if it fails.
55/// If you want to allow failures, use [allow_failure].
56/// If you want to delay failures until the end of bootstrap, use [delay_failure].
57///
58/// By default, the command will print its stdout/stderr to stdout/stderr of bootstrap ([OutputMode::Print]).
59/// If you want to handle the output programmatically, use [BootstrapCommand::run_capture].
60///
61/// Bootstrap will print a debug log to stdout if the command fails and failure is not allowed.
62///
63/// [allow_failure]: BootstrapCommand::allow_failure
64/// [delay_failure]: BootstrapCommand::delay_failure
65pub struct BootstrapCommand {
66    command: Command,
67    pub failure_behavior: BehaviorOnFailure,
68    // Run the command even during dry run
69    pub run_always: bool,
70    // This field makes sure that each command is executed (or disarmed) before it is dropped,
71    // to avoid forgetting to execute a command.
72    drop_bomb: DropBomb,
73}
74
75impl<'a> BootstrapCommand {
76    #[track_caller]
77    pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
78        Command::new(program).into()
79    }
80
81    pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
82        self.command.arg(arg.as_ref());
83        self
84    }
85
86    pub fn args<I, S>(&mut self, args: I) -> &mut Self
87    where
88        I: IntoIterator<Item = S>,
89        S: AsRef<OsStr>,
90    {
91        self.command.args(args);
92        self
93    }
94
95    pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
96    where
97        K: AsRef<OsStr>,
98        V: AsRef<OsStr>,
99    {
100        self.command.env(key, val);
101        self
102    }
103
104    pub fn get_envs(&self) -> CommandEnvs<'_> {
105        self.command.get_envs()
106    }
107
108    pub fn get_args(&self) -> CommandArgs<'_> {
109        self.command.get_args()
110    }
111
112    pub fn env_remove<K: AsRef<OsStr>>(&mut self, key: K) -> &mut Self {
113        self.command.env_remove(key);
114        self
115    }
116
117    pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
118        self.command.current_dir(dir);
119        self
120    }
121
122    pub fn stdin(&mut self, stdin: std::process::Stdio) -> &mut Self {
123        self.command.stdin(stdin);
124        self
125    }
126
127    #[must_use]
128    pub fn delay_failure(self) -> Self {
129        Self { failure_behavior: BehaviorOnFailure::DelayFail, ..self }
130    }
131
132    pub fn fail_fast(self) -> Self {
133        Self { failure_behavior: BehaviorOnFailure::Exit, ..self }
134    }
135
136    #[must_use]
137    pub fn allow_failure(self) -> Self {
138        Self { failure_behavior: BehaviorOnFailure::Ignore, ..self }
139    }
140
141    pub fn run_always(&mut self) -> &mut Self {
142        self.run_always = true;
143        self
144    }
145
146    /// Run the command, while printing stdout and stderr.
147    /// Returns true if the command has succeeded.
148    #[track_caller]
149    pub fn run(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> bool {
150        exec_ctx.as_ref().run(self, OutputMode::Print, OutputMode::Print).is_success()
151    }
152
153    /// Run the command, while capturing and returning all its output.
154    #[track_caller]
155    pub fn run_capture(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
156        exec_ctx.as_ref().run(self, OutputMode::Capture, OutputMode::Capture)
157    }
158
159    /// Run the command, while capturing and returning stdout, and printing stderr.
160    #[track_caller]
161    pub fn run_capture_stdout(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> CommandOutput {
162        exec_ctx.as_ref().run(self, OutputMode::Capture, OutputMode::Print)
163    }
164
165    /// Spawn the command in background, while capturing and returning all its output.
166    #[track_caller]
167    pub fn start_capture(
168        &'a mut self,
169        exec_ctx: impl AsRef<ExecutionContext>,
170    ) -> DeferredCommand<'a> {
171        exec_ctx.as_ref().start(self, OutputMode::Capture, OutputMode::Capture)
172    }
173
174    /// Spawn the command in background, while capturing and returning stdout, and printing stderr.
175    #[track_caller]
176    pub fn start_capture_stdout(
177        &'a mut self,
178        exec_ctx: impl AsRef<ExecutionContext>,
179    ) -> DeferredCommand<'a> {
180        exec_ctx.as_ref().start(self, OutputMode::Capture, OutputMode::Print)
181    }
182
183    /// Provides access to the stdlib Command inside.
184    /// FIXME: This function should be eventually removed from bootstrap.
185    pub fn as_command_mut(&mut self) -> &mut Command {
186        // We don't know what will happen with the returned command, so we need to mark this
187        // command as executed proactively.
188        self.mark_as_executed();
189        &mut self.command
190    }
191
192    /// Mark the command as being executed, disarming the drop bomb.
193    /// If this method is not called before the command is dropped, its drop will panic.
194    pub fn mark_as_executed(&mut self) {
195        self.drop_bomb.defuse();
196    }
197
198    /// Returns the source code location where this command was created.
199    pub fn get_created_location(&self) -> std::panic::Location<'static> {
200        self.drop_bomb.get_created_location()
201    }
202
203    /// If in a CI environment, forces the command to run with colors.
204    pub fn force_coloring_in_ci(&mut self) {
205        if CiEnv::is_ci() {
206            // Due to use of stamp/docker, the output stream of bootstrap is not
207            // a TTY in CI, so coloring is by-default turned off.
208            // The explicit `TERM=xterm` environment is needed for
209            // `--color always` to actually work. This env var was lost when
210            // compiling through the Makefile. Very strange.
211            self.env("TERM", "xterm").args(["--color", "always"]);
212        }
213    }
214}
215
216impl Debug for BootstrapCommand {
217    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
218        write!(f, "{:?}", self.command)?;
219        write!(f, " (failure_mode={:?})", self.failure_behavior)
220    }
221}
222
223impl From<Command> for BootstrapCommand {
224    #[track_caller]
225    fn from(command: Command) -> Self {
226        let program = command.get_program().to_owned();
227
228        Self {
229            command,
230            failure_behavior: BehaviorOnFailure::Exit,
231            run_always: false,
232            drop_bomb: DropBomb::arm(program),
233        }
234    }
235}
236
237/// Represents the current status of `BootstrapCommand`.
238enum CommandStatus {
239    /// The command has started and finished with some status.
240    Finished(ExitStatus),
241    /// It was not even possible to start the command.
242    DidNotStart,
243}
244
245/// Create a new BootstrapCommand. This is a helper function to make command creation
246/// shorter than `BootstrapCommand::new`.
247#[track_caller]
248#[must_use]
249pub fn command<S: AsRef<OsStr>>(program: S) -> BootstrapCommand {
250    BootstrapCommand::new(program)
251}
252
253/// Represents the output of an executed process.
254pub struct CommandOutput {
255    status: CommandStatus,
256    stdout: Option<Vec<u8>>,
257    stderr: Option<Vec<u8>>,
258}
259
260impl CommandOutput {
261    #[must_use]
262    pub fn did_not_start(stdout: OutputMode, stderr: OutputMode) -> Self {
263        Self {
264            status: CommandStatus::DidNotStart,
265            stdout: match stdout {
266                OutputMode::Print => None,
267                OutputMode::Capture => Some(vec![]),
268            },
269            stderr: match stderr {
270                OutputMode::Print => None,
271                OutputMode::Capture => Some(vec![]),
272            },
273        }
274    }
275
276    #[must_use]
277    pub fn from_output(output: Output, stdout: OutputMode, stderr: OutputMode) -> Self {
278        Self {
279            status: CommandStatus::Finished(output.status),
280            stdout: match stdout {
281                OutputMode::Print => None,
282                OutputMode::Capture => Some(output.stdout),
283            },
284            stderr: match stderr {
285                OutputMode::Print => None,
286                OutputMode::Capture => Some(output.stderr),
287            },
288        }
289    }
290
291    #[must_use]
292    pub fn is_success(&self) -> bool {
293        match self.status {
294            CommandStatus::Finished(status) => status.success(),
295            CommandStatus::DidNotStart => false,
296        }
297    }
298
299    #[must_use]
300    pub fn is_failure(&self) -> bool {
301        !self.is_success()
302    }
303
304    pub fn status(&self) -> Option<ExitStatus> {
305        match self.status {
306            CommandStatus::Finished(status) => Some(status),
307            CommandStatus::DidNotStart => None,
308        }
309    }
310
311    #[must_use]
312    pub fn stdout(&self) -> String {
313        String::from_utf8(
314            self.stdout.clone().expect("Accessing stdout of a command that did not capture stdout"),
315        )
316        .expect("Cannot parse process stdout as UTF-8")
317    }
318
319    #[must_use]
320    pub fn stdout_if_present(&self) -> Option<String> {
321        self.stdout.as_ref().and_then(|s| String::from_utf8(s.clone()).ok())
322    }
323
324    #[must_use]
325    pub fn stdout_if_ok(&self) -> Option<String> {
326        if self.is_success() { Some(self.stdout()) } else { None }
327    }
328
329    #[must_use]
330    pub fn stderr(&self) -> String {
331        String::from_utf8(
332            self.stderr.clone().expect("Accessing stderr of a command that did not capture stderr"),
333        )
334        .expect("Cannot parse process stderr as UTF-8")
335    }
336
337    #[must_use]
338    pub fn stderr_if_present(&self) -> Option<String> {
339        self.stderr.as_ref().and_then(|s| String::from_utf8(s.clone()).ok())
340    }
341}
342
343impl Default for CommandOutput {
344    fn default() -> Self {
345        Self {
346            status: CommandStatus::Finished(ExitStatus::default()),
347            stdout: Some(vec![]),
348            stderr: Some(vec![]),
349        }
350    }
351}
352
353/// Helper trait to format both Command and BootstrapCommand as a short execution line,
354/// without all the other details (e.g. environment variables).
355#[cfg(feature = "tracing")]
356pub trait FormatShortCmd {
357    fn format_short_cmd(&self) -> String;
358}
359
360#[cfg(feature = "tracing")]
361impl FormatShortCmd for BootstrapCommand {
362    fn format_short_cmd(&self) -> String {
363        self.command.format_short_cmd()
364    }
365}
366
367#[cfg(feature = "tracing")]
368impl FormatShortCmd for Command {
369    fn format_short_cmd(&self) -> String {
370        let program = Path::new(self.get_program());
371        let mut line = vec![program.file_name().unwrap().to_str().unwrap()];
372        line.extend(self.get_args().map(|arg| arg.to_str().unwrap()));
373        line.join(" ")
374    }
375}