kernel_builder/
lib.rs

1use dialoguer::theme::ColorfulTheme;
2use dialoguer::Select;
3use dialoguer::{console::Term, Confirm};
4use indicatif::ProgressBar;
5use serde::Deserialize;
6use std::io::{BufRead, BufReader};
7use std::num::NonZeroUsize;
8use std::{
9    os::unix,
10    path::{Path, PathBuf},
11    process::{Command, Stdio},
12    time::Duration,
13};
14
15mod error;
16pub use error::BuilderErr;
17mod cli;
18pub use cli::Args;
19
20#[derive(Debug, Deserialize)]
21pub struct KBConfig {
22    /// Path to the kernel bz image on the boot partition
23    #[serde(rename = "kernel")]
24    pub kernel_file_path: PathBuf,
25    /// Path to the initramfs on the boot partition
26    #[serde(rename = "initramfs")]
27    pub initramfs_file_path: Option<PathBuf>,
28    /// path to the `.config` file that will be symlinked
29    #[serde(rename = "kernel-config")]
30    pub kernel_config_file_path: PathBuf,
31    /// path to the kernel sources
32    #[serde(rename = "kernel-src")]
33    pub kernel_src: PathBuf,
34    #[serde(rename = "keep-last-kernel")]
35    pub keep_last_kernel: bool,
36    #[serde(rename = "last-kernel-suffix")]
37    pub last_kernel_suffix: Option<String>,
38}
39
40#[derive(Clone, Debug)]
41struct VersionEntry {
42    path: PathBuf,
43    version_string: String,
44}
45
46#[derive(Debug)]
47pub struct KernelBuilder {
48    config: KBConfig,
49    versions: Vec<VersionEntry>,
50}
51
52impl KernelBuilder {
53    pub const LINUX_PATH: &'static str = "/usr/src";
54
55    #[must_use]
56    pub fn new(config: KBConfig) -> Self {
57        let mut builder = Self {
58            config,
59            versions: vec![],
60        };
61        builder.get_available_version();
62
63        builder
64    }
65
66    fn get_available_version(&mut self) {
67        if self.versions.is_empty() {
68            if let Ok(directories) = std::fs::read_dir(&self.config.kernel_src) {
69                self.versions = directories
70                    .filter_map(|dir| dir.ok().map(|d| d.path()))
71                    .filter(|path| path.starts_with(&self.config.kernel_src) && !path.is_symlink())
72                    .filter_map(|path| {
73                        path.strip_prefix(&self.config.kernel_src)
74                            .ok()
75                            .and_then(|p| {
76                                let tmp = p.to_owned();
77                                let version_string = tmp.to_string_lossy();
78                                version_string
79                                    .starts_with("linux-")
80                                    .then_some(VersionEntry {
81                                        path: path.clone(),
82                                        version_string: version_string.to_string(),
83                                    })
84                            })
85                    })
86                    .collect::<Vec<_>>();
87            }
88        }
89    }
90
91    ///
92    /// # Errors
93    ///
94    /// - Error on missing kernel config
95    /// - Failing creating symlinks
96    /// - Failing kernel build
97    ///
98    /// if selected:
99    /// - Failing installing kernel modules
100    /// - Failing generating initramfs
101    pub fn build(&self, cli: &Args) -> Result<(), BuilderErr> {
102        let Some(version_entry) = self.prompt_for_kernel_version() else {
103            return Ok(());
104        };
105
106        let VersionEntry {
107            path,
108            version_string,
109        } = &version_entry;
110
111        // create symlink from /usr/src/.config
112        let link = path.join(".config");
113        if !link.exists() {
114            let dot_config = &self.config.kernel_config_file_path;
115            if !dot_config.exists() || !dot_config.is_file() {
116                return Err(BuilderErr::KernelConfigMissing);
117            }
118
119            unix::fs::symlink(dot_config, link).map_err(BuilderErr::LinkingFileError)?;
120        }
121
122        let linux = PathBuf::from(&self.config.kernel_src).join("linux");
123        let linux_target = linux.read_link().map_err(BuilderErr::LinkingFileError)?;
124
125        if linux_target.to_string_lossy() != *version_string {
126            std::fs::remove_file(&linux).map_err(BuilderErr::LinkingFileError)?;
127            unix::fs::symlink(path, linux).map_err(BuilderErr::LinkingFileError)?;
128        }
129
130        if cli.menuconfig {
131            Self::make_menuconfig(path)?;
132            if !Self::confirm_prompt("Continue build process?")? {
133                return Ok(());
134            }
135        }
136
137        if !cli.no_build {
138            self.build_kernel(path, cli.replace)?;
139        }
140
141        if !cli.no_modules && Self::confirm_prompt("Do you want to install kernel modules?")? {
142            Self::install_kernel_modules(path)?;
143        }
144
145        #[cfg(feature = "dracut")]
146        if !cli.no_initramfs
147            && Self::confirm_prompt("Do you want to generate initramfs with dracut?")?
148        {
149            self.generate_initramfs(&version_entry, cli.replace)?;
150        }
151        Ok(())
152    }
153
154    fn build_kernel(&self, path: &Path, replace: bool) -> Result<(), BuilderErr> {
155        let new_flags = Command::new("make")
156            .arg("listnewconfigs")
157            .current_dir(path)
158            .output()
159            .map_err(BuilderErr::KernelBuildFail)?;
160
161        if !new_flags.stdout.is_empty() {
162            let make_oldconfig = Command::new("make")
163                .arg("oldconfig")
164                .current_dir(path)
165                .stdin(Stdio::inherit()) // Allow interaction with the terminal for input
166                .stdout(Stdio::inherit())
167                .stderr(Stdio::piped())
168                .spawn()
169                .map_err(BuilderErr::KernelBuildFail)?;
170
171            if let Some(stderr) = make_oldconfig.stderr {
172                let reader = BufReader::new(stderr);
173                for line in reader.lines() {
174                    let line = line.expect("Failed to read error line");
175                    eprintln!("{line}");
176                }
177            }
178        }
179
180        let threads: NonZeroUsize =
181            std::thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap());
182        let pb = ProgressBar::new_spinner();
183        pb.enable_steady_tick(Duration::from_millis(120));
184        let mut cmd = Command::new("make")
185            .current_dir(path)
186            .args(["-j", &threads.to_string()])
187            .stdout(Stdio::piped())
188            .stdin(Stdio::piped())
189            .spawn()
190            .map_err(BuilderErr::KernelBuildFail)?;
191
192        {
193            let stdout = cmd.stdout.as_mut().unwrap();
194            let stdout_reader = BufReader::new(stdout);
195            let stdout_lines = stdout_reader.lines();
196
197            for line in stdout_lines {
198                let line = line
199                    .map_err(BuilderErr::KernelBuildFail)?
200                    .to_ascii_lowercase();
201                pb.set_message(format!("Compiling kernel: {line}"));
202            }
203        }
204
205        cmd.wait().map_err(BuilderErr::KernelBuildFail)?;
206
207        pb.finish_with_message("Finished compiling Kernel");
208
209        if self.config.keep_last_kernel && !replace {
210            let path = self.config.kernel_file_path.clone();
211            let mut filename = path
212                .file_name()
213                .map(|p| p.to_string_lossy().to_string())
214                .expect("could not get filename of kernel file path");
215            let suff = format!(
216                "-{}",
217                self.config
218                    .last_kernel_suffix
219                    .clone()
220                    .unwrap_or(String::from("prev"))
221            );
222            filename.push_str(&suff);
223            let path = path.with_file_name(filename);
224
225            std::fs::copy(self.config.kernel_file_path.clone(), path)
226                .map_err(BuilderErr::KernelBuildFail)?;
227        }
228
229        std::fs::copy(
230            path.join("arch/x86/boot/bzImage"),
231            self.config.kernel_file_path.clone(),
232        )
233        .map_err(BuilderErr::KernelBuildFail)?;
234
235        Ok(())
236    }
237
238    fn make_menuconfig(path: &Path) -> Result<(), BuilderErr> {
239        let mut cmd = Command::new("make")
240            .current_dir(path)
241            .arg("menuconfig")
242            .spawn()
243            .map_err(|_| BuilderErr::MenuconfigError)?;
244
245        cmd.wait().map_err(|_| BuilderErr::MenuconfigError)?;
246
247        Ok(())
248    }
249
250    fn install_kernel_modules(path: &Path) -> Result<(), BuilderErr> {
251        let pb = ProgressBar::new_spinner();
252        pb.enable_steady_tick(Duration::from_millis(120));
253        pb.set_message("Install kernel modules");
254        Command::new("make")
255            .current_dir(path)
256            .arg("modules_install")
257            .stdout(Stdio::null())
258            .stderr(Stdio::null())
259            .spawn()
260            .map_err(BuilderErr::KernelBuildFail)?
261            .wait()
262            .map_err(BuilderErr::KernelBuildFail)?;
263        pb.finish_with_message("Finished installing modules");
264
265        Ok(())
266    }
267
268    #[cfg(feature = "dracut")]
269    fn generate_initramfs(
270        &self,
271        VersionEntry {
272            path,
273            version_string,
274        }: &VersionEntry,
275        replace: bool,
276    ) -> Result<(), BuilderErr> {
277        let initramfs_file_path = &self
278            .config
279            .initramfs_file_path
280            .clone()
281            .ok_or(BuilderErr::KernelConfigMissingOption("initramfs".into()))?;
282
283        if self.config.keep_last_kernel && !replace {
284            let mut filename = initramfs_file_path
285                .file_stem()
286                .map(|p| p.to_string_lossy().to_string())
287                .expect("could not get filename of initramfs file path");
288            let suff = format!(
289                "-{}.img",
290                self.config
291                    .last_kernel_suffix
292                    .clone()
293                    .unwrap_or(String::from("prev"))
294            );
295            filename.push_str(&suff);
296            let path = initramfs_file_path.with_file_name(filename);
297
298            std::fs::copy(initramfs_file_path, path).map_err(BuilderErr::KernelBuildFail)?;
299        }
300
301        let pb = ProgressBar::new_spinner();
302        pb.enable_steady_tick(Duration::from_millis(120));
303        let mut cmd = Command::new("dracut")
304            .current_dir(path)
305            .args([
306                "--hostonly",
307                "--kver",
308                version_string.strip_prefix("linux-").unwrap(),
309                "--force",
310                initramfs_file_path.to_string_lossy().as_ref(),
311            ])
312            .stdout(Stdio::piped())
313            .stderr(Stdio::null())
314            .spawn()
315            .map_err(BuilderErr::KernelBuildFail)?;
316
317        {
318            let stdout = cmd.stdout.as_mut().unwrap();
319            let stdout_reader = BufReader::new(stdout);
320            let stdout_lines = stdout_reader.lines();
321
322            for line in stdout_lines {
323                pb.set_message(format!(
324                    "Generating initramfs: {}",
325                    line.map_err(BuilderErr::KernelBuildFail)?
326                ));
327            }
328        }
329
330        cmd.wait().map_err(BuilderErr::KernelBuildFail)?;
331        pb.finish_with_message("Finished initramfs");
332
333        Ok(())
334    }
335
336    fn prompt_for_kernel_version(&self) -> Option<VersionEntry> {
337        let versions = self
338            .versions
339            .clone()
340            .into_iter()
341            .map(|v| v.version_string)
342            .collect::<Vec<_>>();
343
344        Select::with_theme(&ColorfulTheme::default())
345            .with_prompt("Pick version to build and install")
346            .items(versions.as_slice())
347            .default(versions.len().saturating_sub(1)) // select the last entry
348            .interact_on_opt(&Term::stderr())
349            .ok()
350            .flatten()
351            .map(|selection| self.versions[selection].clone())
352    }
353
354    fn confirm_prompt(message: &str) -> Result<bool, BuilderErr> {
355        Confirm::new()
356            .with_prompt(message)
357            .interact()
358            .map_err(BuilderErr::PromptError)
359    }
360}