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 #[serde(rename = "kernel")]
24 pub kernel_file_path: PathBuf,
25 #[serde(rename = "initramfs")]
27 pub initramfs_file_path: Option<PathBuf>,
28 #[serde(rename = "kernel-config")]
30 pub kernel_config_file_path: PathBuf,
31 #[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 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 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()) .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)) .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}