Skip to main content

kernel_builder/
lib.rs

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