use std::io::ErrorKind;
use std::path::PathBuf;
use tokio::process::Command;
use super::ForgeFuture;
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct ForgeCommand {
pub(crate) arguments: Vec<String>,
pub(crate) environment: Vec<(String, String)>,
pub(crate) executable: &'static str,
pub(crate) working_directory: Option<PathBuf>,
}
impl ForgeCommand {
pub(crate) fn new(executable: &'static str, arguments: Vec<String>) -> Self {
Self {
arguments,
environment: Vec::new(),
executable,
working_directory: None,
}
}
pub(crate) fn with_environment(mut self, key: &str, value: impl Into<String>) -> Self {
self.environment.push((key.to_string(), value.into()));
self
}
pub(crate) fn with_working_directory(mut self, working_directory: PathBuf) -> Self {
self.working_directory = Some(working_directory);
self
}
pub(crate) fn with_optional_working_directory(
self,
working_directory: Option<PathBuf>,
) -> Self {
match working_directory {
Some(working_directory) => self.with_working_directory(working_directory),
None => self,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct ForgeCommandOutput {
pub(crate) exit_code: Option<i32>,
pub(crate) stderr: String,
pub(crate) stdout: String,
}
impl ForgeCommandOutput {
pub(crate) fn success(&self) -> bool {
self.exit_code == Some(0)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum ForgeCommandError {
ExecutableNotFound { executable: String },
SpawnFailed {
executable: String,
message: String,
},
}
#[cfg_attr(test, mockall::automock)]
pub(crate) trait ForgeCommandRunner: Send + Sync {
fn run(
&self,
command: ForgeCommand,
) -> ForgeFuture<Result<ForgeCommandOutput, ForgeCommandError>>;
}
pub(crate) struct RealForgeCommandRunner;
impl ForgeCommandRunner for RealForgeCommandRunner {
fn run(
&self,
command: ForgeCommand,
) -> ForgeFuture<Result<ForgeCommandOutput, ForgeCommandError>> {
Box::pin(async move { run_command(command).await })
}
}
async fn run_command(command: ForgeCommand) -> Result<ForgeCommandOutput, ForgeCommandError> {
let mut process = Command::new(command.executable);
process.args(&command.arguments);
for (key, value) in &command.environment {
process.env(key, value);
}
if let Some(working_directory) = &command.working_directory {
process.current_dir(working_directory);
}
let output = process.output().await.map_err(|error| {
if error.kind() == ErrorKind::NotFound {
return ForgeCommandError::ExecutableNotFound {
executable: command.executable.to_string(),
};
}
ForgeCommandError::SpawnFailed {
executable: command.executable.to_string(),
message: error.to_string(),
}
})?;
Ok(ForgeCommandOutput {
exit_code: output.status.code(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
})
}
pub(crate) fn command_output_detail(output: &ForgeCommandOutput) -> String {
let stderr_text = output.stderr.trim();
if !stderr_text.is_empty() {
return stderr_text.to_string();
}
let stdout_text = output.stdout.trim();
if !stdout_text.is_empty() {
return stdout_text.to_string();
}
"Unknown forge CLI error".to_string()
}