From 13fe596595c2f798e7c99810a7e15ba831e5282d Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 28 Jan 2026 11:20:42 +0530 Subject: [PATCH 01/10] dockerfile: Clear cargo's local cache before fetching Signed-off-by: Pragyan Poudyal --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 27abdbd37..d97312697 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,7 +34,8 @@ WORKDIR /src # We aren't using the full recommendations there, just the simple bits. # First we download all of our Rust dependencies # Note: Local path dependencies (from [patch] sections) are auto-detected and bind-mounted by the Justfile -RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome cargo fetch +RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome \ + rm -rf /var/roothome/.cargo/registry; cargo fetch # We always do a "from scratch" build # https://docs.fedoraproject.org/en-US/bootc/building-from-scratch/ From 162a7f002e25b22630cb47c6c912375761da6112 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 28 Jan 2026 11:23:23 +0530 Subject: [PATCH 02/10] xtask: Introduce `composefs` variant This is the Non UKI version, i.e. version with Type1 bootloader entries Signed-off-by: Pragyan Poudyal --- crates/tests-integration/src/container.rs | 6 ++--- crates/xtask/src/tmt.rs | 13 +++++++++++ crates/xtask/src/xtask.rs | 28 ++++++++++++++++++++++- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/crates/tests-integration/src/container.rs b/crates/tests-integration/src/container.rs index 2063aac2e..032d9842c 100644 --- a/crates/tests-integration/src/container.rs +++ b/crates/tests-integration/src/container.rs @@ -52,8 +52,8 @@ pub(crate) fn test_bootc_container_inspect() -> Result<()> { .expect("kernel.unified should be a boolean"); if let Some(variant) = std::env::var("BOOTC_variant").ok() { match variant.as_str() { - "ostree" => { - assert!(!unified, "Expected unified=false for ostree variant"); + v @ "ostree" | v @ "composefs" => { + assert!(!unified, "Expected unified=false for variant {v}"); // For traditional kernels, version should look like a uname (contains digits) assert!( version.chars().any(|c| c.is_ascii_digit()), @@ -159,7 +159,7 @@ fn test_variant_base_crosscheck() -> Result<()> { // TODO add this to `bootc status` or so? let boot_efi = Utf8Path::new("/boot/EFI"); match variant.as_str() { - "ostree" => { + "composefs" | "ostree" => { assert!(!boot_efi.try_exists()?); } "composefs-sealeduki-sdboot" => { diff --git a/crates/xtask/src/tmt.rs b/crates/xtask/src/tmt.rs index fa1d19146..2e917c158 100644 --- a/crates/xtask/src/tmt.rs +++ b/crates/xtask/src/tmt.rs @@ -29,6 +29,8 @@ const ENV_BOOTC_UPGRADE_IMAGE: &str = "BOOTC_upgrade_image"; // Distro identifiers const DISTRO_CENTOS_9: &str = "centos-9"; +const COMPOSEFS_KERNEL_ARGS: [&str; 1] = ["--karg=enforcing=0"]; + // Import the argument types from xtask.rs use crate::{RunTmtArgs, TmtProvisionArgs}; @@ -430,6 +432,17 @@ pub(crate) fn run_tmt(sh: &Shell, args: &RunTmtArgs) -> Result<()> { opts.push("--filesystem=xfs".to_string()); } } + + if args.composefs_backend { + opts.push("--filesystem=ext4".into()); + opts.push("--composefs-backend".into()); + opts.extend(COMPOSEFS_KERNEL_ARGS.map(|x| x.into())); + } + + if let Some(b) = &args.bootloader { + opts.push(format!("--bootloader={b}")); + } + opts }; diff --git a/crates/xtask/src/xtask.rs b/crates/xtask/src/xtask.rs index 977ddec65..6886f7fc9 100644 --- a/crates/xtask/src/xtask.rs +++ b/crates/xtask/src/xtask.rs @@ -5,13 +5,14 @@ //! end up as a lot of nontrivial bash code. use std::borrow::Cow; +use std::fmt::Display; use std::fs::File; use std::io::{BufRead, BufReader, BufWriter, Write}; use std::process::Command; use anyhow::{Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; -use clap::{Args, Parser, Subcommand}; +use clap::{Args, Parser, Subcommand, ValueEnum}; use fn_error_context::context; use xshell::{Shell, cmd}; @@ -76,6 +77,25 @@ pub(crate) struct LocalRustDepsArgs { pub(crate) format: String, } +/// Bootloader passed as --bootloader param for composefs builds +// TODO: Find a better way to share this Enum between this and crates/lib +#[derive(Debug, Clone, ValueEnum)] +pub enum Bootloader { + /// grub as bootloader + Grub, + /// systemd-boot as bootloader + Systemd, +} + +impl Display for Bootloader { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Bootloader::Grub => f.write_str("grub"), + Bootloader::Systemd => f.write_str("systemd"), + } + } +} + /// Arguments for run-tmt command #[derive(Debug, Args)] pub(crate) struct RunTmtArgs { @@ -101,6 +121,12 @@ pub(crate) struct RunTmtArgs { /// Preserve VMs after test completion (useful for debugging) #[arg(long)] pub(crate) preserve_vm: bool, + + #[arg(long)] + pub(crate) composefs_backend: bool, + + #[arg(long, requires = "composefs_backend")] + pub(crate) bootloader: Option, } /// Arguments for tmt-provision command From b3b92c243a58ed78248bf889fca7f6e2aa7f740a Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 28 Jan 2026 11:36:12 +0530 Subject: [PATCH 03/10] tmt/tests: Add composefs/ostree distinguishing checks The tests assume an ostree system right now, but we want the same tests for composefs systems as well. Add a `is_composefs` function to differentitate between the two backends during testing Signed-off-by: Pragyan Poudyal --- tmt/tests/booted/readonly/001-test-status.nu | 2 +- .../010-test-bootc-container-store.nu | 2 +- .../readonly/011-test-ostree-ext-cli.nu | 2 +- .../booted/readonly/011-test-resolvconf.nu | 2 +- .../booted/readonly/012-test-unit-status.nu | 2 +- tmt/tests/booted/readonly/015-test-fsck.nu | 2 +- .../booted/readonly/017-test-bound-storage.nu | 2 +- .../booted/readonly/030-test-composefs.nu | 2 +- tmt/tests/booted/tap.nu | 5 +++ .../booted/test-image-pushpull-upgrade.nu | 37 +++++++++++-------- .../test-install-to-filesystem-var-mount.sh | 11 ++++++ tmt/tests/booted/test-soft-reboot.nu | 9 ++++- .../booted/test-switch-mutate-in-place.nu | 4 +- 13 files changed, 53 insertions(+), 29 deletions(-) diff --git a/tmt/tests/booted/readonly/001-test-status.nu b/tmt/tests/booted/readonly/001-test-status.nu index 80c29028a..f239a5485 100644 --- a/tmt/tests/booted/readonly/001-test-status.nu +++ b/tmt/tests/booted/readonly/001-test-status.nu @@ -14,7 +14,7 @@ assert ($opts | any { |o| $o == "ro" }) "/sysroot should be mounted read-only" let st = bootc status --json | from json # Detect composefs by checking if composefs field is present -let is_composefs = ($st.status.booted.composefs? != null) +let is_composefs = (tap is_composefs) assert equal $st.apiVersion org.containers.bootc/v1 diff --git a/tmt/tests/booted/readonly/010-test-bootc-container-store.nu b/tmt/tests/booted/readonly/010-test-bootc-container-store.nu index ef23a039d..e344ff8e0 100644 --- a/tmt/tests/booted/readonly/010-test-bootc-container-store.nu +++ b/tmt/tests/booted/readonly/010-test-bootc-container-store.nu @@ -5,7 +5,7 @@ tap begin "verify bootc-owned container storage" # Detect composefs by checking if composefs field is present let st = bootc status --json | from json -let is_composefs = ($st.status.booted.composefs? != null) +let is_composefs = (tap is_composefs) if $is_composefs { print "# TODO composefs: skipping test - /usr/lib/bootc/storage doesn't exist with composefs" diff --git a/tmt/tests/booted/readonly/011-test-ostree-ext-cli.nu b/tmt/tests/booted/readonly/011-test-ostree-ext-cli.nu index edac11cba..817bc6003 100644 --- a/tmt/tests/booted/readonly/011-test-ostree-ext-cli.nu +++ b/tmt/tests/booted/readonly/011-test-ostree-ext-cli.nu @@ -8,7 +8,7 @@ tap begin "verify bootc wrapping ostree-ext" # Parse the status and get the booted image let st = bootc status --json | from json # Detect composefs by checking if composefs field is present -let is_composefs = ($st.status.booted.composefs? != null) +let is_composefs = (tap is_composefs) if $is_composefs { print "# TODO composefs: skipping test - ostree-container commands don't work with composefs" } else { diff --git a/tmt/tests/booted/readonly/011-test-resolvconf.nu b/tmt/tests/booted/readonly/011-test-resolvconf.nu index 8f040d665..fa61a46be 100644 --- a/tmt/tests/booted/readonly/011-test-resolvconf.nu +++ b/tmt/tests/booted/readonly/011-test-resolvconf.nu @@ -6,7 +6,7 @@ tap begin "verify there's not an empty /etc/resolv.conf in the image" let st = bootc status --json | from json # Detect composefs by checking if composefs field is present -let is_composefs = ($st.status.booted.composefs? != null) +let is_composefs = (tap is_composefs) if $is_composefs { print "# TODO composefs: skipping test - ostree commands don't work with composefs" } else { diff --git a/tmt/tests/booted/readonly/012-test-unit-status.nu b/tmt/tests/booted/readonly/012-test-unit-status.nu index ebc5363e8..4271a9653 100644 --- a/tmt/tests/booted/readonly/012-test-unit-status.nu +++ b/tmt/tests/booted/readonly/012-test-unit-status.nu @@ -6,7 +6,7 @@ tap begin "verify our systemd units" # Detect composefs by checking if composefs field is present let st = bootc status --json | from json -let is_composefs = ($st.status.booted.composefs? != null) +let is_composefs = (tap is_composefs) if $is_composefs { print "# TODO composefs: skipping test - bootc-status-updated.path watches /ostree/bootc which doesn't exist with composefs" diff --git a/tmt/tests/booted/readonly/015-test-fsck.nu b/tmt/tests/booted/readonly/015-test-fsck.nu index 555842681..8700a5f66 100644 --- a/tmt/tests/booted/readonly/015-test-fsck.nu +++ b/tmt/tests/booted/readonly/015-test-fsck.nu @@ -5,7 +5,7 @@ tap begin "Run fsck" # Detect composefs by checking if composefs field is present let st = bootc status --json | from json -let is_composefs = ($st.status.booted.composefs? != null) +let is_composefs = (tap is_composefs) if $is_composefs { print "# TODO composefs: skipping test - fsck requires ostree-booted host" diff --git a/tmt/tests/booted/readonly/017-test-bound-storage.nu b/tmt/tests/booted/readonly/017-test-bound-storage.nu index 9d5640356..56aaea7ea 100644 --- a/tmt/tests/booted/readonly/017-test-bound-storage.nu +++ b/tmt/tests/booted/readonly/017-test-bound-storage.nu @@ -10,7 +10,7 @@ if not (bootc_testlib have_hostexports) { bootc status let st = bootc status --json | from json -let is_composefs = ($st.status.booted.composefs? != null) +let is_composefs = (tap is_composefs) if $is_composefs { # TODO we don't have imageDigest yet in status exit 0 diff --git a/tmt/tests/booted/readonly/030-test-composefs.nu b/tmt/tests/booted/readonly/030-test-composefs.nu index b7f028e44..81e2acc4b 100644 --- a/tmt/tests/booted/readonly/030-test-composefs.nu +++ b/tmt/tests/booted/readonly/030-test-composefs.nu @@ -9,7 +9,7 @@ def parse_cmdline [] { # Detect composefs by checking if composefs field is present let st = bootc status --json | from json -let is_composefs = ($st.status.booted.composefs? != null) +let is_composefs = (tap is_composefs) let expecting_composefs = ($env.BOOTC_variant? | default "" | find "composefs") != null if $expecting_composefs { assert $is_composefs diff --git a/tmt/tests/booted/tap.nu b/tmt/tests/booted/tap.nu index 096638fa0..ae1576dad 100644 --- a/tmt/tests/booted/tap.nu +++ b/tmt/tests/booted/tap.nu @@ -13,3 +13,8 @@ export def ok [] { export def fail [] { print "not ok" } + +export def is_composefs [] { + let st = bootc status --json | from json + $st.status.booted.composefs? != null +} diff --git a/tmt/tests/booted/test-image-pushpull-upgrade.nu b/tmt/tests/booted/test-image-pushpull-upgrade.nu index 39f757b56..78a3354fa 100644 --- a/tmt/tests/booted/test-image-pushpull-upgrade.nu +++ b/tmt/tests/booted/test-image-pushpull-upgrade.nu @@ -22,7 +22,7 @@ const quoted_karg = '"thisarg=quoted with spaces"' bootc status let st = bootc status --json | from json let booted = $st.status.booted.image -let is_composefs = ($st.status.booted.composefs? != null) +let is_composefs = (tap is_composefs) # Parse the kernel commandline into a list. # This is not a proper parser, but good enough @@ -54,7 +54,11 @@ RUN echo test content > /usr/share/blah.txt let v = podman run --rm localhost/bootc-derived cat /usr/share/blah.txt | str trim assert equal $v "test content" - let orig_root_mtime = ls -Dl /ostree/bootc | get modified + mut orig_root_mtime = null; + + if not $is_composefs { + $orig_root_mtime = ls -Dl /ostree/bootc | get modified + } # Now, fetch it back into the bootc storage! # We also test the progress API here @@ -68,24 +72,25 @@ RUN echo test content > /usr/share/blah.txt systemd-run -u test-cat-progress -- /bin/bash -c $"exec cat ($progress_fifo) > ($progress_json)" # nushell doesn't do fd passing right now either, so run via bash bash -c $"bootc switch --progress-fd 3 --transport containers-storage localhost/bootc-derived 3>($progress_fifo)" - # Now, let's do some checking of the progress json - let progress = open --raw $progress_json | from json -o - sanity_check_switch_progress_json $progress - # Check that /run/reboot-required exists and is not empty - let rr_meta = (ls /run/reboot-required | first) - assert ($rr_meta.size > 0b) + if not $is_composefs { + # Now, let's do some checking of the progress json + let progress = open --raw $progress_json | from json -o + sanity_check_switch_progress_json $progress - # Verify that we logged to the journal - journalctl _MESSAGE_ID=3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7 + # Check that /run/reboot-required exists and is not empty + let rr_meta = (ls /run/reboot-required | first) + assert ($rr_meta.size > 0b) - # The mtime should change on modification - let new_root_mtime = ls -Dl /ostree/bootc | get modified - assert ($new_root_mtime > $orig_root_mtime) + # Verify that we logged to the journal + journalctl _MESSAGE_ID=3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7 - # Test for https://github.com/ostreedev/ostree/issues/3544 - # Add a quoted karg using rpm-ostree if available - if not $is_composefs { + # The mtime should change on modification + let new_root_mtime = ls -Dl /ostree/bootc | get modified + assert ($new_root_mtime > $orig_root_mtime) + + # Test for https://github.com/ostreedev/ostree/issues/3544 + # Add a quoted karg using rpm-ostree if available # Check rpm-ostree and rpm-ostreed service status before run rpm-ostree # And collect info for flaky error "error: System transaction in progress" rpm-ostree status diff --git a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh index 5fe76d8a6..25fe1dddc 100644 --- a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh +++ b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh @@ -114,6 +114,16 @@ echo "Filesystem layout:" mount | grep /var/mnt/target || true df -h /var/mnt/target /var/mnt/target/boot /var/mnt/target/boot/efi /var/mnt/target/var +COMPOSEFS_BACKEND=() + +is_composefs=$(bootc status --json | jq '.status.booted.composefs') + +if [[ $is_composefs != "null" ]]; then + COMPOSEFS_BACKEND=("--composefs-backend") +fi + +echo "${COMPOSEFS_BACKEND[@]}" + # Run bootc install to-filesystem from within the container image under test podman run \ --rm --privileged \ @@ -124,6 +134,7 @@ podman run \ "$TARGET_IMAGE" \ bootc install to-filesystem \ --disable-selinux \ + "${COMPOSEFS_BACKEND[@]}" \ --karg=root=UUID="$ROOT_UUID" \ --root-mount-spec=UUID="$ROOT_UUID" \ --boot-mount-spec=UUID="$BOOT_UUID" \ diff --git a/tmt/tests/booted/test-soft-reboot.nu b/tmt/tests/booted/test-soft-reboot.nu index dd3374e13..2b3a81316 100644 --- a/tmt/tests/booted/test-soft-reboot.nu +++ b/tmt/tests/booted/test-soft-reboot.nu @@ -41,8 +41,13 @@ RUN echo test content > /usr/share/testfile-for-soft-reboot.txt assert ("/run/nextroot" | path exists) - # See ../bug-soft-reboot.md - TMT cannot handle systemd soft-reboots - ostree admin prepare-soft-reboot --reset + if not (tap is_composefs) { + # See ../bug-soft-reboot.md - TMT cannot handle systemd soft-reboots + ostree admin prepare-soft-reboot --reset + } else { + bootc internals prep-soft-reboot --reset + } + # https://tmt.readthedocs.io/en/stable/stories/features.html#reboot-during-test tmt-reboot } diff --git a/tmt/tests/booted/test-switch-mutate-in-place.nu b/tmt/tests/booted/test-switch-mutate-in-place.nu index c112c8410..1c12907db 100644 --- a/tmt/tests/booted/test-switch-mutate-in-place.nu +++ b/tmt/tests/booted/test-switch-mutate-in-place.nu @@ -9,9 +9,7 @@ use bootc_testlib.nu # See https://github.com/bootc-dev/bootc/issues/1854 -let st = bootc status --json | from json -let is_composefs = ($st.status.booted.composefs? != null) -if not $is_composefs { +if not (tap is_composefs) { # This is aiming to reproduce an environment closer to the Anaconda case # where we're chrooted into a non-booted system. TODO: What we really want # is to add `bootc switch --sysroot` too. From 1033d0349f685eaa80aded59e81bf3a4fe92b596 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 28 Jan 2026 11:40:17 +0530 Subject: [PATCH 04/10] justfile/ci: Add composefs tests Add tests for composefs backend for grub and sdboot. Update the testing matrix in CI Signed-off-by: Pragyan Poudyal --- .github/workflows/ci.yml | 20 ++++++++++++++++---- Justfile | 26 +++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4936d028b..a0c30f9cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -161,7 +161,7 @@ jobs: matrix: # No fedora-44 due to https://bugzilla.redhat.com/show_bug.cgi?id=2429501 test_os: [fedora-43, centos-9, centos-10] - variant: [ostree, composefs-sealeduki-sdboot] + variant: [ostree, composefs-sealeduki-sdboot, composefs-sdboot, composefs-grub] exclude: # centos-9 UKI is experimental/broken (https://github.com/bootc-dev/bootc/issues/1812) - test_os: centos-9 @@ -182,7 +182,18 @@ jobs: run: | BASE=$(just pullspec-for-os base ${{ matrix.test_os }}) echo "BOOTC_base=${BASE}" >> $GITHUB_ENV - echo "BOOTC_variant=${{ matrix.variant }}" >> $GITHUB_ENV + + case "${{ matrix.variant }}" in + composefs-grub|composefs-sdboot) + echo "BOOTC_variant=composefs" >> $GITHUB_ENV + ;; + + *) + echo "BOOTC_variant=${{ matrix.variant }}" >> $GITHUB_ENV + ;; + esac + + if [ "${{ matrix.variant }}" = "composefs-sealeduki-sdboot" ]; then BUILDROOTBASE=$(just pullspec-for-os buildroot-base ${{ matrix.test_os }}) @@ -211,11 +222,12 @@ jobs: - name: Run TMT integration tests run: | - if [ "${{ matrix.variant }}" = "composefs-sealeduki-sdboot" ]; then - just test-composefs + if [[ "${{ matrix.variant }}" = composefs* ]]; then + just "test-${{ matrix.variant }}" else just test-tmt integration fi + just clean-local-images - name: Archive TMT logs diff --git a/Justfile b/Justfile index dc865a38f..81cfe0acf 100644 --- a/Justfile +++ b/Justfile @@ -105,9 +105,32 @@ test-container: build build-units # Build and test sealed composefs images [group('core')] -test-composefs: +test-composefs-sealeduki-sdboot: just variant=composefs-sealeduki-sdboot test-tmt readonly local-upgrade-reboot +[group('core')] +test-composefs bootloader: + just variant=composefs test-tmt --composefs-backend --bootloader {{bootloader}} \ + readonly \ + bib-build \ + download-only \ + image-pushpull-upgrade \ + image-upgrade-reboot \ + install-outside-container \ + install-to-filesystem-var-mount \ + soft-reboot \ + usroverlay + +# Build and test composefs images booted using Type1 boot entries and systemd-boot as the bootloader +[group('core')] +test-composefs-sdboot: + just test-composefs systemd + +# Build and test composefs images booted using Type1 boot entries and grub as the bootloader +[group('core')] +test-composefs-grub: + just test-composefs grub + # Run cargo fmt and clippy checks in container [group('core')] validate: @@ -220,6 +243,7 @@ clean-local-images: podman image prune -f podman rmi {{fedora-coreos}} -f + # Build packages (RPM) into target/packages/ [group('maintenance')] package: From 176c381db790dd27d9d6e9942e7d13f0671b62c5 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 29 Jan 2026 19:43:55 +0530 Subject: [PATCH 05/10] tests: Install sdboot unsigned for composefs variant Signed-off-by: Pragyan Poudyal --- Dockerfile | 4 +++- contrib/packaging/install-unsigned-sdboot | 13 +++++++++++++ .../booted/test-install-to-filesystem-var-mount.sh | 3 ++- 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100755 contrib/packaging/install-unsigned-sdboot diff --git a/Dockerfile b/Dockerfile index d97312697..f4bb11d7c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -144,13 +144,15 @@ RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp # Perform all filesystem transformations except generating the sealed UKI (if configured) FROM base as base-penultimate ARG variant -# Switch to a signed systemd-boot, if configured +# Switch to systemd-boot (signed or unsigned), if configured RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ --mount=type=bind,from=packaging,src=/,target=/run/packaging \ --mount=type=bind,from=sdboot-signed,src=/,target=/run/sdboot-signed </dev/null; then + rpm -e bootupd + rm -vrf /usr/lib/bootupd/updates +fi + +# Install the unsigned systemd-boot RPM that was downloaded by the tools stage +# The RPM is available in /run/sdboot-signed/out (copied from tools stage) +rpm -Uvh /run/sdboot-signed/out/*.rpm \ No newline at end of file diff --git a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh index 25fe1dddc..662292369 100644 --- a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh +++ b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh @@ -119,7 +119,8 @@ COMPOSEFS_BACKEND=() is_composefs=$(bootc status --json | jq '.status.booted.composefs') if [[ $is_composefs != "null" ]]; then - COMPOSEFS_BACKEND=("--composefs-backend") + COMPOSEFS_BACKEND+=("--composefs-backend") + COMPOSEFS_BACKEND+=("--filesystem=ext4") fi echo "${COMPOSEFS_BACKEND[@]}" From a83550c6ab1cae79c019e505d484cad559bfb050 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Mon, 2 Feb 2026 16:05:27 +0530 Subject: [PATCH 06/10] composefs/boot: Copy system-boot binaries manually On systemd v258, bootctl doesn't copy the systemd-boot binaries to the ESP, so we copy the binaries ourselves if we don't find them in the final ESP Signed-off-by: Pragyan Poudyal --- .github/workflows/ci.yml | 2 - crates/lib/src/bootc_composefs/boot.rs | 1 + crates/lib/src/bootloader.rs | 70 +++++++++++++++++++++++++- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0c30f9cf..c5482919a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -193,8 +193,6 @@ jobs: ;; esac - - if [ "${{ matrix.variant }}" = "composefs-sealeduki-sdboot" ]; then BUILDROOTBASE=$(just pullspec-for-os buildroot-base ${{ matrix.test_os }}) echo "BOOTC_buildroot_base=${BUILDROOTBASE}" >> $GITHUB_ENV diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 801a019bd..192ba9e17 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -1248,6 +1248,7 @@ pub(crate) async fn setup_composefs_boot( &state.config_opts, None, get_secureboot_keys(&mounted_fs, BOOTC_AUTOENROLL_PATH)?, + &mounted_fs, )?; } diff --git a/crates/lib/src/bootloader.rs b/crates/lib/src/bootloader.rs index be1d5882b..ae92b6bcf 100644 --- a/crates/lib/src/bootloader.rs +++ b/crates/lib/src/bootloader.rs @@ -158,13 +158,14 @@ pub(crate) fn install_via_bootupd( } } -#[context("Installing bootloader")] +#[context("Installing systemd boot")] pub(crate) fn install_systemd_boot( device: &PartitionTable, _rootfs: &Utf8Path, _configopts: &crate::install::InstallConfigOpts, _deployment_path: Option<&str>, autoenroll: Option, + mounted_erofs: &Dir, ) -> Result<()> { let esp_part = device .find_partition_of_type(discoverable_partition_specification::ESP) @@ -180,6 +181,73 @@ pub(crate) fn install_systemd_boot( .log_debug() .run_inherited_with_cmd_context()?; + // Check for systemd-boot binaries in the EFI/systemd directory + // systemd v258 won't copy the binary if an EFI booted system is not detected + let systemd_dir = esp_mount.fd.open_dir_optional("EFI/systemd")?; + + let systemd_boot_found = if let Some(dir) = systemd_dir { + let mut found = false; + for entry in dir.entries()? { + let entry = entry?; + let name = entry.file_name(); + + if let Some(name_str) = name.to_str() { + if name_str.starts_with("systemd-boot") && name_str.ends_with(".efi") { + found = true; + break; + } + } + } + + found + } else { + false + }; + + if !systemd_boot_found { + println!("Copying systemd-boot binary manually"); + + // Find the systemd-boot binary in the source directory + let boot_dir = mounted_erofs.open_dir("usr/lib/systemd/boot/efi")?; + let mut systemd_boot_binary = None; + + for entry in boot_dir.entries()? { + let entry = entry?; + let name = entry.file_name(); + if let Some(name_str) = name.to_str() { + if name_str.starts_with("systemd-boot") && name_str.ends_with(".efi") { + systemd_boot_binary = Some(name_str.to_string()); + break; + } + } + } + + let binary_name = systemd_boot_binary + .ok_or_else(|| anyhow::anyhow!("No systemd-boot binary found in source"))?; + + let src_path = format!("usr/lib/systemd/boot/efi/{}", binary_name); + let systemd_dest_path = format!("EFI/systemd/{}", binary_name); + + // Determine the appropriate BOOT binary name based on architecture + let boot_binary_name = if binary_name.contains("x64") { + "BOOTX64.EFI" + } else if binary_name.contains("ia32") { + "BOOTIA32.EFI" + } else if binary_name.contains("aa64") { + "BOOTAA64.EFI" + } else { + "BOOTX64.EFI" // Default fallback + }; + + mounted_erofs.copy(&src_path, &esp_mount.fd, &systemd_dest_path)?; + + mounted_erofs.copy( + &src_path, + &esp_mount.fd, + &format!("EFI/BOOT/{}", boot_binary_name), + )?; + } + if let Some(SecurebootKeys { dir, keys }) = autoenroll { let path = esp_path.join(SYSTEMD_KEY_DIR); create_dir_all(&path)?; From 27fd63fcb1772b40d8ea6c04ba589a983e5af5f7 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 3 Feb 2026 11:17:28 +0530 Subject: [PATCH 07/10] Use signed systemd-boot Signed-off-by: Pragyan Poudyal --- .github/workflows/ci.yml | 25 +++++++++++++++---- Dockerfile | 5 ++-- Justfile | 6 +++-- crates/lib/src/bootloader.rs | 2 +- .../booted/test-image-pushpull-upgrade.nu | 2 +- .../test-install-to-filesystem-var-mount.sh | 1 - 6 files changed, 28 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5482919a..7c0e1af4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -160,8 +160,8 @@ jobs: fail-fast: false matrix: # No fedora-44 due to https://bugzilla.redhat.com/show_bug.cgi?id=2429501 - test_os: [fedora-43, centos-9, centos-10] - variant: [ostree, composefs-sealeduki-sdboot, composefs-sdboot, composefs-grub] + test_os: [fedora-43, centos-10] + variant: [composefs-sdboot, composefs-grub] exclude: # centos-9 UKI is experimental/broken (https://github.com/bootc-dev/bootc/issues/1812) - test_os: centos-9 @@ -182,14 +182,28 @@ jobs: run: | BASE=$(just pullspec-for-os base ${{ matrix.test_os }}) echo "BOOTC_base=${BASE}" >> $GITHUB_ENV + echo "RUST_BACKTRACE=full" >> $GITHUB_ENV + echo "RUST_LOG=trace" >> $GITHUB_ENV case "${{ matrix.variant }}" in - composefs-grub|composefs-sdboot) + composefs-grub) echo "BOOTC_variant=composefs" >> $GITHUB_ENV + echo "BOOTC_bootloader=grub" >> $GITHUB_ENV + ;; + + composefs-sdboot) + echo "BOOTC_variant=composefs" >> $GITHUB_ENV + echo "BOOTC_bootloader=systemd" >> $GITHUB_ENV ;; - *) + composefs-sealeduki-sdboot) echo "BOOTC_variant=${{ matrix.variant }}" >> $GITHUB_ENV + echo "BOOTC_bootloader=systemd" >> $GITHUB_ENV + ;; + + ostree) + echo "BOOTC_variant=${{ matrix.variant }}" >> $GITHUB_ENV + echo "BOOTC_bootloader=grub" >> $GITHUB_ENV ;; esac @@ -206,7 +220,8 @@ jobs: - name: Build container run: | - BOOTC_SKIP_PACKAGE=1 just build + BOOTC_SKIP_PACKAGE=1 just bootloader=$BOOTC_bootloader build + # Extra cross-check (duplicating the integration test) that we're using the right base used_vid=$(podman run --rm localhost/bootc bash -c '. /usr/lib/os-release && echo ${ID}-${VERSION_ID}') test ${{ matrix.test_os }} = "${used_vid}" diff --git a/Dockerfile b/Dockerfile index f4bb11d7c..af22620ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -144,15 +144,14 @@ RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp # Perform all filesystem transformations except generating the sealed UKI (if configured) FROM base as base-penultimate ARG variant +ARG bootloader # Switch to systemd-boot (signed or unsigned), if configured RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ --mount=type=bind,from=packaging,src=/,target=/run/packaging \ --mount=type=bind,from=sdboot-signed,src=/,target=/run/sdboot-signed < /usr/share/blah.txt let v = podman run --rm localhost/bootc-derived cat /usr/share/blah.txt | str trim assert equal $v "test content" - mut orig_root_mtime = null; + mut orig_root_mtime = 0; if not $is_composefs { $orig_root_mtime = ls -Dl /ostree/bootc | get modified diff --git a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh index 662292369..9d4c6debb 100644 --- a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh +++ b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh @@ -120,7 +120,6 @@ is_composefs=$(bootc status --json | jq '.status.booted.composefs') if [[ $is_composefs != "null" ]]; then COMPOSEFS_BACKEND+=("--composefs-backend") - COMPOSEFS_BACKEND+=("--filesystem=ext4") fi echo "${COMPOSEFS_BACKEND[@]}" From c1474cec0cbc9a0353fca16f5e0a5fda32c3b808 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Fri, 6 Feb 2026 13:16:49 +0530 Subject: [PATCH 08/10] Use insecure UEFI for sdboot Signed-off-by: Pragyan Poudyal --- crates/xtask/src/tmt.rs | 15 +++++++--- crates/xtask/src/xtask.rs | 2 +- .../test-install-to-filesystem-var-mount.sh | 28 ++++++++++++++++--- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/crates/xtask/src/tmt.rs b/crates/xtask/src/tmt.rs index 2e917c158..bd5c3ec49 100644 --- a/crates/xtask/src/tmt.rs +++ b/crates/xtask/src/tmt.rs @@ -32,7 +32,7 @@ const DISTRO_CENTOS_9: &str = "centos-9"; const COMPOSEFS_KERNEL_ARGS: [&str; 1] = ["--karg=enforcing=0"]; // Import the argument types from xtask.rs -use crate::{RunTmtArgs, TmtProvisionArgs}; +use crate::{Bootloader, RunTmtArgs, TmtProvisionArgs}; /// Generate a random alphanumeric suffix for VM names fn generate_random_suffix() -> String { @@ -111,7 +111,11 @@ const DEFAULT_SB_KEYS_DIR: &str = "target/test-secureboot"; /// /// For sealed images, secure boot keys must be present or an error is returned. #[context("Building firmware arguments")] -fn build_firmware_args(sh: &Shell, image: &str) -> Result> { +fn build_firmware_args( + sh: &Shell, + image: &str, + bootloader: &Option, +) -> Result> { let is_sealed = is_sealed_image(sh, image)?; let sb_keys_dir = Utf8Path::new(DEFAULT_SB_KEYS_DIR); @@ -133,6 +137,8 @@ fn build_firmware_args(sh: &Shell, image: &str) -> Result> { sb_keys_dir ); } + } else if matches!(bootloader, Some(Bootloader::Systemd)) { + vec!["--firmware=uefi-insecure".into()] } else { Vec::new() }; @@ -310,7 +316,7 @@ pub(crate) fn run_tmt(sh: &Shell, args: &RunTmtArgs) -> Result<()> { println!("Detected distro: {}", distro); println!("Detected VARIANT_ID: {variant_id}"); - let firmware_args = build_firmware_args(sh, image)?; + let firmware_args = build_firmware_args(sh, image, &args.bootloader)?; // Create tmt-workdir and copy tmt bits to it // This works around https://github.com/teemtee/tmt/issues/4062 @@ -697,7 +703,8 @@ pub(crate) fn tmt_provision(sh: &Shell, args: &TmtProvisionArgs) -> Result<()> { println!(" Image: {}", image); println!(" VM name: {}\n", vm_name); - let firmware_args = build_firmware_args(sh, image)?; + // TODO: Send bootloader param here + let firmware_args = build_firmware_args(sh, image, &None)?; // Launch VM with bcvk // Use ds=iid-datasource-none to disable cloud-init for faster boot diff --git a/crates/xtask/src/xtask.rs b/crates/xtask/src/xtask.rs index 6886f7fc9..cb6afe29f 100644 --- a/crates/xtask/src/xtask.rs +++ b/crates/xtask/src/xtask.rs @@ -79,7 +79,7 @@ pub(crate) struct LocalRustDepsArgs { /// Bootloader passed as --bootloader param for composefs builds // TODO: Find a better way to share this Enum between this and crates/lib -#[derive(Debug, Clone, ValueEnum)] +#[derive(Debug, Clone, ValueEnum, PartialEq, Eq)] pub enum Bootloader { /// grub as bootloader Grub, diff --git a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh index 9d4c6debb..88a0a5214 100644 --- a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh +++ b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh @@ -120,6 +120,8 @@ is_composefs=$(bootc status --json | jq '.status.booted.composefs') if [[ $is_composefs != "null" ]]; then COMPOSEFS_BACKEND+=("--composefs-backend") + tune2fs -O verity /dev/BL/var02 + tune2fs -O verity /dev/BL/root02 fi echo "${COMPOSEFS_BACKEND[@]}" @@ -142,9 +144,27 @@ podman run \ # Verify the installation succeeded echo "Verifying installation..." -test -d /var/mnt/target/ostree -test -d /var/mnt/target/ostree/repo -# Verify bootloader was installed (grub2 or loader for different configurations) -test -d /var/mnt/target/boot/grub2 || test -d /var/mnt/target/boot/loader + +if [[ $is_composefs == "null" ]]; then + test -d /var/mnt/target/ostree + test -d /var/mnt/target/ostree/repo + + # Verify bootloader was installed (grub2 or loader for different configurations) + test -d /var/mnt/target/boot/grub2 || test -d /var/mnt/target/boot/loader +else + test -d /var/mnt/target/composefs + ls -lahR /var/mnt/target/boot + + bootloader=$(bootc status --json | jq '.status.booted.composefs.bootloader' | tr '[:upper:]' '[:lower:]') + + if [[ $bootloader == "grub" ]]; then + test -d /var/mnt/target/boot/grub2 || test -d /var/mnt/target/boot/loader + else + test -d /var/mnt/target/boot/efi/EFI + test -d /var/mnt/target/boot/efi/loader/entries + fi + +fi + echo "Installation to-filesystem with separate /var mount succeeded!" From 5bd5aac85a6dea06e4635c3955811f36c15aa2d0 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Fri, 6 Feb 2026 15:34:58 +0530 Subject: [PATCH 09/10] Remove root= cmdline for sdboot Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/boot.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 192ba9e17..25b31ae96 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -68,7 +68,7 @@ use std::path::Path; use anyhow::{Context, Result, anyhow, bail}; use bootc_blockdev::find_parent_devices; -use bootc_kernel_cmdline::utf8::{Cmdline, Parameter}; +use bootc_kernel_cmdline::utf8::{Cmdline, Parameter, ParameterKey}; use bootc_mount::inspect_filesystem_of_dir; use bootc_mount::tempmount::TempMount; use camino::{Utf8Path, Utf8PathBuf}; @@ -566,6 +566,12 @@ pub(crate) fn setup_composefs_bls_boot( } }; + // Remove "root=" from kernel cmdline as systemd-auto-gpt-generator should use DPS + // UUID + if bootloader == Bootloader::Systemd { + cmdline_refs.remove(&ParameterKey::from("root")); + } + let is_upgrade = matches!(setup_type, BootSetupType::Upgrade(..)); let current_root = if is_upgrade { From 8ebc656c5b55a42a97423e45144a54a9a6e38374 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Fri, 6 Feb 2026 15:35:12 +0530 Subject: [PATCH 10/10] Make tests work for sdboot Signed-off-by: Pragyan Poudyal --- Justfile | 1 - .../booted/readonly/030-test-composefs.nu | 24 ++++++++++++------- .../booted/test-install-outside-container.nu | 12 +++++++++- .../test-install-to-filesystem-var-mount.sh | 14 +++++++---- 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/Justfile b/Justfile index fa60222db..fae9f1048 100644 --- a/Justfile +++ b/Justfile @@ -114,7 +114,6 @@ test-composefs bootloader: just variant=composefs bootloader={{bootloader}} \ test-tmt --composefs-backend --bootloader {{bootloader}} \ readonly \ - bib-build \ download-only \ image-pushpull-upgrade \ image-upgrade-reboot \ diff --git a/tmt/tests/booted/readonly/030-test-composefs.nu b/tmt/tests/booted/readonly/030-test-composefs.nu index 81e2acc4b..8ac4a3574 100644 --- a/tmt/tests/booted/readonly/030-test-composefs.nu +++ b/tmt/tests/booted/readonly/030-test-composefs.nu @@ -13,15 +13,21 @@ let is_composefs = (tap is_composefs) let expecting_composefs = ($env.BOOTC_variant? | default "" | find "composefs") != null if $expecting_composefs { assert $is_composefs - # When using systemd-boot with DPS (Discoverable Partition Specification), - # /proc/cmdline should NOT contain a root= parameter because systemd-gpt-auto-generator - # discovers the root partition automatically - # Note that there is `bootctl --json=pretty` but it doesn't actually output JSON - let bootctl_output = (bootctl) - if ($bootctl_output | str contains 'Product: systemd-boot') { - let cmdline = parse_cmdline - let has_root_param = ($cmdline | any { |param| $param | str starts-with 'root=' }) - assert (not $has_root_param) "systemd-boot image should not have root= in kernel cmdline; systemd-gpt-auto-generator should discover the root partition via DPS" + + let bootloader = ($st.status.booted.composefs.bootloader | str downcase) + + if $bootloader == "systemd" { + # When using systemd-boot with DPS (Discoverable Partition Specification), + # /proc/cmdline should NOT contain a root= parameter because systemd-gpt-auto-generator + # discovers the root partition automatically + # Note that there is `bootctl --json=pretty` but it doesn't actually output JSON + let bootctl_output = (bootctl) + + if ($bootctl_output | str contains 'Product: systemd-boot') { + let cmdline = parse_cmdline + let has_root_param = ($cmdline | any { |param| $param | str starts-with 'root=' }) + assert (not $has_root_param) "systemd-boot image should not have root= in kernel cmdline; systemd-gpt-auto-generator should discover the root partition via DPS" + } } } diff --git a/tmt/tests/booted/test-install-outside-container.nu b/tmt/tests/booted/test-install-outside-container.nu index fb205f6af..a39429aea 100644 --- a/tmt/tests/booted/test-install-outside-container.nu +++ b/tmt/tests/booted/test-install-outside-container.nu @@ -28,6 +28,16 @@ umount /var/mnt # so we mask off /sysroot/ostree # And using systemd-run here breaks our install_t so we disable SELinux enforcement setenforce 0 + +let st = bootc status --json | from json +let bootloader = ($st.status.booted.composefs.bootloader | str downcase) + +let install_cmd = if (tap is_composefs) { + $"bootc install to-disk --disable-selinux --via-loopback --composefs-backend --bootloader=($bootloader) --filesystem ext4 --source-imgref ($target_image) ./disk.img" +} else { + $"bootc install to-disk --disable-selinux --via-loopback --filesystem xfs --source-imgref ($target_image) ./disk.img" +} + systemd-run -p MountFlags=slave -qdPG -- /bin/sh -c $" set -xeuo pipefail bootc usr-overlay @@ -36,7 +46,7 @@ if test -d /sysroot/ostree; then mount --bind /usr/share/empty /sysroot/ostree; rm -vrf /usr/lib/bootupd/updates # Another bootc install bug, we should not look at this in outside-of-container flows rm -vrf /usr/lib/bootc/bound-images.d -bootc install to-disk --disable-selinux --via-loopback --filesystem xfs --source-imgref ($target_image) ./disk.img +($install_cmd) " tap ok diff --git a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh index 88a0a5214..7c7069b0d 100644 --- a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh +++ b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh @@ -56,13 +56,13 @@ parted -s "$LOOP_DEV" mklabel gpt # BIOS boot partition (for GRUB on GPT) parted -s "$LOOP_DEV" mkpart primary 1MiB 2MiB parted -s "$LOOP_DEV" set 1 bios_grub on -# EFI partition (200 MiB) -parted -s "$LOOP_DEV" mkpart primary fat32 2MiB 202MiB +# EFI partition (1 GiB) +parted -s "$LOOP_DEV" mkpart primary fat32 2MiB 1026MiB parted -s "$LOOP_DEV" set 2 esp on # Boot partition (1 GiB) -parted -s "$LOOP_DEV" mkpart primary ext4 202MiB 1226MiB +parted -s "$LOOP_DEV" mkpart primary ext4 1026MiB 2052MiB # LVM partition (rest of disk) -parted -s "$LOOP_DEV" mkpart primary 1226MiB 100% +parted -s "$LOOP_DEV" mkpart primary 2052MiB 100% # Reload partition table partprobe "$LOOP_DEV" @@ -153,9 +153,13 @@ if [[ $is_composefs == "null" ]]; then test -d /var/mnt/target/boot/grub2 || test -d /var/mnt/target/boot/loader else test -d /var/mnt/target/composefs - ls -lahR /var/mnt/target/boot + # TODO(Johan-Liebert1): This is getting bootloader from the VM, which is not quite correct + # It works for now as the CI runs separately for each bootloader, but we need to get the + # bootloader from the installed systemd if we wish to run the tests locally without rebuilding the images + # This probably also happens in other tests, one instance is install-outside-container bootloader=$(bootc status --json | jq '.status.booted.composefs.bootloader' | tr '[:upper:]' '[:lower:]') + bootloader=${bootloader//\"/} if [[ $bootloader == "grub" ]]; then test -d /var/mnt/target/boot/grub2 || test -d /var/mnt/target/boot/loader