use anyhow::{bail, Context, Result};
use chrono::NaiveDate;
use regex::Regex;
use serde::Deserialize;
use std::collections::HashSet;
use std::fs;
use std::io::Write;
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<NaiveDate>,
pub eol: Option<NaiveDate>,
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();
let re = Regex::new(r"linux-image-(\d+\.\d+\.\d+)-\d+-generic").unwrap();
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;
}
re.captures(pkg).map(|c| c[1].to_string())
})
.collect()
}
pub fn fetch_releases() -> Result<Vec<VanillaRelease>> {
let resp = ureq::get("https://www.kernel.org/releases.json")
.call()
.context("failed to reach kernel.org")?;
let json: KernelReleasesJson = resp
.into_json()
.context("failed to parse kernel.org JSON")?;
let installed = dpkg_installed_vanilla_versions();
let releases: Vec<VanillaRelease> = json
.releases
.into_iter()
.map(|r| {
let released = r
.released
.as_ref()
.and_then(|d| NaiveDate::parse_from_str(&d.isodate, "%Y-%m-%d").ok());
let eol = r.eol.as_ref().and_then(|v| {
v.as_str()
.and_then(|s| NaiveDate::parse_from_str(s, "%Y-%m-%d").ok())
});
let base_version = r.version.split('-').next().unwrap_or(&r.version).to_string();
let is_installed = installed.contains(&base_version);
VanillaRelease {
version: r.version.clone(),
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 resp = ureq::get(url)
.call()
.with_context(|| format!("failed to download {}", url))?;
let total = resp
.header("Content-Length")
.and_then(|v| v.parse::<u64>().ok());
let mut file = fs::File::create(dest)?;
let mut reader = resp.into_reader();
let mut buf = [0u8; 65536];
let mut downloaded: u64 = 0;
let mut last_reported = 0u64;
loop {
let n = reader.read(&mut buf)?;
if n == 0 {
break;
}
file.write_all(&buf[..n])?;
downloaded += n as u64;
if let Some(total) = total {
let pct = downloaded * 100 / total;
if pct >= last_reported + 10 {
last_reported = pct;
let _ = tx.send(format!(
" {}% ({} / {} bytes)",
pct, downloaded, total
));
}
}
}
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 body = ureq::get(&base_url)
.call()
.with_context(|| format!("failed to fetch {}", base_url))?
.into_string()?;
let re = Regex::new(r#"href="([^"]*\.deb)""#).unwrap();
let mut urls: Vec<String> = re
.captures_iter(&body)
.map(|c| {
let href = &c[1];
if href.starts_with("http") {
href.to_string()
} else {
format!("{}{}", base_url, href)
}
})
.filter(|url| {
let fname = url.split('/').last().unwrap_or("");
!fname.contains("_all.deb") || fname.contains("linux-headers-")
})
.collect();
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(())
}
use std::io::Read;