1use std::env;
10use std::sync::OnceLock;
11use tokio::process::Command;
12use tracing::{debug, info, warn};
13
14static COMPOSE_COMMAND_TYPE: OnceLock<ComposeCommandType> = OnceLock::new();
16
17#[derive(Debug, Clone, Copy, PartialEq, Default)]
19pub enum ComposeCommandType {
20 DockerComposeSubcommand,
22 DockerComposeStandalone,
24 #[default]
26 Unknown,
27}
28
29pub async fn detect_compose_command_type() -> ComposeCommandType {
31 info!("🔍 Detecting Docker Compose command type...");
32
33 let output = Command::new("docker")
35 .args(["compose", "version"])
36 .output()
37 .await;
38
39 if let Ok(output) = output {
40 if output.status.success() {
41 let version_info = String::from_utf8_lossy(&output.stdout);
42 info!(
43 " ✅ Using docker compose subcommand: {}",
44 version_info.trim()
45 );
46 return ComposeCommandType::DockerComposeSubcommand;
47 }
48 debug!(
49 " docker compose version returned non-zero status: {:?}",
50 output.status
51 );
52 }
53
54 debug!(" Trying standalone docker-compose command...");
56 let output = Command::new("docker-compose")
57 .arg("--version")
58 .output()
59 .await;
60
61 if let Ok(output) = output {
62 if output.status.success() {
63 let version_info = String::from_utf8_lossy(&output.stdout);
64 info!(
65 " ✅ Using standalone docker-compose command: {}",
66 version_info.trim()
67 );
68 return ComposeCommandType::DockerComposeStandalone;
69 }
70 }
71
72 warn!(" ⚠️ No available Docker Compose command detected");
73 ComposeCommandType::Unknown
74}
75
76pub fn set_compose_command_type(compose_type: ComposeCommandType) {
81 if COMPOSE_COMMAND_TYPE.set(compose_type).is_err() {
82 debug!("Compose command type already set; ignoring duplicate initialization");
83 }
84}
85
86pub fn get_compose_command_type() -> ComposeCommandType {
90 COMPOSE_COMMAND_TYPE
91 .get()
92 .copied()
93 .unwrap_or(ComposeCommandType::Unknown)
94}
95
96#[derive(Debug, Clone, PartialEq)]
98pub enum HostOs {
99 WindowsWsl2,
101 WindowsNative,
103 LinuxNative,
105 MacOs,
107}
108
109impl HostOs {
110 pub fn display_name(&self) -> &'static str {
112 match self {
113 HostOs::WindowsWsl2 => "Windows (WSL2)",
114 HostOs::WindowsNative => "Windows (Native)",
115 HostOs::LinuxNative => "Linux",
116 HostOs::MacOs => "macOS",
117 }
118 }
119
120 pub fn is_windows(&self) -> bool {
122 matches!(self, HostOs::WindowsWsl2 | HostOs::WindowsNative)
123 }
124
125 pub fn is_wsl2(&self) -> bool {
127 matches!(self, HostOs::WindowsWsl2)
128 }
129
130 pub fn needs_early_mount_check(&self) -> bool {
135 self.is_windows()
136 }
137}
138
139#[derive(Debug, Clone, PartialEq)]
141pub enum PathFormat {
142 Wsl2,
144 Windows,
146 Posix,
148}
149
150impl PathFormat {
151 pub fn display_name(&self) -> &'static str {
152 match self {
153 PathFormat::Wsl2 => "WSL2",
154 PathFormat::Windows => "Windows",
155 PathFormat::Posix => "POSIX",
156 }
157 }
158}
159
160#[derive(Debug, Clone)]
162pub struct RuntimeEnvironment {
163 pub host_os: HostOs,
164 pub path_format: PathFormat,
165}
166
167impl RuntimeEnvironment {
168 pub fn summary(&self) -> String {
170 format!(
171 "{} ({})",
172 self.host_os.display_name(),
173 self.path_format.display_name()
174 )
175 }
176
177 pub fn needs_special_handling(&self) -> bool {
185 self.host_os.is_windows()
186 }
187
188 pub fn is_wsl2(&self) -> bool {
190 self.host_os.is_wsl2()
191 }
192}
193
194pub fn detect_runtime_environment() -> RuntimeEnvironment {
196 debug!("🔍 Detecting runtime environment...");
197
198 let host_os = detect_host_os();
200 debug!(" Host OS: {:?}", host_os);
201
202 let path_format = detect_path_format(&host_os);
204 debug!(" Path format: {:?}", path_format);
205
206 let env = RuntimeEnvironment {
207 host_os,
208 path_format,
209 };
210
211 info!("✅ Runtime environment detected: {}", env.summary());
212
213 if env.needs_special_handling() {
214 info!("⚠️ Windows environment detected; mount directories should be created early");
215 }
216
217 env
218}
219
220fn detect_host_os() -> HostOs {
222 if is_running_in_wsl() {
224 return HostOs::WindowsWsl2;
225 }
226
227 match std::env::consts::OS {
229 "windows" => HostOs::WindowsNative,
230 "linux" => HostOs::LinuxNative,
231 "macos" => HostOs::MacOs,
232 other => {
233 debug!("Unknown operating system: {}, assuming Linux", other);
234 HostOs::LinuxNative
235 }
236 }
237}
238
239fn is_running_in_wsl() -> bool {
241 if let Ok(version) = std::fs::read_to_string("/proc/version") {
243 if version.to_lowercase().contains("microsoft") {
244 debug!("Detected WSL marker in /proc/version");
245 return true;
246 }
247 }
248
249 if env::var("WSL_DISTRO_NAME").is_ok() {
251 debug!("Detected WSL_DISTRO_NAME environment variable");
252 return true;
253 }
254
255 if env::var("WSLENV").is_ok() {
256 debug!("Detected WSLENV environment variable");
257 return true;
258 }
259
260 false
261}
262
263fn detect_path_format(host_os: &HostOs) -> PathFormat {
265 match host_os {
266 HostOs::WindowsWsl2 => PathFormat::Wsl2,
267 HostOs::WindowsNative => PathFormat::Windows,
268 HostOs::LinuxNative | HostOs::MacOs => PathFormat::Posix,
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_host_os_display_name() {
278 assert_eq!(HostOs::WindowsWsl2.display_name(), "Windows (WSL2)");
279 assert_eq!(HostOs::WindowsNative.display_name(), "Windows (Native)");
280 assert_eq!(HostOs::LinuxNative.display_name(), "Linux");
281 assert_eq!(HostOs::MacOs.display_name(), "macOS");
282 }
283
284 #[test]
285 fn test_host_os_is_windows() {
286 assert!(HostOs::WindowsWsl2.is_windows());
287 assert!(HostOs::WindowsNative.is_windows());
288 assert!(!HostOs::LinuxNative.is_windows());
289 assert!(!HostOs::MacOs.is_windows());
290 }
291
292 #[test]
293 fn test_host_os_needs_early_mount_check() {
294 assert!(HostOs::WindowsWsl2.needs_early_mount_check());
295 assert!(HostOs::WindowsNative.needs_early_mount_check());
296 assert!(!HostOs::LinuxNative.needs_early_mount_check());
297 assert!(!HostOs::MacOs.needs_early_mount_check());
298 }
299
300 #[test]
301 fn test_path_format_display_name() {
302 assert_eq!(PathFormat::Wsl2.display_name(), "WSL2");
303 assert_eq!(PathFormat::Windows.display_name(), "Windows");
304 assert_eq!(PathFormat::Posix.display_name(), "POSIX");
305 }
306
307 #[test]
308 fn test_runtime_environment_summary() {
309 let env = RuntimeEnvironment {
310 host_os: HostOs::WindowsWsl2,
311 path_format: PathFormat::Wsl2,
312 };
313
314 assert_eq!(env.summary(), "Windows (WSL2) (WSL2)");
315 }
316
317 #[test]
318 fn test_runtime_environment_is_wsl2() {
319 let env_wsl2 = RuntimeEnvironment {
320 host_os: HostOs::WindowsWsl2,
321 path_format: PathFormat::Wsl2,
322 };
323 assert!(env_wsl2.is_wsl2());
324
325 let env_linux = RuntimeEnvironment {
326 host_os: HostOs::LinuxNative,
327 path_format: PathFormat::Posix,
328 };
329 assert!(!env_linux.is_wsl2());
330 }
331
332 #[test]
333 fn test_runtime_environment_needs_special_handling() {
334 let env_wsl2 = RuntimeEnvironment {
336 host_os: HostOs::WindowsWsl2,
337 path_format: PathFormat::Wsl2,
338 };
339 assert!(env_wsl2.needs_special_handling());
340
341 let env_windows_native = RuntimeEnvironment {
343 host_os: HostOs::WindowsNative,
344 path_format: PathFormat::Windows,
345 };
346 assert!(env_windows_native.needs_special_handling());
347
348 let env_linux = RuntimeEnvironment {
350 host_os: HostOs::LinuxNative,
351 path_format: PathFormat::Posix,
352 };
353 assert!(!env_linux.needs_special_handling());
354
355 let env_macos = RuntimeEnvironment {
357 host_os: HostOs::MacOs,
358 path_format: PathFormat::Posix,
359 };
360 assert!(!env_macos.needs_special_handling());
361 }
362
363 #[test]
364 fn test_compose_command_type_default() {
365 assert_eq!(ComposeCommandType::default(), ComposeCommandType::Unknown);
366 }
367
368 #[test]
369 fn test_compose_command_type_equality() {
370 assert_eq!(
371 ComposeCommandType::DockerComposeSubcommand,
372 ComposeCommandType::DockerComposeSubcommand
373 );
374 assert_eq!(
375 ComposeCommandType::DockerComposeStandalone,
376 ComposeCommandType::DockerComposeStandalone
377 );
378 assert_eq!(ComposeCommandType::Unknown, ComposeCommandType::Unknown);
379
380 assert_ne!(
381 ComposeCommandType::DockerComposeSubcommand,
382 ComposeCommandType::DockerComposeStandalone
383 );
384 assert_ne!(
385 ComposeCommandType::DockerComposeSubcommand,
386 ComposeCommandType::Unknown
387 );
388 }
389
390 #[test]
391 fn test_compose_command_type_clone_copy() {
392 let original = ComposeCommandType::DockerComposeSubcommand;
393 let cloned = original.clone();
394 let copied = original;
395
396 assert_eq!(original, cloned);
397 assert_eq!(original, copied);
398 }
399
400 #[test]
401 fn test_compose_command_type_debug() {
402 let subcommand = ComposeCommandType::DockerComposeSubcommand;
404 let standalone = ComposeCommandType::DockerComposeStandalone;
405 let unknown = ComposeCommandType::Unknown;
406
407 assert_eq!(format!("{:?}", subcommand), "DockerComposeSubcommand");
408 assert_eq!(format!("{:?}", standalone), "DockerComposeStandalone");
409 assert_eq!(format!("{:?}", unknown), "Unknown");
410 }
411}