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