395 lines
10 KiB
Rust
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(())
|
|
}
|