vmrunner-sysroot 0.0.5

micro-vm runner sysroot extraction helpers
use std::{path::PathBuf, process};

use clap::{Args as ClapArgs, Parser, Subcommand};
use color_eyre::eyre::Result;
#[cfg(feature = "bsd")]
use vmrunner_sysroot::bsd::extract_bsd_sysroot;
use vmrunner_sysroot::{
    GuestfsCopyOptions, GuestfsDiskFormat, GuestfsMountMode, SysrootOptions, copy_paths_from_guest,
    linux::extract_linux_sysroot,
};

const DEFAULT_SYSROOT_OUTPUT_PARENT: &str = "target/vmrunner-sysroot";

#[derive(Debug, Parser)]
#[command(
    name = "vmrunner-sysroot",
    about = "Extract vmrunner compiler sysroots from VM disk images",
    after_help = "Requires libguestfs at runtime."
)]
struct Args {
    #[command(subcommand)]
    command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
    #[cfg(feature = "bsd")]
    #[command(about = "Extract a BSD sysroot from a QEMU/libguestfs-readable root image")]
    Bsd(SysrootArgs),
    #[command(about = "Extract a Linux sysroot from a QEMU/libguestfs-readable root image")]
    Linux(SysrootArgs),
    #[command(about = "Copy arbitrary paths out of a libguestfs-readable QEMU disk image")]
    CopyOut(CopyOutArgs),
}

#[derive(Debug, ClapArgs)]
struct SysrootArgs {
    #[arg(long = "target", value_name = "TARGET")]
    target: String,

    #[arg(long = "image", value_name = "PATH")]
    image: PathBuf,

    #[arg(
        long = "image-format",
        value_name = "FORMAT",
        default_value = "qcow2",
        help = "Disk image format passed to libguestfs, for example qcow2 or raw; use auto for libguestfs autodetection."
    )]
    image_format: GuestfsDiskFormat,

    #[arg(
        long = "output-parent",
        value_name = "DIR",
        default_value = DEFAULT_SYSROOT_OUTPUT_PARENT
    )]
    output_parent: PathBuf,

    #[arg(
        long = "mount",
        value_name = "MOUNT",
        help = "Manual libguestfs mount, for example /dev/sda3:/; repeatable. Defaults to libguestfs inspection."
    )]
    mounts: Vec<String>,

    #[arg(long, help = "Replace an existing invalid or complete sysroot")]
    force: bool,
}

#[derive(Debug, ClapArgs)]
struct CopyOutArgs {
    #[arg(long = "image", value_name = "PATH")]
    image: PathBuf,

    #[arg(
        long = "image-format",
        value_name = "FORMAT",
        default_value = "qcow2",
        help = "Disk image format passed to libguestfs, for example qcow2 or raw; use auto for libguestfs autodetection."
    )]
    image_format: GuestfsDiskFormat,

    #[arg(
        long = "path",
        value_name = "GUEST_PATH",
        required = true,
        help = "Absolute guest path to copy out; repeatable."
    )]
    guest_paths: Vec<String>,

    #[arg(long = "output", value_name = "DIR")]
    output: PathBuf,

    #[arg(
        long = "mount",
        value_name = "MOUNT",
        help = "Manual libguestfs mount, for example /dev/sda3:/; repeatable. Defaults to libguestfs inspection."
    )]
    mounts: Vec<String>,
}

impl SysrootArgs {
    fn sysroot_options(self) -> SysrootOptions {
        SysrootOptions::new(self.image, self.target, self.output_parent)
            .with_disk_format(self.image_format)
            .with_mount_mode(mount_mode_from_cli(self.mounts))
            .with_force(self.force)
    }
}

impl CopyOutArgs {
    fn copy_options(self) -> (GuestfsCopyOptions, PathBuf) {
        let options = GuestfsCopyOptions::new(self.image, self.guest_paths)
            .with_disk_format(self.image_format)
            .with_mount_mode(mount_mode_from_cli(self.mounts));
        (options, self.output)
    }
}

fn mount_mode_from_cli(mounts: Vec<String>) -> GuestfsMountMode {
    if mounts.is_empty() {
        GuestfsMountMode::Inspect
    } else {
        GuestfsMountMode::Manual(mounts)
    }
}

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

fn run() -> Result<()> {
    match Args::parse().command {
        #[cfg(feature = "bsd")]
        Command::Bsd(args) => {
            let sysroot = extract_bsd_sysroot(args.sysroot_options())?;
            println!("extracted BSD sysroot: {}", sysroot.display());
        }
        Command::Linux(args) => {
            let sysroot = extract_linux_sysroot(args.sysroot_options())?;
            println!("extracted Linux sysroot: {}", sysroot.display());
        }
        Command::CopyOut(args) => {
            let (options, output) = args.copy_options();
            copy_paths_from_guest(options, &output)?;
            println!("copied guest paths to: {}", output.display());
        }
    }
    Ok(())
}

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

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