axbuild 0.4.17

An OS build lib toolkit used by arceos
Documentation
use super::*;

pub(super) struct QemuArgs<'a> {
    args: &'a [String],
}

impl<'a> QemuArgs<'a> {
    fn new(args: &'a [String]) -> Self {
        Self { args }
    }

    fn option_value(&self, option: &str) -> Option<&str> {
        let index = self.args.iter().position(|arg| arg == option)?;
        self.args.get(index + 1).map(String::as_str)
    }
}

pub(super) struct QemuArgsMut<'a> {
    args: &'a mut Vec<String>,
}

impl<'a> QemuArgsMut<'a> {
    fn new(args: &'a mut Vec<String>) -> Self {
        Self { args }
    }

    fn set_option_value(&mut self, option: &str, value: String) {
        if let Some(index) = self.args.iter().position(|arg| arg == option)
            && let Some(existing) = self.args.get_mut(index + 1)
        {
            *existing = value;
            return;
        }

        self.args.push(option.to_string());
        self.args.push(value);
    }
}

pub(crate) fn apply_smp_qemu_arg(qemu: &mut QemuConfig, smp: Option<usize>) {
    let Some(cpu_num) = smp else {
        return;
    };

    QemuArgsMut::new(&mut qemu.args).set_option_value("-smp", cpu_num.to_string());
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum DynamicPlatformBootArch {
    X86_64,
    LoongArch64,
}

pub(crate) fn apply_dynamic_platform_qemu_boot(qemu: &mut QemuConfig, cargo: &Cargo) {
    apply_dynamic_platform_qemu_boot_with_kvm_probe(qemu, cargo, host_kvm_available);
}

pub(super) fn apply_dynamic_platform_qemu_boot_with_kvm_probe(
    qemu: &mut QemuConfig,
    cargo: &Cargo,
    kvm_available: impl FnOnce() -> bool,
) {
    apply_x86_64_kvm_accel_if_available_with_probe(qemu, cargo, kvm_available);

    let Some(arch) = cargo_dynamic_platform_boot_arch(cargo) else {
        return;
    };

    qemu.uefi = true;
    qemu.to_bin = true;
    apply_drive_snapshot_without_global_snapshot(qemu);

    if arch != DynamicPlatformBootArch::X86_64 {
        return;
    }

    ensure_uefi_drive_bus(qemu);
    keep_qemu_default_devices_for_uefi(qemu);
    disable_unneeded_default_x86_64_devices(qemu);
    disable_dynamic_x86_64_five_level_paging(qemu);
    enable_dynamic_x86_64_nested_virtualization_features(qemu, cargo);
    apply_dynamic_x86_64_qemu_debug_args(qemu);
}

pub(super) fn apply_x86_64_kvm_accel_if_available_with_probe(
    qemu: &mut QemuConfig,
    cargo: &Cargo,
    kvm_available: impl FnOnce() -> bool,
) {
    if !cargo_target_is_x86_64(&cargo.target) {
        return;
    }
    if qemu.args.iter().any(|arg| arg == "-accel") {
        return;
    }
    if !kvm_available() {
        return;
    }

    qemu.args.push("-accel".to_string());
    qemu.args.push("kvm".to_string());
}

#[cfg(unix)]
pub(super) fn host_kvm_available() -> bool {
    use std::fs::OpenOptions;

    OpenOptions::new()
        .read(true)
        .write(true)
        .open("/dev/kvm")
        .is_ok()
}

#[cfg(not(unix))]
pub(super) fn host_kvm_available() -> bool {
    false
}

pub(super) fn apply_dynamic_x86_64_qemu_debug_args(qemu: &mut QemuConfig) {
    let Ok(value) = std::env::var(DYNAMIC_X86_64_QEMU_DEBUG_ENV) else {
        return;
    };
    if !env_flag_enabled(&value) {
        return;
    }

    push_unique_arg(&mut qemu.args, "-no-reboot");
    push_unique_arg(&mut qemu.args, "-S");
    push_unique_arg(&mut qemu.args, "-s");
}

pub(super) fn env_flag_enabled(value: &str) -> bool {
    !matches!(
        value.trim().to_ascii_lowercase().as_str(),
        "" | "0" | "false" | "no" | "off"
    )
}

pub(super) fn push_unique_arg(args: &mut Vec<String>, arg: &str) {
    if !args.iter().any(|existing| existing == arg) {
        args.push(arg.to_string());
    }
}

pub(super) fn ensure_uefi_drive_bus(qemu: &mut QemuConfig) {
    for index in 0..qemu.args.len() {
        if qemu.args.get(index).is_some_and(|arg| arg == "-machine")
            && let Some(machine) = qemu.args.get_mut(index + 1)
        {
            remove_machine_option(machine, "sata=off");
            remove_machine_option(machine, "i8042=off");
        }
    }
}

pub(super) fn remove_machine_option(machine: &mut String, option: &str) {
    let parts = machine
        .split(',')
        .filter(|part| *part != option)
        .collect::<Vec<_>>();
    *machine = parts.join(",");
}

pub(super) fn keep_qemu_default_devices_for_uefi(qemu: &mut QemuConfig) {
    qemu.args.retain(|arg| arg != "-nodefaults");
}

pub(super) fn disable_unneeded_default_x86_64_devices(qemu: &mut QemuConfig) {
    let has_network_arg = qemu
        .args
        .iter()
        .any(|arg| matches!(arg.as_str(), "-net" | "-netdev" | "-nic"));
    if !has_network_arg {
        qemu.args.push("-net".to_string());
        qemu.args.push("none".to_string());
    }

    let has_vga_arg = qemu.args.iter().any(|arg| arg == "-vga");
    if !has_vga_arg {
        qemu.args.push("-vga".to_string());
        qemu.args.push("none".to_string());
    }
}

pub(super) fn disable_dynamic_x86_64_five_level_paging(qemu: &mut QemuConfig) {
    for index in 0..qemu.args.len() {
        if qemu.args.get(index).is_some_and(|arg| arg == "-cpu")
            && let Some(cpu) = qemu.args.get_mut(index + 1)
        {
            disable_qemu_cpu_feature(cpu, "la57");
        }
    }
}

pub(super) fn enable_dynamic_x86_64_nested_virtualization_features(
    qemu: &mut QemuConfig,
    cargo: &Cargo,
) {
    for index in 0..qemu.args.len() {
        if qemu.args.get(index).is_some_and(|arg| arg == "-cpu")
            && let Some(cpu) = qemu.args.get_mut(index + 1)
        {
            for feature in dynamic_x86_64_nested_virtualization_features(cargo) {
                enable_qemu_cpu_feature(cpu, feature);
            }
        }
    }
}

pub(super) fn dynamic_x86_64_nested_virtualization_features(
    cargo: &Cargo,
) -> &'static [&'static str] {
    if cargo.features.iter().any(|feature| {
        matches!(
            feature.as_str(),
            "svm" | "axvm/svm" | "x86_vcpu/svm" | "x86-vcpu/svm"
        )
    }) {
        &["svm", "npt", "nrip-save"]
    } else if cargo.features.iter().any(|feature| {
        matches!(
            feature.as_str(),
            "vmx" | "axvm/vmx" | "x86_vcpu/vmx" | "x86-vcpu/vmx"
        )
    }) {
        &["vmx-ept", "vmx-unrestricted-guest", "vmx-flexpriority"]
    } else {
        &[]
    }
}

pub(super) fn enable_qemu_cpu_feature(cpu: &mut String, feature: &str) {
    let enabled_feature = format!("+{feature}");
    if cpu.split(',').any(|part| part.trim() == enabled_feature) {
        return;
    }

    if !cpu.is_empty() {
        cpu.push(',');
    }
    cpu.push_str(&enabled_feature);
}

pub(super) fn disable_qemu_cpu_feature(cpu: &mut String, feature: &str) {
    let disabled_feature = format!("-{feature}");
    if cpu.split(',').any(|part| part.trim() == disabled_feature) {
        return;
    }

    if !cpu.is_empty() {
        cpu.push(',');
    }
    cpu.push_str(&disabled_feature);
}

pub(crate) fn apply_drive_snapshot_without_global_snapshot(qemu: &mut QemuConfig) {
    let mut global_snapshot = false;
    qemu.args.retain(|arg| {
        let keep = arg != "-snapshot";
        if !keep {
            global_snapshot = true;
        }
        keep
    });
    if !global_snapshot {
        return;
    }

    for index in 0..qemu.args.len() {
        if qemu.args.get(index).is_some_and(|arg| arg == "-drive")
            && let Some(drive) = qemu.args.get_mut(index + 1)
        {
            ensure_drive_snapshot_on(drive);
        }
    }
}

pub(super) fn ensure_drive_snapshot_on(drive: &mut String) {
    let mut replaced = false;
    let parts = drive
        .split(',')
        .map(|part| {
            if part.starts_with("snapshot=") {
                replaced = true;
                "snapshot=on".to_string()
            } else {
                part.to_string()
            }
        })
        .collect::<Vec<_>>();
    if replaced {
        *drive = parts.join(",");
    } else {
        drive.push_str(",snapshot=on");
    }
}

pub(super) fn cargo_dynamic_platform_boot_arch(cargo: &Cargo) -> Option<DynamicPlatformBootArch> {
    if cargo_target_is_dynamic_x86_64(&cargo.target) {
        Some(DynamicPlatformBootArch::X86_64)
    } else if cargo_target_is_dynamic_loongarch64(&cargo.target) {
        Some(DynamicPlatformBootArch::LoongArch64)
    } else {
        None
    }
}

pub(super) fn cargo_target_is_dynamic_x86_64(target: &str) -> bool {
    cargo_target_is_x86_64(target)
}

pub(super) fn cargo_target_is_x86_64(target: &str) -> bool {
    let target = target.strip_suffix(".json").unwrap_or(target);
    target.ends_with("x86_64-unknown-none") || target.ends_with("x86_64-unknown-linux-musl")
}

pub(super) fn cargo_target_is_dynamic_loongarch64(target: &str) -> bool {
    let target = target.strip_suffix(".json").unwrap_or(target);
    target.ends_with("loongarch64-unknown-none-softfloat")
        || target.ends_with("loongarch64-unknown-linux-musl")
}

pub(crate) fn smp_from_qemu_arg(qemu: &QemuConfig) -> Option<usize> {
    let args = QemuArgs::new(&qemu.args);
    let value = args.option_value("-smp")?;
    parse_smp_qemu_value(value)
}

pub(super) fn parse_smp_qemu_value(value: &str) -> Option<usize> {
    let first = value.split(',').next()?;
    if let Ok(cpu_num) = first.parse() {
        return Some(cpu_num);
    }

    value.split(',').find_map(|part| {
        let cpu_num = part.strip_prefix("cpus=")?;
        cpu_num.parse().ok()
    })
}

pub(crate) fn apply_timeout_scale(qemu: &mut QemuConfig) {
    let Some(timeout) = qemu.timeout else {
        return;
    };
    if timeout == 0 {
        return;
    }

    let scale = match std::env::var(TIMEOUT_SCALE_ENV) {
        Ok(value) => match value.trim().parse::<u64>() {
            Ok(scale) if scale > 1 => scale,
            Ok(_) | Err(_) => {
                eprintln!(
                    "warning: ignoring invalid {TIMEOUT_SCALE_ENV} value `{}`; expected integer > \
                     1",
                    value.trim()
                );
                return;
            }
        },
        Err(_) => return,
    };

    qemu.timeout = timeout.checked_mul(scale).or(Some(u64::MAX));
}

pub(crate) fn qemu_timeout_summary(qemu: &QemuConfig) -> String {
    match qemu.timeout {
        Some(0) | None => "disabled".to_string(),
        Some(timeout) => format!("{timeout}s"),
    }
}