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::path::{Path, PathBuf};
6use std::process::Output;
7use tracing::info;
8
9#[cfg(feature = "dracut")]
10use crate::consts::DRACUT_COMMAND;
11
12#[derive(Debug)]
13pub struct BootManager {
14    paths: KernelPaths,
15    keep_last_kernel: bool,
16    last_kernel_suffix: String,
17}
18
19impl BootManager {
20    #[must_use]
21    pub fn new(
22        paths: KernelPaths,
23        keep_last_kernel: bool,
24        last_kernel_suffix: Option<String>,
25    ) -> Self {
26        Self {
27            paths,
28            keep_last_kernel,
29            last_kernel_suffix: last_kernel_suffix.unwrap_or_else(|| "prev".to_string()),
30        }
31    }
32
33    /// Link kernel config file to kernel source directory.
34    ///
35    /// # Errors
36    ///
37    /// Returns an error if file operations fail.
38    pub fn link_kernel_config(&self, kernel_path: &Path) -> Result<(), BuilderErr> {
39        let link = kernel_path.join(CONFIG_FILENAME);
40        let dot_config = &self.paths.kernel_config;
41
42        if link.exists() {
43            if link.is_symlink() {
44                std::fs::remove_file(&link)
45                    .map_err(|e| BuilderErr::linking_file_error(e, link.clone()))?;
46            } else {
47                let mut old_file = link.clone();
48                old_file.set_file_name(format!("{CONFIG_FILENAME}.old"));
49                std::fs::copy(&link, &old_file)
50                    .map_err(|e| BuilderErr::linking_file_error(e, link.clone()))?;
51            }
52        }
53
54        std::os::unix::fs::symlink(dot_config, &link)
55            .map_err(|e| BuilderErr::linking_file_error(e, link.clone()))?;
56
57        Ok(())
58    }
59
60    /// Update the /usr/src/linux symlink to point to the selected kernel.
61    ///
62    /// # Errors
63    ///
64    /// Returns an error if symlink operations fail.
65    pub fn update_linux_symlink(&self, version_entry: &VersionEntry) -> Result<(), BuilderErr> {
66        let linux = &self.paths.linux_symlink;
67
68        if linux.exists() || linux.is_symlink() {
69            if let Ok(target) = linux.read_link() {
70                if target == version_entry.path {
71                    info!(
72                        "Linux symlink already points to {}",
73                        version_entry.version_string
74                    );
75                    return Ok(());
76                }
77            }
78            std::fs::remove_file(linux)
79                .map_err(|e| BuilderErr::linking_file_error(e, linux.clone()))?;
80        }
81
82        std::os::unix::fs::symlink(&version_entry.path, linux)
83            .map_err(|e| BuilderErr::linking_file_error(e, linux.clone()))?;
84
85        info!("Updated linux symlink to {}", version_entry.version_string);
86        Ok(())
87    }
88
89    /// Check if there are new kernel config options.
90    ///
91    /// # Errors
92    ///
93    /// Returns an error if the command fails.
94    pub fn check_new_config_options(&self, kernel_path: &Path) -> Result<bool, BuilderErr> {
95        let output = duct::cmd(MAKE_COMMAND, &["listnewconfig"])
96            .dir(kernel_path)
97            .stdout_capture()
98            .run()
99            .map_err(|e| BuilderErr::CommandError(e.to_string()))?;
100
101        let stdout = String::from_utf8_lossy(&output.stdout);
102        Ok(!stdout.trim().is_empty())
103    }
104
105    /// Run make olddefconfig to integrate new kernel config options.
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if olddefconfig fails.
110    pub fn run_olddefconfig(&self, kernel_path: &Path) -> Result<(), BuilderErr> {
111        info!("Running make olddefconfig to integrate new kernel options");
112
113        let output = duct::cmd(MAKE_COMMAND, &["olddefconfig"])
114            .dir(kernel_path)
115            .stdout_capture()
116            .stderr_capture()
117            .run()
118            .map_err(|e| {
119                BuilderErr::CommandError(format!("Failed to run make olddefconfig: {e}"))
120            })?;
121
122        if !output.status.success() {
123            let stderr = String::from_utf8_lossy(&output.stderr);
124            return Err(BuilderErr::kernel_config_update_error(stderr.to_string()));
125        }
126
127        let mut old_config = self.paths.kernel_config.clone();
128        old_config.pop();
129        old_config.push(format!("{CONFIG_FILENAME}.old"));
130        std::fs::copy(&self.paths.kernel_config, &old_config)
131            .map_err(|e| BuilderErr::CommandError(e.to_string()))?;
132
133        std::fs::copy(kernel_path.join(CONFIG_FILENAME), &self.paths.kernel_config)
134            .map_err(|e| BuilderErr::CommandError(e.to_string()))?;
135
136        std::fs::remove_file(kernel_path.join(format!("{CONFIG_FILENAME}.old"))).ok();
137
138        let dot_config = kernel_path.join(CONFIG_FILENAME);
139        std::fs::remove_file(&dot_config).ok();
140        std::os::unix::fs::symlink(&self.paths.kernel_config, &dot_config)
141            .map_err(|e| BuilderErr::linking_file_error(e, dot_config))?;
142
143        Ok(())
144    }
145
146    /// Build the kernel with the specified number of threads.
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if the build fails.
151    pub fn build_kernel(&self, kernel_path: &Path, threads: usize) -> Result<Output, BuilderErr> {
152        info!("Building kernel with {threads} threads");
153
154        let output = duct::cmd(MAKE_COMMAND, &["-j".to_string(), threads.to_string()])
155            .dir(kernel_path)
156            .stdout_capture()
157            .stderr_capture()
158            .run()
159            .map_err(|e| BuilderErr::CommandError(format!("Kernel build failed: {e}")))?;
160
161        if !output.status.success() {
162            let stderr = String::from_utf8_lossy(&output.stderr);
163            return Err(BuilderErr::KernelBuildFail(stderr.to_string()));
164        }
165
166        Ok(output)
167    }
168
169    /// Install the compiled kernel to the boot partition.
170    ///
171    /// # Errors
172    ///
173    /// Returns an error if file operations fail.
174    pub fn install_kernel(&self, kernel_path: &Path, replace: bool) -> Result<(), BuilderErr> {
175        let kernel_image = kernel_path.join(KERNEL_IMAGE_PATH);
176
177        if !kernel_image.exists() {
178            return Err(BuilderErr::KernelBuildFail(format!(
179                "Kernel image not found at {}",
180                kernel_image.display()
181            )));
182        }
183
184        if self.keep_last_kernel && !replace && self.paths.kernel_image.exists() {
185            let backup_path = self.paths.backup_path(
186                &self.paths.kernel_image,
187                &format!("-{}", self.last_kernel_suffix),
188            );
189            info!("Backing up current kernel to {}", backup_path.display());
190            std::fs::copy(&self.paths.kernel_image, &backup_path)
191                .map_err(|e| BuilderErr::CommandError(e.to_string()))?;
192        }
193
194        info!("Installing kernel to {}", self.paths.kernel_image.display());
195        std::fs::copy(&kernel_image, &self.paths.kernel_image)
196            .map_err(|e| BuilderErr::CommandError(e.to_string()))?;
197
198        Ok(())
199    }
200
201    /// Install kernel modules.
202    ///
203    /// # Errors
204    ///
205    /// Returns an error if `modules_install` fails.
206    pub fn install_modules(&self, kernel_path: &Path) -> Result<Output, BuilderErr> {
207        info!("Installing kernel modules");
208
209        let output = duct::cmd(MAKE_COMMAND, &["modules_install"])
210            .dir(kernel_path)
211            .stdout_capture()
212            .stderr_capture()
213            .run()
214            .map_err(|e| BuilderErr::CommandError(format!("Failed to install modules: {e}")))?;
215
216        if !output.status.success() {
217            let stderr = String::from_utf8_lossy(&output.stderr);
218            return Err(BuilderErr::KernelBuildFail(stderr.to_string()));
219        }
220
221        Ok(output)
222    }
223
224    #[cfg(feature = "dracut")]
225    /// Generate initramfs using dracut.
226    ///
227    /// # Errors
228    ///
229    /// Returns an error if dracut fails.
230    pub fn generate_initramfs(
231        &self,
232        version_entry: &VersionEntry,
233        replace: bool,
234    ) -> Result<Output, BuilderErr> {
235        let initramfs_path = self.paths.initramfs.as_ref().ok_or_else(|| {
236            BuilderErr::KernelConfigMissingOption("initramfs path not configured".to_string())
237        })?;
238
239        if self.keep_last_kernel && !replace && initramfs_path.exists() {
240            let backup_path = self
241                .paths
242                .backup_path(initramfs_path, &format!("-{}.img", self.last_kernel_suffix));
243            info!("Backing up current initramfs to {}", backup_path.display());
244            std::fs::copy(initramfs_path, &backup_path)
245                .map_err(|e| BuilderErr::CommandError(e.to_string()))?;
246        }
247
248        info!("Generating initramfs for {}", version_entry.version_string);
249
250        let kver = version_entry
251            .version_string
252            .strip_prefix("linux-")
253            .unwrap_or(&version_entry.version_string);
254
255        let output = duct::cmd(
256            DRACUT_COMMAND,
257            &[
258                "--hostonly",
259                "--kver",
260                kver,
261                "--force",
262                initramfs_path.to_string_lossy().as_ref(),
263            ],
264        )
265        .dir(&version_entry.path)
266        .stdout_capture()
267        .stderr_capture()
268        .run()
269        .map_err(|e| BuilderErr::CommandError(format!("Failed to generate initramfs: {e}")))?;
270
271        if !output.status.success() {
272            let stderr = String::from_utf8_lossy(&output.stderr);
273            return Err(BuilderErr::KernelBuildFail(stderr.to_string()));
274        }
275
276        Ok(output)
277    }
278
279    /// Run menuconfig for kernel configuration.
280    ///
281    /// # Errors
282    ///
283    /// Returns an error if menuconfig fails.
284    pub fn run_menuconfig(&self, kernel_path: &Path) -> Result<Output, BuilderErr> {
285        info!("Running menuconfig");
286
287        let output = duct::cmd(MAKE_COMMAND, &["menuconfig"])
288            .dir(kernel_path)
289            .stdout_capture()
290            .stderr_capture()
291            .run()
292            .map_err(|e| BuilderErr::CommandError(format!("Failed to run menuconfig: {e}")))?;
293
294        Ok(output)
295    }
296
297    /// Get the current kernel symlink target.
298    #[must_use]
299    pub fn get_current_kernel(&self) -> Option<PathBuf> {
300        if self.paths.linux_symlink.exists() || self.paths.linux_symlink.is_symlink() {
301            self.paths.linux_symlink.read_link().ok()
302        } else {
303            None
304        }
305    }
306
307    /// Remove a kernel directory and its build artifacts.
308    ///
309    /// # Errors
310    ///
311    /// Returns an error if directory removal fails.
312    pub fn remove_kernel(&self, kernel_path: &Path) -> Result<(), BuilderErr> {
313        info!("Removing kernel at {}", kernel_path.display());
314        std::fs::remove_dir_all(kernel_path)
315            .map_err(|e| BuilderErr::CommandError(format!("Failed to remove kernel: {e}")))?;
316        Ok(())
317    }
318}