use anyhow::{bail, Context, Result};
use serde::Deserialize;
use std::collections::HashSet;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::sync::mpsc;
use super::manager::trigger_grub_update;
#[derive(Debug, Clone, PartialEq)]
pub enum ReleaseType {
Mainline,
Stable,
Longterm,
Eol,
Unknown,
}
impl ReleaseType {
fn from_str(s: &str) -> Self {
match s {
"mainline" => Self::Mainline,
"stable" => Self::Stable,
"longterm" => Self::Longterm,
"EOL" | "eol" => Self::Eol,
_ => Self::Unknown,
}
}
pub fn label(&self) -> &'static str {
match self {
Self::Mainline => "mainline",
Self::Stable => "stable",
Self::Longterm => "longterm",
Self::Eol => "EOL",
Self::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone)]
pub struct VanillaRelease {
pub version: String,
pub release_type: ReleaseType,
pub released: Option<String>,
pub eol: Option<String>,
pub changelog_url: String,
pub installed: bool,
}
#[derive(Deserialize)]
struct KernelReleasesJson {
releases: Vec<KernelReleaseJson>,
}
#[derive(Deserialize)]
struct KernelReleaseJson {
version: String,
moniker: String,
released: Option<KernelDate>,
eol: Option<serde_json::Value>,
changelog: Option<String>,
}
#[derive(Deserialize)]
struct KernelDate {
isodate: String,
}
fn dpkg_installed_vanilla_versions() -> HashSet<String> {
let stdout = Command::new("dpkg-query")
.args(["-f", "${Package}\\t${db:Status-Abbrev}\\n", "-W", "linux-image-*-generic"])
.output()
.map(|o| o.stdout)
.unwrap_or_default();
String::from_utf8_lossy(&stdout)
.lines()
.filter_map(|line| {
let mut parts = line.splitn(2, '\t');
let pkg = parts.next()?;
let status = parts.next().unwrap_or("").trim();
if !status.starts_with("ii") {
return None;
}
let rest = pkg.strip_prefix("linux-image-")?;
let without_generic = rest.strip_suffix("-generic")?;
let ver = if let Some((base, suffix)) = without_generic.rsplit_once('-') {
if suffix.chars().all(|c| c.is_ascii_digit()) { base } else { without_generic }
} else {
without_generic
};
Some(ver.to_string())
})
.collect()
}
pub fn fetch_releases() -> Result<Vec<VanillaRelease>> {
let out = Command::new("curl")
.args(["-fsSL", "--max-time", "15", "https://www.kernel.org/releases.json"])
.output()
.context("curl not found — install curl to fetch kernel releases")?;
if !out.status.success() {
bail!("failed to reach kernel.org");
}
let json: KernelReleasesJson = serde_json::from_slice(&out.stdout)
.context("failed to parse kernel.org JSON")?;
let installed = dpkg_installed_vanilla_versions();
let releases = json.releases.into_iter().map(|r| {
let released = r.released.map(|d| d.isodate);
let eol = r.eol.as_ref().and_then(|v| v.as_str().map(|s| s.to_string()));
let base_version = r.version.split('-').next().unwrap_or(&r.version).to_string();
let is_installed = installed.contains(&base_version);
VanillaRelease {
version: r.version,
release_type: ReleaseType::from_str(&r.moniker),
released,
eol,
changelog_url: r.changelog.unwrap_or_default(),
installed: is_installed,
}
}).collect();
Ok(releases)
}
fn download_file(url: &str, dest: &PathBuf, tx: &mpsc::Sender<String>) -> Result<()> {
let _ = tx.send(format!("Downloading {}", url));
let status = Command::new("curl")
.args(["-fsSL", "--max-time", "120", "-o", dest.to_str().unwrap_or(""), url])
.status()
.context("curl not found — install curl to download kernel packages")?;
if !status.success() {
bail!("failed to download {}", url);
}
let _ = tx.send(format!(" Saved to {}", dest.display()));
Ok(())
}
fn find_deb_urls(version: &str, tx: &mpsc::Sender<String>) -> Result<Vec<String>> {
let base_url = format!("https://kernel.ubuntu.com/mainline/v{}/amd64/", version);
let _ = tx.send(format!("Fetching package list from {}", base_url));
let out = Command::new("curl")
.args(["-fsSL", "--max-time", "15", &base_url])
.output()
.context("curl not found")?;
if !out.status.success() {
bail!("failed to fetch {}", base_url);
}
let body = String::from_utf8_lossy(&out.stdout);
let mut urls: Vec<String> = Vec::new();
let mut remaining: &str = &body;
while let Some(pos) = remaining.find("href=\"") {
remaining = &remaining[pos + 6..];
if let Some(end) = remaining.find('"') {
let href = &remaining[..end];
if href.ends_with(".deb") {
let url = if href.starts_with("http") {
href.to_string()
} else {
format!("{}{}", base_url, href)
};
let fname = url.split('/').last().unwrap_or("");
if !fname.contains("_all.deb") || fname.contains("linux-headers-") {
urls.push(url);
}
}
remaining = &remaining[end + 1..];
}
}
urls.sort();
urls.dedup();
if urls.is_empty() {
bail!("No .deb packages found for kernel {} at {}", version, base_url);
}
Ok(urls)
}
pub fn install_vanilla(version: &str, tx: &mpsc::Sender<String>) -> Result<()> {
let tmp_dir = PathBuf::from(format!("/tmp/rustpm-kernel-{}", version));
fs::create_dir_all(&tmp_dir)?;
let deb_urls = find_deb_urls(version, tx)?;
let mut deb_paths: Vec<PathBuf> = Vec::new();
for url in &deb_urls {
let filename = url.split('/').last().unwrap_or("package.deb");
let dest = tmp_dir.join(filename);
download_file(url, &dest, tx)?;
deb_paths.push(dest);
}
let _ = tx.send("Installing downloaded packages with dpkg...".into());
let path_strs: Vec<&str> = deb_paths.iter().map(|p| p.to_str().unwrap_or("")).collect();
let mut args = vec!["--install"];
args.extend_from_slice(&path_strs);
let status = Command::new("dpkg").args(&args).status()?;
if !status.success() {
bail!("dpkg install failed — run 'sudo apt-get install -f' to fix broken deps");
}
trigger_grub_update(tx)?;
let _ = fs::remove_dir_all(&tmp_dir);
let _ = tx.send(format!("Vanilla kernel {} installed successfully.", version));
Ok(())
}
pub fn remove_vanilla(version: &str, tx: &mpsc::Sender<String>) -> Result<()> {
let _ = tx.send(format!("Removing vanilla kernel {}...", version));
let out = Command::new("dpkg-query")
.args(["-f", "${Package}\\t${db:Status-Abbrev}\\n", "-W", &format!("linux-*{}*", version)])
.output()?;
let pkgs: Vec<String> = String::from_utf8_lossy(&out.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();
if pkgs.is_empty() {
bail!("No installed packages found for vanilla kernel {}", version);
}
let pkg_refs: Vec<&str> = pkgs.iter().map(|s| s.as_str()).collect();
let mut args = vec!["remove", "-y"];
args.extend_from_slice(&pkg_refs);
let status = Command::new("apt-get").args(&args).status()?;
if !status.success() {
bail!("Failed to remove vanilla kernel {}", version);
}
trigger_grub_update(tx)?;
Ok(())
}