Implement the next OSDK

This commit is contained in:
Zhang Junyang 2024-04-13 01:08:36 +08:00 committed by Tate, Hongliang Tian
parent 79bdbbe4f9
commit e4c2151566
41 changed files with 1819 additions and 1466 deletions

View File

@ -58,7 +58,7 @@ jobs:
- name: Boot Test (MicroVM) - name: Boot Test (MicroVM)
id: 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) - name: Boot Test (Linux Legacy 32-bit Boot Protocol)
id: boot_test_linux_legacy32 id: boot_test_linux_legacy32
@ -74,7 +74,7 @@ jobs:
- name: Syscall Test at Ext2 (MicroVM) - name: Syscall Test at Ext2 (MicroVM)
id: syscall_test_at_ext2 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 - name: Syscall Test at Exfat
id: syscall_test_at_exfat_linux id: syscall_test_at_exfat_linux
@ -82,4 +82,4 @@ jobs:
- name: Regression Test (MicroVM) - name: Regression Test (MicroVM)
id: regression_test_linux 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

View File

@ -1,38 +1,37 @@
# SPDX-License-Identifier: MPL-2.0 # SPDX-License-Identifier: MPL-2.0
# Project-wide options. # Global options.
ARCH ?= x86_64 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 # The Makefile provides a way to run arbitrary tests in the kernel
# mode using the kernel command line. # mode using the kernel command line.
# Here are the options for the auto test feature. # Here are the options for the auto test feature.
AUTO_TEST ?= none AUTO_TEST ?= none
BOOT_LOADER ?= grub
BOOT_PROTOCOL ?= multiboot2
BUILD_SYSCALL_TEST ?= 0
ENABLE_KVM ?= 1
EXTRA_BLOCKLISTS_DIRS ?= "" EXTRA_BLOCKLISTS_DIRS ?= ""
INTEL_TDX ?= 0
SCHEMA ?= ""
RELEASE_MODE ?= 0
SKIP_GRUB_MENU ?= 1
SYSCALL_TEST_DIR ?= /tmp SYSCALL_TEST_DIR ?= /tmp
# End of auto test features. # End of auto test features.
CARGO_OSDK := ~/.cargo/bin/cargo-osdk CARGO_OSDK := ~/.cargo/bin/cargo-osdk
CARGO_OSDK_ARGS := --arch=$(ARCH) CARGO_OSDK_ARGS := --target-arch=$(ARCH)
ifeq ($(AUTO_TEST), syscall) ifeq ($(AUTO_TEST), syscall)
BUILD_SYSCALL_TEST := 1 BUILD_SYSCALL_TEST := 1
CARGO_OSDK_ARGS += --kcmd_args+="SYSCALL_TEST_DIR=$(SYSCALL_TEST_DIR)" 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 += --kcmd-args="EXTRA_BLOCKLISTS_DIRS=$(EXTRA_BLOCKLISTS_DIRS)"
CARGO_OSDK_ARGS += --init_args+="/opt/syscall_test/run_syscall_test.sh" CARGO_OSDK_ARGS += --init-args="/opt/syscall_test/run_syscall_test.sh"
else ifeq ($(AUTO_TEST), regression) 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) else ifeq ($(AUTO_TEST), boot)
CARGO_OSDK_ARGS += --init_args+="/regression/boot_hello.sh" CARGO_OSDK_ARGS += --init-args="/regression/boot_hello.sh"
endif endif
ifeq ($(RELEASE_MODE), 1) ifeq ($(RELEASE_MODE), 1)
@ -43,21 +42,26 @@ ifeq ($(INTEL_TDX), 1)
CARGO_OSDK_ARGS += --features intel_tdx CARGO_OSDK_ARGS += --features intel_tdx
endif endif
CARGO_OSDK_ARGS += --bootloader="$(BOOT_LOADER)" ifneq ($(SCHEME), "")
CARGO_OSDK_ARGS += --boot_protocol="$(BOOT_PROTOCOL)" CARGO_OSDK_ARGS += --scheme $(SCHEME)
else
ifneq ($(SCHEMA), "") CARGO_OSDK_ARGS += --boot-method="$(BOOT_METHOD)"
CARGO_OSDK_ARGS += --schema $(SCHEMA)
endif endif
# To test the linux-efi-handover64 boot protocol, we need to use Debian's # 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. # GRUB release, which is installed in /usr/bin in our Docker image.
ifeq ($(BOOT_PROTOCOL), linux-efi-handover64) ifeq ($(BOOT_PROTOCOL), linux-efi-handover64)
CARGO_OSDK_ARGS += --grub-mkrescue=/usr/bin/grub-mkrescue 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 endif
ifeq ($(ENABLE_KVM), 1) ifeq ($(ENABLE_KVM), 1)
CARGO_OSDK_ARGS += --qemu_args+="--enable-kvm" CARGO_OSDK_ARGS += --qemu-args="--enable-kvm"
endif endif
# Pass make variables to all subdirectory makes # 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) (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 @rm /tmp/all_crates /tmp/combined_crates
@for dir in $(NON_OSDK_CRATES); do \ @for dir in $(NON_OSDK_CRATES); do \
echo "Checking $$dir"; \
(cd $$dir && cargo clippy -- -D warnings) || exit 1; \ (cd $$dir && cargo clippy -- -D warnings) || exit 1; \
done done
@for dir in $(OSDK_CRATES); do \ @for dir in $(OSDK_CRATES); do \
echo "Checking $$dir"; \
(cd $$dir && cargo osdk clippy -- -- -D warnings) || exit 1; \ (cd $$dir && cargo osdk clippy -- -- -D warnings) || exit 1; \
done done
@make --no-print-directory -C regression check @make --no-print-directory -C regression check

178
OSDK.toml
View File

@ -1,7 +1,19 @@
[project] vars = [
type = "kernel" ["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] [run]
vars = [
["OVMF_PATH", "/usr/share/OVMF"],
]
[run.boot]
kcmd_args = [ kcmd_args = [
"SHELL=/bin/sh", "SHELL=/bin/sh",
"LOGNAME=root", "LOGNAME=root",
@ -12,117 +24,67 @@ kcmd_args = [
] ]
init_args = ["sh", "-l"] init_args = ["sh", "-l"]
initramfs = "regression/build/initramfs.cpio.gz" 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] [test]
boot_protocol = "multiboot" boot.method = "qemu-direct"
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",
]
[grub]
protocol = "multiboot2"
['cfg(arch="x86_64", schema="iommu")'.run] [qemu]
drive_files = [ args = "$(./tools/qemu_args.sh)"
["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",
]
['cfg(arch="x86_64", schema="microvm")'.run] [scheme."microvm"]
bootloader = "qemu" boot.method = "qemu-direct"
drive_files = [ vars = [
["regression/build/ext2.img", "if=none,format=raw,id=x0"], ["MICROVM", "true"],
["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",
] ]
qemu.args = "$(./tools/qemu_args.sh)"
['cfg(arch="riscv64")'.run] [scheme."iommu"]
qemu_args = [ supported_archs = ["x86_64"]
"-machine virt", vars = [
"--no-reboot", ["IOMMU_DEV_EXTRA", ",iommu_platform=on,ats=on"],
"-m 2G", ["IOMMU_EXTRA_ARGS", """\
"-nographic", -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 \
"""

View File

@ -58,7 +58,7 @@ qemu_args = [ # <10>
"-m 2G", "-m 2G",
] ]
['cfg(arch="x86_64", schema=microvm)'.run] # <12> ['cfg(arch="x86_64", scheme=microvm)'.run] # <12>
bootloader = "qemu" bootloader = "qemu"
qemu_args = [ # <10> qemu_args = [ # <10>
"-machine microvm,rtc=on", "-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 Cfg is an advanced feature to create multiple profiles for
the same actions under different scenarios. Currently we 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 The key `arch` has a fixed set of values which is aligned
with the CLI `--arch` argument. If an action has no specified with the CLI `--arch` argument. If an action has no specified
arch, it matches all the architectures. The key `schema` allows arch, it matches all the architectures. The key `scheme` allows
user-defined values and can be selected by the `--schema` CLI user-defined values and can be selected by the `--scheme` CLI
argument. The key `schema` can be used to create special settings argument. The key `scheme` can be used to create special settings
(especially special QEMU configurations). If a cfg action is (especially special QEMU configurations). If a cfg action is
matched, unspecified and required arguments will be inherited matched, unspecified and required arguments will be inherited
from the action that has no cfg (i.e. the default action setting). from the action that has no cfg (i.e. the default action setting).

7
osdk/Cargo.lock generated
View File

@ -135,6 +135,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
"shlex",
"syn", "syn",
"toml", "toml",
] ]
@ -470,6 +471,12 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.0" version = "0.11.0"

View File

@ -19,6 +19,7 @@ quote = "1.0.35"
serde = { version = "1.0.195", features = ["derive"] } serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.111" serde_json = "1.0.111"
sha2 = "0.10.8" sha2 = "0.10.8"
shlex = "1.3.0"
syn = { version = "2.0.52", features = ["extra-traits", "full", "parsing", "printing"] } syn = { version = "2.0.52", features = ["extra-traits", "full", "parsing", "printing"] }
toml = { version = "0.8.8", features = ["preserve_order"] } toml = { version = "0.8.8", features = ["preserve_order"] }

View File

@ -12,8 +12,11 @@ use std::fmt::{self, Display, Formatter};
/// <https://doc.rust-lang.org/reference/conditional-compilation.html#target_arch> /// <https://doc.rust-lang.org/reference/conditional-compilation.html#target_arch>
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum Arch { pub enum Arch {
#[serde(rename = "aarch64")]
Aarch64, Aarch64,
#[serde(rename = "riscv64")]
X86_64, X86_64,
#[serde(rename = "x86_64")]
RiscV64, RiscV64,
} }
@ -33,11 +36,19 @@ impl ValueEnum for Arch {
impl Arch { impl Arch {
/// Get the target triple for the architecture. /// Get the target triple for the architecture.
pub fn triple(&self) -> String { pub fn triple(&self) -> &'static str {
match self { match self {
Arch::Aarch64 => "aarch64-unknown-none".to_owned(), Arch::Aarch64 => "aarch64-unknown-none",
Arch::RiscV64 => "riscv64gc-unknown-none-elf".to_owned(), Arch::RiscV64 => "riscv64gc-unknown-none-elf",
Arch::X86_64 => "x86_64-unknown-none".to_owned(), 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",
} }
} }

View File

@ -7,7 +7,7 @@ pub mod vm_image;
use bin::AsterBin; use bin::AsterBin;
use file::{BundleFile, Initramfs}; use file::{BundleFile, Initramfs};
use std::process; use std::process;
use vm_image::AsterVmImage; use vm_image::{AsterVmImage, AsterVmImageType};
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -16,11 +16,9 @@ use std::{
}; };
use crate::{ use crate::{
arch::Arch, config::{
cli::CargoArgs, scheme::{ActionChoice, BootMethod},
config_manager::{ Config,
action::{ActionSettings, Bootloader},
RunConfig,
}, },
error::Errno, error::Errno,
error_msg, error_msg,
@ -42,16 +40,20 @@ pub struct BundleManifest {
pub initramfs: Option<Initramfs>, pub initramfs: Option<Initramfs>,
pub aster_bin: Option<AsterBin>, pub aster_bin: Option<AsterBin>,
pub vm_image: Option<AsterVmImage>, pub vm_image: Option<AsterVmImage>,
pub settings: ActionSettings, pub config: Config,
pub cargo_args: CargoArgs, pub action: ActionChoice,
pub last_modified: SystemTime, pub last_modified: SystemTime,
} }
impl Bundle { impl Bundle {
/// This function creates a new `Bundle` without adding any files. /// This function creates a new `Bundle` without adding any files.
pub fn new(path: impl AsRef<Path>, settings: ActionSettings, cargo_args: CargoArgs) -> Self { pub fn new(path: impl AsRef<Path>, config: &Config, action: ActionChoice) -> Self {
std::fs::create_dir_all(path.as_ref()).unwrap(); 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() { if !initramfs.exists() {
error_msg!("initramfs file not found: {}", initramfs.display()); error_msg!("initramfs file not found: {}", initramfs.display());
process::exit(Errno::BuildCrate as _); process::exit(Errno::BuildCrate as _);
@ -65,8 +67,8 @@ impl Bundle {
initramfs, initramfs,
aster_bin: None, aster_bin: None,
vm_image: None, vm_image: None,
settings, config: config.clone(),
cargo_args, action,
last_modified: SystemTime::now(), last_modified: SystemTime::now(),
}, },
path: path.as_ref().to_path_buf(), path: path.as_ref().to_path_buf(),
@ -109,105 +111,139 @@ impl Bundle {
}) })
} }
pub fn can_run_with_config(&self, config: &RunConfig) -> bool { pub fn can_run_with_config(&self, config: &Config, action: ActionChoice) -> Result<(), String> {
// Compare the manifest with the run configuration. // If built for testing, better not to run it. Vice versa.
// TODO: This pairwise comparison will result in some false negatives. We may if self.manifest.action != action {
// fix it by pondering upon each fields with more care. return Err(format!(
if self.manifest.settings != config.settings "The bundle is built for {:?}",
|| self.manifest.cargo_args != config.cargo_args 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. // 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)) => { (Some(initramfs), Some(initramfs_path)) => {
let config_initramfs = Initramfs::new(initramfs_path); let config_initramfs = Initramfs::new(initramfs_path);
if initramfs.sha256sum() != config_initramfs.sha256sum() { if initramfs.sha256sum() != config_initramfs.sha256sum() {
return false; return Err(initramfs_err);
} }
} }
(None, None) => {} (None, None) => {}
_ => { _ => {
return false; return Err(initramfs_err);
} }
}; };
true Ok(())
} }
pub fn last_modified_time(&self) -> SystemTime { pub fn last_modified_time(&self) -> SystemTime {
self.manifest.last_modified self.manifest.last_modified
} }
pub fn run(&self, config: &RunConfig) { pub fn run(&self, config: &Config, action: ActionChoice) {
if !self.can_run_with_config(config) { match self.can_run_with_config(config, action) {
error_msg!("The bundle is not compatible with the run configuration"); Ok(()) => {}
std::process::exit(Errno::RunBundle as _); Err(msg) => {
} error_msg!("{}", msg);
let mut qemu_cmd = Command::new(config.settings.qemu_exe.clone().unwrap_or_else(|| { std::process::exit(Errno::RunBundle as _);
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);
} }
} }
match config.settings.bootloader { let action = match action {
Some(Bootloader::Qemu) => { ActionChoice::Run => &config.run,
let Some(ref aster_bin) = self.manifest.aster_bin else { ActionChoice::Test => &config.test,
error_msg!("Kernel ELF binary is required for direct QEMU booting"); };
std::process::exit(Errno::RunBundle as _); 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 qemu_cmd
.arg("-kernel") .arg("-kernel")
.arg(self.path.join(aster_bin.path())); .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); qemu_cmd.arg("-initrd").arg(initramfs);
} else { } else {
info!("No initramfs specified"); info!("No initramfs specified");
}; };
qemu_cmd qemu_cmd.arg("-append").arg(action.boot.kcmdline.join(" "));
.arg("-append")
.arg(config.settings.combined_kcmd_args().join(" "));
} }
Some(Bootloader::Grub) => { BootMethod::GrubRescueIso => {
let Some(ref vm_image) = self.manifest.vm_image else { let vm_image = self.manifest.vm_image.as_ref().unwrap();
error_msg!("VM image is required for QEMU booting"); assert!(matches!(vm_image.typ(), AsterVmImageType::GrubIso(_)));
std::process::exit(Errno::RunBundle as _);
};
qemu_cmd.arg("-cdrom").arg(self.path.join(vm_image.path())); 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 => { BootMethod::GrubQcow2 => {
error_msg!("Bootloader is required for QEMU booting"); let vm_image = self.manifest.vm_image.as_ref().unwrap();
std::process::exit(Errno::RunBundle as _); 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 { info!("Running QEMU: {:#?}", qemu_cmd);
qemu_cmd.arg("-drive").arg(format!(
"file={},{}",
drive_file.path.display(),
drive_file.append,
));
}
let exit_status = qemu_cmd.status().unwrap(); let exit_status = qemu_cmd.status().unwrap();
if !exit_status.success() { if !exit_status.success() {

View File

@ -18,7 +18,7 @@ pub struct AsterVmImage {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AsterVmImageType { pub enum AsterVmImageType {
GrubIso(AsterGrubIsoImageMeta), GrubIso(AsterGrubIsoImageMeta),
// TODO: add more vm image types such as qcow2, etc. Qcow2(AsterQcow2ImageMeta),
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -26,6 +26,11 @@ pub struct AsterGrubIsoImageMeta {
pub grub_version: String, pub grub_version: String,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AsterQcow2ImageMeta {
pub grub_version: String,
}
impl BundleFile for AsterVmImage { impl BundleFile for AsterVmImage {
fn path(&self) -> &PathBuf { fn path(&self) -> &PathBuf {
&self.path &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. /// Move the binary to the `base` directory and convert the path to a relative path.
pub fn move_to(self, base: impl AsRef<Path>) -> Self { pub fn move_to(self, base: impl AsRef<Path>) -> Self {
let file_name = self.path.file_name().unwrap(); let file_name = self.path.file_name().unwrap();

View File

@ -10,37 +10,40 @@ use crate::{
execute_build_command, execute_debug_command, execute_forwarded_command, execute_build_command, execute_debug_command, execute_forwarded_command,
execute_new_command, execute_run_command, execute_test_command, execute_new_command, execute_run_command, execute_test_command,
}, },
config_manager::{ config::{
action::{BootProtocol, Bootloader}, manifest::{ProjectType, TomlManifest},
manifest::ProjectType, scheme::{BootMethod, BootProtocol},
BuildConfig, DebugConfig, RunConfig, TestConfig, Config,
}, },
}; };
pub fn main() { pub fn main() {
let osdk_subcommand = match Cli::parse() { let (osdk_subcommand, common_args) = match Cli::parse() {
Cli { Cli {
cargo_subcommand: CargoSubcommand::Osdk(osdk_subcommand), 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 { match &osdk_subcommand {
OsdkSubcommand::New(args) => execute_new_command(args), OsdkSubcommand::New(args) => execute_new_command(args),
OsdkSubcommand::Build(build_args) => { OsdkSubcommand::Build(build_args) => {
let build_config = BuildConfig::parse(build_args); execute_build_command(&load_config(), build_args);
execute_build_command(&build_config);
} }
OsdkSubcommand::Run(run_args) => { OsdkSubcommand::Run(run_args) => {
let run_config = RunConfig::parse(run_args); execute_run_command(&load_config(), &run_args.gdb_server_args);
execute_run_command(&run_config);
} }
OsdkSubcommand::Debug(debug_args) => { OsdkSubcommand::Debug(debug_args) => {
let debug_config = DebugConfig::parse(debug_args); execute_debug_command(&load_config().run.build.profile, debug_args);
execute_debug_command(&debug_config);
} }
OsdkSubcommand::Test(test_args) => { OsdkSubcommand::Test(test_args) => {
let test_config = TestConfig::parse(test_args); execute_test_command(&load_config(), test_args);
execute_test_command(&test_config);
} }
OsdkSubcommand::Check(args) => execute_forwarded_command("check", &args.args), OsdkSubcommand::Check(args) => execute_forwarded_command("check", &args.args),
OsdkSubcommand::Clippy(args) => execute_forwarded_command("clippy", &args.args), OsdkSubcommand::Clippy(args) => execute_forwarded_command("clippy", &args.args),
@ -54,6 +57,8 @@ pub fn main() {
pub struct Cli { pub struct Cli {
#[clap(subcommand)] #[clap(subcommand)]
cargo_subcommand: CargoSubcommand, cargo_subcommand: CargoSubcommand,
#[command(flatten)]
common_args: CommonArgs,
} }
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
@ -136,18 +141,23 @@ impl NewArgs {
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub struct BuildArgs { pub struct BuildArgs {
#[command(flatten)] #[arg(
pub cargo_args: CargoArgs, long = "for-test",
#[command(flatten)] help = "Build for running unit tests",
pub osdk_args: OsdkArgs, 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<PathBuf>,
} }
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub struct RunArgs { pub struct RunArgs {
#[command(flatten)]
pub cargo_args: CargoArgs,
#[command(flatten)]
pub osdk_args: OsdkArgs,
#[command(flatten)] #[command(flatten)]
pub gdb_server_args: GdbServerArgs, pub gdb_server_args: GdbServerArgs,
} }
@ -181,10 +191,6 @@ pub struct GdbServerArgs {
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub struct DebugArgs { pub struct DebugArgs {
#[command(flatten)]
pub cargo_args: CargoArgs,
#[command(flatten)]
pub osdk_args: OsdkArgs,
#[arg( #[arg(
long, long,
help = "Specify the address of the remote target", help = "Specify the address of the remote target",
@ -195,15 +201,11 @@ pub struct DebugArgs {
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub struct TestArgs { pub struct TestArgs {
#[command(flatten)]
pub cargo_args: CargoArgs,
#[arg( #[arg(
name = "TESTNAME", name = "TESTNAME",
help = "Only run tests containing this string in their names" help = "Only run tests containing this string in their names"
)] )]
pub test_name: Option<String>, pub test_name: Option<String>,
#[command(flatten)]
pub osdk_args: OsdkArgs,
} }
#[derive(Debug, Args, Default, Clone, Eq, PartialEq, Serialize, Deserialize)] #[derive(Debug, Args, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
@ -211,79 +213,120 @@ pub struct CargoArgs {
#[arg( #[arg(
long, long,
help = "The Cargo build profile (built-in candidates are 'dev', 'release', 'test' and 'bench')", 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<String>,
#[arg( #[arg(
long, long,
help = "Build artifacts in release mode", help = "Build artifacts in release mode",
conflicts_with = "profile" conflicts_with = "profile",
global = true
)] )]
pub release: bool, 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<String>, pub features: Vec<String>,
} }
impl CargoArgs {
pub fn profile(&self) -> Option<String> {
if self.release {
Some("release".to_owned())
} else {
self.profile.clone()
}
}
}
#[derive(Debug, Args)] #[derive(Debug, Args)]
pub struct OsdkArgs { pub struct CommonArgs {
#[arg(long, value_name = "ARCH", help = "The architecture to build for")] #[command(flatten)]
pub arch: Option<Arch>, pub build_args: CargoArgs,
#[arg( #[arg(
long = "schema", long = "linux-x86-legacy-boot",
help = "Select the specific configuration schema provided in the OSDK manifest", help = "Enable legacy 32-bit boot support for the Linux x86 boot protocol",
value_name = "SCHEMA" global = true
)] )]
pub schema: Option<String>, pub linux_x86_legacy_boot: bool,
#[arg( #[arg(
long = "kcmd_args+", long = "target-arch",
value_name = "ARCH",
help = "The architecture to build for",
global = true
)]
pub target_arch: Option<Arch>,
#[arg(
long = "scheme",
help = "Select the specific configuration scheme provided in the OSDK manifest",
value_name = "SCHEME",
global = true
)]
pub scheme: Option<String>,
#[arg(
long = "kcmd-args",
require_equals = true, require_equals = true,
help = "Extra or overriding command line arguments for guest kernel", help = "Extra or overriding command line arguments for guest kernel",
value_name = "ARGS" value_name = "ARGS",
global = true
)] )]
pub kcmd_args: Vec<String>, pub kcmd_args: Vec<String>,
#[arg( #[arg(
long = "init_args+", long = "init-args",
require_equals = true, require_equals = true,
help = "Extra command line arguments for init process", help = "Extra command line arguments for init process",
value_name = "ARGS" value_name = "ARGS",
global = true
)] )]
pub init_args: Vec<String>, pub init_args: Vec<String>,
#[arg(long, help = "Path of initramfs", value_name = "PATH")] #[arg(long, help = "Path of initramfs", value_name = "PATH", global = true)]
pub initramfs: Option<PathBuf>, pub initramfs: Option<PathBuf>,
#[arg(long = "ovmf", help = "Path of OVMF", value_name = "PATH")]
pub ovmf: Option<PathBuf>,
#[arg(long = "opensbi", help = "Path of OpenSBI", value_name = "PATH")]
pub opensbi: Option<PathBuf>,
#[arg( #[arg(
long = "bootloader", long = "boot-method",
help = "Loader for booting the kernel", help = "Loader for booting the kernel",
value_name = "BOOTLOADER" value_name = "BOOTMETHOD",
global = true
)] )]
pub bootloader: Option<Bootloader>, pub boot_method: Option<BootMethod>,
#[arg(
long = "display-grub-menu",
help = "Display the GRUB menu if booting with GRUB",
global = true
)]
pub display_grub_menu: bool,
#[arg( #[arg(
long = "grub-mkrescue", long = "grub-mkrescue",
help = "Path of grub-mkrescue", help = "Path of grub-mkrescue",
value_name = "PATH" value_name = "PATH",
global = true
)] )]
pub grub_mkrescue: Option<PathBuf>, pub grub_mkrescue: Option<PathBuf>,
#[arg( #[arg(
long = "boot_protocol", long = "grub-boot-protocol",
help = "Protocol for booting the kernel", help = "Protocol for booting the kernel",
value_name = "BOOT_PROTOCOL" value_name = "BOOT_PROTOCOL",
global = true
)] )]
pub boot_protocol: Option<BootProtocol>, pub grub_boot_protocol: Option<BootProtocol>,
#[arg( #[arg(
long = "qemu_exe", long = "qemu-exe",
help = "The QEMU executable file", help = "The QEMU executable file",
value_name = "FILE" value_name = "FILE",
global = true
)] )]
pub qemu_exe: Option<PathBuf>, pub qemu_exe: Option<PathBuf>,
#[arg( #[arg(
long = "qemu_args+", long = "qemu-args",
require_equals = true, require_equals = true,
help = "Extra arguments or overriding arguments for running QEMU", help = "Extra arguments or overriding arguments for running QEMU",
value_name = "ARGS" value_name = "ARGS",
global = true
)] )]
pub qemu_args_add: Vec<String>, pub qemu_args: Vec<String>,
} }

View File

@ -14,7 +14,6 @@ use crate::{
bin::{AsterBin, AsterBinType, AsterBzImageMeta, AsterElfMeta}, bin::{AsterBin, AsterBinType, AsterBzImageMeta, AsterElfMeta},
file::BundleFile, file::BundleFile,
}, },
config_manager::action::BootProtocol,
util::get_current_crate_info, util::get_current_crate_info,
}; };
@ -22,13 +21,13 @@ pub fn make_install_bzimage(
install_dir: impl AsRef<Path>, install_dir: impl AsRef<Path>,
target_dir: impl AsRef<Path>, target_dir: impl AsRef<Path>,
aster_elf: &AsterBin, aster_elf: &AsterBin,
protocol: &BootProtocol, linux_x86_legacy_boot: bool,
) -> AsterBin { ) -> AsterBin {
let target_name = get_current_crate_info().name; let target_name = get_current_crate_info().name;
let image_type = match protocol { let image_type = if linux_x86_legacy_boot {
BootProtocol::LinuxLegacy32 => BzImageType::Legacy32, BzImageType::Legacy32
BootProtocol::LinuxEfiHandover64 => BzImageType::Efi64, } else {
_ => unreachable!(), BzImageType::Efi64
}; };
let setup_bin = { let setup_bin = {
let setup_install_dir = target_dir.as_ref(); let setup_install_dir = target_dir.as_ref();
@ -60,9 +59,9 @@ pub fn make_install_bzimage(
AsterBin::new( AsterBin::new(
&install_path, &install_path,
AsterBinType::BzImage(AsterBzImageMeta { AsterBinType::BzImage(AsterBzImageMeta {
support_legacy32_boot: matches!(protocol, BootProtocol::LinuxLegacy32), support_legacy32_boot: linux_x86_legacy_boot,
support_efi_boot: false, support_efi_boot: false,
support_efi_handover: matches!(protocol, BootProtocol::LinuxEfiHandover64), support_efi_handover: !linux_x86_legacy_boot,
}), }),
aster_elf.version().clone(), aster_elf.version().clone(),
aster_elf.stripped(), aster_elf.stripped(),

View File

@ -12,7 +12,10 @@ use crate::{
file::BundleFile, file::BundleFile,
vm_image::{AsterGrubIsoImageMeta, AsterVmImage, AsterVmImageType}, vm_image::{AsterGrubIsoImageMeta, AsterVmImage, AsterVmImageType},
}, },
config_manager::{action::BootProtocol, BuildConfig}, config::{
scheme::{ActionChoice, BootProtocol},
Config,
},
util::get_current_crate_info, util::get_current_crate_info,
}; };
@ -20,11 +23,16 @@ pub fn create_bootdev_image(
target_dir: impl AsRef<Path>, target_dir: impl AsRef<Path>,
aster_bin: &AsterBin, aster_bin: &AsterBin,
initramfs_path: Option<impl AsRef<Path>>, initramfs_path: Option<impl AsRef<Path>>,
config: &BuildConfig, config: &Config,
action: ActionChoice,
) -> AsterVmImage { ) -> AsterVmImage {
let target_name = get_current_crate_info().name; let target_name = get_current_crate_info().name;
let iso_root = &target_dir.as_ref().join("iso_root"); 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. // Clear or make the iso dir.
if iso_root.exists() { if iso_root.exists() {
@ -43,12 +51,12 @@ pub fn create_bootdev_image(
// Make the kernel image and place it in the boot directory. // Make the kernel image and place it in the boot directory.
match protocol { match protocol {
Some(BootProtocol::LinuxLegacy32) | Some(BootProtocol::LinuxEfiHandover64) => { BootProtocol::Linux => {
make_install_bzimage( make_install_bzimage(
iso_root.join("boot"), iso_root.join("boot"),
&target_dir, &target_dir,
aster_bin, aster_bin,
&protocol.clone().unwrap(), action.build.linux_x86_legacy_boot,
); );
} }
_ => { _ => {
@ -65,22 +73,17 @@ pub fn create_bootdev_image(
None None
}; };
let grub_cfg = generate_grub_cfg( let grub_cfg = generate_grub_cfg(
&config.settings.combined_kcmd_args().join(" "), &action.boot.kcmdline.join(" "),
true, !action.grub.display_grub_menu,
initramfs_in_image, initramfs_in_image,
&protocol.clone().unwrap_or(BootProtocol::Multiboot2), protocol,
); );
let grub_cfg_path = iso_root.join("boot").join("grub").join("grub.cfg"); let grub_cfg_path = iso_root.join("boot").join("grub").join("grub.cfg");
fs::write(grub_cfg_path, grub_cfg).unwrap(); fs::write(grub_cfg_path, grub_cfg).unwrap();
// Make the boot device CDROM image using `grub-mkrescue`. // Make the boot device CDROM image using `grub-mkrescue`.
let iso_path = &target_dir.as_ref().join(target_name.to_string() + ".iso"); let iso_path = &target_dir.as_ref().join(target_name.to_string() + ".iso");
let grub_mkrescue_bin = &config let mut grub_mkrescue_cmd = std::process::Command::new(action.grub.grub_mkrescue.as_os_str());
.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());
grub_mkrescue_cmd grub_mkrescue_cmd
.arg(iso_root.as_os_str()) .arg(iso_root.as_os_str())
.arg("-o") .arg("-o")
@ -92,7 +95,7 @@ pub fn create_bootdev_image(
AsterVmImage::new( AsterVmImage::new(
iso_path, iso_path,
AsterVmImageType::GrubIso(AsterGrubIsoImageMeta { 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(), aster_bin.version().clone(),
) )
@ -115,7 +118,7 @@ fn generate_grub_cfg(
"#GRUB_TIMEOUT_STYLE#", "#GRUB_TIMEOUT_STYLE#",
if skip_grub_menu { "hidden" } else { "menu" }, 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. // Replace all occurrences of "#KERNEL_COMMAND_LINE#" with the desired value.
let grub_cfg = grub_cfg.replace("#KERNEL_COMMAND_LINE#", kcmdline); let grub_cfg = grub_cfg.replace("#KERNEL_COMMAND_LINE#", kcmdline);
// Replace the grub commands according to the protocol selected. // Replace the grub commands according to the protocol selected.
@ -147,7 +150,7 @@ fn generate_grub_cfg(
"".to_owned() "".to_owned()
}, },
), ),
BootProtocol::LinuxLegacy32 | BootProtocol::LinuxEfiHandover64 => grub_cfg BootProtocol::Linux => grub_cfg
.replace("#GRUB_CMD_KERNEL#", "linux") .replace("#GRUB_CMD_KERNEL#", "linux")
.replace("#KERNEL#", &aster_bin_path_on_device) .replace("#KERNEL#", &aster_bin_path_on_device)
.replace( .replace(

View File

@ -3,7 +3,12 @@
mod bin; mod bin;
mod grub; 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; use bin::strip_elf_for_qemu;
@ -15,39 +20,53 @@ use crate::{
bin::{AsterBin, AsterBinType, AsterElfMeta}, bin::{AsterBin, AsterBinType, AsterElfMeta},
Bundle, Bundle,
}, },
cli::CargoArgs, cli::BuildArgs,
config_manager::{action::Bootloader, BuildConfig}, config::{
scheme::{ActionChoice, BootMethod},
Config,
},
error::Errno, error::Errno,
error_msg, 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) { pub fn execute_build_command(config: &Config, build_args: &BuildArgs) {
let ws_target_directory = get_target_directory(); let cargo_target_directory = get_target_directory();
let osdk_target_directory = ws_target_directory.join(DEFAULT_TARGET_RELPATH); let osdk_output_directory = build_args
if !osdk_target_directory.exists() { .output
std::fs::create_dir_all(&osdk_target_directory).unwrap(); .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 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, bundle_path,
&osdk_target_directory, &osdk_output_directory,
&ws_target_directory, &cargo_target_directory,
config, config,
action,
&[], &[],
); );
} }
pub fn create_base_and_build( pub fn create_base_and_cached_build(
bundle_path: impl AsRef<Path>, bundle_path: impl AsRef<Path>,
osdk_target_directory: impl AsRef<Path>, osdk_output_directory: impl AsRef<Path>,
cargo_target_directory: impl AsRef<Path>, cargo_target_directory: impl AsRef<Path>,
config: &BuildConfig, config: &Config,
action: ActionChoice,
rustflags: &[&str], rustflags: &[&str],
) -> Bundle { ) -> 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( new_base_crate(
&base_crate_path, &base_crate_path,
&get_current_crate_info().name, &get_current_crate_info().name,
@ -55,55 +74,108 @@ pub fn create_base_and_build(
); );
let original_dir = std::env::current_dir().unwrap(); let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&base_crate_path).unwrap(); std::env::set_current_dir(&base_crate_path).unwrap();
let bundle = do_build( let bundle = do_cached_build(
&bundle_path, &bundle_path,
&osdk_target_directory, &osdk_output_directory,
&cargo_target_directory, &cargo_target_directory,
config, config,
action,
rustflags, rustflags,
); );
std::env::set_current_dir(original_dir).unwrap(); std::env::set_current_dir(original_dir).unwrap();
bundle 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<Path>,
osdk_output_directory: impl AsRef<Path>,
cargo_target_directory: impl AsRef<Path>,
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( pub fn do_build(
bundle_path: impl AsRef<Path>, bundle_path: impl AsRef<Path>,
osdk_target_directory: impl AsRef<Path>, osdk_output_directory: impl AsRef<Path>,
cargo_target_directory: impl AsRef<Path>, cargo_target_directory: impl AsRef<Path>,
config: &BuildConfig, config: &Config,
action: ActionChoice,
rustflags: &[&str], rustflags: &[&str],
) -> Bundle { ) -> Bundle {
if bundle_path.as_ref().exists() { if bundle_path.as_ref().exists() {
std::fs::remove_dir_all(&bundle_path).unwrap(); std::fs::remove_dir_all(&bundle_path).unwrap();
} }
let mut bundle = Bundle::new( let mut bundle = Bundle::new(&bundle_path, config, action);
&bundle_path,
config.settings.clone(),
config.cargo_args.clone(),
);
info!("Building kernel ELF"); info!("Building kernel ELF");
let aster_elf = build_kernel_elf( let aster_elf = build_kernel_elf(
&config.arch, &config.target_arch,
&config.cargo_args, &config.build.profile,
&config.build.features[..],
&cargo_target_directory, &cargo_target_directory,
rustflags, rustflags,
); );
if matches!(config.settings.bootloader, Some(Bootloader::Qemu)) { let boot = match action {
let stripped_elf = strip_elf_for_qemu(&osdk_target_directory, &aster_elf); ActionChoice::Run => &config.run.boot,
bundle.consume_aster_bin(stripped_elf); ActionChoice::Test => &config.test.boot,
} };
if matches!(config.settings.bootloader, Some(Bootloader::Grub)) { match boot.method {
info!("Building boot device image"); BootMethod::GrubRescueIso => {
let bootdev_image = grub::create_bootdev_image( info!("Building boot device image");
&osdk_target_directory, let bootdev_image = grub::create_bootdev_image(
&aster_elf, &osdk_output_directory,
config.settings.initramfs.as_ref(), &aster_elf,
config, boot.initramfs.as_ref(),
); config,
bundle.consume_vm_image(bootdev_image); 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 bundle
@ -111,7 +183,8 @@ pub fn do_build(
fn build_kernel_elf( fn build_kernel_elf(
arch: &Arch, arch: &Arch,
cargo_args: &CargoArgs, profile: &str,
features: &[String],
cargo_target_directory: impl AsRef<Path>, cargo_target_directory: impl AsRef<Path>,
rustflags: &[&str], rustflags: &[&str],
) -> AsterBin { ) -> AsterBin {
@ -135,12 +208,13 @@ fn build_kernel_elf(
command.env_remove("RUSTUP_TOOLCHAIN"); command.env_remove("RUSTUP_TOOLCHAIN");
command.env("RUSTFLAGS", rustflags.join(" ")); command.env("RUSTFLAGS", rustflags.join(" "));
command.arg("build"); command.arg("build");
command.arg("--features").arg(features.join(" "));
command.arg("--target").arg(&target_os_string); command.arg("--target").arg(&target_os_string);
command command
.arg("--target-dir") .arg("--target-dir")
.arg(cargo_target_directory.as_ref()); .arg(cargo_target_directory.as_ref());
command.args(COMMON_CARGO_ARGS); command.args(COMMON_CARGO_ARGS);
command.arg("--profile=".to_string() + &cargo_args.profile); command.arg("--profile=".to_string() + profile);
let status = command.status().unwrap(); let status = command.status().unwrap();
if !status.success() { if !status.success() {
error_msg!("Cargo build failed"); 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 = 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") aster_bin_path.join("debug")
} else { } else {
aster_bin_path.join(&cargo_args.profile) aster_bin_path.join(profile)
} }
.join(get_current_crate_info().name); .join(get_current_crate_info().name);
@ -167,3 +241,21 @@ fn build_kernel_elf(
false, false,
) )
} }
fn get_last_modified_time(path: impl AsRef<Path>) -> 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
}

View File

@ -1,15 +1,13 @@
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
use crate::commands::util::{bin_file_name, profile_adapter}; use crate::commands::util::bin_file_name;
use crate::config_manager::DebugConfig;
use crate::util::get_target_directory; use crate::{cli::DebugArgs, util::get_target_directory};
use std::process::Command; use std::process::Command;
pub fn execute_debug_command(config: &DebugConfig) { pub fn execute_debug_command(profile: &String, args: &DebugArgs) {
let DebugConfig { cargo_args, remote } = config; let remote = &args.remote;
let profile = profile_adapter(&cargo_args.profile);
let file_path = get_target_directory() let file_path = get_target_directory()
.join("x86_64-unknown-none") .join("x86_64-unknown-none")
.join(profile) .join(profile)

View File

@ -19,12 +19,11 @@ use crate::arch::get_default_arch;
/// Execute the forwarded cargo command with args containing the subcommand and its arguments. /// Execute the forwarded cargo command with args containing the subcommand and its arguments.
pub fn execute_forwarded_command(subcommand: &str, args: &Vec<String>) -> ! { pub fn execute_forwarded_command(subcommand: &str, args: &Vec<String>) -> ! {
let mut cargo = util::cargo(); let mut cargo = util::cargo();
cargo cargo.arg(subcommand).args(util::COMMON_CARGO_ARGS);
.arg(subcommand) if !args.contains(&"--target".to_owned()) {
.args(util::COMMON_CARGO_ARGS) cargo.arg("--target").arg(get_default_arch().triple());
.arg("--target") }
.arg(get_default_arch().triple()) cargo.args(args);
.args(args);
let status = cargo.status().expect("Failed to execute cargo"); let status = cargo.status().expect("Failed to execute cargo");
std::process::exit(status.code().unwrap_or(1)); std::process::exit(status.code().unwrap_or(1));
} }

View File

@ -1,18 +1,25 @@
[project] project_type = "kernel"
type = "kernel"
[run] vars = [
bootloader = "grub" ["OVMF_PATH", "/usr/share/OVMF"],
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",
] ]
[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 \
"""

View File

@ -1,17 +1,19 @@
[project] project_type = "lib"
type = "library"
[test] [boot]
bootloader = "qemu" method = "qemu-direct"
qemu_args = [
"-machine q35,kernel-irqchip=split", [qemu]
"-cpu Icelake-Server,+x2apic", args = """\
"--no-reboot", -machine q35,kernel-irqchip=split \
"-m 2G", -cpu Icelake-Server,+x2apic \
"-nographic", --no-reboot \
"-serial chardev:mux", -m 2G \
"-monitor chardev:mux", -smp 1 \
"-chardev stdio,id=mux,mux=on,signal=off", -nographic \
"-display none", -serial chardev:mux \
"-device isa-debug-exit,iobase=0xf4,iosize=0x04", -monitor chardev:mux \
] -chardev stdio,id=mux,mux=on,signal=off \
-display none \
-device isa-debug-exit,iobase=0xf4,iosize=0x04 \
"""

View File

@ -4,7 +4,7 @@ use std::{fs, path::PathBuf, process, str::FromStr};
use crate::{ use crate::{
cli::NewArgs, cli::NewArgs,
config_manager::manifest::ProjectType, config::manifest::ProjectType,
error::Errno, error::Errno,
error_msg, error_msg,
util::{aster_crate_dep, cargo_new_lib, get_cargo_metadata}, util::{aster_crate_dep, cargo_new_lib, get_cargo_metadata},

View File

@ -1,19 +1,14 @@
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
use std::{ use super::{build::create_base_and_cached_build, util::DEFAULT_TARGET_RELPATH};
path::{Path, PathBuf},
time::{Duration, SystemTime},
};
use super::{build::create_base_and_build, util::DEFAULT_TARGET_RELPATH};
use crate::{ use crate::{
bundle::Bundle, cli::GdbServerArgs,
config_manager::{BuildConfig, RunConfig}, config::{scheme::ActionChoice, Config},
util::{get_cargo_metadata, get_current_crate_info, get_target_directory}, util::{get_current_crate_info, get_target_directory},
}; };
pub fn execute_run_command(config: &RunConfig) { pub fn execute_run_command(config: &Config, gdb_server_args: &GdbServerArgs) {
if config.gdb_server_args.is_gdb_enabled { if gdb_server_args.is_gdb_enabled {
use std::env; use std::env;
env::set_var( env::set_var(
"RUSTFLAGS", "RUSTFLAGS",
@ -21,111 +16,48 @@ pub fn execute_run_command(config: &RunConfig) {
); );
} }
let ws_target_directory = get_target_directory(); let cargo_target_directory = get_target_directory();
let osdk_target_directory = ws_target_directory.join(DEFAULT_TARGET_RELPATH); let osdk_output_directory = cargo_target_directory.join(DEFAULT_TARGET_RELPATH);
let target_name = get_current_crate_info().name; 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 { let mut config = config.clone();
settings: { if gdb_server_args.is_gdb_enabled {
if config.gdb_server_args.is_gdb_enabled { let qemu_gdb_args = {
let qemu_gdb_args: Vec<_> = { let gdb_stub_addr = gdb_server_args.gdb_server_addr.as_str();
let gdb_stub_addr = config.gdb_server_args.gdb_server_addr.as_str(); match gdb::stub_type_of(gdb_stub_addr) {
match gdb::stub_type_of(gdb_stub_addr) { gdb::StubAddrType::Unix => {
gdb::StubAddrType::Unix => { format!(
let chardev = format!( " -chardev socket,path={},server=on,wait=off,id=gdb0 -gdb chardev:gdb0 -S",
"-chardev socket,path={},server=on,wait=off,id=gdb0", gdb_stub_addr
gdb_stub_addr )
); }
let stub = "-gdb chardev:gdb0".to_owned(); gdb::StubAddrType::Tcp => {
vec![chardev, stub, "-S".into()] format!(
} " -gdb tcp:{} -S",
gdb::StubAddrType::Tcp => { gdb::tcp_addr_util::format_tcp_addr(gdb_stub_addr)
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;
}
} }
} }
} };
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 { let default_bundle_directory = osdk_output_directory.join(target_name);
arch: config.arch, let bundle = create_base_and_cached_build(
settings: config.settings.clone(),
cargo_args: config.cargo_args.clone(),
};
let bundle = create_base_and_build(
default_bundle_directory, default_bundle_directory,
&osdk_target_directory, &osdk_output_directory,
&ws_target_directory, &cargo_target_directory,
&required_build_config, &config,
ActionChoice::Run,
&[], &[],
); );
bundle.run(&config);
}
fn get_last_modified_time(path: impl AsRef<Path>) -> SystemTime { bundle.run(&config, ActionChoice::Run);
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
} }
mod gdb { mod gdb {

View File

@ -2,38 +2,38 @@
use std::fs; use std::fs;
use super::{build::do_build, util::DEFAULT_TARGET_RELPATH}; use super::{build::do_cached_build, util::DEFAULT_TARGET_RELPATH};
use crate::{ use crate::{
base_crate::new_base_crate, base_crate::new_base_crate,
cli::GdbServerArgs, cli::TestArgs,
config_manager::{BuildConfig, RunConfig, TestConfig}, config::{scheme::ActionChoice, Config},
util::{get_cargo_metadata, get_current_crate_info, get_target_directory}, 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(); let crates = get_workspace_default_members();
for crate_path in crates { for crate_path in crates {
std::env::set_current_dir(crate_path).unwrap(); 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 current_crate = get_current_crate_info();
let ws_target_directory = get_target_directory(); let cargo_target_directory = get_target_directory();
let osdk_target_directory = ws_target_directory.join(DEFAULT_TARGET_RELPATH); let osdk_output_directory = cargo_target_directory.join(DEFAULT_TARGET_RELPATH);
let target_crate_dir = osdk_target_directory.join("base"); let target_crate_dir = osdk_output_directory.join("base");
new_base_crate(&target_crate_dir, &current_crate.name, &current_crate.path); new_base_crate(&target_crate_dir, &current_crate.name, &current_crate.path);
let main_rs_path = target_crate_dir.join("src").join("main.rs"); 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), Some(name) => format!(r#"Some(&["{}"])"#, name),
None => r#"None"#.to_string(), None => r#"None"#.to_string(),
}; };
let mut ktest_crate_whitelist = vec![current_crate.name]; 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()); 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 // Build the kernel with the given base crate
let target_name = get_current_crate_info().name; let target_name = get_current_crate_info().name;
let default_bundle_directory = osdk_target_directory.join(target_name); let default_bundle_directory = osdk_output_directory.join(target_name);
let required_build_config = BuildConfig {
arch: config.arch,
settings: config.settings.clone(),
cargo_args: config.cargo_args.clone(),
};
let original_dir = std::env::current_dir().unwrap(); let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&target_crate_dir).unwrap(); std::env::set_current_dir(&target_crate_dir).unwrap();
let bundle = do_build( let bundle = do_cached_build(
default_bundle_directory, default_bundle_directory,
&osdk_target_directory, &osdk_output_directory,
&ws_target_directory, &cargo_target_directory,
&required_build_config, config,
ActionChoice::Test,
&["--cfg ktest"], &["--cfg ktest"],
); );
std::env::remove_var("RUSTFLAGS"); std::env::remove_var("RUSTFLAGS");
std::env::set_current_dir(original_dir).unwrap(); std::env::set_current_dir(original_dir).unwrap();
let required_run_config = RunConfig { bundle.run(config, ActionChoice::Test);
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);
} }
fn get_workspace_default_members() -> Vec<String> { fn get_workspace_default_members() -> Vec<String> {

48
osdk/src/config/eval.rs Normal file
View File

@ -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<String> {
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<String> {
let env_keys: Vec<String> = 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())
}

355
osdk/src/config/manifest.rs Normal file
View File

@ -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<ProjectType>,
pub default_scheme: Scheme,
pub map: HashMap<String, Scheme>,
}
impl TomlManifest {
pub fn load(feature_strings: &Vec<String>) -> 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>) -> 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(&current_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 &current_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) = &current_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<impl ToString>) -> &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<Path>) -> Option<TomlManifest> {
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<D>(deserializer: D) -> Result<Self, D::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, v: &str) -> Result<Self::Value, E>
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<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let mut project_type = None;
let mut default_scheme = Scheme::empty();
let mut scheme_map = HashMap::<String, Scheme>::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<String, Scheme> = map.next_value()?;
scheme_map = scheme;
}
}
}
Ok(TomlManifest {
project_type,
default_scheme,
map: scheme_map,
})
}
}
deserializer.deserialize_struct("TomlManifest", EXPECTED, TomlManifestVisitor)
}
}

130
osdk/src/config/mod.rs Normal file
View File

@ -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,
}
}
}

View File

@ -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<String>,
pub features: Vec<String>,
/// 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<String>,
#[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<BootScheme>,
pub grub: Option<GrubScheme>,
pub qemu: Option<QemuScheme>,
pub build: Option<BuildScheme>,
}
#[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(),
}
}
}

View File

@ -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<String>,
/// Command line arguments for the guest init process
#[serde(default)]
pub init_args: Vec<String>,
/// The path of initramfs
pub initramfs: Option<PathBuf>,
/// The infrastructures used to boot the guest
pub method: Option<BootMethod>,
}
#[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<String>,
pub initramfs: Option<PathBuf>,
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),
}
}
}

View File

@ -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<PathBuf>,
/// The boot protocol specified in the GRUB configuration
pub boot_protocol: Option<BootProtocol>,
/// 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,
}
}
}

View File

@ -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<Arch>,
#[serde(default)]
pub vars: Vars,
pub boot: Option<BootScheme>,
pub grub: Option<GrubScheme>,
pub qemu: Option<QemuScheme>,
pub build: Option<BuildScheme>,
pub run: Option<ActionScheme>,
pub test: Option<ActionScheme>,
}
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);
}
}

View File

@ -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<String>,
/// The path of qemu
pub path: Option<PathBuf>,
}
#[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<String>) {
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 _);
}
}

View File

@ -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 \
"""

View File

@ -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::<String>);
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();
}

View File

@ -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<String>,
/// Command line arguments for the guest init process
#[serde(default)]
pub init_args: Vec<String>,
/// The path of initramfs
pub initramfs: Option<PathBuf>,
pub bootloader: Option<Bootloader>,
pub boot_protocol: Option<BootProtocol>,
/// The path of `grub_mkrecue`. Only be `Some(_)` if `loader` is `Bootloader::grub`
pub grub_mkrescue: Option<PathBuf>,
/// The path of OVMF binaries. Only required if `protocol` is `BootProtocol::LinuxEfiHandover64`
pub ovmf: Option<PathBuf>,
/// The path of OpenSBI binaries. Only required for RISC-V.
pub opensbi: Option<PathBuf>,
/// QEMU's available machines appended with various machine configurations
pub qemu_machine: Option<String>,
/// The additional arguments for running QEMU, except `-cpu` and `-machine`
#[serde(default)]
pub qemu_args: Vec<String>,
/// The additional drive files attaching to QEMU
#[serde(default)]
pub drive_files: Vec<DriveFile>,
/// The path of qemu
pub qemu_exe: Option<PathBuf>,
}
#[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<std::path::Path>) {
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<String> {
let mut kcmd_args = self.kcmd_args.clone();
kcmd_args.push("--".to_owned());
kcmd_args.extend(self.init_args.clone());
kcmd_args
}
}

View File

@ -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<String, String>);
#[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<T: fmt::Display>(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<K, V, const N: usize> From<[(K, V); N]> for Cfg
where
K: Into<String>,
V: Into<String>,
{
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<Self, CfgParseError> {
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<String, String> {
&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());
}
}

View File

@ -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<ActionSettings>,
pub test: Option<ActionSettings>,
}
#[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<Cfg, CfgArgs>,
}
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<Path>,
arch: Arch,
schema: Option<String>,
) -> 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::<Vec<_>>()
} else {
filtered_by_arch
.filter(|(cfg, _)| cfg == &&Cfg::empty())
.collect::<Vec<_>>()
};
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<ActionSettings>,
pub test: Option<ActionSettings>,
}
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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
enum Field {
Project,
Run,
Test,
Cfg(Cfg),
}
impl<'de> Deserialize<'de> for Field {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, v: &str) -> Result<Self::Value, E>
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<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let mut project: Option<Project> = None;
let default_cfg = Cfg::empty();
let mut cfg_map = BTreeMap::<Cfg, CfgArgs>::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,
)
}
}

View File

@ -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<String>,
}
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<String> {
cargo_args
.features
.iter()
.map(|feature| format!("--features={}", feature))
.collect()
}

View File

@ -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<String>, args: &Vec<String>) {
// 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 _);
}
}

View File

@ -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",
]

View File

@ -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<String> = 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<String> = 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<String> = 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")
);
}

View File

@ -12,7 +12,7 @@ mod base_crate;
mod bundle; mod bundle;
mod cli; mod cli;
mod commands; mod commands;
mod config_manager; mod config;
mod error; mod error;
mod util; mod util;

View File

@ -6,7 +6,7 @@ use crate::util::*;
fn cli_help_message() { fn cli_help_message() {
let output = cargo_osdk(&["-h"]).output().unwrap(); let output = cargo_osdk(&["-h"]).output().unwrap();
assert_success(&output); assert_success(&output);
assert_stdout_contains_msg(&output, "cargo osdk <COMMAND>"); assert_stdout_contains_msg(&output, "cargo osdk [OPTIONS] <COMMAND>");
} }
#[test] #[test]

65
tools/qemu_args.sh Executable file
View File

@ -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