commit d20b34f9533684edeab295b0a76f3e12620d4e09 Author: Mark Poliakov Date: Mon Mar 4 17:55:39 2024 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1eba605 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "qemu" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8399fe3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "qemu" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/src/aarch64.rs b/src/aarch64.rs new file mode 100644 index 0000000..51e9bc8 --- /dev/null +++ b/src/aarch64.rs @@ -0,0 +1,153 @@ +use std::{path::PathBuf, process::Command}; + +use crate::{Architecture, IntoArgs}; + +#[derive(Debug)] +pub enum Machine { + Virt { virtualize: bool }, +} + +#[derive(Debug)] +pub enum Cpu { + CortexA57, +} + +#[derive(Debug)] +pub enum Image { + Kernel { + kernel: PathBuf, + initrd: Option, + }, +} + +#[derive(Debug)] +pub struct QemuAArch64; + +impl IntoArgs for Machine { + fn add_args(&self, command: &mut Command) { + command.arg("-M"); + match self { + &Self::Virt { virtualize } => { + let mut arg = "virt".to_owned(); + if virtualize { + arg.push_str(",virtualization=on"); + } + command.arg(arg); + } + } + } +} + +impl IntoArgs for Cpu { + fn add_args(&self, command: &mut Command) { + command.arg("-cpu"); + match self { + Self::CortexA57 => { + command.arg("cortex-a57"); + } + } + } +} + +impl IntoArgs for Image { + fn add_args(&self, command: &mut Command) { + match self { + Self::Kernel { kernel, initrd } => { + command.arg("-kernel").arg(kernel); + if let Some(initrd) = initrd { + command.arg("-initrd").arg(initrd); + } + } + } + } +} + +impl IntoArgs for QemuAArch64 { + fn add_args(&self, _command: &mut Command) {} +} + +impl Architecture for QemuAArch64 { + type MachineType = Machine; + type CpuType = Cpu; + type ImageType = Image; + + const DEFAULT_COMMAND: &'static str = "qemu-system-aarch64"; +} + +#[cfg(test)] +mod tests { + use crate::{aarch64::Machine, device::QemuSerialTarget, Qemu}; + + use super::{Cpu, Image}; + + #[test] + fn boot_options() { + let mut qemu = Qemu::new_aarch64(); + qemu.with_boot_image(Image::Kernel { + kernel: "my-kernel.bin".into(), + initrd: Some("my-initrd.img".into()), + }); + let command = qemu.into_command(); + + assert_eq!( + command.get_args().collect::>(), + vec!["-kernel", "my-kernel.bin", "-initrd", "my-initrd.img"] + ); + + let mut qemu = Qemu::new_aarch64(); + qemu.with_boot_image(Image::Kernel { + kernel: "my-kernel.bin".into(), + initrd: None, + }); + let command = qemu.into_command(); + + assert_eq!( + command.get_args().collect::>(), + vec!["-kernel", "my-kernel.bin"] + ); + } + + #[test] + fn machine_options() { + let mut qemu = Qemu::new_aarch64(); + qemu.with_machine(Machine::Virt { virtualize: true }); + + let command = qemu.into_command(); + + assert_eq!( + command.get_args().collect::>(), + vec!["-M", "virt,virtualization=on"] + ); + } + + #[test] + fn full_commands() { + let mut qemu = Qemu::new_aarch64(); + qemu.override_qemu("my-qemu"); + qemu.with_boot_image(Image::Kernel { + kernel: "my-kernel.bin".into(), + initrd: Some("my-initrd.img".into()), + }); + qemu.with_machine(Machine::Virt { virtualize: false }); + qemu.with_cpu(Cpu::CortexA57); + qemu.with_serial(QemuSerialTarget::MonStdio); + + let command = qemu.into_command(); + + assert_eq!( + command.get_args().collect::>(), + vec![ + "-serial", + "mon:stdio", + "-M", + "virt", + "-cpu", + "cortex-a57", + "-kernel", + "my-kernel.bin", + "-initrd", + "my-initrd.img" + ] + ); + } +} diff --git a/src/device.rs b/src/device.rs new file mode 100644 index 0000000..099d3cd --- /dev/null +++ b/src/device.rs @@ -0,0 +1,80 @@ +use std::process::Command; + +use crate::IntoArgs; + +#[derive(Debug)] +pub enum QemuNic { + VirtioPci { mac: Option }, +} + +#[derive(Debug)] +pub enum QemuDevice { + NetworkTap { + nic: QemuNic, + script: Option, + ifname: Option, + }, +} + +#[derive(Debug)] +pub enum QemuSerialTarget { + MonStdio, + Stdio, +} + +impl IntoArgs for QemuNic { + fn add_args(&self, command: &mut Command) { + match self { + Self::VirtioPci { mac } => { + command.arg("-device"); + let mut val = "virtio-net-pci,netdev=net0".to_owned(); + if let Some(mac) = mac { + val.push_str(",mac="); + val.push_str(mac); + } + command.arg(val); + } + } + } +} + +// TODO separate handling for devices +impl IntoArgs for QemuDevice { + fn add_args(&self, command: &mut Command) { + match self { + Self::NetworkTap { + nic, + script, + ifname, + } => { + command.arg("-netdev"); + let mut val = "tap,id=net0".to_owned(); + if let Some(script) = script { + val.push_str(",script="); + val.push_str(script); + } + if let Some(ifname) = ifname { + val.push_str(",ifname="); + val.push_str(ifname); + } + command.arg(val); + + nic.add_args(command); + } + } + } +} + +impl IntoArgs for QemuSerialTarget { + fn add_args(&self, command: &mut Command) { + command.arg("-serial"); + match self { + Self::MonStdio => { + command.arg("mon:stdio"); + } + Self::Stdio => { + command.arg("stdio"); + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..166d09f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,206 @@ +use std::{fmt::Debug, path::PathBuf, process::Command}; + +use device::{QemuDevice, QemuSerialTarget}; + +pub mod aarch64; +pub mod x86_64; + +pub mod device; + +pub trait Architecture: IntoArgs { + type MachineType: IntoArgs; + type CpuType: IntoArgs; + type ImageType: IntoArgs; + + const DEFAULT_COMMAND: &'static str; +} + +pub trait IntoArgs { + fn add_args(&self, command: &mut Command); +} + +impl IntoArgs for Option { + fn add_args(&self, command: &mut Command) { + if let Some(val) = self { + val.add_args(command); + } + } +} + +#[derive(Debug, Default)] +struct QemuCommon { + devices: Vec, + serial: Option, +} + +#[derive(Debug, Default)] +pub struct Qemu { + binary_override: Option, + common: QemuCommon, + cpu: Option, + machine: Option, + boot_image: Option, + no_display: bool, + memory_megabytes: Option, + smp: Option, + arch: A, +} + +impl Qemu { + pub fn new_aarch64() -> Self { + Qemu::new(aarch64::QemuAArch64) + } +} + +impl Qemu { + pub fn new_x86_64() -> Self { + Qemu::new(x86_64::QemuX86_64::default()) + } + + pub fn with_bios_image(&mut self, image: PathBuf) -> &mut Self { + self.arch.bios_image = Some(image); + self + } + + pub fn with_boot_slot(&mut self, slot: char) -> &mut Self { + self.arch.boot_slot = Some(slot); + self + } +} + +impl Qemu { + pub fn new(arch: A) -> Self { + Self { + binary_override: None, + common: QemuCommon::default(), + + memory_megabytes: None, + no_display: false, + cpu: None, + machine: None, + boot_image: None, + smp: None, + arch, + } + } + + pub fn with_device(&mut self, device: QemuDevice) -> &mut Self { + self.common.devices.push(device); + self + } + + pub fn with_memory_megabytes(&mut self, value: usize) -> &mut Self { + self.memory_megabytes = Some(value); + self + } + + pub fn with_serial(&mut self, serial: QemuSerialTarget) -> &mut Self { + self.common.serial = Some(serial); + self + } + + pub fn with_boot_image(&mut self, image: A::ImageType) -> &mut Self { + self.boot_image = Some(image); + self + } + + pub fn with_cpu(&mut self, cpu: A::CpuType) -> &mut Self { + self.cpu = Some(cpu); + self + } + + pub fn with_machine(&mut self, machine: A::MachineType) -> &mut Self { + self.machine = Some(machine); + self + } + + pub fn disable_display(&mut self) -> &mut Self { + self.no_display = true; + self + } + + pub fn override_qemu>(&mut self, qemu: S) -> &mut Self { + self.binary_override = Some(qemu.into()); + self + } + + pub fn into_command(self) -> Command { + let qemu = self + .binary_override + .clone() + .unwrap_or_else(|| A::DEFAULT_COMMAND.into()); + + let mut command = Command::new(qemu); + + self.add_args(&mut command); + + command + } +} + +impl IntoArgs for QemuCommon { + fn add_args(&self, command: &mut Command) { + self.serial.add_args(command); + } +} + +impl IntoArgs for Qemu { + fn add_args(&self, command: &mut Command) { + self.common.add_args(command); + + for device in &self.common.devices { + device.add_args(command); + } + + if let Some(mem) = self.memory_megabytes { + command.args(["-m", &format!("{}m", mem)]); + } + + self.machine.add_args(command); + self.cpu.add_args(command); + self.boot_image.add_args(command); + + if self.no_display { + command.args(["-display", "none"]); + } + + if let Some(smp) = self.smp { + command.args(["-smp", &format!("{}", smp)]); + } + + self.arch.add_args(command); + } +} + +#[cfg(test)] +mod tests { + use crate::{device::QemuSerialTarget, Qemu}; + + #[test] + fn empty() { + let builder = Qemu::new_aarch64(); + let command = builder.into_command(); + assert_eq!(command.get_program(), "qemu-system-aarch64"); + assert_eq!(command.get_args().len(), 0); + } + + #[test] + fn qemu_override() { + let mut qemu = Qemu::new_aarch64(); + qemu.override_qemu("test-qemu"); + let command = qemu.into_command(); + assert_eq!(command.get_program(), "test-qemu"); + } + + #[test] + fn common_options() { + let mut qemu = Qemu::new_aarch64(); + qemu.with_serial(QemuSerialTarget::MonStdio); + let command = qemu.into_command(); + + assert_eq!( + command.get_args().collect::>(), + vec!["-serial", "mon:stdio"] + ); + } +} diff --git a/src/x86_64.rs b/src/x86_64.rs new file mode 100644 index 0000000..2121b5e --- /dev/null +++ b/src/x86_64.rs @@ -0,0 +1,84 @@ +use std::{path::PathBuf, process::Command}; + +use crate::{Architecture, IntoArgs}; + +#[derive(Debug)] +pub enum Machine { + Q35, +} + +#[derive(Debug)] +pub enum Cpu { + Host { enable_kvm: bool }, +} + +#[derive(Debug)] +pub enum Image { + Drive(PathBuf), +} + +#[derive(Debug, Default)] +pub struct QemuX86_64 { + pub bios_image: Option, + pub boot_slot: Option, +} + +impl IntoArgs for Machine { + fn add_args(&self, command: &mut Command) { + match self { + Self::Q35 => { + command.args(["-M", "q35"]); + } + } + } +} + +impl IntoArgs for Cpu { + fn add_args(&self, command: &mut Command) { + match self { + &Self::Host { enable_kvm } => { + command.args(["-cpu", "host"]); + if enable_kvm { + command.arg("-enable-kvm"); + } + } + } + } +} + +impl IntoArgs for Image { + fn add_args(&self, command: &mut Command) { + match self { + Self::Drive(path) => { + command.args(["-drive", &format!("format=raw,file={}", path.display())]); + } + } + } +} + +impl IntoArgs for QemuX86_64 { + fn add_args(&self, command: &mut Command) { + if let Some(slot) = self.boot_slot { + command.arg("-boot"); + command.arg(format!("{}", slot)); + } + + if let Some(bios_image) = self.bios_image.as_ref() { + command.args([ + "-drive", + &format!( + "format=raw,if=pflash,readonly=on,file={}", + bios_image.display() + ), + ]); + } + } +} + +impl Architecture for QemuX86_64 { + type MachineType = Machine; + type CpuType = Cpu; + type ImageType = Image; + + const DEFAULT_COMMAND: &'static str = "qemu-system-x86_64"; +}