executesoft 0.2.9

ExecuteSoft repository automation CLI
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"))
        })
}