Skip to main content

kernel_builder/
lib.rs

1pub mod boot;
2pub mod cli;
3pub mod consts;
4pub mod discovery;
5mod error;
6
7pub use cli::Args;
8pub use consts::KernelPaths;
9pub use discovery::VersionEntry;
10pub use error::BuilderErr;
11
12use crate::boot::BootManager;
13use dialoguer::{console::Term, theme::ColorfulTheme, Confirm, Select};
14use indicatif::{ProgressBar, ProgressStyle};
15use serde::Deserialize;
16use std::num::NonZeroUsize;
17use std::path::{Path, PathBuf};
18use tracing::info;
19
20#[derive(Debug, Clone)]
21pub struct BuildProgress {
22    steps: Vec<BuildStep>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum BuildStep {
27    ConfigLink,
28    SymlinkUpdate,
29    Menuconfig,
30    ConfigUpdate,
31    BuildKernel,
32    InstallKernel,
33    InstallModules,
34    GenerateInitramfs,
35    CleanupKernel,
36}
37
38impl BuildStep {
39    #[must_use]
40    pub fn label(&self) -> &'static str {
41        match self {
42            BuildStep::ConfigLink => "Linking kernel config",
43            BuildStep::SymlinkUpdate => "Updating linux symlink",
44            BuildStep::Menuconfig => "Running menuconfig",
45            BuildStep::ConfigUpdate => "Updating kernel config",
46            BuildStep::BuildKernel => "Building kernel",
47            BuildStep::InstallKernel => "Installing kernel",
48            BuildStep::InstallModules => "Installing modules",
49            BuildStep::GenerateInitramfs => "Generating initramfs",
50            BuildStep::CleanupKernel => "Cleaning up old kernels",
51        }
52    }
53}
54
55impl BuildProgress {
56    #[must_use]
57    pub fn new() -> Self {
58        Self { steps: Vec::new() }
59    }
60
61    pub fn add_step(&mut self, step: BuildStep) {
62        self.steps.push(step);
63    }
64
65    /// Create a progress bar for the build steps.
66    #[must_use]
67    pub fn create_progress_bar(&self) -> ProgressBar {
68        let count = self.steps.len();
69        if count == 0 {
70            return ProgressBar::hidden();
71        }
72
73        let pb = ProgressBar::new(count as u64);
74        pb.set_style(
75            ProgressStyle::with_template("{spinner:.cyan} [{bar:40.cyan/dim}] {pos}/{len} {msg}")
76                .expect("valid template")
77                .progress_chars("=>-"),
78        );
79        pb.set_message("Starting...");
80        pb
81    }
82}
83
84impl Default for BuildProgress {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90pub fn init_logging(verbose: bool) {
91    use tracing_subscriber::{fmt, prelude::*, EnvFilter};
92
93    if !verbose {
94        return;
95    }
96
97    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("debug"));
98
99    tracing_subscriber::registry()
100        .with(fmt::layer().with_target(false).without_time())
101        .with(filter)
102        .init();
103}
104
105#[derive(Debug, Deserialize)]
106pub struct KBConfig {
107    #[serde(rename = "kernel")]
108    pub kernel_file_path: PathBuf,
109    #[serde(rename = "initramfs")]
110    pub initramfs_file_path: Option<PathBuf>,
111    #[serde(rename = "kernel-config")]
112    pub kernel_config_file_path: PathBuf,
113    #[serde(rename = "kernel-src")]
114    pub kernel_src: PathBuf,
115    #[serde(rename = "keep-last-kernel")]
116    pub keep_last_kernel: bool,
117    #[serde(rename = "last-kernel-suffix")]
118    pub last_kernel_suffix: Option<String>,
119    #[serde(rename = "cleanup-keep-count", default)]
120    pub cleanup_keep_count: Option<u32>,
121}
122
123impl KBConfig {
124    /// Validate the configuration.
125    ///
126    /// # Errors
127    ///
128    /// Returns an error if validation fails.
129    pub fn validate(&self) -> Result<(), BuilderErr> {
130        if !self.kernel_src.exists() {
131            return Err(BuilderErr::invalid_config(format!(
132                "Kernel source directory does not exist: {}",
133                self.kernel_src.display()
134            )));
135        }
136
137        if !self.kernel_src.is_dir() {
138            return Err(BuilderErr::invalid_config(format!(
139                "Kernel source path is not a directory: {}",
140                self.kernel_src.display()
141            )));
142        }
143
144        if self.kernel_file_path.parent().is_some_and(|p| !p.exists()) {
145            return Err(BuilderErr::invalid_config(format!(
146                "Kernel file parent directory does not exist: {}",
147                self.kernel_file_path.display()
148            )));
149        }
150
151        if let Some(ref initramfs) = self.initramfs_file_path {
152            if initramfs.parent().is_some_and(|p| !p.exists()) {
153                return Err(BuilderErr::invalid_config(format!(
154                    "Initramfs parent directory does not exist: {}",
155                    initramfs.display()
156                )));
157            }
158        }
159
160        Ok(())
161    }
162
163    #[must_use]
164    pub fn to_kernel_paths(&self) -> KernelPaths {
165        KernelPaths::new(
166            self.kernel_file_path.clone(),
167            self.initramfs_file_path.clone(),
168            self.kernel_config_file_path.clone(),
169            self.kernel_src.clone(),
170        )
171    }
172}
173
174#[derive(Debug)]
175pub struct KernelBuilder {
176    config: KBConfig,
177    versions: Vec<VersionEntry>,
178    boot_manager: BootManager,
179}
180
181impl KernelBuilder {
182    /// Create a new `KernelBuilder` instance.
183    ///
184    /// # Errors
185    ///
186    /// Returns an error if validation fails or version scanning fails.
187    pub fn new(config: KBConfig) -> Result<Self, BuilderErr> {
188        config.validate()?;
189
190        let versions = Self::scan_versions(&config.kernel_src)?;
191        let paths = config.to_kernel_paths();
192        let boot_manager = BootManager::new(
193            paths,
194            config.keep_last_kernel,
195            config.last_kernel_suffix.clone(),
196        );
197
198        Ok(Self {
199            config,
200            versions,
201            boot_manager,
202        })
203    }
204
205    #[must_use]
206    pub fn versions(&self) -> &[VersionEntry] {
207        &self.versions
208    }
209
210    fn scan_versions(kernel_src: &Path) -> Result<Vec<VersionEntry>, BuilderErr> {
211        let entries = std::fs::read_dir(kernel_src).map_err(|e| {
212            BuilderErr::discovery_error(format!(
213                "Failed to read kernel source directory {}: {e}",
214                kernel_src.display()
215            ))
216        })?;
217
218        let mut versions: Vec<VersionEntry> = Vec::new();
219        for dir in entries.flatten() {
220            let path = dir.path();
221            if path.is_dir() && !path.is_symlink() {
222                if let Some(entry) = VersionEntry::from_path(&path) {
223                    versions.push(entry);
224                }
225            }
226        }
227
228        versions.sort();
229        versions.reverse();
230
231        tracing::debug!("Found {} kernel versions", versions.len());
232        for v in &versions {
233            tracing::debug!("  - {v}");
234        }
235
236        Ok(versions)
237    }
238
239    /// Execute the kernel build workflow.
240    ///
241    /// # Errors
242    ///
243    /// Returns an error if any step of the build process fails.
244    pub fn build(&self, cli: &Args) -> Result<(), BuilderErr> {
245        let Some(version_entry) = self.prompt_for_kernel_version() else {
246            tracing::debug!("No kernel version selected, exiting");
247            return Ok(());
248        };
249
250        tracing::debug!("Selected kernel: {}", version_entry.version_string);
251
252        if !self.config.kernel_config_file_path.exists() {
253            return Err(BuilderErr::kernel_config_missing());
254        }
255
256        let mut progress = BuildProgress::new();
257        progress.add_step(BuildStep::ConfigLink);
258        progress.add_step(BuildStep::SymlinkUpdate);
259
260        if cli.menuconfig {
261            progress.add_step(BuildStep::Menuconfig);
262        }
263
264        if !cli.no_build {
265            progress.add_step(BuildStep::ConfigUpdate);
266            progress.add_step(BuildStep::BuildKernel);
267            progress.add_step(BuildStep::InstallKernel);
268        }
269
270        if !cli.no_modules {
271            progress.add_step(BuildStep::InstallModules);
272        }
273
274        #[cfg(feature = "dracut")]
275        if !cli.no_initramfs {
276            progress.add_step(BuildStep::GenerateInitramfs);
277        }
278
279        let mut pb = progress.create_progress_bar();
280
281        self.boot_manager.link_kernel_config(&version_entry.path)?;
282        pb.set_message(BuildStep::ConfigLink.label());
283        pb.inc(1);
284
285        self.boot_manager.update_linux_symlink(&version_entry)?;
286        pb.set_message(BuildStep::SymlinkUpdate.label());
287        pb.inc(1);
288
289        if cli.menuconfig {
290            let output = self.boot_manager.run_menuconfig(&version_entry.path)?;
291            if !output.status.success() {
292                pb.finish_with_message("menuconfig failed");
293                return Err(BuilderErr::MenuconfigError);
294            }
295            pb.set_message(BuildStep::Menuconfig.label());
296            pb.inc(1);
297            if !Self::confirm_prompt("Continue build process?")? {
298                pb.finish_with_message("Cancelled by user");
299                return Ok(());
300            }
301        }
302
303        if !cli.no_build {
304            self.build_kernel(&version_entry, cli.replace, &mut pb)?;
305        }
306
307        if !cli.no_modules && Self::confirm_prompt("Do you want to install kernel modules?")? {
308            self.install_modules(&version_entry, &mut pb)?;
309        }
310
311        #[cfg(feature = "dracut")]
312        if !cli.no_initramfs
313            && Self::confirm_prompt("Do you want to generate initramfs with dracut?")?
314        {
315            self.generate_initramfs(&version_entry, cli.replace, &mut pb)?;
316        }
317
318        if let Some(keep_count) = self.config.cleanup_keep_count {
319            self.cleanup_old_kernels(keep_count, &mut pb)?;
320        }
321
322        pb.finish_with_message("Build complete!");
323        Ok(())
324    }
325
326    fn cleanup_old_kernels(&self, keep_count: u32, pb: &mut ProgressBar) -> Result<(), BuilderErr> {
327        let current_kernel = self.boot_manager.get_current_kernel();
328        let current_version = current_kernel
329            .as_ref()
330            .and_then(|p| self.versions.iter().find(|v| v.path == *p));
331
332        let mut to_keep: Vec<&VersionEntry> = Vec::new();
333        if let Some(current) = current_version {
334            to_keep.push(current);
335        }
336
337        for v in &self.versions {
338            if to_keep.len() >= keep_count as usize {
339                break;
340            }
341            if !to_keep.contains(&v) {
342                to_keep.push(v);
343            }
344        }
345
346        let to_delete: Vec<&VersionEntry> = self
347            .versions
348            .iter()
349            .filter(|v| !to_keep.contains(v))
350            .collect();
351
352        if to_delete.is_empty() {
353            info!("No old kernels to clean up");
354            return Ok(());
355        }
356
357        pb.set_message(BuildStep::CleanupKernel.label());
358
359        for v in &to_delete {
360            info!("Cleaning up old kernel: {}", v.version_string);
361            self.boot_manager.remove_kernel(&v.path)?;
362        }
363
364        pb.inc(1);
365        info!("Cleaned up {} old kernel(s)", to_delete.len());
366        Ok(())
367    }
368
369    fn build_kernel(
370        &self,
371        version_entry: &VersionEntry,
372        replace: bool,
373        pb: &mut ProgressBar,
374    ) -> Result<(), BuilderErr> {
375        pb.set_message(BuildStep::ConfigUpdate.label());
376
377        if self
378            .boot_manager
379            .check_new_config_options(&version_entry.path)?
380        {
381            tracing::debug!("New config options detected, running olddefconfig");
382            self.boot_manager.run_olddefconfig(&version_entry.path)?;
383        }
384        pb.inc(1);
385
386        let threads = std::thread::available_parallelism()
387            .unwrap_or(NonZeroUsize::new(1).unwrap())
388            .get();
389
390        pb.set_message(BuildStep::BuildKernel.label());
391
392        let output = self
393            .boot_manager
394            .build_kernel(&version_entry.path, threads)?;
395
396        let stdout = String::from_utf8_lossy(&output.stdout);
397        tracing::debug!("Build output: {}", stdout.lines().last().unwrap_or(""));
398        pb.inc(1);
399
400        pb.set_message(BuildStep::InstallKernel.label());
401        self.boot_manager
402            .install_kernel(&version_entry.path, replace)?;
403        pb.inc(1);
404
405        Ok(())
406    }
407
408    fn install_modules(
409        &self,
410        version_entry: &VersionEntry,
411        pb: &mut ProgressBar,
412    ) -> Result<(), BuilderErr> {
413        pb.set_message(BuildStep::InstallModules.label());
414        self.boot_manager.install_modules(&version_entry.path)?;
415        pb.inc(1);
416        Ok(())
417    }
418
419    #[cfg(feature = "dracut")]
420    fn generate_initramfs(
421        &self,
422        version_entry: &VersionEntry,
423        replace: bool,
424        pb: &mut ProgressBar,
425    ) -> Result<(), BuilderErr> {
426        pb.set_message(BuildStep::GenerateInitramfs.label());
427
428        let output = self
429            .boot_manager
430            .generate_initramfs(version_entry, replace)?;
431
432        let stdout = String::from_utf8_lossy(&output.stdout);
433        tracing::debug!("Initramfs output: {}", stdout.lines().last().unwrap_or(""));
434        pb.inc(1);
435        Ok(())
436    }
437
438    fn prompt_for_kernel_version(&self) -> Option<VersionEntry> {
439        if self.versions.is_empty() {
440            tracing::debug!(
441                "No kernel versions found in {}",
442                self.config.kernel_src.display()
443            );
444            return None;
445        }
446
447        let version_strings: Vec<&str> = self
448            .versions
449            .iter()
450            .map(|v| v.version_string.as_str())
451            .collect();
452
453        Select::with_theme(&ColorfulTheme::default())
454            .with_prompt("Pick version to build and install")
455            .items(&version_strings)
456            .default(0)
457            .interact_on_opt(&Term::stderr())
458            .ok()
459            .flatten()
460            .map(|idx| self.versions[idx].clone())
461    }
462
463    fn confirm_prompt(message: &str) -> Result<bool, BuilderErr> {
464        Confirm::new()
465            .with_prompt(message)
466            .interact()
467            .map_err(BuilderErr::PromptError)
468    }
469}