From a0e743a45371d70312ce4ba08becb0e21db74d2e Mon Sep 17 00:00:00 2001 From: Cappy Ishihara Date: Thu, 10 Oct 2024 16:34:07 +0700 Subject: [PATCH] Add Legacy BIOS support - UEFI and Legacy backends now auto-generate GRUB configuration files at /boot/grub2/grub.cfg - Added a special bootloader option called `grub_bios` that will set the disk image to boot in Legacy BIOS mode - Updated tests to Ultramarine 40 --- src/builder.rs | 37 ++++++++- src/config.rs | 53 ++++++++----- tests/ng/katsu-bios.yaml | 97 ++++++++++++++++++++++++ tests/ng/modules/base.yaml | 2 +- tests/ng/modules/scripts/grub-confgen.sh | 3 + tests/ng/modules/scripts/grub-install.sh | 5 -- 6 files changed, 172 insertions(+), 25 deletions(-) create mode 100644 tests/ng/katsu-bios.yaml create mode 100644 tests/ng/modules/scripts/grub-confgen.sh diff --git a/src/builder.rs b/src/builder.rs index 683d9b9..c17a703 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -20,10 +20,11 @@ const WORKDIR: &str = "katsu-work"; crate::prepend_comment!(GRUB_PREPEND_COMMENT: "/boot/grub/grub.cfg", "Grub configurations", katsu::builder::Bootloader::cp_grub); crate::prepend_comment!(LIMINE_PREPEND_COMMENT: "/boot/limine.cfg", "Limine configurations", katsu::builder::Bootloader::cp_limine); -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum Bootloader { #[default] Grub, + GrubBios, Limine, SystemdBoot, } @@ -33,6 +34,7 @@ impl From<&str> for Bootloader { match &*value.to_lowercase() { "limine" => Self::Limine, "grub" | "grub2" => Self::Grub, + "grub-bios" => Self::GrubBios, "systemd-boot" => Self::SystemdBoot, _ => { warn!("Unknown bootloader: {value}, falling back to GRUB"); @@ -48,6 +50,9 @@ impl Bootloader { Self::Grub => info!("GRUB is not required to be installed to image, skipping"), Self::Limine => cmd_lib::run_cmd!(limine bios-install $image 2>&1)?, Self::SystemdBoot => cmd_lib::run_cmd!(bootctl --image=$image install 2>&1)?, + Self::GrubBios => { + cmd_lib::run_cmd!(grub-install --target=i386-pc --boot-directory=$image/boot 2>&1)? + }, } Ok(()) } @@ -55,6 +60,7 @@ impl Bootloader { match *self { Self::Grub => ("boot/efi/EFI/fedora/shim.efi", "boot/eltorito.img"), Self::Limine => ("boot/limine-uefi-cd.bin", "boot/limine-bios-cd.bin"), + Self::GrubBios => todo!(), Self::SystemdBoot => todo!(), } } @@ -262,9 +268,14 @@ impl Bootloader { Self::Grub => self.cp_grub(manifest, chroot)?, Self::Limine => self.cp_limine(manifest, chroot)?, Self::SystemdBoot => todo!(), + Self::GrubBios => self.cp_grub_bios(chroot)?, } Ok(()) } + + pub fn cp_grub_bios(&self, chroot: &Path) -> Result<()> { + todo!() + } } pub trait RootBuilder { @@ -361,6 +372,15 @@ impl RootBuilder for DnfRootBuilder { manifest.users.iter().try_for_each(|user| user.add_to_chroot(&chroot))?; } + if manifest.bootloader == Bootloader::GrubBios || manifest.bootloader == Bootloader::Grub { + info!("Generating GRUB configuration"); + crate::chroot_run_cmd!(&chroot, + echo "GRUB_DISABLE_OS_PROBER=true" > /etc/default/grub; + grub2-mkconfig -o /boot/grub2/grub.cfg; + rm -f /etc/default/grub; + )?; + } + // now, let's run some funny post-install scripts info!("Running post-install scripts"); @@ -482,17 +502,30 @@ impl ImageBuilder for DiskImageBuilder { // disk.mount_to_chroot(&loopdev.path().unwrap(), &chroot)?; // disk.unmount_from_chroot(&loopdev.path().unwrap(), &chroot)?; // } + let uefi = { self.bootloader != Bootloader::GrubBios }; + let arch = manifest.dnf.arch.as_deref().unwrap_or(std::env::consts::ARCH); let (ldp, hdl) = loopdev_with_file(sparse_path)?; // Partition disk - disk.apply(&ldp, manifest.dnf.arch.as_deref().unwrap_or(std::env::consts::ARCH))?; + disk.apply(&ldp, arch, uefi)?; // Mount partitions to chroot disk.mount_to_chroot(&ldp, chroot)?; self.root_builder.build(&chroot.canonicalize()?, manifest)?; + if !uefi { + info!("Not UEFI, Setting up extra configs"); + + // Let's use grub2-install to bless the disk + + info!("Blessing disk image with MBR"); + cmd_lib::run_cmd!( + grub2-install --target=i386-pc --boot-directory=$chroot/boot $ldp 2>&1; + )?; + } + disk.unmount_from_chroot(chroot)?; drop(hdl); diff --git a/src/config.rs b/src/config.rs index e3acf58..54e28fc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,7 +9,7 @@ use std::{ io::Write, path::{Path, PathBuf}, }; -use tracing::{debug, info, trace}; +use tracing::{debug, info, trace, warn}; const DEFAULT_VOLID: &str = "KATSU-LIVEOS"; #[derive(Deserialize, Debug, Clone, Serialize)] @@ -334,7 +334,13 @@ impl PartitionLayout { // trim trailing slashes let am = a.mountpoint.trim_end_matches('/').matches('/').count(); let bm = b.mountpoint.trim_end_matches('/').matches('/').count(); - if a.mountpoint == "/" { + if a.mountpoint.is_empty() { + // empty mountpoint should always come first + std::cmp::Ordering::Less + } else if b.mountpoint.is_empty() { + // empty mountpoint should always come first + std::cmp::Ordering::Greater + } else if a.mountpoint == "/" { // / should always come first std::cmp::Ordering::Less } else if b.mountpoint == "/" { @@ -358,6 +364,17 @@ impl PartitionLayout { // Ok, so for some reason the partitions are swapped? for (index, part) in &ordered { + // println!("Partition {index}: {part:#?}"); + + if part.mountpoint.is_empty() + || part.filesystem == "none" + || part.filesystem == "swap" + || part.mountpoint == "-" + { + // skip empty mountpoints + warn!(?part, "This partition is not supposed to be mounted! Skipping... If you want this partition to be mounted, please specify a mountpoint starting with /"); + continue; + } let devname = partition_name(&disk.to_string_lossy(), *index); // clean the mountpoint so we don't have the slash at the start @@ -378,6 +395,9 @@ impl PartitionLayout { // unmount partitions from chroot // sort partitions by mountpoint for mp in self.sort_partitions().into_iter().rev().map(|(_, p)| p.mountpoint) { + if mp.is_empty() || mp == "-" { + continue; + } let mp = chroot.join(mp.trim_start_matches('/')); trace!("umount {mp:?}"); cmd_lib::run_cmd!(umount $mp 2>&1)?; @@ -395,21 +415,20 @@ impl PartitionLayout { let mut entries = vec![]; ordered.iter().try_for_each(|(_, part)| -> Result<()> { - let mp = PathBuf::from(&part.mountpoint).to_string_lossy().to_string(); - let mountpoint_chroot = part.mountpoint.trim_start_matches('/'); - let mountpoint_chroot = chroot.join(mountpoint_chroot); - let devname = cmd_lib::run_fun!(findmnt -n -o SOURCE $mountpoint_chroot)?; + if part.filesystem != "none" { + let mp = PathBuf::from(&part.mountpoint).to_string_lossy().to_string(); + let mountpoint_chroot = part.mountpoint.trim_start_matches('/'); + let mountpoint_chroot = chroot.join(mountpoint_chroot); + let devname = cmd_lib::run_fun!(findmnt -n -o SOURCE $mountpoint_chroot)?; - // We will generate by UUID - let uuid = cmd_lib::run_fun!(blkid -s UUID -o value $devname)?; + // We will generate by UUID + let uuid = cmd_lib::run_fun!(blkid -s UUID -o value $devname)?; - // clean the mountpoint so we don't have the slash at the start - // let mp_cleaned = part.mountpoint.trim_start_matches('/'); - - let fsname = if part.filesystem == "efi" { "vfat" } else { &part.filesystem }; - let fsck = if part.filesystem == "efi" { 0 } else { 2 }; + let fsname = if part.filesystem == "efi" { "vfat" } else { &part.filesystem }; + let fsck = if part.filesystem == "efi" { 0 } else { 2 }; - entries.push(TplFstabEntry { uuid, mp, fsname, fsck }); + entries.push(TplFstabEntry { uuid, mp, fsname, fsck }); + } Ok(()) })?; @@ -418,7 +437,7 @@ impl PartitionLayout { Ok(crate::tpl!("fstab.tera" => { PREPEND, entries })) } - pub fn apply(&self, disk: &PathBuf, target_arch: &str) -> Result<()> { + pub fn apply(&self, disk: &PathBuf, target_arch: &str, uefi: bool) -> Result<()> { // This is a destructive operation, so we need to make sure we don't accidentally wipe the wrong disk info!("Applying partition layout to disk: {disk:#?}"); @@ -439,7 +458,7 @@ impl PartitionLayout { let start_string = if i == 1 { // create partition at start of disk - "1MiB".to_string() + "0".to_string() } else { // create partition after last partition ByteSize::b(last_end).to_string_as(true).replace(' ', "") @@ -453,7 +472,6 @@ impl PartitionLayout { ByteSize::b(last_end).to_string_as(true).replace(' ', "") }); - // TODO: primary/extended/logical is a MBR concept, since we're using GPT, we should be using this field to set the label // not going to change this for now though, but will revisit debug!(start = start_string, end = end_string, "Creating partition"); trace!("parted -s {disk:?} mkpart primary fat32 {start_string} {end_string}"); @@ -497,6 +515,7 @@ impl PartitionLayout { if fsname == "efi" { trace!("mkfs.fat -F32 {devname}"); cmd_lib::run_cmd!(mkfs.fat -F32 $devname 2>&1)?; + } else if fsname == "none" { } else { trace!("mkfs.{fsname} {devname}"); cmd_lib::run_cmd!(mkfs.$fsname $devname 2>&1)?; diff --git a/tests/ng/katsu-bios.yaml b/tests/ng/katsu-bios.yaml new file mode 100644 index 0000000..33bb884 --- /dev/null +++ b/tests/ng/katsu-bios.yaml @@ -0,0 +1,97 @@ +# Example manifest for a Katsu build +builder: dnf +distro: Katsu Ultramarine +bootloader: grub-bios + +scripts: + post: + - id: post-test + name: Postinstall test + inline: | + echo "Hello from post.sh" + + - id: image-cleanup + name: Clean up root filesystem + file: modules/scripts/image-cleanup.sh + + - id: selinux-label + name: Relabel SELinux for new filesystem + file: modules/scripts/selinux.sh + + +disk: + size: 8GiB + partitions: + - label: mbr_grub + type: 21686148-6449-6E6F-744E-656564454649 + size: 1MiB + filesystem: none # no filesystem + mountpoint: "" + + - label: boot + type: xbootldr + size: 1GiB + filesystem: ext4 + mountpoint: /boot + + - label: root + type: xbootldr + flags: + - grow-fs + # size: 2.5MiB + filesystem: ext4 + mountpoint: / + +users: + - username: ultramarine + # plaintext password: ultramarine + password: "$y$j9T$6/DebcxXazPrtBYnNXtEM.$yaUJHww5Mo1L8xNJ9IDJ.bvKOrIJxAG9PGQKWioBMx3" + groups: + - wheel + + +dnf: + dnf5: false + releasever: 40 + repodir: modules/repodir/ + options: + - --setopt=cachedir=/var/cache/dnf + - --nogpgcheck + - --setopt=keepcache=True + - --verbose + exclude: + - fedora-release* + - generic-release* + packages: + - filesystem + - setup + - lvm2 + - btrfs-progs + - dmraid + - nvme-cli + - kernel + - glibc + - glibc-common + - dracut-config-generic + - dracut-tools + - dnf + - dracut + # - mkpasswd # maybe not needed soon + - "@core" + - "@standard" + - grub2-tools + - "@hardware-support" + - NetworkManager + - rpm + - libgomp + - ultramarine-release-identity-basic + - ultramarine-release-basic + - fedora-repos + + arch_packages: + x86_64: + - grub2-pc + - grub2-pc-modules + - shim-x64 + - shim-unsigned-x64 + diff --git a/tests/ng/modules/base.yaml b/tests/ng/modules/base.yaml index 93bad70..a6cbd84 100644 --- a/tests/ng/modules/base.yaml +++ b/tests/ng/modules/base.yaml @@ -24,7 +24,7 @@ scripts: dnf: - releasever: 39 + releasever: 40 repodir: repodir/ options: - --setopt=cachedir=/var/cache/dnf diff --git a/tests/ng/modules/scripts/grub-confgen.sh b/tests/ng/modules/scripts/grub-confgen.sh new file mode 100644 index 0000000..a19d0b5 --- /dev/null +++ b/tests/ng/modules/scripts/grub-confgen.sh @@ -0,0 +1,3 @@ +#!/bin/bash -x + +grub2-mkconfig -o /boot/grub2/grub.cfg diff --git a/tests/ng/modules/scripts/grub-install.sh b/tests/ng/modules/scripts/grub-install.sh index af7bb1e..23f0c5d 100644 --- a/tests/ng/modules/scripts/grub-install.sh +++ b/tests/ng/modules/scripts/grub-install.sh @@ -2,11 +2,6 @@ set -x # Disable os-prober for now -echo "Disabling os-prober..." - -echo "GRUB_DISABLE_OS_PROBER=true" > /etc/default/grub -grub2-mkconfig > /boot/grub2/grub.cfg -rm /etc/default/grub # get /dev/ of /boot, or / if /boot is not a separate partition