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