use anyhow::Result;
use regex::Regex;
use std::collections::{HashMap, HashSet};
use std::process::Command;
use std::sync::OnceLock;
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,
}
static PKG_RE: OnceLock<Regex> = OnceLock::new();
static AVAIL_RE: OnceLock<Regex> = OnceLock::new();
fn pkg_re() -> &'static Regex {
PKG_RE.get_or_init(|| {
Regex::new(r"^linux-image-(\d+\.\d+[^\s]+-amd64)\s").unwrap()
})
}
fn avail_re() -> &'static Regex {
AVAIL_RE.get_or_init(|| {
Regex::new(r"^linux-image-(\d+\.\d+[^\s]+-amd64)\s").unwrap()
})
}
pub fn running_kernel() -> String {
std::process::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;
}
if let Some(caps) = avail_re().captures(line) {
let version = caps[1].to_string();
let pkg_name = format!("linux-image-{}", version);
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, 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(caps) = pkg_re().captures(&format!("{} ", pkg)) {
versions.insert(caps[1].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);
let installed = installed_images.contains(&image_pkg);
let headers_installed = installed_headers.contains(&headers_pkg);
let is_running = version == running;
let available_in_apt = available.contains_key(&version);
let apt_version = available.get(&version).cloned();
let is_held = held.contains(&image_pkg);
KernelEntry {
version,
image_package: image_pkg,
headers_package: headers_pkg,
installed,
headers_installed,
is_running,
available_in_apt,
apt_version,
is_held,
}
})
.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)
}