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