From 7bb688aa0096cee67b3caf42553e82cbb9a42ab8 Mon Sep 17 00:00:00 2001 From: stablegenius49 <185121704+stablegenius49@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:11:44 -0700 Subject: [PATCH] fix: install krunkit for macOS podman setup --- src/tui/utils/platform.py | 2 + src/tui/utils/startup_checks.py | 33 +++++++++++- tests/unit/test_startup_checks_podman.py | 69 ++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_startup_checks_podman.py diff --git a/src/tui/utils/platform.py b/src/tui/utils/platform.py index a1bc138ca..221f7911a 100644 --- a/src/tui/utils/platform.py +++ b/src/tui/utils/platform.py @@ -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 """ diff --git a/src/tui/utils/startup_checks.py b/src/tui/utils/startup_checks.py index d4ecbe1bd..c4bb38e15 100644 --- a/src/tui/utils/startup_checks.py +++ b/src/tui/utils/startup_checks.py @@ -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"): @@ -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: @@ -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( diff --git a/tests/unit/test_startup_checks_podman.py b/tests/unit/test_startup_checks_podman.py new file mode 100644 index 000000000..f35f32a88 --- /dev/null +++ b/tests/unit/test_startup_checks_podman.py @@ -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()