Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

xtask/src/main.rs

//! `cargo xtask <task>`: gunnar-ui's repository tasks.
//!
//! ```text
//!   cargo xtask build [--variant N] build what the recipe declares
//!   cargo xtask publish [--push]    build the PUBLIC branch and prove it compiles
//! ```
//!
//! `build` carries no build knowledge of its own: every package, feature,
//! profile and target it uses is a field of a variant `gunnar-ui-recipe`
//! declares, and `.nornir/build-variants.toml` is that declaration EMITTED for
//! the tools that discover this repository by that path.
//!
//! Nothing here is a build dependency of anything: repository tooling is
//! invoked as a tool, never as a dependency.

use std::process::ExitCode;

/// `cargo xtask publish [--push]`; see [`xtask::publish`].
///
/// Two steps, and the second is the one that matters: it writes branch `public`
/// from HEAD's tree minus the declared exclusion list, and then compiles that
/// branch in a throwaway worktree. Excluding a file a build reads produces a
/// branch that looks right and does not build, and nothing about writing the
/// list would have said so.
fn task_publish() -> ExitCode {
    let mut push = false;
    for a in std::env::args().skip(2) {
        match a.as_str() {
            "--push" => push = true,
            other => {
                eprintln!("xtask publish: unknown argument `{other}`");
                eprintln!("usage: cargo xtask publish [--push]");
                return ExitCode::FAILURE;
            }
        }
    }
    match xtask::publish::run(&xtask::workspace_root(), push) {
        Ok(r) => {
            println!("\npublic tree {}", r.tree);
            println!("commit      {}", r.commit);
            println!("branch      public");
            println!("\n  kept      {} path(s)", r.kept);
            println!("  excluded  {} path(s):", r.excluded_total());
            for (why, n) in &r.excluded_by_reason {
                println!("    {n:>4}  {why}");
            }
            println!(
                "\n  cargo metadata + check -p gunnar-ui-server + readme_version: {}",
                if r.checked { "PASS" } else { "not run" }
            );
            if r.pushed {
                println!("  pushed to github-public (main) and gunnar-rs (main)");
            } else {
                println!("  NOT pushed: `cargo xtask publish --push` does that");
            }
            ExitCode::SUCCESS
        }
        Err(e) => {
            eprintln!("xtask publish: {e}");
            ExitCode::FAILURE
        }
    }
}

/// `cargo xtask build [--variant <name>] [--list]`; see [`xtask::build`].
///
/// The declaration is `gunnar-ui-recipe`'s. This function knows the name of no
/// package, no feature and no triple — it resolves rows through the
/// constellation's shared `suite_cli` parser and hands each one to the shared
/// builder. There is no second place a build point of this repository is
/// defined.
fn task_build() -> ExitCode {
    let argv: Vec<String> = std::env::args().skip(2).collect();
    match xtask::build::run(&xtask::workspace_root(), &argv) {
        Ok(names) => {
            println!("\n{} variant(s): {}", names.len(), names.join(" · "));
            ExitCode::SUCCESS
        }
        Err(e) => {
            eprintln!("xtask build: {e}");
            ExitCode::FAILURE
        }
    }
}

/// Every task's one-line usage, so a typo and a bare invocation print the same
/// list and neither can go stale on its own.
const USAGE: &str = "usage: cargo xtask build [--variant <name>] [--list]
       cargo xtask publish [--push]
       cargo xtask ci [--quick|--nightly] [--list] [--only <prefix>]
       cargo xtask browser [<test filter>]
       cargo xtask console";

/// `cargo xtask ci`: gunnar-ui's CI plan on the shared runner (nornir-xtask-support
/// `ci`): its locks, its tests, the wasm console checked for its target, and (full)
/// its declared rows. One JSON receipt per run.
fn task_ci() -> ExitCode {
    let args: Vec<String> = std::env::args().skip(2).collect();
    // the quick set's lean profile (QUICK_ENV) on every step: it fits the forge's 40 GB target
    let plan = xtask::ci::for_mode(xtask::ci::plan(&xtask::workspace_root()), nornir_xtask_support::ci::Mode::from_args(&args));
    ExitCode::from(nornir_xtask_support::ci::main(&plan, &args).clamp(0, 255) as u8)
}

/// **`cargo xtask console`** (lane GUN-PRODUCTS, 2026-10-30): the community console bundle
/// (`trunk build --release` in `crates/gunnar-ui-web`, the browser build this repository
/// names), copied into `crates/gunnar-ui/console`, where the product crate `gunnar-ui`
/// builds it in (its build.rs) and a published crate carries it. `$TRUNK` names trunk when
/// it is not on the PATH.
fn task_console() -> ExitCode {
    let root = xtask::workspace_root();
    let dist = std::env::var_os("CARGO_TARGET_DIR").map(std::path::PathBuf::from).unwrap_or_else(|| root.join("target")).join("console-dist");
    let into = root.join("crates/gunnar-ui/console");
    let trunk = std::env::var("TRUNK").unwrap_or_else(|_| "trunk".to_string());
    let status = std::process::Command::new(&trunk)
        .current_dir(root.join("crates/gunnar-ui-web"))
        .args(["build", "--release", "--dist"])
        .arg(&dist)
        .status();
    match status {
        Ok(s) if s.success() => {}
        Ok(s) => {
            eprintln!("xtask console: {trunk} build exited {s}");
            return ExitCode::FAILURE;
        }
        Err(e) => {
            eprintln!("xtask console: could not start {trunk} ({e}): the console is a WebAssembly bundle and trunk builds it (`cargo install trunk --locked`)");
            return ExitCode::FAILURE;
        }
    }
    if !dist.join("index.html").is_file() {
        eprintln!("xtask console: trunk wrote no index.html into {}", dist.display());
        return ExitCode::FAILURE;
    }
    let _ = std::fs::remove_dir_all(&into);
    if let Err(e) = copy_dir(&dist, &into) {
        eprintln!("xtask console: copy {} to {}: {e}", dist.display(), into.display());
        return ExitCode::FAILURE;
    }
    println!("the console bundle is in {} (`cargo install --path crates/gunnar-ui` builds it in)", into.display());
    ExitCode::SUCCESS
}

fn copy_dir(from: &std::path::Path, to: &std::path::Path) -> std::io::Result<()> {
    std::fs::create_dir_all(to)?;
    for e in std::fs::read_dir(from)? {
        let e = e?;
        let p = e.path();
        if p.is_dir() {
            copy_dir(&p, &to.join(e.file_name()))?;
        } else {
            std::fs::copy(&p, to.join(e.file_name()))?;
        }
    }
    Ok(())
}

/// **`cargo xtask browser`** (RA2-7): gunnar-ui's browser tests, the A29-20 first touch
/// (`gunnar-ui-server`'s `passkeys_at_gunnar::first_touch`, metered), in the browser image: the
/// toolchain image plus Debian's Chromium (`browser/Containerfile`), built once per content of
/// that file ON its base image and tagged by both (GHUNT-17). `GUNNAR_UI_NEEDS_BROWSER=1` makes a
/// missing Chromium red.
/// Inside a container (the CI runner's, or `NORNIR_CI_HOST=1`) the tests run right here and
/// `$CHROME` must name a Chromium.
fn task_browser(filter: Option<String>) -> ExitCode {
    use std::process::Command;
    let root = xtask::workspace_root();
    let runs = xtask::ci::browser_runs(filter.as_deref());
    let mut env = vec![("GUNNAR_UI_NEEDS_BROWSER".to_string(), "1".to_string())];
    // GUN-GUEST: the console's built bundle for the deep-link test, the one named or the
    // recipe's `dist-metered` where `cargo xtask build` left one (inside the workspace, so the
    // image sees it too).
    let bundle = std::env::var("GUNNAR_UI_BUNDLE").ok().filter(|b| !b.is_empty()).or_else(|| {
        let built = root.join("crates/gunnar-ui-web/dist-metered");
        built.join("index.html").is_file().then(|| built.display().to_string())
    });
    if let Some(bundle) = bundle {
        env.push(("GUNNAR_UI_BUNDLE".to_string(), bundle));
    }
    let run = |mut c: Command, what: &str| -> ExitCode {
        match c.status() {
            Ok(s) if s.success() => ExitCode::SUCCESS,
            Ok(s) => {
                eprintln!("xtask browser: {what} exited {s}");
                ExitCode::FAILURE
            }
            Err(e) => {
                eprintln!("xtask browser: could not start {what}: {e}");
                ExitCode::FAILURE
            }
        }
    };
    if nornir_xtask_support::ci::toolchain_is_here() {
        for args in &runs {
            let mut c = Command::new("cargo");
            c.args(args).envs(env.iter().map(|(k, v)| (k, v))).current_dir(&root);
            let ran = run(c, "the browser tests");
            if ran != ExitCode::SUCCESS {
                return ran;
            }
        }
        return ExitCode::SUCCESS;
    }
    let mut image = nornir_xtask_support::ci::Container::from_env(&root);
    let file = root.join("browser/Containerfile");
    let recipe = match std::fs::read(&file) {
        Ok(r) => r,
        Err(e) => {
            eprintln!("xtask browser: read {}: {e}", file.display());
            return ExitCode::FAILURE;
        }
    };
    // GHUNT-17: the tag names the base image's id too, so a moved base builds a new image.
    let base = Command::new(&image.engine)
        .args(["image", "inspect", "--format", "{{.Id}}", &image.image])
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_owned())
        .filter(|id| !id.is_empty())
        .unwrap_or_else(|| image.image.clone());
    let tag = format!("localhost/gunnar-ui-browser:{}", xtask::ci::image_tag(&recipe, &base));
    let present = Command::new(&image.engine).args(["image", "exists", &tag]).status().is_ok_and(|s| s.success());
    if !present {
        let mut c = Command::new(&image.engine);
        c.args(["build", "-t", &tag, "--build-arg", &format!("BASE={}", image.image), "-f"]).arg(&file).arg(root.join("browser"));
        let built = run(c, "the browser image's build");
        if built != ExitCode::SUCCESS {
            return built;
        }
    }
    image.image = tag;
    if let Err(e) = std::fs::create_dir_all(&image.target) {
        eprintln!("xtask browser: create {}: {e}", image.target.display());
        return ExitCode::FAILURE;
    }
    for args in &runs {
        let argv = image.argv(None, &env, "cargo", args);
        let mut c = Command::new(&argv[0]);
        c.args(&argv[1..]);
        let ran = run(c, "the browser tests in their image");
        if ran != ExitCode::SUCCESS {
            return ran;
        }
    }
    ExitCode::SUCCESS
}

fn main() -> ExitCode {
    match std::env::args().nth(1).as_deref() {
        Some("build") => task_build(),
        Some("publish") => task_publish(),
        Some("ci") => task_ci(),
        Some("browser") => task_browser(std::env::args().nth(2)),
        Some("console") => task_console(),
        Some(other) => {
            eprintln!("xtask: unknown task `{other}`");
            eprintln!("{USAGE}");
            ExitCode::FAILURE
        }
        None => {
            eprintln!("{USAGE}");
            ExitCode::FAILURE
        }
    }
}