vmrunner 0.0.4

micro-vm runner for testcases that require root or invasive IO
use std::{ffi::OsString, path::PathBuf, process};

use anyhow::Result;
use clap::{ArgGroup, Parser};
use vmrunner::{
    GuestInitBuildOptions, ensure_guest_init_with_options, ensure_root_qcow2,
    guest_init_path_for_root_qcow2, install_guest_init_blob,
};

#[derive(Debug, Parser)]
#[command(
    name = "build-krun-init-blob",
    about = "Build libkrun's guest init outside the vmrunner dependency graph.",
    after_help = "Environment fallbacks:\n  LIBKRUN_DIR\n  KRUN_INIT_GUEST_TARGET\n  KRUN_INIT_TARGET_DIR\n  KRUN_INIT_CARGO_TOOLCHAIN\n  CC_LINUX\n  VMRUNNER_LINUX_SYSROOT\n\nBy default, vmrunner extracts a Linux compiler sysroot from --root-qcow2 and builds guest init with clang --sysroot.",
    group(
        ArgGroup::new("destination")
            .required(true)
            .multiple(false)
            .args(["root_qcow2", "output"])
    )
)]
struct Args {
    #[arg(
        long = "libkrun-dir",
        value_name = "PATH",
        help = "libkrun checkout containing the krun-init-blob package"
    )]
    libkrun_dir: Option<PathBuf>,

    #[arg(
        long = "target",
        value_name = "TARGET",
        help = "Linux guest init rustup target matching the host arch, for example aarch64-unknown-linux-gnu"
    )]
    guest_target: Option<String>,

    #[arg(
        long = "root-qcow2",
        value_name = "PATH",
        help = "root qcow2 path or URL; writes sibling .NAME.init.krun"
    )]
    root_qcow2: Option<PathBuf>,

    #[arg(
        long,
        value_name = "PATH",
        help = "explicit output path for the built init binary"
    )]
    output: Option<PathBuf>,

    #[arg(
        long = "cc-linux",
        value_name = "COMMAND",
        help = "compiler command for guest init C code"
    )]
    cc_linux: Option<String>,

    #[arg(
        long = "target-dir",
        value_name = "PATH",
        help = "Cargo target dir for the standalone init build"
    )]
    cargo_target_dir: Option<PathBuf>,

    #[arg(
        long = "cargo-toolchain",
        value_name = "T",
        help = "rustup toolchain passed as +T before cargo build"
    )]
    cargo_toolchain: Option<OsString>,
}

fn main() {
    if let Err(error) = run() {
        eprintln!("{error:#}");
        process::exit(1);
    }
}

fn run() -> Result<()> {
    let args = Args::parse();
    let mut options = GuestInitBuildOptions::default();
    if let Some(guest_target) = args.guest_target {
        options = options.guest_target(guest_target);
    }
    if let Some(libkrun_dir) = args.libkrun_dir {
        options = options.libkrun_dir(libkrun_dir);
    }
    if let Some(cc_linux) = args.cc_linux {
        options = options.cc_linux(cc_linux);
    }
    if let Some(cargo_target_dir) = args.cargo_target_dir {
        options = options.cargo_target_dir(cargo_target_dir);
    }
    if let Some(cargo_toolchain) = args.cargo_toolchain {
        options = options.cargo_toolchain(cargo_toolchain);
    }

    let output_path = if let Some(root_qcow2) = args.root_qcow2 {
        let root_qcow2_path = ensure_root_qcow2(root_qcow2)?;
        ensure_guest_init_with_options(&root_qcow2_path, options)?;
        guest_init_path_for_root_qcow2(root_qcow2_path)
    } else if let Some(output_path) = args.output {
        install_guest_init_blob(&output_path, options)?;
        output_path
    } else {
        unreachable!("clap requires --root-qcow2 or --output")
    };
    println!("installed guest init: {}", output_path.display());
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::Args;
    use clap::CommandFactory;

    #[test]
    fn clap_definition_is_valid() {
        Args::command().debug_assert();
    }
}