fix: locate uv binary before upgrade to handle non-standard PATH

When running as root or via sudo, /root/.local/bin may not be in PATH.
Use shutil.which with fallback to ~/.local/bin/uv, and fail clearly
if uv can't be found.
This commit is contained in:
Justin Bollinger
2026-03-19 18:05:00 -04:00
parent 0aa61a4c7c
commit c8b18f9595
2 changed files with 40 additions and 2 deletions

View File

@@ -831,11 +831,23 @@ def _run_upgrade():
)
raise SystemExit(1)
repo_root = git_root_result.stdout.strip()
# Locate the uv binary. It may not be in PATH when running as root or via sudo.
import shutil
uv = shutil.which("uv") or os.path.expanduser("~/.local/bin/uv")
if not os.path.isfile(uv):
print(
"\n Could not find the uv binary."
"\n Run manually: git pull && git fetch --tags && uv sync --reinstall-package hate_crack\n"
)
raise SystemExit(1)
result = subprocess.run(
# git fetch --tags ensures new release tags are visible to setuptools-scm.
# uv sync --reinstall-package forces hate_crack to be rebuilt from
# current source so setuptools-scm generates the correct version.
"git pull && git fetch --tags && uv sync --reinstall-package hate_crack",
f"git pull && git fetch --tags && {uv} sync --reinstall-package hate_crack",
shell=True,
cwd=repo_root,
)

View File

@@ -151,7 +151,9 @@ class TestCheckForUpdates:
hc_module, "REQUESTS_AVAILABLE", True
), patch("builtins.input", return_value="y"), patch(
"subprocess.run", side_effect=[git_root_proc, make_proc]
) as mock_run, pytest.raises(
) as mock_run, patch("shutil.which", return_value="/usr/local/bin/uv"), patch(
"os.path.isfile", return_value=True
), pytest.raises(
SystemExit
):
mock_requests.get.return_value = mock_resp
@@ -181,6 +183,8 @@ class TestCheckForUpdates:
hc_module, "REQUESTS_AVAILABLE", True
), patch("builtins.input", return_value="y"), patch(
"subprocess.run", side_effect=[git_root_proc, make_proc]
), patch("shutil.which", return_value="/usr/local/bin/uv"), patch(
"os.path.isfile", return_value=True
), pytest.raises(
SystemExit
):
@@ -216,6 +220,14 @@ class TestCheckForUpdates:
class TestRunUpgrade:
"""Tests for _run_upgrade() called directly via --update flag."""
@pytest.fixture(autouse=True)
def mock_uv(self):
"""Patch shutil.which and os.path.isfile so uv is always 'found'."""
with patch("shutil.which", return_value="/usr/local/bin/uv"), patch(
"os.path.isfile", return_value=True
):
yield
def test_run_upgrade_success(self, hc_module, capsys):
git_root_proc = MagicMock()
git_root_proc.returncode = 0
@@ -263,6 +275,20 @@ class TestRunUpgrade:
output = capsys.readouterr().out
assert "Run manually" in output
def test_run_upgrade_uv_not_found(self, hc_module, capsys):
git_root_proc = MagicMock()
git_root_proc.returncode = 0
git_root_proc.stdout = "/fake/repo\n"
with patch("subprocess.run", return_value=git_root_proc), patch(
"shutil.which", return_value=None
), patch("os.path.isfile", return_value=False), pytest.raises(SystemExit) as exc:
hc_module._run_upgrade()
assert exc.value.code == 1
output = capsys.readouterr().out
assert "uv binary" in output
def test_upgrade_prompt_ctrl_c_continues(self, hc_module, capsys):
mock_resp = MagicMock()
mock_resp.json.return_value = {"tag_name": "v99.0.0"}