Skip to content

Commit

Permalink
Wrap std::process::Command to track arguments and environment varia…
Browse files Browse the repository at this point in the history
…bles

Before, this was accomplished by using `get_env` and `get_args` manually, but these features are unavailable in 1.30 (current MSRV). To get around this, `Command` is wrapped as `WrappedCommand` and tracks any environment variables or arguments that are set by the caller. Then, the `Display` implementation actually handles pretty-printing the invocation, with the added benefit that other errors can take advantage of this nicer output!
  • Loading branch information
Finchiedev committed Dec 19, 2023
1 parent d5be171 commit a0640aa
Showing 1 changed file with 90 additions and 30 deletions.
120 changes: 90 additions & 30 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,23 @@ use std::env;
use std::error;
use std::ffi::{OsStr, OsString};
use std::fmt;
use std::fmt::Display;
use std::io;
use std::ops::{Bound, RangeBounds};
use std::path::PathBuf;
use std::process::{Command, Output};
use std::str;

/// Wrapper struct to polyfill methods introduced in 1.57 (`get_envs`, `get_args` etc).
/// This is needed to reconstruct the pkg-config command for output in a copy-
/// paste friendly format via `Display`.
struct WrappedCommand {
inner: Command,
program: OsString,
env_vars: Vec<(OsString, OsString)>,
args: Vec<OsString>,
}

#[derive(Clone, Debug)]
pub struct Config {
statik: Option<bool>,
Expand Down Expand Up @@ -148,6 +159,81 @@ pub enum Error {
__Nonexhaustive,
}

impl WrappedCommand {
fn new<S: AsRef<OsStr>>(program: S) -> Self {
Self {
inner: Command::new(program.as_ref()),
program: program.as_ref().to_os_string(),
env_vars: Vec::new(),
args: Vec::new(),
}
}

fn args<I, S>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = S> + Clone,
S: AsRef<OsStr>,
{
self.inner.args(args.clone());
self.args
.extend(args.into_iter().map(|arg| arg.as_ref().to_os_string()));

self
}

fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
self.inner.arg(arg.as_ref());
self.args.push(arg.as_ref().to_os_string());

self
}

fn env<K, V>(&mut self, key: K, value: V) -> &mut Self
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.inner.env(key.as_ref(), value.as_ref());
self.env_vars
.push((key.as_ref().to_os_string(), value.as_ref().to_os_string()));

self
}

fn output(&mut self) -> io::Result<Output> {
self.inner.output()
}
}

/// Output a command invocation that can be copy-pasted into the terminal.
/// `Command`'s existing debug implementation is not used for that reason,
/// as it can sometimes lead to output such as:
/// `PKG_CONFIG_ALLOW_SYSTEM_CFLAGS="1" PKG_CONFIG_ALLOW_SYSTEM_LIBS="1" "pkg-config" "--libs" "--cflags" "mylibrary"`
/// Which cannot be copy-pasted into terminals such as nushell, and is a bit noisy.
/// This will look something like:
/// `PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1 PKG_CONFIG_ALLOW_SYSTEM_LIBS=1 pkg-config --libs --cflags mylibrary`
impl Display for WrappedCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Format all explicitly defined environment variables
let envs = self
.env_vars
.iter()
.map(|(env, arg)| format!("{}={}", env.to_string_lossy(), arg.to_string_lossy()))
.collect::<Vec<String>>()
.join(" ");

// Format all pkg-config arguments
let args = self
.args
.iter()
.map(|arg| arg.to_string_lossy().to_string())
.collect::<Vec<String>>()
.join(" ");

write!(f, "{} {} {}", envs, self.program.to_string_lossy(), args)
}
}

impl error::Error for Error {}

impl fmt::Debug for Error {
Expand Down Expand Up @@ -558,47 +644,21 @@ impl Config {
if output.status.success() {
Ok(output.stdout)
} else {
// Collect all explicitly-defined environment variables
// this is used to display the equivalent pkg-config shell invocation
let envs = cmd
.get_envs()
.map(|(env, optional_arg)| {
if let Some(arg) = optional_arg {
format!("{}={}", env.to_string_lossy(), arg.to_string_lossy())
} else {
env.to_string_lossy().to_string()
}
})
.collect::<Vec<String>>();

// Collect arguments for the same reason
let args = cmd
.get_args()
.map(|arg| arg.to_string_lossy().to_string())
.collect::<Vec<String>>();

// This will look something like:
// PKG_CONFIG_ALLOW_SYSTEM_CFLAGS=1 PKG_CONFIG_ALLOW_SYSTEM_LIBS=1 pkg-config --libs --cflags {library}
Err(Error::Failure {
command: format!(
"{} {} {}",
envs.join(" "),
cmd.get_program().to_string_lossy(),
args.join(" ")
),
command: format!("{}", cmd),
output,
})
}
}
Err(cause) => Err(Error::Command {
command: format!("{:?}", cmd),
command: format!("{}", cmd),
cause,
}),
}
}

fn command(&self, exe: OsString, name: &str, args: &[&str]) -> Command {
let mut cmd = Command::new(exe);
fn command(&self, exe: OsString, name: &str, args: &[&str]) -> WrappedCommand {
let mut cmd = WrappedCommand::new(exe);
if self.is_static(name) {
cmd.arg("--static");
}
Expand Down

0 comments on commit a0640aa

Please sign in to comment.