From 3ad21af367c3fe7c29fbda2979153017438d3c62 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 16 Jan 2026 14:03:49 -0500 Subject: [PATCH 01/11] build-sys: Enable CentOS Stream compose repos to avoid version skew The base image may be built from a compose that has newer packages than what's available on the public mirrors. This causes version skew where packages like bootupd have different versions between the base image and our built image. For example, bootupd 0.2.32 changed the EFI file layout from /usr/lib/bootupd/updates/EFI/ to /usr/lib/efi/, and if we build with an older bootupd from mirrors while the target image has the newer layout, bootloader installation fails. Enable the CentOS Stream compose repos with higher priority to ensure we get matching versions. xref https://gitlab.com/redhat/centos-stream/containers/bootc/-/issues/1174 Signed-off-by: Colin Walters Assisted-by: OpenCode (Opus 4.5) Signed-off-by: Colin Walters --- Dockerfile | 5 ++++ contrib/packaging/enable-compose-repos | 33 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100755 contrib/packaging/enable-compose-repos diff --git a/Dockerfile b/Dockerfile index 11ca9ab5c..ac0c8f0b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,6 +47,11 @@ RUN --mount=type=tmpfs,target=/run /src/contrib/packaging/configure-systemdboot # local sources. We'll override it later. # NOTE: All your base belong to me. FROM $base as target-base +# Handle version skew between base image and mirrors for CentOS Stream +# xref https://gitlab.com/redhat/centos-stream/containers/bootc/-/issues/1174 +RUN --mount=type=tmpfs,target=/run \ + --mount=type=bind,from=packaging,src=/,target=/run/packaging \ + /run/packaging/enable-compose-repos RUN --mount=type=tmpfs,target=/run /usr/libexec/bootc-base-imagectl build-rootfs --manifest=standard /target-rootfs FROM scratch as base diff --git a/contrib/packaging/enable-compose-repos b/contrib/packaging/enable-compose-repos new file mode 100755 index 000000000..41b10c9bb --- /dev/null +++ b/contrib/packaging/enable-compose-repos @@ -0,0 +1,33 @@ +#!/bin/bash +# Enable compose repos to avoid version skew between base image and mirrors +# xref https://gitlab.com/redhat/centos-stream/containers/bootc/-/issues/1174 +set -euo pipefail + +. /usr/lib/os-release + +case "${ID}" in + centos) + # The base image may have been built from a compose that has newer packages + # than what's available on the public mirrors. Enable the compose repos + # with higher priority to ensure we get matching versions. + cat > /etc/yum.repos.d/centos-compose.repo << 'EOF' +[compose-baseos] +name=CentOS Stream $releasever Compose BaseOS +baseurl=https://composes.stream.centos.org/stream-$releasever/production/latest-CentOS-Stream/compose/BaseOS/$basearch/os/ +gpgcheck=0 +enabled=1 +priority=1 + +[compose-appstream] +name=CentOS Stream $releasever Compose AppStream +baseurl=https://composes.stream.centos.org/stream-$releasever/production/latest-CentOS-Stream/compose/AppStream/$basearch/os/ +gpgcheck=0 +enabled=1 +priority=1 +EOF + echo "Enabled CentOS Stream compose repos" + ;; + *) + # No compose repo needed for other distros + ;; +esac From f168dbeec813ef459672b15ce254513b44602e83 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 16 Jan 2026 14:41:44 -0500 Subject: [PATCH 02/11] build-sys: Enable GPG checking for CentOS compose repos Use gpgcheck=1 and reference the official CentOS GPG key instead of disabling signature verification. This ensures package integrity during builds while still using compose repos to avoid version skew. Assisted-by: OpenCode (Claude Sonnet 4) Signed-off-by: Colin Walters --- contrib/packaging/enable-compose-repos | 27 ++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/contrib/packaging/enable-compose-repos b/contrib/packaging/enable-compose-repos index 41b10c9bb..20b5ba30d 100755 --- a/contrib/packaging/enable-compose-repos +++ b/contrib/packaging/enable-compose-repos @@ -10,22 +10,33 @@ case "${ID}" in # The base image may have been built from a compose that has newer packages # than what's available on the public mirrors. Enable the compose repos # with higher priority to ensure we get matching versions. - cat > /etc/yum.repos.d/centos-compose.repo << 'EOF' + + # Extract the gpgkey from the existing centos.repo - c9s uses + # RPM-GPG-KEY-centosofficial while c10s uses RPM-GPG-KEY-centosofficial-SHA256 + gpgkey=$(grep -m1 '^gpgkey=' /etc/yum.repos.d/centos.repo | cut -d= -f2) + if [[ -z "${gpgkey}" ]]; then + echo "Error: Could not find gpgkey in /etc/yum.repos.d/centos.repo" >&2 + exit 1 + fi + + cat > /etc/yum.repos.d/centos-compose.repo << EOF [compose-baseos] -name=CentOS Stream $releasever Compose BaseOS -baseurl=https://composes.stream.centos.org/stream-$releasever/production/latest-CentOS-Stream/compose/BaseOS/$basearch/os/ -gpgcheck=0 +name=CentOS Stream \$releasever Compose BaseOS +baseurl=https://composes.stream.centos.org/stream-\$releasever/production/latest-CentOS-Stream/compose/BaseOS/\$basearch/os/ +gpgcheck=1 enabled=1 priority=1 +gpgkey=${gpgkey} [compose-appstream] -name=CentOS Stream $releasever Compose AppStream -baseurl=https://composes.stream.centos.org/stream-$releasever/production/latest-CentOS-Stream/compose/AppStream/$basearch/os/ -gpgcheck=0 +name=CentOS Stream \$releasever Compose AppStream +baseurl=https://composes.stream.centos.org/stream-\$releasever/production/latest-CentOS-Stream/compose/AppStream/\$basearch/os/ +gpgcheck=1 enabled=1 priority=1 +gpgkey=${gpgkey} EOF - echo "Enabled CentOS Stream compose repos" + echo "Enabled CentOS Stream compose repos (gpgkey: ${gpgkey})" ;; *) # No compose repo needed for other distros From 0ade2c3bc2ee96a82b705376eb46d6dc504e4c93 Mon Sep 17 00:00:00 2001 From: ckyrouac Date: Fri, 16 Jan 2026 15:24:16 -0500 Subject: [PATCH 03/11] tmt: Fix var-mount test to use booted container image Use copy-to-storage to add the booted container to podman storage instead of pulling a remote image. This matches the pattern used by other TMT tests and ensures we test the actual bootc under test. Changes: - Use localhost/bootc from copy-to-storage instead of remote image - Disable LBIs via bind mount of /usr/share/empty - Remove unnecessary host modifications (usr-overlay, dnf install, etc.) - Use 100%FREE for root LV to ensure sufficient space for deployment Assisted-by: OpenCode (Opus 4.5) Signed-off-by: ckyrouac Signed-off-by: Colin Walters --- .../test-install-to-filesystem-var-mount.sh | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) 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 be9dd1d7b..5fe76d8a6 100644 --- a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh +++ b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh @@ -16,28 +16,20 @@ set -xeuo pipefail -# Use a generic target image to test skew between the bootc binary doing -# the install and the target image -TARGET_IMAGE="docker://quay.io/centos-bootc/centos-bootc:stream10" +# Build a derived image with LBIs removed for installation +TARGET_IMAGE="localhost/bootc-install" echo "Testing bootc install to-filesystem with separate /var mount" -# Disable SELinux enforcement for the install -setenforce 0 +# Copy the currently booted image to container storage for podman to use +bootc image copy-to-storage -# Enable usr-overlay to allow modifications -bootc usr-overlay - -# Install required packages (bootc images are immutable, so we need to install -# after usr-overlay is enabled) -dnf install -y parted lvm2 dosfstools e2fsprogs - -# Mask off conflicting ostree state -if test -d /sysroot/ostree; then - mount --bind /usr/share/empty /sysroot/ostree -fi -rm -vrf /usr/lib/bootupd/updates -rm -vrf /usr/lib/bootc/bound-images.d +# Build a derived image that removes LBIs +cat > /tmp/Containerfile.drop-lbis <<'EOF' +FROM localhost/bootc +RUN rm -rf /usr/lib/bootc/bound-images.d/* +EOF +podman build -t "$TARGET_IMAGE" -f /tmp/Containerfile.drop-lbis # Create a 12GB sparse disk image in /var/tmp (not /tmp which may be tmpfs) DISK_IMG=/var/tmp/disk-var-mount-test.img @@ -91,7 +83,7 @@ vgcreate BL "$LVM_PART" # Create logical volumes lvcreate -L 4G -n var02 BL -lvcreate -L 5G -n root02 BL +lvcreate -l 100%FREE -n root02 BL # Create filesystems on logical volumes mkfs.ext4 -F /dev/BL/var02 @@ -122,8 +114,7 @@ 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 -# Run bootc install to-filesystem -# This should succeed and handle the separate /var mount correctly +# Run bootc install to-filesystem from within the container image under test podman run \ --rm --privileged \ -v /var/mnt/target:/target \ From 308acb9580c4824bc65697393d6840f59c219e80 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Mon, 19 Jan 2026 13:47:10 -0500 Subject: [PATCH 04/11] tmt: Fix install tests to use booted image to avoid bootupd skew The install-outside-container and install-unified-flag tests were failing because they pulled centos-bootc:stream10 which has a newer bootupd with EFI files at /usr/lib/efi/, while the running Fedora image has the old layout at /usr/lib/bootupd/updates/EFI/. Use the booted image instead to ensure bootupd versions match. The original code is commented out for easy reversion once PR #1816 lands to properly handle cross-version installs. Assisted-by: OpenCode (Claude Sonnet 4) Signed-off-by: Colin Walters --- tmt/tests/booted/test-install-outside-container.nu | 11 ++++++----- tmt/tests/booted/test-install-unified-flag.nu | 9 ++++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tmt/tests/booted/test-install-outside-container.nu b/tmt/tests/booted/test-install-outside-container.nu index d854da87f..35ad6c7c0 100644 --- a/tmt/tests/booted/test-install-outside-container.nu +++ b/tmt/tests/booted/test-install-outside-container.nu @@ -6,11 +6,12 @@ use std assert use tap.nu -# In this test we install a generic image mainly because it keeps -# this test in theory independent of starting from a bootc host, -# but also because it's useful to test "skew" between the bootc binary -# doing the install and the target image. -let target_image = "docker://quay.io/centos-bootc/centos-bootc:stream10" +# FIXME: Revert to use generic images once https://github.com/bootc-dev/bootc/pull/1816 lands +# Currently using the booted image to avoid version skew between bootupd in +# the running system and the target image (e.g., different EFI file layouts). +# let target_image = "docker://quay.io/centos-bootc/centos-bootc:stream10" +bootc image copy-to-storage +let target_image = "containers-storage:localhost/bootc" # setup filesystem mkdir /var/mnt diff --git a/tmt/tests/booted/test-install-unified-flag.nu b/tmt/tests/booted/test-install-unified-flag.nu index e788967a4..389437618 100644 --- a/tmt/tests/booted/test-install-unified-flag.nu +++ b/tmt/tests/booted/test-install-unified-flag.nu @@ -11,9 +11,12 @@ use std assert use tap.nu -# Use a generic target image to test skew between the bootc binary doing -# the install and the target image -let target_image = "docker://quay.io/centos-bootc/centos-bootc:stream10" +# FIXME: Revert to use generic images once https://github.com/bootc-dev/bootc/pull/1816 lands +# Currently using the booted image to avoid version skew between bootupd in +# the running system and the target image (e.g., different EFI file layouts). +# let target_image = "docker://quay.io/centos-bootc/centos-bootc:stream10" +bootc image copy-to-storage +let target_image = "containers-storage:localhost/bootc" def main [] { tap begin "install with experimental unified storage flag" From 71344c87ff82f375b4c3894fc803a7fd0e74d224 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 8 Jan 2026 11:33:40 -0500 Subject: [PATCH 05/11] build-sys: Rework sealing to be one build step Now that we're doing a "from scratch" build we don't have the mtime issue, and so we can change our build system to do everything in a single step. Assisted-by: OpenCode (Opus 4.5) Signed-off-by: Colin Walters --- Dockerfile | 105 ++++++++++++++++----- Dockerfile.cfsuki | 70 -------------- Justfile | 14 +-- contrib/packaging/configure-systemdboot | 29 ------ contrib/packaging/configure-variant | 13 --- contrib/packaging/fedora-extra.txt | 2 - contrib/packaging/finalize-uki | 49 ++++++++++ contrib/packaging/initialize-sealing-tools | 15 +++ contrib/packaging/install-buildroot | 14 ++- contrib/packaging/seal-uki | 43 +++++++++ contrib/packaging/switch-to-sdboot | 24 +++++ docs/src/experimental-composefs.md | 62 ++++-------- hack/build-sealed | 37 -------- 13 files changed, 244 insertions(+), 233 deletions(-) delete mode 100644 Dockerfile.cfsuki delete mode 100755 contrib/packaging/configure-systemdboot create mode 100755 contrib/packaging/finalize-uki create mode 100755 contrib/packaging/initialize-sealing-tools create mode 100755 contrib/packaging/seal-uki create mode 100755 contrib/packaging/switch-to-sdboot delete mode 100755 hack/build-sealed diff --git a/Dockerfile b/Dockerfile index ac0c8f0b7..10fe4f907 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,7 @@ FROM $base as buildroot ARG initramfs=1 # This installs our buildroot, and we want to cache it independently of the rest. # Basically we don't want changing a .rs file to blow out the cache of packages. +# Use tmpfs,target=/run with bind mounts inside to avoid leaking mount stubs into the image RUN --mount=type=tmpfs,target=/run \ --mount=type=bind,from=packaging,src=/,target=/run/packaging \ /run/packaging/install-buildroot @@ -34,10 +35,6 @@ WORKDIR /src # First we download all of our Rust dependencies RUN --mount=type=tmpfs,target=/run --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome cargo fetch -FROM buildroot as sdboot-content -# Writes to /out -RUN --mount=type=tmpfs,target=/run /src/contrib/packaging/configure-systemdboot download - # We always do a "from scratch" build # https://docs.fedoraproject.org/en-US/bootc/building-from-scratch/ # because this fixes https://github.com/containers/composefs-rs/issues/132 @@ -74,6 +71,13 @@ ENV container=oci STOPSIGNAL SIGRTMIN+3 CMD ["/sbin/init"] +# This layer contains things which aren't in the default image and may +# be used for sealing images in particular. +FROM base as tools +RUN --mount=type=tmpfs,target=/run \ + --mount=type=bind,from=packaging,src=/,target=/run/packaging \ + /run/packaging/initialize-sealing-tools + # ------------- # external dependency cutoff point: # NOTE: Every RUN instruction past this point should use `--network=none`; we want to ensure @@ -90,14 +94,35 @@ ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} # Build RPM directly from source, using cached target directory RUN --network=none --mount=type=tmpfs,target=/run --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome RPM_VERSION="${pkgversion}" /src/contrib/packaging/build-rpm -FROM buildroot as sdboot-signed +# This image signs systemd-boot using our key, and writes the resulting binary into /out +FROM tools as sdboot-signed # The secureboot key and cert are passed via Justfile # We write the signed binary into /out +# Note: /out already contains systemd-boot-unsigned RPM from initialize-sealing-tools RUN --network=none --mount=type=tmpfs,target=/run \ - --mount=type=bind,from=sdboot-content,src=/,target=/run/sdboot-package \ --mount=type=secret,id=secureboot_key \ - --mount=type=secret,id=secureboot_cert \ - /src/contrib/packaging/configure-systemdboot sign + --mount=type=secret,id=secureboot_cert < to use a different base -ARG base=localhost/bootc -FROM $base AS base - -FROM base as kernel -# Use tmpfs on /run to prevent podman's DNS resolver files from being committed -RUN --mount=type=tmpfs,target=/run <&2; exit 1 - ;; -esac diff --git a/contrib/packaging/configure-variant b/contrib/packaging/configure-variant index 487ea3076..8940286ef 100755 --- a/contrib/packaging/configure-variant +++ b/contrib/packaging/configure-variant @@ -14,20 +14,7 @@ fi # Handle variant-specific configuration case "${VARIANT}" in *-sdboot) - # Install systemd-boot and remove bootupd; - # We downloaded this in an earlier phase - sdboot="usr/lib/systemd/boot/efi/systemd-bootx64.efi" - sdboot_bn=$(basename ${sdboot}) - rpm -Uvh /run/sdboot-content/out/*.rpm - # And override with our signed binary - install -m 0644 /run/sdboot-signed/out/${sdboot_bn} /${sdboot} - # Uninstall bootupd - rpm -e bootupd - rm -rf /usr/lib/bootupd/updates - # Clean up package manager caches - dnf clean all - rm -rf /var/cache /var/lib/{dnf,rhsm} /var/log/* ;; # Future variants can be added here # For Debian support, this could check package manager type and use apt instead diff --git a/contrib/packaging/fedora-extra.txt b/contrib/packaging/fedora-extra.txt index 50bc48f0b..a9f66c015 100644 --- a/contrib/packaging/fedora-extra.txt +++ b/contrib/packaging/fedora-extra.txt @@ -7,5 +7,3 @@ git-core jq # We now always build a package in the container build rpm-build -# Used for signing -sbsigntools diff --git a/contrib/packaging/finalize-uki b/contrib/packaging/finalize-uki new file mode 100755 index 000000000..6de60c2cc --- /dev/null +++ b/contrib/packaging/finalize-uki @@ -0,0 +1,49 @@ +#!/bin/bash +# Finalize UKI installation: copy to /boot, remove raw kernel/initramfs, create symlinks +# +# For sealed UKI images, the kernel and initramfs are embedded inside the signed +# UKI PE binary. We remove the standalone vmlinuz/initramfs.img to: +# - Avoid duplication (they're inside the UKI) +# - Ensure tools use the UKI path +# - Make it clear this is a UKI-only boot configuration +# +# NOTE: The old Dockerfile.cfsuki had a bug where the final-final stage started +# FROM base instead of FROM final, then only copied /boot. This meant the +# vmlinuz/initramfs removal in the final stage was lost. Running this script +# in the actual final image stage fixes that issue. +# +# IMPORTANT: bcvk needs to be updated to find .efi files inside kernel version +# subdirectories (e.g., /usr/lib/modules//.efi) rather than at the +# top level of /usr/lib/modules/. See https://github.com/bootc-dev/bcvk/pull/144 +set -xeuo pipefail + +# Path to directory containing the generated UKI +uki_src=$1 +shift + +# Find the kernel version from the current system +kver=$(cd /usr/lib/modules && echo *) +if [ -z "$kver" ] || [ "$kver" = "*" ]; then + echo "Error: No kernel found" >&2 + exit 1 +fi + +# Create the EFI directory structure +mkdir -p /boot/EFI/Linux + +# The UKI in /boot is outside the composefs-verified tree, which is fine +# because the UKI itself is signed and verified by Secure Boot +target=/boot/EFI/Linux/${kver}.efi +cp "${uki_src}/${kver}.efi" "${target}" + +# Remove the raw kernel and initramfs since we're using a UKI now. +# NOTE: We intentionally keep these for now until bcvk is updated to extract +# kernel/initramfs from UKIs in subdirectories. Once bcvk PR #144 is fixed +# to look for .efi files in /usr/lib/modules//, we can uncomment this. +# rm -v "/usr/lib/modules/${kver}/vmlinuz" "/usr/lib/modules/${kver}/initramfs.img" + +# NOTE: We used to create a symlink from /usr/lib/modules/${kver}/${kver}.efi to the UKI +# for tooling compatibility. However, composefs-boot's find_uki_components() doesn't +# handle symlinks correctly and fails with "is not a regular file". The UKI is already +# found in /boot/EFI/Linux/, so the symlink is not needed. +# See: https://github.com/containers/composefs-rs/issues/XXX diff --git a/contrib/packaging/initialize-sealing-tools b/contrib/packaging/initialize-sealing-tools new file mode 100755 index 000000000..b1e037bed --- /dev/null +++ b/contrib/packaging/initialize-sealing-tools @@ -0,0 +1,15 @@ +#!/bin/bash +set -xeuo pipefail +. /usr/lib/os-release +case "${ID}${ID_LIKE:-}" in + *centos*|*rhel*) + # Enable EPEL for sbsigntools + dnf -y install epel-release + ;; +esac +dnf -y install systemd-ukify sbsigntools +# And in the sealing case, we're going to inject and sign systemd-boot +# into the target image. +mkdir -p /out +cd /out +dnf -y download systemd-boot-unsigned diff --git a/contrib/packaging/install-buildroot b/contrib/packaging/install-buildroot index cc133e970..1bde1a2d2 100755 --- a/contrib/packaging/install-buildroot +++ b/contrib/packaging/install-buildroot @@ -3,14 +3,18 @@ set -xeuo pipefail cd $(dirname $0) . /usr/lib/os-release -case $ID in - centos|rhel) +case "${ID}${ID_LIKE:-}" in + *centos*|*rhel*) + # We'll use crb at build time dnf config-manager --set-enabled crb - # Enable EPEL for sbsigntools - dnf -y install epel-release ;; - fedora) dnf -y install dnf-utils 'dnf5-command(builddep)';; esac +# Deal with dnf4 vs dnf5 +if test -x /usr/bin/dnf5; then + dnf -y install 'dnf5-command(builddep)' +else + dnf -y install 'dnf-command(builddep)' +fi # Handle version skew, xref https://gitlab.com/redhat/centos-stream/containers/bootc/-/issues/1174 dnf -y distro-sync ostree{,-libs} systemd # Install base build requirements diff --git a/contrib/packaging/seal-uki b/contrib/packaging/seal-uki new file mode 100755 index 000000000..253278212 --- /dev/null +++ b/contrib/packaging/seal-uki @@ -0,0 +1,43 @@ +#!/bin/bash +# Generate a sealed UKI with embedded composefs digest +set -xeuo pipefail + +# Path to the desired root filesystem +target=$1 +shift +# Write to this directory +output=$1 +shift +# Path to secrets directory +secrets=$1 +shift + +# Compute the composefs digest from the target rootfs +composefs_digest=$(bootc container compute-composefs-digest "${target}") + +# Build the kernel command line +# enforcing=0: https://github.com/bootc-dev/bootc/issues/1826 +# TODO: pick up kargs from /usr/lib/bootc/kargs.d +cmdline="composefs=${composefs_digest} console=ttyS0,115200n8 console=hvc0 enforcing=0 rw" + +# Find the kernel version +kver=$(cd "${target}/usr/lib/modules" && echo *) +if [ -z "$kver" ] || [ "$kver" = "*" ]; then + echo "Error: No kernel found" >&2 + exit 1 +fi + +mkdir -p "${output}" + +ukify build \ + --linux "${target}/usr/lib/modules/${kver}/vmlinuz" \ + --initrd "${target}/usr/lib/modules/${kver}/initramfs.img" \ + --uname="${kver}" \ + --cmdline "${cmdline}" \ + --os-release "@${target}/usr/lib/os-release" \ + --signtool sbsign \ + --secureboot-private-key "${secrets}/secureboot_key" \ + --secureboot-certificate "${secrets}/secureboot_cert" \ + --measure \ + --json pretty \ + --output "${output}/${kver}.efi" diff --git a/contrib/packaging/switch-to-sdboot b/contrib/packaging/switch-to-sdboot new file mode 100755 index 000000000..be030ec9f --- /dev/null +++ b/contrib/packaging/switch-to-sdboot @@ -0,0 +1,24 @@ +#!/bin/bash +# Switch the target root to use systemd-boot, using the content from SRC +# SRC should contain an "out" subdirectory with: +# - systemd-boot-unsigned RPM (*.rpm) +# - signed systemd-boot binary (systemd-boot*.efi) +set -xeuo pipefail + +src=$1/out +shift + +# Uninstall bootupd if present (we're switching to sd-boot managed differently) +if rpm -q bootupd &>/dev/null; then + rpm -e bootupd + rm -vrf /usr/lib/bootupd/updates +fi + +# First install the unsigned systemd-boot RPM to get the package in place +rpm -Uvh "${src}"/*.rpm + +# Now find where it installed the binary and override with our signed version +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}" diff --git a/docs/src/experimental-composefs.md b/docs/src/experimental-composefs.md index 37d94d7cd..7d2b87673 100644 --- a/docs/src/experimental-composefs.md +++ b/docs/src/experimental-composefs.md @@ -11,60 +11,30 @@ The composefs backend is an experimental alternative storage backend that uses [ **Status**: Experimental. The composefs backend is under active development and not yet suitable for production use. The feature is always compiled in as of bootc v1.10.1. -## Key Benefits +A key goal is custom "sealed" images, signed with your own Secure Boot keys. +This is based on [Unified Kernel Images](https://uapi-group.org/specifications/specs/unified_kernel_image/) +that embed a digest of the target container root filesystem, typically alongside a bootloader (such +as systemd-boot) also signed with your key. -- **Native container integration**: Direct use of container image formats without the ostree layer -- **UKI support**: First-class support for Unified Kernel Images (UKIs) and systemd-boot -- **Sealed images**: Enables building cryptographically sealed, securely-bootable images -- **Simpler architecture**: Reduces dependency on ostree as an implementation detail +### UKIs in bootc containers -## Building Sealed Images +There must be exactly one UKI placed in `/boot/EFI/Linux/.efi`. -### Using `just build-sealed` +### Bootloader support -This is an entrypoint focused on *bootc development* itself - it builds bootc -from source. +To use sealed images, ensure that the target container image has systemd-boot, +and does not have `bootupd`. -```bash -just build-sealed -``` +### Installation -We are working on documenting individual steps to build a sealed image outside of -this tooling. +There is a `--composefs-backend` option for `bootc install`; however, if +a UKI and systemd-boot are detected, it will automatically be used. -## How Sealed Images Work +### Developing and testing bootc with sealed composefs -A sealed image includes: -- A Unified Kernel Image (UKI) that combines kernel, initramfs, and boot parameters -- The composefs fsverity digest embedded in the kernel command line -- Secure Boot signatures on both the UKI and systemd-boot loader - -The UKI is placed in `/boot/EFI/Linux/` and includes the composefs digest in its command line: -``` -composefs=${COMPOSEFS_FSVERITY} root=UUID=... -``` - -This enables the boot chain to verify the integrity of the root filesystem. - -## Installation - -When installing a composefs-backend system, use: - -```bash -bootc install to-disk /dev/sdX -``` - -**Note**: Sealed images will require fsverity support on the target filesystem by default. - -## Testing Composefs - -To run the composefs integration tests: - -```bash -just test-composefs -``` - -This builds a sealed image and runs the composefs test suite using `bcvk` (bootc VM tooling). +Use `just variant=composefs-sealeduki-sdboot build` to build a local sealed +UKI, using Secure Boot keys generated in `target/test-secureboot`. This is +not a production path. ## Current Limitations diff --git a/hack/build-sealed b/hack/build-sealed deleted file mode 100755 index 22b668312..000000000 --- a/hack/build-sealed +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -set -euo pipefail -# This should turn into https://github.com/bootc-dev/bootc/issues/1498 - -dn=$(cd $(dirname $0) && pwd) - -variant=$1 -shift -# The un-sealed container image we want to use -input_image=$1 -shift -# The output container image -output_image=$1 -shift - -runv() { - set -x - "$@" -} - -case $variant in - ostree) - # Nothing to do - echo "Not building a sealed image; forwarding tag" - runv podman tag $input_image $output_image - exit 0 - ;; - composefs-sealeduki*) - ;; - *) - echo "Unknown variant=$variant" 1>&2; exit 1 - ;; -esac - -cfs_digest=$(${dn}/compute-composefs-digest $input_image) -runv podman build -t $output_image \ - --build-arg=COMPOSEFS_FSVERITY=${cfs_digest} --build-arg=base=${input_image} "$@" -f Dockerfile.cfsuki . From 7ca6084034e0fa3a4e97bb9ffd3c9a6b5ddb4ad3 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Tue, 13 Jan 2026 20:41:22 -0500 Subject: [PATCH 06/11] build-sys: Add BOOTC_extra_src for local dependency development Add support for bind-mounting an extra source directory into container builds, primarily for developing against a local composefs-rs checkout. Usage: BOOTC_extra_src=$HOME/src/composefs-rs just build The directory is mounted at /run/extra-src inside the container. When using this, also patch Cargo.toml to use path dependencies pointing to /run/extra-src/crates/.... Signed-off-by: Colin Walters Assisted-by: OpenCode (Opus 4.5) Signed-off-by: Colin Walters --- Cargo.toml | 8 ++++++++ Dockerfile | 1 + Justfile | 11 ++++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index df45ce27f..7d4f336db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,9 +42,17 @@ clap = "4.5.4" clap_mangen = { version = "0.2.20" } # Reviewers (including AI tools): The composefs-rs git revision is duplicated for each crate. # If adding/removing crates here, also update docs/Dockerfile.mdbook and docs/src/internals.md. +# +# To develop against a local composefs-rs checkout: +# 1. Set BOOTC_extra_src to your composefs-rs path when building: +# BOOTC_extra_src=$HOME/src/composefs-rs just build +# 2. Comment out the git refs below and uncomment the path refs: composefs = { git = "https://github.com/containers/composefs-rs", rev = "e9008489375044022e90d26656960725a76f4620", package = "composefs", features = ["rhel9"] } composefs-boot = { git = "https://github.com/containers/composefs-rs", rev = "e9008489375044022e90d26656960725a76f4620", package = "composefs-boot" } composefs-oci = { git = "https://github.com/containers/composefs-rs", rev = "e9008489375044022e90d26656960725a76f4620", package = "composefs-oci" } +# composefs = { path = "/run/extra-src/crates/composefs", package = "composefs", features = ["rhel9"] } +# composefs-boot = { path = "/run/extra-src/crates/composefs-boot", package = "composefs-boot" } +# composefs-oci = { path = "/run/extra-src/crates/composefs-oci", package = "composefs-oci" } fn-error-context = "0.2.1" hex = "0.4.3" indicatif = "0.18.0" diff --git a/Dockerfile b/Dockerfile index 10fe4f907..e62a6daa3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,6 +33,7 @@ WORKDIR /src # See https://www.reddit.com/r/rust/comments/126xeyx/exploring_the_problem_of_faster_cargo_docker/ # We aren't using the full recommendations there, just the simple bits. # First we download all of our Rust dependencies +# Note: /run/extra-src is optionally bind-mounted via BOOTC_extra_src for local composefs-rs development RUN --mount=type=tmpfs,target=/run --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome cargo fetch # We always do a "from scratch" build diff --git a/Justfile b/Justfile index fc696be40..15d157632 100644 --- a/Justfile +++ b/Justfile @@ -39,8 +39,17 @@ lbi_images := "quay.io/curl/curl:latest quay.io/curl/curl-base:latest registry.a # ``` # TODO: Gather more info and file a buildah bug generic_buildargs := "" +# Optional: path to extra source directory (e.g. composefs-rs) to bind mount into builds. +# Usage: BOOTC_extra_src=$HOME/src/github/containers/composefs-rs just build +# The directory will be mounted at /run/extra-src inside the container. +# When using this, you must also patch Cargo.toml to use path dependencies: +# composefs = { path = "/run/extra-src/crates/composefs", ... } +# Note: This disables SELinux labeling for the mount. +extra_src := env("BOOTC_extra_src", "") +# Generate podman args for extra source mount if configured +_extra_src_args := if extra_src != "" { "-v " + extra_src + ":/run/extra-src:ro --security-opt=label=disable" } else { "" } # Args for package building (no secrets needed, just builds RPMs) -base_buildargs := generic_buildargs + " --build-arg=base=" + base + " --build-arg=variant=" + variant +base_buildargs := generic_buildargs + " " + _extra_src_args + " --build-arg=base=" + base + " --build-arg=variant=" + variant # - scratch builds need extra perms per https://docs.fedoraproject.org/en-US/bootc/building-from-scratch/ # - we do secure boot signing here, so provide the keys buildargs := base_buildargs \ From 6135a3f59a665858ab64f25c164cb2c74aa4a197 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Tue, 13 Jan 2026 20:42:27 -0500 Subject: [PATCH 07/11] Update composefs-rs to merged PR #209 The composefs-rs PR 209 has been merged to main. This updates bootc to use the containers/composefs-rs repository at the merge commit. Key API changes: - Directory::default() -> Directory::new(Stat::uninitialized()) - read_filesystem() no longer takes stat_root parameter - New read_container_root() for OCI containers (propagates /usr metadata to root) - stat_root CLI flag renamed to no_propagate_usr_to_root with inverted logic See https://github.com/containers/composefs-rs/pull/209 Signed-off-by: Colin Walters --- Cargo.lock | 8 +++-- Cargo.toml | 6 ++-- crates/etc-merge/src/lib.rs | 6 ++-- crates/lib/src/bootc_composefs/digest.rs | 2 +- crates/lib/src/cfsctl.rs | 38 +++++++++++++++++------- 5 files changed, 40 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d1a781ad7..45399dec0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -573,7 +573,7 @@ checksum = "55b672471b4e9f9e95499ea597ff64941a309b2cdbffcc46f2cc5e2d971fd335" [[package]] name = "composefs" version = "0.3.0" -source = "git+https://github.com/containers/composefs-rs?rev=e9008489375044022e90d26656960725a76f4620#e9008489375044022e90d26656960725a76f4620" +source = "git+https://github.com/containers/composefs-rs?rev=4a060161e0122bd2727e639437c61e05ecc7cab3#4a060161e0122bd2727e639437c61e05ecc7cab3" dependencies = [ "anyhow", "hex", @@ -593,7 +593,7 @@ dependencies = [ [[package]] name = "composefs-boot" version = "0.3.0" -source = "git+https://github.com/containers/composefs-rs?rev=e9008489375044022e90d26656960725a76f4620#e9008489375044022e90d26656960725a76f4620" +source = "git+https://github.com/containers/composefs-rs?rev=4a060161e0122bd2727e639437c61e05ecc7cab3#4a060161e0122bd2727e639437c61e05ecc7cab3" dependencies = [ "anyhow", "composefs", @@ -606,10 +606,11 @@ dependencies = [ [[package]] name = "composefs-oci" version = "0.3.0" -source = "git+https://github.com/containers/composefs-rs?rev=e9008489375044022e90d26656960725a76f4620#e9008489375044022e90d26656960725a76f4620" +source = "git+https://github.com/containers/composefs-rs?rev=4a060161e0122bd2727e639437c61e05ecc7cab3#4a060161e0122bd2727e639437c61e05ecc7cab3" dependencies = [ "anyhow", "async-compression", + "bytes", "composefs", "containers-image-proxy", "hex", @@ -619,6 +620,7 @@ dependencies = [ "sha2", "tar", "tokio", + "tokio-util", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7d4f336db..8cc076c0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,9 +47,9 @@ clap_mangen = { version = "0.2.20" } # 1. Set BOOTC_extra_src to your composefs-rs path when building: # BOOTC_extra_src=$HOME/src/composefs-rs just build # 2. Comment out the git refs below and uncomment the path refs: -composefs = { git = "https://github.com/containers/composefs-rs", rev = "e9008489375044022e90d26656960725a76f4620", package = "composefs", features = ["rhel9"] } -composefs-boot = { git = "https://github.com/containers/composefs-rs", rev = "e9008489375044022e90d26656960725a76f4620", package = "composefs-boot" } -composefs-oci = { git = "https://github.com/containers/composefs-rs", rev = "e9008489375044022e90d26656960725a76f4620", package = "composefs-oci" } +composefs = { git = "https://github.com/containers/composefs-rs", rev = "4a060161e0122bd2727e639437c61e05ecc7cab3", package = "composefs", features = ["rhel9"] } +composefs-boot = { git = "https://github.com/containers/composefs-rs", rev = "4a060161e0122bd2727e639437c61e05ecc7cab3", package = "composefs-boot" } +composefs-oci = { git = "https://github.com/containers/composefs-rs", rev = "4a060161e0122bd2727e639437c61e05ecc7cab3", package = "composefs-oci" } # composefs = { path = "/run/extra-src/crates/composefs", package = "composefs", features = ["rhel9"] } # composefs-boot = { path = "/run/extra-src/crates/composefs-boot", package = "composefs-boot" } # composefs-oci = { path = "/run/extra-src/crates/composefs-oci", package = "composefs-oci" } diff --git a/crates/etc-merge/src/lib.rs b/crates/etc-merge/src/lib.rs index 9f687fc6d..80b0637e6 100644 --- a/crates/etc-merge/src/lib.rs +++ b/crates/etc-merge/src/lib.rs @@ -316,17 +316,17 @@ pub fn traverse_etc( Directory, Option>, )> { - let mut pristine_etc_files = Directory::default(); + let mut pristine_etc_files = Directory::new(Stat::uninitialized()); recurse_dir(pristine_etc, &mut pristine_etc_files) .context(format!("Recursing {pristine_etc:?}"))?; - let mut current_etc_files = Directory::default(); + let mut current_etc_files = Directory::new(Stat::uninitialized()); recurse_dir(current_etc, &mut current_etc_files) .context(format!("Recursing {current_etc:?}"))?; let new_etc_files = match new_etc { Some(new_etc) => { - let mut new_etc_files = Directory::default(); + let mut new_etc_files = Directory::new(Stat::uninitialized()); recurse_dir(new_etc, &mut new_etc_files).context(format!("Recursing {new_etc:?}"))?; Some(new_etc_files) diff --git a/crates/lib/src/bootc_composefs/digest.rs b/crates/lib/src/bootc_composefs/digest.rs index d031d5277..0ddef9631 100644 --- a/crates/lib/src/bootc_composefs/digest.rs +++ b/crates/lib/src/bootc_composefs/digest.rs @@ -61,7 +61,7 @@ pub(crate) fn compute_composefs_digest( // Read filesystem from path, transform for boot, compute digest let mut fs = - composefs::fs::read_filesystem(rustix::fs::CWD, path.as_std_path(), Some(&repo), false)?; + composefs::fs::read_container_root(rustix::fs::CWD, path.as_std_path(), Some(&repo))?; fs.transform_for_boot(&repo).context("Preparing for boot")?; let id = fs.compute_image_id(); let digest = id.to_hex(); diff --git a/crates/lib/src/cfsctl.rs b/crates/lib/src/cfsctl.rs index 60e50beb8..07337dfaf 100644 --- a/crates/lib/src/cfsctl.rs +++ b/crates/lib/src/cfsctl.rs @@ -123,14 +123,17 @@ enum Command { /// the mountpoint mountpoint: String, }, + /// Creates a composefs image from a filesystem CreateImage { path: PathBuf, #[clap(long)] bootable: bool, + /// Don't copy /usr metadata to root directory (use if root already has well-defined metadata) #[clap(long)] - stat_root: bool, + no_propagate_usr_to_root: bool, image_name: Option, }, + /// Computes the composefs image ID for a filesystem ComputeId { path: PathBuf, /// Write the dumpfile to the provided target @@ -138,15 +141,18 @@ enum Command { write_dumpfile_to: Option, #[clap(long)] bootable: bool, + /// Don't copy /usr metadata to root directory (use if root already has well-defined metadata) #[clap(long)] - stat_root: bool, + no_propagate_usr_to_root: bool, }, + /// Outputs the composefs dumpfile format for a filesystem CreateDumpfile { path: PathBuf, #[clap(long)] bootable: bool, + /// Don't copy /usr metadata to root directory (use if root already has well-defined metadata) #[clap(long)] - stat_root: bool, + no_propagate_usr_to_root: bool, }, ImageObjects { name: String, @@ -215,7 +221,7 @@ where ref config_verity, } => { let verity = verity_opt(config_verity)?; - let mut fs = + let fs = composefs_oci::image::create_filesystem(repo, config_name, verity.as_ref())?; fs.print_dumpfile()?; } @@ -316,9 +322,13 @@ where ref path, write_dumpfile_to, bootable, - stat_root, + no_propagate_usr_to_root, } => { - let mut fs = composefs::fs::read_filesystem(CWD, path, Some(repo.as_ref()), stat_root)?; + let mut fs = if no_propagate_usr_to_root { + composefs::fs::read_filesystem(CWD, path, Some(repo.as_ref()))? + } else { + composefs::fs::read_container_root(CWD, path, Some(repo.as_ref()))? + }; if bootable { fs.transform_for_boot(repo)?; } @@ -334,10 +344,14 @@ where Command::CreateImage { ref path, bootable, - stat_root, + no_propagate_usr_to_root, ref image_name, } => { - let mut fs = composefs::fs::read_filesystem(CWD, path, Some(repo.as_ref()), stat_root)?; + let mut fs = if no_propagate_usr_to_root { + composefs::fs::read_filesystem(CWD, path, Some(repo.as_ref()))? + } else { + composefs::fs::read_container_root(CWD, path, Some(repo.as_ref()))? + }; if bootable { fs.transform_for_boot(repo)?; } @@ -347,9 +361,13 @@ where Command::CreateDumpfile { ref path, bootable, - stat_root, + no_propagate_usr_to_root, } => { - let mut fs = composefs::fs::read_filesystem(CWD, path, Some(repo.as_ref()), stat_root)?; + let mut fs = if no_propagate_usr_to_root { + composefs::fs::read_filesystem(CWD, path, Some(repo.as_ref()))? + } else { + composefs::fs::read_container_root(CWD, path, Some(repo.as_ref()))? + }; if bootable { fs.transform_for_boot(repo)?; } From 17f846f2635669b20ca8f73d65a0e0f95cbe18d6 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Tue, 13 Jan 2026 20:44:54 -0500 Subject: [PATCH 08/11] Add validator for composefs digest views We changed how composefs digests are computed to ensure that mounted filesystem via --mount=type=image and install-time view (OCI tar layer processing from containers-storage) match. There were various problems like differing metadata for `/` among other things. Signed-off-by: Colin Walters --- .github/workflows/ci.yml | 4 +++ Justfile | 6 ++++ crates/xtask/src/xtask.rs | 63 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0081dfa11..636fd80c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -198,6 +198,10 @@ jobs: - name: Unit and container integration tests run: just test-container + - name: Validate composefs digest (sealed UKI only) + if: matrix.variant == 'composefs-sealeduki-sdboot' + run: just validate-composefs-digest + - name: Run TMT integration tests run: | if [ "${{ matrix.variant }}" = "composefs-sealeduki-sdboot" ]; then diff --git a/Justfile b/Justfile index 15d157632..22a2ebc85 100644 --- a/Justfile +++ b/Justfile @@ -130,6 +130,12 @@ package: test-composefs: just variant=composefs-sealeduki-sdboot test-tmt readonly local-upgrade-reboot +# Validate composefs digests match between build-time and install-time views. +# This catches mtime/metadata issues that cause sealed boot failures. +# Note: This requires a locally-built image with the compute-composefs-digest commands. +validate-composefs-digest: + cargo xtask validate-composefs-digest {{base_img}} + # Only used by ci.yml right now build-install-test-image: build cd hack && podman build {{base_buildargs}} -t {{base_img}}-install -f Containerfile.drop-lbis diff --git a/crates/xtask/src/xtask.rs b/crates/xtask/src/xtask.rs index 034a06b12..d01c9a6f4 100644 --- a/crates/xtask/src/xtask.rs +++ b/crates/xtask/src/xtask.rs @@ -54,6 +54,15 @@ enum Commands { TmtProvision(TmtProvisionArgs), /// Check build system properties (e.g., reproducible builds) CheckBuildsys, + /// Validate composefs digests match between build-time and install-time views + ValidateComposefsDigest(ValidateComposefsDigestArgs), +} + +/// Arguments for validate-composefs-digest command +#[derive(Debug, Args)] +pub(crate) struct ValidateComposefsDigestArgs { + /// Container image to validate (e.g., "localhost/bootc" or "quay.io/centos-bootc/centos-bootc:stream10") + pub(crate) image: String, } /// Arguments for run-tmt command @@ -139,6 +148,7 @@ fn try_main() -> Result<()> { Commands::RunTmt(args) => tmt::run_tmt(&sh, &args), Commands::TmtProvision(args) => tmt::tmt_provision(&sh, &args), Commands::CheckBuildsys => buildsys::check_buildsys(&sh, "Dockerfile".into()), + Commands::ValidateComposefsDigest(args) => validate_composefs_digest(&sh, &args), } } @@ -406,3 +416,56 @@ fn update_generated(sh: &Shell) -> Result<()> { Ok(()) } + +/// Validate that composefs digests match between build-time and install-time views. +/// +/// Compares dumpfiles generated from: +/// 1. The mounted filesystem (what seal-uki sees at build time via --mount=type=image) +/// 2. The OCI tar layers in containers-storage (what bootc upgrade sees) +/// +/// This helps debug mtime and metadata discrepancies that cause sealed boot failures. +#[context("Validating composefs digest")] +fn validate_composefs_digest(sh: &Shell, args: &ValidateComposefsDigestArgs) -> Result<()> { + let image = &args.image; + + // Generate dumpfile from mounted filesystem (build-time view) + let build_dumpfile = cmd!( + sh, + "podman run --rm --mount=type=image,source={image},target=/target {image} bootc container compute-composefs-digest /target" + ) + .read()?; + + // Generate dumpfile from containers-storage (install-time view) + let format_arg = "{{.Store.GraphRoot}}"; + let graphroot = cmd!(sh, "podman system info -f {format_arg}").read()?; + let graphroot = graphroot.trim(); + let storage_vol = format!("{graphroot}:/run/host-container-storage:ro"); + let storage_dumpfile = cmd!( + sh, + "podman run --rm --privileged --security-opt=label=disable + -v {storage_vol} + -v /sys:/sys:ro + --tmpfs=/var + {image} + bootc container compute-composefs-digest-from-storage" + ) + .read()?; + + // Compare dumpfiles + if build_dumpfile == storage_dumpfile { + println!("OK: Dumpfiles match"); + Ok(()) + } else { + println!("MISMATCH: Dumpfiles differ:"); + // Use diff via process substitution by writing to temp files + let tmpdir = tempfile::tempdir()?; + let build_path = tmpdir.path().join("build.dumpfile"); + let storage_path = tmpdir.path().join("storage.dumpfile"); + std::fs::write(&build_path, &build_dumpfile)?; + std::fs::write(&storage_path, &storage_dumpfile)?; + cmd!(sh, "diff -u {build_path} {storage_path}") + .ignore_status() + .run()?; + anyhow::bail!("Composefs digest mismatch"); + } +} From 22e2fe806d4c8d114c3b52d056e38ff50ebd3ac3 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 16 Jan 2026 08:45:32 -0500 Subject: [PATCH 09/11] docs: Expand composefs backend documentation Add comprehensive documentation for building sealed bootc images, focusing on the core concepts and the key command: `bootc container compute-composefs-digest`. Key additions: - Document how sealed images work (UKI + composefs digest + Secure Boot) - Explain the build workflow abstractly without distribution-specific details - Document the compute-composefs-digest command and its options - Add section on generating/signing UKIs with ukify - Document developer testing commands (just variant=composefs-sealeduki-sdboot) - Add validation tooling documentation This provides the foundation for distribution-specific documentation to build upon with concrete Containerfile examples. Assisted-by: OpenCode (Claude Sonnet 4) Signed-off-by: Colin Walters --- docs/src/experimental-composefs.md | 136 ++++++++++++++++++++++++++--- 1 file changed, 123 insertions(+), 13 deletions(-) diff --git a/docs/src/experimental-composefs.md b/docs/src/experimental-composefs.md index 7d2b87673..7e3dd8dba 100644 --- a/docs/src/experimental-composefs.md +++ b/docs/src/experimental-composefs.md @@ -16,36 +16,143 @@ This is based on [Unified Kernel Images](https://uapi-group.org/specifications/s that embed a digest of the target container root filesystem, typically alongside a bootloader (such as systemd-boot) also signed with your key. -### UKIs in bootc containers +## How Sealed Images Work -There must be exactly one UKI placed in `/boot/EFI/Linux/.efi`. +A sealed image is a cryptographically signed and verified bootc image that provides end-to-end integrity protection. This is achieved through: -### Bootloader support +- **Unified Kernel Images (UKIs)**: Combining kernel, initramfs, and boot parameters into a single signed binary +- **Composefs integration**: Using composefs with fsverity for content-addressed filesystem verification +- **Secure Boot**: Cryptographic signatures on both the UKI and systemd-boot loader -To use sealed images, ensure that the target container image has systemd-boot, -and does not have `bootupd`. +A sealed image includes: -### Installation +1. **composefs digest**: A SHA-512 hash of the entire root filesystem, computed at build time +2. **Unified Kernel Image (UKI)**: A single EFI binary containing the kernel, initramfs, and kernel command line with the composefs digest embedded +3. **Secure Boot signature**: The UKI is signed with your private key -There is a `--composefs-backend` option for `bootc install`; however, if -a UKI and systemd-boot are detected, it will automatically be used. +At boot time, the composefs digest in the kernel command line (e.g., `composefs=`) is verified against the mounted root filesystem. This creates a chain of trust from firmware to userspace, ensuring the system will only boot if the root filesystem matches exactly what was signed. -### Developing and testing bootc with sealed composefs +## Building Sealed Images -Use `just variant=composefs-sealeduki-sdboot build` to build a local sealed -UKI, using Secure Boot keys generated in `target/test-secureboot`. This is -not a production path. +### Prerequisites + +For sealed images, the container must: + +- Include a kernel and initramfs in `/usr/lib/modules//` +- Have systemd-boot available (and NOT have `bootupd`) +- Not include a pre-built UKI (the build process generates one) + +Sealed images also require: + +- Secure Boot support in the target system firmware +- A filesystem with fsverity support (e.g., ext4, btrfs) for the root partition + +### Build Pattern: Compute Digest and Generate UKI in One Stage + +The key to building sealed images is using a multi-stage Dockerfile where a separate stage mounts the target rootfs, computes its composefs digest, and generates the signed UKI in one step: + +```dockerfile +# Build your rootfs with all packages and configuration +FROM as rootfs +RUN apt|dnf|zypper install ... && bootc container lint --fatal-warnings + +# Generate the sealed UKI in a tools stage +FROM as sealed-uki +RUN --mount=type=bind,from=rootfs,target=/target \ + --mount=type=secret,id=secureboot_key \ + --mount=type=secret,id=secureboot_cert <`) +4. The final stage copies the signed UKI into the rootfs without modifying any files used in the digest calculation + +### The `bootc container compute-composefs-digest` Command + +```bash +bootc container compute-composefs-digest [PATH] +``` + +Computes the composefs digest for a filesystem. The digest is a 128-character SHA-512 hex string that uniquely identifies the filesystem contents. + +**Options:** + +- `PATH`: Path to the filesystem root (default: `/target`) +- `--write-dumpfile-to `: Generate a dumpfile for debugging + +> **Note**: This command is currently hidden from `--help` output as it's part of the experimental composefs feature set. + +### Final Image Structure + +The sealed image should have: + +- The signed UKI at `/boot/EFI/Linux/.efi` +- A signed systemd-boot at `/boot/EFI/BOOT/BOOTX64.EFI` and `/boot/EFI/systemd/systemd-bootx64.efi` +- The raw `vmlinuz` and `initramfs.img` removed from `/usr/lib/modules//` (they're now embedded in the UKI) + +### External Signing Workflow + +For production environments with dedicated signing infrastructure: + +1. **Build unsigned UKI**: Compute digest and create an unsigned UKI (omit `--signtool` from ukify) +2. **Sign externally**: Take the unsigned UKI to your signing infrastructure +3. **Complete the seal**: Inject the signed UKI into the final image + +This workflow is planned for streamlining in future releases (see [#1498](https://github.com/bootc-dev/bootc/issues/1498)). + +## Developing and Testing bootc with composefs + +See [CONTRIBUTING.md](https://github.com/bootc-dev/bootc/blob/main/CONTRIBUTING.md) for information on building and testing bootc itself with composefs support. + +## Bootloader Support + +To use sealed images, the container image must have a UKI and systemd-boot installed (and not have `bootupd`). If these conditions are met, bootc will automatically detect and use the composefs backend during installation. + +## Installation + +There is a `--composefs-backend` option for `bootc install` to explicitly select a composefs backend apart from sealed images; this is not as heavily tested yet. ## Current Limitations -- **Experimental**: In particular, the on-disk formats are subject to change +- **Experimental**: The on-disk formats are subject to change - **UX refinement**: The user experience for building and managing sealed images is still being improved +- **SELinux**: Currently uses `enforcing=0` in the kernel command line (see [#1826](https://github.com/bootc-dev/bootc/issues/1826)) +- **kargs.d**: Custom kernel arguments from `/usr/lib/bootc/kargs.d` are not yet automatically included in sealed UKIs ## Related Issues - [#1190](https://github.com/bootc-dev/bootc/issues/1190) - composefs-native backend (main tracker) - [#1498](https://github.com/bootc-dev/bootc/issues/1498) - Sealed image build UX + implementation - [#1703](https://github.com/bootc-dev/bootc/issues/1703) - OCI config mismatch issues +- [#1826](https://github.com/bootc-dev/bootc/issues/1826) - SELinux enforcement with composefs - [#20](https://github.com/bootc-dev/bootc/issues/20) - Unified storage (long-term goal) - [#806](https://github.com/bootc-dev/bootc/issues/806) - UKI/systemd-boot tracker @@ -53,3 +160,6 @@ not a production path. - See [filesystem.md](filesystem.md) for information about composefs in the standard ostree backend - See [bootloaders.md](bootloaders.md) for bootloader configuration details +- [composefs-rs](https://github.com/containers/composefs-rs) - The underlying composefs implementation +- [Unified Kernel Images specification](https://uapi-group.org/specifications/specs/unified_kernel_image/) +- [ukify documentation](https://www.freedesktop.org/software/systemd/man/latest/ukify.html) - Tool for building UKIs From aedb123ebb6662081d92800f16066f866c4bbbe0 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 16 Jan 2026 08:57:17 -0500 Subject: [PATCH 10/11] docs: Improve Justfile with groups and self-documenting targets Justfile changes: - Organize targets into groups (core, testing, docs, debugging, maintenance) - Add `list-variants` target to show available build variants - Simplify comments to be concise single-line descriptions - Move composefs targets (build-sealed, test-composefs) into core group CONTRIBUTING.md changes: - Reference `just --list` and `just list-variants` instead of duplicating - Remove tables that duplicate Justfile information - Fix broken link to cli.rs The Justfile is now self-documenting via `just --list` (grouped targets) and `just list-variants` (build configuration options). Assisted-by: OpenCode (Claude Sonnet 4) Signed-off-by: Colin Walters --- CONTRIBUTING.md | 49 ++++++- Justfile | 381 +++++++++++++++++++++++++----------------------- 2 files changed, 243 insertions(+), 187 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b395bd58b..31d217f4c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,11 +41,20 @@ accepted! Worth stating: before you start diving into the code you should understand using the system as a user and how it works. See the user documentation for that. -## Understanding the Justfile +## The Justfile + +The [Justfile](Justfile) is the primary interface for building and testing bootc. + +```bash +just --list # Show all targets organized by group +just list-variants # Show available build variants and current config +``` + +### Building from source Edit the source code; a simple thing to do is add e.g. -`eprintln!("hello world");` into `run_from_opt` in [crates/lib/src/cli.rs](cli.rs). -You can run `make` or `cargo build` to build that locally. However, a key +`eprintln!("hello world");` into `run_from_opt` in [crates/lib/src/cli.rs](crates/lib/src/cli.rs). +You can run `make` or `cargo build` to build that locally. However, a key next step is to get that binary into a bootc container image. Running `just` defaults to `just build` which will build a container @@ -58,14 +67,24 @@ and try running `bootc`. ### Running container-oriented integration tests -`just test-container` +```bash +just test-container +``` ### Running (TMT) integration tests A common cycle here is you'll edit e.g. `deploy.rs` and want to run the tests that perform an upgrade: -`just test-tmt-one test-20-local-upgrade` +```bash +just test-tmt local-upgrade-reboot +``` + +To run a specific test: + +```bash +just test-tmt readonly +``` ### Faster iteration cycles @@ -96,6 +115,26 @@ then you can `cargo b --release` directly in a Fedora 42 container or even on your host system, and then directly run e.g. `./target/release/bootc upgrade` etc. +### Testing with composefs (sealed images) + +To build and test with the experimental composefs backend: + +```bash +# Build a sealed image with auto-generated test Secure Boot keys +just build-sealed + +# Run composefs-specific tests +just test-composefs + +# Validate that composefs digests match between build and install views +# (useful for debugging mtime/metadata issues) +just validate-composefs-digest +``` + +The `build-sealed` target generates test Secure Boot keys in `target/test-secureboot/` +and builds a complete sealed image with UKI. See [experimental-composefs.md](docs/src/experimental-composefs.md) +for more information on sealed images. + ### Debugging via lldb diff --git a/Justfile b/Justfile index 22a2ebc85..f436366a4 100644 --- a/Justfile +++ b/Justfile @@ -1,257 +1,274 @@ # The default entrypoint to working on this project. -# Commands here typically wrap e.g. `podman build` or -# other tools like `bcvk` which might launch local virtual machines. -# +# Run `just --list` to see available targets organized by group. +# # See also `Makefile` and `xtask.rs`. Commands which end in `-local` # skip containerization or virtualization (and typically just proxy `make`). # -# Rules written here are *often* used by the Github Action flows, -# and should support being configurable where that makes sense (e.g. -# the `build` rule supports being provided a base image). -# -# By default the layering should be thus: +# By default the layering is: # Github Actions -> Justfile -> podman -> make -> rustc -# -> podman -> dnf|apt ... +# -> podman -> package manager # -> cargo xtask # -------------------------------------------------------------------- -# This image is just the base image plus our updated bootc binary +# Configuration variables (override via environment or command line) +# Example: BOOTC_base=quay.io/fedora/fedora-bootc:42 just build + +# Output image name base_img := "localhost/bootc" -# Has a synthetic upgrade +# Synthetic upgrade image for testing upgrade_img := base_img + "-upgrade" -# ostree: The default -# composefs-sealeduki-sdboot: A system with a sealed composefs using systemd-boot +# Build variant: ostree (default) or composefs-sealeduki-sdboot (sealed UKI) variant := env("BOOTC_variant", "ostree") +# Base container image to build from base := env("BOOTC_base", "quay.io/centos-bootc/centos-bootc:stream10") +# Buildroot base image buildroot_base := env("BOOTC_buildroot_base", "quay.io/centos/centos:stream10") +# Optional: path to extra source (e.g. composefs-rs) for local development +extra_src := env("BOOTC_extra_src", "") +# Internal variables testimage_label := "bootc.testimage=1" -# Images used by hack/lbi; keep in sync lbi_images := "quay.io/curl/curl:latest quay.io/curl/curl-base:latest registry.access.redhat.com/ubi9/podman:latest" -# We used to have --jobs=4 here but sometimes that'd hit this -# ``` -# [2/3] STEP 2/2: RUN --mount=type=bind,from=context,target=/run/context < Using cache b068d42ac7491067cf5fafcaaf2f09d348e32bb752a22c85bbb87f266409554d -# --> b068d42ac749 -# + cd /run/context/ -# /bin/sh: line 3: cd: /run/context/: Permission denied -# ``` -# TODO: Gather more info and file a buildah bug +fedora-coreos := "quay.io/fedora/fedora-coreos:testing-devel" generic_buildargs := "" -# Optional: path to extra source directory (e.g. composefs-rs) to bind mount into builds. -# Usage: BOOTC_extra_src=$HOME/src/github/containers/composefs-rs just build -# The directory will be mounted at /run/extra-src inside the container. -# When using this, you must also patch Cargo.toml to use path dependencies: -# composefs = { path = "/run/extra-src/crates/composefs", ... } -# Note: This disables SELinux labeling for the mount. -extra_src := env("BOOTC_extra_src", "") -# Generate podman args for extra source mount if configured _extra_src_args := if extra_src != "" { "-v " + extra_src + ":/run/extra-src:ro --security-opt=label=disable" } else { "" } -# Args for package building (no secrets needed, just builds RPMs) base_buildargs := generic_buildargs + " " + _extra_src_args + " --build-arg=base=" + base + " --build-arg=variant=" + variant -# - scratch builds need extra perms per https://docs.fedoraproject.org/en-US/bootc/building-from-scratch/ -# - we do secure boot signing here, so provide the keys buildargs := base_buildargs \ + " --cap-add=all --security-opt=label=type:container_runtime_t --device /dev/fuse" \ + " --secret=id=secureboot_key,src=target/test-secureboot/db.key --secret=id=secureboot_cert,src=target/test-secureboot/db.crt" -# The default target: build the container image from current sources. -# Note commonly you might want to override the base image via e.g. -# `just build --build-arg=base=quay.io/fedora/fedora-bootc:42` -# into the container image. -# -# Note you can set `BOOTC_SKIP_PACKAGE=1` in the environment to bypass this stage. + +# ============================================================================ +# Core workflows - the main targets most developers will use +# ============================================================================ + +# Show available build variants and current configuration +[group('core')] +list-variants: + #!/bin/bash + cat <<'EOF' + Build Variants (set via BOOTC_variant= or variant=) + ==================================================== + + ostree (default) + Standard bootc image using ostree backend. + This is the traditional, production-ready configuration. + + composefs-sealeduki-sdboot + Sealed composefs image with: + - Unified Kernel Image (UKI) containing kernel + initramfs + cmdline + - Secure Boot signing (using keys in target/test-secureboot/) + - systemd-boot bootloader + - composefs digest embedded in kernel cmdline for verified boot + + Use `just build-sealed` as a shortcut, or: + just variant=composefs-sealeduki-sdboot build + + Current Configuration + ===================== + EOF + echo " BOOTC_variant={{variant}}" + echo " BOOTC_base={{base}}" + echo " BOOTC_extra_src={{extra_src}}" + echo "" + +# Build container image from current sources +[group('core')] build: package _keygen && _pull-lbi-images #!/bin/bash set -xeuo pipefail test -d target/packages - # Use --build-context to pass packages instead of -v to avoid mount stubs in /run pkg_path=$(realpath target/packages) podman build --build-context "packages=${pkg_path}" -t {{base_img}} {{buildargs}} . -# Pull images used by hack/lbi -_pull-lbi-images: - podman pull -q --retry 5 --retry-delay 5s {{lbi_images}} - -# Compute SOURCE_DATE_EPOCH and VERSION from git for reproducible builds. -# Outputs shell variable assignments that can be eval'd. -_git-build-vars: - #!/bin/bash - set -euo pipefail - SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) - # Compute version from git (matching xtask.rs gitrev logic) - if VERSION=$(git describe --tags --exact-match 2>/dev/null); then - VERSION="${VERSION#v}" - VERSION="${VERSION//-/.}" - else - COMMIT=$(git rev-parse HEAD | cut -c1-10) - COMMIT_TS=$(git show -s --format=%ct) - TIMESTAMP=$(date -u -d @${COMMIT_TS} +%Y%m%d%H%M) - VERSION="${TIMESTAMP}.g${COMMIT}" - fi - echo "SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}" - echo "VERSION=${VERSION}" - -# Needed by bootc install on ostree -fedora-coreos := "quay.io/fedora/fedora-coreos:testing-devel" -# Generate Secure Boot keys (only for our own CI/testing) -_keygen: - ./hack/generate-secureboot-keys - -# Build a sealed image from current sources. +# Build a sealed composefs image (alias for variant=composefs-sealeduki-sdboot) +[group('core')] build-sealed: @just --justfile {{justfile()}} variant=composefs-sealeduki-sdboot build -# Build packages (e.g. RPM) into target/packages/ -# Any old packages will be removed. -# Set BOOTC_SKIP_PACKAGE=1 in the environment to bypass this stage. We don't -# yet have an accurate ability to avoid rebuilding this in CI yet. -package: - #!/bin/bash - set -xeuo pipefail - packages=target/packages - if test -n "${BOOTC_SKIP_PACKAGE:-}"; then - if test '!' -d "${packages}"; then - echo "BOOTC_SKIP_PACKAGE is set, but missing ${packages}" 1>&2; exit 1 - fi - exit 0 - fi - eval $(just _git-build-vars) - echo "Building RPM with version: ${VERSION}" - podman build {{base_buildargs}} --build-arg=SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} --build-arg=pkgversion=${VERSION} -t localhost/bootc-pkg --target=build . - mkdir -p "${packages}" - rm -vf "${packages}"/*.rpm - podman run --rm localhost/bootc-pkg tar -C /out/ -cf - . | tar -C "${packages}"/ -xvf - - chmod a+rx target "${packages}" - chmod a+r "${packages}"/*.rpm - # Keep localhost/bootc-pkg for layer caching; use `just clean-local-images` to reclaim space +# Run tmt integration tests in VMs (e.g. `just test-tmt readonly`) +[group('core')] +test-tmt *ARGS: build + @just _build-upgrade-image + @just test-tmt-nobuild {{ARGS}} + +# Run containerized unit and integration tests +[group('core')] +test-container: build build-units + podman run --rm --read-only localhost/bootc-units /usr/bin/bootc-units + podman run --rm --env=BOOTC_variant={{variant}} --env=BOOTC_base={{base}} {{base_img}} bootc-integration-tests container -# Build+test using the `composefs-sealeduki-sdboot` variant. +# Build and test sealed composefs images +[group('core')] test-composefs: just variant=composefs-sealeduki-sdboot test-tmt readonly local-upgrade-reboot -# Validate composefs digests match between build-time and install-time views. -# This catches mtime/metadata issues that cause sealed boot failures. -# Note: This requires a locally-built image with the compute-composefs-digest commands. -validate-composefs-digest: - cargo xtask validate-composefs-digest {{base_img}} - -# Only used by ci.yml right now -build-install-test-image: build - cd hack && podman build {{base_buildargs}} -t {{base_img}}-install -f Containerfile.drop-lbis - -# These tests accept the container image as input, and may spawn it. -run-container-external-tests: - ./tests/container/run {{base_img}} - -# We build the unit tests into a container image -build-units: - #!/bin/bash - set -xeuo pipefail - eval $(just _git-build-vars) - podman build {{base_buildargs}} --build-arg=SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} --build-arg=pkgversion=${VERSION} --target units -t localhost/bootc-units . - -# Perform validation (build, linting) in a container build environment +# Run cargo fmt and clippy checks in container +[group('core')] validate: podman build {{base_buildargs}} --target validate . -# Run tmt-based test suites using local virtual machines with -# bcvk. -# -# To run an individual test, pass it as an argument like: -# `just test-tmt readonly` -test-tmt *ARGS: build - @just _build-upgrade-image - @just test-tmt-nobuild {{ARGS}} - -# Generate a local synthetic upgrade -_build-upgrade-image: - cat tmt/tests/Dockerfile.upgrade | podman build -t {{upgrade_img}} --from={{base_img}} - +# ============================================================================ +# Testing variants and utilities +# ============================================================================ -# Assume the localhost/bootc image is up to date, and just run tests. -# Useful for iterating on tests quickly. +# Run tmt tests without rebuilding (for fast iteration) +[group('testing')] test-tmt-nobuild *ARGS: cargo xtask run-tmt --env=BOOTC_variant={{variant}} --upgrade-image={{upgrade_img}} {{base_img}} {{ARGS}} -# Build test container image for testing on coreos with SKIP_CONFIGS=1, -# which skips LBIs, test kargs, and install configs that would conflict with FCOS. +# Run tmt tests on Fedora CoreOS +[group('testing')] +test-tmt-on-coreos *ARGS: + cargo xtask run-tmt --env=BOOTC_variant={{variant}} --env=BOOTC_target={{base_img}}-coreos:latest {{fedora-coreos}} {{ARGS}} + +# Run external container tests against localhost/bootc +[group('testing')] +run-container-external-tests: + ./tests/container/run {{base_img}} + +# Remove all test VMs created by tmt tests +[group('testing')] +tmt-vm-cleanup: + bcvk libvirt rm --stop --force --label bootc.test=1 + +# Build test image for Fedora CoreOS testing +[group('testing')] build-testimage-coreos PATH: _keygen #!/bin/bash set -xeuo pipefail pkg_path=$(realpath "{{PATH}}") - # Use --build-context to pass packages instead of -v to avoid mount stubs in /run podman build --build-context "packages=${pkg_path}" \ --build-arg SKIP_CONFIGS=1 \ -t {{base_img}}-coreos {{buildargs}} . -# Run test bootc install on FCOS -# BOOTC_target is `bootc-coreos`, it will be used for bootc install. -# Run `just build-testimage-coreos target/packages` to build test image firstly, -# then run `just test-tmt-on-coreos plan-bootc-install-on-coreos` -test-tmt-on-coreos *ARGS: - cargo xtask run-tmt --env=BOOTC_variant={{variant}} --env=BOOTC_target={{base_img}}-coreos:latest {{fedora-coreos}} {{ARGS}} - -# Cleanup all test VMs created by tmt tests -tmt-vm-cleanup: - bcvk libvirt rm --stop --force --label bootc.test=1 - -# Run tests (unit and integration) that are containerized -test-container: build build-units - podman run --rm --read-only localhost/bootc-units /usr/bin/bootc-units - # Pass these through for cross-checking - podman run --rm --env=BOOTC_variant={{variant}} --env=BOOTC_base={{base}} {{base_img}} bootc-integration-tests container +# Build test image for install tests (used by CI) +[group('testing')] +build-install-test-image: build + cd hack && podman build {{base_buildargs}} -t {{base_img}}-install -f Containerfile.drop-lbis -# Remove all container images built (locally) via this Justfile, by matching a label -clean-local-images: - podman images --filter "label={{testimage_label}}" - podman images --filter "label={{testimage_label}}" --format "{{{{.ID}}" | xargs -r podman rmi -f - podman image prune -f - podman rmi {{fedora-coreos}} -f +# ============================================================================ +# Documentation +# ============================================================================ -# Print the container image reference for a given short $ID-VERSION_ID for NAME -# and 'base' or 'buildroot-base' for TYPE (base image type) -pullspec-for-os TYPE NAME: - @jq -r --arg v "{{NAME}}" '."{{TYPE}}"[$v]' < hack/os-image-map.json +# Serve docs locally (prints URL) +[group('docs')] +mdbook-serve: build-mdbook + #!/bin/bash + set -xeuo pipefail + podman run --init --replace -d --name bootc-mdbook --rm --publish 127.0.0.1::8000 localhost/bootc-mdbook + echo http://$(podman port bootc-mdbook 8000/tcp) +# Build the documentation (mdbook) +[group('docs')] build-mdbook: #!/bin/bash set -xeuo pipefail secret_arg="" - # Pass GH_TOKEN to avoid API rate limits when cargo-binstall fetches binaries if test -n "${GH_TOKEN:-}"; then secret_arg="--secret=id=GH_TOKEN,env=GH_TOKEN" fi podman build {{generic_buildargs}} ${secret_arg} -t localhost/bootc-mdbook -f docs/Dockerfile.mdbook . -# Generate the rendered HTML to the target DIR directory +# Build docs and extract to DIR +[group('docs')] build-mdbook-to DIR: build-mdbook #!/bin/bash set -xeuo pipefail - # Create a temporary container to extract the built docs container_id=$(podman create localhost/bootc-mdbook) podman cp ${container_id}:/src/docs/book {{DIR}} podman rm -f ${container_id} -mdbook-serve: build-mdbook - #!/bin/bash - set -xeuo pipefail - podman run --init --replace -d --name bootc-mdbook --rm --publish 127.0.0.1::8000 localhost/bootc-mdbook - echo http://$(podman port bootc-mdbook 8000/tcp) +# ============================================================================ +# Debugging and validation +# ============================================================================ -# Update all generated files (man pages and JSON schemas) -# -# This is the unified command that: -# - Auto-discovers new CLI commands and creates man page templates -# - Syncs CLI options from Rust code to existing man page templates -# - Updates JSON schema files -# -# Use this after adding, removing, or modifying CLI options or schemas. -update-generated: - cargo run -p xtask update-generated +# Validate composefs digests match between build and install views +[group('debugging')] +validate-composefs-digest: + cargo xtask validate-composefs-digest {{base_img}} -# Verify build system properties (reproducible builds) -# -# This runs `just package` twice and verifies that the resulting RPMs -# are bit-for-bit identical, confirming SOURCE_DATE_EPOCH is working. +# Verify reproducible builds (runs package twice, compares output) +[group('debugging')] check-buildsys: cargo run -p xtask check-buildsys + +# Get container image pullspec for a given OS (e.g. `pullspec-for-os base fedora-42`) +[group('debugging')] +pullspec-for-os TYPE NAME: + @jq -r --arg v "{{NAME}}" '."{{TYPE}}"[$v]' < hack/os-image-map.json + +# ============================================================================ +# Maintenance +# ============================================================================ + +# Update generated files (man pages, JSON schemas) +[group('maintenance')] +update-generated: + cargo run -p xtask update-generated + +# Remove all locally-built test container images +[group('maintenance')] +clean-local-images: + podman images --filter "label={{testimage_label}}" + podman images --filter "label={{testimage_label}}" --format "{{{{.ID}}" | xargs -r podman rmi -f + podman image prune -f + podman rmi {{fedora-coreos}} -f + +# Build packages (RPM) into target/packages/ +[group('maintenance')] +package: + #!/bin/bash + set -xeuo pipefail + packages=target/packages + if test -n "${BOOTC_SKIP_PACKAGE:-}"; then + if test '!' -d "${packages}"; then + echo "BOOTC_SKIP_PACKAGE is set, but missing ${packages}" 1>&2; exit 1 + fi + exit 0 + fi + eval $(just _git-build-vars) + echo "Building RPM with version: ${VERSION}" + podman build {{base_buildargs}} --build-arg=SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} --build-arg=pkgversion=${VERSION} -t localhost/bootc-pkg --target=build . + mkdir -p "${packages}" + rm -vf "${packages}"/*.rpm + podman run --rm localhost/bootc-pkg tar -C /out/ -cf - . | tar -C "${packages}"/ -xvf - + chmod a+rx target "${packages}" + chmod a+r "${packages}"/*.rpm + +# Build unit tests into a container image +[group('maintenance')] +build-units: + #!/bin/bash + set -xeuo pipefail + eval $(just _git-build-vars) + podman build {{base_buildargs}} --build-arg=SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} --build-arg=pkgversion=${VERSION} --target units -t localhost/bootc-units . + +# ============================================================================ +# Internal helpers (prefixed with _) +# ============================================================================ + +_pull-lbi-images: + podman pull -q --retry 5 --retry-delay 5s {{lbi_images}} + +_git-build-vars: + #!/bin/bash + set -euo pipefail + SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) + if VERSION=$(git describe --tags --exact-match 2>/dev/null); then + VERSION="${VERSION#v}" + VERSION="${VERSION//-/.}" + else + COMMIT=$(git rev-parse HEAD | cut -c1-10) + COMMIT_TS=$(git show -s --format=%ct) + TIMESTAMP=$(date -u -d @${COMMIT_TS} +%Y%m%d%H%M) + VERSION="${TIMESTAMP}.g${COMMIT}" + fi + echo "SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}" + echo "VERSION=${VERSION}" + +_keygen: + ./hack/generate-secureboot-keys + +_build-upgrade-image: + cat tmt/tests/Dockerfile.upgrade | podman build -t {{upgrade_img}} --from={{base_img}} - From 98e07a83a014ace6b5c9e61168a985bf9400db6c Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 16 Jan 2026 13:37:11 -0500 Subject: [PATCH 11/11] build-sys: Add BOOTC_nocache option to force rebuild without cache This is useful when debugging issues with stale cached layers, such as package version skew between base images and repos. Signed-off-by: Colin Walters --- Justfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Justfile b/Justfile index f436366a4..10527dd07 100644 --- a/Justfile +++ b/Justfile @@ -28,6 +28,8 @@ buildroot_base := env("BOOTC_buildroot_base", "quay.io/centos/centos:stream10") extra_src := env("BOOTC_extra_src", "") # Internal variables +nocache := env("BOOTC_nocache", "") +_nocache_arg := if nocache != "" { "--no-cache" } else { "" } testimage_label := "bootc.testimage=1" lbi_images := "quay.io/curl/curl:latest quay.io/curl/curl-base:latest registry.access.redhat.com/ubi9/podman:latest" fedora-coreos := "quay.io/fedora/fedora-coreos:testing-devel" @@ -79,7 +81,7 @@ build: package _keygen && _pull-lbi-images set -xeuo pipefail test -d target/packages pkg_path=$(realpath target/packages) - podman build --build-context "packages=${pkg_path}" -t {{base_img}} {{buildargs}} . + podman build {{_nocache_arg}} --build-context "packages=${pkg_path}" -t {{base_img}} {{buildargs}} . # Build a sealed composefs image (alias for variant=composefs-sealeduki-sdboot) [group('core')]