use std::env;
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
pub(crate) type AnyError = Box<dyn Error>;
pub(crate) type Result<T> = std::result::Result<T, AnyError>;
pub(crate) fn usage_error<T>(message: String) -> Result<T> {
Err(message.into())
}
pub(crate) fn repo_root() -> PathBuf {
if let Ok(root) = env::var("EXECUTESOFT_ROOT") {
let root = PathBuf::from(root);
if is_repo_root(&root) {
return root;
}
}
let mut dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
loop {
if is_repo_root(&dir) {
return dir;
}
if !dir.pop() {
break;
}
}
if let Some(root) = configured_repo_root() {
return root;
}
PathBuf::from(".")
}
pub(crate) fn repo_path(path: &str) -> PathBuf {
let raw = PathBuf::from(path);
if raw.is_absolute() || raw.exists() {
return raw;
}
let from_root = repo_root().join(path);
if from_root.exists() {
return from_root;
}
raw
}
pub(crate) fn repo_root_help() -> String {
"Run `exe config set-root --path /path/to/executesoft` from the cloned ExecuteSoft repository, or set EXECUTESOFT_ROOT=/path/to/executesoft.".to_string()
}
pub(crate) fn run_cmd(dir: &Path, program: &str, args: &[String]) -> Result<()> {
let status = Command::new(program)
.args(args)
.current_dir(dir)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()?;
if status.success() {
Ok(())
} else {
Err(format!("{program} exited with {status}").into())
}
}
pub(crate) fn run_make(dir: &Path, target: &str, vars: &[String]) -> Result<()> {
let mut args = vec![target.to_string()];
args.extend(vars.iter().cloned());
run_cmd(dir, "make", &args)
}
pub(crate) fn make_vars(args: &[String]) -> Vec<String> {
let mut vars = Vec::new();
for arg in args {
if let Some(raw) = arg.strip_prefix("--") {
if let Some((key, value)) = raw.split_once('=') {
vars.push(format!(
"{}={}",
key.replace('-', "_").to_uppercase(),
value
));
}
} else if arg.contains('=') {
vars.push(arg.clone());
}
}
vars
}
pub(crate) fn configured_repo_root() -> Option<PathBuf> {
let path = repo_root_config_file()?;
let root = fs::read_to_string(path).ok()?.trim().to_string();
if root.is_empty() {
return None;
}
let root = PathBuf::from(root);
is_repo_root(&root).then_some(root)
}
pub(crate) fn persist_repo_root(path: &Path) -> Result<PathBuf> {
let root = path.canonicalize()?;
if !is_repo_root(&root) {
return usage_error(format!(
"not an ExecuteSoft repository root: {}\nExpected to find services/ and tools/templates/.",
root.display()
));
}
let config_file = repo_root_config_file()
.ok_or_else(|| "HOME is not set; cannot store ExecuteSoft CLI config".to_string())?;
if let Some(parent) = config_file.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&config_file, format!("{}\n", root.display()))?;
Ok(root)
}
pub(crate) fn repo_root_config_path() -> Option<PathBuf> {
repo_root_config_file()
}
pub(crate) fn is_repo_root(path: &Path) -> bool {
path.join("services").is_dir() && path.join("tools/templates").is_dir()
}
fn repo_root_config_file() -> Option<PathBuf> {
env::var("EXECUTESOFT_CONFIG")
.ok()
.map(PathBuf::from)
.or_else(|| {
env::var("HOME")
.ok()
.map(|home| PathBuf::from(home).join(".config/executesoft/root"))
})
}