Skip to main content

mvm_core/
platform.rs

1use std::path::Path;
2use std::sync::OnceLock;
3
4/// The execution environment for running workloads.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Platform {
7    /// macOS — Apple Virtualization.framework on 26+, Lima fallback on older
8    MacOS,
9    /// Native Linux with /dev/kvm available — run Firecracker directly
10    LinuxNative,
11    /// Linux without /dev/kvm (not WSL) — requires Lima or Docker
12    LinuxNoKvm,
13    /// WSL2 — may have KVM (Hyper-V nested virt), prefers Docker as fallback
14    Wsl2,
15    /// Native Windows — Docker only (no Linux kernel)
16    Windows,
17}
18
19impl Platform {
20    /// Whether this platform needs Lima to run Firecracker.
21    /// Returns false for platforms that have better alternatives (Apple VZ, Docker, native KVM).
22    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, // Lima doesn't run on Windows
29        }
30    }
31
32    /// Whether this platform can run Firecracker directly via /dev/kvm.
33    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    /// Whether the microvm.nix runner can execute natively (without Lima).
42    pub fn supports_native_runner(self) -> bool {
43        matches!(self, Platform::LinuxNative) || (matches!(self, Platform::Wsl2) && self.has_kvm())
44    }
45
46    /// Whether Apple Containers are available on this platform.
47    ///
48    /// Requires macOS 26+ on Apple Silicon.
49    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    /// Whether Docker is available on this platform.
57    ///
58    /// Runtime check — calls `docker version` to verify the daemon is running.
59    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    /// Whether Nix is available on the host and can build Linux targets.
71    ///
72    /// On macOS this requires nix-daemon with a linux-builder configured.
73    /// On native Linux this is always true if `nix` is on PATH.
74    /// When true, `nix build` can run on the host without Lima.
75    pub fn has_host_nix(self) -> bool {
76        static HOST_NIX: OnceLock<bool> = OnceLock::new();
77        *HOST_NIX.get_or_init(|| {
78            // Try PATH first
79            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            // Check common Nix install locations (freshly installed Nix may
90            // not be on PATH if the shell profile hasn't been sourced yet)
91            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    /// Whether this platform is WSL2.
104    pub fn is_wsl(self) -> bool {
105        matches!(self, Platform::Wsl2)
106    }
107
108    /// Whether this platform is native Windows.
109    pub fn is_windows(self) -> bool {
110        matches!(self, Platform::Windows)
111    }
112}
113
114/// Check whether the current macOS version is 26.0 or later.
115fn 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/// Read the macOS major version number via sysctl.
130#[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
143/// Check if running inside WSL2 by reading /proc/version.
144fn 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
178/// Cached platform detection result.
179static DETECTED: OnceLock<Platform> = OnceLock::new();
180
181/// Detect the current platform. Result is cached after the first call.
182pub 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        // Unknown OS — try Docker as universal fallback
201        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        // macOS: needs Lima only if Apple Containers are NOT available
226        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        // Just verify it doesn't panic; result depends on environment
264        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}