axbuild 0.4.3

An OS build lib toolkit used by arceos
Documentation
use std::{
    fs,
    path::{Path, PathBuf},
};

use ostool::run::qemu::QemuConfig;

use crate::context::ResolvedAxvisorRequest;

const LEGACY_DEFAULT_ROOTFS: &str = "${workspaceFolder}/tmp/rootfs.img";
const AXVISOR_DEFAULT_ROOTFS: &str = "${workspaceFolder}/os/axvisor/tmp/rootfs.img";

pub(crate) fn default_qemu_config_template_path(axvisor_dir: &Path, arch: &str) -> PathBuf {
    axvisor_dir.join(format!("scripts/ostool/qemu-{arch}.toml"))
}

pub(crate) fn apply_rootfs_path(
    config: &mut QemuConfig,
    request: &ResolvedAxvisorRequest,
) -> anyhow::Result<()> {
    let rootfs_path = infer_rootfs_path(&request.vmconfigs)?
        .unwrap_or_else(|| default_rootfs_path(&request.axvisor_dir));
    ensure_rootfs_drive_arg(&mut config.args, &rootfs_path);
    Ok(())
}

fn default_rootfs_path(axvisor_dir: &Path) -> PathBuf {
    axvisor_dir.join("tmp/rootfs.img")
}

pub(crate) fn infer_rootfs_path(vmconfigs: &[PathBuf]) -> anyhow::Result<Option<PathBuf>> {
    for vmconfig in vmconfigs {
        let content = fs::read_to_string(vmconfig)
            .map_err(|e| anyhow!("failed to read vm config {}: {e}", vmconfig.display()))?;
        let value: toml::Value = toml::from_str(&content)
            .map_err(|e| anyhow!("failed to parse vm config {}: {e}", vmconfig.display()))?;
        let Some(kernel_path) = value
            .get("kernel")
            .and_then(|kernel| kernel.get("kernel_path"))
            .and_then(|path| path.as_str())
        else {
            continue;
        };
        let rootfs_path = Path::new(kernel_path)
            .parent()
            .map(|dir| dir.join("rootfs.img"));
        if let Some(rootfs_path) = rootfs_path
            && rootfs_path.exists()
        {
            return Ok(Some(rootfs_path));
        }
    }
    Ok(None)
}

#[cfg(test)]
fn load_qemu_config(path: &Path) -> anyhow::Result<QemuConfig> {
    let content = fs::read_to_string(path).map_err(|e| {
        anyhow!(
            "failed to read QEMU config template {}: {e}",
            path.display()
        )
    })?;
    toml::from_str(&content).map_err(|e| {
        anyhow!(
            "failed to parse QEMU config template {}: {e}",
            path.display()
        )
    })
}

fn ensure_rootfs_drive_arg(args: &mut Vec<String>, rootfs_path: &Path) {
    let rootfs_path = rootfs_path.display().to_string();
    let replacement = format!("id=disk0,if=none,format=raw,file={rootfs_path}");
    let mut replaced = false;

    for arg in args.iter_mut() {
        if arg.contains(LEGACY_DEFAULT_ROOTFS) {
            *arg = arg.replace(LEGACY_DEFAULT_ROOTFS, &rootfs_path);
            replaced = true;
        }
        if arg.contains(AXVISOR_DEFAULT_ROOTFS) {
            *arg = arg.replace(AXVISOR_DEFAULT_ROOTFS, &rootfs_path);
            replaced = true;
        }
        if arg.starts_with("id=disk0,if=none,format=raw,file=") {
            *arg = replacement.clone();
            replaced = true;
        }
    }

    if replaced {
        return;
    }

    if let Some(device_pos) = args.iter().position(|arg| {
        matches!(
            arg.as_str(),
            "virtio-blk-device,drive=disk0" | "virtio-blk-pci,drive=disk0"
        )
    }) {
        let insert_pos = device_pos + 1;
        args.insert(insert_pos, "-drive".to_string());
        args.insert(insert_pos + 1, replacement);
    }
}

#[cfg(test)]
mod tests {
    use tempfile::tempdir;

    use super::*;

    #[test]
    fn infer_rootfs_path_uses_vmconfig_kernel_sibling() {
        let root = tempdir().unwrap();
        let image_dir = root.path().join("image");
        fs::create_dir_all(&image_dir).unwrap();
        fs::write(image_dir.join("rootfs.img"), b"rootfs").unwrap();
        let vmconfig = root.path().join("vm.toml");
        fs::write(
            &vmconfig,
            format!(
                r#"
[kernel]
kernel_path = "{}"
"#,
                image_dir.join("qemu-aarch64").display()
            ),
        )
        .unwrap();

        assert_eq!(
            infer_rootfs_path(&[vmconfig]).unwrap(),
            Some(image_dir.join("rootfs.img"))
        );
    }

    #[test]
    fn apply_rootfs_path_overrides_rootfs_when_vmconfig_provides_one() {
        let root = tempdir().unwrap();
        let image_dir = root.path().join("image");
        fs::create_dir_all(&image_dir).unwrap();
        let rootfs_path = image_dir.join("rootfs.img");
        fs::write(&rootfs_path, b"rootfs").unwrap();
        let vmconfig = root.path().join("vm.toml");
        fs::write(
            &vmconfig,
            format!(
                r#"
[kernel]
kernel_path = "{}"
"#,
                image_dir.join("qemu-aarch64").display()
            ),
        )
        .unwrap();

        let mut qemu = QemuConfig {
            args: vec![format!(
                "id=disk0,if=none,format=raw,file={AXVISOR_DEFAULT_ROOTFS}"
            )],
            ..Default::default()
        };
        apply_rootfs_path(
            &mut qemu,
            &ResolvedAxvisorRequest {
                package: crate::axvisor::build::AXVISOR_PACKAGE.to_string(),
                axvisor_dir: root.path().join("os/axvisor"),
                arch: "aarch64".to_string(),
                target: "aarch64-unknown-none-softfloat".to_string(),
                plat_dyn: None,
                debug: false,
                build_info_path: root.path().join(".build.toml"),
                qemu_config: None,
                uboot_config: None,
                vmconfigs: vec![vmconfig],
            },
        )
        .unwrap();

        assert_eq!(
            qemu.args,
            vec![format!(
                "id=disk0,if=none,format=raw,file={}",
                rootfs_path.display()
            )]
        );
    }

    #[test]
    fn apply_rootfs_path_uses_axvisor_tmp_rootfs_by_default() {
        let root = tempdir().unwrap();
        let axvisor_dir = root.path().join("os/axvisor");
        let mut qemu = QemuConfig {
            args: vec![format!(
                "id=disk0,if=none,format=raw,file={AXVISOR_DEFAULT_ROOTFS}"
            )],
            ..Default::default()
        };

        apply_rootfs_path(
            &mut qemu,
            &ResolvedAxvisorRequest {
                package: crate::axvisor::build::AXVISOR_PACKAGE.to_string(),
                axvisor_dir: axvisor_dir.clone(),
                arch: "aarch64".to_string(),
                target: "aarch64-unknown-none-softfloat".to_string(),
                plat_dyn: None,
                debug: false,
                build_info_path: axvisor_dir.join(".build.toml"),
                qemu_config: None,
                uboot_config: None,
                vmconfigs: vec![],
            },
        )
        .unwrap();

        assert_eq!(
            qemu.args,
            vec![format!(
                "id=disk0,if=none,format=raw,file={}",
                axvisor_dir.join("tmp/rootfs.img").display()
            )]
        );
    }

    #[test]
    fn apply_rootfs_path_inserts_drive_arg_when_template_omits_it() {
        let root = tempdir().unwrap();
        let axvisor_dir = root.path().join("os/axvisor");
        let mut qemu = QemuConfig {
            args: vec![
                "-device".to_string(),
                "virtio-blk-device,drive=disk0".to_string(),
                "-append".to_string(),
                "root=/dev/vda rw init=/init".to_string(),
            ],
            ..Default::default()
        };

        apply_rootfs_path(
            &mut qemu,
            &ResolvedAxvisorRequest {
                package: crate::axvisor::build::AXVISOR_PACKAGE.to_string(),
                axvisor_dir: axvisor_dir.clone(),
                arch: "aarch64".to_string(),
                target: "aarch64-unknown-none-softfloat".to_string(),
                plat_dyn: None,
                debug: false,
                build_info_path: axvisor_dir.join(".build.toml"),
                qemu_config: None,
                uboot_config: None,
                vmconfigs: vec![],
            },
        )
        .unwrap();

        assert_eq!(
            qemu.args,
            vec![
                "-device".to_string(),
                "virtio-blk-device,drive=disk0".to_string(),
                "-drive".to_string(),
                format!(
                    "id=disk0,if=none,format=raw,file={}",
                    axvisor_dir.join("tmp/rootfs.img").display()
                ),
                "-append".to_string(),
                "root=/dev/vda rw init=/init".to_string(),
            ]
        );
    }

    #[test]
    fn load_qemu_config_parses_template_file() {
        let root = tempdir().unwrap();
        let qemu_config = root.path().join("qemu-aarch64.toml");
        fs::write(
            &qemu_config,
            r#"
args = ["-nographic"]
success_regex = []
fail_regex = []
to_bin = true
uefi = false
"#,
        )
        .unwrap();

        let qemu = load_qemu_config(&qemu_config).unwrap();

        assert_eq!(qemu.args, vec!["-nographic".to_string()]);
        assert!(qemu.to_bin);
    }
}