Skip to main content

uv_platform/
host.rs

1//! Host system information (OS type, kernel release, distro metadata).
2
3use std::convert::Infallible;
4use std::fmt;
5use std::str::FromStr;
6
7/// The operating system type.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum OsType {
10    /// Linux, with the kernel type from `/proc/sys/kernel/ostype` (typically `"Linux"`).
11    Linux(String),
12    /// macOS / Darwin.
13    Darwin,
14    /// Windows NT.
15    WindowsNt,
16}
17
18impl OsType {
19    /// Returns the operating system type for the current host.
20    ///
21    /// Returns `None` on unsupported platforms.
22    pub fn from_env() -> Option<Self> {
23        #[cfg(target_os = "linux")]
24        {
25            fs_err::read_to_string("/proc/sys/kernel/ostype")
26                .ok()
27                .map(|s| Self::Linux(s.trim().to_string()))
28        }
29        #[cfg(target_os = "macos")]
30        {
31            Some(Self::Darwin)
32        }
33        #[cfg(target_os = "windows")]
34        {
35            Some(Self::WindowsNt)
36        }
37        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
38        {
39            None
40        }
41    }
42}
43
44impl fmt::Display for OsType {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            Self::Linux(os_type) => f.write_str(os_type),
48            Self::Darwin => f.write_str("Darwin"),
49            Self::WindowsNt => f.write_str("Windows_NT"),
50        }
51    }
52}
53
54/// The OS kernel release version.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum OsRelease {
57    /// Unix kernel release from `uname -r` (e.g., `"6.8.0-90-generic"`).
58    Unix(String),
59    /// Windows build number from the registry (e.g., `"22631"`).
60    Windows(String),
61}
62
63impl OsRelease {
64    /// Returns the OS kernel release for the current host.
65    ///
66    /// Returns `None` on unsupported platforms or if the release cannot be read.
67    pub fn from_env() -> Option<Self> {
68        #[cfg(unix)]
69        {
70            let uname = rustix::system::uname();
71            let release = uname.release().to_str().ok()?;
72            Some(Self::Unix(release.to_string()))
73        }
74        #[cfg(windows)]
75        {
76            let key = windows_registry::LOCAL_MACHINE
77                .open(r"SOFTWARE\Microsoft\Windows NT\CurrentVersion")
78                .ok()?;
79            Some(Self::Windows(key.get_string("CurrentBuildNumber").ok()?))
80        }
81        #[cfg(not(any(unix, windows)))]
82        {
83            None
84        }
85    }
86}
87
88impl fmt::Display for OsRelease {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        match self {
91            Self::Unix(release) => f.write_str(release),
92            Self::Windows(build) => f.write_str(build),
93        }
94    }
95}
96
97/// Parsed fields from `/etc/os-release`.
98#[derive(Debug, Clone, Default)]
99pub struct LinuxOsRelease {
100    /// Distribution name (e.g., `"Ubuntu"`).
101    pub name: Option<String>,
102    /// Version identifier (e.g., `"22.04"`).
103    pub version_id: Option<String>,
104    /// Version codename (e.g., `"jammy"`).
105    pub version_codename: Option<String>,
106}
107
108impl LinuxOsRelease {
109    /// Reads and parses `/etc/os-release` on Linux, falling back to `/usr/lib/os-release`.
110    ///
111    /// See <https://www.freedesktop.org/software/systemd/man/latest/os-release.html>.
112    ///
113    /// Returns `None` on non-Linux platforms or if the file cannot be read.
114    pub fn from_env() -> Option<Self> {
115        #[cfg(target_os = "linux")]
116        {
117            let content = fs_err::read_to_string("/etc/os-release")
118                .or_else(|_| fs_err::read_to_string("/usr/lib/os-release"))
119                .ok()?;
120            Some(content.parse().unwrap())
121        }
122        #[cfg(not(target_os = "linux"))]
123        {
124            None
125        }
126    }
127}
128
129impl FromStr for LinuxOsRelease {
130    type Err = Infallible;
131
132    /// Parse the contents of an os-release file (KEY=VALUE format, optionally quoted).
133    fn from_str(contents: &str) -> Result<Self, Self::Err> {
134        let mut release = Self::default();
135        for line in contents.lines() {
136            let line = line.trim();
137            if line.is_empty() || line.starts_with('#') {
138                continue;
139            }
140            let Some((key, value)) = line.split_once('=') else {
141                continue;
142            };
143            let value = unquote(value);
144            match key {
145                "NAME" => release.name = Some(value.to_string()),
146                "VERSION_ID" => release.version_id = Some(value.to_string()),
147                "VERSION_CODENAME" => release.version_codename = Some(value.to_string()),
148                _ => {}
149            }
150        }
151        Ok(release)
152    }
153}
154
155/// Strip matching single or double quotes from a value.
156fn unquote(s: &str) -> &str {
157    for quote in ['"', '\''] {
158        if let Some(inner) = s.strip_prefix(quote).and_then(|s| s.strip_suffix(quote)) {
159            return inner;
160        }
161    }
162    s
163}
164
165#[cfg(test)]
166mod tests {
167    use insta::assert_debug_snapshot;
168
169    use super::*;
170
171    #[test]
172    fn test_parse_os_release_ubuntu() {
173        let contents = "\
174NAME=\"Ubuntu\"
175VERSION_ID=\"22.04\"
176VERSION_CODENAME=jammy
177ID=ubuntu
178";
179        let release: LinuxOsRelease = contents.parse().unwrap();
180        assert_debug_snapshot!(release, @r#"
181        LinuxOsRelease {
182            name: Some(
183                "Ubuntu",
184            ),
185            version_id: Some(
186                "22.04",
187            ),
188            version_codename: Some(
189                "jammy",
190            ),
191        }
192        "#);
193    }
194
195    #[test]
196    fn test_parse_os_release_empty() {
197        let release: LinuxOsRelease = "".parse().unwrap();
198        assert_eq!(release.name, None);
199        assert_eq!(release.version_id, None);
200        assert_eq!(release.version_codename, None);
201    }
202
203    #[test]
204    fn test_parse_os_release_comments_and_blanks() {
205        let contents = "\
206# This is a comment
207
208NAME='Fedora Linux'
209VERSION_ID=40
210";
211        let release: LinuxOsRelease = contents.parse().unwrap();
212        assert_eq!(release.name.as_deref(), Some("Fedora Linux"));
213        assert_eq!(release.version_id.as_deref(), Some("40"));
214        assert_eq!(release.version_codename, None);
215    }
216
217    #[test]
218    fn test_unquote() {
219        assert_eq!(unquote("\"hello\""), "hello");
220        assert_eq!(unquote("'hello'"), "hello");
221        assert_eq!(unquote("hello"), "hello");
222        assert_eq!(unquote("\"\""), "");
223        assert_eq!(unquote(""), "");
224    }
225
226    #[test]
227    fn test_os_type_returns_value() {
228        let os_type =
229            OsType::from_env().expect("OsType should be available on supported platforms");
230        #[cfg(target_os = "linux")]
231        assert!(matches!(os_type, OsType::Linux(_)));
232        #[cfg(target_os = "macos")]
233        assert_eq!(os_type, OsType::Darwin);
234        #[cfg(target_os = "windows")]
235        assert_eq!(os_type, OsType::WindowsNt);
236    }
237
238    #[test]
239    fn test_os_release_returns_value() {
240        let os_release =
241            OsRelease::from_env().expect("OsRelease should be available on supported platforms");
242        #[cfg(unix)]
243        assert!(matches!(os_release, OsRelease::Unix(_)));
244        #[cfg(windows)]
245        assert!(matches!(os_release, OsRelease::Windows(_)));
246    }
247}