Gunnar

nordisk/heimdall

xtask/src/main.rs

// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Ignalina ApS and Vetra Automation AB

//! `cargo xtask ci [--quick|--nightly]`: Heimdall's CI verb for the dwarves CI forge
//! (nordisk/dwarves pipelines/ci-main.runa.ron). Heimdall is a defensive sandbox; the
//! verb only runs scripts/ci.sh, which builds and tests Heimdall itself. All logic is in
//! the script; `--quick` and `--nightly` run the same script.

use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};

/// The program and arguments `cargo xtask <args>` runs from `root`, or why not.
fn plan(args: &[String], root: &Path) -> Result<(PathBuf, Vec<String>), String> {
    const USAGE: &str = "usage: cargo xtask ci [--quick|--nightly]";
    match args.split_first() {
        None => Err(USAGE.into()),
        Some((verb, flags)) if verb == "ci" => {
            if let Some(bad) = flags
                .iter()
                .find(|f| !matches!(f.as_str(), "--quick" | "--nightly"))
            {
                return Err(format!("ci takes no `{bad}`; {USAGE}"));
            }
            let script = root.join("scripts").join("ci.sh");
            Ok((PathBuf::from("sh"), vec![script.display().to_string()]))
        }
        Some((verb, _)) => Err(format!("no verb `{verb}`; {USAGE}")),
    }
}

fn main() -> ExitCode {
    let args: Vec<String> = std::env::args().skip(1).collect();
    let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
    let root = manifest.parent().unwrap_or(manifest);
    match plan(&args, root) {
        Ok((program, argv)) => match Command::new(&program)
            .args(&argv)
            .current_dir(root)
            .status()
        {
            Ok(s) if s.success() => ExitCode::SUCCESS,
            Ok(s) => {
                eprintln!("xtask: {} {} ended {s}", program.display(), argv.join(" "));
                ExitCode::FAILURE
            }
            Err(e) => {
                eprintln!("xtask: {} did not start: {e}", program.display());
                ExitCode::FAILURE
            }
        },
        Err(why) => {
            eprintln!("xtask: {why}");
            ExitCode::from(2)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn s(v: &[&str]) -> Vec<String> {
        v.iter().map(|x| x.to_string()).collect()
    }

    #[test]
    fn ci_and_its_quick_and_nightly_forms_all_run_scripts_ci_sh() {
        let root = Path::new("/r");
        for a in [&["ci"][..], &["ci", "--quick"], &["ci", "--nightly"]] {
            assert_eq!(
                plan(&s(a), root),
                Ok((PathBuf::from("sh"), s(&["/r/scripts/ci.sh"]))),
                "{a:?}"
            );
        }
    }

    #[test]
    fn an_unknown_verb_or_flag_is_refused_by_name() {
        let root = Path::new("/r");
        assert!(plan(&s(&[]), root).unwrap_err().contains("usage"));
        assert!(plan(&s(&["build"]), root).unwrap_err().contains("`build`"));
        assert!(plan(&s(&["ci", "--fast"]), root)
            .unwrap_err()
            .contains("`--fast`"));
    }
}