Skip to main content

mvm_cli/
bootstrap.rs

1use anyhow::Result;
2
3use crate::ui;
4use mvm_core::platform::{self, Platform};
5use mvm_runtime::shell;
6
7/// Check that a package manager is available for the current platform.
8///
9/// - macOS: requires Homebrew
10/// - Linux: requires apt, dnf, or pacman
11pub fn check_package_manager() -> Result<()> {
12    if cfg!(target_os = "macos") {
13        check_homebrew()
14    } else {
15        check_linux_package_manager()
16    }
17}
18
19/// Check if Homebrew is installed and accessible (macOS only).
20pub fn check_homebrew() -> Result<()> {
21    which::which("brew").map_err(|_| {
22        anyhow::anyhow!(
23            "Homebrew is not installed.\n\
24             Install it first:\n\n  \
25             /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n\n\
26             Then run 'mvm bootstrap' again."
27        )
28    })?;
29    ui::info("Homebrew found.");
30    Ok(())
31}
32
33/// Check that a Linux package manager is available.
34fn check_linux_package_manager() -> Result<()> {
35    for cmd in &["apt-get", "dnf", "pacman"] {
36        if which::which(cmd).is_ok() {
37            ui::info(&format!("Package manager found: {}", cmd));
38            return Ok(());
39        }
40    }
41    anyhow::bail!(
42        "No supported package manager found (apt-get, dnf, or pacman).\n\
43         Install Lima manually: https://lima-vm.io/docs/installation/"
44    )
45}
46
47/// Install Lima if not already installed.
48///
49/// On native Linux with KVM, Lima is not required — this is a no-op.
50/// On macOS or Linux without KVM: installs Lima via package manager.
51pub fn ensure_lima() -> Result<()> {
52    if platform::current() == Platform::LinuxNative {
53        ui::info("Native Linux with KVM detected — Lima not required.");
54        return Ok(());
55    }
56
57    if which::which("limactl").is_ok() {
58        let output = shell::run_host("limactl", &["--version"])?;
59        let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
60        ui::info(&format!("Lima already installed: {}", version));
61        return Ok(());
62    }
63
64    if cfg!(target_os = "macos") {
65        ui::info("Installing Lima via Homebrew...");
66        shell::run_host_visible("brew", &["install", "lima"])?;
67    } else {
68        install_lima_linux()?;
69    }
70
71    which::which("limactl").map_err(|_| {
72        anyhow::anyhow!("Lima installation completed but 'limactl' not found in PATH.")
73    })?;
74
75    ui::success("Lima installed successfully.");
76    Ok(())
77}
78
79/// Install Lima on Linux via package manager or direct binary download.
80fn install_lima_linux() -> Result<()> {
81    if which::which("apt-get").is_ok() {
82        ui::info("Installing Lima via apt...");
83        shell::run_host_visible(
84            "bash",
85            &["-c", "curl -fsSL https://lima-vm.io/install.sh | sudo sh"],
86        )?;
87    } else if which::which("dnf").is_ok() {
88        ui::info("Installing Lima via dnf...");
89        shell::run_host_visible(
90            "bash",
91            &["-c", "curl -fsSL https://lima-vm.io/install.sh | sudo sh"],
92        )?;
93    } else if which::which("pacman").is_ok() {
94        ui::info("Installing Lima via pacman...");
95        shell::run_host_visible("sudo", &["pacman", "-S", "--noconfirm", "lima"])?;
96    } else {
97        anyhow::bail!(
98            "No supported package manager found. Install Lima manually:\n\
99             https://lima-vm.io/docs/installation/"
100        );
101    }
102    Ok(())
103}
104
105/// Check if the platform requires Lima.
106pub fn is_lima_required() -> bool {
107    platform::current().needs_lima()
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_check_homebrew_error_message() {
116        if which::which("brew").is_err() {
117            let err = check_homebrew().unwrap_err();
118            let msg = err.to_string();
119            assert!(msg.contains("Homebrew is not installed"));
120            assert!(msg.contains("curl -fsSL"));
121            assert!(msg.contains("mvm bootstrap"));
122        } else {
123            assert!(check_homebrew().is_ok());
124        }
125    }
126
127    #[test]
128    fn test_ensure_lima_when_limactl_present() {
129        if which::which("limactl").is_ok() {
130            assert!(ensure_lima().is_ok());
131        }
132    }
133
134    #[test]
135    fn test_is_lima_required() {
136        let _ = is_lima_required();
137    }
138}