Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/tui/utils/platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,8 @@ def get_installation_instructions(self) -> str:

Or Podman:
brew install podman
brew tap slp/krunkit
brew install krunkit
podman machine init --memory 8192
podman machine start
"""
Expand Down
33 changes: 32 additions & 1 deletion src/tui/utils/startup_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,33 @@ def install_homebrew() -> bool:
return False


def ensure_krunkit_on_macos() -> bool:
"""Ensure krunkit is installed on macOS for Podman machine workflows."""
if get_platform() != "macOS":
return True

if has_cmd("krunkit"):
say(f"krunkit already installed: {shutil.which('krunkit')}")
return True

if not install_homebrew():
say("Cannot install krunkit without Homebrew.")
return False

say("krunkit is required for Podman machine on macOS.")
if not ask_yes_no("Install krunkit via Homebrew?"):
return False

try:
subprocess.run(["brew", "tap", "slp/krunkit"], check=True)
subprocess.run(["brew", "install", "krunkit"], check=True)
return True
except Exception as e:
say(f"Failed to install krunkit: {e}")
say("You can install it manually with: brew tap slp/krunkit && brew install krunkit")
return False


def install_podman() -> bool:
"""Install Podman CLI (not Desktop). Returns True if successful."""
if has_cmd("podman"):
Expand All @@ -254,11 +281,12 @@ def install_podman() -> bool:
say("Installing Podman via Homebrew...")
try:
subprocess.run(["brew", "install", "podman"], check=True)
return True
except Exception as e:
say(f"Failed: {e}")
return False

return ensure_krunkit_on_macos()

elif plat in ("Linux", "WSL"):
say("Installing Podman via package manager (may prompt for sudo password)...")
try:
Expand Down Expand Up @@ -351,6 +379,9 @@ def setup_podman_machine() -> bool:
if get_platform() != "macOS":
return podman_ready()

if not ensure_krunkit_on_macos():
return False

# Check if machine exists
try:
result = subprocess.run(
Expand Down
69 changes: 69 additions & 0 deletions tests/unit/test_startup_checks_podman.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Tests for Podman setup helpers in startup checks."""

from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
from unittest.mock import MagicMock, patch

MODULE_PATH = Path(__file__).resolve().parents[2] / "src" / "tui" / "utils" / "startup_checks.py"
SPEC = spec_from_file_location("startup_checks_under_test", MODULE_PATH)
startup_checks = module_from_spec(SPEC)
assert SPEC and SPEC.loader
SPEC.loader.exec_module(startup_checks)


def _run_result(returncode: int = 0, stdout: str = "", stderr: str = "") -> MagicMock:
result = MagicMock()
result.returncode = returncode
result.stdout = stdout
result.stderr = stderr
return result


def test_ensure_krunkit_on_macos_installs_when_missing():
with patch.object(startup_checks, "get_platform", return_value="macOS"), patch.object(
startup_checks, "has_cmd", side_effect=lambda cmd: cmd == "brew"
), patch.object(startup_checks, "ask_yes_no", return_value=True), patch.object(
startup_checks.subprocess, "run"
) as mock_run:
ok = startup_checks.ensure_krunkit_on_macos()

assert ok is True
calls = [call.args[0] for call in mock_run.call_args_list]
assert ["brew", "tap", "slp/krunkit"] in calls
assert ["brew", "install", "krunkit"] in calls


def test_ensure_krunkit_on_macos_skips_when_already_installed():
with patch.object(startup_checks, "get_platform", return_value="macOS"), patch.object(
startup_checks, "has_cmd", side_effect=lambda cmd: cmd == "krunkit"
), patch.object(startup_checks.subprocess, "run") as mock_run:
ok = startup_checks.ensure_krunkit_on_macos()

assert ok is True
mock_run.assert_not_called()


def test_setup_podman_machine_requires_krunkit_before_init():
with patch.object(startup_checks, "get_platform", return_value="macOS"), patch.object(
startup_checks, "ensure_krunkit_on_macos", return_value=False
) as mock_ensure, patch.object(startup_checks.subprocess, "run") as mock_run:
ok = startup_checks.setup_podman_machine()

assert ok is False
mock_ensure.assert_called_once_with()
mock_run.assert_not_called()


def test_install_podman_on_macos_installs_krunkit_dependency():
with patch.object(startup_checks, "get_platform", return_value="macOS"), patch.object(
startup_checks, "has_cmd", side_effect=lambda cmd: cmd == "brew"
), patch.object(startup_checks, "ask_yes_no", return_value=True), patch.object(
startup_checks, "ensure_krunkit_on_macos", return_value=True
) as mock_ensure, patch.object(
startup_checks.subprocess, "run", return_value=_run_result()
) as mock_run:
ok = startup_checks.install_podman()

assert ok is True
mock_run.assert_called_once_with(["brew", "install", "podman"], check=True)
mock_ensure.assert_called_once_with()
Loading