vmrunner-sysroot 0.0.5

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

use color_eyre::eyre::{Result, WrapErr, eyre};
use fs_err as fs;

use crate::{
    CopySpec, SysrootExtract, SysrootKind, SysrootOptions, extract_sysroot, sysroot_copy_options,
};

const BSD_TARGET_OSES: &[&str] = &["freebsd", "openbsd", "netbsd", "dragonfly"];
const FREEBSD_REQUIRED_FILES: &[&str] = &[
    "usr/include/kenv.h",
    "lib/crt1.o",
    "lib/crti.o",
    "lib/crtbegin.o",
    "lib/libc.a",
    "lib/crtend.o",
    "lib/crtn.o",
];
const GENERIC_BSD_REQUIRED_DIRS: &[&str] = &["usr/include", "lib"];
const BSD_COPY_PATHS: &[CopySpec] = &[
    CopySpec {
        guest_path: "/usr/include",
        local_parent: "usr",
    },
    CopySpec {
        guest_path: "/usr/lib",
        local_parent: "",
    },
];

pub fn extract_bsd_sysroot(options: SysrootOptions) -> Result<PathBuf> {
    let SysrootOptions {
        root_image,
        guest_target,
        output_parent,
        disk_format,
        mount_mode,
        force,
    } = options;
    let dir_name = bsd_sysroot_dir_name(&guest_target)?;
    let copy_options = sysroot_copy_options(root_image, BSD_COPY_PATHS, &[])
        .with_disk_format(disk_format)
        .with_mount_mode(mount_mode);

    extract_sysroot(
        SysrootExtract {
            kind: SysrootKind::Bsd,
            output_parent,
            dir_name,
            copy_options,
            force,
        },
        normalize_bsd_sysroot_layout,
        |path| validate_bsd_sysroot(&guest_target, path),
    )
}

pub fn bsd_sysroot_dir_name(guest_target: &str) -> Result<String> {
    let (arch, os) = bsd_target_arch_os(guest_target)?;
    Ok(format!("{os}-{arch}"))
}

pub fn validate_bsd_sysroot(guest_target: &str, sysroot: impl AsRef<Path>) -> Result<()> {
    let (_, os) = bsd_target_arch_os(guest_target)?;
    let sysroot = sysroot.as_ref();
    if os == "freebsd" {
        return validate_freebsd_sysroot(sysroot);
    }
    validate_generic_bsd_sysroot(sysroot)
}

fn bsd_target_arch_os(guest_target: &str) -> Result<(&str, &str)> {
    let parts = Vec::from_iter(guest_target.split('-'));
    let arch = parts
        .first()
        .copied()
        .filter(|arch| !arch.is_empty())
        .ok_or_else(|| eyre!("BSD sysroot target '{guest_target}' is missing an architecture"))?;
    if parts.len() < 3 {
        return Err(unsupported_bsd_target(guest_target));
    }
    let os = parts
        .iter()
        .copied()
        .find(|part| BSD_TARGET_OSES.contains(part))
        .ok_or_else(|| unsupported_bsd_target(guest_target))?;
    Ok((arch, os))
}

fn unsupported_bsd_target(guest_target: &str) -> color_eyre::Report {
    eyre!(
        "unsupported BSD sysroot target '{guest_target}'; expected a Rust target triple containing one of: {}",
        BSD_TARGET_OSES.join(", ")
    )
}

fn validate_freebsd_sysroot(sysroot: &Path) -> Result<()> {
    let missing = Vec::from_iter(
        FREEBSD_REQUIRED_FILES
            .iter()
            .filter(|relative| !sysroot.join(relative).is_file())
            .copied(),
    );
    if missing.is_empty() {
        return Ok(());
    }
    Err(eyre!(
        "FreeBSD sysroot '{}' is missing required files: {}",
        sysroot.display(),
        missing.join(", ")
    ))
}

fn validate_generic_bsd_sysroot(sysroot: &Path) -> Result<()> {
    let missing = Vec::from_iter(
        GENERIC_BSD_REQUIRED_DIRS
            .iter()
            .filter(|relative| !sysroot.join(relative).is_dir())
            .copied(),
    );
    if missing.is_empty() {
        return Ok(());
    }
    Err(eyre!(
        "BSD sysroot '{}' is missing required directories: {}",
        sysroot.display(),
        missing.join(", ")
    ))
}

fn normalize_bsd_sysroot_layout(sysroot: &Path) -> Result<()> {
    move_if_destination_missing(sysroot.join("include"), sysroot.join("usr/include"))?;
    move_if_destination_missing(sysroot.join("usr/lib"), sysroot.join("lib"))?;
    Ok(())
}

fn move_if_destination_missing(source: PathBuf, destination: PathBuf) -> Result<()> {
    if !source.exists() || destination.exists() {
        return Ok(());
    }
    let parent = destination
        .parent()
        .filter(|path| !path.as_os_str().is_empty())
        .unwrap_or_else(|| Path::new("."));
    fs::create_dir_all(parent)
        .wrap_err_with(|| format!("create directory '{}'", parent.display()))?;
    fs::rename(&source, &destination).wrap_err_with(|| {
        format!(
            "move extracted sysroot path '{}' to '{}'",
            source.display(),
            destination.display()
        )
    })
}

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

    #[test]
    fn bsd_sysroot_dir_name_matches_rust_targets() {
        assert_eq!(
            bsd_sysroot_dir_name("aarch64-unknown-freebsd").unwrap(),
            "freebsd-aarch64"
        );
        assert_eq!(
            bsd_sysroot_dir_name("x86_64-unknown-freebsd").unwrap(),
            "freebsd-x86_64"
        );
        assert_eq!(
            bsd_sysroot_dir_name("riscv64gc-unknown-freebsd").unwrap(),
            "freebsd-riscv64gc"
        );
        assert_eq!(
            bsd_sysroot_dir_name("x86_64-unknown-openbsd").unwrap(),
            "openbsd-x86_64"
        );
        assert!(bsd_sysroot_dir_name("aarch64-freebsd").is_err());
        assert!(bsd_sysroot_dir_name("aarch64-unknown-linux-musl").is_err());
    }

    #[test]
    fn normalizes_copy_out_basename_layout() -> Result<()> {
        let dir = tempfile::tempdir()?;
        fs::create_dir_all(dir.path().join("include"))?;
        fs::write(dir.path().join("include/kenv.h"), b"header")?;
        fs::create_dir_all(dir.path().join("lib"))?;

        normalize_bsd_sysroot_layout(dir.path())?;

        assert!(dir.path().join("usr/include/kenv.h").is_file());
        assert!(dir.path().join("lib").is_dir());
        assert!(!dir.path().join("include").exists());
        Ok(())
    }

    #[test]
    fn normalizes_full_path_layout() -> Result<()> {
        let dir = tempfile::tempdir()?;
        fs::create_dir_all(dir.path().join("usr/include"))?;
        fs::write(dir.path().join("usr/include/kenv.h"), b"header")?;
        fs::create_dir_all(dir.path().join("usr/lib"))?;
        fs::write(dir.path().join("usr/lib/libc.a"), b"libc")?;

        normalize_bsd_sysroot_layout(dir.path())?;

        assert!(dir.path().join("usr/include/kenv.h").is_file());
        assert!(dir.path().join("lib/libc.a").is_file());
        assert!(!dir.path().join("usr/lib/libc.a").exists());
        Ok(())
    }

    #[test]
    fn validates_required_freebsd_files() -> Result<()> {
        let dir = tempfile::tempdir()?;
        for file in FREEBSD_REQUIRED_FILES {
            let path = dir.path().join(file);
            fs::create_dir_all(path.parent().unwrap())?;
            fs::write(path, b"required")?;
        }

        validate_bsd_sysroot("x86_64-unknown-freebsd", dir.path())?;

        fs::remove_file(dir.path().join("lib/libc.a"))?;
        assert!(validate_bsd_sysroot("x86_64-unknown-freebsd", dir.path()).is_err());
        Ok(())
    }

    #[test]
    fn validates_generic_bsd_directories() -> Result<()> {
        let dir = tempfile::tempdir()?;
        fs::create_dir_all(dir.path().join("usr/include"))?;
        fs::create_dir_all(dir.path().join("lib"))?;

        validate_bsd_sysroot("x86_64-unknown-openbsd", dir.path())?;

        fs::remove_dir_all(dir.path().join("lib"))?;
        assert!(validate_bsd_sysroot("x86_64-unknown-openbsd", dir.path()).is_err());
        Ok(())
    }
}