Skip to content

Commit d593e72

Browse files
authored
Rollup merge of #142591 - Shourya742:2025-06-14-add-spawn-execution-api, r=Kobzol
Add spawn APIs for BootstrapCommand to support deferred command execution This PR adds new deferred command support in the ExecutionContext and provides APIs to spawn commands and wait for their completion. This structure enables moving away from the start_process helper functions towards a more unified and reusable command execution flow. r? `@Kobzol`
2 parents 73ca7da + 0512280 commit d593e72

File tree

4 files changed

+176
-118
lines changed

4 files changed

+176
-118
lines changed

src/bootstrap/src/utils/channel.rs

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::path::Path;
1111
use super::execution_context::ExecutionContext;
1212
use super::helpers;
1313
use crate::Build;
14-
use crate::utils::helpers::{start_process, t};
14+
use crate::utils::helpers::t;
1515

1616
#[derive(Clone, Default)]
1717
pub enum GitInfo {
@@ -46,7 +46,7 @@ impl GitInfo {
4646

4747
let mut git_command = helpers::git(Some(dir));
4848
git_command.arg("rev-parse");
49-
let output = git_command.allow_failure().run_capture(exec_ctx);
49+
let output = git_command.allow_failure().run_capture(&exec_ctx);
5050

5151
if output.is_failure() {
5252
return GitInfo::Absent;
@@ -59,23 +59,32 @@ impl GitInfo {
5959
}
6060

6161
// Ok, let's scrape some info
62-
let ver_date = start_process(
63-
helpers::git(Some(dir))
64-
.arg("log")
65-
.arg("-1")
66-
.arg("--date=short")
67-
.arg("--pretty=format:%cd")
68-
.as_command_mut(),
69-
);
62+
// We use the command's spawn API to execute these commands concurrently, which leads to performance improvements.
63+
let mut git_log_cmd = helpers::git(Some(dir));
64+
let ver_date = git_log_cmd
65+
.arg("log")
66+
.arg("-1")
67+
.arg("--date=short")
68+
.arg("--pretty=format:%cd")
69+
.run_always()
70+
.start_capture_stdout(&exec_ctx);
71+
72+
let mut git_hash_cmd = helpers::git(Some(dir));
7073
let ver_hash =
71-
start_process(helpers::git(Some(dir)).arg("rev-parse").arg("HEAD").as_command_mut());
72-
let short_ver_hash = start_process(
73-
helpers::git(Some(dir)).arg("rev-parse").arg("--short=9").arg("HEAD").as_command_mut(),
74-
);
74+
git_hash_cmd.arg("rev-parse").arg("HEAD").run_always().start_capture_stdout(&exec_ctx);
75+
76+
let mut git_short_hash_cmd = helpers::git(Some(dir));
77+
let short_ver_hash = git_short_hash_cmd
78+
.arg("rev-parse")
79+
.arg("--short=9")
80+
.arg("HEAD")
81+
.run_always()
82+
.start_capture_stdout(&exec_ctx);
83+
7584
GitInfo::Present(Some(Info {
76-
commit_date: ver_date().trim().to_string(),
77-
sha: ver_hash().trim().to_string(),
78-
short_sha: short_ver_hash().trim().to_string(),
85+
commit_date: ver_date.wait_for_output(&exec_ctx).stdout().trim().to_string(),
86+
sha: ver_hash.wait_for_output(&exec_ctx).stdout().trim().to_string(),
87+
short_sha: short_ver_hash.wait_for_output(&exec_ctx).stdout().trim().to_string(),
7988
}))
8089
}
8190

src/bootstrap/src/utils/exec.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
//!
33
//! This module provides a structured way to execute and manage commands efficiently,
44
//! ensuring controlled failure handling and output management.
5-
65
use std::ffi::OsStr;
76
use std::fmt::{Debug, Formatter};
87
use std::path::Path;
@@ -11,7 +10,7 @@ use std::process::{Command, CommandArgs, CommandEnvs, ExitStatus, Output, Stdio}
1110
use build_helper::ci::CiEnv;
1211
use build_helper::drop_bomb::DropBomb;
1312

14-
use super::execution_context::ExecutionContext;
13+
use super::execution_context::{DeferredCommand, ExecutionContext};
1514

1615
/// What should be done when the command fails.
1716
#[derive(Debug, Copy, Clone)]
@@ -73,7 +72,7 @@ pub struct BootstrapCommand {
7372
drop_bomb: DropBomb,
7473
}
7574

76-
impl BootstrapCommand {
75+
impl<'a> BootstrapCommand {
7776
#[track_caller]
7877
pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
7978
Command::new(program).into()
@@ -158,6 +157,24 @@ impl BootstrapCommand {
158157
exec_ctx.as_ref().run(self, OutputMode::Capture, OutputMode::Print)
159158
}
160159

160+
/// Spawn the command in background, while capturing and returning all its output.
161+
#[track_caller]
162+
pub fn start_capture(
163+
&'a mut self,
164+
exec_ctx: impl AsRef<ExecutionContext>,
165+
) -> DeferredCommand<'a> {
166+
exec_ctx.as_ref().start(self, OutputMode::Capture, OutputMode::Capture)
167+
}
168+
169+
/// Spawn the command in background, while capturing and returning stdout, and printing stderr.
170+
#[track_caller]
171+
pub fn start_capture_stdout(
172+
&'a mut self,
173+
exec_ctx: impl AsRef<ExecutionContext>,
174+
) -> DeferredCommand<'a> {
175+
exec_ctx.as_ref().start(self, OutputMode::Capture, OutputMode::Print)
176+
}
177+
161178
/// Provides access to the stdlib Command inside.
162179
/// FIXME: This function should be eventually removed from bootstrap.
163180
pub fn as_command_mut(&mut self) -> &mut Command {

src/bootstrap/src/utils/execution_context.rs

Lines changed: 130 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
//! This module provides the [`ExecutionContext`] type, which holds global configuration
44
//! relevant during the execution of commands in bootstrap. This includes dry-run
55
//! mode, verbosity level, and behavior on failure.
6+
use std::panic::Location;
7+
use std::process::Child;
68
use std::sync::{Arc, Mutex};
79

810
use crate::core::config::DryRun;
@@ -80,23 +82,24 @@ impl ExecutionContext {
8082
/// Note: Ideally, you should use one of the BootstrapCommand::run* functions to
8183
/// execute commands. They internally call this method.
8284
#[track_caller]
83-
pub fn run(
85+
pub fn start<'a>(
8486
&self,
85-
command: &mut BootstrapCommand,
87+
command: &'a mut BootstrapCommand,
8688
stdout: OutputMode,
8789
stderr: OutputMode,
88-
) -> CommandOutput {
90+
) -> DeferredCommand<'a> {
8991
command.mark_as_executed();
92+
93+
let created_at = command.get_created_location();
94+
let executed_at = std::panic::Location::caller();
95+
9096
if self.dry_run() && !command.run_always {
91-
return CommandOutput::default();
97+
return DeferredCommand { process: None, stdout, stderr, command, executed_at };
9298
}
9399

94100
#[cfg(feature = "tracing")]
95101
let _run_span = trace_cmd!(command);
96102

97-
let created_at = command.get_created_location();
98-
let executed_at = std::panic::Location::caller();
99-
100103
self.verbose(|| {
101104
println!("running: {command:?} (created at {created_at}, executed at {executed_at})")
102105
});
@@ -105,92 +108,149 @@ impl ExecutionContext {
105108
cmd.stdout(stdout.stdio());
106109
cmd.stderr(stderr.stdio());
107110

108-
let output = cmd.output();
111+
let child = cmd.spawn();
109112

110-
use std::fmt::Write;
113+
DeferredCommand { process: Some(child), stdout, stderr, command, executed_at }
114+
}
111115

112-
let mut message = String::new();
113-
let output: CommandOutput = match output {
114-
// Command has succeeded
115-
Ok(output) if output.status.success() => {
116-
CommandOutput::from_output(output, stdout, stderr)
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.");
117147
}
118-
// Command has started, but then it failed
119-
Ok(output) => {
120-
writeln!(
121-
message,
122-
r#"
123-
Command {command:?} did not execute successfully.
148+
}
149+
exit!(1);
150+
}
151+
}
152+
153+
impl AsRef<ExecutionContext> for ExecutionContext {
154+
fn as_ref(&self) -> &ExecutionContext {
155+
self
156+
}
157+
}
158+
159+
pub 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+
167+
impl<'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#"
194+
Command {:?} did not execute successfully.
124195
Expected success, got {}
125196
Created at: {created_at}
126197
Executed at: {executed_at}"#,
127-
output.status,
128-
)
129-
.unwrap();
198+
self.command, result.status,
199+
)
200+
.unwrap();
201+
202+
let output = CommandOutput::from_output(result, self.stdout, self.stderr);
130203

131-
let output: CommandOutput = CommandOutput::from_output(output, stdout, stderr);
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+
}
132210

133-
// If the output mode is OutputMode::Capture, we can now print the output.
134-
// If it is OutputMode::Print, then the output has already been printed to
135-
// stdout/stderr, and we thus don't have anything captured to print anyway.
136-
if stdout.captures() {
137-
writeln!(message, "\nSTDOUT ----\n{}", output.stdout().trim()).unwrap();
211+
output
138212
}
139-
if stderr.captures() {
140-
writeln!(message, "\nSTDERR ----\n{}", output.stderr().trim()).unwrap();
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)
141226
}
142-
output
143-
}
144-
// The command did not even start
227+
},
145228
Err(e) => {
229+
// Failed to spawn the command
230+
use std::fmt::Write;
231+
146232
writeln!(
147233
message,
148-
"\n\nCommand {command:?} did not execute successfully.\
149-
\nIt was not possible to execute the command: {e:?}"
234+
"\n\nCommand {:?} did not execute successfully.\
235+
\nIt was not possible to execute the command: {e:?}",
236+
self.command
150237
)
151238
.unwrap();
152-
CommandOutput::did_not_start(stdout, stderr)
153-
}
154-
};
155239

156-
let fail = |message: &str, output: CommandOutput| -> ! {
157-
if self.is_verbose() {
158-
println!("{message}");
159-
} else {
160-
let (stdout, stderr) = (output.stdout_if_present(), output.stderr_if_present());
161-
// If the command captures output, the user would not see any indication that
162-
// it has failed. In this case, print a more verbose error, since to provide more
163-
// context.
164-
if stdout.is_some() || stderr.is_some() {
165-
if let Some(stdout) =
166-
output.stdout_if_present().take_if(|s| !s.trim().is_empty())
167-
{
168-
println!("STDOUT:\n{stdout}\n");
169-
}
170-
if let Some(stderr) =
171-
output.stderr_if_present().take_if(|s| !s.trim().is_empty())
172-
{
173-
println!("STDERR:\n{stderr}\n");
174-
}
175-
println!("Command {command:?} has failed. Rerun with -v to see more details.");
176-
} else {
177-
println!("Command has failed. Rerun with -v to see more details.");
178-
}
240+
CommandOutput::did_not_start(self.stdout, self.stderr)
179241
}
180-
exit!(1);
181242
};
182243

183244
if !output.is_success() {
184-
match command.failure_behavior {
245+
match self.command.failure_behavior {
185246
BehaviorOnFailure::DelayFail => {
186-
if self.fail_fast {
187-
fail(&message, output);
247+
if exec_ctx.fail_fast {
248+
exec_ctx.fail(&message, output);
188249
}
189-
190-
self.add_to_delay_failure(message);
250+
exec_ctx.add_to_delay_failure(message);
191251
}
192252
BehaviorOnFailure::Exit => {
193-
fail(&message, output);
253+
exec_ctx.fail(&message, output);
194254
}
195255
BehaviorOnFailure::Ignore => {
196256
// If failures are allowed, either the error has been printed already
@@ -199,6 +259,7 @@ Executed at: {executed_at}"#,
199259
}
200260
}
201261
}
262+
202263
output
203264
}
204265
}

0 commit comments

Comments
 (0)