kernel-builder 0.7.6

Select, build and install kernel version from local sources.
Documentation
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()),
        }
    }

    /// Link kernel config file to kernel source directory.
    ///
    /// # Errors
    ///
    /// Returns an error if file operations fail.
    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(())
    }

    /// Update the /usr/src/linux symlink to point to the selected kernel.
    ///
    /// # Errors
    ///
    /// Returns an error if symlink operations fail.
    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(())
    }

    /// Check if there are new kernel config options.
    ///
    /// # Errors
    ///
    /// Returns an error if the command fails.
    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())
    }

    /// Run make olddefconfig to integrate new kernel config options.
    ///
    /// # Errors
    ///
    /// Returns an error if olddefconfig fails.
    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(())
    }

    /// Build the kernel with the specified number of threads.
    ///
    /// # Errors
    ///
    /// Returns an error if the build fails.
    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)
    }

    /// Install the compiled kernel to the boot partition.
    ///
    /// # Errors
    ///
    /// Returns an error if file operations fail.
    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(())
    }

    /// Install kernel modules.
    ///
    /// # Errors
    ///
    /// Returns an error if `modules_install` fails.
    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")]
    /// Generate initramfs using dracut.
    ///
    /// # Errors
    ///
    /// Returns an error if dracut fails.
    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)
    }

    /// Get the current kernel symlink target.
    #[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
        }
    }

    /// Remove a kernel directory and its build artifacts.
    ///
    /// # Errors
    ///
    /// Returns an error if directory removal fails.
    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(())
    }
}