use std::path::Path;
use std::sync::OnceLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Platform {
MacOS,
LinuxNative,
LinuxNoKvm,
Wsl2,
Windows,
}
impl Platform {
pub fn needs_lima(self) -> bool {
match self {
Platform::MacOS => !self.has_apple_containers(),
Platform::LinuxNoKvm => true,
Platform::LinuxNative => false,
Platform::Wsl2 => !self.has_kvm() && !self.has_docker(),
Platform::Windows => false, }
}
pub fn has_kvm(self) -> bool {
match self {
Platform::LinuxNative => true,
Platform::Wsl2 => Path::new("/dev/kvm").exists(),
_ => false,
}
}
pub fn supports_native_runner(self) -> bool {
matches!(self, Platform::LinuxNative) || (matches!(self, Platform::Wsl2) && self.has_kvm())
}
pub fn has_apple_containers(self) -> bool {
if !matches!(self, Platform::MacOS) {
return false;
}
is_macos_26_or_later()
}
pub fn has_docker(self) -> bool {
static DOCKER_AVAILABLE: OnceLock<bool> = OnceLock::new();
*DOCKER_AVAILABLE.get_or_init(|| {
std::process::Command::new("docker")
.args(["version", "--format", "{{.Server.Version}}"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
})
}
pub fn has_host_nix(self) -> bool {
static HOST_NIX: OnceLock<bool> = OnceLock::new();
*HOST_NIX.get_or_init(|| {
if std::process::Command::new("nix")
.args(["--version"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
{
return true;
}
for path in &[
"/nix/var/nix/profiles/default/bin/nix",
"/run/current-system/sw/bin/nix",
] {
if Path::new(path).exists() {
return true;
}
}
false
})
}
pub fn is_wsl(self) -> bool {
matches!(self, Platform::Wsl2)
}
pub fn is_windows(self) -> bool {
matches!(self, Platform::Windows)
}
}
fn is_macos_26_or_later() -> bool {
#[cfg(target_os = "macos")]
{
if cfg!(not(target_arch = "aarch64")) {
return false;
}
macos_major_version() >= 26
}
#[cfg(not(target_os = "macos"))]
{
false
}
}
#[cfg(target_os = "macos")]
fn macos_major_version() -> u32 {
use std::process::Command;
Command::new("sw_vers")
.arg("-productVersion")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.and_then(|v| v.trim().split('.').next().map(String::from))
.and_then(|major| major.parse::<u32>().ok())
.unwrap_or(0)
}
fn is_wsl2() -> bool {
#[cfg(target_os = "linux")]
{
std::fs::read_to_string("/proc/version")
.map(|v| {
let lower = v.to_lowercase();
lower.contains("microsoft") || lower.contains("wsl")
})
.unwrap_or(false)
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
impl std::fmt::Display for Platform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Platform::MacOS => write!(f, "macOS"),
Platform::LinuxNative => write!(f, "Linux (native KVM)"),
Platform::LinuxNoKvm => write!(f, "Linux (no KVM)"),
Platform::Wsl2 => {
if self.has_kvm() {
write!(f, "WSL2 (KVM available)")
} else {
write!(f, "WSL2")
}
}
Platform::Windows => write!(f, "Windows"),
}
}
}
static DETECTED: OnceLock<Platform> = OnceLock::new();
pub fn current() -> Platform {
*DETECTED.get_or_init(detect)
}
fn detect() -> Platform {
if cfg!(target_os = "macos") {
Platform::MacOS
} else if cfg!(target_os = "linux") {
if is_wsl2() {
Platform::Wsl2
} else if Path::new("/dev/kvm").exists() {
Platform::LinuxNative
} else {
Platform::LinuxNoKvm
}
} else if cfg!(target_os = "windows") {
Platform::Windows
} else {
Platform::LinuxNoKvm
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_returns_consistent_result() {
let a = current();
let b = current();
assert_eq!(a, b);
}
#[test]
fn test_platform_display() {
assert_eq!(Platform::LinuxNative.to_string(), "Linux (native KVM)");
assert_eq!(Platform::LinuxNoKvm.to_string(), "Linux (no KVM)");
assert_eq!(Platform::Windows.to_string(), "Windows");
}
#[test]
fn test_needs_lima() {
let macos_needs = Platform::MacOS.needs_lima();
if Platform::MacOS.has_apple_containers() {
assert!(!macos_needs, "macOS 26+ should not need Lima");
} else {
assert!(macos_needs, "macOS <26 should need Lima");
}
assert!(!Platform::LinuxNative.needs_lima());
assert!(Platform::LinuxNoKvm.needs_lima());
assert!(!Platform::Windows.needs_lima());
}
#[test]
fn test_has_kvm() {
assert!(!Platform::MacOS.has_kvm());
assert!(Platform::LinuxNative.has_kvm());
assert!(!Platform::LinuxNoKvm.has_kvm());
assert!(!Platform::Windows.has_kvm());
}
#[test]
fn test_supports_native_runner() {
assert!(!Platform::MacOS.supports_native_runner());
assert!(Platform::LinuxNative.supports_native_runner());
assert!(!Platform::LinuxNoKvm.supports_native_runner());
assert!(!Platform::Windows.supports_native_runner());
}
#[test]
fn test_has_apple_containers_non_macos() {
assert!(!Platform::LinuxNative.has_apple_containers());
assert!(!Platform::LinuxNoKvm.has_apple_containers());
assert!(!Platform::Wsl2.has_apple_containers());
assert!(!Platform::Windows.has_apple_containers());
}
#[test]
fn test_has_docker_returns_bool() {
let _ = Platform::MacOS.has_docker();
}
#[test]
fn test_current_platform_valid() {
let p = current();
let _ = p.needs_lima();
let _ = p.has_kvm();
let _ = p.supports_native_runner();
let _ = p.has_apple_containers();
let _ = p.has_docker();
}
#[test]
#[cfg(target_os = "macos")]
fn test_macos_major_version_is_reasonable() {
let version = macos_major_version();
assert!(version >= 10, "macOS version {version} seems too low");
}
}