1use std::path::Path;
8
9use anyhow::{Context, Result};
10use bootc_kernel_cmdline::utf8::Cmdline;
11use camino::Utf8PathBuf;
12use cap_std_ext::cap_std::fs::Dir;
13use cap_std_ext::dirext::CapStdExtDirExt;
14use composefs_ctl::composefs_boot;
15use serde::Serialize;
16
17use crate::bootc_composefs::boot::EFI_LINUX;
18
19#[derive(Debug, Serialize)]
21#[serde(rename_all = "kebab-case")]
22pub(crate) struct Kernel {
23 pub(crate) version: String,
27 pub(crate) unified: bool,
29}
30
31pub(crate) enum KernelType {
37 Uki {
38 path: Utf8PathBuf,
39 cmdline: Option<Cmdline<'static>>,
42 },
43 Vmlinuz {
44 path: Utf8PathBuf,
45 initramfs: Utf8PathBuf,
46 },
47}
48
49pub(crate) struct KernelInternal {
56 pub(crate) kernel: Kernel,
57 pub(crate) k_type: KernelType,
58}
59
60impl From<KernelInternal> for Kernel {
61 fn from(kernel_internal: KernelInternal) -> Self {
62 kernel_internal.kernel
63 }
64}
65
66pub(crate) fn find_kernel(root: &Dir) -> Result<Option<KernelInternal>> {
74 if let Some(uki_path) = find_uki_path(root)? {
76 let version = uki_path.file_stem().unwrap_or(uki_path.as_str()).to_owned();
77
78 let mut uki = root.open(&uki_path).context("Opening UKI")?;
79
80 let cmdline = composefs_boot::uki::get_section_buffered(&mut uki, ".cmdline");
82
83 let cmdline = match cmdline {
84 Ok(cmdline) => {
85 let cmdline_str = std::str::from_utf8(&cmdline)?;
86 Some(Cmdline::from(cmdline_str.to_owned()))
87 }
88
89 Err(uki_error) => match uki_error {
90 composefs_boot::uki::UkiError::MissingSection(_) => {
91 None
94 }
95
96 e => anyhow::bail!("Failed to read UKI cmdline: {e:?}"),
97 },
98 };
99
100 return Ok(Some(KernelInternal {
101 kernel: Kernel {
102 version,
103 unified: true,
104 },
105 k_type: KernelType::Uki {
106 path: uki_path,
107 cmdline,
108 },
109 }));
110 }
111
112 if let Some(modules_dir) = ostree_ext::bootabletree::find_kernel_dir_fs(root)? {
114 let version = modules_dir
115 .file_name()
116 .ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {modules_dir}"))?
117 .to_owned();
118 let vmlinuz = modules_dir.join("vmlinuz");
119 let initramfs = modules_dir.join("initramfs.img");
120 return Ok(Some(KernelInternal {
121 kernel: Kernel {
122 version,
123 unified: false,
124 },
125 k_type: KernelType::Vmlinuz {
126 path: vmlinuz,
127 initramfs,
128 },
129 }));
130 }
131
132 Ok(None)
133}
134
135fn find_uki_path(root: &Dir) -> Result<Option<Utf8PathBuf>> {
140 let Some(boot) = root.open_dir_optional(crate::install::BOOT)? else {
141 return Ok(None);
142 };
143 let Some(efi_linux) = boot.open_dir_optional(EFI_LINUX)? else {
144 return Ok(None);
145 };
146
147 let mut uki_files = Vec::new();
148 for entry in efi_linux.entries()? {
149 let entry = entry?;
150 let name = entry.file_name();
151 let name_path = Path::new(&name);
152 let extension = name_path.extension().and_then(|v| v.to_str());
153 if extension == Some("efi") {
154 if let Some(name_str) = name.to_str() {
155 uki_files.push(name_str.to_owned());
156 }
157 }
158 }
159
160 uki_files.sort();
162 Ok(uki_files
163 .into_iter()
164 .next()
165 .map(|filename| Utf8PathBuf::from(format!("boot/{EFI_LINUX}/{filename}"))))
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use bootc_utils::create_minimal_pe;
172 use cap_std_ext::{cap_std, cap_tempfile, dirext::CapStdExtDirExt};
173
174 #[test]
175 fn test_find_kernel_none() -> Result<()> {
176 let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
177 assert!(find_kernel(&tempdir)?.is_none());
178 Ok(())
179 }
180
181 #[test]
182 fn test_find_kernel_traditional() -> Result<()> {
183 let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
184 tempdir.create_dir_all("usr/lib/modules/6.12.0-100.fc41.x86_64")?;
185 tempdir.atomic_write(
186 "usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz",
187 b"fake kernel",
188 )?;
189
190 let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
191 assert_eq!(kernel_internal.kernel.version, "6.12.0-100.fc41.x86_64");
192 assert!(!kernel_internal.kernel.unified);
193 match &kernel_internal.k_type {
194 KernelType::Vmlinuz { path, initramfs } => {
195 assert_eq!(
196 path.as_str(),
197 "usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz"
198 );
199 assert_eq!(
200 initramfs.as_str(),
201 "usr/lib/modules/6.12.0-100.fc41.x86_64/initramfs.img"
202 );
203 }
204 KernelType::Uki { .. } => panic!("Expected Vmlinuz, got Uki"),
205 }
206 Ok(())
207 }
208
209 #[test]
210 fn test_find_kernel_uki() -> Result<()> {
211 let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
212 tempdir.create_dir_all("boot/EFI/Linux")?;
213 tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", &create_minimal_pe())?;
214
215 let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
216 assert_eq!(kernel_internal.kernel.version, "fedora-6.12.0");
217 assert!(kernel_internal.kernel.unified);
218 match &kernel_internal.k_type {
219 KernelType::Uki { path, .. } => {
220 assert_eq!(path.as_str(), "boot/EFI/Linux/fedora-6.12.0.efi");
221 }
222 KernelType::Vmlinuz { .. } => panic!("Expected Uki, got Vmlinuz"),
223 }
224 Ok(())
225 }
226
227 #[test]
228 fn test_find_kernel_uki_takes_precedence() -> Result<()> {
229 let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
230 tempdir.create_dir_all("usr/lib/modules/6.12.0-100.fc41.x86_64")?;
232 tempdir.atomic_write(
233 "usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz",
234 b"fake kernel",
235 )?;
236 tempdir.create_dir_all("boot/EFI/Linux")?;
237 tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", &create_minimal_pe())?;
238
239 let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
240 assert_eq!(kernel_internal.kernel.version, "fedora-6.12.0");
242 assert!(kernel_internal.kernel.unified);
243 Ok(())
244 }
245
246 #[test]
247 fn test_find_uki_path_sorted() -> Result<()> {
248 let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
249 tempdir.create_dir_all("boot/EFI/Linux")?;
250 tempdir.atomic_write("boot/EFI/Linux/zzz.efi", &create_minimal_pe())?;
251 tempdir.atomic_write("boot/EFI/Linux/aaa.efi", &create_minimal_pe())?;
252 tempdir.atomic_write("boot/EFI/Linux/mmm.efi", &create_minimal_pe())?;
253
254 let path = find_uki_path(&tempdir)?.expect("should find uki");
256 assert_eq!(path.as_str(), "boot/EFI/Linux/aaa.efi");
257 Ok(())
258 }
259}