Skip to content

Commit e437c63

Browse files
committed
feat: Add bootc container ukify command
Add a new subcommand that builds a Unified Kernel Image (UKI) by computing the necessary arguments from a container image and invoking ukify. This simplifies the sealed image build workflow by having bootc internally compute: - The composefs digest (via existing compute-composefs-digest logic) - Kernel arguments from /usr/lib/bootc/kargs.d/*.toml files - Paths to kernel, initrd, and os-release Any additional arguments are passed through to ukify unchanged, allowing full control over signing, output paths, and other ukify options. The seal-uki script is updated to use this new command instead of manually computing these values and invoking ukify directly. Also adds kargs.d configuration files for the sealed UKI workflow: - 10-rootfs-rw.toml: Mount root filesystem read-write - 20-selinux-permissive.toml: SELinux permissive workaround (#1826) - 21-console-hvc0.toml: Console configuration for QEMU/virtio Closes: #1955 Assisted-by: OpenCode (Opus 4.5) Signed-off-by: John Eckersberg <jeckersb@redhat.com>
1 parent 5c52b25 commit e437c63

10 files changed

Lines changed: 223 additions & 17 deletions

File tree

contrib/packaging/seal-uki

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,7 @@ shift
1212
secrets=$1
1313
shift
1414

15-
# Compute the composefs digest from the target rootfs
16-
composefs_digest=$(bootc container compute-composefs-digest "${target}")
17-
18-
# Build the kernel command line
19-
# enforcing=0: https://github.com/bootc-dev/bootc/issues/1826
20-
# TODO: pick up kargs from /usr/lib/bootc/kargs.d
21-
cmdline="composefs=${composefs_digest} console=ttyS0,115200n8 console=hvc0 enforcing=0 rw"
22-
23-
# Find the kernel version
15+
# Find the kernel version (needed for output filename)
2416
kver=$(bootc container inspect --rootfs "${target}" --json | jq -r '.kernel.version')
2517
if [ -z "$kver" ] || [ "$kver" = "null" ]; then
2618
echo "Error: No kernel found" >&2
@@ -29,12 +21,9 @@ fi
2921

3022
mkdir -p "${output}"
3123

32-
ukify build \
33-
--linux "${target}/usr/lib/modules/${kver}/vmlinuz" \
34-
--initrd "${target}/usr/lib/modules/${kver}/initramfs.img" \
35-
--uname="${kver}" \
36-
--cmdline "${cmdline}" \
37-
--os-release "@${target}/usr/lib/os-release" \
24+
# Build the UKI using bootc container ukify
25+
# This computes the composefs digest, reads kargs from kargs.d, and invokes ukify
26+
bootc container ukify --rootfs "${target}" \
3827
--signtool sbsign \
3928
--secureboot-private-key "${secrets}/secureboot_key" \
4029
--secureboot-certificate "${secrets}/secureboot_cert" \
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Mount the root filesystem read-write
2+
kargs = ["rw"]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# WORKAROUND: SELinux must be permissive for sealed UKI boot
2+
# See https://github.com/bootc-dev/bootc/issues/1826
3+
kargs = ["enforcing=0"]
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
# https://bugzilla.redhat.com/show_bug.cgi?id=2353887
2-
kargs = ["console=hvc0"]
2+
# console=ttyS0 for QEMU serial, console=hvc0 for virtio/Xen console
3+
kargs = ["console=ttyS0,115200n8", "console=hvc0"]

crates/lib/src/cli.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,23 @@ pub(crate) enum ContainerOpts {
392392
/// Identifier for image; if not provided, the running image will be used.
393393
image: Option<String>,
394394
},
395+
/// Build a Unified Kernel Image (UKI) using ukify.
396+
///
397+
/// This command computes the necessary arguments from the container image
398+
/// (kernel, initrd, cmdline, os-release) and invokes ukify with them.
399+
/// Any additional arguments are passed through to ukify unchanged.
400+
///
401+
/// Example:
402+
/// bootc container ukify --rootfs /target --output /output/uki.efi
403+
Ukify {
404+
/// Operate on the provided rootfs.
405+
#[clap(long, default_value = "/")]
406+
rootfs: Utf8PathBuf,
407+
408+
/// Additional arguments to pass to ukify.
409+
#[clap(trailing_var_arg = true, allow_hyphen_values = true)]
410+
args: Vec<OsString>,
411+
},
395412
}
396413

397414
/// Subcommands which operate on images.
@@ -1598,6 +1615,7 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
15981615

15991616
Ok(())
16001617
}
1618+
ContainerOpts::Ukify { rootfs, args } => crate::ukify::build_ukify(&rootfs, &args),
16011619
},
16021620
Opt::Completion { shell } => {
16031621
use clap_complete::aot::generate;

crates/lib/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ pub mod spec;
9393
mod status;
9494
mod store;
9595
mod task;
96+
mod ukify;
9697
mod utils;
9798

9899
#[cfg(feature = "docgen")]

crates/lib/src/ukify.rs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
//! Build Unified Kernel Images (UKI) using ukify.
2+
//!
3+
//! This module provides functionality to build UKIs by computing the necessary
4+
//! arguments from a container image and invoking the ukify tool.
5+
6+
use std::ffi::OsString;
7+
use std::process::Command;
8+
9+
use anyhow::{Context, Result};
10+
use bootc_utils::CommandRunExt;
11+
use camino::Utf8Path;
12+
use cap_std_ext::cap_std::fs::Dir;
13+
use fn_error_context::context;
14+
15+
use crate::bootc_composefs::digest::compute_composefs_digest;
16+
17+
/// Build a UKI from the given rootfs.
18+
///
19+
/// This function:
20+
/// 1. Verifies that ukify is available
21+
/// 2. Finds the kernel in the rootfs
22+
/// 3. Computes the composefs digest
23+
/// 4. Reads kernel arguments from kargs.d
24+
/// 5. Invokes ukify with computed arguments plus any pass-through args
25+
#[context("Building UKI")]
26+
pub(crate) fn build_ukify(rootfs: &Utf8Path, args: &[OsString]) -> Result<()> {
27+
// Verify ukify is available
28+
if !crate::utils::have_executable("ukify")? {
29+
anyhow::bail!(
30+
"ukify executable not found in PATH. Please install systemd-ukify or equivalent."
31+
);
32+
}
33+
34+
// Open the rootfs directory
35+
let root = Dir::open_ambient_dir(rootfs, cap_std_ext::cap_std::ambient_authority())
36+
.with_context(|| format!("Opening rootfs {rootfs}"))?;
37+
38+
// Find the kernel
39+
let kernel = crate::kernel::find_kernel(&root)?
40+
.ok_or_else(|| anyhow::anyhow!("No kernel found in {rootfs}"))?;
41+
42+
// We can only build a UKI from a traditional kernel, not from an existing UKI
43+
if kernel.unified {
44+
anyhow::bail!(
45+
"Cannot build UKI: rootfs already contains a UKI at boot/EFI/Linux/{}.efi",
46+
kernel.version
47+
);
48+
}
49+
50+
let kver = &kernel.version;
51+
52+
// Compute paths for kernel and initrd
53+
let modules_dir = format!("usr/lib/modules/{kver}");
54+
let vmlinuz_path = rootfs.join(&modules_dir).join("vmlinuz");
55+
let initramfs_path = rootfs.join(&modules_dir).join("initramfs.img");
56+
57+
// Verify kernel and initramfs exist
58+
if !root
59+
.try_exists(format!("{modules_dir}/vmlinuz"))
60+
.context("Checking for vmlinuz")?
61+
{
62+
anyhow::bail!("Kernel not found at {vmlinuz_path}");
63+
}
64+
if !root
65+
.try_exists(format!("{modules_dir}/initramfs.img"))
66+
.context("Checking for initramfs")?
67+
{
68+
anyhow::bail!("Initramfs not found at {initramfs_path}");
69+
}
70+
71+
// Compute the composefs digest
72+
let composefs_digest = compute_composefs_digest(rootfs, None)?;
73+
74+
// Get kernel arguments from kargs.d
75+
let kargs = crate::bootc_kargs::get_kargs_in_root(&root, std::env::consts::ARCH)?;
76+
let kargs_str: Vec<&str> = kargs.iter_str().collect();
77+
78+
// Build the cmdline: composefs digest + kargs from kargs.d
79+
let cmdline = if kargs_str.is_empty() {
80+
format!("composefs={composefs_digest}")
81+
} else {
82+
format!("composefs={composefs_digest} {}", kargs_str.join(" "))
83+
};
84+
85+
// Path to os-release (ukify expects @path format for reading from file)
86+
let os_release_path = rootfs.join("usr/lib/os-release");
87+
let os_release_arg = format!("@{os_release_path}");
88+
89+
// Build the ukify command
90+
let mut cmd = Command::new("ukify");
91+
cmd.arg("build")
92+
.arg("--linux")
93+
.arg(&vmlinuz_path)
94+
.arg("--initrd")
95+
.arg(&initramfs_path)
96+
.arg("--uname")
97+
.arg(kver)
98+
.arg("--cmdline")
99+
.arg(&cmdline)
100+
.arg("--os-release")
101+
.arg(&os_release_arg);
102+
103+
// Add pass-through arguments
104+
cmd.args(args);
105+
106+
tracing::debug!("Executing ukify: {:?}", cmd);
107+
108+
// Run ukify
109+
cmd.run_inherited().context("Running ukify")?;
110+
111+
Ok(())
112+
}
113+
114+
#[cfg(test)]
115+
mod tests {
116+
use super::*;
117+
use std::fs;
118+
119+
#[test]
120+
fn test_build_ukify_no_kernel() {
121+
let tempdir = tempfile::tempdir().unwrap();
122+
let path = Utf8Path::from_path(tempdir.path()).unwrap();
123+
124+
let result = build_ukify(path, &[]);
125+
assert!(result.is_err());
126+
let err = format!("{:#}", result.unwrap_err());
127+
assert!(
128+
err.contains("No kernel found") || err.contains("ukify executable not found"),
129+
"Unexpected error message: {err}"
130+
);
131+
}
132+
133+
#[test]
134+
fn test_build_ukify_already_uki() {
135+
let tempdir = tempfile::tempdir().unwrap();
136+
let path = Utf8Path::from_path(tempdir.path()).unwrap();
137+
138+
// Create a UKI structure
139+
fs::create_dir_all(tempdir.path().join("boot/EFI/Linux")).unwrap();
140+
fs::write(tempdir.path().join("boot/EFI/Linux/test.efi"), b"fake uki").unwrap();
141+
142+
let result = build_ukify(path, &[]);
143+
assert!(result.is_err());
144+
let err = format!("{:#}", result.unwrap_err());
145+
assert!(
146+
err.contains("already contains a UKI") || err.contains("ukify executable not found"),
147+
"Unexpected error message: {err}"
148+
);
149+
}
150+
}

crates/lib/src/utils.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ pub(crate) fn find_mount_option<'a>(
6767
.next()
6868
}
6969

70-
#[allow(dead_code)]
7170
pub fn have_executable(name: &str) -> Result<bool> {
7271
let Some(path) = std::env::var_os("PATH") else {
7372
return Ok(false);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# NAME
2+
3+
bootc-container-ukify - Build a Unified Kernel Image (UKI) using ukify
4+
5+
# SYNOPSIS
6+
7+
bootc container ukify
8+
9+
# DESCRIPTION
10+
11+
Build a Unified Kernel Image (UKI) using ukify
12+
13+
This command computes the necessary arguments from the container image
14+
(kernel, initrd, cmdline, os-release) and invokes ukify with them.
15+
Any additional arguments are passed through to ukify unchanged.
16+
17+
# OPTIONS
18+
19+
<!-- BEGIN GENERATED OPTIONS -->
20+
**ARGS**
21+
22+
Additional arguments to pass to ukify
23+
24+
**--rootfs**=*ROOTFS*
25+
26+
Operate on the provided rootfs
27+
28+
Default: /
29+
30+
<!-- END GENERATED OPTIONS -->
31+
32+
# EXAMPLES
33+
34+
bootc container ukify --rootfs /target --output /output/uki.efi
35+
36+
# SEE ALSO
37+
38+
**bootc**(8)
39+
40+
# VERSION
41+
42+
<!-- VERSION PLACEHOLDER -->

docs/src/man/bootc-container.8.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Operations which can be executed as part of a container build
2121
|---------|-------------|
2222
| **bootc container inspect** | Output information about the container image |
2323
| **bootc container lint** | Perform relatively inexpensive static analysis checks as part of a container build |
24+
| **bootc container ukify** | Build a Unified Kernel Image (UKI) using ukify |
2425

2526
<!-- END GENERATED SUBCOMMANDS -->
2627

0 commit comments

Comments
 (0)