1use std::path::Path;
2use std::sync::OnceLock;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Platform {
7 MacOS,
9 LinuxNative,
11 LinuxNoKvm,
13 Wsl2,
15 Windows,
17}
18
19impl Platform {
20 pub fn needs_lima(self) -> bool {
23 match self {
24 Platform::MacOS => !self.has_apple_containers(),
25 Platform::LinuxNoKvm => true,
26 Platform::LinuxNative => false,
27 Platform::Wsl2 => !self.has_kvm() && !self.has_docker(),
28 Platform::Windows => false, }
30 }
31
32 pub fn has_kvm(self) -> bool {
34 match self {
35 Platform::LinuxNative => true,
36 Platform::Wsl2 => Path::new("/dev/kvm").exists(),
37 _ => false,
38 }
39 }
40
41 pub fn supports_native_runner(self) -> bool {
43 matches!(self, Platform::LinuxNative) || (matches!(self, Platform::Wsl2) && self.has_kvm())
44 }
45
46 pub fn has_apple_containers(self) -> bool {
50 if !matches!(self, Platform::MacOS) {
51 return false;
52 }
53 is_macos_26_or_later()
54 }
55
56 pub fn has_docker(self) -> bool {
60 static DOCKER_AVAILABLE: OnceLock<bool> = OnceLock::new();
61 *DOCKER_AVAILABLE.get_or_init(|| {
62 std::process::Command::new("docker")
63 .args(["version", "--format", "{{.Server.Version}}"])
64 .output()
65 .map(|o| o.status.success())
66 .unwrap_or(false)
67 })
68 }
69
70 pub fn has_host_nix(self) -> bool {
76 static HOST_NIX: OnceLock<bool> = OnceLock::new();
77 *HOST_NIX.get_or_init(|| {
78 if std::process::Command::new("nix")
80 .args(["--version"])
81 .stdout(std::process::Stdio::null())
82 .stderr(std::process::Stdio::null())
83 .status()
84 .map(|s| s.success())
85 .unwrap_or(false)
86 {
87 return true;
88 }
89 for path in &[
92 "/nix/var/nix/profiles/default/bin/nix",
93 "/run/current-system/sw/bin/nix",
94 ] {
95 if Path::new(path).exists() {
96 return true;
97 }
98 }
99 false
100 })
101 }
102
103 pub fn is_wsl(self) -> bool {
105 matches!(self, Platform::Wsl2)
106 }
107
108 pub fn is_windows(self) -> bool {
110 matches!(self, Platform::Windows)
111 }
112}
113
114fn is_macos_26_or_later() -> bool {
116 #[cfg(target_os = "macos")]
117 {
118 if cfg!(not(target_arch = "aarch64")) {
119 return false;
120 }
121 macos_major_version() >= 26
122 }
123 #[cfg(not(target_os = "macos"))]
124 {
125 false
126 }
127}
128
129#[cfg(target_os = "macos")]
131fn macos_major_version() -> u32 {
132 use std::process::Command;
133 Command::new("sw_vers")
134 .arg("-productVersion")
135 .output()
136 .ok()
137 .and_then(|o| String::from_utf8(o.stdout).ok())
138 .and_then(|v| v.trim().split('.').next().map(String::from))
139 .and_then(|major| major.parse::<u32>().ok())
140 .unwrap_or(0)
141}
142
143fn is_wsl2() -> bool {
145 #[cfg(target_os = "linux")]
146 {
147 std::fs::read_to_string("/proc/version")
148 .map(|v| {
149 let lower = v.to_lowercase();
150 lower.contains("microsoft") || lower.contains("wsl")
151 })
152 .unwrap_or(false)
153 }
154 #[cfg(not(target_os = "linux"))]
155 {
156 false
157 }
158}
159
160impl std::fmt::Display for Platform {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 match self {
163 Platform::MacOS => write!(f, "macOS"),
164 Platform::LinuxNative => write!(f, "Linux (native KVM)"),
165 Platform::LinuxNoKvm => write!(f, "Linux (no KVM)"),
166 Platform::Wsl2 => {
167 if self.has_kvm() {
168 write!(f, "WSL2 (KVM available)")
169 } else {
170 write!(f, "WSL2")
171 }
172 }
173 Platform::Windows => write!(f, "Windows"),
174 }
175 }
176}
177
178static DETECTED: OnceLock<Platform> = OnceLock::new();
180
181pub fn current() -> Platform {
183 *DETECTED.get_or_init(detect)
184}
185
186fn detect() -> Platform {
187 if cfg!(target_os = "macos") {
188 Platform::MacOS
189 } else if cfg!(target_os = "linux") {
190 if is_wsl2() {
191 Platform::Wsl2
192 } else if Path::new("/dev/kvm").exists() {
193 Platform::LinuxNative
194 } else {
195 Platform::LinuxNoKvm
196 }
197 } else if cfg!(target_os = "windows") {
198 Platform::Windows
199 } else {
200 Platform::LinuxNoKvm
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_detect_returns_consistent_result() {
211 let a = current();
212 let b = current();
213 assert_eq!(a, b);
214 }
215
216 #[test]
217 fn test_platform_display() {
218 assert_eq!(Platform::LinuxNative.to_string(), "Linux (native KVM)");
219 assert_eq!(Platform::LinuxNoKvm.to_string(), "Linux (no KVM)");
220 assert_eq!(Platform::Windows.to_string(), "Windows");
221 }
222
223 #[test]
224 fn test_needs_lima() {
225 let macos_needs = Platform::MacOS.needs_lima();
227 if Platform::MacOS.has_apple_containers() {
228 assert!(!macos_needs, "macOS 26+ should not need Lima");
229 } else {
230 assert!(macos_needs, "macOS <26 should need Lima");
231 }
232 assert!(!Platform::LinuxNative.needs_lima());
233 assert!(Platform::LinuxNoKvm.needs_lima());
234 assert!(!Platform::Windows.needs_lima());
235 }
236
237 #[test]
238 fn test_has_kvm() {
239 assert!(!Platform::MacOS.has_kvm());
240 assert!(Platform::LinuxNative.has_kvm());
241 assert!(!Platform::LinuxNoKvm.has_kvm());
242 assert!(!Platform::Windows.has_kvm());
243 }
244
245 #[test]
246 fn test_supports_native_runner() {
247 assert!(!Platform::MacOS.supports_native_runner());
248 assert!(Platform::LinuxNative.supports_native_runner());
249 assert!(!Platform::LinuxNoKvm.supports_native_runner());
250 assert!(!Platform::Windows.supports_native_runner());
251 }
252
253 #[test]
254 fn test_has_apple_containers_non_macos() {
255 assert!(!Platform::LinuxNative.has_apple_containers());
256 assert!(!Platform::LinuxNoKvm.has_apple_containers());
257 assert!(!Platform::Wsl2.has_apple_containers());
258 assert!(!Platform::Windows.has_apple_containers());
259 }
260
261 #[test]
262 fn test_has_docker_returns_bool() {
263 let _ = Platform::MacOS.has_docker();
265 }
266
267 #[test]
268 fn test_current_platform_valid() {
269 let p = current();
270 let _ = p.needs_lima();
271 let _ = p.has_kvm();
272 let _ = p.supports_native_runner();
273 let _ = p.has_apple_containers();
274 let _ = p.has_docker();
275 }
276
277 #[test]
278 #[cfg(target_os = "macos")]
279 fn test_macos_major_version_is_reasonable() {
280 let version = macos_major_version();
281 assert!(version >= 10, "macOS version {version} seems too low");
282 }
283}