1use crate::consts::KernelPaths;
2use crate::consts::{CONFIG_FILENAME, KERNEL_IMAGE_PATH, MAKE_COMMAND};
3use crate::discovery::VersionEntry;
4use crate::error::BuilderErr;
5use std::collections::VecDeque;
6use std::io::{BufRead, BufReader};
7use std::path::{Path, PathBuf};
8use tracing::info;
9
10const ERROR_TAIL_LINES: usize = 25;
13
14#[cfg(feature = "dracut")]
15use crate::consts::DRACUT_COMMAND;
16
17#[derive(Debug)]
18pub struct BootManager {
19 paths: KernelPaths,
20 keep_last_kernel: bool,
21 last_kernel_suffix: String,
22}
23
24impl BootManager {
25 #[must_use]
26 pub fn new(
27 paths: KernelPaths,
28 keep_last_kernel: bool,
29 last_kernel_suffix: Option<String>,
30 ) -> Self {
31 Self {
32 paths,
33 keep_last_kernel,
34 last_kernel_suffix: last_kernel_suffix.unwrap_or_else(|| "prev".to_string()),
35 }
36 }
37
38 pub fn link_kernel_config(&self, kernel_path: &Path) -> Result<(), BuilderErr> {
44 let link = kernel_path.join(CONFIG_FILENAME);
45 let dot_config = &self.paths.kernel_config;
46
47 if link.exists() {
48 if link.is_symlink() {
49 std::fs::remove_file(&link)
50 .map_err(|e| BuilderErr::linking_file_error(e, link.clone()))?;
51 } else {
52 let mut old_file = link.clone();
53 old_file.set_file_name(format!("{CONFIG_FILENAME}.old"));
54 std::fs::copy(&link, &old_file)
55 .map_err(|e| BuilderErr::linking_file_error(e, link.clone()))?;
56 std::fs::remove_file(&link)
57 .map_err(|e| BuilderErr::linking_file_error(e, link.clone()))?;
58 }
59 }
60
61 std::os::unix::fs::symlink(dot_config, &link)
62 .map_err(|e| BuilderErr::linking_file_error(e, link.clone()))?;
63
64 Ok(())
65 }
66
67 pub fn update_linux_symlink(&self, version_entry: &VersionEntry) -> Result<(), BuilderErr> {
73 let linux = &self.paths.linux_symlink;
74
75 if linux.exists() || linux.is_symlink() {
76 if let Ok(target) = linux.read_link() {
77 if target == version_entry.path {
78 info!(
79 "Linux symlink already points to {}",
80 version_entry.version_string
81 );
82 return Ok(());
83 }
84 }
85 std::fs::remove_file(linux)
86 .map_err(|e| BuilderErr::linking_file_error(e, linux.clone()))?;
87 }
88
89 std::os::unix::fs::symlink(&version_entry.path, linux)
90 .map_err(|e| BuilderErr::linking_file_error(e, linux.clone()))?;
91
92 info!("Updated linux symlink to {}", version_entry.version_string);
93 Ok(())
94 }
95
96 pub fn check_new_config_options(&self, kernel_path: &Path) -> Result<bool, BuilderErr> {
102 let output = duct::cmd(MAKE_COMMAND, &["listnewconfig"])
103 .dir(kernel_path)
104 .stdout_capture()
105 .run()
106 .map_err(|e| BuilderErr::CommandError(e.to_string()))?;
107
108 let stdout = String::from_utf8_lossy(&output.stdout);
109 Ok(!stdout.trim().is_empty())
110 }
111
112 pub fn run_olddefconfig(&self, kernel_path: &Path) -> Result<(), BuilderErr> {
118 info!("Running make olddefconfig to integrate new kernel options");
119
120 let output = duct::cmd(MAKE_COMMAND, &["olddefconfig"])
121 .dir(kernel_path)
122 .stdout_capture()
123 .stderr_capture()
124 .run()
125 .map_err(|e| {
126 BuilderErr::CommandError(format!("Failed to run make olddefconfig: {e}"))
127 })?;
128
129 if !output.status.success() {
130 let stderr = String::from_utf8_lossy(&output.stderr);
131 return Err(BuilderErr::kernel_config_update_error(stderr.to_string()));
132 }
133
134 let mut old_config = self.paths.kernel_config.clone();
135 old_config.pop();
136 old_config.push(format!("{CONFIG_FILENAME}.old"));
137 std::fs::copy(&self.paths.kernel_config, &old_config)
138 .map_err(|e| BuilderErr::CommandError(e.to_string()))?;
139
140 std::fs::copy(kernel_path.join(CONFIG_FILENAME), &self.paths.kernel_config)
141 .map_err(|e| BuilderErr::CommandError(e.to_string()))?;
142
143 std::fs::remove_file(kernel_path.join(format!("{CONFIG_FILENAME}.old"))).ok();
144
145 let dot_config = kernel_path.join(CONFIG_FILENAME);
146 std::fs::remove_file(&dot_config).ok();
147 std::os::unix::fs::symlink(&self.paths.kernel_config, &dot_config)
148 .map_err(|e| BuilderErr::linking_file_error(e, dot_config))?;
149
150 Ok(())
151 }
152
153 fn run_streaming(
163 expr: &duct::Expression,
164 on_line: &mut dyn FnMut(&str),
165 on_error: impl Fn(String) -> BuilderErr,
166 ) -> Result<(), BuilderErr> {
167 let reader = expr
168 .stderr_to_stdout()
169 .reader()
170 .map_err(|e| on_error(e.to_string()))?;
171
172 let mut buffered = BufReader::new(&reader);
173 let mut tail: VecDeque<String> = VecDeque::with_capacity(ERROR_TAIL_LINES);
174 let mut line = String::new();
175
176 loop {
177 line.clear();
178 match buffered.read_line(&mut line) {
179 Ok(0) => break,
180 Ok(_) => {
181 let trimmed = line.trim_end().to_owned();
182 on_line(&trimmed);
183 if tail.len() == ERROR_TAIL_LINES {
184 tail.pop_front();
185 }
186 tail.push_back(trimmed);
187 }
188 Err(e) => {
191 let mut message: Vec<String> = tail.into_iter().collect();
192 message.push(e.to_string());
193 return Err(on_error(message.join("\n")));
194 }
195 }
196 }
197
198 Ok(())
199 }
200
201 pub fn build_kernel(
209 &self,
210 kernel_path: &Path,
211 threads: usize,
212 on_line: &mut dyn FnMut(&str),
213 ) -> Result<(), BuilderErr> {
214 info!("Building kernel with {threads} threads");
215
216 let expr = duct::cmd(MAKE_COMMAND, &["-j".to_string(), threads.to_string()]).dir(kernel_path);
217 Self::run_streaming(&expr, on_line, BuilderErr::KernelBuildFail)
218 }
219
220 pub fn install_kernel(&self, kernel_path: &Path, replace: bool) -> Result<(), BuilderErr> {
226 let kernel_image = kernel_path.join(KERNEL_IMAGE_PATH);
227
228 if !kernel_image.exists() {
229 return Err(BuilderErr::KernelBuildFail(format!(
230 "Kernel image not found at {}",
231 kernel_image.display()
232 )));
233 }
234
235 if self.keep_last_kernel && !replace && self.paths.kernel_image.exists() {
236 let backup_path = self.paths.backup_path(
237 &self.paths.kernel_image,
238 &format!("-{}", self.last_kernel_suffix),
239 );
240 info!("Backing up current kernel to {}", backup_path.display());
241 std::fs::copy(&self.paths.kernel_image, &backup_path)
242 .map_err(|e| BuilderErr::CommandError(e.to_string()))?;
243 }
244
245 info!("Installing kernel to {}", self.paths.kernel_image.display());
246 std::fs::copy(&kernel_image, &self.paths.kernel_image)
247 .map_err(|e| BuilderErr::CommandError(e.to_string()))?;
248
249 Ok(())
250 }
251
252 pub fn install_modules(
260 &self,
261 kernel_path: &Path,
262 on_line: &mut dyn FnMut(&str),
263 ) -> Result<(), BuilderErr> {
264 info!("Installing kernel modules");
265
266 let expr = duct::cmd(MAKE_COMMAND, &["modules_install"]).dir(kernel_path);
267 Self::run_streaming(&expr, on_line, BuilderErr::KernelBuildFail)
268 }
269
270 #[cfg(feature = "dracut")]
271 pub fn generate_initramfs(
277 &self,
278 version_entry: &VersionEntry,
279 replace: bool,
280 on_line: &mut dyn FnMut(&str),
281 ) -> Result<(), BuilderErr> {
282 let initramfs_path = self.paths.initramfs.as_ref().ok_or_else(|| {
283 BuilderErr::KernelConfigMissingOption("initramfs path not configured".to_string())
284 })?;
285
286 if self.keep_last_kernel && !replace && initramfs_path.exists() {
287 let backup_path = self
288 .paths
289 .backup_path(initramfs_path, &format!("-{}.img", self.last_kernel_suffix));
290 info!("Backing up current initramfs to {}", backup_path.display());
291 std::fs::copy(initramfs_path, &backup_path)
292 .map_err(|e| BuilderErr::CommandError(e.to_string()))?;
293 }
294
295 info!("Generating initramfs for {}", version_entry.version_string);
296
297 let kver = version_entry
298 .version_string
299 .strip_prefix("linux-")
300 .unwrap_or(&version_entry.version_string);
301
302 let expr = duct::cmd(
303 DRACUT_COMMAND,
304 &[
305 "--hostonly",
306 "--kver",
307 kver,
308 "--force",
309 initramfs_path.to_string_lossy().as_ref(),
310 ],
311 )
312 .dir(&version_entry.path);
313
314 Self::run_streaming(&expr, on_line, BuilderErr::KernelBuildFail)
315 }
316
317 #[must_use]
319 pub fn get_current_kernel(&self) -> Option<PathBuf> {
320 if self.paths.linux_symlink.exists() || self.paths.linux_symlink.is_symlink() {
321 self.paths.linux_symlink.read_link().ok()
322 } else {
323 None
324 }
325 }
326
327 pub fn remove_kernel(&self, kernel_path: &Path) -> Result<(), BuilderErr> {
333 info!("Removing kernel at {}", kernel_path.display());
334 std::fs::remove_dir_all(kernel_path)
335 .map_err(|e| BuilderErr::CommandError(format!("Failed to remove kernel: {e}")))?;
336 Ok(())
337 }
338}