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