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