pub mod boot;
pub mod cli;
pub mod consts;
pub mod discovery;
mod error;
pub use cli::Args;
pub use consts::KernelPaths;
pub use discovery::VersionEntry;
pub use error::BuilderErr;
use crate::boot::BootManager;
use crate::consts::MAKE_COMMAND;
use dialoguer::{Confirm, Select, console::Term, theme::ColorfulTheme};
use indicatif::{ProgressBar, ProgressStyle};
use serde::Deserialize;
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf};
use tracing::info;
#[derive(Debug, Clone)]
pub struct BuildProgress {
steps: Vec<BuildStep>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BuildStep {
ConfigLink,
SymlinkUpdate,
Menuconfig,
ConfigUpdate,
BuildKernel,
InstallKernel,
InstallModules,
GenerateInitramfs,
CleanupKernel,
}
impl BuildStep {
#[must_use]
pub fn label(&self) -> &'static str {
match self {
BuildStep::ConfigLink => "Linking kernel config",
BuildStep::SymlinkUpdate => "Updating linux symlink",
BuildStep::Menuconfig => "Running menuconfig",
BuildStep::ConfigUpdate => "Updating kernel config",
BuildStep::BuildKernel => "Building kernel",
BuildStep::InstallKernel => "Installing kernel",
BuildStep::InstallModules => "Installing modules",
BuildStep::GenerateInitramfs => "Generating initramfs",
BuildStep::CleanupKernel => "Cleaning up old kernels",
}
}
}
impl BuildProgress {
#[must_use]
pub fn new() -> Self {
Self { steps: Vec::new() }
}
pub fn add_step(&mut self, step: BuildStep) {
self.steps.push(step);
}
#[must_use]
pub fn create_progress_bar(&self) -> ProgressBar {
let count = self.steps.len();
if count == 0 {
return ProgressBar::hidden();
}
let pb = ProgressBar::new(count as u64);
pb.set_style(
ProgressStyle::with_template("{spinner:.cyan} [{bar:40.cyan/dim}] {pos}/{len} {msg}")
.expect("valid template")
.progress_chars("=>-"),
);
pb.set_message("Starting...");
pb
}
}
impl Default for BuildProgress {
fn default() -> Self {
Self::new()
}
}
pub fn init_logging(verbose: bool) {
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
if !verbose {
return;
}
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("debug"));
tracing_subscriber::registry()
.with(fmt::layer().with_target(false).without_time())
.with(filter)
.init();
}
#[derive(Debug, Deserialize)]
pub struct KBConfig {
#[serde(rename = "kernel")]
pub kernel_file_path: PathBuf,
#[serde(rename = "initramfs")]
pub initramfs_file_path: Option<PathBuf>,
#[serde(rename = "kernel-config")]
pub kernel_config_file_path: PathBuf,
#[serde(rename = "kernel-src")]
pub kernel_src: PathBuf,
#[serde(rename = "keep-last-kernel")]
pub keep_last_kernel: bool,
#[serde(rename = "last-kernel-suffix")]
pub last_kernel_suffix: Option<String>,
#[serde(rename = "cleanup-keep-count", default)]
pub cleanup_keep_count: Option<u32>,
}
impl KBConfig {
pub fn validate(&self) -> Result<(), BuilderErr> {
if !self.kernel_src.exists() {
return Err(BuilderErr::invalid_config(format!(
"Kernel source directory does not exist: {}",
self.kernel_src.display()
)));
}
if !self.kernel_src.is_dir() {
return Err(BuilderErr::invalid_config(format!(
"Kernel source path is not a directory: {}",
self.kernel_src.display()
)));
}
if self.kernel_file_path.parent().is_some_and(|p| !p.exists()) {
return Err(BuilderErr::invalid_config(format!(
"Kernel file parent directory does not exist: {}",
self.kernel_file_path.display()
)));
}
if let Some(ref initramfs) = self.initramfs_file_path {
if initramfs.parent().is_some_and(|p| !p.exists()) {
return Err(BuilderErr::invalid_config(format!(
"Initramfs parent directory does not exist: {}",
initramfs.display()
)));
}
}
Ok(())
}
#[must_use]
pub fn to_kernel_paths(&self) -> KernelPaths {
KernelPaths::new(
self.kernel_file_path.clone(),
self.initramfs_file_path.clone(),
self.kernel_config_file_path.clone(),
self.kernel_src.clone(),
)
}
}
#[derive(Debug)]
pub struct KernelBuilder {
config: KBConfig,
versions: Vec<VersionEntry>,
boot_manager: BootManager,
}
impl KernelBuilder {
pub fn new(config: KBConfig) -> Result<Self, BuilderErr> {
config.validate()?;
let versions = Self::scan_versions(&config.kernel_src)?;
let paths = config.to_kernel_paths();
let boot_manager = BootManager::new(
paths,
config.keep_last_kernel,
config.last_kernel_suffix.clone(),
);
Ok(Self {
config,
versions,
boot_manager,
})
}
#[must_use]
pub fn versions(&self) -> &[VersionEntry] {
&self.versions
}
fn scan_versions(kernel_src: &Path) -> Result<Vec<VersionEntry>, BuilderErr> {
let entries = std::fs::read_dir(kernel_src).map_err(|e| {
BuilderErr::discovery_error(format!(
"Failed to read kernel source directory {}: {e}",
kernel_src.display()
))
})?;
let mut versions: Vec<VersionEntry> = Vec::new();
for dir in entries.flatten() {
let path = dir.path();
if path.is_dir() && !path.is_symlink() {
if let Some(entry) = VersionEntry::from_path(&path) {
versions.push(entry);
}
}
}
versions.sort();
versions.reverse();
tracing::debug!("Found {} kernel versions", versions.len());
for v in &versions {
tracing::debug!(" - {v}");
}
Ok(versions)
}
pub fn build(&self, cli: &Args) -> Result<(), BuilderErr> {
let Some(version_entry) = self.prompt_for_kernel_version() else {
tracing::debug!("No kernel version selected, exiting");
return Ok(());
};
tracing::debug!("Selected kernel: {}", version_entry.version_string);
if !self.config.kernel_config_file_path.exists() {
return Err(BuilderErr::kernel_config_missing());
}
let mut progress = BuildProgress::new();
progress.add_step(BuildStep::ConfigLink);
progress.add_step(BuildStep::SymlinkUpdate);
if cli.menuconfig {
progress.add_step(BuildStep::Menuconfig);
}
if !cli.no_build {
progress.add_step(BuildStep::ConfigUpdate);
progress.add_step(BuildStep::BuildKernel);
progress.add_step(BuildStep::InstallKernel);
}
if !cli.no_modules {
progress.add_step(BuildStep::InstallModules);
}
#[cfg(feature = "dracut")]
if !cli.no_initramfs {
progress.add_step(BuildStep::GenerateInitramfs);
}
let mut pb = progress.create_progress_bar();
pb.set_message(BuildStep::ConfigLink.label());
pb.inc(1);
self.boot_manager.link_kernel_config(&version_entry.path)?;
pb.set_message(BuildStep::SymlinkUpdate.label());
pb.inc(1);
self.boot_manager.update_linux_symlink(&version_entry)?;
if cli.menuconfig {
pb.set_message(BuildStep::Menuconfig.label());
pb.inc(1);
use std::process::{Command, Stdio};
let status = pb.suspend(|| {
Command::new(MAKE_COMMAND)
.arg("menuconfig")
.current_dir(&version_entry.path)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.map_err(|e| BuilderErr::CommandError(format!("Failed to run menuconfig: {e}")))
})?;
if !status.success() {
return Err(BuilderErr::MenuconfigError);
}
if !Self::confirm_prompt("Continue build process?")? {
pb.finish_with_message("Cancelled by user");
return Ok(());
}
}
if !cli.no_build {
pb.set_message(BuildStep::BuildKernel.label());
pb.inc(1);
self.build_kernel(&version_entry, cli.replace, &mut pb)?;
}
if !cli.no_modules && Self::confirm_prompt("Do you want to install kernel modules?")? {
pb.set_message(BuildStep::InstallModules.label());
pb.inc(1);
self.install_modules(&version_entry, &mut pb)?;
}
#[cfg(feature = "dracut")]
if !cli.no_initramfs
&& Self::confirm_prompt("Do you want to generate initramfs with dracut?")?
{
pb.set_message(BuildStep::GenerateInitramfs.label());
pb.inc(1);
self.generate_initramfs(&version_entry, cli.replace, &mut pb)?;
}
if let Some(keep_count) = self.config.cleanup_keep_count {
self.cleanup_old_kernels(keep_count, &mut pb)?;
}
pb.finish_with_message("Build complete!");
Ok(())
}
fn cleanup_old_kernels(&self, keep_count: u32, pb: &mut ProgressBar) -> Result<(), BuilderErr> {
let current_kernel = self.boot_manager.get_current_kernel();
let current_version = current_kernel
.as_ref()
.and_then(|p| self.versions.iter().find(|v| v.path == *p));
let mut to_keep: Vec<&VersionEntry> = Vec::new();
if let Some(current) = current_version {
to_keep.push(current);
}
for v in &self.versions {
if to_keep.len() >= keep_count as usize {
break;
}
if !to_keep.contains(&v) {
to_keep.push(v);
}
}
let to_delete: Vec<&VersionEntry> = self
.versions
.iter()
.filter(|v| !to_keep.contains(v))
.collect();
if to_delete.is_empty() {
info!("No old kernels to clean up");
return Ok(());
}
pb.set_message(BuildStep::CleanupKernel.label());
for v in &to_delete {
info!("Cleaning up old kernel: {}", v.version_string);
self.boot_manager.remove_kernel(&v.path)?;
}
pb.inc(1);
info!("Cleaned up {} old kernel(s)", to_delete.len());
Ok(())
}
fn build_kernel(
&self,
version_entry: &VersionEntry,
replace: bool,
pb: &mut ProgressBar,
) -> Result<(), BuilderErr> {
pb.set_message(BuildStep::ConfigUpdate.label());
if self
.boot_manager
.check_new_config_options(&version_entry.path)?
{
tracing::debug!("New config options detected, running olddefconfig");
self.boot_manager.run_olddefconfig(&version_entry.path)?;
}
pb.inc(1);
let threads = std::thread::available_parallelism()
.unwrap_or(NonZeroUsize::new(1).unwrap())
.get();
pb.set_message(BuildStep::BuildKernel.label());
let output = self
.boot_manager
.build_kernel(&version_entry.path, threads)?;
let stdout = String::from_utf8_lossy(&output.stdout);
tracing::debug!("Build output: {}", stdout.lines().last().unwrap_or(""));
pb.inc(1);
pb.set_message(BuildStep::InstallKernel.label());
self.boot_manager
.install_kernel(&version_entry.path, replace)?;
pb.inc(1);
Ok(())
}
fn install_modules(
&self,
version_entry: &VersionEntry,
pb: &mut ProgressBar,
) -> Result<(), BuilderErr> {
pb.set_message(BuildStep::InstallModules.label());
self.boot_manager.install_modules(&version_entry.path)?;
pb.inc(1);
Ok(())
}
#[cfg(feature = "dracut")]
fn generate_initramfs(
&self,
version_entry: &VersionEntry,
replace: bool,
pb: &mut ProgressBar,
) -> Result<(), BuilderErr> {
pb.set_message(BuildStep::GenerateInitramfs.label());
let output = self
.boot_manager
.generate_initramfs(version_entry, replace)?;
let stdout = String::from_utf8_lossy(&output.stdout);
tracing::debug!("Initramfs output: {}", stdout.lines().last().unwrap_or(""));
pb.inc(1);
Ok(())
}
fn prompt_for_kernel_version(&self) -> Option<VersionEntry> {
if self.versions.is_empty() {
tracing::debug!(
"No kernel versions found in {}",
self.config.kernel_src.display()
);
return None;
}
let version_strings: Vec<&str> = self
.versions
.iter()
.map(|v| v.version_string.as_str())
.collect();
Select::with_theme(&ColorfulTheme::default())
.with_prompt("Pick version to build and install")
.items(&version_strings)
.default(0)
.interact_on_opt(&Term::stderr())
.ok()
.flatten()
.map(|idx| self.versions[idx].clone())
}
fn confirm_prompt(message: &str) -> Result<bool, BuilderErr> {
Confirm::new()
.with_prompt(message)
.interact()
.map_err(BuilderErr::PromptError)
}
}