use std::{
cell::Cell,
collections::BTreeMap,
ffi::OsString,
fmt,
path::PathBuf,
process::{ExitStatus, Output},
str,
};
use anyhow::{Context as _, 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::new(),
env: BTreeMap::new(),
dir: None,
stdout_to_stderr: false,
display_env_vars: Cell::new(false),
};
this.env("CARGO_INCREMENTAL", "0");
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(|| {
ProcessError::new(&format!("could not execute process {self}"), None, None)
})?;
if output.status.success() {
Ok(output)
} else {
Err(ProcessError::new(
&format!("process didn't exit successfully: {self}"),
Some(output.status),
Some(&output),
)
.into())
}
}
pub(crate) fn run_with_output(&mut self) -> Result<Output> {
let output =
self.build().stdout_capture().stderr_capture().unchecked().run().with_context(
|| ProcessError::new(&format!("could not execute process {self}"), None, None),
)?;
if output.status.success() {
Ok(output)
} else {
Err(ProcessError::new(
&format!("process didn't exit successfully: {self}"),
Some(output.status),
Some(&output),
)
.into())
}
}
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 {
write!(f, "`")?;
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 cfg!(windows) {
write!(f, "set {key}={val}&& ")?;
} else {
write!(f, "{key}={val} ")?;
}
}
}
}
write!(f, "{}", self.program.to_string_lossy())?;
for arg in &self.args {
write!(f, " {}", escape(arg.to_string_lossy()))?;
}
write!(f, "`")?;
Ok(())
}
}
#[derive(Debug)]
struct ProcessError {
desc: String,
}
impl ProcessError {
fn new(msg: &str, status: Option<ExitStatus>, output: Option<&Output>) -> Self {
let exit = match status {
Some(s) => s.to_string(),
None => "never executed".to_string(),
};
let mut desc = format!("{msg} ({exit})");
if let Some(out) = output {
match str::from_utf8(&out.stdout) {
Ok(s) if !s.trim().is_empty() => {
desc.push_str("\n--- stdout\n");
desc.push_str(s);
}
Ok(_) | Err(_) => {}
}
match str::from_utf8(&out.stderr) {
Ok(s) if !s.trim().is_empty() => {
desc.push_str("\n--- stderr\n");
desc.push_str(s);
}
Ok(_) | Err(_) => {}
}
}
Self { desc }
}
}
impl fmt::Display for ProcessError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.desc, f)
}
}
impl std::error::Error for ProcessError {}