diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index c4787a917..918198029 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -622,6 +622,8 @@ pub(crate) fn setup_composefs_bls_boot( Some(efi_mount), ) } + + Bootloader::None => unreachable!("Checked at install time"), }; let (bls_config, boot_digest, os_id) = match &entry { @@ -851,6 +853,7 @@ fn write_pe_to_esp( let efi_linux_path = mounted_efi.as_ref().join(match bootloader { Bootloader::Grub => EFI_LINUX, Bootloader::Systemd => SYSTEMD_UKI_DIR, + Bootloader::None => unreachable!("Checked at install time"), }); create_dir_all(&efi_linux_path).context("Creating EFI/Linux")?; @@ -1163,6 +1166,8 @@ pub(crate) fn setup_composefs_uki_boot( } Bootloader::Systemd => write_systemd_uki_config(&esp_mount.fd, &setup_type, uki_info, id)?, + + Bootloader::None => unreachable!("Checked at install time"), }; Ok(boot_digest) diff --git a/crates/lib/src/bootc_composefs/delete.rs b/crates/lib/src/bootc_composefs/delete.rs index cad96c7f6..aa8eec5b7 100644 --- a/crates/lib/src/bootc_composefs/delete.rs +++ b/crates/lib/src/bootc_composefs/delete.rs @@ -241,6 +241,8 @@ fn delete_depl_boot_entries( // For Systemd UKI as well, we use .conf files delete_type1_entry(deployment, boot_dir, deleting_staged) } + + Bootloader::None => unreachable!("Checked at install time"), } } diff --git a/crates/lib/src/bootc_composefs/finalize.rs b/crates/lib/src/bootc_composefs/finalize.rs index 0f8ffab08..f140122ed 100644 --- a/crates/lib/src/bootc_composefs/finalize.rs +++ b/crates/lib/src/bootc_composefs/finalize.rs @@ -120,6 +120,8 @@ pub(crate) async fn composefs_backend_finalize( let entries_dir = boot_dir.open_dir("loader")?; rename_exchange_bls_entries(&entries_dir)?; } + + Bootloader::None => unreachable!("Checked at install time"), }; Ok(()) diff --git a/crates/lib/src/bootc_composefs/gc.rs b/crates/lib/src/bootc_composefs/gc.rs index 7926d250c..142ba7478 100644 --- a/crates/lib/src/bootc_composefs/gc.rs +++ b/crates/lib/src/bootc_composefs/gc.rs @@ -80,6 +80,8 @@ fn list_bootloader_entries(storage: &Storage) -> Result> { .map(|entry| entry.get_verity()) .collect::, _>>()? } + + Bootloader::None => unreachable!("Checked at install time"), }; Ok(entries) diff --git a/crates/lib/src/bootc_composefs/rollback.rs b/crates/lib/src/bootc_composefs/rollback.rs index fd30e99a2..f8af3a9ae 100644 --- a/crates/lib/src/bootc_composefs/rollback.rs +++ b/crates/lib/src/bootc_composefs/rollback.rs @@ -233,6 +233,8 @@ pub(crate) async fn composefs_rollback( // We use BLS entries for systemd UKI as well rollback_composefs_entries(boot_dir, rollback_entry.bootloader.clone())?; } + + Bootloader::None => unreachable!("Checked at install time"), } if reverting { diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index 14fb0bf7b..1e9444435 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -751,6 +751,8 @@ pub(crate) async fn composefs_deployment_status_from( (is_rollback_queued, Some(bls_configs), None) } + + Bootloader::None => unreachable!("Checked at install time"), }; // Determine rollback deployment by matching extra deployment boot entries against entires read from /boot diff --git a/crates/lib/src/bootc_composefs/update.rs b/crates/lib/src/bootc_composefs/update.rs index 978a45360..a4558961e 100644 --- a/crates/lib/src/bootc_composefs/update.rs +++ b/crates/lib/src/bootc_composefs/update.rs @@ -189,6 +189,8 @@ pub(crate) fn validate_update( }, Bootloader::Systemd => rm_staged_type1_ent(boot_dir)?, + + Bootloader::None => unreachable!("Checked at install time"), } // Remove state directory diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index d3f2a40b6..4994deffc 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -373,6 +373,11 @@ pub(crate) struct InstallConfigOpts { #[clap(long)] #[serde(default)] pub(crate) bootupd_skip_boot_uuid: bool, + + /// The bootloader to use. + #[clap(long)] + #[serde(default)] + pub(crate) bootloader: Option, } #[derive(Debug, Default, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)] @@ -387,11 +392,6 @@ pub(crate) struct InstallComposefsOpts { #[serde(default)] pub(crate) insecure: bool, - /// The bootloader to use. - #[clap(long, requires = "composefs_backend")] - #[serde(default)] - pub(crate) bootloader: Option, - /// Name of the UKI addons to install without the ".efi.addon" suffix. /// This option can be provided multiple times if multiple addons are to be installed. #[clap(long, requires = "composefs_backend")] @@ -1590,6 +1590,12 @@ async fn prepare_install( composefs_options.composefs_backend = true; } + if composefs_options.composefs_backend + && matches!(config_opts.bootloader, Some(Bootloader::None)) + { + anyhow::bail!("Bootloader set to none is not supported with the composefs backend"); + } + // We need to access devices that are set up by the host udev bootc_mount::ensure_mirrored_host_mount("/dev")?; // We need to read our own container image (and any logically bound images) @@ -1642,10 +1648,20 @@ async fn prepare_install( .and_then(|b| b.skip_boot_uuid) .unwrap_or(false); } + + if config_opts.bootloader.is_none() { + config_opts.bootloader = config.bootloader.clone(); + } } else { tracing::debug!("No install configuration found"); } + if let Some(crate::spec::Bootloader::None) = config_opts.bootloader { + if cfg!(target_arch = "s390x") { + anyhow::bail!("Bootloader set to none is not supported for the s390x architecture"); + } + } + // Convert the keyfile to a hashmap because GKeyFile isnt Send for probably bad reasons. let prepareroot_config = { let kf = ostree_prepareroot::require_config_from_root(&rootfs)?; @@ -1695,7 +1711,7 @@ impl PostFetchState { // Determine bootloader type for the target system // Priority: user-specified > bootupd availability > systemd-boot fallback let detected_bootloader = { - if let Some(bootloader) = state.composefs_options.bootloader.clone() { + if let Some(bootloader) = state.config_opts.bootloader.clone() { bootloader } else { if crate::bootloader::supports_bootupd(d)? { @@ -1761,6 +1777,9 @@ async fn install_with_sysroot( Bootloader::Systemd => { anyhow::bail!("bootupd is required for ostree-based installs"); } + Bootloader::None => { + tracing::debug!("Skip bootloader installation due set to None"); + } } } tracing::debug!("Installed bootloader"); diff --git a/crates/lib/src/install/config.rs b/crates/lib/src/install/config.rs index 3c42abd58..2e82533e2 100644 --- a/crates/lib/src/install/config.rs +++ b/crates/lib/src/install/config.rs @@ -2,6 +2,7 @@ //! //! This module handles the TOML configuration file for `bootc install`. +use crate::spec::Bootloader; use anyhow::{Context, Result}; use clap::ValueEnum; use fn_error_context::context; @@ -97,6 +98,8 @@ pub(crate) struct InstallConfiguration { pub(crate) boot_mount_spec: Option, /// Bootupd configuration pub(crate) bootupd: Option, + /// Bootloader to use (grub, systemd, none) + pub(crate) bootloader: Option, } fn merge_basic(s: &mut Option, o: Option, _env: &EnvProperties) { @@ -180,6 +183,7 @@ impl Mergeable for InstallConfiguration { merge_basic(&mut self.root_mount_spec, other.root_mount_spec, env); merge_basic(&mut self.boot_mount_spec, other.boot_mount_spec, env); self.bootupd.merge(other.bootupd, env); + merge_basic(&mut self.bootloader, other.bootloader, env); if let Some(other_kargs) = other.kargs { self.kargs .get_or_insert_with(Default::default) @@ -810,3 +814,46 @@ skip-boot-uuid = false assert_eq!(install.bootupd.unwrap().skip_boot_uuid.unwrap(), true); } } + +#[test] +fn test_parse_bootloader() { + let env = EnvProperties { + sys_arch: "x86_64".to_string(), + }; + + // 1. Test parsing "none" + let c: InstallConfigurationToplevel = toml::from_str( + r##"[install] +bootloader = "none" +"##, + ) + .unwrap(); + assert_eq!(c.install.unwrap().bootloader, Some(Bootloader::None)); + + // 2. Test parsing "grub" + let c: InstallConfigurationToplevel = toml::from_str( + r##"[install] +bootloader = "grub" +"##, + ) + .unwrap(); + assert_eq!(c.install.unwrap().bootloader, Some(Bootloader::Grub)); + + // 3. Test merging + // Initial config has "systemd" + let mut install: InstallConfiguration = toml::from_str( + r#"bootloader = "systemd" +"#, + ) + .unwrap(); + + // Incoming config has "none" + let other = InstallConfiguration { + bootloader: Some(Bootloader::None), + ..Default::default() + }; + + // Merge should overwrite systemd with none + install.merge(other, &env); + assert_eq!(install.bootloader, Some(Bootloader::None)); +} diff --git a/crates/lib/src/spec.rs b/crates/lib/src/spec.rs index 8a0ace71d..3a5163e1c 100644 --- a/crates/lib/src/spec.rs +++ b/crates/lib/src/spec.rs @@ -185,12 +185,15 @@ pub struct BootEntryOstree { #[derive( clap::ValueEnum, Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, )] +#[serde(rename_all = "kebab-case")] pub enum Bootloader { /// Use Grub as the bootloader #[default] Grub, /// Use SystemdBoot as the bootloader Systemd, + /// Don't use a bootloader managed by bootc + None, } impl Display for Bootloader { @@ -198,6 +201,7 @@ impl Display for Bootloader { let string = match self { Bootloader::Grub => "grub", Bootloader::Systemd => "systemd", + Bootloader::None => "none", }; write!(f, "{}", string) @@ -211,6 +215,7 @@ impl FromStr for Bootloader { match value { "grub" => Ok(Self::Grub), "systemd" => Ok(Self::Systemd), + "none" => Ok(Self::None), unrecognized => Err(anyhow::anyhow!("Unrecognized bootloader: '{unrecognized}'")), } } diff --git a/crates/lib/src/store/mod.rs b/crates/lib/src/store/mod.rs index 7a516bae4..6f3405896 100644 --- a/crates/lib/src/store/mod.rs +++ b/crates/lib/src/store/mod.rs @@ -205,6 +205,7 @@ impl BootedStorage { Bootloader::Grub => physical_root.open_dir("boot").context("Opening boot")?, // NOTE: Handle XBOOTLDR partitions here if and when we use it Bootloader::Systemd => esp_mount.fd.try_clone().context("Cloning fd")?, + Bootloader::None => unreachable!("Checked at install time"), }; let storage = Storage { diff --git a/docs/src/bootloaders.md b/docs/src/bootloaders.md index d5e0d5187..e48b9cda9 100644 --- a/docs/src/bootloaders.md +++ b/docs/src/bootloaders.md @@ -21,3 +21,11 @@ by default (except on s390x). ## s390x bootc uses `zipl`. + +## none + +It is possible to skip bootloader installation entirely by using `--bootloader=none` (or `bootloader = "none"` in the [install] section of the config file). + +With this option, users can have explicit control over how the boot loading is handled, without bootc or bootupd intervention. + +NOTE: none is only supported for the Ostree backend and not for Composefs. It is also not supported for the s390x architecture. diff --git a/docs/src/man/bootc-install-to-filesystem.8.md b/docs/src/man/bootc-install-to-filesystem.8.md index de8d0af9e..821827316 100644 --- a/docs/src/man/bootc-install-to-filesystem.8.md +++ b/docs/src/man/bootc-install-to-filesystem.8.md @@ -125,6 +125,7 @@ is currently expected to be empty by default. Possible values: - grub - systemd + - none **--uki-addon**=*UKI_ADDON*