1use std::collections::HashMap;
4use std::fs;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum Os {
9 Linux,
10 MacOS,
11 FreeBSD,
12 Windows,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum Distro {
18 Ubuntu,
19 Debian,
20 Fedora,
21 RHEL,
22 CentOS,
23 Arch,
24 Manjaro,
25 Alpine,
26 OpenSUSE,
27 FreeBSD,
28 MacOS,
29 Windows,
30 Unknown,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum Arch {
36 X86_64,
37 Aarch64,
38 Other(String),
39}
40
41#[derive(Debug, Clone)]
43pub struct Platform {
44 pub os: Os,
45 pub distro: Distro,
46 pub version: String,
47 pub arch: Arch,
48}
49
50impl Platform {
51 pub fn detect() -> Self {
57 let arch = detect_arch();
58
59 if cfg!(windows) {
60 return Platform {
61 os: Os::Windows,
62 distro: Distro::Windows,
63 version: String::new(),
64 arch,
65 };
66 }
67
68 if cfg!(target_os = "macos") {
69 let version = read_macos_version().unwrap_or_default();
70 return Platform {
71 os: Os::MacOS,
72 distro: Distro::MacOS,
73 version,
74 arch,
75 };
76 }
77
78 if cfg!(target_os = "freebsd") {
79 let version = read_command_output("freebsd-version", &[]).unwrap_or_default();
80 return Platform {
81 os: Os::FreeBSD,
82 distro: Distro::FreeBSD,
83 version: version.trim().to_string(),
84 arch,
85 };
86 }
87
88 let (distro, version) = parse_os_release().unwrap_or((Distro::Unknown, String::new()));
90 Platform {
91 os: Os::Linux,
92 distro,
93 version,
94 arch,
95 }
96 }
97
98 pub fn matches_any(&self, tags: &[String]) -> bool {
102 if tags.is_empty() {
103 return true;
104 }
105 tags.iter().any(|tag| {
106 tag == self.os.as_str() || tag == self.distro.as_str() || tag == self.arch.as_str()
107 })
108 }
109
110 pub fn native_manager(&self) -> &str {
112 match self.distro {
113 Distro::MacOS => "brew",
114 Distro::Ubuntu | Distro::Debian => "apt",
115 Distro::Fedora => "dnf",
116 Distro::RHEL | Distro::CentOS => {
117 if self.version.starts_with('7') {
120 "yum"
121 } else {
122 "dnf"
123 }
124 }
125 Distro::Arch | Distro::Manjaro => "pacman",
126 Distro::Alpine => "apk",
127 Distro::OpenSUSE => "zypper",
128 Distro::FreeBSD => "pkg",
129 Distro::Windows => "winget",
130 Distro::Unknown => "apt", }
132 }
133}
134
135impl Os {
136 pub fn as_str(&self) -> &str {
137 match self {
138 Os::Linux => "linux",
139 Os::MacOS => "macos",
140 Os::FreeBSD => "freebsd",
141 Os::Windows => "windows",
142 }
143 }
144}
145
146impl Distro {
147 pub fn as_str(&self) -> &str {
148 match self {
149 Distro::Ubuntu => "ubuntu",
150 Distro::Debian => "debian",
151 Distro::Fedora => "fedora",
152 Distro::RHEL => "rhel",
153 Distro::CentOS => "centos",
154 Distro::Arch => "arch",
155 Distro::Manjaro => "manjaro",
156 Distro::Alpine => "alpine",
157 Distro::OpenSUSE => "opensuse",
158 Distro::FreeBSD => "freebsd",
159 Distro::MacOS => "macos",
160 Distro::Windows => "windows",
161 Distro::Unknown => "unknown",
162 }
163 }
164}
165
166impl Arch {
167 pub fn as_str(&self) -> &str {
168 match self {
169 Arch::X86_64 => "x86_64",
170 Arch::Aarch64 => "aarch64",
171 Arch::Other(s) => s,
172 }
173 }
174}
175
176impl std::fmt::Display for Os {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 f.write_str(self.as_str())
179 }
180}
181
182impl std::fmt::Display for Distro {
183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184 f.write_str(self.as_str())
185 }
186}
187
188impl std::fmt::Display for Arch {
189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190 f.write_str(self.as_str())
191 }
192}
193
194fn detect_arch() -> Arch {
195 match std::env::consts::ARCH {
196 "x86_64" => Arch::X86_64,
197 "aarch64" => Arch::Aarch64,
198 other => Arch::Other(other.to_string()),
199 }
200}
201
202fn read_macos_version() -> Option<String> {
203 read_command_output("sw_vers", &["-productVersion"])
204 .map(|s| s.trim().to_string())
205 .ok()
206}
207
208fn read_command_output(cmd: &str, args: &[&str]) -> Result<String, std::io::Error> {
209 let output = std::process::Command::new(cmd)
210 .args(args)
211 .stdout(std::process::Stdio::piped())
212 .stderr(std::process::Stdio::piped())
213 .output()?;
214 if output.status.success() {
215 Ok(crate::stdout_lossy_trimmed(&output))
216 } else {
217 let stderr = crate::stderr_lossy_trimmed(&output);
218 Err(std::io::Error::other(format!(
219 "{} failed: {}",
220 cmd,
221 if stderr.is_empty() {
222 format!("exit code {}", output.status.code().unwrap_or(-1))
223 } else {
224 stderr
225 }
226 )))
227 }
228}
229
230fn parse_os_release() -> Option<(Distro, String)> {
232 let content = fs::read_to_string("/etc/os-release").ok()?;
233 Some(distro_from_os_release_content(&content))
234}
235
236fn distro_from_os_release_content(content: &str) -> (Distro, String) {
239 let fields = parse_os_release_content(content);
240
241 let id = fields
242 .get("ID")
243 .map(|s| s.to_lowercase())
244 .unwrap_or_default();
245 let id_like = fields
246 .get("ID_LIKE")
247 .map(|s| s.to_lowercase())
248 .unwrap_or_default();
249 let version_id = fields.get("VERSION_ID").cloned().unwrap_or_default();
250
251 let distro = match id.as_str() {
252 "ubuntu" => Distro::Ubuntu,
253 "debian" => Distro::Debian,
254 "fedora" => Distro::Fedora,
255 "rhel" | "redhat" => Distro::RHEL,
256 "centos" => Distro::CentOS,
257 "arch" | "archlinux" => Distro::Arch,
258 "manjaro" => Distro::Manjaro,
259 "alpine" => Distro::Alpine,
260 "opensuse" | "opensuse-leap" | "opensuse-tumbleweed" => Distro::OpenSUSE,
261 _ => {
262 if id_like.contains("ubuntu") || id_like.contains("debian") {
264 if id_like.contains("ubuntu") {
265 Distro::Ubuntu
266 } else {
267 Distro::Debian
268 }
269 } else if id_like.contains("fedora") || id_like.contains("rhel") {
270 Distro::Fedora
271 } else if id_like.contains("arch") {
272 Distro::Arch
273 } else if id_like.contains("suse") {
274 Distro::OpenSUSE
275 } else {
276 Distro::Unknown
277 }
278 }
279 };
280
281 (distro, version_id)
282}
283
284pub(crate) fn parse_os_release_content(content: &str) -> HashMap<String, String> {
287 let mut fields = HashMap::new();
288 for line in content.lines() {
289 let line = line.trim();
290 if line.is_empty() || line.starts_with('#') {
291 continue;
292 }
293 if let Some((key, value)) = line.split_once('=') {
294 let value = value.trim_matches('"').trim_matches('\'').to_string();
295 fields.insert(key.to_string(), value);
296 }
297 }
298 fields
299}
300
301#[cfg(test)]
302mod tests;