Skip to main content

kernel_builder/
boot.rs

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
10/// Number of trailing output lines kept for error reporting when a streamed
11/// command fails.
12const 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    /// Link kernel config file to kernel source directory.
39    ///
40    /// # Errors
41    ///
42    /// Returns an error if file operations fail.
43    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    /// Update the /usr/src/linux symlink to point to the selected kernel.
68    ///
69    /// # Errors
70    ///
71    /// Returns an error if symlink operations fail.
72    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    /// Check if there are new kernel config options.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if the command fails.
101    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    /// Run make olddefconfig to integrate new kernel config options.
113    ///
114    /// # Errors
115    ///
116    /// Returns an error if olddefconfig fails.
117    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    /// Run a command, streaming each merged stdout/stderr line to `on_line`.
154    ///
155    /// Output is not captured wholesale; only the last [`ERROR_TAIL_LINES`]
156    /// lines are retained so they can be surfaced if the command fails.
157    ///
158    /// # Errors
159    ///
160    /// Returns the error produced by `on_error`, built from the retained tail,
161    /// if the command exits with a non-zero status or cannot be spawned.
162    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                // The final read returns an error when the child exits
189                // non-zero; the retained tail holds the relevant output.
190                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    /// Build the kernel with the specified number of threads.
202    ///
203    /// Build output is streamed line by line to `on_line` rather than captured.
204    ///
205    /// # Errors
206    ///
207    /// Returns an error if the build fails.
208    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    /// Install the compiled kernel to the boot partition.
221    ///
222    /// # Errors
223    ///
224    /// Returns an error if file operations fail.
225    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    /// Install kernel modules.
253    ///
254    /// Output is streamed line by line to `on_line`.
255    ///
256    /// # Errors
257    ///
258    /// Returns an error if `modules_install` fails.
259    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    /// Generate initramfs using dracut.
272    ///
273    /// # Errors
274    ///
275    /// Returns an error if dracut fails.
276    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    /// Get the current kernel symlink target.
318    #[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    /// Remove a kernel directory and its build artifacts.
328    ///
329    /// # Errors
330    ///
331    /// Returns an error if directory removal fails.
332    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}