yggdrasil/xtask/src/qemu.rs

395 lines
10 KiB
Rust

use std::{
io,
path::{Path, PathBuf},
process::Command,
};
use qemu::{
aarch64,
device::{QemuDevice, QemuDrive, QemuNic, QemuSerialTarget},
i386, riscv64, x86_64, Qemu,
};
use crate::{
build::{self, AllBuilt, ImageBuilt, InitrdGenerated, KernelBin, KernelBuilt, KernelProcessed},
env::{Board, BuildEnv},
error::Error,
util::run_external_command,
};
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
enum QemuNetworkInterface {
#[default]
VirtioNet,
Rtl8139,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case", default)]
struct QemuNetworkConfig {
enable: bool,
interface_name: String,
mac: String,
interface: QemuNetworkInterface,
}
#[derive(Debug, Default, Clone, Copy, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
enum QemuDiskInterface {
#[default]
Nvme,
Ahci,
}
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case", default)]
struct QemuDiskConfig {
interface: QemuDiskInterface,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case", default)]
struct QemuX86_64MachineConfig {
enable_kvm: bool,
memory: usize,
uefi_code_path: PathBuf,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case", default)]
struct QemuAArch64MachineConfig {
memory: usize,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case", default)]
struct QemuI686MachineConfig {
enable_kvm: bool,
memory: usize,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(default)]
struct QemuMachineConfig {
x86_64: QemuX86_64MachineConfig,
aarch64: QemuAArch64MachineConfig,
i686: QemuI686MachineConfig,
smp: usize,
}
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case", default)]
struct QemuConfig {
network: QemuNetworkConfig,
disk: QemuDiskConfig,
machine: QemuMachineConfig,
}
impl Default for QemuMachineConfig {
fn default() -> Self {
Self {
x86_64: Default::default(),
aarch64: Default::default(),
i686: Default::default(),
smp: 4,
}
}
}
impl Default for QemuI686MachineConfig {
fn default() -> Self {
Self {
enable_kvm: true,
memory: 512,
}
}
}
impl Default for QemuAArch64MachineConfig {
fn default() -> Self {
Self { memory: 512 }
}
}
impl Default for QemuX86_64MachineConfig {
fn default() -> Self {
Self {
enable_kvm: true,
memory: 1024,
uefi_code_path: PathBuf::from("/usr/share/edk2-ovmf/x64/OVMF_CODE.fd"),
}
}
}
impl Default for QemuNetworkConfig {
fn default() -> Self {
Self {
enable: true,
interface_name: "qemu-tap0".into(),
mac: "12:34:56:65:43:21".into(),
interface: QemuNetworkInterface::default(),
}
}
}
impl From<QemuDiskInterface> for QemuDrive {
fn from(value: QemuDiskInterface) -> Self {
match value {
QemuDiskInterface::Nvme => Self::Nvme,
QemuDiskInterface::Ahci => Self::Sata,
}
}
}
fn make_kernel_bin<S: AsRef<Path>, D: AsRef<Path>>(src: S, dst: D) -> Result<(), Error> {
run_external_command(
"llvm-objcopy",
[
"-O".as_ref(),
"binary".as_ref(),
src.as_ref().as_os_str(),
dst.as_ref().as_os_str(),
],
false,
)
}
// TODO device settings
fn run_aarch64(
config: &QemuConfig,
env: &BuildEnv,
qemu_bin: Option<PathBuf>,
devices: Vec<QemuDevice>,
kernel_bin: PathBuf,
initrd: PathBuf,
) -> Result<Command, Error> {
let mut qemu = Qemu::new_aarch64();
if let Some(qemu_bin) = qemu_bin {
qemu.override_qemu(qemu_bin);
}
qemu.with_smp(config.machine.smp)
.with_boot_image(aarch64::Image::Kernel {
kernel: kernel_bin,
initrd: Some(initrd),
})
.disable_display();
match env.board {
Board::raspi4b => qemu
.with_machine(aarch64::Machine::Raspi4b)
.with_serial(QemuSerialTarget::None)
.with_serial(QemuSerialTarget::MonStdio),
Board::virt | Board::default => qemu
.with_machine(aarch64::Machine::Virt { virtualize: true })
.with_serial(QemuSerialTarget::MonStdio)
.with_cpu(aarch64::Cpu::Max)
.with_memory_megabytes(config.machine.aarch64.memory),
_ => {
return Err(Error::UnsupportedEmulation(env.board));
}
};
if env.board != Board::raspi4b {
for device in devices {
qemu.with_device(device);
}
}
let mut command = qemu.into_command();
if env.board == Board::raspi4b {
// Provide the dtb
command.args([
"-dtb",
&format!(
"{}/etc/dtb/bcm2711-rpi-4-b.dtb",
env.workspace_root.display()
),
]);
}
Ok(command)
}
fn run_x86_64(
config: &QemuConfig,
qemu_bin: Option<PathBuf>,
devices: Vec<QemuDevice>,
image: PathBuf,
) -> Result<Command, Error> {
let mut qemu = Qemu::new_x86_64();
if let Some(qemu_bin) = qemu_bin {
qemu.override_qemu(qemu_bin);
}
qemu.with_serial(QemuSerialTarget::MonStdio)
.with_cpu(x86_64::Cpu::Host {
enable_kvm: config.machine.x86_64.enable_kvm,
})
.with_smp(config.machine.smp)
.with_machine(x86_64::Machine::Q35)
.with_boot_slot('a')
.with_bios_image(config.machine.x86_64.uefi_code_path.clone())
.with_boot_image(x86_64::Image::Drive(image))
.with_memory_megabytes(config.machine.x86_64.memory);
for device in devices {
qemu.with_device(device);
}
Ok(qemu.into_command())
}
fn run_i686(
config: &QemuConfig,
qemu_bin: Option<PathBuf>,
devices: Vec<QemuDevice>,
image: PathBuf,
) -> Result<Command, Error> {
let mut qemu = Qemu::new_i386();
if let Some(qemu_bin) = qemu_bin {
qemu.override_qemu(qemu_bin);
}
qemu.with_serial(QemuSerialTarget::MonStdio)
.with_cpu(i386::Cpu::Host {
enable_kvm: config.machine.i686.enable_kvm,
})
.with_machine(i386::Machine::Q35)
.with_cdrom(&image)
.with_memory_megabytes(config.machine.i686.memory);
for device in devices {
qemu.with_device(device);
}
Ok(qemu.into_command())
}
fn run_riscv64(
config: &QemuConfig,
env: &BuildEnv,
qemu_bin: Option<PathBuf>,
devices: Vec<QemuDevice>,
kernel: PathBuf,
initrd: PathBuf,
) -> Result<Command, Error> {
let _ = config;
let _ = devices;
let mut qemu = Qemu::new_riscv64();
if let Some(qemu_bin) = qemu_bin {
qemu.override_qemu(qemu_bin);
}
let bios = env.workspace_root.join("boot/riscv/fw_jump.bin");
qemu.with_serial(QemuSerialTarget::MonStdio)
.with_machine(riscv64::Machine::Virt)
.with_cpu(riscv64::Cpu::Rv64)
.with_memory_megabytes(1024)
.disable_display()
.with_boot_image(riscv64::Image::OpenSBI {
kernel,
initrd,
bios,
});
Ok(qemu.into_command())
}
fn load_qemu_config<P: AsRef<Path>>(path: P) -> Result<QemuConfig, Error> {
let path = path.as_ref();
if path.exists() {
let config = std::fs::read_to_string(path)?;
let config =
toml::from_str(&config).map_err(|e| Error::TomlParseError(path.to_owned(), e))?;
Ok(config)
} else {
Ok(QemuConfig::default())
}
}
fn add_devices_from_config(
devices: &mut Vec<QemuDevice>,
disk: Option<&PathBuf>,
config: &QemuConfig,
) -> Result<(), Error> {
if config.network.enable {
let mac = Some(config.network.mac.clone());
let nic = match config.network.interface {
QemuNetworkInterface::VirtioNet => QemuNic::VirtioPci { mac },
QemuNetworkInterface::Rtl8139 => QemuNic::Rtl8139 { mac },
};
devices.push(QemuDevice::NetworkTap {
nic,
script: Some("xtask/scripts/qemu-ifup".into()),
ifname: Some(config.network.interface_name.clone()),
});
}
if let Some(disk) = disk {
devices.push(QemuDevice::Drive {
ty: config.disk.interface.into(),
file: disk.clone(),
serial: Some("deadbeef".into()),
});
}
Ok(())
}
pub fn dump_config<W: io::Write>(env: &BuildEnv, output: &mut W) {
let config_path = env.workspace_root.join("qemu.toml");
if config_path.exists() {
writeln!(output, " * Using qemu.toml:").ok();
} else {
writeln!(output, " * Using default qemu config:").ok();
}
let config = load_qemu_config(config_path);
writeln!(output).ok();
match config.as_ref().map(toml::to_string_pretty) {
Ok(Ok(string)) => writeln!(output, "{string}").ok(),
Ok(Err(error)) => writeln!(output, "<Serialization error: {error}>").ok(),
Err(error) => writeln!(output, "<Load error: {error}>").ok(),
};
}
pub fn run(
env: BuildEnv,
qemu: Option<PathBuf>,
disk: Option<PathBuf>,
extra_args: Vec<String>,
) -> Result<(), Error> {
let config = load_qemu_config(env.workspace_root.join("qemu.toml"))?;
let kernel_output_dir = env.kernel_output_dir.clone();
let kernel_bin = kernel_output_dir.join("kernel.bin");
// Rebuild the image
let built = build::build_all(&env)?;
let mut devices = vec![];
add_devices_from_config(&mut devices, disk.as_ref(), &config)?;
let mut command = match built {
AllBuilt::Riscv64(KernelBin(kernel), InitrdGenerated(initrd)) => {
run_riscv64(&config, &env, qemu, devices, kernel, initrd)?
}
AllBuilt::AArch64(KernelProcessed(KernelBuilt(kernel)), InitrdGenerated(initrd)) => {
make_kernel_bin(kernel, &kernel_bin)?;
run_aarch64(&config, &env, qemu, devices, kernel_bin, initrd)?
}
AllBuilt::X86_64(ImageBuilt(image)) => run_x86_64(&config, qemu, devices, image)?,
AllBuilt::I686(ImageBuilt(image)) => run_i686(&config, qemu, devices, image)?,
};
command.args(extra_args);
log::trace!("Run QEMU: {:?}", command);
log::info!("Start qemu");
command.status()?;
Ok(())
}