1use crate::error::{Error, Result};
4use std::env;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum Runtime {
11 Docker,
13 Podman,
15 Colima,
17 RancherDesktop,
19 OrbStack,
21 DockerDesktop,
23}
24
25impl Runtime {
26 #[must_use]
28 pub fn command(&self) -> &str {
29 match self {
30 Runtime::Docker
31 | Runtime::Colima
32 | Runtime::RancherDesktop
33 | Runtime::OrbStack
34 | Runtime::DockerDesktop => "docker",
35 Runtime::Podman => "podman",
36 }
37 }
38
39 #[must_use]
41 pub fn supports_compose(&self) -> bool {
42 matches!(
43 self,
44 Runtime::Docker
45 | Runtime::DockerDesktop
46 | Runtime::Colima
47 | Runtime::RancherDesktop
48 | Runtime::OrbStack
49 )
50 }
51
52 #[must_use]
54 pub fn compose_command(&self) -> Vec<String> {
55 match self {
56 Runtime::Podman => vec!["podman-compose".to_string()],
57 Runtime::Docker
58 | Runtime::DockerDesktop
59 | Runtime::Colima
60 | Runtime::RancherDesktop
61 | Runtime::OrbStack => {
62 vec!["docker".to_string(), "compose".to_string()]
63 }
64 }
65 }
66}
67
68impl std::fmt::Display for Runtime {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 match self {
71 Runtime::Docker => write!(f, "Docker"),
72 Runtime::Podman => write!(f, "Podman"),
73 Runtime::Colima => write!(f, "Colima"),
74 Runtime::RancherDesktop => write!(f, "Rancher Desktop"),
75 Runtime::OrbStack => write!(f, "OrbStack"),
76 Runtime::DockerDesktop => write!(f, "Docker Desktop"),
77 }
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum Platform {
84 Linux,
86 MacOS,
88 Windows,
90 FreeBSD,
92 Other(String),
94}
95
96impl Platform {
97 #[must_use]
99 pub fn detect() -> Self {
100 match env::consts::OS {
101 "linux" => Platform::Linux,
102 "macos" | "darwin" => Platform::MacOS,
103 "windows" => Platform::Windows,
104 "freebsd" => Platform::FreeBSD,
105 other => Platform::Other(other.to_string()),
106 }
107 }
108
109 #[must_use]
111 pub fn is_wsl(&self) -> bool {
112 if !matches!(self, Platform::Linux) {
113 return false;
114 }
115
116 Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists()
118 || env::var("WSL_DISTRO_NAME").is_ok()
119 || env::var("WSL_INTEROP").is_ok()
120 }
121
122 #[must_use]
124 pub fn default_socket_path(&self) -> PathBuf {
125 match self {
126 Platform::MacOS => {
127 let locations = [
129 "/var/run/docker.sock",
130 "/Users/$USER/.docker/run/docker.sock",
131 "/Users/$USER/.colima/docker.sock",
132 "/Users/$USER/.orbstack/run/docker.sock",
133 ];
134
135 for location in &locations {
136 let path = if location.contains("$USER") {
137 let user = env::var("USER").unwrap_or_else(|_| "unknown".to_string());
138 PathBuf::from(location.replace("$USER", &user))
139 } else {
140 PathBuf::from(location)
141 };
142
143 if path.exists() {
144 return path;
145 }
146 }
147
148 PathBuf::from("/var/run/docker.sock")
149 }
150 Platform::Windows => PathBuf::from("//./pipe/docker_engine"),
151 Platform::Linux | Platform::FreeBSD | Platform::Other(_) => {
152 PathBuf::from("/var/run/docker.sock")
153 }
154 }
155 }
156}
157
158impl std::fmt::Display for Platform {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 match self {
161 Platform::Linux => write!(f, "Linux"),
162 Platform::MacOS => write!(f, "macOS"),
163 Platform::Windows => write!(f, "Windows"),
164 Platform::FreeBSD => write!(f, "FreeBSD"),
165 Platform::Other(s) => write!(f, "{s}"),
166 }
167 }
168}
169
170#[derive(Debug, Clone)]
172pub struct PlatformInfo {
173 pub platform: Platform,
175 pub runtime: Runtime,
177 pub version: String,
179 pub is_wsl: bool,
181 pub socket_path: PathBuf,
183}
184
185impl PlatformInfo {
186 pub fn detect() -> Result<Self> {
192 let platform = Platform::detect();
193 let is_wsl = platform.is_wsl();
194 let socket_path = Self::find_socket_path(&platform);
195
196 let runtime = Self::detect_runtime()?;
198 let version = Self::get_runtime_version(&runtime)?;
199
200 Ok(Self {
201 platform,
202 runtime,
203 version,
204 is_wsl,
205 socket_path,
206 })
207 }
208
209 fn find_socket_path(platform: &Platform) -> PathBuf {
211 if let Ok(docker_host) = env::var("DOCKER_HOST") {
213 if docker_host.starts_with("unix://") {
214 return PathBuf::from(docker_host.trim_start_matches("unix://"));
215 }
216 }
217
218 platform.default_socket_path()
219 }
220
221 fn detect_runtime() -> Result<Runtime> {
223 if env::var("ORBSTACK_HOME").is_ok() {
225 return Ok(Runtime::OrbStack);
226 }
227
228 if env::var("COLIMA_HOME").is_ok() {
229 return Ok(Runtime::Colima);
230 }
231
232 if let Ok(output) = Command::new("docker").arg("version").output() {
234 let version_str = String::from_utf8_lossy(&output.stdout);
235
236 if version_str.contains("Docker Desktop") {
237 return Ok(Runtime::DockerDesktop);
238 }
239
240 if version_str.contains("Rancher Desktop") {
241 return Ok(Runtime::RancherDesktop);
242 }
243
244 if version_str.contains("podman") {
245 return Ok(Runtime::Podman);
246 }
247
248 if version_str.contains("colima") {
249 return Ok(Runtime::Colima);
250 }
251
252 if version_str.contains("OrbStack") {
253 return Ok(Runtime::OrbStack);
254 }
255
256 if version_str.contains("Docker") {
258 return Ok(Runtime::Docker);
259 }
260 }
261
262 if Command::new("podman").arg("version").output().is_ok() {
264 return Ok(Runtime::Podman);
265 }
266
267 Err(Error::DockerNotFound)
268 }
269
270 fn get_runtime_version(runtime: &Runtime) -> Result<String> {
272 let output = Command::new(runtime.command())
273 .arg("version")
274 .arg("--format")
275 .arg("{{.Server.Version}}")
276 .output()
277 .map_err(|e| {
278 Error::command_failed(
279 format!("{} version", runtime.command()),
280 -1,
281 "",
282 e.to_string(),
283 )
284 })?;
285
286 if output.status.success() {
287 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
288 } else {
289 let output = Command::new(runtime.command())
291 .arg("version")
292 .output()
293 .map_err(|e| {
294 Error::command_failed(
295 format!("{} version", runtime.command()),
296 -1,
297 "",
298 e.to_string(),
299 )
300 })?;
301
302 let version_str = String::from_utf8_lossy(&output.stdout);
303 Ok(Self::parse_version(&version_str))
304 }
305 }
306
307 fn parse_version(version_str: &str) -> String {
309 for line in version_str.lines() {
311 if line.contains("Version:") {
312 if let Some(version) = line.split(':').nth(1) {
313 return version.trim().to_string();
314 }
315 }
316 }
317
318 "unknown".to_string()
319 }
320
321 pub fn check_runtime(&self) -> Result<()> {
327 let output = Command::new(self.runtime.command())
328 .arg("info")
329 .output()
330 .map_err(|_| Error::DockerNotFound)?;
331
332 if !output.status.success() {
333 let stderr = String::from_utf8_lossy(&output.stderr);
334 if stderr.contains("Cannot connect to the Docker daemon") {
335 return Err(Error::DaemonNotRunning);
336 }
337 return Err(Error::command_failed(
338 format!("{} info", self.runtime.command()),
339 -1,
340 "",
341 stderr,
342 ));
343 }
344
345 Ok(())
346 }
347
348 #[must_use]
350 pub fn environment_vars(&self) -> Vec<(String, String)> {
351 let mut vars = Vec::new();
352
353 if self.socket_path.exists() {
355 vars.push((
356 "DOCKER_HOST".to_string(),
357 format!("unix://{}", self.socket_path.display()),
358 ));
359 }
360
361 match self.runtime {
363 Runtime::Podman => {
364 vars.push(("DOCKER_BUILDKIT".to_string(), "0".to_string()));
365 }
366 Runtime::DockerDesktop | Runtime::Docker => {
367 vars.push(("DOCKER_BUILDKIT".to_string(), "1".to_string()));
368 }
369 _ => {}
370 }
371
372 vars
373 }
374}
375
376impl std::fmt::Display for PlatformInfo {
377 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378 write!(
379 f,
380 "{} on {} (version: {})",
381 self.runtime, self.platform, self.version
382 )?;
383 if self.is_wsl {
384 write!(f, " [WSL]")?;
385 }
386 Ok(())
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn test_platform_detection() {
396 let platform = Platform::detect();
397 assert!(matches!(
399 platform,
400 Platform::Linux
401 | Platform::MacOS
402 | Platform::Windows
403 | Platform::FreeBSD
404 | Platform::Other(_)
405 ));
406 }
407
408 #[test]
409 fn test_runtime_command() {
410 assert_eq!(Runtime::Docker.command(), "docker");
411 assert_eq!(Runtime::Podman.command(), "podman");
412 assert_eq!(Runtime::Colima.command(), "docker");
413 }
414
415 #[test]
416 fn test_runtime_compose_support() {
417 assert!(Runtime::Docker.supports_compose());
418 assert!(Runtime::DockerDesktop.supports_compose());
419 assert!(Runtime::Colima.supports_compose());
420 assert!(!Runtime::Podman.supports_compose());
421 }
422
423 #[test]
424 fn test_platform_display() {
425 assert_eq!(Platform::Linux.to_string(), "Linux");
426 assert_eq!(Platform::MacOS.to_string(), "macOS");
427 assert_eq!(Platform::Windows.to_string(), "Windows");
428 }
429
430 #[test]
431 fn test_runtime_display() {
432 assert_eq!(Runtime::Docker.to_string(), "Docker");
433 assert_eq!(Runtime::Podman.to_string(), "Podman");
434 assert_eq!(Runtime::OrbStack.to_string(), "OrbStack");
435 }
436}