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