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        pb.set_message(BuildStep::ConfigLink.label());
282        pb.inc(1);
283        self.boot_manager.link_kernel_config(&version_entry.path)?;
284
285        pb.set_message(BuildStep::SymlinkUpdate.label());
286        pb.inc(1);
287        self.boot_manager.update_linux_symlink(&version_entry)?;
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            pb.set_message(BuildStep::BuildKernel.label());
305            pb.inc(1);
306            self.build_kernel(&version_entry, cli.replace, &mut pb)?;
307        }
308
309        if !cli.no_modules && Self::confirm_prompt("Do you want to install kernel modules?")? {
310            pb.set_message(BuildStep::InstallModules.label());
311            pb.inc(1);
312            self.install_modules(&version_entry, &mut pb)?;
313        }
314
315        #[cfg(feature = "dracut")]
316        if !cli.no_initramfs
317            && Self::confirm_prompt("Do you want to generate initramfs with dracut?")?
318        {
319            pb.set_message(BuildStep::GenerateInitramfs.label());
320            pb.inc(1);
321            self.generate_initramfs(&version_entry, cli.replace, &mut pb)?;
322        }
323
324        if let Some(keep_count) = self.config.cleanup_keep_count {
325            self.cleanup_old_kernels(keep_count, &mut pb)?;
326        }
327
328        pb.finish_with_message("Build complete!");
329        Ok(())
330    }
331
332    fn cleanup_old_kernels(&self, keep_count: u32, pb: &mut ProgressBar) -> Result<(), BuilderErr> {
333        let current_kernel = self.boot_manager.get_current_kernel();
334        let current_version = current_kernel
335            .as_ref()
336            .and_then(|p| self.versions.iter().find(|v| v.path == *p));
337
338        let mut to_keep: Vec<&VersionEntry> = Vec::new();
339        if let Some(current) = current_version {
340            to_keep.push(current);
341        }
342
343        for v in &self.versions {
344            if to_keep.len() >= keep_count as usize {
345                break;
346            }
347            if !to_keep.contains(&v) {
348                to_keep.push(v);
349            }
350        }
351
352        let to_delete: Vec<&VersionEntry> = self
353            .versions
354            .iter()
355            .filter(|v| !to_keep.contains(v))
356            .collect();
357
358        if to_delete.is_empty() {
359            info!("No old kernels to clean up");
360            return Ok(());
361        }
362
363        pb.set_message(BuildStep::CleanupKernel.label());
364
365        for v in &to_delete {
366            info!("Cleaning up old kernel: {}", v.version_string);
367            self.boot_manager.remove_kernel(&v.path)?;
368        }
369
370        pb.inc(1);
371        info!("Cleaned up {} old kernel(s)", to_delete.len());
372        Ok(())
373    }
374
375    fn build_kernel(
376        &self,
377        version_entry: &VersionEntry,
378        replace: bool,
379        pb: &mut ProgressBar,
380    ) -> Result<(), BuilderErr> {
381        pb.set_message(BuildStep::ConfigUpdate.label());
382
383        if self
384            .boot_manager
385            .check_new_config_options(&version_entry.path)?
386        {
387            tracing::debug!("New config options detected, running olddefconfig");
388            self.boot_manager.run_olddefconfig(&version_entry.path)?;
389        }
390        pb.inc(1);
391
392        let threads = std::thread::available_parallelism()
393            .unwrap_or(NonZeroUsize::new(1).unwrap())
394            .get();
395
396        pb.set_message(BuildStep::BuildKernel.label());
397
398        let output = self
399            .boot_manager
400            .build_kernel(&version_entry.path, threads)?;
401
402        let stdout = String::from_utf8_lossy(&output.stdout);
403        tracing::debug!("Build output: {}", stdout.lines().last().unwrap_or(""));
404        pb.inc(1);
405
406        pb.set_message(BuildStep::InstallKernel.label());
407        self.boot_manager
408            .install_kernel(&version_entry.path, replace)?;
409        pb.inc(1);
410
411        Ok(())
412    }
413
414    fn install_modules(
415        &self,
416        version_entry: &VersionEntry,
417        pb: &mut ProgressBar,
418    ) -> Result<(), BuilderErr> {
419        pb.set_message(BuildStep::InstallModules.label());
420        self.boot_manager.install_modules(&version_entry.path)?;
421        pb.inc(1);
422        Ok(())
423    }
424
425    #[cfg(feature = "dracut")]
426    fn generate_initramfs(
427        &self,
428        version_entry: &VersionEntry,
429        replace: bool,
430        pb: &mut ProgressBar,
431    ) -> Result<(), BuilderErr> {
432        pb.set_message(BuildStep::GenerateInitramfs.label());
433
434        let output = self
435            .boot_manager
436            .generate_initramfs(version_entry, replace)?;
437
438        let stdout = String::from_utf8_lossy(&output.stdout);
439        tracing::debug!("Initramfs output: {}", stdout.lines().last().unwrap_or(""));
440        pb.inc(1);
441        Ok(())
442    }
443
444    fn prompt_for_kernel_version(&self) -> Option<VersionEntry> {
445        if self.versions.is_empty() {
446            tracing::debug!(
447                "No kernel versions found in {}",
448                self.config.kernel_src.display()
449            );
450            return None;
451        }
452
453        let version_strings: Vec<&str> = self
454            .versions
455            .iter()
456            .map(|v| v.version_string.as_str())
457            .collect();
458
459        Select::with_theme(&ColorfulTheme::default())
460            .with_prompt("Pick version to build and install")
461            .items(&version_strings)
462            .default(0)
463            .interact_on_opt(&Term::stderr())
464            .ok()
465            .flatten()
466            .map(|idx| self.versions[idx].clone())
467    }
468
469    fn confirm_prompt(message: &str) -> Result<bool, BuilderErr> {
470        Confirm::new()
471            .with_prompt(message)
472            .interact()
473            .map_err(BuilderErr::PromptError)
474    }
475}