use anyhow::Context as _;
#[derive(Debug, Clone)]
pub(crate) struct CommandOutput {
pub(crate) success: bool,
pub(crate) stdout: String,
pub(crate) stderr: String,
}
impl CommandOutput {
pub(crate) fn ok_or_stderr(&self, program: &str) -> anyhow::Result<String> {
if self.success {
Ok(self.stdout.trim().to_string())
} else {
let detail = self.stderr.trim();
anyhow::bail!(
"`{program}` failed{}",
if detail.is_empty() {
String::new()
} else {
format!(": {detail}")
}
)
}
}
}
pub(crate) trait CommandRunner {
fn run(&self, program: &str, args: &[&str]) -> anyhow::Result<CommandOutput>;
}
#[derive(Debug, Clone, Default)]
pub(crate) struct RealCommandRunner {
env: Vec<(String, String)>,
unset: Vec<String>,
}
impl RealCommandRunner {
pub(crate) fn with_gh_env(gh_env: &trusty_mpm::core::gh_identity::GhEnv) -> Self {
Self {
env: gh_env.vars().to_vec(),
unset: gh_env.unset_vars().to_vec(),
}
}
}
impl CommandRunner for RealCommandRunner {
fn run(&self, program: &str, args: &[&str]) -> anyhow::Result<CommandOutput> {
let mut cmd = std::process::Command::new(program);
cmd.args(args);
for k in &self.unset {
cmd.env_remove(k);
}
for (k, v) in &self.env {
cmd.env(k, v);
}
let output = cmd.output().with_context(|| {
format!("failed to run `{program}` — is it installed and on your PATH?")
})?;
Ok(CommandOutput {
success: output.status.success(),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn real_runner_applies_env_overrides() {
let gh_env = trusty_mpm::core::gh_identity::resolve_gh_env(Some(
&trusty_mpm::core::trusty_tools_config::GithubConfig {
config_dir: Some(std::path::PathBuf::from("/cfg/project")),
..Default::default()
},
))
.expect("a config_dir binding resolves");
let runner = RealCommandRunner::with_gh_env(&gh_env);
match runner.run("printenv", &["GH_CONFIG_DIR"]) {
Ok(out) => {
assert!(out.success, "printenv failed: {}", out.stderr);
assert_eq!(out.stdout.trim(), "/cfg/project");
}
Err(_) => {
}
}
if let Ok(out) = runner.run("printenv", &["GH_TOKEN"]) {
assert!(
!out.success,
"#6668: a bound child must not inherit GH_TOKEN, got {:?}",
out.stdout
);
}
assert_ne!(
std::env::var("GH_CONFIG_DIR").ok().as_deref(),
Some("/cfg/project"),
"the binding must apply to the child only"
);
}
#[test]
fn real_runner_default_has_no_env() {
let runner = RealCommandRunner::default();
assert!(runner.env.is_empty());
}
}