use std::env;
use std::sync::OnceLock;
use tokio::process::Command;
use tracing::{debug, info, warn};
static COMPOSE_COMMAND_TYPE: OnceLock<ComposeCommandType> = OnceLock::new();
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum ComposeCommandType {
DockerComposeSubcommand,
DockerComposeStandalone,
#[default]
Unknown,
}
pub async fn detect_compose_command_type() -> ComposeCommandType {
info!("🔍 Detecting Docker Compose command type...");
let output = Command::new("docker")
.args(["compose", "version"])
.output()
.await;
if let Ok(output) = output {
if output.status.success() {
let version_info = String::from_utf8_lossy(&output.stdout);
info!(
" ✅ Using docker compose subcommand: {}",
version_info.trim()
);
return ComposeCommandType::DockerComposeSubcommand;
}
debug!(
" docker compose version returned non-zero status: {:?}",
output.status
);
}
debug!(" Trying standalone docker-compose command...");
let output = Command::new("docker-compose")
.arg("--version")
.output()
.await;
if let Ok(output) = output {
if output.status.success() {
let version_info = String::from_utf8_lossy(&output.stdout);
info!(
" ✅ Using standalone docker-compose command: {}",
version_info.trim()
);
return ComposeCommandType::DockerComposeStandalone;
}
}
warn!(" ⚠️ No available Docker Compose command detected");
ComposeCommandType::Unknown
}
pub fn set_compose_command_type(compose_type: ComposeCommandType) {
if COMPOSE_COMMAND_TYPE.set(compose_type).is_err() {
debug!("Compose command type already set; ignoring duplicate initialization");
}
}
pub fn get_compose_command_type() -> ComposeCommandType {
COMPOSE_COMMAND_TYPE
.get()
.copied()
.unwrap_or(ComposeCommandType::Unknown)
}
#[derive(Debug, Clone, PartialEq)]
pub enum HostOs {
WindowsWsl2,
WindowsNative,
LinuxNative,
MacOs,
}
impl HostOs {
pub fn display_name(&self) -> &'static str {
match self {
HostOs::WindowsWsl2 => "Windows (WSL2)",
HostOs::WindowsNative => "Windows (Native)",
HostOs::LinuxNative => "Linux",
HostOs::MacOs => "macOS",
}
}
pub fn is_windows(&self) -> bool {
matches!(self, HostOs::WindowsWsl2 | HostOs::WindowsNative)
}
pub fn is_wsl2(&self) -> bool {
matches!(self, HostOs::WindowsWsl2)
}
pub fn needs_early_mount_check(&self) -> bool {
self.is_windows()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum PathFormat {
Wsl2,
Windows,
Posix,
}
impl PathFormat {
pub fn display_name(&self) -> &'static str {
match self {
PathFormat::Wsl2 => "WSL2",
PathFormat::Windows => "Windows",
PathFormat::Posix => "POSIX",
}
}
}
#[derive(Debug, Clone)]
pub struct RuntimeEnvironment {
pub host_os: HostOs,
pub path_format: PathFormat,
}
impl RuntimeEnvironment {
pub fn summary(&self) -> String {
format!(
"{} ({})",
self.host_os.display_name(),
self.path_format.display_name()
)
}
pub fn needs_special_handling(&self) -> bool {
self.host_os.is_windows()
}
pub fn is_wsl2(&self) -> bool {
self.host_os.is_wsl2()
}
}
pub fn detect_runtime_environment() -> RuntimeEnvironment {
debug!("🔍 Detecting runtime environment...");
let host_os = detect_host_os();
debug!(" Host OS: {:?}", host_os);
let path_format = detect_path_format(&host_os);
debug!(" Path format: {:?}", path_format);
let env = RuntimeEnvironment {
host_os,
path_format,
};
info!("✅ Runtime environment detected: {}", env.summary());
if env.needs_special_handling() {
info!("⚠️ Windows environment detected; mount directories should be created early");
}
env
}
fn detect_host_os() -> HostOs {
if is_running_in_wsl() {
return HostOs::WindowsWsl2;
}
match std::env::consts::OS {
"windows" => HostOs::WindowsNative,
"linux" => HostOs::LinuxNative,
"macos" => HostOs::MacOs,
other => {
debug!("Unknown operating system: {}, assuming Linux", other);
HostOs::LinuxNative
}
}
}
fn is_running_in_wsl() -> bool {
if let Ok(version) = std::fs::read_to_string("/proc/version") {
if version.to_lowercase().contains("microsoft") {
debug!("Detected WSL marker in /proc/version");
return true;
}
}
if env::var("WSL_DISTRO_NAME").is_ok() {
debug!("Detected WSL_DISTRO_NAME environment variable");
return true;
}
if env::var("WSLENV").is_ok() {
debug!("Detected WSLENV environment variable");
return true;
}
false
}
fn detect_path_format(host_os: &HostOs) -> PathFormat {
match host_os {
HostOs::WindowsWsl2 => PathFormat::Wsl2,
HostOs::WindowsNative => PathFormat::Windows,
HostOs::LinuxNative | HostOs::MacOs => PathFormat::Posix,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_host_os_display_name() {
assert_eq!(HostOs::WindowsWsl2.display_name(), "Windows (WSL2)");
assert_eq!(HostOs::WindowsNative.display_name(), "Windows (Native)");
assert_eq!(HostOs::LinuxNative.display_name(), "Linux");
assert_eq!(HostOs::MacOs.display_name(), "macOS");
}
#[test]
fn test_host_os_is_windows() {
assert!(HostOs::WindowsWsl2.is_windows());
assert!(HostOs::WindowsNative.is_windows());
assert!(!HostOs::LinuxNative.is_windows());
assert!(!HostOs::MacOs.is_windows());
}
#[test]
fn test_host_os_needs_early_mount_check() {
assert!(HostOs::WindowsWsl2.needs_early_mount_check());
assert!(HostOs::WindowsNative.needs_early_mount_check());
assert!(!HostOs::LinuxNative.needs_early_mount_check());
assert!(!HostOs::MacOs.needs_early_mount_check());
}
#[test]
fn test_path_format_display_name() {
assert_eq!(PathFormat::Wsl2.display_name(), "WSL2");
assert_eq!(PathFormat::Windows.display_name(), "Windows");
assert_eq!(PathFormat::Posix.display_name(), "POSIX");
}
#[test]
fn test_runtime_environment_summary() {
let env = RuntimeEnvironment {
host_os: HostOs::WindowsWsl2,
path_format: PathFormat::Wsl2,
};
assert_eq!(env.summary(), "Windows (WSL2) (WSL2)");
}
#[test]
fn test_runtime_environment_is_wsl2() {
let env_wsl2 = RuntimeEnvironment {
host_os: HostOs::WindowsWsl2,
path_format: PathFormat::Wsl2,
};
assert!(env_wsl2.is_wsl2());
let env_linux = RuntimeEnvironment {
host_os: HostOs::LinuxNative,
path_format: PathFormat::Posix,
};
assert!(!env_linux.is_wsl2());
}
#[test]
fn test_runtime_environment_needs_special_handling() {
let env_wsl2 = RuntimeEnvironment {
host_os: HostOs::WindowsWsl2,
path_format: PathFormat::Wsl2,
};
assert!(env_wsl2.needs_special_handling());
let env_windows_native = RuntimeEnvironment {
host_os: HostOs::WindowsNative,
path_format: PathFormat::Windows,
};
assert!(env_windows_native.needs_special_handling());
let env_linux = RuntimeEnvironment {
host_os: HostOs::LinuxNative,
path_format: PathFormat::Posix,
};
assert!(!env_linux.needs_special_handling());
let env_macos = RuntimeEnvironment {
host_os: HostOs::MacOs,
path_format: PathFormat::Posix,
};
assert!(!env_macos.needs_special_handling());
}
#[test]
fn test_compose_command_type_default() {
assert_eq!(ComposeCommandType::default(), ComposeCommandType::Unknown);
}
#[test]
fn test_compose_command_type_equality() {
assert_eq!(
ComposeCommandType::DockerComposeSubcommand,
ComposeCommandType::DockerComposeSubcommand
);
assert_eq!(
ComposeCommandType::DockerComposeStandalone,
ComposeCommandType::DockerComposeStandalone
);
assert_eq!(ComposeCommandType::Unknown, ComposeCommandType::Unknown);
assert_ne!(
ComposeCommandType::DockerComposeSubcommand,
ComposeCommandType::DockerComposeStandalone
);
assert_ne!(
ComposeCommandType::DockerComposeSubcommand,
ComposeCommandType::Unknown
);
}
#[test]
fn test_compose_command_type_clone_copy() {
let original = ComposeCommandType::DockerComposeSubcommand;
let cloned = original.clone();
let copied = original;
assert_eq!(original, cloned);
assert_eq!(original, copied);
}
#[test]
fn test_compose_command_type_debug() {
let subcommand = ComposeCommandType::DockerComposeSubcommand;
let standalone = ComposeCommandType::DockerComposeStandalone;
let unknown = ComposeCommandType::Unknown;
assert_eq!(format!("{:?}", subcommand), "DockerComposeSubcommand");
assert_eq!(format!("{:?}", standalone), "DockerComposeStandalone");
assert_eq!(format!("{:?}", unknown), "Unknown");
}
}