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 #[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 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 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 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}