diff --git a/.github/workflows/kernel_test.yml b/.github/workflows/kernel_test.yml index aee8738a..70755a42 100644 --- a/.github/workflows/kernel_test.yml +++ b/.github/workflows/kernel_test.yml @@ -58,7 +58,7 @@ jobs: - name: Boot Test (MicroVM) id: boot_test_microvm - run: make run AUTO_TEST=boot ENABLE_KVM=0 QEMU_MACHINE=microvm RELEASE_MODE=1 + run: make run AUTO_TEST=boot ENABLE_KVM=0 SCHEME=microvm RELEASE_MODE=1 - name: Boot Test (Linux Legacy 32-bit Boot Protocol) id: boot_test_linux_legacy32 @@ -74,7 +74,7 @@ jobs: - name: Syscall Test at Ext2 (MicroVM) id: syscall_test_at_ext2 - run: make run AUTO_TEST=syscall SYSCALL_TEST_DIR=/ext2 ENABLE_KVM=0 QEMU_MACHINE=microvm RELEASE_MODE=1 + run: make run AUTO_TEST=syscall SYSCALL_TEST_DIR=/ext2 ENABLE_KVM=0 SCHEME=microvm RELEASE_MODE=1 - name: Syscall Test at Exfat id: syscall_test_at_exfat_linux @@ -82,4 +82,4 @@ jobs: - name: Regression Test (MicroVM) id: regression_test_linux - run: make run AUTO_TEST=regression ENABLE_KVM=0 QEMU_MACHINE=microvm RELEASE_MODE=1 + run: make run AUTO_TEST=regression ENABLE_KVM=0 SCHEME=microvm RELEASE_MODE=1 diff --git a/Makefile b/Makefile index cc472b09..c078b918 100644 --- a/Makefile +++ b/Makefile @@ -1,38 +1,37 @@ # SPDX-License-Identifier: MPL-2.0 -# Project-wide options. +# Global options. ARCH ?= x86_64 -# End of project-wide options. +BOOT_METHOD ?= grub-rescue-iso +BOOT_PROTOCOL ?= multiboot2 +BUILD_SYSCALL_TEST ?= 0 +ENABLE_KVM ?= 1 +INTEL_TDX ?= 0 +RELEASE_MODE ?= 0 +SCHEME ?= "" +# End of global options. # The Makefile provides a way to run arbitrary tests in the kernel # mode using the kernel command line. # Here are the options for the auto test feature. AUTO_TEST ?= none -BOOT_LOADER ?= grub -BOOT_PROTOCOL ?= multiboot2 -BUILD_SYSCALL_TEST ?= 0 -ENABLE_KVM ?= 1 EXTRA_BLOCKLISTS_DIRS ?= "" -INTEL_TDX ?= 0 -SCHEMA ?= "" -RELEASE_MODE ?= 0 -SKIP_GRUB_MENU ?= 1 SYSCALL_TEST_DIR ?= /tmp # End of auto test features. CARGO_OSDK := ~/.cargo/bin/cargo-osdk -CARGO_OSDK_ARGS := --arch=$(ARCH) +CARGO_OSDK_ARGS := --target-arch=$(ARCH) ifeq ($(AUTO_TEST), syscall) BUILD_SYSCALL_TEST := 1 -CARGO_OSDK_ARGS += --kcmd_args+="SYSCALL_TEST_DIR=$(SYSCALL_TEST_DIR)" -CARGO_OSDK_ARGS += --kcmd_args+="EXTRA_BLOCKLISTS_DIRS=$(EXTRA_BLOCKLISTS_DIRS)" -CARGO_OSDK_ARGS += --init_args+="/opt/syscall_test/run_syscall_test.sh" +CARGO_OSDK_ARGS += --kcmd-args="SYSCALL_TEST_DIR=$(SYSCALL_TEST_DIR)" +CARGO_OSDK_ARGS += --kcmd-args="EXTRA_BLOCKLISTS_DIRS=$(EXTRA_BLOCKLISTS_DIRS)" +CARGO_OSDK_ARGS += --init-args="/opt/syscall_test/run_syscall_test.sh" else ifeq ($(AUTO_TEST), regression) -CARGO_OSDK_ARGS += --init_args+="/regression/run_regression_test.sh" +CARGO_OSDK_ARGS += --init-args="/regression/run_regression_test.sh" else ifeq ($(AUTO_TEST), boot) -CARGO_OSDK_ARGS += --init_args+="/regression/boot_hello.sh" +CARGO_OSDK_ARGS += --init-args="/regression/boot_hello.sh" endif ifeq ($(RELEASE_MODE), 1) @@ -43,21 +42,26 @@ ifeq ($(INTEL_TDX), 1) CARGO_OSDK_ARGS += --features intel_tdx endif -CARGO_OSDK_ARGS += --bootloader="$(BOOT_LOADER)" -CARGO_OSDK_ARGS += --boot_protocol="$(BOOT_PROTOCOL)" - -ifneq ($(SCHEMA), "") -CARGO_OSDK_ARGS += --schema $(SCHEMA) +ifneq ($(SCHEME), "") +CARGO_OSDK_ARGS += --scheme $(SCHEME) +else +CARGO_OSDK_ARGS += --boot-method="$(BOOT_METHOD)" endif # To test the linux-efi-handover64 boot protocol, we need to use Debian's # GRUB release, which is installed in /usr/bin in our Docker image. ifeq ($(BOOT_PROTOCOL), linux-efi-handover64) CARGO_OSDK_ARGS += --grub-mkrescue=/usr/bin/grub-mkrescue +CARGO_OSDK_ARGS += --grub-boot-protocol="linux" +else ifeq ($(BOOT_PROTOCOL), linux-legacy32) +CARGO_OSDK_ARGS += --linux-x86-legacy-boot +CARGO_OSDK_ARGS += --grub-boot-protocol="linux" +else +CARGO_OSDK_ARGS += --grub-boot-protocol=$(BOOT_PROTOCOL) endif ifeq ($(ENABLE_KVM), 1) -CARGO_OSDK_ARGS += --qemu_args+="--enable-kvm" +CARGO_OSDK_ARGS += --qemu-args="--enable-kvm" endif # Pass make variables to all subdirectory makes @@ -186,9 +190,11 @@ check: $(CARGO_OSDK) (echo "Error: STD_CRATES and NOSTD_CRATES combined is not the same as all workspace members" && exit 1) @rm /tmp/all_crates /tmp/combined_crates @for dir in $(NON_OSDK_CRATES); do \ + echo "Checking $$dir"; \ (cd $$dir && cargo clippy -- -D warnings) || exit 1; \ done @for dir in $(OSDK_CRATES); do \ + echo "Checking $$dir"; \ (cd $$dir && cargo osdk clippy -- -- -D warnings) || exit 1; \ done @make --no-print-directory -C regression check diff --git a/OSDK.toml b/OSDK.toml index dfce63d2..43186f93 100644 --- a/OSDK.toml +++ b/OSDK.toml @@ -1,7 +1,19 @@ -[project] -type = "kernel" +vars = [ + ["SMP", "1"], + ["MEM", "2G"], + ["EXT2_IMG", "$OSDK_CWD/regression/build/ext2.img"], + ["EXFAT_IMG", "$OSDK_CWD/regression/build/exfat.img"], +] + +[boot] +method = "grub-rescue-iso" [run] +vars = [ + ["OVMF_PATH", "/usr/share/OVMF"], +] + +[run.boot] kcmd_args = [ "SHELL=/bin/sh", "LOGNAME=root", @@ -12,117 +24,67 @@ kcmd_args = [ ] init_args = ["sh", "-l"] initramfs = "regression/build/initramfs.cpio.gz" -boot_protocol = "multiboot2" -bootloader = "grub" -ovmf = "/root/ovmf/release" -drive_files = [ - ["regression/build/ext2.img", "if=none,format=raw,id=x0"], - ["regression/build/exfat.img", "if=none,format=raw,id=x1"], -] -qemu_args = [ - "-machine q35,kernel-irqchip=split", - "-cpu Icelake-Server,+x2apic", - "--no-reboot", - "-m 2G", - "-nographic", - "-serial chardev:mux", - "-monitor chardev:mux", - "-chardev stdio,id=mux,mux=on,signal=off,logfile=qemu.log", - "-display none", - "-device isa-debug-exit,iobase=0xf4,iosize=0x04", - "-object filter-dump,id=filter0,netdev=net01,file=virtio-net.pcap", - "-netdev user,id=net01,hostfwd=tcp::36788-:22,hostfwd=tcp::55834-:8080", - "-device virtio-blk-pci,bus=pcie.0,addr=0x6,drive=x0,serial=vext2,disable-legacy=on,disable-modern=off", - "-device virtio-blk-pci,bus=pcie.0,addr=0x7,drive=x1,serial=vexfat,disable-legacy=on,disable-modern=off", - "-device virtio-keyboard-pci,disable-legacy=on,disable-modern=off", - "-device virtio-net-pci,netdev=net01,disable-legacy=on,disable-modern=off", - "-device virtio-serial-pci,disable-legacy=on,disable-modern=off", - "-device virtconsole,chardev=mux", -] [test] -boot_protocol = "multiboot" -bootloader = "qemu" -qemu_args = [ - "-machine q35,kernel-irqchip=split", - "-cpu Icelake-Server,+x2apic", - "--no-reboot", - "-m 2G", - "-nographic", - "-serial chardev:mux", - "-monitor chardev:mux", - "-chardev stdio,id=mux,mux=on,signal=off,logfile=qemu.log", - "-display none", - "-device isa-debug-exit,iobase=0xf4,iosize=0x04", - "-netdev user,id=net01,hostfwd=tcp::36788-:22,hostfwd=tcp::55834-:8080", - "-device virtio-keyboard-pci,disable-legacy=on,disable-modern=off", - "-device virtio-net-pci,netdev=net01,disable-legacy=on,disable-modern=off", - "-device virtio-serial-pci,disable-legacy=on,disable-modern=off", - "-device virtconsole,chardev=mux", -] +boot.method = "qemu-direct" +[grub] +protocol = "multiboot2" -['cfg(arch="x86_64", schema="iommu")'.run] -drive_files = [ - ["regression/build/ext2.img", "if=none,format=raw,id=x0"], - ["regression/build/exfat.img", "if=none,format=raw,id=x1"], -] -qemu_args = [ - "-machine q35,kernel-irqchip=split", - "-cpu Icelake-Server,+x2apic", - "--no-reboot", - "-m 2G", - "-nographic", - "-serial chardev:mux", - "-monitor chardev:mux", - "-chardev stdio,id=mux,mux=on,signal=off,logfile=qemu.log", - "-display none", - "-device isa-debug-exit,iobase=0xf4,iosize=0x04", - "-object filter-dump,id=filter0,netdev=net01,file=virtio-net.pcap", - "-netdev user,id=net01,hostfwd=tcp::36788-:22,hostfwd=tcp::55834-:8080", - "-device virtio-blk-pci,bus=pcie.0,addr=0x6,drive=x0,serial=vext2,disable-legacy=on,disable-modern=off,iommu_platform=on,ats=on", - "-device virtio-blk-pci,bus=pcie.0,addr=0x7,drive=x1,serial=vexfat,disable-legacy=on,disable-modern=off,iommu_platform=on,ats=on", - "-device virtio-keyboard-pci,disable-legacy=on,disable-modern=off,iommu_platform=on,ats=on", - "-device virtio-net-pci,netdev=net01,disable-legacy=on,disable-modern=off,iommu_platform=on,ats=on", - "-device virtio-serial-pci,disable-legacy=on,disable-modern=off,iommu_platform=on,ats=on", - "-device virtconsole,chardev=mux", - "-device intel-iommu,intremap=on,device-iotlb=on", - "-device ioh3420,id=pcie.0,chassis=1", -] +[qemu] +args = "$(./tools/qemu_args.sh)" -['cfg(arch="x86_64", schema="microvm")'.run] -bootloader = "qemu" -drive_files = [ - ["regression/build/ext2.img", "if=none,format=raw,id=x0"], - ["regression/build/exfat.img", "if=none,format=raw,id=x1"], -] -qemu_args = [ - "-machine microvm,rtc=on", - "-cpu Icelake-Server,+x2apic", - "--no-reboot", - "-m 2G", - "-nographic", - "-serial chardev:mux", - "-monitor chardev:mux", - "-chardev stdio,id=mux,mux=on,signal=off,logfile=qemu.log", - "-display none", - "-device isa-debug-exit,iobase=0xf4,iosize=0x04", - "-object filter-dump,id=filter0,netdev=net01,file=virtio-net.pcap", - "-netdev user,id=net01,hostfwd=tcp::36788-:22,hostfwd=tcp::55834-:8080", - "-nodefaults", - "-no-user-config", - "-device virtio-blk-device,drive=x0,serial=vext2", - "-device virtio-blk-device,drive=x1,serial=vexfat", - "-device virtio-keyboard-device", - "-device virtio-net-device,netdev=net01", - "-device virtio-serial-device", - "-device virtconsole,chardev=mux", +[scheme."microvm"] +boot.method = "qemu-direct" +vars = [ + ["MICROVM", "true"], ] +qemu.args = "$(./tools/qemu_args.sh)" -['cfg(arch="riscv64")'.run] -qemu_args = [ - "-machine virt", - "--no-reboot", - "-m 2G", - "-nographic", +[scheme."iommu"] +supported_archs = ["x86_64"] +vars = [ + ["IOMMU_DEV_EXTRA", ",iommu_platform=on,ats=on"], + ["IOMMU_EXTRA_ARGS", """\ + -device intel-iommu,intremap=on,device-iotlb=on \ + -device ioh3420,id=pcie.0,chassis=1\ + """], ] +qemu.args = "$(./tools/qemu_args.sh)" + +[scheme."tdx"] +supported_archs = ["x86_64"] +build.features = ["intel_tdx"] +vars = [ + ["MEM", "8G"], + ["OVMF_PATH", "~/tdx-tools/ovmf"], +] +boot.method = "grub-qcow2" +grub.mkrescue_path = "~/tdx-tools/grub" +grub.protocol = "linux" +qemu.args = """\ + -accel kvm \ + -name process=tdxvm,debug-threads=on \ + -m $MEM \ + -smp $SMP \ + -vga none \ + -nographic \ + -monitor pty \ + -no-hpet \ + -nodefaults \ + -monitor telnet:127.0.0.1:9003,server,nowait \ + -bios $OVMF_PATH/OVMF_VARS.fd \ + -object tdx-guest,sept-ve-disable,id=tdx,quote-generation-service=vsock:2:4050 \ + -cpu host,-kvm-steal-time,pmu=off,tsc-freq=1000000000 \ + -machine q35,kernel_irqchip=split,confidential-guest-support=tdx \ + -device virtio-net-pci,netdev=mynet0,disable-legacy=on,disable-modern=off \ + -device virtio-keyboard-pci,disable-legacy=on,disable-modern=off \ + -device virtio-blk-pci,bus=pcie.0,addr=0x6,drive=x0,disable-legacy=on,disable-modern=off \ + -drive file=fs.img,if=none,format=raw,id=x0 \ + -netdev user,id=mynet0,hostfwd=tcp::10027-:22,hostfwd=tcp::54136-:8090 \ + -chardev stdio,id=mux,mux=on,logfile=$OSDK_CWD/$(date '+%Y-%m-%dT%H%M%S').log \ + -device virtio-serial,romfile= \ + -device virtconsole,chardev=mux \ + -monitor chardev:mux \ + -serial chardev:mux \ +""" \ No newline at end of file diff --git a/docs/src/osdk/reference/manifest.md b/docs/src/osdk/reference/manifest.md index c284b1c6..f06aba95 100644 --- a/docs/src/osdk/reference/manifest.md +++ b/docs/src/osdk/reference/manifest.md @@ -58,7 +58,7 @@ qemu_args = [ # <10> "-m 2G", ] -['cfg(arch="x86_64", schema=microvm)'.run] # <12> +['cfg(arch="x86_64", scheme=microvm)'.run] # <12> bootloader = "qemu" qemu_args = [ # <10> "-machine microvm,rtc=on", @@ -169,12 +169,12 @@ which is used to create a GRUB CD_ROM. Cfg is an advanced feature to create multiple profiles for the same actions under different scenarios. Currently we -have two configurable keys, which are `arch` and `schema`. +have two configurable keys, which are `arch` and `scheme`. The key `arch` has a fixed set of values which is aligned with the CLI `--arch` argument. If an action has no specified -arch, it matches all the architectures. The key `schema` allows -user-defined values and can be selected by the `--schema` CLI -argument. The key `schema` can be used to create special settings +arch, it matches all the architectures. The key `scheme` allows +user-defined values and can be selected by the `--scheme` CLI +argument. The key `scheme` can be used to create special settings (especially special QEMU configurations). If a cfg action is matched, unspecified and required arguments will be inherited from the action that has no cfg (i.e. the default action setting). diff --git a/osdk/Cargo.lock b/osdk/Cargo.lock index 16bcf7f7..b5f6ceea 100644 --- a/osdk/Cargo.lock +++ b/osdk/Cargo.lock @@ -135,6 +135,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "shlex", "syn", "toml", ] @@ -470,6 +471,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "strsim" version = "0.11.0" diff --git a/osdk/Cargo.toml b/osdk/Cargo.toml index 2cab2707..b9ffa388 100644 --- a/osdk/Cargo.toml +++ b/osdk/Cargo.toml @@ -19,6 +19,7 @@ quote = "1.0.35" serde = { version = "1.0.195", features = ["derive"] } serde_json = "1.0.111" sha2 = "0.10.8" +shlex = "1.3.0" syn = { version = "2.0.52", features = ["extra-traits", "full", "parsing", "printing"] } toml = { version = "0.8.8", features = ["preserve_order"] } diff --git a/osdk/src/arch.rs b/osdk/src/arch.rs index ac8a0bf3..bca87baa 100644 --- a/osdk/src/arch.rs +++ b/osdk/src/arch.rs @@ -12,8 +12,11 @@ use std::fmt::{self, Display, Formatter}; /// #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)] pub enum Arch { + #[serde(rename = "aarch64")] Aarch64, + #[serde(rename = "riscv64")] X86_64, + #[serde(rename = "x86_64")] RiscV64, } @@ -33,11 +36,19 @@ impl ValueEnum for Arch { impl Arch { /// Get the target triple for the architecture. - pub fn triple(&self) -> String { + pub fn triple(&self) -> &'static str { match self { - Arch::Aarch64 => "aarch64-unknown-none".to_owned(), - Arch::RiscV64 => "riscv64gc-unknown-none-elf".to_owned(), - Arch::X86_64 => "x86_64-unknown-none".to_owned(), + Arch::Aarch64 => "aarch64-unknown-none", + Arch::RiscV64 => "riscv64gc-unknown-none-elf", + Arch::X86_64 => "x86_64-unknown-none", + } + } + + pub fn system_qemu(&self) -> &'static str { + match self { + Arch::Aarch64 => "qemu-system-aarch64", + Arch::RiscV64 => "qemu-system-riscv64", + Arch::X86_64 => "qemu-system-x86_64", } } diff --git a/osdk/src/bundle/mod.rs b/osdk/src/bundle/mod.rs index cc0e9319..b4d60e13 100644 --- a/osdk/src/bundle/mod.rs +++ b/osdk/src/bundle/mod.rs @@ -7,7 +7,7 @@ pub mod vm_image; use bin::AsterBin; use file::{BundleFile, Initramfs}; use std::process; -use vm_image::AsterVmImage; +use vm_image::{AsterVmImage, AsterVmImageType}; use std::{ path::{Path, PathBuf}, @@ -16,11 +16,9 @@ use std::{ }; use crate::{ - arch::Arch, - cli::CargoArgs, - config_manager::{ - action::{ActionSettings, Bootloader}, - RunConfig, + config::{ + scheme::{ActionChoice, BootMethod}, + Config, }, error::Errno, error_msg, @@ -42,16 +40,20 @@ pub struct BundleManifest { pub initramfs: Option, pub aster_bin: Option, pub vm_image: Option, - pub settings: ActionSettings, - pub cargo_args: CargoArgs, + pub config: Config, + pub action: ActionChoice, pub last_modified: SystemTime, } impl Bundle { /// This function creates a new `Bundle` without adding any files. - pub fn new(path: impl AsRef, settings: ActionSettings, cargo_args: CargoArgs) -> Self { + pub fn new(path: impl AsRef, config: &Config, action: ActionChoice) -> Self { std::fs::create_dir_all(path.as_ref()).unwrap(); - let initramfs = if let Some(ref initramfs) = settings.initramfs { + let config_initramfs = match action { + ActionChoice::Run => config.run.boot.initramfs.as_ref(), + ActionChoice::Test => config.test.boot.initramfs.as_ref(), + }; + let initramfs = if let Some(ref initramfs) = config_initramfs { if !initramfs.exists() { error_msg!("initramfs file not found: {}", initramfs.display()); process::exit(Errno::BuildCrate as _); @@ -65,8 +67,8 @@ impl Bundle { initramfs, aster_bin: None, vm_image: None, - settings, - cargo_args, + config: config.clone(), + action, last_modified: SystemTime::now(), }, path: path.as_ref().to_path_buf(), @@ -109,105 +111,139 @@ impl Bundle { }) } - pub fn can_run_with_config(&self, config: &RunConfig) -> bool { - // Compare the manifest with the run configuration. - // TODO: This pairwise comparison will result in some false negatives. We may - // fix it by pondering upon each fields with more care. - if self.manifest.settings != config.settings - || self.manifest.cargo_args != config.cargo_args + pub fn can_run_with_config(&self, config: &Config, action: ActionChoice) -> Result<(), String> { + // If built for testing, better not to run it. Vice versa. + if self.manifest.action != action { + return Err(format!( + "The bundle is built for {:?}", + self.manifest.action + )); + } + + let self_action = match self.manifest.action { + ActionChoice::Run => &self.manifest.config.run, + ActionChoice::Test => &self.manifest.config.test, + }; + let config_action = match action { + ActionChoice::Run => &config.run, + ActionChoice::Test => &config.test, + }; + + // Compare the manifest with the run configuration except the initramfs and the boot method. + if self_action.grub != config_action.grub + || self_action.qemu != config_action.qemu + || self_action.build != config_action.build + || self_action.boot.kcmdline != config_action.boot.kcmdline { - return false; + return Err("The bundle is not compatible with the run configuration".to_owned()); + } + + // Checkout if the files on disk supports the boot method + match config_action.boot.method { + BootMethod::QemuDirect => { + if self.manifest.aster_bin.is_none() { + return Err("Kernel binary is required for direct QEMU booting".to_owned()); + }; + } + BootMethod::GrubRescueIso => { + let Some(ref vm_image) = self.manifest.vm_image else { + return Err("VM image is required for QEMU booting".to_owned()); + }; + if !matches!(vm_image.typ(), AsterVmImageType::GrubIso(_)) { + return Err("VM image in the bundle is not a Grub ISO image".to_owned()); + } + } + BootMethod::GrubQcow2 => { + let Some(ref vm_image) = self.manifest.vm_image else { + return Err("VM image is required for QEMU booting".to_owned()); + }; + if !matches!(vm_image.typ(), AsterVmImageType::Qcow2(_)) { + return Err("VM image in the bundle is not a Qcow2 image".to_owned()); + } + } } // Compare the initramfs. - match (&self.manifest.initramfs, &config.settings.initramfs) { + let initramfs_err = + "The initramfs in the bundle is different from the one in the run configuration" + .to_owned(); + match (&self.manifest.initramfs, &config_action.boot.initramfs) { (Some(initramfs), Some(initramfs_path)) => { let config_initramfs = Initramfs::new(initramfs_path); if initramfs.sha256sum() != config_initramfs.sha256sum() { - return false; + return Err(initramfs_err); } } (None, None) => {} _ => { - return false; + return Err(initramfs_err); } }; - true + Ok(()) } pub fn last_modified_time(&self) -> SystemTime { self.manifest.last_modified } - pub fn run(&self, config: &RunConfig) { - if !self.can_run_with_config(config) { - error_msg!("The bundle is not compatible with the run configuration"); - std::process::exit(Errno::RunBundle as _); - } - let mut qemu_cmd = Command::new(config.settings.qemu_exe.clone().unwrap_or_else(|| { - PathBuf::from(match config.arch { - Arch::Aarch64 => "qemu-system-aarch64", - Arch::RiscV64 => "qemu-system-riscv64", - Arch::X86_64 => "qemu-system-x86_64", - }) - })); - // FIXME: Arguments like "-m 2G" sould be separated into "-m" and "2G". This - // is a dirty hack to make it work. Anything like space in the paths will - // break this. - for arg in &config.settings.qemu_args { - for part in arg.split_whitespace() { - qemu_cmd.arg(part); + pub fn run(&self, config: &Config, action: ActionChoice) { + match self.can_run_with_config(config, action) { + Ok(()) => {} + Err(msg) => { + error_msg!("{}", msg); + std::process::exit(Errno::RunBundle as _); } } - match config.settings.bootloader { - Some(Bootloader::Qemu) => { - let Some(ref aster_bin) = self.manifest.aster_bin else { - error_msg!("Kernel ELF binary is required for direct QEMU booting"); - std::process::exit(Errno::RunBundle as _); - }; + let action = match action { + ActionChoice::Run => &config.run, + ActionChoice::Test => &config.test, + }; + let mut qemu_cmd = Command::new(&action.qemu.path); + match shlex::split(&action.qemu.args) { + Some(v) => { + for arg in v { + qemu_cmd.arg(arg); + } + } + None => { + error_msg!("Failed to parse qemu args: {:#?}", &action.qemu.args); + process::exit(Errno::ParseMetadata as _); + } + } + match action.boot.method { + BootMethod::QemuDirect => { + let aster_bin = self.manifest.aster_bin.as_ref().unwrap(); qemu_cmd .arg("-kernel") .arg(self.path.join(aster_bin.path())); - if let Some(ref initramfs) = config.settings.initramfs { + if let Some(ref initramfs) = action.boot.initramfs { qemu_cmd.arg("-initrd").arg(initramfs); } else { info!("No initramfs specified"); }; - qemu_cmd - .arg("-append") - .arg(config.settings.combined_kcmd_args().join(" ")); + qemu_cmd.arg("-append").arg(action.boot.kcmdline.join(" ")); } - Some(Bootloader::Grub) => { - let Some(ref vm_image) = self.manifest.vm_image else { - error_msg!("VM image is required for QEMU booting"); - std::process::exit(Errno::RunBundle as _); - }; + BootMethod::GrubRescueIso => { + let vm_image = self.manifest.vm_image.as_ref().unwrap(); + assert!(matches!(vm_image.typ(), AsterVmImageType::GrubIso(_))); qemu_cmd.arg("-cdrom").arg(self.path.join(vm_image.path())); - if let Some(ovmf) = &config.settings.ovmf { - qemu_cmd.arg("-drive").arg(format!( - "if=pflash,format=raw,unit=0,readonly=on,file={}", - ovmf.join("OVMF_CODE.fd").display() - )); - qemu_cmd.arg("-drive").arg(format!( - "if=pflash,format=raw,unit=1,file={}", - ovmf.join("OVMF_VARS.fd").display() - )); - } } - None => { - error_msg!("Bootloader is required for QEMU booting"); - std::process::exit(Errno::RunBundle as _); + BootMethod::GrubQcow2 => { + let vm_image = self.manifest.vm_image.as_ref().unwrap(); + assert!(matches!(vm_image.typ(), AsterVmImageType::Qcow2(_))); + qemu_cmd.arg("-drive").arg(format!( + "file={},index=0,media=disk,format=qcow2", + self.path + .join(vm_image.path()) + .into_os_string() + .into_string() + .unwrap() + )); } }; - for drive_file in &config.settings.drive_files { - qemu_cmd.arg("-drive").arg(format!( - "file={},{}", - drive_file.path.display(), - drive_file.append, - )); - } + info!("Running QEMU: {:#?}", qemu_cmd); let exit_status = qemu_cmd.status().unwrap(); if !exit_status.success() { diff --git a/osdk/src/bundle/vm_image.rs b/osdk/src/bundle/vm_image.rs index c6afbc97..1c052c78 100644 --- a/osdk/src/bundle/vm_image.rs +++ b/osdk/src/bundle/vm_image.rs @@ -18,7 +18,7 @@ pub struct AsterVmImage { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum AsterVmImageType { GrubIso(AsterGrubIsoImageMeta), - // TODO: add more vm image types such as qcow2, etc. + Qcow2(AsterQcow2ImageMeta), } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -26,6 +26,11 @@ pub struct AsterGrubIsoImageMeta { pub grub_version: String, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AsterQcow2ImageMeta { + pub grub_version: String, +} + impl BundleFile for AsterVmImage { fn path(&self) -> &PathBuf { &self.path @@ -50,6 +55,10 @@ impl AsterVmImage { } } + pub fn typ(&self) -> &AsterVmImageType { + &self.typ + } + /// Move the binary to the `base` directory and convert the path to a relative path. pub fn move_to(self, base: impl AsRef) -> Self { let file_name = self.path.file_name().unwrap(); diff --git a/osdk/src/cli.rs b/osdk/src/cli.rs index 4d167789..fc255efc 100644 --- a/osdk/src/cli.rs +++ b/osdk/src/cli.rs @@ -10,37 +10,40 @@ use crate::{ execute_build_command, execute_debug_command, execute_forwarded_command, execute_new_command, execute_run_command, execute_test_command, }, - config_manager::{ - action::{BootProtocol, Bootloader}, - manifest::ProjectType, - BuildConfig, DebugConfig, RunConfig, TestConfig, + config::{ + manifest::{ProjectType, TomlManifest}, + scheme::{BootMethod, BootProtocol}, + Config, }, }; pub fn main() { - let osdk_subcommand = match Cli::parse() { + let (osdk_subcommand, common_args) = match Cli::parse() { Cli { cargo_subcommand: CargoSubcommand::Osdk(osdk_subcommand), - } => osdk_subcommand, + common_args, + } => (osdk_subcommand, common_args), + }; + + let load_config = || { + let manifest = TomlManifest::load(&common_args.build_args.features); + let scheme = manifest.get_scheme(common_args.scheme.as_ref()); + Config::new(scheme, &common_args) }; match &osdk_subcommand { OsdkSubcommand::New(args) => execute_new_command(args), OsdkSubcommand::Build(build_args) => { - let build_config = BuildConfig::parse(build_args); - execute_build_command(&build_config); + execute_build_command(&load_config(), build_args); } OsdkSubcommand::Run(run_args) => { - let run_config = RunConfig::parse(run_args); - execute_run_command(&run_config); + execute_run_command(&load_config(), &run_args.gdb_server_args); } OsdkSubcommand::Debug(debug_args) => { - let debug_config = DebugConfig::parse(debug_args); - execute_debug_command(&debug_config); + execute_debug_command(&load_config().run.build.profile, debug_args); } OsdkSubcommand::Test(test_args) => { - let test_config = TestConfig::parse(test_args); - execute_test_command(&test_config); + execute_test_command(&load_config(), test_args); } OsdkSubcommand::Check(args) => execute_forwarded_command("check", &args.args), OsdkSubcommand::Clippy(args) => execute_forwarded_command("clippy", &args.args), @@ -54,6 +57,8 @@ pub fn main() { pub struct Cli { #[clap(subcommand)] cargo_subcommand: CargoSubcommand, + #[command(flatten)] + common_args: CommonArgs, } #[derive(Debug, Parser)] @@ -136,18 +141,23 @@ impl NewArgs { #[derive(Debug, Parser)] pub struct BuildArgs { - #[command(flatten)] - pub cargo_args: CargoArgs, - #[command(flatten)] - pub osdk_args: OsdkArgs, + #[arg( + long = "for-test", + help = "Build for running unit tests", + default_value_t + )] + pub for_test: bool, + #[arg( + long = "output", + short = 'o', + help = "Output directory for all generated artifacts", + value_name = "DIR" + )] + pub output: Option, } #[derive(Debug, Parser)] pub struct RunArgs { - #[command(flatten)] - pub cargo_args: CargoArgs, - #[command(flatten)] - pub osdk_args: OsdkArgs, #[command(flatten)] pub gdb_server_args: GdbServerArgs, } @@ -181,10 +191,6 @@ pub struct GdbServerArgs { #[derive(Debug, Parser)] pub struct DebugArgs { - #[command(flatten)] - pub cargo_args: CargoArgs, - #[command(flatten)] - pub osdk_args: OsdkArgs, #[arg( long, help = "Specify the address of the remote target", @@ -195,15 +201,11 @@ pub struct DebugArgs { #[derive(Debug, Parser)] pub struct TestArgs { - #[command(flatten)] - pub cargo_args: CargoArgs, #[arg( name = "TESTNAME", help = "Only run tests containing this string in their names" )] pub test_name: Option, - #[command(flatten)] - pub osdk_args: OsdkArgs, } #[derive(Debug, Args, Default, Clone, Eq, PartialEq, Serialize, Deserialize)] @@ -211,79 +213,120 @@ pub struct CargoArgs { #[arg( long, help = "The Cargo build profile (built-in candidates are 'dev', 'release', 'test' and 'bench')", - default_value = "dev", - conflicts_with = "release" + conflicts_with = "release", + global = true )] - pub profile: String, + pub profile: Option, #[arg( long, help = "Build artifacts in release mode", - conflicts_with = "profile" + conflicts_with = "profile", + global = true )] pub release: bool, - #[arg(long, value_name = "FEATURES", help = "List of features to activate", value_delimiter = ',', num_args = 1..)] + #[arg( + long, + value_name = "FEATURES", + help = "List of features to activate", + value_delimiter = ',', + num_args = 1.., + global = true, + )] pub features: Vec, } +impl CargoArgs { + pub fn profile(&self) -> Option { + if self.release { + Some("release".to_owned()) + } else { + self.profile.clone() + } + } +} + #[derive(Debug, Args)] -pub struct OsdkArgs { - #[arg(long, value_name = "ARCH", help = "The architecture to build for")] - pub arch: Option, +pub struct CommonArgs { + #[command(flatten)] + pub build_args: CargoArgs, #[arg( - long = "schema", - help = "Select the specific configuration schema provided in the OSDK manifest", - value_name = "SCHEMA" + long = "linux-x86-legacy-boot", + help = "Enable legacy 32-bit boot support for the Linux x86 boot protocol", + global = true )] - pub schema: Option, + pub linux_x86_legacy_boot: bool, #[arg( - long = "kcmd_args+", + long = "target-arch", + value_name = "ARCH", + help = "The architecture to build for", + global = true + )] + pub target_arch: Option, + #[arg( + long = "scheme", + help = "Select the specific configuration scheme provided in the OSDK manifest", + value_name = "SCHEME", + global = true + )] + pub scheme: Option, + #[arg( + long = "kcmd-args", require_equals = true, help = "Extra or overriding command line arguments for guest kernel", - value_name = "ARGS" + value_name = "ARGS", + global = true )] pub kcmd_args: Vec, #[arg( - long = "init_args+", + long = "init-args", require_equals = true, help = "Extra command line arguments for init process", - value_name = "ARGS" + value_name = "ARGS", + global = true )] pub init_args: Vec, - #[arg(long, help = "Path of initramfs", value_name = "PATH")] + #[arg(long, help = "Path of initramfs", value_name = "PATH", global = true)] pub initramfs: Option, - #[arg(long = "ovmf", help = "Path of OVMF", value_name = "PATH")] - pub ovmf: Option, - #[arg(long = "opensbi", help = "Path of OpenSBI", value_name = "PATH")] - pub opensbi: Option, #[arg( - long = "bootloader", + long = "boot-method", help = "Loader for booting the kernel", - value_name = "BOOTLOADER" + value_name = "BOOTMETHOD", + global = true )] - pub bootloader: Option, + pub boot_method: Option, + #[arg( + long = "display-grub-menu", + help = "Display the GRUB menu if booting with GRUB", + global = true + )] + pub display_grub_menu: bool, #[arg( long = "grub-mkrescue", help = "Path of grub-mkrescue", - value_name = "PATH" + value_name = "PATH", + global = true )] pub grub_mkrescue: Option, #[arg( - long = "boot_protocol", + long = "grub-boot-protocol", help = "Protocol for booting the kernel", - value_name = "BOOT_PROTOCOL" + value_name = "BOOT_PROTOCOL", + global = true )] - pub boot_protocol: Option, + pub grub_boot_protocol: Option, #[arg( - long = "qemu_exe", + long = "qemu-exe", help = "The QEMU executable file", - value_name = "FILE" + value_name = "FILE", + global = true )] pub qemu_exe: Option, #[arg( - long = "qemu_args+", + long = "qemu-args", require_equals = true, help = "Extra arguments or overriding arguments for running QEMU", - value_name = "ARGS" + value_name = "ARGS", + global = true )] - pub qemu_args_add: Vec, + pub qemu_args: Vec, } diff --git a/osdk/src/commands/build/bin.rs b/osdk/src/commands/build/bin.rs index f531aca2..6ac7ae7f 100644 --- a/osdk/src/commands/build/bin.rs +++ b/osdk/src/commands/build/bin.rs @@ -14,7 +14,6 @@ use crate::{ bin::{AsterBin, AsterBinType, AsterBzImageMeta, AsterElfMeta}, file::BundleFile, }, - config_manager::action::BootProtocol, util::get_current_crate_info, }; @@ -22,13 +21,13 @@ pub fn make_install_bzimage( install_dir: impl AsRef, target_dir: impl AsRef, aster_elf: &AsterBin, - protocol: &BootProtocol, + linux_x86_legacy_boot: bool, ) -> AsterBin { let target_name = get_current_crate_info().name; - let image_type = match protocol { - BootProtocol::LinuxLegacy32 => BzImageType::Legacy32, - BootProtocol::LinuxEfiHandover64 => BzImageType::Efi64, - _ => unreachable!(), + let image_type = if linux_x86_legacy_boot { + BzImageType::Legacy32 + } else { + BzImageType::Efi64 }; let setup_bin = { let setup_install_dir = target_dir.as_ref(); @@ -60,9 +59,9 @@ pub fn make_install_bzimage( AsterBin::new( &install_path, AsterBinType::BzImage(AsterBzImageMeta { - support_legacy32_boot: matches!(protocol, BootProtocol::LinuxLegacy32), + support_legacy32_boot: linux_x86_legacy_boot, support_efi_boot: false, - support_efi_handover: matches!(protocol, BootProtocol::LinuxEfiHandover64), + support_efi_handover: !linux_x86_legacy_boot, }), aster_elf.version().clone(), aster_elf.stripped(), diff --git a/osdk/src/commands/build/grub.rs b/osdk/src/commands/build/grub.rs index 6187f7e7..fd959449 100644 --- a/osdk/src/commands/build/grub.rs +++ b/osdk/src/commands/build/grub.rs @@ -12,7 +12,10 @@ use crate::{ file::BundleFile, vm_image::{AsterGrubIsoImageMeta, AsterVmImage, AsterVmImageType}, }, - config_manager::{action::BootProtocol, BuildConfig}, + config::{ + scheme::{ActionChoice, BootProtocol}, + Config, + }, util::get_current_crate_info, }; @@ -20,11 +23,16 @@ pub fn create_bootdev_image( target_dir: impl AsRef, aster_bin: &AsterBin, initramfs_path: Option>, - config: &BuildConfig, + config: &Config, + action: ActionChoice, ) -> AsterVmImage { let target_name = get_current_crate_info().name; let iso_root = &target_dir.as_ref().join("iso_root"); - let protocol = &config.settings.boot_protocol; + let action = match &action { + ActionChoice::Run => &config.run, + ActionChoice::Test => &config.test, + }; + let protocol = &action.grub.boot_protocol; // Clear or make the iso dir. if iso_root.exists() { @@ -43,12 +51,12 @@ pub fn create_bootdev_image( // Make the kernel image and place it in the boot directory. match protocol { - Some(BootProtocol::LinuxLegacy32) | Some(BootProtocol::LinuxEfiHandover64) => { + BootProtocol::Linux => { make_install_bzimage( iso_root.join("boot"), &target_dir, aster_bin, - &protocol.clone().unwrap(), + action.build.linux_x86_legacy_boot, ); } _ => { @@ -65,22 +73,17 @@ pub fn create_bootdev_image( None }; let grub_cfg = generate_grub_cfg( - &config.settings.combined_kcmd_args().join(" "), - true, + &action.boot.kcmdline.join(" "), + !action.grub.display_grub_menu, initramfs_in_image, - &protocol.clone().unwrap_or(BootProtocol::Multiboot2), + protocol, ); let grub_cfg_path = iso_root.join("boot").join("grub").join("grub.cfg"); fs::write(grub_cfg_path, grub_cfg).unwrap(); // Make the boot device CDROM image using `grub-mkrescue`. let iso_path = &target_dir.as_ref().join(target_name.to_string() + ".iso"); - let grub_mkrescue_bin = &config - .settings - .grub_mkrescue - .clone() - .unwrap_or_else(|| PathBuf::from("grub-mkrescue")); - let mut grub_mkrescue_cmd = std::process::Command::new(grub_mkrescue_bin.as_os_str()); + let mut grub_mkrescue_cmd = std::process::Command::new(action.grub.grub_mkrescue.as_os_str()); grub_mkrescue_cmd .arg(iso_root.as_os_str()) .arg("-o") @@ -92,7 +95,7 @@ pub fn create_bootdev_image( AsterVmImage::new( iso_path, AsterVmImageType::GrubIso(AsterGrubIsoImageMeta { - grub_version: get_grub_mkrescue_version(grub_mkrescue_bin), + grub_version: get_grub_mkrescue_version(&action.grub.grub_mkrescue), }), aster_bin.version().clone(), ) @@ -115,7 +118,7 @@ fn generate_grub_cfg( "#GRUB_TIMEOUT_STYLE#", if skip_grub_menu { "hidden" } else { "menu" }, ) - .replace("#GRUB_TIMEOUT#", if skip_grub_menu { "0" } else { "1" }); + .replace("#GRUB_TIMEOUT#", if skip_grub_menu { "0" } else { "5" }); // Replace all occurrences of "#KERNEL_COMMAND_LINE#" with the desired value. let grub_cfg = grub_cfg.replace("#KERNEL_COMMAND_LINE#", kcmdline); // Replace the grub commands according to the protocol selected. @@ -147,7 +150,7 @@ fn generate_grub_cfg( "".to_owned() }, ), - BootProtocol::LinuxLegacy32 | BootProtocol::LinuxEfiHandover64 => grub_cfg + BootProtocol::Linux => grub_cfg .replace("#GRUB_CMD_KERNEL#", "linux") .replace("#KERNEL#", &aster_bin_path_on_device) .replace( diff --git a/osdk/src/commands/build/mod.rs b/osdk/src/commands/build/mod.rs index 250c2c2b..f6feb707 100644 --- a/osdk/src/commands/build/mod.rs +++ b/osdk/src/commands/build/mod.rs @@ -3,7 +3,12 @@ mod bin; mod grub; -use std::{ffi::OsString, path::Path, process}; +use std::{ + ffi::OsString, + path::{Path, PathBuf}, + process, + time::{Duration, SystemTime}, +}; use bin::strip_elf_for_qemu; @@ -15,39 +20,53 @@ use crate::{ bin::{AsterBin, AsterBinType, AsterElfMeta}, Bundle, }, - cli::CargoArgs, - config_manager::{action::Bootloader, BuildConfig}, + cli::BuildArgs, + config::{ + scheme::{ActionChoice, BootMethod}, + Config, + }, error::Errno, error_msg, - util::{get_current_crate_info, get_target_directory}, + util::{get_cargo_metadata, get_current_crate_info, get_target_directory}, }; -pub fn execute_build_command(config: &BuildConfig) { - let ws_target_directory = get_target_directory(); - let osdk_target_directory = ws_target_directory.join(DEFAULT_TARGET_RELPATH); - if !osdk_target_directory.exists() { - std::fs::create_dir_all(&osdk_target_directory).unwrap(); +pub fn execute_build_command(config: &Config, build_args: &BuildArgs) { + let cargo_target_directory = get_target_directory(); + let osdk_output_directory = build_args + .output + .clone() + .unwrap_or(cargo_target_directory.join(DEFAULT_TARGET_RELPATH)); + if !osdk_output_directory.exists() { + std::fs::create_dir_all(&osdk_output_directory).unwrap(); } let target_info = get_current_crate_info(); - let bundle_path = osdk_target_directory.join(target_info.name); + let bundle_path = osdk_output_directory.join(target_info.name); - let _bundle = create_base_and_build( + let action = if build_args.for_test { + ActionChoice::Test + } else { + ActionChoice::Run + }; + + let _bundle = create_base_and_cached_build( bundle_path, - &osdk_target_directory, - &ws_target_directory, + &osdk_output_directory, + &cargo_target_directory, config, + action, &[], ); } -pub fn create_base_and_build( +pub fn create_base_and_cached_build( bundle_path: impl AsRef, - osdk_target_directory: impl AsRef, + osdk_output_directory: impl AsRef, cargo_target_directory: impl AsRef, - config: &BuildConfig, + config: &Config, + action: ActionChoice, rustflags: &[&str], ) -> Bundle { - let base_crate_path = osdk_target_directory.as_ref().join("base"); + let base_crate_path = osdk_output_directory.as_ref().join("base"); new_base_crate( &base_crate_path, &get_current_crate_info().name, @@ -55,55 +74,108 @@ pub fn create_base_and_build( ); let original_dir = std::env::current_dir().unwrap(); std::env::set_current_dir(&base_crate_path).unwrap(); - let bundle = do_build( + let bundle = do_cached_build( &bundle_path, - &osdk_target_directory, + &osdk_output_directory, &cargo_target_directory, config, + action, rustflags, ); std::env::set_current_dir(original_dir).unwrap(); bundle } +/// If the source is not since modified and the last build is recent, we can reuse the existing bundle. +pub fn do_cached_build( + bundle_path: impl AsRef, + osdk_output_directory: impl AsRef, + cargo_target_directory: impl AsRef, + config: &Config, + action: ActionChoice, + rustflags: &[&str], +) -> Bundle { + let build_a_new_one = || { + do_build( + &bundle_path, + &osdk_output_directory, + &cargo_target_directory, + config, + action, + rustflags, + ) + }; + + let existing_bundle = Bundle::load(&bundle_path); + let Some(existing_bundle) = existing_bundle else { + return build_a_new_one(); + }; + if existing_bundle.can_run_with_config(config, action).is_err() { + return build_a_new_one(); + } + let Ok(built_since) = SystemTime::now().duration_since(existing_bundle.last_modified_time()) + else { + return build_a_new_one(); + }; + if built_since > Duration::from_secs(600) { + return build_a_new_one(); + } + let workspace_root = { + let meta = get_cargo_metadata(None::<&str>, None::<&[&str]>).unwrap(); + PathBuf::from(meta.get("workspace_root").unwrap().as_str().unwrap()) + }; + if get_last_modified_time(workspace_root) < existing_bundle.last_modified_time() { + return existing_bundle; + } + build_a_new_one() +} + pub fn do_build( bundle_path: impl AsRef, - osdk_target_directory: impl AsRef, + osdk_output_directory: impl AsRef, cargo_target_directory: impl AsRef, - config: &BuildConfig, + config: &Config, + action: ActionChoice, rustflags: &[&str], ) -> Bundle { if bundle_path.as_ref().exists() { std::fs::remove_dir_all(&bundle_path).unwrap(); } - let mut bundle = Bundle::new( - &bundle_path, - config.settings.clone(), - config.cargo_args.clone(), - ); + let mut bundle = Bundle::new(&bundle_path, config, action); info!("Building kernel ELF"); let aster_elf = build_kernel_elf( - &config.arch, - &config.cargo_args, + &config.target_arch, + &config.build.profile, + &config.build.features[..], &cargo_target_directory, rustflags, ); - if matches!(config.settings.bootloader, Some(Bootloader::Qemu)) { - let stripped_elf = strip_elf_for_qemu(&osdk_target_directory, &aster_elf); - bundle.consume_aster_bin(stripped_elf); - } + let boot = match action { + ActionChoice::Run => &config.run.boot, + ActionChoice::Test => &config.test.boot, + }; - if matches!(config.settings.bootloader, Some(Bootloader::Grub)) { - info!("Building boot device image"); - let bootdev_image = grub::create_bootdev_image( - &osdk_target_directory, - &aster_elf, - config.settings.initramfs.as_ref(), - config, - ); - bundle.consume_vm_image(bootdev_image); + match boot.method { + BootMethod::GrubRescueIso => { + info!("Building boot device image"); + let bootdev_image = grub::create_bootdev_image( + &osdk_output_directory, + &aster_elf, + boot.initramfs.as_ref(), + config, + action, + ); + bundle.consume_vm_image(bootdev_image); + } + BootMethod::QemuDirect => { + let stripped_elf = strip_elf_for_qemu(&osdk_output_directory, &aster_elf); + bundle.consume_aster_bin(stripped_elf); + } + BootMethod::GrubQcow2 => { + todo!() + } } bundle @@ -111,7 +183,8 @@ pub fn do_build( fn build_kernel_elf( arch: &Arch, - cargo_args: &CargoArgs, + profile: &str, + features: &[String], cargo_target_directory: impl AsRef, rustflags: &[&str], ) -> AsterBin { @@ -135,12 +208,13 @@ fn build_kernel_elf( command.env_remove("RUSTUP_TOOLCHAIN"); command.env("RUSTFLAGS", rustflags.join(" ")); command.arg("build"); + command.arg("--features").arg(features.join(" ")); command.arg("--target").arg(&target_os_string); command .arg("--target-dir") .arg(cargo_target_directory.as_ref()); command.args(COMMON_CARGO_ARGS); - command.arg("--profile=".to_string() + &cargo_args.profile); + command.arg("--profile=".to_string() + profile); let status = command.status().unwrap(); if !status.success() { error_msg!("Cargo build failed"); @@ -148,10 +222,10 @@ fn build_kernel_elf( } let aster_bin_path = cargo_target_directory.as_ref().join(&target_os_string); - let aster_bin_path = if cargo_args.profile == "dev" { + let aster_bin_path = if profile == "dev" { aster_bin_path.join("debug") } else { - aster_bin_path.join(&cargo_args.profile) + aster_bin_path.join(profile) } .join(get_current_crate_info().name); @@ -167,3 +241,21 @@ fn build_kernel_elf( false, ) } + +fn get_last_modified_time(path: impl AsRef) -> SystemTime { + let mut last_modified = SystemTime::UNIX_EPOCH; + for entry in std::fs::read_dir(path).unwrap() { + let entry = entry.unwrap(); + if entry.file_name() == "target" { + continue; + } + + let metadata = entry.metadata().unwrap(); + if metadata.is_dir() { + last_modified = std::cmp::max(last_modified, get_last_modified_time(&entry.path())); + } else { + last_modified = std::cmp::max(last_modified, metadata.modified().unwrap()); + } + } + last_modified +} diff --git a/osdk/src/commands/debug.rs b/osdk/src/commands/debug.rs index 5f646513..6268f952 100644 --- a/osdk/src/commands/debug.rs +++ b/osdk/src/commands/debug.rs @@ -1,15 +1,13 @@ // SPDX-License-Identifier: MPL-2.0 -use crate::commands::util::{bin_file_name, profile_adapter}; -use crate::config_manager::DebugConfig; +use crate::commands::util::bin_file_name; -use crate::util::get_target_directory; +use crate::{cli::DebugArgs, util::get_target_directory}; use std::process::Command; -pub fn execute_debug_command(config: &DebugConfig) { - let DebugConfig { cargo_args, remote } = config; +pub fn execute_debug_command(profile: &String, args: &DebugArgs) { + let remote = &args.remote; - let profile = profile_adapter(&cargo_args.profile); let file_path = get_target_directory() .join("x86_64-unknown-none") .join(profile) diff --git a/osdk/src/commands/mod.rs b/osdk/src/commands/mod.rs index 4d01b362..2011bb57 100644 --- a/osdk/src/commands/mod.rs +++ b/osdk/src/commands/mod.rs @@ -19,12 +19,11 @@ use crate::arch::get_default_arch; /// Execute the forwarded cargo command with args containing the subcommand and its arguments. pub fn execute_forwarded_command(subcommand: &str, args: &Vec) -> ! { let mut cargo = util::cargo(); - cargo - .arg(subcommand) - .args(util::COMMON_CARGO_ARGS) - .arg("--target") - .arg(get_default_arch().triple()) - .args(args); + cargo.arg(subcommand).args(util::COMMON_CARGO_ARGS); + if !args.contains(&"--target".to_owned()) { + cargo.arg("--target").arg(get_default_arch().triple()); + } + cargo.args(args); let status = cargo.status().expect("Failed to execute cargo"); std::process::exit(status.code().unwrap_or(1)); } diff --git a/osdk/src/commands/new/kernel.OSDK.toml.template b/osdk/src/commands/new/kernel.OSDK.toml.template index b5f711d8..d6fceb22 100644 --- a/osdk/src/commands/new/kernel.OSDK.toml.template +++ b/osdk/src/commands/new/kernel.OSDK.toml.template @@ -1,18 +1,25 @@ -[project] -type = "kernel" +project_type = "kernel" -[run] -bootloader = "grub" -ovmf = "/usr/share/OVMF" -qemu_args = [ - "-machine q35,kernel-irqchip=split", - "-cpu Icelake-Server,+x2apic", - "--no-reboot", - "-m 2G", - "-nographic", - "-serial chardev:mux", - "-monitor chardev:mux", - "-chardev stdio,id=mux,mux=on,signal=off", - "-display none", - "-device isa-debug-exit,iobase=0xf4,iosize=0x04", +vars = [ + ["OVMF_PATH", "/usr/share/OVMF"], ] + +[boot] +method = "grub-rescue-iso" + +[qemu] +args = """\ + -machine q35,kernel-irqchip=split \ + -cpu Icelake-Server,+x2apic \ + --no-reboot \ + -m 2G \ + -smp 1 \ + -nographic \ + -serial chardev:mux \ + -monitor chardev:mux \ + -chardev stdio,id=mux,mux=on,signal=off \ + -display none \ + -device isa-debug-exit,iobase=0xf4,iosize=0x04 \ + -drive if=pflash,format=raw,unit=0,readonly=on,file=$OVMF_PATH/OVMF_CODE.fd \ + -drive if=pflash,format=raw,unit=1,file=$OVMF_PATH/OVMF_VARS.fd \ +""" \ No newline at end of file diff --git a/osdk/src/commands/new/lib.OSDK.toml.template b/osdk/src/commands/new/lib.OSDK.toml.template index d3549d8c..642a5439 100644 --- a/osdk/src/commands/new/lib.OSDK.toml.template +++ b/osdk/src/commands/new/lib.OSDK.toml.template @@ -1,17 +1,19 @@ -[project] -type = "library" +project_type = "lib" -[test] -bootloader = "qemu" -qemu_args = [ - "-machine q35,kernel-irqchip=split", - "-cpu Icelake-Server,+x2apic", - "--no-reboot", - "-m 2G", - "-nographic", - "-serial chardev:mux", - "-monitor chardev:mux", - "-chardev stdio,id=mux,mux=on,signal=off", - "-display none", - "-device isa-debug-exit,iobase=0xf4,iosize=0x04", -] +[boot] +method = "qemu-direct" + +[qemu] +args = """\ + -machine q35,kernel-irqchip=split \ + -cpu Icelake-Server,+x2apic \ + --no-reboot \ + -m 2G \ + -smp 1 \ + -nographic \ + -serial chardev:mux \ + -monitor chardev:mux \ + -chardev stdio,id=mux,mux=on,signal=off \ + -display none \ + -device isa-debug-exit,iobase=0xf4,iosize=0x04 \ +""" \ No newline at end of file diff --git a/osdk/src/commands/new/mod.rs b/osdk/src/commands/new/mod.rs index e0f88757..3fa84487 100644 --- a/osdk/src/commands/new/mod.rs +++ b/osdk/src/commands/new/mod.rs @@ -4,7 +4,7 @@ use std::{fs, path::PathBuf, process, str::FromStr}; use crate::{ cli::NewArgs, - config_manager::manifest::ProjectType, + config::manifest::ProjectType, error::Errno, error_msg, util::{aster_crate_dep, cargo_new_lib, get_cargo_metadata}, diff --git a/osdk/src/commands/run.rs b/osdk/src/commands/run.rs index c5f74760..fe7f13ed 100644 --- a/osdk/src/commands/run.rs +++ b/osdk/src/commands/run.rs @@ -1,19 +1,14 @@ // SPDX-License-Identifier: MPL-2.0 -use std::{ - path::{Path, PathBuf}, - time::{Duration, SystemTime}, -}; - -use super::{build::create_base_and_build, util::DEFAULT_TARGET_RELPATH}; +use super::{build::create_base_and_cached_build, util::DEFAULT_TARGET_RELPATH}; use crate::{ - bundle::Bundle, - config_manager::{BuildConfig, RunConfig}, - util::{get_cargo_metadata, get_current_crate_info, get_target_directory}, + cli::GdbServerArgs, + config::{scheme::ActionChoice, Config}, + util::{get_current_crate_info, get_target_directory}, }; -pub fn execute_run_command(config: &RunConfig) { - if config.gdb_server_args.is_gdb_enabled { +pub fn execute_run_command(config: &Config, gdb_server_args: &GdbServerArgs) { + if gdb_server_args.is_gdb_enabled { use std::env; env::set_var( "RUSTFLAGS", @@ -21,111 +16,48 @@ pub fn execute_run_command(config: &RunConfig) { ); } - let ws_target_directory = get_target_directory(); - let osdk_target_directory = ws_target_directory.join(DEFAULT_TARGET_RELPATH); + let cargo_target_directory = get_target_directory(); + let osdk_output_directory = cargo_target_directory.join(DEFAULT_TARGET_RELPATH); let target_name = get_current_crate_info().name; - let default_bundle_directory = osdk_target_directory.join(target_name); - let existing_bundle = Bundle::load(&default_bundle_directory); - let config = RunConfig { - settings: { - if config.gdb_server_args.is_gdb_enabled { - let qemu_gdb_args: Vec<_> = { - let gdb_stub_addr = config.gdb_server_args.gdb_server_addr.as_str(); - match gdb::stub_type_of(gdb_stub_addr) { - gdb::StubAddrType::Unix => { - let chardev = format!( - "-chardev socket,path={},server=on,wait=off,id=gdb0", - gdb_stub_addr - ); - let stub = "-gdb chardev:gdb0".to_owned(); - vec![chardev, stub, "-S".into()] - } - gdb::StubAddrType::Tcp => { - vec![ - format!( - "-gdb tcp:{}", - gdb::tcp_addr_util::format_tcp_addr(gdb_stub_addr) - ), - "-S".into(), - ] - } - } - }; - - let qemu_gdb_args: Vec<_> = qemu_gdb_args - .into_iter() - .filter(|arg| !config.settings.qemu_args.iter().any(|x| x == arg)) - .map(|x| x.to_string()) - .collect(); - let mut settings = config.settings.clone(); - settings.qemu_args.extend(qemu_gdb_args); - settings - } else { - config.settings.clone() - } - }, - ..config.clone() - }; - let _vsc_launch_file = config.gdb_server_args.vsc_launch_file.then(|| { - vsc::check_gdb_config(&config.gdb_server_args); - let profile = super::util::profile_adapter(&config.cargo_args.profile); - vsc::VscLaunchConfig::new(profile, &config.gdb_server_args.gdb_server_addr) - }); - - // If the source is not since modified and the last build is recent, we can reuse the existing bundle. - if let Some(existing_bundle) = existing_bundle { - if existing_bundle.can_run_with_config(&config) { - if let Ok(built_since) = - SystemTime::now().duration_since(existing_bundle.last_modified_time()) - { - if built_since < Duration::from_secs(600) { - let workspace_root = { - let meta = get_cargo_metadata(None::<&str>, None::<&[&str]>).unwrap(); - PathBuf::from(meta.get("workspace_root").unwrap().as_str().unwrap()) - }; - if get_last_modified_time(workspace_root) < existing_bundle.last_modified_time() - { - existing_bundle.run(&config); - return; - } + let mut config = config.clone(); + if gdb_server_args.is_gdb_enabled { + let qemu_gdb_args = { + let gdb_stub_addr = gdb_server_args.gdb_server_addr.as_str(); + match gdb::stub_type_of(gdb_stub_addr) { + gdb::StubAddrType::Unix => { + format!( + " -chardev socket,path={},server=on,wait=off,id=gdb0 -gdb chardev:gdb0 -S", + gdb_stub_addr + ) + } + gdb::StubAddrType::Tcp => { + format!( + " -gdb tcp:{} -S", + gdb::tcp_addr_util::format_tcp_addr(gdb_stub_addr) + ) } } - } + }; + config.run.qemu.args += &qemu_gdb_args; } + let _vsc_launch_file = gdb_server_args.vsc_launch_file.then(|| { + vsc::check_gdb_config(gdb_server_args); + let profile = super::util::profile_adapter(&config.build.profile); + vsc::VscLaunchConfig::new(profile, &gdb_server_args.gdb_server_addr) + }); - let required_build_config = BuildConfig { - arch: config.arch, - settings: config.settings.clone(), - cargo_args: config.cargo_args.clone(), - }; - - let bundle = create_base_and_build( + let default_bundle_directory = osdk_output_directory.join(target_name); + let bundle = create_base_and_cached_build( default_bundle_directory, - &osdk_target_directory, - &ws_target_directory, - &required_build_config, + &osdk_output_directory, + &cargo_target_directory, + &config, + ActionChoice::Run, &[], ); - bundle.run(&config); -} -fn get_last_modified_time(path: impl AsRef) -> SystemTime { - let mut last_modified = SystemTime::UNIX_EPOCH; - for entry in std::fs::read_dir(path).unwrap() { - let entry = entry.unwrap(); - if entry.file_name() == "target" { - continue; - } - - let metadata = entry.metadata().unwrap(); - if metadata.is_dir() { - last_modified = std::cmp::max(last_modified, get_last_modified_time(&entry.path())); - } else { - last_modified = std::cmp::max(last_modified, metadata.modified().unwrap()); - } - } - last_modified + bundle.run(&config, ActionChoice::Run); } mod gdb { diff --git a/osdk/src/commands/test.rs b/osdk/src/commands/test.rs index dc55b533..5bad28f8 100644 --- a/osdk/src/commands/test.rs +++ b/osdk/src/commands/test.rs @@ -2,38 +2,38 @@ use std::fs; -use super::{build::do_build, util::DEFAULT_TARGET_RELPATH}; +use super::{build::do_cached_build, util::DEFAULT_TARGET_RELPATH}; use crate::{ base_crate::new_base_crate, - cli::GdbServerArgs, - config_manager::{BuildConfig, RunConfig, TestConfig}, + cli::TestArgs, + config::{scheme::ActionChoice, Config}, util::{get_cargo_metadata, get_current_crate_info, get_target_directory}, }; -pub fn execute_test_command(config: &TestConfig) { +pub fn execute_test_command(config: &Config, args: &TestArgs) { let crates = get_workspace_default_members(); for crate_path in crates { std::env::set_current_dir(crate_path).unwrap(); - test_current_crate(config); + test_current_crate(config, args); } } -pub fn test_current_crate(config: &TestConfig) { +pub fn test_current_crate(config: &Config, args: &TestArgs) { let current_crate = get_current_crate_info(); - let ws_target_directory = get_target_directory(); - let osdk_target_directory = ws_target_directory.join(DEFAULT_TARGET_RELPATH); - let target_crate_dir = osdk_target_directory.join("base"); + let cargo_target_directory = get_target_directory(); + let osdk_output_directory = cargo_target_directory.join(DEFAULT_TARGET_RELPATH); + let target_crate_dir = osdk_output_directory.join("base"); new_base_crate(&target_crate_dir, ¤t_crate.name, ¤t_crate.path); let main_rs_path = target_crate_dir.join("src").join("main.rs"); - let ktest_test_whitelist = match &config.test_name { + let ktest_test_whitelist = match &args.test_name { Some(name) => format!(r#"Some(&["{}"])"#, name), None => r#"None"#.to_string(), }; let mut ktest_crate_whitelist = vec![current_crate.name]; - if let Some(name) = &config.test_name { + if let Some(name) = &args.test_name { ktest_crate_whitelist.push(name.clone()); } @@ -54,32 +54,21 @@ pub static KTEST_CRATE_WHITELIST: Option<&[&str]> = Some(&{:#?}); // Build the kernel with the given base crate let target_name = get_current_crate_info().name; - let default_bundle_directory = osdk_target_directory.join(target_name); - let required_build_config = BuildConfig { - arch: config.arch, - settings: config.settings.clone(), - cargo_args: config.cargo_args.clone(), - }; + let default_bundle_directory = osdk_output_directory.join(target_name); let original_dir = std::env::current_dir().unwrap(); std::env::set_current_dir(&target_crate_dir).unwrap(); - let bundle = do_build( + let bundle = do_cached_build( default_bundle_directory, - &osdk_target_directory, - &ws_target_directory, - &required_build_config, + &osdk_output_directory, + &cargo_target_directory, + config, + ActionChoice::Test, &["--cfg ktest"], ); std::env::remove_var("RUSTFLAGS"); std::env::set_current_dir(original_dir).unwrap(); - let required_run_config = RunConfig { - arch: config.arch, - settings: required_build_config.settings.clone(), - cargo_args: required_build_config.cargo_args.clone(), - gdb_server_args: GdbServerArgs::default(), - }; - - bundle.run(&required_run_config); + bundle.run(config, ActionChoice::Test); } fn get_workspace_default_members() -> Vec { diff --git a/osdk/src/config/eval.rs b/osdk/src/config/eval.rs new file mode 100644 index 00000000..6ee86c4f --- /dev/null +++ b/osdk/src/config/eval.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MPL-2.0 + +//! The module implementing the evaluation feature. + +use std::{io, process}; + +pub type Vars = Vec<(String, String)>; + +/// This function is used to evaluate the string using the host's shell recursively +/// in order. +pub fn eval(vars: &Vars, s: &String) -> io::Result { + let mut vars = vars.clone(); + for i in 0..vars.len() { + vars[i].1 = eval_with_finalized_vars(&vars[..i], &vars[i].1)?; + } + eval_with_finalized_vars(&vars[..], s) +} + +fn eval_with_finalized_vars(vars: &[(String, String)], s: &String) -> io::Result { + let env_keys: Vec = std::env::vars().map(|(key, _)| key).collect(); + + let mut eval = process::Command::new("bash"); + let mut cwd = std::env::current_dir()?; + for (key, value) in vars { + // If the key is in the environment, we should ignore it. + // This allows users to override with the environment variables in CLI. + if env_keys.contains(key) { + continue; + } + eval.env(key, value); + if key == "OSDK_CWD" { + cwd = std::path::PathBuf::from(value); + } + } + eval.arg("-c"); + eval.arg(format!("echo \"{}\"", s)); + eval.current_dir(cwd); + let output = eval.output()?; + if !output.stderr.is_empty() { + println!( + "[Info] {}", + String::from_utf8_lossy(&output.stderr).trim_end_matches('\n') + ); + } + Ok(String::from_utf8_lossy(&output.stdout) + .trim_end_matches('\n') + .to_string()) +} diff --git a/osdk/src/config/manifest.rs b/osdk/src/config/manifest.rs new file mode 100644 index 00000000..c07f4aca --- /dev/null +++ b/osdk/src/config/manifest.rs @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: MPL-2.0 + +use std::{ + collections::HashMap, + fmt, fs, + path::{Path, PathBuf}, + process, +}; + +use clap::ValueEnum; +use serde::{de, Deserialize, Deserializer, Serialize}; + +use super::scheme::Scheme; + +use crate::{error::Errno, error_msg, util::get_cargo_metadata}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OsdkMeta { + #[serde(rename(serialize = "type", deserialize = "type"))] + pub type_: ProjectType, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, ValueEnum, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum ProjectType { + Kernel, + #[value(alias("lib"))] + Library, + Module, +} + +/// The osdk manifest from configuration file `OSDK.toml`. +#[derive(Debug, Clone)] +pub struct TomlManifest { + pub project_type: Option, + pub default_scheme: Scheme, + pub map: HashMap, +} + +impl TomlManifest { + pub fn load(feature_strings: &Vec) -> Self { + let workspace_root = { + let cargo_metadata = get_cargo_metadata(None::<&str>, Some(feature_strings)).unwrap(); + PathBuf::from( + cargo_metadata + .get("workspace_root") + .unwrap() + .as_str() + .unwrap(), + ) + }; + // All the custom schemes should inherit settings from the default scheme, this is a helper. + fn finalize(current_manifest: Option) -> TomlManifest { + let Some(mut current_manifest) = current_manifest else { + error_msg!( + "Cannot find `OSDK.toml` in the current directory or the workspace root" + ); + process::exit(Errno::GetMetadata as _); + }; + for scheme in current_manifest.map.values_mut() { + scheme.inherit(¤t_manifest.default_scheme); + } + current_manifest + } + + // Search for OSDK.toml in the current directory first. + let current_manifest_path = PathBuf::from("OSDK.toml").canonicalize().ok(); + let mut current_manifest = match ¤t_manifest_path { + Some(path) => deserialize_toml_manifest(path), + None => None, + }; + // Then search in the workspace root. + let workspace_manifest_path = workspace_root.join("OSDK.toml").canonicalize().ok(); + // The case that the current directory is also the workspace root. + if let Some(current) = ¤t_manifest_path { + if let Some(workspace) = &workspace_manifest_path { + if current == workspace { + return finalize(current_manifest); + } + } + } + let workspace_manifest = match workspace_manifest_path { + Some(path) => deserialize_toml_manifest(path), + None => None, + }; + // The current manifest should inherit settings from the workspace manifest. + if let Some(workspace_manifest) = workspace_manifest { + if current_manifest.is_none() { + current_manifest = Some(workspace_manifest); + } else { + // Inherit one scheme at a time. + let current_manifest = current_manifest.as_mut().unwrap(); + current_manifest + .default_scheme + .inherit(&workspace_manifest.default_scheme); + for (scheme_string, scheme) in workspace_manifest.map { + let current_scheme = current_manifest + .map + .entry(scheme_string) + .or_insert_with(Scheme::empty); + current_scheme.inherit(&scheme); + } + } + } + finalize(current_manifest) + } + + /// Get the scheme given the scheme from the command line arguments. + pub fn get_scheme(&self, scheme: Option) -> &Scheme { + if let Some(scheme) = scheme { + let selected_scheme = self.map.get(&scheme.to_string()); + if selected_scheme.is_none() { + error_msg!("Scheme `{}` not found in `OSDK.toml`", scheme.to_string()); + process::exit(Errno::ParseMetadata as _); + } + selected_scheme.unwrap() + } else { + &self.default_scheme + } + } +} + +fn deserialize_toml_manifest(path: impl AsRef) -> Option { + if !path.as_ref().exists() || !path.as_ref().is_file() { + return None; + } + // Read the file content + let contents = fs::read_to_string(&path).unwrap_or_else(|err| { + error_msg!( + "Cannot read file {}, {}", + path.as_ref().to_string_lossy(), + err, + ); + process::exit(Errno::GetMetadata as _); + }); + // Parse the TOML content + let mut manifest: TomlManifest = toml::from_str(&contents).unwrap_or_else(|err| { + let span = err.span().unwrap(); + let wider_span = + (span.start as isize - 20).max(0) as usize..(span.end + 20).min(contents.len()); + error_msg!( + "Cannot parse TOML file, {}. {}:{:?}:\n {}", + err.message(), + path.as_ref().to_string_lossy(), + span, + &contents[wider_span], + ); + process::exit(Errno::ParseMetadata as _); + }); + + // Preprocess the parsed manifest + let cwd = path.as_ref().parent().unwrap(); + // Canonicalize all the path fields + let canonicalize = |target: &mut PathBuf| { + let last_cwd = std::env::current_dir().unwrap(); + std::env::set_current_dir(cwd).unwrap(); + *target = target.canonicalize().unwrap_or_else(|err| { + error_msg!( + "Cannot canonicalize path `{}`: {}", + target.to_string_lossy(), + err, + ); + std::env::set_current_dir(&last_cwd).unwrap(); + process::exit(Errno::GetMetadata as _); + }); + std::env::set_current_dir(last_cwd).unwrap(); + }; + let canonicalize_scheme = |scheme: &mut Scheme| { + macro_rules! canonicalize_paths_in_scheme { + ($scheme:expr) => { + if let Some(ref mut boot) = $scheme.boot { + if let Some(ref mut initramfs) = boot.initramfs { + canonicalize(initramfs); + } + } + if let Some(ref mut qemu) = $scheme.qemu { + if let Some(ref mut qemu_path) = qemu.path { + canonicalize(qemu_path); + } + } + if let Some(ref mut grub) = $scheme.grub { + if let Some(ref mut grub_mkrescue_path) = grub.grub_mkrescue { + canonicalize(grub_mkrescue_path); + } + } + }; + } + canonicalize_paths_in_scheme!(scheme); + if let Some(ref mut run) = scheme.run { + canonicalize_paths_in_scheme!(run); + } + if let Some(ref mut test) = scheme.test { + canonicalize_paths_in_scheme!(test); + } + }; + canonicalize_scheme(&mut manifest.default_scheme); + for (_, scheme) in manifest.map.iter_mut() { + canonicalize_scheme(scheme); + } + // Set the magic variable `OSDK_CWD` before any variable evaluation + let var = ("OSDK_CWD".to_owned(), cwd.to_string_lossy().to_string()); + manifest.default_scheme.vars = { + let mut vars = vec![var.clone()]; + vars.extend(manifest.default_scheme.vars.clone()); + vars + }; + for (_, scheme) in manifest.map.iter_mut() { + scheme.vars = { + let mut vars = vec![var.clone()]; + vars.extend(scheme.vars.clone()); + vars + }; + } + Some(manifest) +} + +impl<'de> Deserialize<'de> for TomlManifest { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + enum Field { + ProjectType, + SupportedArchs, + Vars, + Boot, + Grub, + Qemu, + Build, + Run, + Test, + Scheme, + } + + const EXPECTED: &[&str] = &[ + "project_type", + "supported_archs", + "vars", + "boot", + "grub", + "qemu", + "build", + "run", + "test", + "scheme", + ]; + + impl<'de> Deserialize<'de> for Field { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct FieldVisitor; + + impl<'de> de::Visitor<'de> for FieldVisitor { + type Value = Field; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str(&EXPECTED.join(", ")) + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + match v { + "project_type" => Ok(Field::ProjectType), + "supported_archs" => Ok(Field::SupportedArchs), + "vars" => Ok(Field::Vars), + "boot" => Ok(Field::Boot), + "grub" => Ok(Field::Grub), + "qemu" => Ok(Field::Qemu), + "build" => Ok(Field::Build), + "run" => Ok(Field::Run), + "test" => Ok(Field::Test), + "scheme" => Ok(Field::Scheme), + _ => Err(de::Error::unknown_field(v, EXPECTED)), + } + } + } + + deserializer.deserialize_identifier(FieldVisitor) + } + } + + struct TomlManifestVisitor; + + impl<'de> de::Visitor<'de> for TomlManifestVisitor { + type Value = TomlManifest; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("Scheme") + } + + fn visit_map(self, mut map: A) -> Result + where + A: de::MapAccess<'de>, + { + let mut project_type = None; + let mut default_scheme = Scheme::empty(); + let mut scheme_map = HashMap::::new(); + + macro_rules! match_and_add_option { + ($field:ident) => {{ + let value = map.next_value()?; + if default_scheme.$field.is_some() { + error_msg!("Duplicated field `{}`", stringify!($field)); + process::exit(Errno::ParseMetadata as _); + } + default_scheme.$field = Some(value); + }}; + } + macro_rules! match_and_add_vec { + ($field:ident) => {{ + let value = map.next_value()?; + if !default_scheme.$field.is_empty() { + error_msg!("Duplicated field `{}`", stringify!($field)); + process::exit(Errno::ParseMetadata as _); + } + default_scheme.$field = value; + }}; + } + + while let Some(key) = map.next_key()? { + match key { + Field::ProjectType => { + let value: ProjectType = map.next_value()?; + project_type = Some(value); + } + Field::SupportedArchs => match_and_add_vec!(supported_archs), + Field::Vars => match_and_add_vec!(vars), + Field::Boot => match_and_add_option!(boot), + Field::Grub => match_and_add_option!(grub), + Field::Qemu => match_and_add_option!(qemu), + Field::Build => match_and_add_option!(build), + Field::Run => match_and_add_option!(run), + Field::Test => match_and_add_option!(test), + Field::Scheme => { + let scheme: HashMap = map.next_value()?; + scheme_map = scheme; + } + } + } + + Ok(TomlManifest { + project_type, + default_scheme, + map: scheme_map, + }) + } + } + + deserializer.deserialize_struct("TomlManifest", EXPECTED, TomlManifestVisitor) + } +} diff --git a/osdk/src/config/mod.rs b/osdk/src/config/mod.rs new file mode 100644 index 00000000..764ce9e1 --- /dev/null +++ b/osdk/src/config/mod.rs @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: MPL-2.0 + +//! This module is responsible for parsing configuration files and combining them with command-line parameters +//! to obtain the final configuration, it will also try searching system to fill valid values for specific +//! arguments if the arguments is missing, e.g., the path of QEMU. The final configuration is stored in `BuildConfig`, +//! `RunConfig` and `TestConfig`. These `*Config` are used for `build`, `run` and `test` subcommand. + +mod eval; + +pub mod manifest; +pub mod scheme; +pub mod unix_args; + +#[cfg(test)] +mod test; + +use scheme::{ + Action, ActionScheme, BootScheme, Build, BuildScheme, GrubScheme, QemuScheme, Scheme, +}; + +use crate::{ + arch::{get_default_arch, Arch}, + cli::CommonArgs, + config::unix_args::apply_kv_array, +}; + +/// The global configuration for the OSDK actions. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Config { + pub target_arch: Arch, + pub build: Build, + pub run: Action, + pub test: Action, +} + +fn apply_args_before_finalize(action_scheme: &mut ActionScheme, args: &CommonArgs) { + if action_scheme.build.is_none() { + action_scheme.build = Some(BuildScheme::default()); + } + if let Some(ref mut build) = action_scheme.build { + if let Some(profile) = &args.build_args.profile() { + build.profile = Some(profile.clone()); + } + build.features.extend(args.build_args.features.clone()); + if args.linux_x86_legacy_boot { + build.linux_x86_legacy_boot = true; + } + } + + if action_scheme.grub.is_none() { + action_scheme.grub = Some(GrubScheme::default()); + } + if let Some(ref mut grub) = action_scheme.grub { + if let Some(grub_mkrescue) = &args.grub_mkrescue { + grub.grub_mkrescue = Some(grub_mkrescue.clone()); + } + if let Some(grub_boot_protocol) = args.grub_boot_protocol { + grub.boot_protocol = Some(grub_boot_protocol); + } + } + + if action_scheme.boot.is_none() { + action_scheme.boot = Some(BootScheme::default()); + } + if let Some(ref mut boot) = action_scheme.boot { + apply_kv_array(&mut boot.kcmd_args, &args.kcmd_args, "=", &[]); + for init_arg in &args.init_args { + for seperated_arg in init_arg.split(' ') { + boot.init_args.push(seperated_arg.to_string()); + } + } + if let Some(initramfs) = &args.initramfs { + boot.initramfs = Some(initramfs.clone()); + } + if let Some(boot_method) = args.boot_method { + boot.method = Some(boot_method); + } + } + + if action_scheme.qemu.is_none() { + action_scheme.qemu = Some(QemuScheme::default()); + } + if let Some(ref mut qemu) = action_scheme.qemu { + if let Some(path) = &args.qemu_exe { + qemu.path = Some(path.clone()); + } + } +} + +fn apply_args_after_finalize(action: &mut Action, args: &CommonArgs) { + action.qemu.apply_qemu_args(&args.qemu_args); + if args.display_grub_menu { + action.grub.display_grub_menu = true; + } +} + +impl Config { + pub fn new(scheme: &Scheme, common_args: &CommonArgs) -> Self { + let target_arch = common_args.target_arch.unwrap_or(get_default_arch()); + let default_scheme = ActionScheme { + vars: scheme.vars.clone(), + boot: scheme.boot.clone(), + grub: scheme.grub.clone(), + qemu: scheme.qemu.clone(), + build: scheme.build.clone(), + }; + let run = { + let mut run = scheme.run.clone().unwrap_or_default(); + run.inherit(&default_scheme); + apply_args_before_finalize(&mut run, common_args); + let mut run = run.finalize(target_arch); + apply_args_after_finalize(&mut run, common_args); + run + }; + let test = { + let mut test = scheme.test.clone().unwrap_or_default(); + test.inherit(&default_scheme); + apply_args_before_finalize(&mut test, common_args); + let mut test = test.finalize(target_arch); + apply_args_after_finalize(&mut test, common_args); + test + }; + Self { + target_arch, + build: scheme.build.clone().unwrap_or_default().finalize(), + run, + test, + } + } +} diff --git a/osdk/src/config/scheme/action.rs b/osdk/src/config/scheme/action.rs new file mode 100644 index 00000000..6e4a997f --- /dev/null +++ b/osdk/src/config/scheme/action.rs @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MPL-2.0 + +use super::{inherit_optional, Boot, BootScheme, Grub, GrubScheme, Qemu, QemuScheme}; + +use crate::config::{scheme::Vars, Arch}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum ActionChoice { + Run, + Test, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BuildScheme { + pub profile: Option, + pub features: Vec, + /// Whether to turn on the support for the + /// [Linux legacy x86 32-bit boot protocol](https://www.kernel.org/doc/html/v5.6/x86/boot.html) + #[serde(default)] + pub linux_x86_legacy_boot: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Build { + pub profile: String, + pub features: Vec, + #[serde(default)] + pub linux_x86_legacy_boot: bool, +} + +impl Default for Build { + fn default() -> Self { + Self { + profile: "dev".to_string(), + features: Vec::new(), + linux_x86_legacy_boot: false, + } + } +} + +impl BuildScheme { + pub fn inherit(&mut self, parent: &Self) { + if parent.profile.is_some() { + self.profile = parent.profile.clone(); + } + self.features = { + let mut features = parent.features.clone(); + features.extend(self.features.clone()); + features + }; + if parent.linux_x86_legacy_boot { + self.linux_x86_legacy_boot = true; + } + } + + pub fn finalize(self) -> Build { + Build { + profile: self.profile.unwrap_or_else(|| "dev".to_string()), + features: self.features, + linux_x86_legacy_boot: self.linux_x86_legacy_boot, + } + } +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ActionScheme { + #[serde(default)] + pub vars: Vars, + pub boot: Option, + pub grub: Option, + pub qemu: Option, + pub build: Option, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Action { + pub boot: Boot, + pub grub: Grub, + pub qemu: Qemu, + pub build: Build, +} + +impl ActionScheme { + pub fn inherit(&mut self, from: &Self) { + self.vars = { + let mut vars = from.vars.clone(); + vars.extend(self.vars.clone()); + vars + }; + inherit_optional!(from, self, .boot); + inherit_optional!(from, self, .grub); + inherit_optional!(from, self, .qemu); + inherit_optional!(from, self, .build); + } + + pub fn finalize(self, arch: Arch) -> Action { + Action { + boot: self.boot.unwrap_or_default().finalize(), + grub: self.grub.unwrap_or_default().finalize(), + qemu: self.qemu.unwrap_or_default().finalize(&self.vars, arch), + build: self.build.unwrap_or_default().finalize(), + } + } +} diff --git a/osdk/src/config/scheme/boot.rs b/osdk/src/config/scheme/boot.rs new file mode 100644 index 00000000..a2741069 --- /dev/null +++ b/osdk/src/config/scheme/boot.rs @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MPL-2.0 + +use clap::ValueEnum; + +use std::path::PathBuf; + +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BootScheme { + /// Command line arguments for the guest kernel + #[serde(default)] + pub kcmd_args: Vec, + /// Command line arguments for the guest init process + #[serde(default)] + pub init_args: Vec, + /// The path of initramfs + pub initramfs: Option, + /// The infrastructures used to boot the guest + pub method: Option, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, ValueEnum)] +#[serde(rename_all = "kebab-case")] +pub enum BootMethod { + /// Boot the kernel by making a rescue CD image. + GrubRescueIso, + /// Boot the kernel by making a Qcow2 image with Grub as the bootloader. + GrubQcow2, + /// Use the [QEMU direct boot](https://qemu-project.gitlab.io/qemu/system/linuxboot.html) + /// to boot the kernel with QEMU's built-in Seabios and Coreboot utilites. + QemuDirect, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Boot { + pub kcmdline: Vec, + pub initramfs: Option, + pub method: BootMethod, +} + +impl Default for Boot { + fn default() -> Self { + Boot { + kcmdline: vec![], + initramfs: None, + method: BootMethod::QemuDirect, + } + } +} + +impl BootScheme { + pub fn inherit(&mut self, from: &Self) { + self.kcmd_args = { + let mut kcmd_args = from.kcmd_args.clone(); + kcmd_args.extend(self.kcmd_args.clone()); + kcmd_args + }; + self.init_args = { + let mut init_args = from.init_args.clone(); + init_args.extend(self.init_args.clone()); + init_args + }; + if self.initramfs.is_none() { + self.initramfs = from.initramfs.clone(); + } + if self.method.is_none() { + self.method = from.method; + } + } + + pub fn finalize(self) -> Boot { + let mut kcmdline = self.kcmd_args; + kcmdline.push("--".to_owned()); + kcmdline.extend(self.init_args); + Boot { + kcmdline, + initramfs: self.initramfs, + method: self.method.unwrap_or(BootMethod::QemuDirect), + } + } +} diff --git a/osdk/src/config/scheme/grub.rs b/osdk/src/config/scheme/grub.rs new file mode 100644 index 00000000..47be5394 --- /dev/null +++ b/osdk/src/config/scheme/grub.rs @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MPL-2.0 + +use clap::ValueEnum; + +use std::path::PathBuf; + +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct GrubScheme { + /// The path of `grub_mkrecue`. Only needed if `boot.method` is `grub` + pub grub_mkrescue: Option, + /// The boot protocol specified in the GRUB configuration + pub boot_protocol: Option, + /// Whether to display the GRUB menu, defaults to `false` + #[serde(default)] + pub display_grub_menu: bool, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, ValueEnum)] +#[serde(rename_all = "kebab-case")] +pub enum BootProtocol { + Linux, + Multiboot, + Multiboot2, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Grub { + pub grub_mkrescue: PathBuf, + pub boot_protocol: BootProtocol, + pub display_grub_menu: bool, +} + +impl Default for Grub { + fn default() -> Self { + Grub { + grub_mkrescue: PathBuf::from("grub-mkrescue"), + boot_protocol: BootProtocol::Multiboot2, + display_grub_menu: false, + } + } +} + +impl GrubScheme { + pub fn inherit(&mut self, from: &Self) { + if self.grub_mkrescue.is_none() { + self.grub_mkrescue = from.grub_mkrescue.clone(); + } + if self.boot_protocol.is_none() { + self.boot_protocol = from.boot_protocol; + } + // `display_grub_menu` is not inherited + } + + pub fn finalize(self) -> Grub { + Grub { + grub_mkrescue: self.grub_mkrescue.unwrap_or(PathBuf::from("grub-mkrescue")), + boot_protocol: self.boot_protocol.unwrap_or(BootProtocol::Multiboot2), + display_grub_menu: self.display_grub_menu, + } + } +} diff --git a/osdk/src/config/scheme/mod.rs b/osdk/src/config/scheme/mod.rs new file mode 100644 index 00000000..038e8a85 --- /dev/null +++ b/osdk/src/config/scheme/mod.rs @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MPL-2.0 + +use super::eval::Vars; + +use crate::arch::Arch; + +mod action; +pub use action::*; +mod boot; +pub use boot::*; +mod grub; +pub use grub::*; +mod qemu; +pub use qemu::*; + +/// All the configurable fields within a scheme. +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Scheme { + #[serde(default)] + pub supported_archs: Vec, + #[serde(default)] + pub vars: Vars, + pub boot: Option, + pub grub: Option, + pub qemu: Option, + pub build: Option, + pub run: Option, + pub test: Option, +} + +macro_rules! inherit_optional { + ($from: ident, $to:ident, .$field:ident) => { + if $from.$field.is_some() { + if let Some($field) = &mut $to.$field { + $field.inherit($from.$field.as_ref().unwrap()); + } else { + $to.$field = $from.$field.clone(); + } + } + }; +} +use inherit_optional; + +impl Scheme { + pub fn empty() -> Self { + Scheme { + supported_archs: vec![], + vars: vec![], + boot: None, + grub: None, + qemu: None, + build: None, + run: None, + test: None, + } + } + + pub fn inherit(&mut self, from: &Self) { + // Supported archs are not inherited + + self.vars = { + let mut vars = from.vars.clone(); + vars.extend(self.vars.clone()); + vars + }; + inherit_optional!(from, self, .boot); + inherit_optional!(from, self, .grub); + inherit_optional!(from, self, .qemu); + inherit_optional!(from, self, .build); + inherit_optional!(from, self, .run); + inherit_optional!(from, self, .test); + } +} diff --git a/osdk/src/config/scheme/qemu.rs b/osdk/src/config/scheme/qemu.rs new file mode 100644 index 00000000..c86632cf --- /dev/null +++ b/osdk/src/config/scheme/qemu.rs @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MPL-2.0 + +//! A module about QEMU settings and arguments. + +use std::{path::PathBuf, process}; + +use crate::{ + arch::{get_default_arch, Arch}, + config::{ + eval::{eval, Vars}, + unix_args::{apply_kv_array, get_key}, + }, + error::Errno, + error_msg, +}; + +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct QemuScheme { + /// The additional arguments for running QEMU, in the form of raw + /// command line arguments. + pub args: Option, + /// The path of qemu + pub path: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Qemu { + pub args: String, + pub path: PathBuf, +} + +impl Default for Qemu { + fn default() -> Self { + Qemu { + args: String::new(), + path: PathBuf::from(get_default_arch().system_qemu()), + } + } +} + +impl Qemu { + pub fn apply_qemu_args(&mut self, args: &Vec) { + let target = match shlex::split(&self.args) { + Some(v) => v, + None => { + error_msg!("Failed to parse qemu args: {:#?}", &self.args); + process::exit(Errno::ParseMetadata as _); + } + }; + + // Join the key value arguments as a single element + let mut joined = Vec::new(); + let mut consumed = false; + for (first, second) in target.iter().zip(target.iter().skip(1)) { + if consumed { + consumed = false; + continue; + } + if first.starts_with('-') && !first.starts_with("--") && !second.starts_with('-') { + joined.push(format!("{} {}", first, second)); + consumed = true; + } else { + joined.push(first.clone()); + } + } + if !consumed { + joined.push(target.last().unwrap().clone()); + } + + // Check the soundness of qemu arguments + for arg in joined.iter() { + check_qemu_arg(arg); + } + for arg in joined.iter() { + check_qemu_arg(arg); + } + + apply_kv_array(&mut joined, args, " ", MULTI_VALUE_KEYS); + + self.args = joined.join(" "); + } +} + +impl QemuScheme { + pub fn inherit(&mut self, from: &Self) { + if from.args.is_some() { + self.args = from.args.clone(); + } + if from.path.is_some() { + self.path = from.path.clone(); + } + } + + pub fn finalize(self, vars: &Vars, arch: Arch) -> Qemu { + Qemu { + args: self + .args + .map(|args| match eval(vars, &args) { + Ok(v) => v, + Err(e) => { + error_msg!("Failed to evaluate qemu args: {:#?}", e); + process::exit(Errno::ParseMetadata as _); + } + }) + .unwrap_or_default(), + path: self.path.unwrap_or(PathBuf::from(arch.system_qemu())), + } + } +} + +// Below are checked keys in qemu arguments. The key list is non-exhaustive. + +/// Keys with multiple values +const MULTI_VALUE_KEYS: &[&str] = &[ + "-device", "-chardev", "-object", "-netdev", "-drive", "-cdrom", +]; +/// Keys with only single value +const SINGLE_VALUE_KEYS: &[&str] = &["-cpu", "-machine", "-m", "-serial", "-monitor", "-display"]; +/// Keys with no value +const NO_VALUE_KEYS: &[&str] = &["--no-reboot", "-nographic", "-enable-kvm"]; +/// Keys are not allowed to set in configuration files and command line +const NOT_ALLOWED_TO_SET_KEYS: &[&str] = &["-kernel", "-append", "-initrd"]; + +fn check_qemu_arg(arg: &str) { + let key = if let Some(key) = get_key(arg, " ") { + key + } else { + arg.to_string() + }; + + if NOT_ALLOWED_TO_SET_KEYS.contains(&key.as_str()) { + error_msg!("`{}` is not allowed to set", arg); + process::exit(Errno::ParseMetadata as _); + } + + if NO_VALUE_KEYS.contains(&key.as_str()) && key.as_str() != arg { + error_msg!("`{}` cannot have value", arg); + process::exit(Errno::ParseMetadata as _); + } + + if (SINGLE_VALUE_KEYS.contains(&key.as_str()) || MULTI_VALUE_KEYS.contains(&key.as_str())) + && key.as_str() == arg + { + error_msg!("`{}` should have value", arg); + process::exit(Errno::ParseMetadata as _); + } +} diff --git a/osdk/src/config/test/OSDK.toml.full b/osdk/src/config/test/OSDK.toml.full new file mode 100644 index 00000000..57337c38 --- /dev/null +++ b/osdk/src/config/test/OSDK.toml.full @@ -0,0 +1,66 @@ +project_type = "kernel" + +supported_archs = ["x86_64"] +vars = [ + ["SMP", "1"], + ["MEM", "2G"], + ["EXT2_IMG", "$OSDK_CWD/regression/build/ext2.img"], + ["EXFAT_IMG", "$OSDK_CWD/regression/build/exfat.img"], +] + +[boot] +method = "grub-rescue-iso" + +[run] +vars = [ + ["OVMF_PATH", "/usr/share/OVMF"], +] +boot.kcmd_args = [ + "SHELL=/bin/sh", + "LOGNAME=root", + "HOME=/", + "USER=root", + "PATH=/bin:/benchmark", + "init=/usr/bin/busybox", +] +boot.init_args = ["sh", "-l"] +boot.initramfs = "/tmp/osdk_test_file" + +[test] +boot.method = "qemu-direct" + +[grub] +protocol = "multiboot2" +display_grub_menu = true + +[qemu] +args = """\ + -machine q35 \ + -smp $SMP \ + -m $MEM \ +""" + +[scheme."iommu"] +supported_archs = ["x86_64"] +vars = [ + ["IOMMU_DEV_EXTRA", ",iommu_platform=on,ats=on"], +] +qemu.args = """\ + -device intel-iommu,intremap=on,device-iotlb=on \ + -device ioh3420,id=pcie.0,chassis=1\ +""" + +[scheme."tdx"] +supported_archs = ["x86_64"] +build.features = ["intel_tdx"] +vars = [ + ["MEM", "8G"], + ["OVMF_PATH", "~/tdx-tools/ovmf"], +] +boot.method = "grub-qcow2" +grub.mkrescue_path = "/tmp/osdk_test_file" +grub.protocol = "linux" +qemu.path = "/tmp/osdk_test_file" +qemu.args = """\ + -name process=tdxvm,debug-threads=on \ +""" diff --git a/osdk/src/config/test/mod.rs b/osdk/src/config/test/mod.rs new file mode 100644 index 00000000..06750f2b --- /dev/null +++ b/osdk/src/config/test/mod.rs @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MPL-2.0 + +use std::{ + fs::{self, File}, + path::PathBuf, +}; + +use super::*; + +#[test] +fn deserialize_toml_manifest() { + let content = include_str!("OSDK.toml.full"); + let toml_manifest: manifest::TomlManifest = toml::from_str(content).unwrap(); + let type_ = toml_manifest.project_type.unwrap(); + assert!(type_ == manifest::ProjectType::Kernel); + let vars = toml_manifest.default_scheme.vars; + assert!(vars.contains(&("SMP".to_owned(), "1".to_owned()))); +} + +#[test] +fn conditional_manifest() { + let tmp_file = "/tmp/osdk_test_file"; + File::create(tmp_file).unwrap(); + + let toml_manifest: manifest::TomlManifest = { + let content = include_str!("OSDK.toml.full"); + toml::from_str(content).unwrap() + }; + + // Default scheme + let scheme = toml_manifest.get_scheme(None::); + assert!(scheme + .qemu + .as_ref() + .unwrap() + .args + .as_ref() + .unwrap() + .contains(&String::from("-machine q35",))); + + // Iommu + let scheme = toml_manifest.get_scheme(Some("iommu".to_owned())); + assert!(scheme + .qemu + .as_ref() + .unwrap() + .args + .as_ref() + .unwrap() + .contains(&String::from("-device ioh3420,id=pcie.0,chassis=1",))); + + // Tdx + let scheme = toml_manifest.get_scheme(Some("tdx".to_owned())); + assert_eq!( + scheme.qemu.as_ref().unwrap().path.as_ref().unwrap(), + &PathBuf::from(tmp_file) + ); + + fs::remove_file(tmp_file).unwrap(); +} diff --git a/osdk/src/config_manager/unix_args.rs b/osdk/src/config/unix_args.rs similarity index 100% rename from osdk/src/config_manager/unix_args.rs rename to osdk/src/config/unix_args.rs diff --git a/osdk/src/config_manager/action.rs b/osdk/src/config_manager/action.rs deleted file mode 100644 index db4b1ba6..00000000 --- a/osdk/src/config_manager/action.rs +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -use std::{path::PathBuf, process}; - -use clap::ValueEnum; - -use super::{qemu, unix_args::apply_kv_array}; - -use crate::{config_manager::OsdkArgs, error::Errno, error_msg}; - -/// The settings for an action (running or testing). -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ActionSettings { - /// Command line arguments for the guest kernel - #[serde(default)] - pub kcmd_args: Vec, - /// Command line arguments for the guest init process - #[serde(default)] - pub init_args: Vec, - /// The path of initramfs - pub initramfs: Option, - pub bootloader: Option, - pub boot_protocol: Option, - /// The path of `grub_mkrecue`. Only be `Some(_)` if `loader` is `Bootloader::grub` - pub grub_mkrescue: Option, - /// The path of OVMF binaries. Only required if `protocol` is `BootProtocol::LinuxEfiHandover64` - pub ovmf: Option, - /// The path of OpenSBI binaries. Only required for RISC-V. - pub opensbi: Option, - /// QEMU's available machines appended with various machine configurations - pub qemu_machine: Option, - /// The additional arguments for running QEMU, except `-cpu` and `-machine` - #[serde(default)] - pub qemu_args: Vec, - /// The additional drive files attaching to QEMU - #[serde(default)] - pub drive_files: Vec, - /// The path of qemu - pub qemu_exe: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ValueEnum)] -#[serde(rename_all = "kebab-case")] -pub enum Bootloader { - Grub, - Qemu, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ValueEnum)] -#[serde(rename_all = "kebab-case")] -pub enum BootProtocol { - LinuxEfiHandover64, - LinuxLegacy32, - Multiboot, - Multiboot2, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct DriveFile { - pub path: PathBuf, - pub append: String, -} - -impl ActionSettings { - pub fn canonicalize_paths(&mut self, cur_dir: impl AsRef) { - macro_rules! canonicalize_path { - ($path:expr) => {{ - let path = if $path.is_relative() { - cur_dir.as_ref().join($path) - } else { - $path.clone() - }; - path.canonicalize().unwrap_or_else(|_| { - error_msg!("File specified but not found: {:#?}", path); - process::exit(Errno::ParseMetadata as _); - }) - }}; - } - macro_rules! canonicalize_optional_path { - ($path:expr) => { - if let Some(path_inner) = &$path { - Some(canonicalize_path!(path_inner)) - } else { - None - } - }; - } - self.initramfs = canonicalize_optional_path!(self.initramfs); - self.grub_mkrescue = canonicalize_optional_path!(self.grub_mkrescue); - self.ovmf = canonicalize_optional_path!(self.ovmf); - self.qemu_exe = canonicalize_optional_path!(self.qemu_exe); - self.opensbi = canonicalize_optional_path!(self.opensbi); - for drive_file in &mut self.drive_files { - drive_file.path = canonicalize_path!(&drive_file.path); - } - } - - pub fn apply_cli_args(&mut self, args: &OsdkArgs) { - macro_rules! apply { - ($item:expr, $arg:expr) => { - if let Some(arg) = $arg.clone() { - $item = Some(arg); - } - }; - } - - apply!(self.initramfs, &args.initramfs); - apply!(self.ovmf, &args.ovmf); - apply!(self.opensbi, &args.opensbi); - apply!(self.grub_mkrescue, &args.grub_mkrescue); - apply!(self.bootloader, &args.bootloader); - apply!(self.boot_protocol, &args.boot_protocol); - apply!(self.qemu_exe, &args.qemu_exe); - - apply_kv_array(&mut self.kcmd_args, &args.kcmd_args, "=", &[]); - for init_arg in &args.init_args { - for seperated_arg in init_arg.split(' ') { - self.init_args.push(seperated_arg.to_string()); - } - } - - qemu::apply_qemu_args_addition(&mut self.qemu_args, &args.qemu_args_add); - } - - pub fn combined_kcmd_args(&self) -> Vec { - let mut kcmd_args = self.kcmd_args.clone(); - kcmd_args.push("--".to_owned()); - kcmd_args.extend(self.init_args.clone()); - kcmd_args - } -} diff --git a/osdk/src/config_manager/cfg.rs b/osdk/src/config_manager/cfg.rs deleted file mode 100644 index cbd396c0..00000000 --- a/osdk/src/config_manager/cfg.rs +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -//! A module for handling configurations. - -use std::{ - collections::BTreeMap, - fmt::{self, Display}, -}; - -/// A configuration that looks like "cfg(k1=v1, k2=v2, ...)". -#[derive(Debug, Clone, Eq, Ord, PartialOrd, PartialEq, Serialize)] -pub struct Cfg(BTreeMap); - -#[derive(Debug)] -pub struct CfgParseError(String); - -impl fmt::Display for CfgParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Failed to parse cfg: {}", self.0) - } -} - -impl serde::ser::StdError for CfgParseError {} -impl serde::de::Error for CfgParseError { - fn custom(msg: T) -> Self { - Self(msg.to_string()) - } -} - -impl CfgParseError { - pub fn new(s: &str) -> Self { - Self(s.to_string()) - } -} - -/// This allows literal constructions like `Cfg::from([("arch", "foo"), ("schema", "bar")])`. -impl From<[(K, V); N]> for Cfg -where - K: Into, - V: Into, -{ - fn from(array: [(K, V); N]) -> Self { - let mut cfg = BTreeMap::new(); - for (k, v) in array.into_iter() { - cfg.insert(k.into(), v.into()); - } - Self(cfg) - } -} - -impl Cfg { - pub fn empty() -> Self { - Self(BTreeMap::new()) - } - - pub fn from_str(s: &str) -> Result { - let s = s.trim(); - - // Match the leading "cfg(" and trailing ")" - if !s.starts_with("cfg(") || !s.ends_with(')') { - return Err(CfgParseError::new(s)); - } - let s = &s[4..s.len() - 1]; - - let mut cfg = BTreeMap::new(); - for kv in s.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) { - let kv: Vec<_> = kv.split('=').collect(); - if kv.len() != 2 { - return Err(CfgParseError::new(s)); - } - cfg.insert( - kv[0].trim().to_string(), - kv[1].trim().trim_matches('\"').to_string(), - ); - } - Ok(Self(cfg)) - } - - pub fn map(&self) -> &BTreeMap { - &self.0 - } -} - -impl Display for Cfg { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "cfg(")?; - for (i, (k, v)) in self.0.iter().enumerate() { - write!(f, "{}=\"{}\"", k, v)?; - if i != self.0.len() - 1 { - write!(f, ", ")?; - } - } - write!(f, ")") - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_cfg_from_str() { - let cfg = Cfg::from([("arch", "x86_64"), ("schema", "foo")]); - let cfg1 = Cfg::from_str("cfg(arch = \"x86_64\", schema=\"foo\", )").unwrap(); - let cfg2 = Cfg::from_str("cfg(arch=\"x86_64\",schema=\"foo\")").unwrap(); - let cfg3 = Cfg::from_str("cfg( arch=\"x86_64\", schema=\"foo\" )").unwrap(); - assert_eq!(cfg, cfg1); - assert_eq!(cfg, cfg2); - assert_eq!(cfg, cfg3); - } - - #[test] - fn test_cfg_display() { - let cfg = Cfg::from([("arch", "x86_64"), ("schema", "foo")]); - let cfg_string = cfg.to_string(); - let cfg_back = Cfg::from_str(&cfg_string).unwrap(); - assert_eq!(cfg_string, "cfg(arch=\"x86_64\", schema=\"foo\")"); - assert_eq!(cfg, cfg_back); - } - - #[test] - fn test_bad_cfg_strings() { - assert!(Cfg::from_str("fg(,,,,arch=\"x86_64 \", schema=\"foo\")").is_err()); - assert!(Cfg::from_str("cfg(arch=\"x86_64\", schema=\"foo\"").is_err()); - assert!(Cfg::from_str("cfgarch=x86_64,,, schema=\"foo\") ").is_err()); - } -} diff --git a/osdk/src/config_manager/manifest.rs b/osdk/src/config_manager/manifest.rs deleted file mode 100644 index 02d11ef8..00000000 --- a/osdk/src/config_manager/manifest.rs +++ /dev/null @@ -1,256 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -use std::{collections::BTreeMap, fmt, path::Path, process}; - -use clap::ValueEnum; -use serde::{de, Deserialize, Deserializer, Serialize}; - -use super::{action::ActionSettings, cfg::Cfg}; - -use crate::{config_manager::Arch, error::Errno, error_msg}; - -/// The settings for the actions summarized from the command line arguments -/// and the configuration file `OSDK.toml`. -#[derive(Debug, Clone)] -pub struct OsdkManifest { - pub project: Project, - pub run: Option, - pub test: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Project { - #[serde(rename(serialize = "type", deserialize = "type"))] - pub type_: ProjectType, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, ValueEnum, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub enum ProjectType { - Kernel, - #[value(alias("lib"))] - Library, - Module, -} - -/// The osdk manifest from configuration file `OSDK.toml`. -#[derive(Debug, Clone)] -pub struct TomlManifest { - pub project: Project, - cfg_map: BTreeMap, -} - -impl TomlManifest { - /// Get the action manifest given the architecture and the schema from the command line arguments. - /// - /// If any entry in the `OSDK.toml` manifest doesn't specify an architecture, we regard it matching - /// all the architectures. - pub fn get_osdk_manifest( - &self, - path_of_self: impl AsRef, - arch: Arch, - schema: Option, - ) -> OsdkManifest { - let filtered_by_arch = self.cfg_map.iter().filter(|(cfg, _)| { - if let Some(got) = cfg.map().get("arch") { - got == &arch.to_string() - } else { - true - } - }); - - let filtered_by_schema = if let Some(schema) = schema { - filtered_by_arch - .filter(|(cfg, _)| { - if let Some(got) = cfg.map().get("schema") { - got == &schema - } else { - false - } - }) - .collect::>() - } else { - filtered_by_arch - .filter(|(cfg, _)| cfg == &&Cfg::empty()) - .collect::>() - }; - - let filtered = filtered_by_schema; - if filtered.len() > 1 { - error_msg!("Multiple entries in OSDK.toml match the given architecture and schema"); - process::exit(Errno::ParseMetadata as _); - } - if filtered.is_empty() { - error_msg!("No entry in OSDK.toml matches the given architecture and schema"); - process::exit(Errno::ParseMetadata as _); - } - let final_cfg_args = filtered.first().unwrap().1; - let mut run = final_cfg_args.run.clone(); - if let Some(run_inner) = &mut run { - run_inner.canonicalize_paths(&path_of_self); - } - let mut test = final_cfg_args.test.clone(); - if let Some(test_inner) = &mut test { - test_inner.canonicalize_paths(&path_of_self); - } - OsdkManifest { - project: self.project.clone(), - run, - test, - } - } -} - -/// A inner adapter for `TomlManifest` to allow the `cfg` field to be optional. -/// The fields should be identical to `TomlManifest` except the `cfg` field. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -struct CfgArgs { - pub run: Option, - pub test: Option, -} - -impl CfgArgs { - pub fn try_accept(&mut self, another: CfgArgs) { - if another.run.is_some() { - if self.run.is_some() { - error_msg!("Duplicate `run` field in OSDK.toml"); - process::exit(Errno::ParseMetadata as _); - } - self.run = another.run; - } - if another.test.is_some() { - if self.test.is_some() { - error_msg!("Duplicate `test` field in OSDK.toml"); - process::exit(Errno::ParseMetadata as _); - } - self.test = another.test; - } - } -} - -impl<'de> Deserialize<'de> for TomlManifest { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - enum Field { - Project, - Run, - Test, - Cfg(Cfg), - } - - impl<'de> Deserialize<'de> for Field { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct FieldVisitor; - - impl<'de> de::Visitor<'de> for FieldVisitor { - type Value = Field; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("`project`, `run`, `test` or cfg") - } - - fn visit_str(self, v: &str) -> Result - where - E: de::Error, - { - match v { - "project" => Ok(Field::Project), - "run" => Ok(Field::Run), - "test" => Ok(Field::Test), - v => Ok(Field::Cfg(Cfg::from_str(v).unwrap_or_else(|e| { - error_msg!("Error parsing cfg: {}", e); - process::exit(Errno::ParseMetadata as _); - }))), - } - } - } - - deserializer.deserialize_identifier(FieldVisitor) - } - } - - struct TomlManifestVisitor; - - impl<'de> de::Visitor<'de> for TomlManifestVisitor { - type Value = TomlManifest; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("struct TomlManifest") - } - - fn visit_map(self, mut map: A) -> Result - where - A: de::MapAccess<'de>, - { - let mut project: Option = None; - let default_cfg = Cfg::empty(); - let mut cfg_map = BTreeMap::::new(); - - while let Some(key) = map.next_key()? { - match key { - Field::Project => { - let value = map.next_value()?; - project = Some(value); - } - Field::Run => { - let value: ActionSettings = map.next_value()?; - cfg_map - .entry(default_cfg.clone()) - .and_modify(|v| { - v.try_accept(CfgArgs { - run: Some(value.clone()), - test: None, - }) - }) - .or_insert(CfgArgs { - run: Some(value.clone()), - test: None, - }); - } - Field::Test => { - let value: ActionSettings = map.next_value()?; - cfg_map - .entry(default_cfg.clone()) - .and_modify(|v| { - v.try_accept(CfgArgs { - run: None, - test: Some(value.clone()), - }) - }) - .or_insert(CfgArgs { - run: None, - test: Some(value.clone()), - }); - } - Field::Cfg(cfg) => { - let value: CfgArgs = map.next_value()?; - cfg_map - .entry(cfg) - .and_modify(|v| v.try_accept(value.clone())) - .or_insert(value.clone()); - } - } - } - - Ok(TomlManifest { - project: project.unwrap_or_else(|| { - error_msg!("`project` field is required in OSDK.toml"); - process::exit(Errno::ParseMetadata as _); - }), - cfg_map, - }) - } - } - - deserializer.deserialize_struct( - "TomlManifest", - &["run", "test", "cfg"], - TomlManifestVisitor, - ) - } -} diff --git a/osdk/src/config_manager/mod.rs b/osdk/src/config_manager/mod.rs deleted file mode 100644 index 8d25616e..00000000 --- a/osdk/src/config_manager/mod.rs +++ /dev/null @@ -1,206 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -//! This module is responsible for parsing configuration files and combining them with command-line parameters -//! to obtain the final configuration, it will also try searching system to fill valid values for specific -//! arguments if the arguments is missing, e.g., the path of QEMU. The final configuration is stored in `BuildConfig`, -//! `RunConfig` and `TestConfig`. These `*Config` are used for `build`, `run` and `test` subcommand. - -pub mod action; -pub mod cfg; -pub mod manifest; -pub mod qemu; -pub mod unix_args; - -use action::ActionSettings; - -#[cfg(test)] -mod test; - -use std::{fs, path::PathBuf, process}; - -use self::manifest::{OsdkManifest, TomlManifest}; -use crate::{ - arch::{get_default_arch, Arch}, - cli::{BuildArgs, CargoArgs, DebugArgs, GdbServerArgs, OsdkArgs, RunArgs, TestArgs}, - error::Errno, - error_msg, - util::get_cargo_metadata, -}; - -/// Configurations for build subcommand -#[derive(Debug)] -pub struct BuildConfig { - pub arch: Arch, - pub settings: ActionSettings, - pub cargo_args: CargoArgs, -} - -impl BuildConfig { - pub fn parse(args: &BuildArgs) -> Self { - let arch = args.osdk_args.arch.unwrap_or_else(get_default_arch); - let cargo_args = parse_cargo_args(&args.cargo_args); - let mut manifest = load_osdk_manifest(&args.cargo_args, &args.osdk_args); - if let Some(run) = manifest.run.as_mut() { - run.apply_cli_args(&args.osdk_args); - } - Self { - arch, - settings: manifest.run.unwrap(), - cargo_args, - } - } -} - -/// Configurations for run subcommand -#[derive(Debug, Clone)] -pub struct RunConfig { - pub arch: Arch, - pub settings: ActionSettings, - pub cargo_args: CargoArgs, - pub gdb_server_args: GdbServerArgs, -} - -impl RunConfig { - pub fn parse(args: &RunArgs) -> Self { - let arch = args.osdk_args.arch.unwrap_or_else(get_default_arch); - let cargo_args = parse_cargo_args(&args.cargo_args); - let mut manifest = load_osdk_manifest(&args.cargo_args, &args.osdk_args); - if let Some(run) = manifest.run.as_mut() { - run.apply_cli_args(&args.osdk_args); - } - Self { - arch, - settings: manifest.run.unwrap(), - cargo_args, - gdb_server_args: args.gdb_server_args.clone(), - } - } -} - -#[derive(Debug)] -pub struct DebugConfig { - pub cargo_args: CargoArgs, - pub remote: String, -} - -impl DebugConfig { - pub fn parse(args: &DebugArgs) -> Self { - Self { - cargo_args: parse_cargo_args(&args.cargo_args), - remote: args.remote.clone(), - } - } -} - -/// Configurations for test subcommand -#[derive(Debug)] -pub struct TestConfig { - pub arch: Arch, - pub settings: ActionSettings, - pub cargo_args: CargoArgs, - pub test_name: Option, -} - -impl TestConfig { - pub fn parse(args: &TestArgs) -> Self { - let arch = args.osdk_args.arch.unwrap_or_else(get_default_arch); - let cargo_args = parse_cargo_args(&args.cargo_args); - let manifest = load_osdk_manifest(&args.cargo_args, &args.osdk_args); - // Use run settings if test settings are not provided - let mut test = if let Some(test) = manifest.test { - test - } else { - manifest.run.unwrap() - }; - test.apply_cli_args(&args.osdk_args); - Self { - arch, - settings: test, - cargo_args, - test_name: args.test_name.clone(), - } - } -} - -fn load_osdk_manifest(cargo_args: &CargoArgs, osdk_args: &OsdkArgs) -> OsdkManifest { - let feature_strings = get_feature_strings(cargo_args); - let cargo_metadata = get_cargo_metadata(None::<&str>, Some(&feature_strings)).unwrap(); - let workspace_root = PathBuf::from( - cargo_metadata - .get("workspace_root") - .unwrap() - .as_str() - .unwrap(), - ); - - // Search for OSDK.toml in the current directory. If not, dive into the workspace root. - let manifest_path = PathBuf::from("OSDK.toml"); - let (contents, manifest_path) = if let Ok(contents) = fs::read_to_string("OSDK.toml") { - (contents, manifest_path) - } else { - let manifest_path = workspace_root.join("OSDK.toml"); - let Ok(contents) = fs::read_to_string(&manifest_path) else { - error_msg!( - "Cannot read file {}", - manifest_path.to_string_lossy().to_string() - ); - process::exit(Errno::GetMetadata as _); - }; - (contents, manifest_path) - }; - - let toml_manifest: TomlManifest = toml::from_str(&contents).unwrap_or_else(|err| { - let span = err.span().unwrap(); - let wider_span = - (span.start as isize - 20).max(0) as usize..(span.end + 20).min(contents.len()); - error_msg!( - "Cannot parse TOML file, {}. {}:{:?}:\n {}", - err.message(), - manifest_path.to_string_lossy().to_string(), - span, - &contents[wider_span], - ); - process::exit(Errno::ParseMetadata as _); - }); - let osdk_manifest = toml_manifest.get_osdk_manifest( - workspace_root, - osdk_args.arch.unwrap_or_else(get_default_arch), - osdk_args.schema.as_ref().map(|s| s.to_string()), - ); - osdk_manifest -} - -/// Parse cargo args. -/// 1. Split `features` in `cargo_args` to ensure each string contains exactly one feature. -/// 2. Change `profile` to `release` if `--release` is set. -fn parse_cargo_args(cargo_args: &CargoArgs) -> CargoArgs { - let mut features = Vec::new(); - - for feature in cargo_args.features.iter() { - for feature in feature.split(',') { - if !feature.is_empty() { - features.push(feature.to_string()); - } - } - } - - let profile = if cargo_args.release { - "release".to_string() - } else { - cargo_args.profile.clone() - }; - - CargoArgs { - profile, - release: cargo_args.release, - features, - } -} - -fn get_feature_strings(cargo_args: &CargoArgs) -> Vec { - cargo_args - .features - .iter() - .map(|feature| format!("--features={}", feature)) - .collect() -} diff --git a/osdk/src/config_manager/qemu.rs b/osdk/src/config_manager/qemu.rs deleted file mode 100644 index 8133aacf..00000000 --- a/osdk/src/config_manager/qemu.rs +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -//! A module about QEMU arguments. - -use std::process; - -use super::unix_args::{apply_kv_array, get_key}; - -use crate::{error::Errno, error_msg}; - -pub fn apply_qemu_args_addition(target: &mut Vec, args: &Vec) { - // check qemu_args - for arg in target.iter() { - check_qemu_arg(arg); - } - for arg in args.iter() { - check_qemu_arg(arg); - } - - apply_kv_array(target, args, " ", MULTI_VALUE_KEYS); -} - -// Below are keys in qemu arguments. The key list is not complete. - -/// Keys with multiple values -const MULTI_VALUE_KEYS: &[&str] = &[ - "-device", "-chardev", "-object", "-netdev", "-drive", "-cdrom", -]; -/// Keys with only single value -const SINGLE_VALUE_KEYS: &[&str] = &["-cpu", "-machine", "-m", "-serial", "-monitor", "-display"]; -/// Keys with no value -const NO_VALUE_KEYS: &[&str] = &["--no-reboot", "-nographic", "-enable-kvm"]; -/// Keys are not allowed to set in configuration files and command line -const NOT_ALLOWED_TO_SET_KEYS: &[&str] = &["-kernel", "-initrd"]; - -fn check_qemu_arg(arg: &str) { - let key = if let Some(key) = get_key(arg, " ") { - key - } else { - arg.to_string() - }; - - if NOT_ALLOWED_TO_SET_KEYS.contains(&key.as_str()) { - error_msg!("`{}` is not allowed to set", arg); - process::exit(Errno::ParseMetadata as _); - } - - if NO_VALUE_KEYS.contains(&key.as_str()) && key.as_str() != arg { - error_msg!("`{}` cannot have value", arg); - process::exit(Errno::ParseMetadata as _); - } - - if (SINGLE_VALUE_KEYS.contains(&key.as_str()) || MULTI_VALUE_KEYS.contains(&key.as_str())) - && key.as_str() == arg - { - error_msg!("`{}` should have value", arg); - process::exit(Errno::ParseMetadata as _); - } -} diff --git a/osdk/src/config_manager/test/OSDK.toml.full b/osdk/src/config_manager/test/OSDK.toml.full deleted file mode 100644 index ab52922e..00000000 --- a/osdk/src/config_manager/test/OSDK.toml.full +++ /dev/null @@ -1,110 +0,0 @@ -[project] -type = "kernel" - -[run] -kcmd_args = [ - "SHELL=/bin/sh", - "LOGNAME=root", - "HOME=/", - "USER=root", - "PATH=/bin:/benchmark", - "init=/usr/bin/busybox", -] -init_args = ["sh", "-l"] -initramfs = "/usr/bin/bash" -boot_protocol = "multiboot2" -bootloader = "grub" -ovmf = "/usr/bin/bash" -opensbi = "/usr/bin/bash" -drive_files = [ - ["/usr/bin/bash", "if=none,format=raw,id=x0"], - ["/usr/bin/bash", "if=none,format=raw,id=x1"], -] -qemu_args = [ - "-machine q35,kernel-irqchip=split", - "-cpu Icelake-Server,+x2apic", - "--no-reboot", - "-m 2G", - "-nographic", - "-serial chardev:mux", - "-monitor chardev:mux", - "-chardev stdio,id=mux,mux=on,signal=off,logfile=qemu.log", - "-display none", - "-device isa-debug-exit,iobase=0xf4,iosize=0x04", - "-object filter-dump,id=filter0,netdev=net01,file=virtio-net.pcap", - "-netdev user,id=net01,hostfwd=tcp::36788-:22,hostfwd=tcp::55834-:8080", - "-device virtio-blk-pci,bus=pcie.0,addr=0x6,drive=x0,serial=vext2,disable-legacy=on,disable-modern=off", - "-device virtio-blk-pci,bus=pcie.0,addr=0x7,drive=x1,serial=vexfat,disable-legacy=on,disable-modern=off", - "-device virtio-keyboard-pci,disable-legacy=on,disable-modern=off", - "-device virtio-net-pci,netdev=net01,disable-legacy=on,disable-modern=off", - "-device virtio-serial-pci,disable-legacy=on,disable-modern=off", - "-device virtconsole,chardev=mux", -] - -['cfg(arch="x86_64", schema="iommu")'.run] -drive_files = [ - ["/usr/bin/bash", "if=none,format=raw,id=x0"], - ["/usr/bin/bash", "if=none,format=raw,id=x1"], -] -qemu_args = [ - "-machine q35,kernel-irqchip=split", - "-cpu Icelake-Server,+x2apic", - "--no-reboot", - "-m 2G", - "-nographic", - "-serial chardev:mux", - "-monitor chardev:mux", - "-chardev stdio,id=mux,mux=on,signal=off,logfile=qemu.log", - "-display none", - "-device isa-debug-exit,iobase=0xf4,iosize=0x04", - "-object filter-dump,id=filter0,netdev=net01,file=virtio-net.pcap", - "-netdev user,id=net01,hostfwd=tcp::36788-:22,hostfwd=tcp::55834-:8080", - "-device virtio-blk-pci,bus=pcie.0,addr=0x6,drive=x0,serial=vext2,disable-legacy=on,disable-modern=off,iommu_platform=on,ats=on", - "-device virtio-blk-pci,bus=pcie.0,addr=0x7,drive=x1,serial=vexfat,disable-legacy=on,disable-modern=off,iommu_platform=on,ats=on", - "-device virtio-keyboard-pci,disable-legacy=on,disable-modern=off,iommu_platform=on,ats=on", - "-device virtio-net-pci,netdev=net01,disable-legacy=on,disable-modern=off,iommu_platform=on,ats=on", - "-device virtio-serial-pci,disable-legacy=on,disable-modern=off,iommu_platform=on,ats=on", - "-device virtconsole,chardev=mux", - "-device intel-iommu,intremap=on,device-iotlb=on", - "-device ioh3420,id=pcie.0,chassis=1", -] - -['cfg(arch="x86_64", schema="microvm")'.run] -bootloader = "qemu" -drive_files = [ - ["/usr/bin/bash", "if=none,format=raw,id=x0"], - ["/usr/bin/bash", "if=none,format=raw,id=x1"], -] -qemu_args = [ - "-machine microvm,rtc=on", - "-cpu Icelake-Server,+x2apic", - "--no-reboot", - "-m 2G", - "-nographic", - "-serial chardev:mux", - "-monitor chardev:mux", - "-chardev stdio,id=mux,mux=on,signal=off,logfile=qemu.log", - "-display none", - "-device isa-debug-exit,iobase=0xf4,iosize=0x04", - "-object filter-dump,id=filter0,netdev=net01,file=virtio-net.pcap", - "-netdev user,id=net01,hostfwd=tcp::36788-:22,hostfwd=tcp::55834-:8080", - "-nodefaults", - "-no-user-config", - "-device virtio-blk-device,drive=x0,serial=vext2", - "-device virtio-blk-device,drive=x1,serial=vexfat", - "-device virtio-keyboard-device", - "-device virtio-net-device,netdev=net01", - "-device virtio-serial-device", - "-device virtconsole,chardev=mux", -] - -['cfg(schema="intel_tdx")'.run] -qemu_exe = "/usr/bin/bash" - -['cfg(arch="riscv64")'.run] -qemu_args = [ - "-machine virt", - "--no-reboot", - "-m 2G", - "-nographic", -] diff --git a/osdk/src/config_manager/test/mod.rs b/osdk/src/config_manager/test/mod.rs deleted file mode 100644 index d7f5c457..00000000 --- a/osdk/src/config_manager/test/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -use super::*; - -#[test] -fn deserialize_toml_manifest() { - let content = include_str!("OSDK.toml.full"); - let toml_manifest: TomlManifest = toml::from_str(content).unwrap(); - assert!(toml_manifest.project.type_ == manifest::ProjectType::Kernel); -} - -#[test] -fn conditional_manifest() { - let toml_manifest: TomlManifest = { - let content = include_str!("OSDK.toml.full"); - toml::from_str(content).unwrap() - }; - let arch = crate::arch::Arch::X86_64; - - // Default schema - let schema: Option = None; - let manifest = toml_manifest.get_osdk_manifest(PathBuf::from("/"), arch, schema); - assert!(manifest.run.unwrap().qemu_args.contains(&String::from( - "-device virtio-blk-pci,bus=pcie.0,addr=0x7,drive=x1,serial=vexfat,disable-legacy=on,disable-modern=off", - ))); - - // Iommu - let schema: Option = Some("iommu".to_owned()); - let manifest = toml_manifest.get_osdk_manifest(PathBuf::from("/"), arch, schema); - assert!(manifest - .run - .unwrap() - .qemu_args - .contains(&String::from("-device ioh3420,id=pcie.0,chassis=1"))); - - // Tdx - let schema: Option = Some("intel_tdx".to_owned()); - let manifest = toml_manifest.get_osdk_manifest(PathBuf::from("/"), arch, schema); - assert_eq!( - manifest.run.unwrap().qemu_exe.unwrap(), - PathBuf::from("/usr/bin/bash") - ); -} diff --git a/osdk/src/main.rs b/osdk/src/main.rs index e88c81b5..9a36ce85 100644 --- a/osdk/src/main.rs +++ b/osdk/src/main.rs @@ -12,7 +12,7 @@ mod base_crate; mod bundle; mod cli; mod commands; -mod config_manager; +mod config; mod error; mod util; diff --git a/osdk/tests/cli/mod.rs b/osdk/tests/cli/mod.rs index ed909baa..f4338f8f 100644 --- a/osdk/tests/cli/mod.rs +++ b/osdk/tests/cli/mod.rs @@ -6,7 +6,7 @@ use crate::util::*; fn cli_help_message() { let output = cargo_osdk(&["-h"]).output().unwrap(); assert_success(&output); - assert_stdout_contains_msg(&output, "cargo osdk "); + assert_stdout_contains_msg(&output, "cargo osdk [OPTIONS] "); } #[test] diff --git a/tools/qemu_args.sh b/tools/qemu_args.sh new file mode 100755 index 00000000..7063157f --- /dev/null +++ b/tools/qemu_args.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# SPDX-License-Identifier: MPL-2.0 + +RAND_PORT_NUM1=$(shuf -i 1024-65535 -n 1) +RAND_PORT_NUM2=$(shuf -i 1024-65535 -n 1) + +echo "Forwarded QEMU guest port: $RAND_PORT_NUM1->22; $RAND_PORT_NUM2->8080" 1>&2 + +COMMON_QEMU_ARGS="\ + -cpu Icelake-Server,+x2apic \ + -smp $SMP \ + -m $MEM \ + --no-reboot \ + -nographic \ + -display none \ + -serial chardev:mux \ + -monitor chardev:mux \ + -chardev stdio,id=mux,mux=on,signal=off,logfile=qemu.log \ + -netdev user,id=net01,hostfwd=tcp::$RAND_PORT_NUM1-:22,hostfwd=tcp::$RAND_PORT_NUM2-:8080 \ + -object filter-dump,id=filter0,netdev=net01,file=virtio-net.pcap \ + -device isa-debug-exit,iobase=0xf4,iosize=0x04 \ + -drive if=none,format=raw,id=x0,file=$EXT2_IMG \ + -drive if=none,format=raw,id=x1,file=$EXFAT_IMG \ +" + +QEMU_ARGS="\ + $COMMON_QEMU_ARGS \ + -machine q35,kernel-irqchip=split \ + -device virtio-blk-pci,bus=pcie.0,addr=0x6,drive=x0,serial=vext2,disable-legacy=on,disable-modern=off$IOMMU_DEV_EXTRA \ + -device virtio-blk-pci,bus=pcie.0,addr=0x7,drive=x1,serial=vexfat,disable-legacy=on,disable-modern=off$IOMMU_DEV_EXTRA \ + -device virtio-keyboard-pci,disable-legacy=on,disable-modern=off$IOMMU_DEV_EXTRA \ + -device virtio-net-pci,netdev=net01,disable-legacy=on,disable-modern=off$IOMMU_DEV_EXTRA \ + -device virtio-serial-pci,disable-legacy=on,disable-modern=off$IOMMU_DEV_EXTRA \ + -device virtconsole,chardev=mux \ + $IOMMU_EXTRA_ARGS \ +" + +MICROVM_QEMU_ARGS="\ + $COMMON_QEMU_ARGS \ + -machine microvm,rtc=on \ + -nodefaults \ + -no-user-config \ + -device virtio-blk-device,drive=x0,serial=vext2 \ + -device virtio-blk-device,drive=x1,serial=vexfat \ + -device virtio-keyboard-device \ + -device virtio-net-device,netdev=net01 \ + -device virtio-serial-device \ + -device virtconsole,chardev=mux \ +" + +if [ "$MICROVM" ]; then + QEMU_ARGS=$MICROVM_QEMU_ARGS + echo $QEMU_ARGS + exit 0 +fi + +if [ "$OVMF_PATH" ]; then + QEMU_ARGS="${QEMU_ARGS}\ + -drive if=pflash,format=raw,unit=0,readonly=on,file=$OVMF_PATH/OVMF_CODE.fd \ + -drive if=pflash,format=raw,unit=1,file=$OVMF_PATH/OVMF_VARS.fd \ + " +fi + +echo $QEMU_ARGS