diff --git a/flocks/updater/updater.py b/flocks/updater/updater.py index ff772bf..49db33f 100644 --- a/flocks/updater/updater.py +++ b/flocks/updater/updater.py @@ -110,6 +110,12 @@ def _windows_command_candidates(name: str) -> list[str]: return [name, f"{name}.exe", f"{name}.cmd", f"{name}.bat"] +def _running_from_legacy_uv_tool_install() -> bool: + """Return True when the current interpreter still comes from ``uv tool``.""" + executable = (sys.executable or "").replace("\\", "/").lower() + return "/uv/tools/flocks/" in executable + + def _windows_paths_match(left: str, right: str) -> bool: """Return True when two Windows paths likely point to the same launcher/script.""" if not left or not right: @@ -1771,7 +1777,6 @@ async def perform_update( ) install_root = _get_repo_root() current_version = get_current_version() - handover_prepared = False fmt = _choose_archive_format(ucfg.archive_format) @@ -2142,6 +2147,13 @@ def _refresh_global_cli_entry(install_root: Path) -> None: link.unlink(missing_ok=True) link.symlink_to(target) + if _running_from_legacy_uv_tool_install(): + log.info( + "updater.refresh_cli.defer_legacy_uninstall", + {"sys_executable": sys.executable}, + ) + return + uv = shutil.which("uv") if uv: try: diff --git a/tests/updater/test_updater.py b/tests/updater/test_updater.py index e7c8d27..53e2892 100644 --- a/tests/updater/test_updater.py +++ b/tests/updater/test_updater.py @@ -1,7 +1,6 @@ import os import shutil import subprocess -import sys import tarfile from os import utime from pathlib import Path @@ -544,6 +543,68 @@ def test_refresh_global_cli_entry_noop_when_venv_missing( assert not (link_dir / "flocks").exists() +def test_refresh_global_cli_entry_defers_legacy_uv_tool_uninstall_for_running_tool_env( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + monkeypatch.setattr(updater.sys, "platform", "darwin") + monkeypatch.setattr(updater.sys, "executable", "/Users/test/.local/share/uv/tools/flocks/bin/python") + monkeypatch.setattr(updater.Path, "home", lambda: tmp_path / "home") + monkeypatch.setattr(updater.shutil, "which", lambda _name: "/usr/local/bin/uv") + + calls: list[list[str]] = [] + + def fake_run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess[str]: + calls.append(cmd) + return subprocess.CompletedProcess(args=cmd, returncode=0, stdout="flocks 0.0.0\n", stderr="") + + monkeypatch.setattr(updater.subprocess, "run", fake_run) + + install_root = tmp_path / "project" + venv_flocks = install_root / ".venv" / "bin" / "flocks" + venv_flocks.parent.mkdir(parents=True) + venv_flocks.write_text("#!/usr/bin/env python\n", encoding="utf-8") + + updater._refresh_global_cli_entry(install_root) + + link = tmp_path / "home" / ".local" / "bin" / "flocks" + assert link.is_symlink() + assert link.resolve() == venv_flocks.resolve() + assert calls == [] + + +def test_refresh_global_cli_entry_uninstalls_legacy_uv_tool_after_switching_runtime( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + monkeypatch.setattr(updater.sys, "platform", "darwin") + monkeypatch.setattr(updater.sys, "executable", str(tmp_path / "project" / ".venv" / "bin" / "python")) + monkeypatch.setattr(updater.Path, "home", lambda: tmp_path / "home") + monkeypatch.setattr(updater.shutil, "which", lambda _name: "/usr/local/bin/uv") + + calls: list[list[str]] = [] + + def fake_run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess[str]: + calls.append(cmd) + if cmd == ["/usr/local/bin/uv", "tool", "list"]: + return subprocess.CompletedProcess(args=cmd, returncode=0, stdout="flocks 0.0.0\n", stderr="") + return subprocess.CompletedProcess(args=cmd, returncode=0, stdout="", stderr="") + + monkeypatch.setattr(updater.subprocess, "run", fake_run) + + install_root = tmp_path / "project" + venv_flocks = install_root / ".venv" / "bin" / "flocks" + venv_flocks.parent.mkdir(parents=True) + venv_flocks.write_text("#!/usr/bin/env python\n", encoding="utf-8") + + updater._refresh_global_cli_entry(install_root) + + assert calls == [ + ["/usr/local/bin/uv", "tool", "list"], + ["/usr/local/bin/uv", "tool", "uninstall", "flocks"], + ] + + @pytest.mark.asyncio async def test_validate_windows_restart_runtime_requires_venv_python(tmp_path: Path) -> None: assert await updater._validate_windows_restart_runtime(tmp_path) == (