use super::Distro;
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use tracing::{debug, warn};
pub struct DistroDetector;
impl DistroDetector {
pub fn detect() -> Result<Distro> {
if let Ok(distro) = Self::detect_from_os_release() {
debug!("Detected distribution from os-release: {:?}", distro);
return Ok(distro);
}
if let Ok(distro) = Self::detect_from_lsb_release() {
debug!("Detected distribution from lsb-release: {:?}", distro);
return Ok(distro);
}
if let Ok(distro) = Self::detect_from_specific_files() {
debug!("Detected distribution from specific files: {:?}", distro);
return Ok(distro);
}
if let Ok(distro) = Self::detect_from_package_manager() {
debug!("Detected distribution from package manager: {:?}", distro);
return Ok(distro);
}
warn!("Could not detect distribution, using Unknown");
Ok(Distro::Unknown)
}
fn detect_from_os_release() -> Result<Distro> {
let content = fs::read_to_string("/etc/os-release")
.context("Failed to read /etc/os-release")?;
let vars = Self::parse_shell_vars(&content);
let id = vars.get("ID").map(|s| s.as_str()).unwrap_or("");
let id_like = vars.get("ID_LIKE").map(|s| s.as_str()).unwrap_or("");
debug!("os-release ID: {}, ID_LIKE: {}", id, id_like);
let distro = Self::map_id_to_distro(id);
if distro != Distro::Unknown {
return Ok(distro);
}
for like_id in id_like.split_whitespace() {
let distro = Self::map_id_to_distro(like_id);
if distro != Distro::Unknown {
return Ok(distro);
}
}
anyhow::bail!("Unknown distribution in os-release: {}", id)
}
fn detect_from_lsb_release() -> Result<Distro> {
let content = fs::read_to_string("/etc/lsb-release")
.context("Failed to read /etc/lsb-release")?;
let vars = Self::parse_shell_vars(&content);
let distro_id = vars
.get("DISTRIB_ID")
.map(|s| s.to_lowercase())
.unwrap_or_default();
debug!("lsb-release DISTRIB_ID: {}", distro_id);
let distro = Self::map_id_to_distro(&distro_id);
if distro != Distro::Unknown {
Ok(distro)
} else {
anyhow::bail!("Unknown distribution in lsb-release: {}", distro_id)
}
}
fn detect_from_specific_files() -> Result<Distro> {
if Path::new("/etc/debian_version").exists() {
if Path::new("/etc/lsb-release").exists() {
let content = fs::read_to_string("/etc/lsb-release").ok();
if let Some(content) = content {
if content.contains("Ubuntu") {
return Ok(Distro::Ubuntu);
}
}
}
return Ok(Distro::Debian);
}
if Path::new("/etc/fedora-release").exists() {
return Ok(Distro::Fedora);
}
if Path::new("/etc/redhat-release").exists() {
let content = fs::read_to_string("/etc/redhat-release").ok();
if let Some(content) = content {
let lower = content.to_lowercase();
if lower.contains("fedora") {
return Ok(Distro::Fedora);
} else if lower.contains("red hat") || lower.contains("rhel") {
return Ok(Distro::RHEL);
}
}
return Ok(Distro::RHEL);
}
if Path::new("/etc/arch-release").exists() {
return Ok(Distro::Arch);
}
if Path::new("/etc/manjaro-release").exists() {
return Ok(Distro::Manjaro);
}
if Path::new("/etc/SuSE-release").exists() || Path::new("/etc/SUSE-brand").exists() {
return Ok(Distro::OpenSUSE);
}
anyhow::bail!("No distribution-specific files found")
}
fn detect_from_package_manager() -> Result<Distro> {
if Self::command_exists("apt") || Self::command_exists("apt-get") {
return Ok(Distro::Debian);
}
if Self::command_exists("dnf") {
return Ok(Distro::Fedora);
}
if Self::command_exists("yum") {
return Ok(Distro::RHEL);
}
if Self::command_exists("pacman") {
return Ok(Distro::Arch);
}
if Self::command_exists("zypper") {
return Ok(Distro::OpenSUSE);
}
anyhow::bail!("No known package manager found")
}
fn map_id_to_distro(id: &str) -> Distro {
let id_lower = id.to_lowercase();
match id_lower.as_str() {
"debian" => Distro::Debian,
"ubuntu" => Distro::Ubuntu,
"fedora" => Distro::Fedora,
"rhel" | "redhat" | "red hat" => Distro::RHEL,
"centos" => Distro::RHEL, "rocky" | "rockylinux" => Distro::RHEL, "alma" | "almalinux" => Distro::RHEL, "arch" => Distro::Arch,
"manjaro" => Distro::Manjaro,
"opensuse" | "opensuse-leap" | "opensuse-tumbleweed" | "suse" => Distro::OpenSUSE,
"linuxmint" | "mint" => Distro::Ubuntu, "pop" | "pop!_os" => Distro::Ubuntu, "elementary" | "elementaryos" => Distro::Ubuntu, "zorin" | "zorinos" => Distro::Ubuntu, "kali" => Distro::Debian, "parrot" => Distro::Debian, "raspbian" => Distro::Debian, "endeavouros" | "endeavour" => Distro::Arch, "garuda" | "garudalinux" => Distro::Arch, "artix" => Distro::Arch, _ => Distro::Unknown,
}
}
fn parse_shell_vars(content: &str) -> HashMap<String, String> {
let mut vars = HashMap::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(eq_pos) = line.find('=') {
let key = line[..eq_pos].trim().to_string();
let value = line[eq_pos + 1..].trim();
let value = if (value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\''))
{
value[1..value.len() - 1].to_string()
} else {
value.to_string()
};
vars.insert(key, value);
}
}
vars
}
fn command_exists(cmd: &str) -> bool {
std::process::Command::new("which")
.arg(cmd)
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
pub fn is_wsl() -> bool {
if Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists() {
return true;
}
if let Ok(content) = fs::read_to_string("/proc/version") {
if content.to_lowercase().contains("microsoft") {
return true;
}
}
false
}
pub fn is_container() -> bool {
if Path::new("/.dockerenv").exists() {
return true;
}
if let Ok(content) = fs::read_to_string("/proc/1/cgroup") {
if content.contains("docker") || content.contains("lxc") || content.contains("kubepods")
{
return true;
}
}
if let Ok(content) = fs::read_to_string("/run/systemd/container") {
if !content.trim().is_empty() {
return true;
}
}
false
}
pub fn get_info() -> Result<DistroInfo> {
let distro = Self::detect()?;
let content = fs::read_to_string("/etc/os-release")
.or_else(|_| fs::read_to_string("/etc/lsb-release"))
.context("Failed to read release file")?;
let vars = Self::parse_shell_vars(&content);
Ok(DistroInfo {
distro,
name: vars
.get("NAME")
.or_else(|| vars.get("DISTRIB_ID"))
.cloned()
.unwrap_or_else(|| distro.as_str().to_string()),
version: vars
.get("VERSION_ID")
.or_else(|| vars.get("DISTRIB_RELEASE"))
.cloned(),
codename: vars
.get("VERSION_CODENAME")
.or_else(|| vars.get("DISTRIB_CODENAME"))
.cloned(),
pretty_name: vars.get("PRETTY_NAME").cloned(),
is_wsl: Self::is_wsl(),
is_container: Self::is_container(),
})
}
}
#[derive(Debug, Clone)]
pub struct DistroInfo {
pub distro: Distro,
pub name: String,
pub version: Option<String>,
pub codename: Option<String>,
pub pretty_name: Option<String>,
pub is_wsl: bool,
pub is_container: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_shell_vars() {
let content = r#"
# Comment
NAME="Ubuntu"
ID=ubuntu
VERSION_ID="22.04"
SIMPLE=value
EMPTY=
"#;
let vars = DistroDetector::parse_shell_vars(content);
assert_eq!(vars.get("NAME"), Some(&"Ubuntu".to_string()));
assert_eq!(vars.get("ID"), Some(&"ubuntu".to_string()));
assert_eq!(vars.get("VERSION_ID"), Some(&"22.04".to_string()));
assert_eq!(vars.get("SIMPLE"), Some(&"value".to_string()));
assert_eq!(vars.get("EMPTY"), Some(&"".to_string()));
}
#[test]
fn test_map_id_to_distro() {
assert_eq!(DistroDetector::map_id_to_distro("ubuntu"), Distro::Ubuntu);
assert_eq!(DistroDetector::map_id_to_distro("debian"), Distro::Debian);
assert_eq!(DistroDetector::map_id_to_distro("fedora"), Distro::Fedora);
assert_eq!(DistroDetector::map_id_to_distro("arch"), Distro::Arch);
assert_eq!(
DistroDetector::map_id_to_distro("manjaro"),
Distro::Manjaro
);
assert_eq!(
DistroDetector::map_id_to_distro("opensuse"),
Distro::OpenSUSE
);
assert_eq!(DistroDetector::map_id_to_distro("rhel"), Distro::RHEL);
assert_eq!(DistroDetector::map_id_to_distro("centos"), Distro::RHEL);
}
#[test]
fn test_map_derivatives() {
assert_eq!(DistroDetector::map_id_to_distro("mint"), Distro::Ubuntu);
assert_eq!(DistroDetector::map_id_to_distro("pop"), Distro::Ubuntu);
assert_eq!(DistroDetector::map_id_to_distro("kali"), Distro::Debian);
assert_eq!(DistroDetector::map_id_to_distro("parrot"), Distro::Debian);
assert_eq!(
DistroDetector::map_id_to_distro("endeavour"),
Distro::Arch
);
assert_eq!(DistroDetector::map_id_to_distro("garuda"), Distro::Arch);
assert_eq!(DistroDetector::map_id_to_distro("rocky"), Distro::RHEL);
assert_eq!(DistroDetector::map_id_to_distro("alma"), Distro::RHEL);
}
#[test]
fn test_unknown_distro() {
assert_eq!(
DistroDetector::map_id_to_distro("unknown_distro"),
Distro::Unknown
);
}
}