use std::{
cell::Cell,
collections::BTreeMap,
ffi::OsString,
fmt,
path::PathBuf,
process::{ExitStatus, Output},
str,
};
use anyhow::{Context as _, Error, Result};
use shell_escape::escape;
macro_rules! cmd {
($program:expr $(, $arg:expr)* $(,)?) => {{
let mut _cmd = $crate::process::ProcessBuilder::new($program);
$(
_cmd.arg($arg);
)*
_cmd
}};
}
#[must_use]
#[derive(Clone)]
pub(crate) struct ProcessBuilder {
program: OsString,
args: Vec<OsString>,
env: BTreeMap<String, Option<OsString>>,
dir: Option<PathBuf>,
stdout_to_stderr: bool,
display_env_vars: Cell<bool>,
}
impl From<cargo_config2::PathAndArgs> for ProcessBuilder {
fn from(value: cargo_config2::PathAndArgs) -> Self {
let mut cmd = ProcessBuilder::new(value.path);
cmd.args(value.args);
cmd
}
}
impl ProcessBuilder {
pub(crate) fn new(program: impl Into<OsString>) -> Self {
let mut this = Self {
program: program.into(),
args: vec![],
env: BTreeMap::new(),
dir: None,
stdout_to_stderr: false,
display_env_vars: Cell::new(false),
};
this.env_remove("LLVM_COV_FLAGS");
this.env_remove("LLVM_PROFDATA_FLAGS");
this
}
pub(crate) fn arg(&mut self, arg: impl Into<OsString>) -> &mut Self {
self.args.push(arg.into());
self
}
pub(crate) fn args(
&mut self,
args: impl IntoIterator<Item = impl Into<OsString>>,
) -> &mut Self {
self.args.extend(args.into_iter().map(Into::into));
self
}
pub(crate) fn env(&mut self, key: impl Into<String>, val: impl Into<OsString>) -> &mut Self {
self.env.insert(key.into(), Some(val.into()));
self
}
pub(crate) fn env_remove(&mut self, key: impl Into<String>) -> &mut Self {
self.env.insert(key.into(), None);
self
}
pub(crate) fn dir(&mut self, path: impl Into<PathBuf>) -> &mut Self {
self.dir = Some(path.into());
self
}
pub(crate) fn stdout_to_stderr(&mut self) -> &mut Self {
self.stdout_to_stderr = true;
self
}
pub(crate) fn display_env_vars(&mut self) -> &mut Self {
self.display_env_vars.set(true);
self
}
pub(crate) fn run(&mut self) -> Result<Output> {
let output = self.build().unchecked().run().with_context(|| {
process_error(format!("could not execute process {self}"), None, None)
})?;
if output.status.success() {
Ok(output)
} else {
Err(process_error(
format!("process didn't exit successfully: {self}"),
Some(output.status),
Some(&output),
))
}
}
pub(crate) fn run_with_output(&mut self) -> Result<Output> {
let output =
self.build().stdout_capture().stderr_capture().unchecked().run().with_context(
|| process_error(format!("could not execute process {self}"), None, None),
)?;
if output.status.success() {
Ok(output)
} else {
Err(process_error(
format!("process didn't exit successfully: {self}"),
Some(output.status),
Some(&output),
))
}
}
pub(crate) fn read(&mut self) -> Result<String> {
assert!(!self.stdout_to_stderr);
let mut output = String::from_utf8(self.run_with_output()?.stdout)
.with_context(|| format!("failed to parse output from {self}"))?;
while output.ends_with('\n') || output.ends_with('\r') {
output.pop();
}
Ok(output)
}
fn build(&self) -> duct::Expression {
let mut cmd = duct::cmd(&*self.program, &self.args);
for (k, v) in &self.env {
match v {
Some(v) => {
cmd = cmd.env(k, v);
}
None => {
cmd = cmd.env_remove(k);
}
}
}
if let Some(path) = &self.dir {
cmd = cmd.dir(path);
}
if self.stdout_to_stderr {
cmd = cmd.stdout_to_stderr();
}
cmd
}
}
impl fmt::Display for ProcessBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("`")?;
if self.display_env_vars.get() {
for (key, val) in &self.env {
if let Some(val) = val {
let val = escape(val.to_string_lossy());
if is_unix_terminal() {
write!(f, "{key}={val} ")?;
} else {
write!(f, "set {key}={val}&& ")?;
}
}
}
}
f.write_str(&escape(self.program.to_string_lossy()))?;
for arg in &self.args {
write!(f, " {}", escape(arg.to_string_lossy()))?;
}
f.write_str("`")?;
Ok(())
}
}
fn process_error(mut msg: String, status: Option<ExitStatus>, output: Option<&Output>) -> Error {
match status {
Some(s) => {
msg.push_str(" (");
msg.push_str(&s.to_string());
msg.push(')');
}
None => msg.push_str(" (never executed)"),
}
if let Some(out) = output {
match str::from_utf8(&out.stdout) {
Ok(s) if !s.trim().is_empty() => {
msg.push_str("\n--- stdout\n");
msg.push_str(s);
}
Ok(_) | Err(_) => {}
}
match str::from_utf8(&out.stderr) {
Ok(s) if !s.trim().is_empty() => {
msg.push_str("\n--- stderr\n");
msg.push_str(s);
}
Ok(_) | Err(_) => {}
}
}
Error::msg(msg)
}
fn is_unix_terminal() -> bool {
cfg!(unix) || std::env::var_os("MSYSTEM").is_some()
}