Skip to content

Commit 3ab5e60

Browse files
committed
Add execution context
1 parent 0b65d0d commit 3ab5e60

File tree

2 files changed

+222
-2
lines changed

2 files changed

+222
-2
lines changed

src/bootstrap/src/utils/exec.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use std::process::{Command, CommandArgs, CommandEnvs, ExitStatus, Output, Stdio}
1111
use build_helper::ci::CiEnv;
1212
use build_helper::drop_bomb::DropBomb;
1313

14+
use super::execution_context::ExecutionContext;
1415
use crate::Build;
1516

1617
/// What should be done when the command fails.
@@ -125,7 +126,6 @@ impl BootstrapCommand {
125126
Self { failure_behavior: BehaviorOnFailure::DelayFail, ..self }
126127
}
127128

128-
#[expect(dead_code)]
129129
pub fn fail_fast(self) -> Self {
130130
Self { failure_behavior: BehaviorOnFailure::Exit, ..self }
131131
}
@@ -140,6 +140,29 @@ impl BootstrapCommand {
140140
self
141141
}
142142

143+
#[track_caller]
144+
pub fn run_exec_ctx(&mut self, exec_ctx: impl AsRef<ExecutionContext>) -> bool {
145+
exec_ctx.as_ref().run(self, OutputMode::Print, OutputMode::Print).is_success()
146+
}
147+
148+
/// Run the command, while capturing and returning all its output.
149+
#[track_caller]
150+
pub fn run_capture_exec_ctx(
151+
&mut self,
152+
exec_ctx: impl AsRef<ExecutionContext>,
153+
) -> CommandOutput {
154+
exec_ctx.as_ref().run(self, OutputMode::Capture, OutputMode::Capture)
155+
}
156+
157+
/// Run the command, while capturing and returning stdout, and printing stderr.
158+
#[track_caller]
159+
pub fn run_capture_stdout_exec_ctx(
160+
&mut self,
161+
exec_ctx: impl AsRef<ExecutionContext>,
162+
) -> CommandOutput {
163+
exec_ctx.as_ref().run(self, OutputMode::Capture, OutputMode::Print)
164+
}
165+
143166
/// Run the command, while printing stdout and stderr.
144167
/// Returns true if the command has succeeded.
145168
#[track_caller]
@@ -280,7 +303,6 @@ impl CommandOutput {
280303
!self.is_success()
281304
}
282305

283-
#[expect(dead_code)]
284306
pub fn status(&self) -> Option<ExitStatus> {
285307
match self.status {
286308
CommandStatus::Finished(status) => Some(status),
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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.
6+
use std::sync::{Arc, Mutex};
7+
8+
use crate::core::config::DryRun;
9+
use crate::{BehaviorOnFailure, BootstrapCommand, CommandOutput, OutputMode, exit};
10+
11+
#[derive(Clone, Default)]
12+
pub struct ExecutionContext {
13+
dry_run: DryRun,
14+
verbose: u8,
15+
pub fail_fast: bool,
16+
delayed_failures: Arc<Mutex<Vec<String>>>,
17+
}
18+
19+
impl ExecutionContext {
20+
pub fn new() -> Self {
21+
ExecutionContext::default()
22+
}
23+
24+
pub fn dry_run(&self) -> bool {
25+
match self.dry_run {
26+
DryRun::Disabled => false,
27+
DryRun::SelfCheck | DryRun::UserSelected => true,
28+
}
29+
}
30+
31+
pub fn verbose(&self, f: impl Fn()) {
32+
if self.is_verbose() {
33+
f()
34+
}
35+
}
36+
37+
pub fn is_verbose(&self) -> bool {
38+
self.verbose > 0
39+
}
40+
41+
pub fn fail_fast(&self) -> bool {
42+
self.fail_fast
43+
}
44+
45+
pub fn set_dry_run(&mut self, value: DryRun) {
46+
self.dry_run = value;
47+
}
48+
49+
pub fn set_verbose(&mut self, value: u8) {
50+
self.verbose = value;
51+
}
52+
53+
pub fn set_fail_fast(&mut self, value: bool) {
54+
self.fail_fast = value;
55+
}
56+
57+
pub fn add_to_delay_failure(&self, message: String) {
58+
self.delayed_failures.lock().unwrap().push(message);
59+
}
60+
61+
pub fn report_failures_and_exit(&self) {
62+
let failures = self.delayed_failures.lock().unwrap();
63+
if failures.is_empty() {
64+
return;
65+
}
66+
eprintln!("\n{} command(s) did not execute successfully:\n", failures.len());
67+
for failure in &*failures {
68+
eprintln!(" - {failure}");
69+
}
70+
exit!(1);
71+
}
72+
73+
/// Execute a command and return its output.
74+
/// Note: Ideally, you should use one of the BootstrapCommand::run* functions to
75+
/// execute commands. They internally call this method.
76+
#[track_caller]
77+
pub fn run(
78+
&self,
79+
command: &mut BootstrapCommand,
80+
stdout: OutputMode,
81+
stderr: OutputMode,
82+
) -> CommandOutput {
83+
command.mark_as_executed();
84+
if self.dry_run() && !command.run_always {
85+
return CommandOutput::default();
86+
}
87+
88+
#[cfg(feature = "tracing")]
89+
let _run_span = trace_cmd!(command);
90+
91+
let created_at = command.get_created_location();
92+
let executed_at = std::panic::Location::caller();
93+
94+
self.verbose(|| {
95+
println!("running: {command:?} (created at {created_at}, executed at {executed_at})")
96+
});
97+
98+
let cmd = command.as_command_mut();
99+
cmd.stdout(stdout.stdio());
100+
cmd.stderr(stderr.stdio());
101+
102+
let output = cmd.output();
103+
104+
use std::fmt::Write;
105+
106+
let mut message = String::new();
107+
let output: CommandOutput = match output {
108+
// Command has succeeded
109+
Ok(output) if output.status.success() => {
110+
CommandOutput::from_output(output, stdout, stderr)
111+
}
112+
// Command has started, but then it failed
113+
Ok(output) => {
114+
writeln!(
115+
message,
116+
r#"
117+
Command {command:?} did not execute successfully.
118+
Expected success, got {}
119+
Created at: {created_at}
120+
Executed at: {executed_at}"#,
121+
output.status,
122+
)
123+
.unwrap();
124+
125+
let output: CommandOutput = CommandOutput::from_output(output, stdout, stderr);
126+
127+
// If the output mode is OutputMode::Capture, we can now print the output.
128+
// If it is OutputMode::Print, then the output has already been printed to
129+
// stdout/stderr, and we thus don't have anything captured to print anyway.
130+
if stdout.captures() {
131+
writeln!(message, "\nSTDOUT ----\n{}", output.stdout().trim()).unwrap();
132+
}
133+
if stderr.captures() {
134+
writeln!(message, "\nSTDERR ----\n{}", output.stderr().trim()).unwrap();
135+
}
136+
output
137+
}
138+
// The command did not even start
139+
Err(e) => {
140+
writeln!(
141+
message,
142+
"\n\nCommand {command:?} did not execute successfully.\
143+
\nIt was not possible to execute the command: {e:?}"
144+
)
145+
.unwrap();
146+
CommandOutput::did_not_start(stdout, stderr)
147+
}
148+
};
149+
150+
let fail = |message: &str, output: CommandOutput| -> ! {
151+
if self.is_verbose() {
152+
println!("{message}");
153+
} else {
154+
let (stdout, stderr) = (output.stdout_if_present(), output.stderr_if_present());
155+
// If the command captures output, the user would not see any indication that
156+
// it has failed. In this case, print a more verbose error, since to provide more
157+
// context.
158+
if stdout.is_some() || stderr.is_some() {
159+
if let Some(stdout) =
160+
output.stdout_if_present().take_if(|s| !s.trim().is_empty())
161+
{
162+
println!("STDOUT:\n{stdout}\n");
163+
}
164+
if let Some(stderr) =
165+
output.stderr_if_present().take_if(|s| !s.trim().is_empty())
166+
{
167+
println!("STDERR:\n{stderr}\n");
168+
}
169+
println!("Command {command:?} has failed. Rerun with -v to see more details.");
170+
} else {
171+
println!("Command has failed. Rerun with -v to see more details.");
172+
}
173+
}
174+
exit!(1);
175+
};
176+
177+
if !output.is_success() {
178+
match command.failure_behavior {
179+
BehaviorOnFailure::DelayFail => {
180+
if self.fail_fast {
181+
fail(&message, output);
182+
}
183+
184+
self.add_to_delay_failure(message);
185+
}
186+
BehaviorOnFailure::Exit => {
187+
fail(&message, output);
188+
}
189+
BehaviorOnFailure::Ignore => {
190+
// If failures are allowed, either the error has been printed already
191+
// (OutputMode::Print) or the user used a capture output mode and wants to
192+
// handle the error output on their own.
193+
}
194+
}
195+
}
196+
output
197+
}
198+
}

0 commit comments

Comments
 (0)