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 #[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)]
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 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 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 let mut make_oldconfig = Command::new("make")
171 .arg("olddefconfig")
172 .current_dir(path)
173 .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 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 std::fs::copy(
193 path.join(".config"),
194 self.config.kernel_config_file_path.clone(),
195 )
196 .map_err(BuilderErr::KernelBuildFail)?;
197
198 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)) .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}