diff --git a/contrib/packaging/switch-to-sdboot b/contrib/packaging/switch-to-sdboot index 6a6c130bcf..d4d13eb469 100755 --- a/contrib/packaging/switch-to-sdboot +++ b/contrib/packaging/switch-to-sdboot @@ -37,3 +37,8 @@ sdboot=$(ls /usr/lib/systemd/boot/efi/systemd-boot*.efi) sdboot_bn=$(basename "${sdboot}") # Override with our signed binary install -m 0644 "${src}/${sdboot_bn}" "${sdboot}" + +mkdir -p /usr/lib/bootc/install/systemd-boot +cat < /usr/lib/bootc/install/systemd-boot/loader.conf +timeout 1 +EOF diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 155bd6dce7..a29f922628 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -135,10 +135,6 @@ pub(crate) const EFI_UUID_FILE: &str = "efiuuid.cfg"; /// The EFI Linux directory pub(crate) const EFI_LINUX: &str = "EFI/Linux"; -/// Timeout for systemd-boot bootloader menu -const SYSTEMD_TIMEOUT: &str = "timeout 5"; -const SYSTEMD_LOADER_CONF_PATH: &str = "loader/loader.conf"; - pub(crate) const INITRD: &str = "initrd"; pub(crate) const VMLINUZ: &str = "vmlinuz"; @@ -1795,13 +1791,6 @@ fn write_systemd_uki_config( )?; } - // Write the timeout for bootloader menu if not exists - if !esp_dir.exists(SYSTEMD_LOADER_CONF_PATH) { - esp_dir - .atomic_write(SYSTEMD_LOADER_CONF_PATH, SYSTEMD_TIMEOUT) - .with_context(|| format!("Writing to {SYSTEMD_LOADER_CONF_PATH}"))?; - } - let esp_dir = esp_dir .reopen_as_ownedfd() .context("Reopening as owned fd")?; diff --git a/crates/lib/src/bootloader.rs b/crates/lib/src/bootloader.rs index 08a208d281..111627c4bc 100644 --- a/crates/lib/src/bootloader.rs +++ b/crates/lib/src/bootloader.rs @@ -262,6 +262,13 @@ pub(crate) fn install_via_bootupd( } } +/// Path in the container image where an image author may ship an initial +/// systemd-boot configuration. When present, it is copied verbatim onto the +/// ESP after `bootctl install` +const SYSTEMD_LOADER_CONF_SRC: &str = "usr/lib/bootc/install/systemd-boot/loader.conf"; +/// Destination of the systemd-boot loader configuration on the ESP +const SYSTEMD_LOADER_CONF_PATH: &str = "loader/loader.conf"; + /// Install systemd-boot using a pre-prepared boot root. #[context("Installing bootloader")] pub(crate) fn install_systemd_boot( @@ -358,6 +365,22 @@ pub(crate) fn install_systemd_boot( } } + // If the container image ships an initial systemd-boot configuration, + // copy it verbatim onto the ESP, overwriting the one generated by bootctl + if let Some(mut src) = prepared_root + .dir() + .open_optional(SYSTEMD_LOADER_CONF_SRC) + .context("Opening image systemd-boot loader config")? + { + let esp_dir = prepared_root.open_esp_dir()?; + + esp_dir + .atomic_replace_with(SYSTEMD_LOADER_CONF_PATH, |writer| { + std::io::copy(&mut src, writer) + }) + .with_context(|| format!("Writing to {SYSTEMD_LOADER_CONF_PATH}"))?; + } + Ok(()) } diff --git a/docs/src/bootc-bootloaders.7.md b/docs/src/bootc-bootloaders.7.md index 6f88aec2fb..486dfe2f3f 100644 --- a/docs/src/bootc-bootloaders.7.md +++ b/docs/src/bootc-bootloaders.7.md @@ -18,6 +18,17 @@ NOTE: systemd-boot is only supported for Composefs Backend and not for Ostree If bootupd is not present in the input container image, then systemd-boot will be used by default (except on s390x). +### Initial loader configuration + +`bootc` does not synthesize a systemd-boot [`loader.conf`](https://www.freedesktop.org/software/systemd/man/latest/loader.conf.html). +Instead, if the container image ships one at `/usr/lib/bootc/install/systemd-boot/loader.conf`, +`bootc` copies it verbatim onto the ESP (as `loader/loader.conf`) after running +`bootctl install`. This lets the base image or a package define the initial +configuration for eg. the boot menu timeout. + +If the image does not ship this file, the commented-out template written by `bootctl install` +is left in place. + ## s390x bootc uses `zipl`. diff --git a/tmt/tests/booted/readonly/030-test-composefs.nu b/tmt/tests/booted/readonly/030-test-composefs.nu index 801c5a7eec..8592ce36a6 100644 --- a/tmt/tests/booted/readonly/030-test-composefs.nu +++ b/tmt/tests/booted/readonly/030-test-composefs.nu @@ -30,6 +30,13 @@ if $expecting_composefs { let bootctl_output = (bootctl) if ($bootctl_output | str contains 'Product: systemd-boot') { + # Make sure our "timeout 1" is actually present (see ../../../../contrib/packaging/switch-to-sdboot:41) + + mkdir /var/tmp/efi + mount /dev/disk/by-partlabel/EFI-SYSTEM /var/tmp/efi + + assert (open /var/tmp/efi/loader/loader.conf | lines | any {|s| $s == "timeout 1" }) + find_root_eq_in_cmdline "systemd-boot" } } diff --git a/tmt/tests/booted/test-install-composefs-native.nu b/tmt/tests/booted/test-install-composefs-native.nu index 1a96ef8c56..00b4774ddf 100644 --- a/tmt/tests/booted/test-install-composefs-native.nu +++ b/tmt/tests/booted/test-install-composefs-native.nu @@ -62,7 +62,7 @@ def install [image: string, ...args: string] { let dropin = glob $"($MNT)/state/deploy/*/etc/tmpfiles.d/bootc-root-ssh.conf" | append (glob $"($MNT)/ostree/deploy/*/deploy/*/etc/tmpfiles.d/bootc-root-ssh.conf") | each {|p| {content: (open --raw $p | decode utf-8), mode: (stat -c %a $p | str trim)}} - umount $MNT + umount -l $MNT rm -f $DISK assert equal $dropin [{content: $"f~ /var/roothome/.ssh/authorized_keys 600 root root - ($KEY | encode base64)\n", mode: "644"}] "root ssh drop-in" match [$composefs $ostree] { diff --git a/tmt/tests/booted/test-multi-device-esp.nu b/tmt/tests/booted/test-multi-device-esp.nu index acf5bff4ff..91c2ba0345 100644 --- a/tmt/tests/booted/test-multi-device-esp.nu +++ b/tmt/tests/booted/test-multi-device-esp.nu @@ -37,7 +37,7 @@ const ROOT_TYPE = "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709" # Cleanup function for LVM and loop devices def cleanup [vg_name: string, loops: list, mountpoint: string] { # Unmount if mounted - do { umount $mountpoint } | complete | ignore + do { umount -l $mountpoint } | complete | ignore do { rmdir $mountpoint } | complete | ignore # Deactivate and remove LVM @@ -126,7 +126,7 @@ def setup_disk_with_root [ # Simple cleanup for non-LVM scenarios (single loop device, no VG) def cleanup_simple [loop: string, mountpoint: string] { - do { umount $mountpoint } | complete | ignore + do { umount -l $mountpoint } | complete | ignore do { rmdir $mountpoint } | complete | ignore if ($loop | path exists) { @@ -143,14 +143,14 @@ def validate_esp [esp_partition: string] { # Check for EFI directory with bootloader files let efi_dir = $"($esp_mount)/EFI" if not ($efi_dir | path exists) { - umount $esp_mount + umount -l $esp_mount rmdir $esp_mount error make {msg: $"ESP validation failed: EFI directory not found on ($esp_partition)"} } # Verify there's actual content in EFI (not just empty) let efi_contents = (ls $efi_dir | length) - umount $esp_mount + umount -l $esp_mount rmdir $esp_mount if $efi_contents == 0 { @@ -164,7 +164,7 @@ def with_esp [esp_partition: string, f: closure] { mkdir $esp_mount mount $esp_partition $esp_mount let r = (do $f $esp_mount) - umount $esp_mount + umount -l $esp_mount rmdir $esp_mount $r }