use crate::consts::KernelPaths;
use crate::consts::{CONFIG_FILENAME, KERNEL_IMAGE_PATH, MAKE_COMMAND};
use crate::discovery::VersionEntry;
use crate::error::BuilderErr;
use std::path::{Path, PathBuf};
use std::process::Output;
use tracing::info;
#[cfg(feature = "dracut")]
use crate::consts::DRACUT_COMMAND;
#[derive(Debug)]
pub struct BootManager {
paths: KernelPaths,
keep_last_kernel: bool,
last_kernel_suffix: String,
}
impl BootManager {
#[must_use]
pub fn new(
paths: KernelPaths,
keep_last_kernel: bool,
last_kernel_suffix: Option<String>,
) -> Self {
Self {
paths,
keep_last_kernel,
last_kernel_suffix: last_kernel_suffix.unwrap_or_else(|| "prev".to_string()),
}
}
pub fn link_kernel_config(&self, kernel_path: &Path) -> Result<(), BuilderErr> {
let link = kernel_path.join(CONFIG_FILENAME);
let dot_config = &self.paths.kernel_config;
if link.exists() {
if link.is_symlink() {
std::fs::remove_file(&link)
.map_err(|e| BuilderErr::linking_file_error(e, link.clone()))?;
} else {
let mut old_file = link.clone();
old_file.set_file_name(format!("{CONFIG_FILENAME}.old"));
std::fs::copy(&link, &old_file)
.map_err(|e| BuilderErr::linking_file_error(e, link.clone()))?;
}
}
std::os::unix::fs::symlink(dot_config, &link)
.map_err(|e| BuilderErr::linking_file_error(e, link.clone()))?;
Ok(())
}
pub fn update_linux_symlink(&self, version_entry: &VersionEntry) -> Result<(), BuilderErr> {
let linux = &self.paths.linux_symlink;
if linux.exists() || linux.is_symlink() {
if let Ok(target) = linux.read_link() {
if target == version_entry.path {
info!(
"Linux symlink already points to {}",
version_entry.version_string
);
return Ok(());
}
}
std::fs::remove_file(linux)
.map_err(|e| BuilderErr::linking_file_error(e, linux.clone()))?;
}
std::os::unix::fs::symlink(&version_entry.path, linux)
.map_err(|e| BuilderErr::linking_file_error(e, linux.clone()))?;
info!("Updated linux symlink to {}", version_entry.version_string);
Ok(())
}
pub fn check_new_config_options(&self, kernel_path: &Path) -> Result<bool, BuilderErr> {
let output = duct::cmd(MAKE_COMMAND, &["listnewconfig"])
.dir(kernel_path)
.stdout_capture()
.run()
.map_err(|e| BuilderErr::CommandError(e.to_string()))?;
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(!stdout.trim().is_empty())
}
pub fn run_olddefconfig(&self, kernel_path: &Path) -> Result<(), BuilderErr> {
info!("Running make olddefconfig to integrate new kernel options");
let output = duct::cmd(MAKE_COMMAND, &["olddefconfig"])
.dir(kernel_path)
.stdout_capture()
.stderr_capture()
.run()
.map_err(|e| {
BuilderErr::CommandError(format!("Failed to run make olddefconfig: {e}"))
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(BuilderErr::kernel_config_update_error(stderr.to_string()));
}
let mut old_config = self.paths.kernel_config.clone();
old_config.pop();
old_config.push(format!("{CONFIG_FILENAME}.old"));
std::fs::copy(&self.paths.kernel_config, &old_config)
.map_err(|e| BuilderErr::CommandError(e.to_string()))?;
std::fs::copy(kernel_path.join(CONFIG_FILENAME), &self.paths.kernel_config)
.map_err(|e| BuilderErr::CommandError(e.to_string()))?;
std::fs::remove_file(kernel_path.join(format!("{CONFIG_FILENAME}.old"))).ok();
let dot_config = kernel_path.join(CONFIG_FILENAME);
std::fs::remove_file(&dot_config).ok();
std::os::unix::fs::symlink(&self.paths.kernel_config, &dot_config)
.map_err(|e| BuilderErr::linking_file_error(e, dot_config))?;
Ok(())
}
pub fn build_kernel(&self, kernel_path: &Path, threads: usize) -> Result<Output, BuilderErr> {
info!("Building kernel with {threads} threads");
let output = duct::cmd(MAKE_COMMAND, &["-j".to_string(), threads.to_string()])
.dir(kernel_path)
.stdout_capture()
.stderr_capture()
.run()
.map_err(|e| BuilderErr::CommandError(format!("Kernel build failed: {e}")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(BuilderErr::KernelBuildFail(stderr.to_string()));
}
Ok(output)
}
pub fn install_kernel(&self, kernel_path: &Path, replace: bool) -> Result<(), BuilderErr> {
let kernel_image = kernel_path.join(KERNEL_IMAGE_PATH);
if !kernel_image.exists() {
return Err(BuilderErr::KernelBuildFail(format!(
"Kernel image not found at {}",
kernel_image.display()
)));
}
if self.keep_last_kernel && !replace && self.paths.kernel_image.exists() {
let backup_path = self.paths.backup_path(
&self.paths.kernel_image,
&format!("-{}", self.last_kernel_suffix),
);
info!("Backing up current kernel to {}", backup_path.display());
std::fs::copy(&self.paths.kernel_image, &backup_path)
.map_err(|e| BuilderErr::CommandError(e.to_string()))?;
}
info!("Installing kernel to {}", self.paths.kernel_image.display());
std::fs::copy(&kernel_image, &self.paths.kernel_image)
.map_err(|e| BuilderErr::CommandError(e.to_string()))?;
Ok(())
}
pub fn install_modules(&self, kernel_path: &Path) -> Result<Output, BuilderErr> {
info!("Installing kernel modules");
let output = duct::cmd(MAKE_COMMAND, &["modules_install"])
.dir(kernel_path)
.stdout_capture()
.stderr_capture()
.run()
.map_err(|e| BuilderErr::CommandError(format!("Failed to install modules: {e}")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(BuilderErr::KernelBuildFail(stderr.to_string()));
}
Ok(output)
}
#[cfg(feature = "dracut")]
pub fn generate_initramfs(
&self,
version_entry: &VersionEntry,
replace: bool,
) -> Result<Output, BuilderErr> {
let initramfs_path = self.paths.initramfs.as_ref().ok_or_else(|| {
BuilderErr::KernelConfigMissingOption("initramfs path not configured".to_string())
})?;
if self.keep_last_kernel && !replace && initramfs_path.exists() {
let backup_path = self
.paths
.backup_path(initramfs_path, &format!("-{}.img", self.last_kernel_suffix));
info!("Backing up current initramfs to {}", backup_path.display());
std::fs::copy(initramfs_path, &backup_path)
.map_err(|e| BuilderErr::CommandError(e.to_string()))?;
}
info!("Generating initramfs for {}", version_entry.version_string);
let kver = version_entry
.version_string
.strip_prefix("linux-")
.unwrap_or(&version_entry.version_string);
let output = duct::cmd(
DRACUT_COMMAND,
&[
"--hostonly",
"--kver",
kver,
"--force",
initramfs_path.to_string_lossy().as_ref(),
],
)
.dir(&version_entry.path)
.stdout_capture()
.stderr_capture()
.run()
.map_err(|e| BuilderErr::CommandError(format!("Failed to generate initramfs: {e}")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(BuilderErr::KernelBuildFail(stderr.to_string()));
}
Ok(output)
}
#[must_use]
pub fn get_current_kernel(&self) -> Option<PathBuf> {
if self.paths.linux_symlink.exists() || self.paths.linux_symlink.is_symlink() {
self.paths.linux_symlink.read_link().ok()
} else {
None
}
}
pub fn remove_kernel(&self, kernel_path: &Path) -> Result<(), BuilderErr> {
info!("Removing kernel at {}", kernel_path.display());
std::fs::remove_dir_all(kernel_path)
.map_err(|e| BuilderErr::CommandError(format!("Failed to remove kernel: {e}")))?;
Ok(())
}
}