use anyhow::Result;
use std::collections::{HashMap, HashSet};
use std::process::Command;
fn run_stdout(program: &str, args: &[&str]) -> Vec<u8> {
Command::new(program).args(args).output()
.map(|o| o.stdout).unwrap_or_default()
}
#[derive(Debug, Clone)]
pub struct KernelEntry {
pub version: String,
pub image_package: String,
pub headers_package: String,
pub installed: bool,
pub headers_installed: bool,
pub is_running: bool,
pub available_in_apt: bool,
pub apt_version: Option<String>,
pub is_held: bool,
}
fn kernel_version_from_pkg(pkg: &str) -> Option<&str> {
let rest = pkg.strip_prefix("linux-image-")?;
if rest.ends_with("-amd64") && rest.starts_with(|c: char| c.is_ascii_digit()) {
Some(rest)
} else {
None
}
}
pub fn running_kernel() -> String {
Command::new("uname").arg("-r").output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default()
}
fn dpkg_installed_packages(glob: &str) -> HashSet<String> {
let stdout = run_stdout("dpkg-query", &["-f", "${Package}\\t${db:Status-Abbrev}\\n", "-W", glob]);
String::from_utf8_lossy(&stdout).lines().filter_map(|line| {
let mut parts = line.splitn(2, '\t');
let pkg = parts.next()?.to_string();
let status = parts.next().unwrap_or("").trim();
if status.starts_with("ii") { Some(pkg) } else { None }
}).collect()
}
fn apt_held_packages() -> HashSet<String> {
let stdout = run_stdout("apt-mark", &["showhold"]);
String::from_utf8_lossy(&stdout).lines().map(|l| l.to_string()).collect()
}
fn apt_available_kernels() -> HashMap<String, String> {
let raw = run_stdout("apt-cache", &["search", "linux-image-"]);
let stdout = String::from_utf8_lossy(&raw).into_owned();
let mut map = HashMap::new();
for line in stdout.lines() {
if line.contains("-dbg ") || line.contains("-unsigned ")
|| line.contains("-rt-") || line.contains("-cloud-") {
continue;
}
let pkg_name = line.split_ascii_whitespace().next().unwrap_or("");
if let Some(version) = kernel_version_from_pkg(pkg_name) {
let policy_raw = run_stdout("apt-cache", &["policy", pkg_name]);
let policy_str = String::from_utf8_lossy(&policy_raw).into_owned();
let apt_ver = policy_str.lines().find_map(|l| {
l.trim().strip_prefix("Candidate: ").map(|v| v.to_string())
});
map.insert(version.to_string(), apt_ver.unwrap_or_default());
}
}
map
}
pub fn detect_kernels() -> Result<Vec<KernelEntry>> {
let running = running_kernel();
let installed_images = dpkg_installed_packages("linux-image-*");
let installed_headers = dpkg_installed_packages("linux-headers-*");
let held = apt_held_packages();
let available = apt_available_kernels();
let mut versions: HashSet<String> = HashSet::new();
for pkg in &installed_images {
if let Some(ver) = kernel_version_from_pkg(pkg) {
versions.insert(ver.to_string());
}
}
for ver in available.keys() {
versions.insert(ver.clone());
}
let mut entries: Vec<KernelEntry> = versions.into_iter().map(|version| {
let image_pkg = format!("linux-image-{}", version);
let headers_pkg = format!("linux-headers-{}", version);
KernelEntry {
installed: installed_images.contains(&image_pkg),
headers_installed: installed_headers.contains(&headers_pkg),
is_running: version == running,
available_in_apt: available.contains_key(&version),
apt_version: available.get(&version).cloned(),
is_held: held.contains(&image_pkg),
image_package: image_pkg,
headers_package: headers_pkg,
version,
}
}).collect();
entries.sort_by(|a, b| {
b.is_running.cmp(&a.is_running)
.then(b.installed.cmp(&a.installed))
.then(b.version.cmp(&a.version))
});
Ok(entries)
}