From 34c0e164281dbd85128dd5aec9f9f3b5f906d9f0 Mon Sep 17 00:00:00 2001 From: Javier Martinez Canillas Date: Mon, 16 Feb 2026 17:41:28 +0100 Subject: [PATCH 1/3] install: Don't restrict bootloader option to the composefs backend This command line argument can be also be used by the ostree backend. For example, when using Android Boot Images (aboot) there is a need to avoid calling to bootupd since all the boot setup is managed outside of bootc. A future change will add a bootloader=none option to force this behavior. Signed-off-by: Javier Martinez Canillas --- crates/lib/src/install.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index d3f2a40b6..50adb7612 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")] @@ -1695,7 +1695,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)? { From 3747e00160be0cfd315e33446cdb97a000604a2b Mon Sep 17 00:00:00 2001 From: Javier Martinez Canillas Date: Mon, 16 Feb 2026 17:42:43 +0100 Subject: [PATCH 2/3] install: Add a Bootloader::None option Currently, the bootc install workflow assumes that the bootloader must be managed by bootupd. This works well for server and edge environments, but it is too inflexible for embedded or custom platforms where the bootloader is managed externally (e.g., aboot for automotive use cases). In these scenarios, users want to install the filesystem content (OSTree commit, kernel, initramfs, etc), without bootc assuming that a boot or ESP partition exists that have to be setup or udpated by bootupd. By adding a --bootloader=none option users can have explicit control over how the boot loading is handled, without bootc or bootupd intervention. Note that so far only support for the ostree backend has been added and the bootloader=none option is not supported by the composefs backend. Signed-off-by: Javier Martinez Canillas --- crates/lib/src/bootc_composefs/boot.rs | 5 +++++ crates/lib/src/bootc_composefs/delete.rs | 2 ++ crates/lib/src/bootc_composefs/finalize.rs | 2 ++ crates/lib/src/bootc_composefs/gc.rs | 2 ++ crates/lib/src/bootc_composefs/rollback.rs | 2 ++ crates/lib/src/bootc_composefs/status.rs | 2 ++ crates/lib/src/bootc_composefs/update.rs | 2 ++ crates/lib/src/install.rs | 15 +++++++++++++++ crates/lib/src/spec.rs | 4 ++++ crates/lib/src/store/mod.rs | 1 + docs/src/bootloaders.md | 8 ++++++++ docs/src/man/bootc-install-to-filesystem.8.md | 1 + 12 files changed, 46 insertions(+) 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 50adb7612..f2924b3a0 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -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) @@ -1646,6 +1652,12 @@ async fn prepare_install( 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)?; @@ -1761,6 +1773,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/spec.rs b/crates/lib/src/spec.rs index 8a0ace71d..a9763153f 100644 --- a/crates/lib/src/spec.rs +++ b/crates/lib/src/spec.rs @@ -191,6 +191,8 @@ pub enum Bootloader { Grub, /// Use SystemdBoot as the bootloader Systemd, + /// Don't use a bootloader managed by bootc + None, } impl Display for Bootloader { @@ -198,6 +200,7 @@ impl Display for Bootloader { let string = match self { Bootloader::Grub => "grub", Bootloader::Systemd => "systemd", + Bootloader::None => "none", }; write!(f, "{}", string) @@ -211,6 +214,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* From 5f9cdd863eaa4ea2cc122eb75a975649a35c3759 Mon Sep 17 00:00:00 2001 From: Javier Martinez Canillas Date: Mon, 16 Feb 2026 17:44:01 +0100 Subject: [PATCH 3/3] install: Make the bootloader to also be a config file option There is already a --bootloader command line argument. Make it this to also be available as an install file configuration option. Signed-off-by: Javier Martinez Canillas --- crates/lib/src/install.rs | 4 +++ crates/lib/src/install/config.rs | 47 ++++++++++++++++++++++++++++++++ crates/lib/src/spec.rs | 1 + 3 files changed, 52 insertions(+) diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index f2924b3a0..4994deffc 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -1648,6 +1648,10 @@ 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"); } 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 a9763153f..3a5163e1c 100644 --- a/crates/lib/src/spec.rs +++ b/crates/lib/src/spec.rs @@ -185,6 +185,7 @@ 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]