1use std::path::Path;
8
9use anyhow::Result;
10use camino::Utf8PathBuf;
11use cap_std_ext::cap_std::fs::Dir;
12use cap_std_ext::dirext::CapStdExtDirExt;
13use serde::Serialize;
14
15use crate::bootc_composefs::boot::EFI_LINUX;
16
17#[derive(Debug, Serialize)]
19#[serde(rename_all = "kebab-case")]
20pub(crate) struct Kernel {
21 pub(crate) version: String,
25 pub(crate) unified: bool,
27}
28
29pub(crate) enum KernelPath {
35 Uki(Utf8PathBuf),
36 Vmlinuz {
37 path: Utf8PathBuf,
38 initramfs: Utf8PathBuf,
39 },
40}
41
42pub(crate) struct KernelInternal {
49 pub(crate) kernel: Kernel,
50 pub(crate) path: KernelPath,
51}
52
53impl From<KernelInternal> for Kernel {
54 fn from(kernel_internal: KernelInternal) -> Self {
55 kernel_internal.kernel
56 }
57}
58
59pub(crate) fn find_kernel(root: &Dir) -> Result<Option<KernelInternal>> {
67 if let Some(uki_path) = find_uki_path(root)? {
69 let version = uki_path.file_stem().unwrap_or(uki_path.as_str()).to_owned();
70 return Ok(Some(KernelInternal {
71 kernel: Kernel {
72 version,
73 unified: true,
74 },
75 path: KernelPath::Uki(uki_path),
76 }));
77 }
78
79 if let Some(modules_dir) = ostree_ext::bootabletree::find_kernel_dir_fs(root)? {
81 let version = modules_dir
82 .file_name()
83 .ok_or_else(|| anyhow::anyhow!("kernel dir should have a file name: {modules_dir}"))?
84 .to_owned();
85 let vmlinuz = modules_dir.join("vmlinuz");
86 let initramfs = modules_dir.join("initramfs.img");
87 return Ok(Some(KernelInternal {
88 kernel: Kernel {
89 version,
90 unified: false,
91 },
92 path: KernelPath::Vmlinuz {
93 path: vmlinuz,
94 initramfs,
95 },
96 }));
97 }
98
99 Ok(None)
100}
101
102fn find_uki_path(root: &Dir) -> Result<Option<Utf8PathBuf>> {
107 let Some(boot) = root.open_dir_optional(crate::install::BOOT)? else {
108 return Ok(None);
109 };
110 let Some(efi_linux) = boot.open_dir_optional(EFI_LINUX)? else {
111 return Ok(None);
112 };
113
114 let mut uki_files = Vec::new();
115 for entry in efi_linux.entries()? {
116 let entry = entry?;
117 let name = entry.file_name();
118 let name_path = Path::new(&name);
119 let extension = name_path.extension().and_then(|v| v.to_str());
120 if extension == Some("efi") {
121 if let Some(name_str) = name.to_str() {
122 uki_files.push(name_str.to_owned());
123 }
124 }
125 }
126
127 uki_files.sort();
129 Ok(uki_files
130 .into_iter()
131 .next()
132 .map(|filename| Utf8PathBuf::from(format!("boot/{EFI_LINUX}/{filename}"))))
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use cap_std_ext::{cap_std, cap_tempfile, dirext::CapStdExtDirExt};
139
140 #[test]
141 fn test_find_kernel_none() -> Result<()> {
142 let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
143 assert!(find_kernel(&tempdir)?.is_none());
144 Ok(())
145 }
146
147 #[test]
148 fn test_find_kernel_traditional() -> Result<()> {
149 let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
150 tempdir.create_dir_all("usr/lib/modules/6.12.0-100.fc41.x86_64")?;
151 tempdir.atomic_write(
152 "usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz",
153 b"fake kernel",
154 )?;
155
156 let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
157 assert_eq!(kernel_internal.kernel.version, "6.12.0-100.fc41.x86_64");
158 assert!(!kernel_internal.kernel.unified);
159 match &kernel_internal.path {
160 KernelPath::Vmlinuz { path, initramfs } => {
161 assert_eq!(
162 path.as_str(),
163 "usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz"
164 );
165 assert_eq!(
166 initramfs.as_str(),
167 "usr/lib/modules/6.12.0-100.fc41.x86_64/initramfs.img"
168 );
169 }
170 KernelPath::Uki(_) => panic!("Expected Vmlinuz, got Uki"),
171 }
172 Ok(())
173 }
174
175 #[test]
176 fn test_find_kernel_uki() -> Result<()> {
177 let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
178 tempdir.create_dir_all("boot/EFI/Linux")?;
179 tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", b"fake uki")?;
180
181 let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
182 assert_eq!(kernel_internal.kernel.version, "fedora-6.12.0");
183 assert!(kernel_internal.kernel.unified);
184 match &kernel_internal.path {
185 KernelPath::Uki(path) => {
186 assert_eq!(path.as_str(), "boot/EFI/Linux/fedora-6.12.0.efi");
187 }
188 KernelPath::Vmlinuz { .. } => panic!("Expected Uki, got Vmlinuz"),
189 }
190 Ok(())
191 }
192
193 #[test]
194 fn test_find_kernel_uki_takes_precedence() -> Result<()> {
195 let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
196 tempdir.create_dir_all("usr/lib/modules/6.12.0-100.fc41.x86_64")?;
198 tempdir.atomic_write(
199 "usr/lib/modules/6.12.0-100.fc41.x86_64/vmlinuz",
200 b"fake kernel",
201 )?;
202 tempdir.create_dir_all("boot/EFI/Linux")?;
203 tempdir.atomic_write("boot/EFI/Linux/fedora-6.12.0.efi", b"fake uki")?;
204
205 let kernel_internal = find_kernel(&tempdir)?.expect("should find kernel");
206 assert_eq!(kernel_internal.kernel.version, "fedora-6.12.0");
208 assert!(kernel_internal.kernel.unified);
209 Ok(())
210 }
211
212 #[test]
213 fn test_find_uki_path_sorted() -> Result<()> {
214 let tempdir = cap_tempfile::tempdir(cap_std::ambient_authority())?;
215 tempdir.create_dir_all("boot/EFI/Linux")?;
216 tempdir.atomic_write("boot/EFI/Linux/zzz.efi", b"fake uki")?;
217 tempdir.atomic_write("boot/EFI/Linux/aaa.efi", b"fake uki")?;
218 tempdir.atomic_write("boot/EFI/Linux/mmm.efi", b"fake uki")?;
219
220 let path = find_uki_path(&tempdir)?.expect("should find uki");
222 assert_eq!(path.as_str(), "boot/EFI/Linux/aaa.efi");
223 Ok(())
224 }
225}