cargo/crates/cargo-util/src/process_builder.rs

418 lines
14 KiB
Rust
Raw Normal View History

2021-03-20 18:28:38 +00:00
use crate::process_error::ProcessError;
use crate::read2;
use anyhow::{bail, Context, Result};
use jobserver::Client;
use shell_escape::escape;
use std::collections::BTreeMap;
2015-02-06 07:27:53 +00:00
use std::env;
2018-03-14 15:17:44 +00:00
use std::ffi::{OsStr, OsString};
use std::fmt;
use std::iter::once;
use std::path::Path;
2018-03-14 15:17:44 +00:00
use std::process::{Command, Output, Stdio};
2014-03-19 18:44:43 +00:00
2021-03-20 18:28:38 +00:00
/// A builder object for an external process, similar to [`std::process::Command`].
Add a GNU make jobserver implementation to Cargo This commit adds a GNU make jobserver implementation to Cargo, both as a client of existing jobservers and also a creator of new jobservers. The jobserver is actually just an IPC semaphore which manifests itself as a pipe with N bytes of tokens on Unix and a literal IPC semaphore on Windows. The rough protocol is then if you want to run a job you read acquire the semaphore (read a byte on Unix or wait on the semaphore on Windows) and then you release it when you're done. All the hairy details of the jobserver implementation are housed in the `jobserver` crate on crates.io instead of Cargo. This should hopefully make it much easier for the compiler to also share a jobserver implementation eventually. The main tricky bit here is that on Unix and Windows acquiring a jobserver token will block the calling thread. We need to either way for a running job to exit or to acquire a new token when we want to spawn a new job. To handle this the current implementation spawns a helper thread that does the blocking and sends a message back to Cargo when it receives a token. It's a little trickier with shutting down this thread gracefully as well but more details can be found in the `jobserver` crate. Unfortunately crates are unlikely to see an immediate benefit of this once implemented. Most crates are run with a manual `make -jN` and this overrides the jobserver in the environment, creating a new jobserver in the sub-make. If the `-jN` argument is removed, however, then `make` will share Cargo's jobserver and properly limit parallelism. Closes #1744
2017-05-30 04:09:53 +00:00
#[derive(Clone, Debug)]
2014-03-19 18:44:43 +00:00
pub struct ProcessBuilder {
2017-09-28 14:01:56 +00:00
/// The program to execute.
program: OsString,
2017-09-28 14:01:56 +00:00
/// A list of arguments to pass to the program.
args: Vec<OsString>,
2017-09-28 14:01:56 +00:00
/// Any environment variables that should be set for the program.
env: BTreeMap<String, Option<OsString>>,
2019-02-03 04:01:23 +00:00
/// The directory to run the program from.
cwd: Option<OsString>,
2021-03-20 18:28:38 +00:00
/// The `make` jobserver. See the [jobserver crate] for
2017-09-28 14:01:56 +00:00
/// more information.
///
2021-03-20 18:28:38 +00:00
/// [jobserver crate]: https://docs.rs/jobserver/
Add a GNU make jobserver implementation to Cargo This commit adds a GNU make jobserver implementation to Cargo, both as a client of existing jobservers and also a creator of new jobservers. The jobserver is actually just an IPC semaphore which manifests itself as a pipe with N bytes of tokens on Unix and a literal IPC semaphore on Windows. The rough protocol is then if you want to run a job you read acquire the semaphore (read a byte on Unix or wait on the semaphore on Windows) and then you release it when you're done. All the hairy details of the jobserver implementation are housed in the `jobserver` crate on crates.io instead of Cargo. This should hopefully make it much easier for the compiler to also share a jobserver implementation eventually. The main tricky bit here is that on Unix and Windows acquiring a jobserver token will block the calling thread. We need to either way for a running job to exit or to acquire a new token when we want to spawn a new job. To handle this the current implementation spawns a helper thread that does the blocking and sends a message back to Cargo when it receives a token. It's a little trickier with shutting down this thread gracefully as well but more details can be found in the `jobserver` crate. Unfortunately crates are unlikely to see an immediate benefit of this once implemented. Most crates are run with a manual `make -jN` and this overrides the jobserver in the environment, creating a new jobserver in the sub-make. If the `-jN` argument is removed, however, then `make` will share Cargo's jobserver and properly limit parallelism. Closes #1744
2017-05-30 04:09:53 +00:00
jobserver: Option<Client>,
2019-02-03 04:01:23 +00:00
/// `true` to include environment variable in display.
2019-01-27 13:39:49 +00:00
display_env_vars: bool,
2014-03-19 18:44:43 +00:00
}
2015-01-23 18:42:29 +00:00
impl fmt::Display for ProcessBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "`")?;
if self.display_env_vars {
for (key, val) in self.env.iter() {
if let Some(val) = val {
let val = escape(val.to_string_lossy());
if cfg!(windows) {
write!(f, "set {}={}&& ", key, val)?;
} else {
write!(f, "{}={} ", key, val)?;
}
}
}
}
write!(f, "{}", self.program.to_string_lossy())?;
2014-05-09 00:50:10 +00:00
2017-09-22 13:56:47 +00:00
for arg in &self.args {
write!(f, " {}", escape(arg.to_string_lossy()))?;
2014-05-09 00:50:10 +00:00
}
2014-05-20 05:40:50 +00:00
write!(f, "`")
2014-05-09 00:50:10 +00:00
}
}
2014-03-19 18:44:43 +00:00
impl ProcessBuilder {
2021-03-20 18:28:38 +00:00
/// Creates a new [`ProcessBuilder`] with the given executable path.
pub fn new<T: AsRef<OsStr>>(cmd: T) -> ProcessBuilder {
ProcessBuilder {
program: cmd.as_ref().to_os_string(),
args: Vec::new(),
cwd: None,
env: BTreeMap::new(),
jobserver: None,
display_env_vars: false,
}
}
2019-02-03 04:01:23 +00:00
/// (chainable) Sets the executable for the process.
pub fn program<T: AsRef<OsStr>>(&mut self, program: T) -> &mut ProcessBuilder {
self.program = program.as_ref().to_os_string();
self
}
2019-02-03 04:01:23 +00:00
/// (chainable) Adds `arg` to the args list.
pub fn arg<T: AsRef<OsStr>>(&mut self, arg: T) -> &mut ProcessBuilder {
self.args.push(arg.as_ref().to_os_string());
self
}
2019-02-03 04:01:23 +00:00
/// (chainable) Adds multiple `args` to the args list.
pub fn args<T: AsRef<OsStr>>(&mut self, args: &[T]) -> &mut ProcessBuilder {
2018-03-14 15:17:44 +00:00
self.args
2019-02-03 04:01:23 +00:00
.extend(args.iter().map(|t| t.as_ref().to_os_string()));
self
}
2019-02-03 04:01:23 +00:00
/// (chainable) Replaces the args list with the given `args`.
pub fn args_replace<T: AsRef<OsStr>>(&mut self, args: &[T]) -> &mut ProcessBuilder {
2019-03-21 22:57:14 +00:00
self.args = args.iter().map(|t| t.as_ref().to_os_string()).collect();
self
}
2019-02-03 04:01:23 +00:00
/// (chainable) Sets the current working directory of the process.
pub fn cwd<T: AsRef<OsStr>>(&mut self, path: T) -> &mut ProcessBuilder {
self.cwd = Some(path.as_ref().to_os_string());
self
}
2019-02-03 04:01:23 +00:00
/// (chainable) Sets an environment variable for the process.
2018-03-14 15:17:44 +00:00
pub fn env<T: AsRef<OsStr>>(&mut self, key: &str, val: T) -> &mut ProcessBuilder {
self.env
.insert(key.to_string(), Some(val.as_ref().to_os_string()));
self
}
2019-02-03 04:01:23 +00:00
/// (chainable) Unsets an environment variable for the process.
pub fn env_remove(&mut self, key: &str) -> &mut ProcessBuilder {
self.env.insert(key.to_string(), None);
2014-05-28 00:21:28 +00:00
self
}
2019-02-03 04:01:23 +00:00
/// Gets the executable name.
pub fn get_program(&self) -> &OsString {
&self.program
}
2019-02-03 04:01:23 +00:00
/// Gets the program arguments.
pub fn get_args(&self) -> &[OsString] {
&self.args
}
2019-02-03 04:01:23 +00:00
/// Gets the current working directory for the process.
pub fn get_cwd(&self) -> Option<&Path> {
self.cwd.as_ref().map(Path::new)
}
2019-02-03 04:01:23 +00:00
/// Gets an environment variable as the process will see it (will inherit from environment
2017-09-28 14:01:56 +00:00
/// unless explicitally unset).
pub fn get_env(&self, var: &str) -> Option<OsString> {
2018-03-14 15:17:44 +00:00
self.env
.get(var)
.cloned()
.or_else(|| Some(env::var_os(var)))
.and_then(|s| s)
}
2019-02-03 04:01:23 +00:00
/// Gets all environment variables explicitly set or unset for the process (not inherited
2017-09-28 14:01:56 +00:00
/// vars).
pub fn get_envs(&self) -> &BTreeMap<String, Option<OsString>> {
2018-03-14 15:17:44 +00:00
&self.env
}
2019-02-03 04:01:23 +00:00
/// Sets the `make` jobserver. See the [jobserver crate][jobserver_docs] for
2017-09-28 14:01:56 +00:00
/// more information.
///
/// [jobserver_docs]: https://docs.rs/jobserver/0.1.6/jobserver/
Add a GNU make jobserver implementation to Cargo This commit adds a GNU make jobserver implementation to Cargo, both as a client of existing jobservers and also a creator of new jobservers. The jobserver is actually just an IPC semaphore which manifests itself as a pipe with N bytes of tokens on Unix and a literal IPC semaphore on Windows. The rough protocol is then if you want to run a job you read acquire the semaphore (read a byte on Unix or wait on the semaphore on Windows) and then you release it when you're done. All the hairy details of the jobserver implementation are housed in the `jobserver` crate on crates.io instead of Cargo. This should hopefully make it much easier for the compiler to also share a jobserver implementation eventually. The main tricky bit here is that on Unix and Windows acquiring a jobserver token will block the calling thread. We need to either way for a running job to exit or to acquire a new token when we want to spawn a new job. To handle this the current implementation spawns a helper thread that does the blocking and sends a message back to Cargo when it receives a token. It's a little trickier with shutting down this thread gracefully as well but more details can be found in the `jobserver` crate. Unfortunately crates are unlikely to see an immediate benefit of this once implemented. Most crates are run with a manual `make -jN` and this overrides the jobserver in the environment, creating a new jobserver in the sub-make. If the `-jN` argument is removed, however, then `make` will share Cargo's jobserver and properly limit parallelism. Closes #1744
2017-05-30 04:09:53 +00:00
pub fn inherit_jobserver(&mut self, jobserver: &Client) -> &mut Self {
self.jobserver = Some(jobserver.clone());
self
}
2019-02-03 04:01:23 +00:00
/// Enables environment variable display.
pub fn display_env_vars(&mut self) -> &mut Self {
self.display_env_vars = true;
self
}
2019-02-03 04:01:23 +00:00
/// Runs the process, waiting for completion, and mapping non-success exit codes to an error.
2021-03-20 18:28:38 +00:00
pub fn exec(&self) -> Result<()> {
2014-05-20 05:40:50 +00:00
let mut command = self.build_command();
2021-03-20 18:28:38 +00:00
let exit = command.status().with_context(|| {
ProcessError::new(&format!("could not execute process {}", self), None, None)
})?;
if exit.success() {
Ok(())
} else {
2021-03-20 18:28:38 +00:00
Err(ProcessError::new(
&format!("process didn't exit successfully: {}", self),
Some(exit),
2018-03-14 15:17:44 +00:00
None,
2018-12-08 11:19:47 +00:00
)
.into())
}
}
/// Replaces the current process with the target process.
///
2019-02-03 04:01:23 +00:00
/// On Unix, this executes the process using the Unix syscall `execvp`, which will block
/// this process, and will only return if there is an error.
///
/// On Windows this isn't technically possible. Instead we emulate it to the best of our
2019-02-03 04:01:23 +00:00
/// ability. One aspect we fix here is that we specify a handler for the Ctrl-C handler.
/// In doing so (and by effectively ignoring it) we should emulate proxying Ctrl-C
/// handling to the application at hand, which will either terminate or handle it itself.
2019-02-03 04:01:23 +00:00
/// According to Microsoft's documentation at
/// <https://docs.microsoft.com/en-us/windows/console/ctrl-c-and-ctrl-break-signals>.
/// the Ctrl-C signal is sent to all processes attached to a terminal, which should
/// include our child process. If the child terminates then we'll reap them in Cargo
/// pretty quickly, and if the child handles the signal then we won't terminate
/// (and we shouldn't!) until the process itself later exits.
2021-03-20 18:28:38 +00:00
pub fn exec_replace(&self) -> Result<()> {
imp::exec_replace(self)
}
2019-02-03 04:01:23 +00:00
/// Executes the process, returning the stdio output, or an error if non-zero exit status.
2021-03-20 18:28:38 +00:00
pub fn exec_with_output(&self) -> Result<Output> {
let mut command = self.build_command();
2021-03-20 18:28:38 +00:00
let output = command.output().with_context(|| {
ProcessError::new(&format!("could not execute process {}", self), None, None)
})?;
if output.status.success() {
Ok(output)
} else {
2021-03-20 18:28:38 +00:00
Err(ProcessError::new(
&format!("process didn't exit successfully: {}", self),
Some(output.status),
2018-03-14 15:17:44 +00:00
Some(&output),
2018-12-08 11:19:47 +00:00
)
.into())
}
}
2019-02-03 04:01:23 +00:00
/// Executes a command, passing each line of stdout and stderr to the supplied callbacks, which
2017-09-28 14:01:56 +00:00
/// can mutate the string data.
///
/// If any invocations of these function return an error, it will be propagated.
///
2019-05-12 00:35:25 +00:00
/// If `capture_output` is true, then all the output will also be buffered
/// and stored in the returned `Output` object. If it is false, no caching
/// is done, and the callbacks are solely responsible for handling the
/// output.
2018-03-14 15:17:44 +00:00
pub fn exec_with_streaming(
&self,
2021-03-20 18:28:38 +00:00
on_stdout_line: &mut dyn FnMut(&str) -> Result<()>,
on_stderr_line: &mut dyn FnMut(&str) -> Result<()>,
capture_output: bool,
2021-03-20 18:28:38 +00:00
) -> Result<Output> {
2016-09-14 18:10:30 +00:00
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let mut cmd = self.build_command();
cmd.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null());
let mut callback_error = None;
let status = (|| {
let mut child = cmd.spawn()?;
2016-09-14 18:10:30 +00:00
let out = child.stdout.take().unwrap();
let err = child.stderr.take().unwrap();
read2(out, err, &mut |is_out, data, eof| {
2016-09-14 18:10:30 +00:00
let idx = if eof {
data.len()
} else {
match data.iter().rposition(|b| *b == b'\n') {
Some(i) => i + 1,
None => return,
}
};
2018-12-08 11:19:47 +00:00
{
// scope for new_lines
let new_lines = if capture_output {
let dst = if is_out { &mut stdout } else { &mut stderr };
let start = dst.len();
let data = data.drain(..idx);
dst.extend(data);
&dst[start..]
2016-09-14 18:10:30 +00:00
} else {
&data[..idx]
};
for line in String::from_utf8_lossy(new_lines).lines() {
if callback_error.is_some() {
break;
}
let callback_result = if is_out {
on_stdout_line(line)
} else {
on_stderr_line(line)
};
if let Err(e) = callback_result {
callback_error = Some(e);
}
2016-09-14 18:10:30 +00:00
}
}
if !capture_output {
data.drain(..idx);
}
})?;
2016-09-14 18:10:30 +00:00
child.wait()
2018-03-14 15:17:44 +00:00
})()
2021-03-20 18:28:38 +00:00
.with_context(|| {
ProcessError::new(&format!("could not execute process {}", self), None, None)
})?;
2016-09-14 18:10:30 +00:00
let output = Output {
2021-03-01 03:03:06 +00:00
status,
stdout,
stderr,
2016-09-14 18:10:30 +00:00
};
{
let to_print = if capture_output { Some(&output) } else { None };
if let Some(e) = callback_error {
2021-03-20 18:28:38 +00:00
let cx = ProcessError::new(
&format!("failed to parse process output: {}", self),
Some(output.status),
to_print,
);
bail!(anyhow::Error::new(cx).context(e));
} else if !output.status.success() {
2021-03-20 18:28:38 +00:00
bail!(ProcessError::new(
&format!("process didn't exit successfully: {}", self),
Some(output.status),
to_print,
));
}
2016-09-14 18:10:30 +00:00
}
Ok(output)
2016-09-14 18:10:30 +00:00
}
2019-02-03 04:01:23 +00:00
/// Converts `ProcessBuilder` into a `std::process::Command`, and handles the jobserver, if
2017-09-28 14:01:56 +00:00
/// present.
pub fn build_command(&self) -> Command {
2015-01-13 16:41:04 +00:00
let mut command = Command::new(&self.program);
if let Some(cwd) = self.get_cwd() {
command.current_dir(cwd);
}
2017-09-22 13:56:47 +00:00
for arg in &self.args {
2015-01-13 16:41:04 +00:00
command.arg(arg);
}
2017-09-22 13:56:47 +00:00
for (k, v) in &self.env {
match *v {
2018-03-14 15:17:44 +00:00
Some(ref v) => {
command.env(k, v);
}
None => {
command.env_remove(k);
}
}
}
Add a GNU make jobserver implementation to Cargo This commit adds a GNU make jobserver implementation to Cargo, both as a client of existing jobservers and also a creator of new jobservers. The jobserver is actually just an IPC semaphore which manifests itself as a pipe with N bytes of tokens on Unix and a literal IPC semaphore on Windows. The rough protocol is then if you want to run a job you read acquire the semaphore (read a byte on Unix or wait on the semaphore on Windows) and then you release it when you're done. All the hairy details of the jobserver implementation are housed in the `jobserver` crate on crates.io instead of Cargo. This should hopefully make it much easier for the compiler to also share a jobserver implementation eventually. The main tricky bit here is that on Unix and Windows acquiring a jobserver token will block the calling thread. We need to either way for a running job to exit or to acquire a new token when we want to spawn a new job. To handle this the current implementation spawns a helper thread that does the blocking and sends a message back to Cargo when it receives a token. It's a little trickier with shutting down this thread gracefully as well but more details can be found in the `jobserver` crate. Unfortunately crates are unlikely to see an immediate benefit of this once implemented. Most crates are run with a manual `make -jN` and this overrides the jobserver in the environment, creating a new jobserver in the sub-make. If the `-jN` argument is removed, however, then `make` will share Cargo's jobserver and properly limit parallelism. Closes #1744
2017-05-30 04:09:53 +00:00
if let Some(ref c) = self.jobserver {
c.configure(&mut command);
}
command
}
/// Wraps an existing command with the provided wrapper, if it is present and valid.
///
/// # Examples
///
/// ```rust
2021-03-20 18:28:38 +00:00
/// use cargo_util::ProcessBuilder;
/// // Running this would execute `rustc`
2021-03-20 18:28:38 +00:00
/// let cmd = ProcessBuilder::new("rustc");
///
/// // Running this will execute `sccache rustc`
/// let cmd = cmd.wrapped(Some("sccache"));
/// ```
pub fn wrapped(mut self, wrapper: Option<impl AsRef<OsStr>>) -> Self {
let wrapper = if let Some(wrapper) = wrapper.as_ref() {
wrapper.as_ref()
} else {
return self;
};
if wrapper.is_empty() {
return self;
}
let args = once(self.program).chain(self.args.into_iter()).collect();
self.program = wrapper.to_os_string();
self.args = args;
self
}
2014-03-19 18:44:43 +00:00
}
#[cfg(unix)]
mod imp {
2021-03-20 18:28:38 +00:00
use super::{ProcessBuilder, ProcessError};
use anyhow::Result;
use std::os::unix::process::CommandExt;
2021-03-20 18:28:38 +00:00
pub fn exec_replace(process_builder: &ProcessBuilder) -> Result<()> {
let mut command = process_builder.build_command();
let error = command.exec();
2021-03-20 18:28:38 +00:00
Err(anyhow::Error::from(error).context(ProcessError::new(
2020-06-03 22:44:59 +00:00
&format!("could not execute process {}", process_builder),
None,
None,
)))
}
}
#[cfg(windows)]
mod imp {
2021-03-20 18:28:38 +00:00
use super::{ProcessBuilder, ProcessError};
use anyhow::Result;
2018-12-12 21:20:44 +00:00
use winapi::shared::minwindef::{BOOL, DWORD, FALSE, TRUE};
use winapi::um::consoleapi::SetConsoleCtrlHandler;
unsafe extern "system" fn ctrlc_handler(_: DWORD) -> BOOL {
2019-02-03 04:01:23 +00:00
// Do nothing; let the child process handle it.
TRUE
}
2021-03-20 18:28:38 +00:00
pub fn exec_replace(process_builder: &ProcessBuilder) -> Result<()> {
unsafe {
if SetConsoleCtrlHandler(Some(ctrlc_handler), TRUE) == FALSE {
2021-03-20 18:28:38 +00:00
return Err(ProcessError::new("Could not set Ctrl-C handler.", None, None).into());
}
}
2019-02-03 04:01:23 +00:00
// Just execute the process as normal.
process_builder.exec()
}
}