Skip to main content

bootc_lib/
ukify.rs

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
6use std::ffi::OsString;
7use std::process::Command;
8
9use anyhow::{Context, Result};
10use bootc_kernel_cmdline::utf8::Cmdline;
11use bootc_utils::CommandRunExt;
12use camino::Utf8Path;
13use cap_std_ext::cap_std::fs::Dir;
14use fn_error_context::context;
15
16use crate::bootc_composefs::digest::compute_composefs_digest;
17use crate::bootc_composefs::status::ComposefsCmdline;
18
19/// Build a UKI from the given rootfs.
20///
21/// This function:
22/// 1. Verifies that ukify is available
23/// 2. Finds the kernel in the rootfs
24/// 3. Computes the composefs digest
25/// 4. Reads kernel arguments from kargs.d
26/// 5. Appends any additional kargs provided via --karg
27/// 6. Invokes ukify with computed arguments plus any pass-through args
28#[context("Building UKI")]
29pub(crate) async fn build_ukify(
30    rootfs: &Utf8Path,
31    extra_kargs: &[String],
32    args: &[OsString],
33    allow_missing_fsverity: bool,
34    write_dumpfile_to: Option<&Utf8Path>,
35) -> Result<()> {
36    // Warn if --karg is used (temporary workaround)
37    if !extra_kargs.is_empty() {
38        tracing::warn!(
39            "The --karg flag is temporary and will be removed as soon as possible \
40            (https://github.com/bootc-dev/bootc/issues/1826)"
41        );
42    }
43
44    // Verify ukify is available
45    if !crate::utils::have_executable("ukify")? {
46        anyhow::bail!(
47            "ukify executable not found in PATH. Please install systemd-ukify or equivalent."
48        );
49    }
50
51    // Open the rootfs directory
52    let root = Dir::open_ambient_dir(rootfs, cap_std_ext::cap_std::ambient_authority())
53        .with_context(|| format!("Opening rootfs {rootfs}"))?;
54
55    // Find the kernel
56    let kernel = crate::kernel::find_kernel(&root)?
57        .ok_or_else(|| anyhow::anyhow!("No kernel found in {rootfs}"))?;
58
59    // Extract vmlinuz and initramfs paths, or bail if this is already a UKI
60    let (vmlinuz_path, initramfs_path) = match kernel.k_type {
61        crate::kernel::KernelType::Vmlinuz { path, initramfs } => (path, initramfs),
62        crate::kernel::KernelType::Uki { path, .. } => {
63            anyhow::bail!("Cannot build UKI: rootfs already contains a UKI at {path}");
64        }
65    };
66
67    // Verify kernel and initramfs exist
68    if !root
69        .try_exists(&vmlinuz_path)
70        .context("Checking for vmlinuz")?
71    {
72        anyhow::bail!("Kernel not found at {vmlinuz_path}");
73    }
74    if !root
75        .try_exists(&initramfs_path)
76        .context("Checking for initramfs")?
77    {
78        anyhow::bail!("Initramfs not found at {initramfs_path}");
79    }
80
81    // Compute the composefs digest
82    let composefs_digest = compute_composefs_digest(rootfs, write_dumpfile_to).await?;
83
84    // Get kernel arguments from kargs.d
85    let mut cmdline = crate::bootc_kargs::get_kargs_in_root(&root, std::env::consts::ARCH)?;
86
87    // Add the composefs digest
88    cmdline.extend(&Cmdline::from(
89        ComposefsCmdline::build(&composefs_digest, allow_missing_fsverity).to_string(),
90    ));
91
92    // Add any extra kargs provided via --karg
93    for karg in extra_kargs {
94        cmdline.extend(&Cmdline::from(karg));
95    }
96
97    let cmdline_str = cmdline.to_string();
98
99    // Build the ukify command with cwd set to rootfs so paths can be relative
100    let mut cmd = Command::new("ukify");
101    cmd.current_dir(rootfs);
102    cmd.arg("build")
103        .arg("--linux")
104        .arg(&vmlinuz_path)
105        .arg("--initrd")
106        .arg(&initramfs_path)
107        .arg("--uname")
108        .arg(&kernel.kernel.version)
109        .arg("--cmdline")
110        .arg(&cmdline_str)
111        .arg("--os-release")
112        .arg("@usr/lib/os-release");
113
114    // Add pass-through arguments
115    cmd.args(args);
116
117    tracing::debug!("Executing ukify: {:?}", cmd);
118
119    // Run ukify
120    cmd.run_inherited().context("Running ukify")?;
121
122    Ok(())
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use std::fs;
129
130    #[tokio::test]
131    async fn test_build_ukify_no_kernel() {
132        let tempdir = tempfile::tempdir().unwrap();
133        let path = Utf8Path::from_path(tempdir.path()).unwrap();
134
135        let result = build_ukify(path, &[], &[], false, None).await;
136        assert!(result.is_err());
137        let err = format!("{:#}", result.unwrap_err());
138        assert!(
139            err.contains("No kernel found") || err.contains("ukify executable not found"),
140            "Unexpected error message: {err}"
141        );
142    }
143
144    #[tokio::test]
145    async fn test_build_ukify_already_uki() {
146        let tempdir = tempfile::tempdir().unwrap();
147        let path = Utf8Path::from_path(tempdir.path()).unwrap();
148
149        // Create a UKI structure
150        fs::create_dir_all(tempdir.path().join("boot/EFI/Linux")).unwrap();
151        fs::write(tempdir.path().join("boot/EFI/Linux/test.efi"), b"fake uki").unwrap();
152
153        let result = build_ukify(path, &[], &[], false, None).await;
154        assert!(result.is_err());
155        let err = format!("{:#}", result.unwrap_err());
156        assert!(
157            err.contains("already contains a UKI") || err.contains("ukify executable not found"),
158            "Unexpected error message: {err}"
159        );
160    }
161}