neomake 0.2.1

Yet another task runner as make alternative, inspired by GitLab pipelines.
#![feature(hash_drain_filter)]

mod args;
mod config;
mod error;
mod output;

use std::{
    collections::{
        HashMap,
        HashSet,
        VecDeque,
    },
    error::Error,
    iter::FromIterator,
    result::Result,
    sync::{
        Arc,
        Mutex,
    },
};

use interactive_process::InteractiveProcess;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let args = args::ClapArgumentLoader::load()?;
    match args.command {
        | args::Command::Init => init().await,
        | args::Command::Run { config, chains, args } => run(config, chains, args).await,
        | args::Command::Describe { config, chains } => describe(config, chains).await,
    }
}

async fn init() -> Result<(), Box<dyn Error>> {
    std::fs::write("./.neomake.yaml", crate::config::default_config())?;
    Ok(())
}

fn determine_order(
    chains: &HashMap<String, config::Chain>,
    entries: &Vec<String>,
) -> Result<Vec<HashSet<String>>, Box<dyn Error>> {
    let mut map = HashMap::<String, Vec<String>>::new();

    let mut seen = HashSet::<String>::new();
    let mut pending = VecDeque::<String>::new();
    pending.extend(entries.to_owned());

    while let Some(next) = pending.pop_back() {
        if seen.contains(&next) {
            continue;
        }
        seen.insert(next.clone());

        if let Some(pre) = &chains[&next].pre {
            map.insert(next, pre.clone());
            pending.extend(pre.clone());
        } else {
            map.insert(next, Vec::<String>::new());
        }
    }
    seen.clear();

    let mut result = Vec::<HashSet<String>>::new();
    while map.len() > 0 {
        let leafs = map
            .drain_filter(|_, v| {
                for v_item in v {
                    if !seen.contains(v_item) {
                        return false;
                    }
                }
                true
            })
            .collect::<Vec<_>>();
        if leafs.len() == 0 {
            return Err(Box::new(crate::error::TaskChainRecursion::new(
                "recursion in graph detected",
            )));
        }
        let set = leafs.iter().map(|x| x.0.clone());
        seen.extend(set.clone());
        result.push(HashSet::<String>::from_iter(set));
    }

    Ok(result)
}

async fn run(
    conf: crate::config::Config,
    chains: Vec<String>,
    args: HashMap<String, String>,
) -> Result<(), Box<dyn Error>> {
    fn recursive_add(namespace: &mut std::collections::VecDeque<String>, parent: &mut serde_json::Value, value: &str) {
        let current_namespace = namespace.pop_front().unwrap();
        match namespace.len() {
            | 0 => {
                parent
                    .as_object_mut()
                    .unwrap()
                    .entry(&current_namespace)
                    .or_insert(serde_json::Value::String(value.to_owned()));
            },
            | _ => {
                let p = parent
                    .as_object_mut()
                    .unwrap()
                    .entry(&current_namespace)
                    .or_insert(serde_json::Value::Object(serde_json::Map::new()));
                recursive_add(namespace, p, value);
            },
        }
    }

    fn execute_matrix_entry(
        conf: &config::Config,
        chain: &config::Chain,
        mat: &config::MatrixEntry,
        args: &HashMap<String, String>,
        output: Arc<Mutex<output::Controller>>,
    ) -> Result<(), Box<dyn Error>> {
        let mut hb = handlebars::Handlebars::new();
        hb.set_strict_mode(true);
        let mut values_json = serde_json::Value::Object(serde_json::Map::new());
        for arg in args {
            let namespaces_vec: Vec<String> = arg.0.split('.').map(|s| s.to_string()).collect();
            let mut namespaces = VecDeque::from(namespaces_vec);
            recursive_add(&mut namespaces, &mut values_json, arg.1);
        }

        for task in &chain.tasks {
            let rendered_cmd = hb.render_template(&task.script, &values_json)?;

            let workdir = if let Some(workdir) = &task.workdir {
                Some(workdir)
            } else if let Some(workdir) = &mat.workdir {
                Some(workdir)
            } else {
                None
            };

            let mut envs_merged = HashMap::<&String, &String>::new();
            for source in vec![&conf.env, &chain.env, &mat.env, &task.env] {
                if let Some(m) = source {
                    envs_merged.extend(m);
                }
            }

            let mut cmd_proc = std::process::Command::new("sh");
            cmd_proc.envs(envs_merged);
            if let Some(w) = workdir {
                cmd_proc.current_dir(w);
            }
            cmd_proc.arg("-c");
            cmd_proc.arg(&rendered_cmd);
            let closure_controller = output.clone();
            let cmd_exit_code = InteractiveProcess::new(cmd_proc, move |l| match l {
                | Ok(v) => {
                    let mut lock = closure_controller.lock().unwrap();
                    lock.append(v);
                    lock.draw().unwrap();
                },
                | Err(..) => {},
            })?
            .wait()?
            .code();
            if let Some(code) = cmd_exit_code {
                if code != 0 {
                    let err_msg = format!("command \"{}\" failed with code {}", &rendered_cmd, code,);
                    return Err(Box::new(crate::error::ChildProcessError::new(&err_msg)));
                }
            }
        }
        Ok(())
    }

    let output = Arc::new(Mutex::new(output::Controller::new(10)));
    for stage_chains in determine_order(&conf.chains, &chains)? {
        for chain_name in stage_chains {
            let chain = &conf.chains[&chain_name];

            if let Some(matrix) = &chain.matrix {
                for mat in matrix {
                    execute_matrix_entry(&conf, chain, mat, &args, output.clone())?;
                }
            } else {
                execute_matrix_entry(
                    &conf,
                    chain,
                    &config::MatrixEntry { ..Default::default() },
                    &args,
                    output.clone(),
                )?;
            }
        }
    }
    Ok(())
}

async fn describe(config: crate::config::Config, chains: Vec<String>) -> Result<(), Box<dyn Error>> {
    let structure = determine_order(&config.chains, &chains)?;

    #[derive(Debug, serde::Serialize)]
    struct Output {
        stages: Vec<HashSet<String>>,
    }

    let mut info = Output { stages: Vec::new() };
    for s in structure {
        info.stages
            .push(s.iter().map(|s| s.to_owned()).into_iter().collect::<HashSet<_>>());
    }
    println!("{}", serde_json::ser::to_string(&info)?);

    Ok(())
}