diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec952c33..db938864 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,14 @@ jobs: - name: Run integration tests run: cargo test + # test feature gates (only on one OS is enough) + - name: Test with only UEFI feature + if: runner.os == 'Linux' + run: cargo test --no-default-features --features uefi + - name: Test with only BIOS feature + if: runner.os == 'Linux' + run: cargo test --no-default-features --features bios + fmt: name: Check Formatting runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 4bea9b23..e1364c64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,12 +37,17 @@ bootloader_api = { version = "0.11.0", path = "api" } bootloader-x86_64-common = { version = "0.11.0", path = "common" } bootloader-x86_64-bios-common = { version = "0.11.0", path = "bios/common" } +[features] +default = ["bios", "uefi"] +bios = ["dep:mbrman", "bootloader_test_runner/bios"] +uefi = ["dep:gpt", "bootloader_test_runner/uefi"] + [dependencies] anyhow = "1.0.32" fatfs = "0.3.4" -gpt = "3.0.0" -mbrman = "0.5.1" tempfile = "3.3.0" +mbrman = { version = "0.5.1", optional = true } +gpt = { version = "3.0.0", optional = true } [dev-dependencies] bootloader_test_runner = { path = "tests/runner" } diff --git a/build.rs b/build.rs index 7f04dddf..e752d28f 100644 --- a/build.rs +++ b/build.rs @@ -3,47 +3,49 @@ use std::{ process::Command, }; -const BOOTLOADER_X86_64_UEFI_VERSION: &str = env!("CARGO_PKG_VERSION"); - -const BOOTLOADER_X86_64_BIOS_BOOT_SECTOR_VERSION: &str = env!("CARGO_PKG_VERSION"); -const BOOTLOADER_X86_64_BIOS_STAGE_2_VERSION: &str = env!("CARGO_PKG_VERSION"); -const BOOTLOADER_X86_64_BIOS_STAGE_3_VERSION: &str = env!("CARGO_PKG_VERSION"); -const BOOTLOADER_X86_64_BIOS_STAGE_4_VERSION: &str = env!("CARGO_PKG_VERSION"); +const BOOTLOADER_VERSION: &str = env!("CARGO_PKG_VERSION"); fn main() { let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); - let uefi_path = build_uefi_bootloader(&out_dir); - println!( - "cargo:rustc-env=UEFI_BOOTLOADER_PATH={}", - uefi_path.display() - ); + #[cfg(feature = "uefi")] + { + let uefi_path = build_uefi_bootloader(&out_dir); + println!( + "cargo:rustc-env=UEFI_BOOTLOADER_PATH={}", + uefi_path.display() + ); + } - let bios_boot_sector_path = build_bios_boot_sector(&out_dir); - println!( - "cargo:rustc-env=BIOS_BOOT_SECTOR_PATH={}", - bios_boot_sector_path.display() - ); - let bios_stage_2_path = build_bios_stage_2(&out_dir); - println!( - "cargo:rustc-env=BIOS_STAGE_2_PATH={}", - bios_stage_2_path.display() - ); + #[cfg(feature = "bios")] + { + let bios_boot_sector_path = build_bios_boot_sector(&out_dir); + println!( + "cargo:rustc-env=BIOS_BOOT_SECTOR_PATH={}", + bios_boot_sector_path.display() + ); + let bios_stage_2_path = build_bios_stage_2(&out_dir); + println!( + "cargo:rustc-env=BIOS_STAGE_2_PATH={}", + bios_stage_2_path.display() + ); - let bios_stage_3_path = build_bios_stage_3(&out_dir); - println!( - "cargo:rustc-env=BIOS_STAGE_3_PATH={}", - bios_stage_3_path.display() - ); + let bios_stage_3_path = build_bios_stage_3(&out_dir); + println!( + "cargo:rustc-env=BIOS_STAGE_3_PATH={}", + bios_stage_3_path.display() + ); - let bios_stage_4_path = build_bios_stage_4(&out_dir); - println!( - "cargo:rustc-env=BIOS_STAGE_4_PATH={}", - bios_stage_4_path.display() - ); + let bios_stage_4_path = build_bios_stage_4(&out_dir); + println!( + "cargo:rustc-env=BIOS_STAGE_4_PATH={}", + bios_stage_4_path.display() + ); + } } #[cfg(not(docsrs_dummy_build))] +#[cfg(feature = "uefi")] fn build_uefi_bootloader(out_dir: &Path) -> PathBuf { let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()); let mut cmd = Command::new(cargo); @@ -53,7 +55,7 @@ fn build_uefi_bootloader(out_dir: &Path) -> PathBuf { cmd.arg("--path").arg("uefi"); println!("cargo:rerun-if-changed=uefi"); } else { - cmd.arg("--version").arg(BOOTLOADER_X86_64_UEFI_VERSION); + cmd.arg("--version").arg(BOOTLOADER_VERSION); } cmd.arg("--locked"); cmd.arg("--target").arg("x86_64-unknown-uefi"); @@ -78,6 +80,7 @@ fn build_uefi_bootloader(out_dir: &Path) -> PathBuf { } #[cfg(not(docsrs_dummy_build))] +#[cfg(feature = "bios")] fn build_bios_boot_sector(out_dir: &Path) -> PathBuf { let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()); let mut cmd = Command::new(cargo); @@ -90,8 +93,7 @@ fn build_bios_boot_sector(out_dir: &Path) -> PathBuf { cmd.arg("--path").arg(&local_path); println!("cargo:rerun-if-changed={}", local_path.display()); } else { - cmd.arg("--version") - .arg(BOOTLOADER_X86_64_BIOS_BOOT_SECTOR_VERSION); + cmd.arg("--version").arg(BOOTLOADER_VERSION); } cmd.arg("--locked"); cmd.arg("--target").arg("i386-code16-boot-sector.json"); @@ -121,6 +123,7 @@ fn build_bios_boot_sector(out_dir: &Path) -> PathBuf { } #[cfg(not(docsrs_dummy_build))] +#[cfg(feature = "bios")] fn build_bios_stage_2(out_dir: &Path) -> PathBuf { let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()); let mut cmd = Command::new(cargo); @@ -133,8 +136,7 @@ fn build_bios_stage_2(out_dir: &Path) -> PathBuf { cmd.arg("--path").arg(&local_path); println!("cargo:rerun-if-changed={}", local_path.display()); } else { - cmd.arg("--version") - .arg(BOOTLOADER_X86_64_BIOS_STAGE_2_VERSION); + cmd.arg("--version").arg(BOOTLOADER_VERSION); } cmd.arg("--locked"); cmd.arg("--target").arg("i386-code16-stage-2.json"); @@ -162,6 +164,7 @@ fn build_bios_stage_2(out_dir: &Path) -> PathBuf { } #[cfg(not(docsrs_dummy_build))] +#[cfg(feature = "bios")] fn build_bios_stage_3(out_dir: &Path) -> PathBuf { let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()); let mut cmd = Command::new(cargo); @@ -174,8 +177,7 @@ fn build_bios_stage_3(out_dir: &Path) -> PathBuf { cmd.arg("--path").arg(&local_path); println!("cargo:rerun-if-changed={}", local_path.display()); } else { - cmd.arg("--version") - .arg(BOOTLOADER_X86_64_BIOS_STAGE_3_VERSION); + cmd.arg("--version").arg(BOOTLOADER_VERSION); } cmd.arg("--locked"); cmd.arg("--target").arg("i686-stage-3.json"); @@ -203,6 +205,7 @@ fn build_bios_stage_3(out_dir: &Path) -> PathBuf { } #[cfg(not(docsrs_dummy_build))] +#[cfg(feature = "bios")] fn build_bios_stage_4(out_dir: &Path) -> PathBuf { let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()); let mut cmd = Command::new(cargo); @@ -215,8 +218,7 @@ fn build_bios_stage_4(out_dir: &Path) -> PathBuf { cmd.arg("--path").arg(&local_path); println!("cargo:rerun-if-changed={}", local_path.display()); } else { - cmd.arg("--version") - .arg(BOOTLOADER_X86_64_BIOS_STAGE_4_VERSION); + cmd.arg("--version").arg(BOOTLOADER_VERSION); } cmd.arg("--locked"); cmd.arg("--target").arg("x86_64-stage-4.json"); @@ -244,6 +246,7 @@ fn build_bios_stage_4(out_dir: &Path) -> PathBuf { convert_elf_to_bin(elf_path) } +#[cfg(feature = "bios")] fn convert_elf_to_bin(elf_path: PathBuf) -> PathBuf { let flat_binary_path = elf_path.with_extension("bin"); diff --git a/src/mbr.rs b/src/bios/mbr.rs similarity index 100% rename from src/mbr.rs rename to src/bios/mbr.rs diff --git a/src/bios/mod.rs b/src/bios/mod.rs new file mode 100644 index 00000000..03952152e --- /dev/null +++ b/src/bios/mod.rs @@ -0,0 +1,67 @@ +use crate::fat; +use anyhow::Context; +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, +}; +use tempfile::NamedTempFile; + +mod mbr; + +const BIOS_STAGE_3: &str = "boot-stage-3"; +const BIOS_STAGE_4: &str = "boot-stage-4"; + +/// Create disk images for booting on legacy BIOS systems. +pub struct BiosBoot { + kernel: PathBuf, +} + +impl BiosBoot { + /// Start creating a disk image for the given bootloader ELF executable. + pub fn new(kernel_path: &Path) -> Self { + Self { + kernel: kernel_path.to_owned(), + } + } + + /// Create a bootable UEFI disk image at the given path. + pub fn create_disk_image(&self, out_path: &Path) -> anyhow::Result<()> { + let bootsector_path = Path::new(env!("BIOS_BOOT_SECTOR_PATH")); + let stage_2_path = Path::new(env!("BIOS_STAGE_2_PATH")); + + let fat_partition = self + .create_fat_partition() + .context("failed to create FAT partition")?; + + mbr::create_mbr_disk( + bootsector_path, + stage_2_path, + fat_partition.path(), + out_path, + ) + .context("failed to create BIOS MBR disk image")?; + + fat_partition + .close() + .context("failed to delete FAT partition after disk image creation")?; + + Ok(()) + } + + /// Creates an BIOS-bootable FAT partition with the kernel. + fn create_fat_partition(&self) -> anyhow::Result { + let stage_3_path = Path::new(env!("BIOS_STAGE_3_PATH")); + let stage_4_path = Path::new(env!("BIOS_STAGE_4_PATH")); + + let mut files = BTreeMap::new(); + files.insert(crate::KERNEL_FILE_NAME, self.kernel.as_path()); + files.insert(BIOS_STAGE_3, stage_3_path); + files.insert(BIOS_STAGE_4, stage_4_path); + + let out_file = NamedTempFile::new().context("failed to create temp file")?; + fat::create_fat_filesystem(files, out_file.path()) + .context("failed to create BIOS FAT filesystem")?; + + Ok(out_file) + } +} diff --git a/src/lib.rs b/src/lib.rs index 11d1ebf0..cd41805a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,132 +4,16 @@ An experimental x86_64 bootloader that works on both BIOS and UEFI systems. #![warn(missing_docs)] -use anyhow::Context; -use std::{ - collections::BTreeMap, - path::{Path, PathBuf}, -}; -use tempfile::NamedTempFile; - +#[cfg(feature = "bios")] +mod bios; mod fat; -mod gpt; -mod mbr; -mod pxe; - -const KERNEL_FILE_NAME: &str = "kernel-x86_64"; -const BIOS_STAGE_3: &str = "boot-stage-3"; -const BIOS_STAGE_4: &str = "boot-stage-4"; - -/// Create disk images for booting on legacy BIOS systems. -pub struct BiosBoot { - kernel: PathBuf, -} - -impl BiosBoot { - /// Start creating a disk image for the given bootloader ELF executable. - pub fn new(kernel_path: &Path) -> Self { - Self { - kernel: kernel_path.to_owned(), - } - } - - /// Create a bootable UEFI disk image at the given path. - pub fn create_disk_image(&self, out_path: &Path) -> anyhow::Result<()> { - let bootsector_path = Path::new(env!("BIOS_BOOT_SECTOR_PATH")); - let stage_2_path = Path::new(env!("BIOS_STAGE_2_PATH")); - - let fat_partition = self - .create_fat_partition() - .context("failed to create FAT partition")?; - - mbr::create_mbr_disk( - bootsector_path, - stage_2_path, - fat_partition.path(), - out_path, - ) - .context("failed to create BIOS MBR disk image")?; - - fat_partition - .close() - .context("failed to delete FAT partition after disk image creation")?; - - Ok(()) - } - - /// Creates an BIOS-bootable FAT partition with the kernel. - fn create_fat_partition(&self) -> anyhow::Result { - let stage_3_path = Path::new(env!("BIOS_STAGE_3_PATH")); - let stage_4_path = Path::new(env!("BIOS_STAGE_4_PATH")); - - let mut files = BTreeMap::new(); - files.insert(KERNEL_FILE_NAME, self.kernel.as_path()); - files.insert(BIOS_STAGE_3, stage_3_path); - files.insert(BIOS_STAGE_4, stage_4_path); - - let out_file = NamedTempFile::new().context("failed to create temp file")?; - fat::create_fat_filesystem(files, out_file.path()) - .context("failed to create BIOS FAT filesystem")?; +#[cfg(feature = "uefi")] +mod uefi; - Ok(out_file) - } -} +#[cfg(feature = "bios")] +pub use bios::BiosBoot; -/// Create disk images for booting on UEFI systems. -pub struct UefiBoot { - kernel: PathBuf, -} +#[cfg(feature = "uefi")] +pub use uefi::UefiBoot; -impl UefiBoot { - /// Start creating a disk image for the given bootloader ELF executable. - pub fn new(kernel_path: &Path) -> Self { - Self { - kernel: kernel_path.to_owned(), - } - } - - /// Create a bootable BIOS disk image at the given path. - pub fn create_disk_image(&self, out_path: &Path) -> anyhow::Result<()> { - let fat_partition = self - .create_fat_partition() - .context("failed to create FAT partition")?; - - gpt::create_gpt_disk(fat_partition.path(), out_path) - .context("failed to create UEFI GPT disk image")?; - - fat_partition - .close() - .context("failed to delete FAT partition after disk image creation")?; - - Ok(()) - } - - /// Prepare a folder for use with booting over UEFI_PXE. - /// - /// This places the bootloader executable under the path "bootloader". The - /// DHCP server should set the filename option to that path, otherwise the - /// bootloader won't be found. - pub fn create_pxe_tftp_folder(&self, out_path: &Path) -> anyhow::Result<()> { - let bootloader_path = Path::new(env!("UEFI_BOOTLOADER_PATH")); - - pxe::create_uefi_tftp_folder(bootloader_path, self.kernel.as_path(), out_path) - .context("failed to create UEFI PXE tftp folder")?; - - Ok(()) - } - - /// Creates an UEFI-bootable FAT partition with the kernel. - fn create_fat_partition(&self) -> anyhow::Result { - let bootloader_path = Path::new(env!("UEFI_BOOTLOADER_PATH")); - - let mut files = BTreeMap::new(); - files.insert("efi/boot/bootx64.efi", bootloader_path); - files.insert(KERNEL_FILE_NAME, self.kernel.as_path()); - - let out_file = NamedTempFile::new().context("failed to create temp file")?; - fat::create_fat_filesystem(files, out_file.path()) - .context("failed to create UEFI FAT filesystem")?; - - Ok(out_file) - } -} +const KERNEL_FILE_NAME: &str = "kernel-x86_64"; diff --git a/src/gpt.rs b/src/uefi/gpt.rs similarity index 100% rename from src/gpt.rs rename to src/uefi/gpt.rs diff --git a/src/uefi/mod.rs b/src/uefi/mod.rs new file mode 100644 index 00000000..cfbc8c9f --- /dev/null +++ b/src/uefi/mod.rs @@ -0,0 +1,69 @@ +use crate::fat; +use anyhow::Context; +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, +}; +use tempfile::NamedTempFile; + +mod gpt; +mod pxe; + +/// Create disk images for booting on UEFI systems. +pub struct UefiBoot { + kernel: PathBuf, +} + +impl UefiBoot { + /// Start creating a disk image for the given bootloader ELF executable. + pub fn new(kernel_path: &Path) -> Self { + Self { + kernel: kernel_path.to_owned(), + } + } + + /// Create a bootable BIOS disk image at the given path. + pub fn create_disk_image(&self, out_path: &Path) -> anyhow::Result<()> { + let fat_partition = self + .create_fat_partition() + .context("failed to create FAT partition")?; + + gpt::create_gpt_disk(fat_partition.path(), out_path) + .context("failed to create UEFI GPT disk image")?; + + fat_partition + .close() + .context("failed to delete FAT partition after disk image creation")?; + + Ok(()) + } + + /// Prepare a folder for use with booting over UEFI_PXE. + /// + /// This places the bootloader executable under the path "bootloader". The + /// DHCP server should set the filename option to that path, otherwise the + /// bootloader won't be found. + pub fn create_pxe_tftp_folder(&self, out_path: &Path) -> anyhow::Result<()> { + let bootloader_path = Path::new(env!("UEFI_BOOTLOADER_PATH")); + + pxe::create_uefi_tftp_folder(bootloader_path, self.kernel.as_path(), out_path) + .context("failed to create UEFI PXE tftp folder")?; + + Ok(()) + } + + /// Creates an UEFI-bootable FAT partition with the kernel. + fn create_fat_partition(&self) -> anyhow::Result { + let bootloader_path = Path::new(env!("UEFI_BOOTLOADER_PATH")); + + let mut files = BTreeMap::new(); + files.insert("efi/boot/bootx64.efi", bootloader_path); + files.insert(crate::KERNEL_FILE_NAME, self.kernel.as_path()); + + let out_file = NamedTempFile::new().context("failed to create temp file")?; + fat::create_fat_filesystem(files, out_file.path()) + .context("failed to create UEFI FAT filesystem")?; + + Ok(out_file) + } +} diff --git a/src/pxe.rs b/src/uefi/pxe.rs similarity index 100% rename from src/pxe.rs rename to src/uefi/pxe.rs diff --git a/tests/runner/Cargo.toml b/tests/runner/Cargo.toml index 51421aa0..127184a6 100644 --- a/tests/runner/Cargo.toml +++ b/tests/runner/Cargo.toml @@ -6,7 +6,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +bios = ["bootloader/bios"] +uefi = ["bootloader/uefi", "dep:ovmf-prebuilt"] + [dependencies] -bootloader = { path = "../.." } +bootloader = { path = "../..", default-features = false } strip-ansi-escapes = "0.1.1" -ovmf-prebuilt = "0.1.0-alpha.1" +ovmf-prebuilt = { version = "0.1.0-alpha.1", optional = true } diff --git a/tests/runner/src/lib.rs b/tests/runner/src/lib.rs index 7f50cd42..3b580173 100644 --- a/tests/runner/src/lib.rs +++ b/tests/runner/src/lib.rs @@ -13,27 +13,35 @@ const QEMU_ARGS: &[&str] = &[ pub fn run_test_kernel(kernel_binary_path: &str) { let kernel_path = Path::new(kernel_binary_path); - // create an MBR disk image for legacy BIOS booting - let mbr_path = kernel_path.with_extension("mbr"); - bootloader::BiosBoot::new(kernel_path) - .create_disk_image(&mbr_path) - .unwrap(); + #[cfg(feature = "uefi")] + { + // create a GPT disk image for UEFI booting + let gpt_path = kernel_path.with_extension("gpt"); + let uefi_builder = bootloader::UefiBoot::new(kernel_path); + uefi_builder.create_disk_image(&gpt_path).unwrap(); + + // create a TFTP folder with the kernel executable and UEFI bootloader for + // UEFI PXE booting + let tftp_path = kernel_path.with_extension(".tftp"); + uefi_builder.create_pxe_tftp_folder(&tftp_path).unwrap(); - // create a GPT disk image for UEFI booting - let gpt_path = kernel_path.with_extension("gpt"); - let uefi_builder = bootloader::UefiBoot::new(kernel_path); - uefi_builder.create_disk_image(&gpt_path).unwrap(); + run_test_kernel_on_uefi(&gpt_path); + run_test_kernel_on_uefi_pxe(&tftp_path); + } - // create a TFTP folder with the kernel executable and UEFI bootloader for - // UEFI PXE booting - let tftp_path = kernel_path.with_extension(".tftp"); - uefi_builder.create_pxe_tftp_folder(&tftp_path).unwrap(); + #[cfg(feature = "bios")] + { + // create an MBR disk image for legacy BIOS booting + let mbr_path = kernel_path.with_extension("mbr"); + bootloader::BiosBoot::new(kernel_path) + .create_disk_image(&mbr_path) + .unwrap(); - run_test_kernel_on_uefi(&gpt_path); - run_test_kernel_on_bios(&mbr_path); - run_test_kernel_on_uefi_pxe(&tftp_path); + run_test_kernel_on_bios(&mbr_path); + } } +#[cfg(feature = "uefi")] pub fn run_test_kernel_on_uefi(out_gpt_path: &Path) { let mut run_cmd = Command::new("qemu-system-x86_64"); run_cmd @@ -57,6 +65,7 @@ pub fn run_test_kernel_on_uefi(out_gpt_path: &Path) { } } +#[cfg(feature = "bios")] pub fn run_test_kernel_on_bios(out_mbr_path: &Path) { let mut run_cmd = Command::new("qemu-system-x86_64"); run_cmd @@ -79,6 +88,7 @@ pub fn run_test_kernel_on_bios(out_mbr_path: &Path) { } } +#[cfg(feature = "uefi")] pub fn run_test_kernel_on_uefi_pxe(out_tftp_path: &Path) { let mut run_cmd = Command::new("qemu-system-x86_64"); run_cmd.arg("-netdev").arg(format!(