From 1afc2327f04c3767f60a1b0540d495bd4678bbb6 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Thu, 14 May 2026 21:13:52 +0530 Subject: [PATCH 1/7] dockerfile/uki: Rework to remove kernel + initrd Now that we can pass kernel and initrd paths to `bootc ukify`, rework our UKI Dockerfile to remove kernel + initrd from the final layer and only keep the UKI This still will not *remove* the kernel + initrd from the tarball but have whiteout instead See https://github.com/bootc-dev/bootc/issues/2027#issuecomment-4244181869 Signed-off-by: Pragyan Poudyal --- Dockerfile | 44 ++++++++-- contrib/packaging/finalize-uki | 15 +--- contrib/packaging/seal-uki | 86 +++++++++++++------ crates/tests-integration/src/container.rs | 2 + tmt/tests/Dockerfile.upgrade | 34 ++++++-- tmt/tests/booted/tap.nu | 28 +++++- .../test-install-to-filesystem-var-mount.sh | 55 ++++++++---- 7 files changed, 192 insertions(+), 72 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9831efcca..6a77f0aec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -83,7 +83,7 @@ RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ # Install systemd-ukify and systemd-boot for UKIs # This also installs systemd-boot for the grub UKI case which is not ideal... if [[ "${boot_type}" == "uki" ]]; then - pkgs_to_install+=(systemd-ukify) + pkgs_to_install+=(systemd-ukify binutils) fi if [[ ${#pkgs_to_install[@]} -gt 0 ]]; then @@ -135,7 +135,10 @@ ARG pkgversion ARG SOURCE_DATE_EPOCH 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=tmpfs,target=/tmp --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome RPM_VERSION="${pkgversion}" /src/contrib/packaging/build-rpm +RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ + --mount=type=cache,target=/src/target \ + --mount=type=cache,target=/var/roothome \ + RPM_VERSION="${pkgversion}" /src/contrib/packaging/build-rpm # Build a systemd-sysext containing just the bootc binary. # Skips RPM machinery entirely for fast incremental rebuilds. @@ -218,7 +221,7 @@ COPY --from=update-generated-from-code /src/docs/src/*.schema.json /docs/src/ # ---- # Perform all filesystem transformations except generating the sealed UKI (if configured) -FROM base as base-penultimate +FROM base as base-penultimate-source ARG variant ARG bootloader ARG boot_type @@ -247,6 +250,10 @@ rm -rf /var/cache rm -rf /run/rhsm EORUN + +FROM base-penultimate-source as base-penultimate +ARG boot_type + # Configure the rootfs ARG rootfs="" RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ @@ -266,9 +273,19 @@ RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp install -D -m 0644 -t /usr/lib/bootc/kargs.d /run/usr-extras/lib/bootc/kargs.d/*.toml # Clean up package manager caches 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=base-penultimate-source,src=/,target=/run/base-penultimate-src \ + --mount=type=bind,from=packaging,src=/,target=/run/packaging <&2 - exit 1 -fi +kver=$1 +shift # Create the EFI directory structure mkdir -p /boot/EFI/Linux @@ -36,12 +31,6 @@ mkdir -p /boot/EFI/Linux 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 diff --git a/contrib/packaging/seal-uki b/contrib/packaging/seal-uki index 66de92ffd..bfc7c4bec 100755 --- a/contrib/packaging/seal-uki +++ b/contrib/packaging/seal-uki @@ -2,32 +2,70 @@ # 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 -allow_missing_verity=$1 -shift -seal_state=$1 -shift - -if [[ $seal_state == "sealed" && $allow_missing_verity == "true" ]]; then +missing_verity=() + +while [ ! -z "${1:-}" ]; do + case "$1" in + # Path to the desired root filesystem + "--target") + target="$2" + shift + shift + ;; + + # Write to this directory + "--output") + output="$2" + shift + shift + ;; + + # Path to secrets directory + "--secrets") + secrets="$2" + shift + shift + ;; + + "--allow-missing-verity") + missing_verity=(--allow-missing-verity) + shift + ;; + + "--seal-state") + seal_state="$2" + shift + shift + ;; + + # Path to the directory containing kernel and initramfs + "--kernel-dir") + kernel_dir="$2" + shift + shift + ;; + + * ) + echo "Argument $1 not understood" + exit 1 + ;; + esac +done + +if [[ $seal_state == "sealed" && ${#missing_verity[@]} -gt 0 ]]; then echo "Cannot have missing verity with sealed UKI" >&2 exit 1 fi -# Find the kernel version (needed for output filename) -kver=$(bootc container inspect --rootfs "${target}" --json | jq -r '.kernel.version') -if [ -z "$kver" ] || [ "$kver" = "null" ]; then - echo "Error: No kernel found" >&2 - exit 1 +if [[ -z $kernel_dir ]]; then + echo "kernel dir is required" >&2 + exit 1 fi +kernel_params=(--kernel-dir "$kernel_dir") + +kver=$(basename "$kernel_dir") + mkdir -p "${output}" # Baseline ukify options @@ -45,12 +83,6 @@ fi # Baseline container ukify options containerukifyargs=(--rootfs "${target}") -missing_verity=() - -if [[ $allow_missing_verity == "true" ]]; then - missing_verity+=(--allow-missing-verity) -fi - # Build the UKI using bootc container ukify # This computes the composefs digest, reads kargs from kargs.d, and invokes ukify -bootc container ukify "${containerukifyargs[@]}" "${missing_verity[@]}" -- "${ukifyargs[@]}" +bootc container ukify "${containerukifyargs[@]}" "${kernel_params[@]}" "${missing_verity[@]}" -- "${ukifyargs[@]}" diff --git a/crates/tests-integration/src/container.rs b/crates/tests-integration/src/container.rs index a7d8e89d6..b03a69f58 100644 --- a/crates/tests-integration/src/container.rs +++ b/crates/tests-integration/src/container.rs @@ -51,6 +51,8 @@ pub(crate) fn test_bootc_container_inspect() -> Result<()> { .as_bool() .expect("kernel.unified should be a boolean"); + println!("kernel: {kernel:#?}"); + let is_uki = std::env::var("BOOTC_boot_type").is_ok_and(|var| var == "uki"); if let Some(variant) = std::env::var("BOOTC_variant").ok() { diff --git a/tmt/tests/Dockerfile.upgrade b/tmt/tests/Dockerfile.upgrade index 561e2e0a7..3b1ab73c3 100644 --- a/tmt/tests/Dockerfile.upgrade +++ b/tmt/tests/Dockerfile.upgrade @@ -13,6 +13,18 @@ ARG filesystem=ext4 FROM scratch AS packaging COPY contrib/packaging / +# Get kernel + initrd from the UKI +FROM localhost/bootc as kernel +ARG boot_type +RUN <<-EOF + if test "${boot_type}" = "uki"; then + kver=$(bootc container inspect --rootfs / --json | jq -r '.kernel.version') + mkdir -p "/boot/$kver" + objcopy -O binary --only-section=.initrd "/boot/EFI/Linux/$kver.efi" "/boot/$kver/initramfs.img" + objcopy -O binary --only-section=.linux "/boot/EFI/Linux/$kver.efi" "/boot/$kver/vmlinuz" + fi +EOF + # Create the upgrade content (a simple marker file). # For UKI builds, we also remove the existing UKI so that seal-uki can # regenerate it with the correct composefs digest for this derived image. @@ -36,16 +48,26 @@ RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp --mount=type=secret,id=secureboot_key \ --mount=type=secret,id=secureboot_cert \ --mount=type=bind,from=packaging,src=/,target=/run/packaging \ + --mount=type=bind,from=kernel,src=/,target=/run/kernel \ --mount=type=bind,from=upgrade-base,src=/,target=/run/target <> /tmp/Containerfile.drop-lbis <<-EOF - FROM base as base-final - RUN rm -rf /boot/EFI/Linux/*.efi + allow_missing_verity=() - FROM base as sealed-uki - RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ - --mount=type=bind,from=base-final,src=/,target=/run/target \ - /usr/bin/seal-uki /run/target /out /run/secrets $allow_missing_verity $seal_state + if [[ "$(bootc status --json | jq -r '.status.booted.composefs.missingVerityAllowed')" == "true" ]]; then + allow_missing_verity=("--allow-missing-verity") + fi - FROM base-final + seal_state="unsealed" - # Copy the sealed UKI and finalize the image remove raw kernel, create symlinks - RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ - --mount=type=bind,from=sealed-uki,src=/,target=/run/sealed-uki \ - /usr/bin/finalize-uki /run/sealed-uki/out + cat >> /tmp/Containerfile.drop-lbis <<-EOF +FROM base as kernel +RUN <<-RUNEOF + kver=\$(bootc container inspect --rootfs / --json | jq -r '.kernel.version') + mkdir -p "/boot/\$kver" + objcopy -O binary --only-section=.initrd "/boot/EFI/Linux/\$kver.efi" "/boot/\$kver/initramfs.img" + objcopy -O binary --only-section=.linux "/boot/EFI/Linux/\$kver.efi" "/boot/\$kver/vmlinuz" +RUNEOF + +FROM base as base-final +RUN rm -rf /boot/EFI/Linux/*.efi + +FROM base as sealed-uki +RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ + --mount=type=bind,from=kernel,src=/,target=/run/kernel \ + --mount=type=bind,from=base-final,src=/,target=/run/target < Date: Fri, 15 May 2026 12:51:30 +0530 Subject: [PATCH 2/7] test/integration: Test vmlinuz non-existence with UKI vmlinuz and intrd should not be present in UKI images; add test for the same Signed-off-by: Pragyan Poudyal --- crates/tests-integration/src/container.rs | 27 +++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/crates/tests-integration/src/container.rs b/crates/tests-integration/src/container.rs index b03a69f58..14396107b 100644 --- a/crates/tests-integration/src/container.rs +++ b/crates/tests-integration/src/container.rs @@ -1,8 +1,10 @@ +use cap_std_ext::cap_std::ambient_authority; +use cap_std_ext::cap_std::fs::Dir; use indoc::indoc; use scopeguard::defer; use serde::Deserialize; -use std::fs; use std::process::Command; +use std::{fs, path::Path}; use anyhow::{Context, Result}; use camino::Utf8Path; @@ -51,8 +53,6 @@ pub(crate) fn test_bootc_container_inspect() -> Result<()> { .as_bool() .expect("kernel.unified should be a boolean"); - println!("kernel: {kernel:#?}"); - let is_uki = std::env::var("BOOTC_boot_type").is_ok_and(|var| var == "uki"); if let Some(variant) = std::env::var("BOOTC_variant").ok() { @@ -74,6 +74,24 @@ pub(crate) fn test_bootc_container_inspect() -> Result<()> { ); // Version should be non-empty after stripping extension assert!(!version.is_empty(), "version should not be empty for UKI"); + + let target_root = Dir::open_ambient_dir(TARGET, ambient_authority()) + .with_context(|| format!("{TARGET} not found"))?; + + // For UKI make sure vmlinuz + initrd don't exist + let usr_lib_mod = Path::new("usr/lib/modules").join(version); + assert!( + target_root.exists(&usr_lib_mod), + "'{usr_lib_mod:?}' does not exist" + ); + assert!( + !target_root.exists(usr_lib_mod.join("vmlinuz")), + "vmlinuz should not exist for UKI" + ); + assert!( + !target_root.exists(usr_lib_mod.join("initramfs.img")), + "initramfs should not exist for UKI" + ); } o => eprintln!("notice: Unhandled variant for kernel check: {o:?}"), } @@ -190,6 +208,8 @@ fn test_variant_base_crosscheck() -> Result<()> { Ok(()) } +const TARGET: &str = "/run/target"; + /// Verify exported tar has correct size/mode/content vs source. /// Checks all critical paths (kernel, boot) plus ~10% random sample. pub(crate) fn test_container_export_tar() -> Result<()> { @@ -197,7 +217,6 @@ pub(crate) fn test_container_export_tar() -> Result<()> { use std::io::Read; use std::os::unix::fs::MetadataExt; - const TARGET: &str = "/run/target"; const CRITICAL: &[&str] = &["usr/lib/modules/", "usr/lib/ostree-boot/", "boot/"]; anyhow::ensure!( From a5a2eb7c4ac9f13bdda9050e51f3be908186bc6f Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 2 Jun 2026 13:57:28 +0530 Subject: [PATCH 3/7] cli: Add cmd `bootc internals uki-extract` So we can just use bootc to extract the `.linux` and `.initrd` sections from the UKI and not have to use objcopy Signed-off-by: Pragyan Poudyal --- Dockerfile | 2 +- crates/lib/src/cli.rs | 53 ++++++++++++++++++- tmt/tests/Dockerfile.upgrade | 4 +- tmt/tests/booted/tap.nu | 4 +- .../test-install-to-filesystem-var-mount.sh | 4 +- 5 files changed, 56 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6a77f0aec..df6a74ca3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -83,7 +83,7 @@ RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ # Install systemd-ukify and systemd-boot for UKIs # This also installs systemd-boot for the grub UKI case which is not ideal... if [[ "${boot_type}" == "uki" ]]; then - pkgs_to_install+=(systemd-ukify binutils) + pkgs_to_install+=(systemd-ukify) fi if [[ ${#pkgs_to_install[@]} -gt 0 ]]; then diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 9a7c307ff..2a9159b49 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -4,7 +4,7 @@ use std::ffi::{CString, OsStr, OsString}; use std::fs::File; -use std::io::{BufWriter, Seek}; +use std::io::{BufWriter, Seek, SeekFrom}; use std::os::fd::AsFd; use std::os::unix::process::CommandExt; use std::process::Command; @@ -27,6 +27,7 @@ use composefs_boot::BootOps as _; use etc_merge::{compute_diff, print_diff}; use fn_error_context::context; use indoc::indoc; +use ocidir::cap_std::ambient_authority; use ostree::gio; use ostree_container::store::PrepareResult; use ostree_ext::container as ostree_container; @@ -626,6 +627,19 @@ pub(crate) enum FsverityOpts { }, } +#[derive(Debug, clap::Subcommand, PartialEq, Eq)] +pub(crate) enum UkiSubcommands { + /// Extract kernel + initrd from a UKI + /// The output (vmlinuz + initramfs.img) is placed in a directory named + /// after the kernel version found in the UKI + Extract { + /// The path to the UKI PE + path: Utf8PathBuf, + /// The output path + output_path: Utf8PathBuf, + }, +} + /// Hidden, internal only options #[derive(Debug, clap::Subcommand, PartialEq, Eq)] pub(crate) enum InternalsOpts { @@ -742,6 +756,9 @@ pub(crate) enum InternalsOpts { /// Block device inspection tools. #[clap(subcommand)] Blockdev(BlockdevOpts), + /// For various UKI operations + #[clap(subcommand)] + Uki(UkiSubcommands), } /// Subcommands for `bootc internals blockdev`. @@ -2319,6 +2336,40 @@ async fn run_from_opt(opt: Opt) -> Result<()> { println!(); Ok(()) } + InternalsOpts::Uki(uki_opts) => match uki_opts { + UkiSubcommands::Extract { path, output_path } => { + let mut uki_file = + std::fs::File::open(&path).with_context(|| format!("Opening {path}"))?; + + let uname = + composefs_boot::uki::get_text_section_buffered(&mut uki_file, ".uname") + .context("Getting uname")?; + + std::fs::create_dir_all(&output_path).context("Creating output directory")?; + + let output_dir = Dir::open_ambient_dir(&output_path, ambient_authority()) + .context("Opening output dir")?; + output_dir.create_dir(&uname)?; + + let output_dir = output_dir.open_dir(&uname)?; + + for (section_name, file_name) in + [(".linux", "vmlinuz"), (".initrd", "initramfs.img")] + { + uki_file + .seek(SeekFrom::Start(0)) + .context("Seeking to start")?; + let section = + composefs_boot::uki::get_section_buffered(&mut uki_file, section_name) + .with_context(|| format!("Getting {section_name} section"))?; + output_dir + .write(file_name, section) + .with_context(|| format!("Writing {file_name}"))?; + } + + Ok(()) + } + }, }, Opt::State(opts) => match opts { StateOpts::WipeOstree => { diff --git a/tmt/tests/Dockerfile.upgrade b/tmt/tests/Dockerfile.upgrade index 3b1ab73c3..b66393114 100644 --- a/tmt/tests/Dockerfile.upgrade +++ b/tmt/tests/Dockerfile.upgrade @@ -19,9 +19,7 @@ ARG boot_type RUN <<-EOF if test "${boot_type}" = "uki"; then kver=$(bootc container inspect --rootfs / --json | jq -r '.kernel.version') - mkdir -p "/boot/$kver" - objcopy -O binary --only-section=.initrd "/boot/EFI/Linux/$kver.efi" "/boot/$kver/initramfs.img" - objcopy -O binary --only-section=.linux "/boot/EFI/Linux/$kver.efi" "/boot/$kver/vmlinuz" + bootc internals uki extract /boot/EFI/Linux/$kver.efi /boot fi EOF diff --git a/tmt/tests/booted/tap.nu b/tmt/tests/booted/tap.nu index f571bfff4..c25a89479 100644 --- a/tmt/tests/booted/tap.nu +++ b/tmt/tests/booted/tap.nu @@ -102,9 +102,7 @@ export def make_uki_containerfile [containerfile: string] { FROM base as kernel RUN <<-EOF kver=$\(bootc container inspect --rootfs / --json | jq -r '.kernel.version'\) - mkdir -p /boot/$kver - objcopy -O binary --only-section=.initrd /boot/EFI/Linux/$kver.efi /boot/$kver/initramfs.img - objcopy -O binary --only-section=.linux /boot/EFI/Linux/$kver.efi /boot/$kver/vmlinuz + bootc internals uki extract /boot/EFI/Linux/$kver.efi /boot EOF FROM base as base-final 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 c29b4154d..84e09da8e 100644 --- a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh +++ b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh @@ -46,9 +46,7 @@ if [[ $is_composefs != "null" && $boot_type == "uki" ]]; then FROM base as kernel RUN <<-RUNEOF kver=\$(bootc container inspect --rootfs / --json | jq -r '.kernel.version') - mkdir -p "/boot/\$kver" - objcopy -O binary --only-section=.initrd "/boot/EFI/Linux/\$kver.efi" "/boot/\$kver/initramfs.img" - objcopy -O binary --only-section=.linux "/boot/EFI/Linux/\$kver.efi" "/boot/\$kver/vmlinuz" + bootc internals uki extract /boot/EFI/Linux/\$kver.efi /boot RUNEOF FROM base as base-final From e8d7a3673da727ba7b94ba92c0cf593b3532ca3f Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 16 Jun 2026 10:10:14 +0530 Subject: [PATCH 4/7] cli: Add cmd `bootc container split-kernel-and-rootfs` This command is equivalent to `mv /target-root/usr/lib/modules/$kver/{vmlinuz,initramfs.img} /out/$kver` We could just use `mv`, but having an actual bootc cmd is cleaner Signed-off-by: Pragyan Poudyal --- crates/lib/src/cli.rs | 52 +++++++++++++++++++ ...otc-container-split-kernel-and-rootfs.8.md | 38 ++++++++++++++ docs/src/man/bootc-container.8.md | 1 + 3 files changed, 91 insertions(+) create mode 100644 docs/src/man/bootc-container-split-kernel-and-rootfs.8.md diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 2a9159b49..06845b9e6 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -416,6 +416,23 @@ pub(crate) enum ContainerOpts { /// Identifier for image; if not provided, the running image will be used. image: Option, }, + /// Split kernel and rootfs from a container image + /// + /// This command extracts the kernel (vmlinuz and initramfs.img) from the + /// container rootfs and moves them to a separate output directory, organized + /// by kernel version + /// + /// Example: + /// bootc container split-kernel-rootfs --rootfs /target-rootfs --output /out + SplitKernelAndRootfs { + /// Operate on the provided rootfs + #[clap(long, default_value = "/")] + rootfs: Utf8PathBuf, + + /// Output directory for the extracted kernel files + #[clap(long)] + output: Utf8PathBuf, + }, /// Build a Unified Kernel Image (UKI) using ukify. /// /// This command computes the necessary arguments from the container image @@ -1854,6 +1871,41 @@ async fn run_from_opt(opt: Opt) -> Result<()> { )?; Ok(()) } + ContainerOpts::SplitKernelAndRootfs { rootfs, output } => { + use crate::kernel::{KernelType, find_kernel}; + + let root = Dir::open_ambient_dir(&rootfs, ambient_authority())?; + + let kernel_internal = find_kernel(&root)? + .ok_or_else(|| anyhow::anyhow!("No kernel found in rootfs"))?; + + if kernel_internal.kernel.unified { + anyhow::bail!("UKIs are not supported"); + } + + let kver = &kernel_internal.kernel.version; + let kernel_output_dir = output.join(kver); + std::fs::create_dir_all(&kernel_output_dir)?; + + match &kernel_internal.k_type { + KernelType::Vmlinuz { path, initramfs } => { + let vmlinuz_src = rootfs.join(path); + let initramfs_src = rootfs.join(initramfs); + let vmlinuz_dst = kernel_output_dir.join("vmlinuz"); + let initramfs_dst = kernel_output_dir.join("initramfs.img"); + + std::fs::rename(&vmlinuz_src, &vmlinuz_dst).context("Moving vmlinuz")?; + std::fs::rename(&initramfs_src, &initramfs_dst) + .context("Moving initramfs")?; + } + + KernelType::Uki { .. } => { + unreachable!("UKIs should have been rejected above"); + } + } + + Ok(()) + } ContainerOpts::ComputeComposefsDigest { path, write_dumpfile_to, diff --git a/docs/src/man/bootc-container-split-kernel-and-rootfs.8.md b/docs/src/man/bootc-container-split-kernel-and-rootfs.8.md new file mode 100644 index 000000000..61c2a85f5 --- /dev/null +++ b/docs/src/man/bootc-container-split-kernel-and-rootfs.8.md @@ -0,0 +1,38 @@ +# NAME + +bootc-container-split-kernel-and-rootfs - Split kernel and rootfs from a container image + +# SYNOPSIS + +bootc container split-kernel-and-rootfs + +# DESCRIPTION + +Split kernel and rootfs from a container image + +# OPTIONS + + +**--rootfs**=*ROOTFS* + + Operate on the provided rootfs + + Default: / + +**--output**=*OUTPUT* + + Output directory for the extracted kernel files + + + +# EXAMPLES + +TODO: Add practical examples showing how to use this command. + +# SEE ALSO + +**bootc**(8) + +# VERSION + + diff --git a/docs/src/man/bootc-container.8.md b/docs/src/man/bootc-container.8.md index b8b540972..1b1fce62f 100644 --- a/docs/src/man/bootc-container.8.md +++ b/docs/src/man/bootc-container.8.md @@ -21,6 +21,7 @@ Operations which can be executed as part of a container build |---------|-------------| | **bootc container inspect** | Output information about the container image | | **bootc container lint** | Perform relatively inexpensive static analysis checks as part of a container build | +| **bootc container split-kernel-and-rootfs** | Split kernel and rootfs from a container image | | **bootc container ukify** | Build a Unified Kernel Image (UKI) using ukify | From 1fb9551c2f817e4d335d308fb91ed5428e256f92 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 16 Jun 2026 10:14:01 +0530 Subject: [PATCH 5/7] dockerfile: Changes for UKI variant Since we do not want kernel + initrd in the final UKI dockerfile, we now build the initrd inside the `target-rootfs` generated by `bootc-base-imagectl` by chrooting into it and installing the required packages. After that's done we split the rootfs and vmlinuz + initrd into /target-rootfs and /kernel/$kver respectively Signed-off-by: Pragyan Poudyal --- Dockerfile | 133 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 89 insertions(+), 44 deletions(-) diff --git a/Dockerfile b/Dockerfile index df6a74ca3..2c19d032f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,12 +46,93 @@ RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp --mount=type=c # local sources. We'll override it later. # NOTE: All your base belong to me. FROM $base as target-base + +# SKIP_CONFIGS=1 skips LBIs, test kargs, and install configs (for FCOS testing) +ARG SKIP_CONFIGS +ARG boot_type +ARG seal_state +ARG variant +ARG baseconfigs="" +ARG rootfs="" + # 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=tmpfs,target=/tmp \ --mount=type=bind,from=packaging,src=/,target=/run/packaging \ /run/packaging/enable-compose-repos -RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp /usr/libexec/bootc-base-imagectl build-rootfs --manifest=standard /target-rootfs + +RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ + --mount=type=bind,from=src,src=/src/hack,target=/run/hack \ + --mount=type=bind,from=packaging,src=/,target=/run/packaging < Date: Tue, 16 Jun 2026 10:58:06 +0530 Subject: [PATCH 6/7] justfile: Build packages as deps for stage fetch Now since we need to build our initramfs before the `fetch` build stage, we need packages built first as we need `bootc` and `bootc-initramfs-setup` binaries Signed-off-by: Pragyan Poudyal --- Justfile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Justfile b/Justfile index 6ba80d0c7..3c16ea1aa 100644 --- a/Justfile +++ b/Justfile @@ -102,7 +102,7 @@ build: package _keygen && _pull-lbi-images # The retry parameters can be overridden via environment variables: # BOOTC_CI_RETRIES=10 BOOTC_CI_DELAY=60 just build-fetch [group('core')] -build-fetch: _keygen +build-fetch: _keygen package #!/bin/bash set -euo pipefail retries=${BOOTC_CI_RETRIES:-3} @@ -131,12 +131,15 @@ build-fetch: _keygen for img in {{lbi_images}}; do retry podman pull -q "$img" done + + pkg_path=$(realpath target/packages) + # Build the network-heavy fetch stage of the main image. If this # succeeds, `just build` will get a cache hit on the fetch layer and # run entirely offline. # Note: buildargs (not base_buildargs) is needed here because the # target-base stage requires --cap-add/--security-opt for bwrap. - retry podman build {{_nocache_arg}} --target=fetch {{buildargs}} . + retry podman build {{_nocache_arg}} --build-context "packages=${pkg_path}" --target=fetch {{buildargs}} . # Same for the upgrade-source image used by test-upgrade. retry podman build {{_nocache_arg}} --build-arg=base={{base}} \ --target=fetch -f tmt/tests/Dockerfile.upgrade-source . From e2b3c1db519bd08dfa8dbfb01e3cf274e7bd5e5b Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 17 Jun 2026 16:52:48 +0530 Subject: [PATCH 7/7] Only perform non-network operations inside chroot To avoid complexity with setting up networking inside `/target-rootfs` for when we chroot and install stuff, now we make sure to only have operations that do not require networking inside the chrooted env Separate out `downgrade-kernel.sh` script and accept a rootfs parameter so we can run this from outside target-rootfs but still operate on the target-rootfs Signed-off-by: Pragyan Poudyal --- Dockerfile | 28 ++++++++-------------------- hack/downgrade-kernel.sh | 34 ++++++++++++++++++++++++++++++++++ hack/provision-fetch.sh | 27 --------------------------- 3 files changed, 42 insertions(+), 47 deletions(-) create mode 100755 hack/downgrade-kernel.sh diff --git a/Dockerfile b/Dockerfile index 2c19d032f..6b6a498a4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,6 @@ RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp --mount=type=c FROM $base as target-base # SKIP_CONFIGS=1 skips LBIs, test kargs, and install configs (for FCOS testing) -ARG SKIP_CONFIGS ARG boot_type ARG seal_state ARG variant @@ -61,36 +60,24 @@ RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ --mount=type=bind,from=packaging,src=/,target=/run/packaging \ /run/packaging/enable-compose-repos -RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ - --mount=type=bind,from=src,src=/src/hack,target=/run/hack \ - --mount=type=bind,from=packaging,src=/,target=/run/packaging <= 7.2 +kernel_ver=$(rpm --root "$rootfs" -q --qf '%{VERSION}' kernel 2>/dev/null || true) +case "${kernel_ver}" in + 7.0.*|7.1.*) + arch=$(uname -m) + koji_kver="6.19.10" + koji_krel="300.fc44" + koji_base="https://kojipkgs.fedoraproject.org/packages/kernel/${koji_kver}/${koji_krel}/${arch}" + kernel_td=$(mktemp -d) + trap 'rm -rf "${kernel_td}"' EXIT + for pkg in kernel kernel-core kernel-modules kernel-modules-core; do + curl --retry 5 --retry-delay 5 --retry-all-errors -fL \ + "${koji_base}/${pkg}-${koji_kver}-${koji_krel}.${arch}.rpm" \ + -o "${kernel_td}/${pkg}.rpm" + done + # TMPDIR=/var/tmp: works around an rpm-ostree bug + TMPDIR=/var/tmp dnf --installroot="$rootfs" -y downgrade "${kernel_td}"/*.rpm + # Note: we should also fix the Fedora kernel packaging to not copy symvers into /boot + rm -rf "${rootfs}"/boot/* + rm -rf "${kernel_td}" + trap - EXIT + ;; +esac + +dnf clean all +# Clean logs and caches +rm "$rootfs"/var/log/* "$rootfs"/var/cache "$rootfs"/var/lib/{dnf,rpm-state,rhsm} -rf diff --git a/hack/provision-fetch.sh b/hack/provision-fetch.sh index 4eae43b47..ef2ee9e73 100755 --- a/hack/provision-fetch.sh +++ b/hack/provision-fetch.sh @@ -86,33 +86,6 @@ if ! rpm -q ostree 2>/dev/null | grep -q "2026\." ; then esac fi -# Temporary: downgrade kernel to last 6.x when 7.0 or 7.1 is present. -# Kernel 7.x broke composefs ("has no fs-verity digest"), fixed in 7.2. -# xref https://github.com/bootc-dev/bootc/issues/2174 -# TODO: Remove once all base images ship kernel >= 7.2 -kernel_ver=$(rpm -q --qf '%{VERSION}' kernel 2>/dev/null || true) -case "${kernel_ver}" in - 7.0.*|7.1.*) - arch=$(uname -m) - koji_kver="6.19.10" - koji_krel="300.fc44" - koji_base="https://kojipkgs.fedoraproject.org/packages/kernel/${koji_kver}/${koji_krel}/${arch}" - kernel_td=$(mktemp -d) - trap 'rm -rf "${kernel_td}"' EXIT - for pkg in kernel kernel-core kernel-modules kernel-modules-core; do - curl --retry 5 --retry-delay 5 --retry-all-errors -fL \ - "${koji_base}/${pkg}-${koji_kver}-${koji_krel}.${arch}.rpm" \ - -o "${kernel_td}/${pkg}.rpm" - done - # TMPDIR=/var/tmp: works around an rpm-ostree bug - TMPDIR=/var/tmp dnf -y downgrade "${kernel_td}"/*.rpm - # Note: we should also fix the Fedora kernel packaging to not copy symvers into /boot - rm -rf /boot/* - rm -rf "${kernel_td}" - trap - EXIT - ;; -esac - dnf clean all # Clean logs and caches rm /var/log/* /var/cache /var/lib/{dnf,rpm-state,rhsm} -rf