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 #[serde(rename = "kernel")]
22 pub kernel_file_path: PathBuf,
23 #[serde(rename = "initramfs")]
25 pub initramfs_file_path: Option<PathBuf>,
26 #[serde(rename = "kernel-config")]
28 pub kernel_config_file_path: PathBuf,
29 #[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 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 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 let mut make_oldconfig = Command::new("make")
190 .arg("olddefconfig")
191 .current_dir(path)
192 .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 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 std::fs::copy(
212 path.join(".config"),
213 self.config.kernel_config_file_path.clone(),
214 )
215 .map_err(BuilderErr::KernelBuildFail)?;
216
217 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}