use std::ffi::OsStr;
use std::path::Path;
use std::process::Stdio;
use std::time::{Duration, Instant};
use tokio::process::Command;
use tracing::{debug, instrument};
use crate::error::{Error, Fix};
#[derive(Debug, Clone)]
pub struct CommandOutput {
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
pub duration: Duration,
}
impl CommandOutput {
pub fn success(&self) -> bool {
self.exit_code == 0
}
}
#[derive(Debug, Clone, Default)]
pub struct CommandRunner {
pub working_dir: Option<std::path::PathBuf>,
pub env: Vec<(String, String)>,
pub inherit_env: bool,
}
impl CommandRunner {
pub fn new() -> Self {
Self {
working_dir: None,
env: Vec::new(),
inherit_env: true,
}
}
pub fn with_working_dir(mut self, dir: impl AsRef<Path>) -> Self {
self.working_dir = Some(dir.as_ref().to_path_buf());
self
}
pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.push((key.into(), value.into()));
self
}
pub fn with_ghc_bin(mut self, bin_dir: impl AsRef<Path>) -> Self {
let current_path = self
.env
.iter()
.rev()
.find(|(k, _)| k == "PATH")
.map(|(_, v)| v.clone())
.unwrap_or_else(|| std::env::var("PATH").unwrap_or_default());
let bin_str = bin_dir.as_ref().to_string_lossy();
#[cfg(windows)]
let separator = ";";
#[cfg(not(windows))]
let separator = ":";
let new_path = format!("{}{}{}", bin_str, separator, current_path);
self.env.push(("PATH".into(), new_path));
self
}
#[instrument(skip(self, args), fields(program = %program.as_ref().to_string_lossy()))]
pub async fn run<S, I>(&self, program: S, args: I) -> Result<CommandOutput, Error>
where
S: AsRef<OsStr>,
I: IntoIterator<Item = S>,
{
let program_ref = program.as_ref();
let args_vec: Vec<_> = args
.into_iter()
.map(|a| a.as_ref().to_os_string())
.collect();
debug!(
"Running command: {} {:?}",
program_ref.to_string_lossy(),
args_vec
);
let mut cmd = Command::new(program_ref);
cmd.args(&args_vec)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if let Some(ref dir) = self.working_dir {
cmd.current_dir(dir);
}
if !self.inherit_env {
cmd.env_clear();
}
for (key, value) in &self.env {
cmd.env(key, value);
}
let start = Instant::now();
let output = cmd.output().await.map_err(|e| {
let program_str = program_ref.to_string_lossy().to_string();
if e.kind() == std::io::ErrorKind::NotFound {
Error::ToolchainMissing {
tool: program_str.clone(),
source: Some(Box::new(e)),
fixes: vec![Fix::with_command(
format!("Install {}", program_str),
"hx toolchain install".to_string(),
)],
}
} else {
Error::Io {
message: format!("failed to execute {}", program_str),
path: None,
source: e,
}
}
})?;
let duration = start.elapsed();
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(-1);
debug!(
exit_code = exit_code,
duration_ms = duration.as_millis(),
"Command completed"
);
Ok(CommandOutput {
exit_code,
stdout,
stderr,
duration,
})
}
pub async fn run_checked<S, I>(&self, program: S, args: I) -> Result<CommandOutput, Error>
where
S: AsRef<OsStr>,
I: IntoIterator<Item = S>,
{
let program_str = program.as_ref().to_string_lossy().to_string();
let output = self.run(program, args).await?;
if !output.success() {
return Err(Error::CommandFailed {
command: program_str,
exit_code: Some(output.exit_code),
stdout: output.stdout,
stderr: output.stderr,
fixes: vec![],
});
}
Ok(output)
}
}