whatadistro/
lib.rs

1use std::collections::HashSet;
2use std::fmt::Display;
3
4#[allow(non_camel_case_types)]
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6/// IDs of supported linux distros
7pub enum DistroId {
8    /// Arch Linux
9    /// 
10    /// ```bash
11    /// ID=arch
12    /// ```
13    Arch,
14
15    /// Debian
16    /// 
17    /// ```bash
18    /// ID=debian
19    /// ```
20    Debian,
21
22    /// Ubuntu
23    /// 
24    /// ```bash
25    /// ID=ubuntu
26    /// ```
27    Ubuntu,
28
29    /// Linux Mint
30    /// 
31    /// ```bash
32    /// ID=linuxmint
33    /// ```
34    Mint,
35
36    /// Red Hat Enterprise Linux (RHEL)
37    /// 
38    /// ```bash
39    /// ID=rhel
40    /// ```
41    RHEL,
42
43    /// Fedora (workstation, silverblue)
44    /// 
45    /// ```bash
46    /// ID=fedora
47    /// ```
48    Fedora,
49
50    /// OpenSUSE (leap, tumbleweed)
51    /// 
52    /// ```bash
53    /// ID=suse
54    /// ID=opensuse
55    /// ID=opensuse-tumbleweed
56    /// ```
57    OpenSUSE,
58
59    /// Gentoo
60    /// 
61    /// ```bash
62    /// ID=gentoo
63    /// ```
64    Gentoo,
65
66    /// ```bash
67    /// ID=nixos
68    /// ```
69    NixOS,
70
71    /// Nothing from above
72    Other(String)
73}
74
75impl DistroId {
76    /// List distro ids similar to the current one.
77    /// Always include current distro itself
78    pub fn list_similar(&self) -> Vec<Self> {
79        match self {
80            Self::Arch => vec![
81                Self::Arch
82            ],
83
84            Self::Debian => vec![
85                Self::Debian,
86                Self::Ubuntu,
87                Self::Mint
88            ],
89
90            Self::Ubuntu => vec![
91                Self::Ubuntu,
92                Self::Debian,
93                Self::Mint
94            ],
95
96            Self::Mint => vec![
97                Self::Mint,
98                Self::Debian,
99                Self::Ubuntu
100            ],
101
102            Self::RHEL => vec![
103                Self::RHEL,
104                Self::Fedora,
105                Self::OpenSUSE
106            ],
107
108            Self::Fedora => vec![
109                Self::Fedora,
110                Self::RHEL,
111                Self::OpenSUSE
112            ],
113
114            Self::OpenSUSE => vec![
115                Self::OpenSUSE,
116                Self::Fedora,
117                Self::RHEL
118            ],
119
120            Self::Gentoo => vec![
121                Self::Gentoo
122            ],
123
124            Self::NixOS => vec![
125                Self::NixOS
126            ],
127
128            Self::Other(id) => vec![
129                Self::Other(id.clone())
130            ]
131        }
132    }
133
134    #[inline]
135    /// Compare given distro id with the current one
136    pub fn is_similar<T: Into<Self>>(&self, other: T) -> bool {
137        self.list_similar().contains(&other.into())
138    }
139}
140
141impl<T> From<T> for DistroId where T: AsRef<str> {
142    fn from(str: T) -> Self {
143        match str.as_ref() {
144            "arch"   => Self::Arch,
145            "debian" => Self::Debian,
146            "ubuntu" => Self::Ubuntu,
147
148            "mint"      => Self::Mint,
149            "linuxmint" => Self::Mint,
150
151            "rhel"   => Self::RHEL,
152            "fedora" => Self::Fedora,
153
154            "suse"                => Self::OpenSUSE,
155            "opensuse"            => Self::OpenSUSE,
156            "opensuse_tumbleweed" => Self::OpenSUSE,
157
158            "gentoo" => Self::Gentoo,
159            "nixos"  => Self::NixOS,
160
161            id => Self::Other(id.to_string())
162        }
163    }
164}
165
166impl Display for DistroId {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        match self {
169            Self::Arch     => write!(f, "arch"),
170            Self::Debian   => write!(f, "debian"),
171            Self::Ubuntu   => write!(f, "ubuntu"),
172            Self::Mint     => write!(f, "linuxmint"),
173            Self::RHEL     => write!(f, "rhel"),
174            Self::Fedora   => write!(f, "fedora"),
175            Self::OpenSUSE => write!(f, "opensuse"),
176            Self::Gentoo   => write!(f, "gentoo"),
177            Self::NixOS    => write!(f, "nixos"),
178
179            Self::Other(id) => write!(f, "{id}")
180        }
181    }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct Distro {
186    name: String,
187    id: DistroId,
188    similar_ids: HashSet<DistroId>
189}
190
191impl Distro {
192    #[inline]
193    /// Identify current linux distro using `/etc/os-release` file
194    pub fn current() -> Option<Self> {
195        identify()
196    }
197
198    #[inline]
199    /// Get current distro name (`NAME` entry)
200    pub fn name(&self) -> &str {
201        &self.name
202    }
203
204    #[inline]
205    /// Get current distro id (`ID` entry)
206    pub fn id(&self) -> &DistroId {
207        &self.id
208    }
209
210    #[inline]
211    /// Get list of similar distros (`ID_LIKE` entry)
212    /// 
213    /// ```
214    /// if let Some(distro) = whatadistro::identify() {
215    ///     println!("Your distro: {} ({})", distro.name(), distro.id());
216    ///     println!("Similar distros: {:?}", distro.id().list_similar());
217    /// }
218    /// ```
219    pub fn similar_ids(&self) -> &HashSet<DistroId> {
220        &self.similar_ids
221    }
222
223    #[inline]
224    /// Compare current distro with some another
225    /// 
226    /// ```
227    /// let status = whatadistro::identify()
228    ///     .map(|distro| distro.is_similar("arch")) // whatadistro::Distro::Arch can be used as well
229    ///     .unwrap_or(false);
230    /// 
231    /// println!("Is current system arch-based: {:?}", status);
232    /// ```
233    pub fn is_similar<T: Into<DistroId>>(&self, other: T) -> bool {
234        let other = other.into();
235
236        self.similar_ids.contains(&other) || self.id.is_similar(other)
237    }
238}
239
240/// Identify current linux distro using `/etc/os-release` file
241/// 
242/// ```
243/// let distro = whatadistro::identify()
244///     .expect("Failed to parse os-release file");
245/// 
246/// println!("Your distro name is {}", distro.name());
247/// ```
248pub fn identify() -> Option<Distro> {
249    let mut id: Option<DistroId> = None;
250    let mut name: Option<String> = None;
251    let mut similar_ids: Option<HashSet<DistroId>> = None;
252
253    if let Ok(release) = std::fs::read_to_string("/etc/os-release") {
254        for line in release.lines() {
255            if let Some(distro_id) = line.strip_prefix("ID=") {
256                id = Some(distro_id.into());
257            }
258
259            else if let Some(distro_name) = line.strip_prefix("NAME=") {
260                name = Some(distro_name.to_string());
261            }
262
263            else if let Some(ids) = line.strip_prefix("ID_LIKE=") {
264                similar_ids = Some(ids.split_whitespace().map(|id| id.into()).collect());
265            }
266        }
267
268        let Some(id) = id else {
269            return None;
270        };
271
272        // TODO: maybe I can use here something like id.name() ?
273        let Some(name) = name else {
274            return None;
275        };
276
277        Some(Distro {
278            id,
279            name,
280            similar_ids: similar_ids.unwrap_or_default()
281        })
282    }
283
284    else {
285        None
286    }
287}