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