Skip to main content

kernel_builder/
lib.rs

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