diff --git a/myagents/commands/uninstall.py b/myagents/commands/uninstall.py index 4bef9bd..515aa45 100644 --- a/myagents/commands/uninstall.py +++ b/myagents/commands/uninstall.py @@ -57,10 +57,16 @@ def _remove_path(path: Path, removed: list[Path]) -> None: def _pip_uninstall() -> list[str]: """Uninstall the pip packages (removes their pip-owned entry points).""" + import sysconfig + if os.environ.get("VIRTUAL_ENV") and shutil.which("uv"): base = ["uv", "pip", "uninstall"] else: - base = [sys.executable, "-m", "pip", "uninstall", "-y"] + base = [sys.executable, "-m", "pip", "uninstall"] + stdlib = Path(sysconfig.get_path("stdlib")) + if (stdlib / "EXTERNALLY-MANAGED").exists(): + base.append("--break-system-packages") + base.append("-y") warnings: list[str] = [] for pkg in PY_PACKAGES: result = subprocess.run([*base, pkg], capture_output=True, text=True) diff --git a/myagents/commands/upgrade.py b/myagents/commands/upgrade.py index 87cebcc..225e6b1 100644 --- a/myagents/commands/upgrade.py +++ b/myagents/commands/upgrade.py @@ -147,11 +147,18 @@ def _extract(tarball: Path, dest: Path) -> None: def _pip_install(pkg_dir: Path) -> subprocess.CompletedProcess: import os + import sysconfig if os.environ.get("VIRTUAL_ENV") and shutil.which("uv"): cmd = ["uv", "pip", "install", "-e", str(pkg_dir), "--quiet"] else: - cmd = [sys.executable, "-m", "pip", "install", "-e", str(pkg_dir), "--quiet"] + cmd = [sys.executable, "-m", "pip", "install"] + # Ubuntu 24.04+ marks the system Python externally-managed (PEP 668); + # without the flag pip refuses to install there at all. + stdlib = Path(sysconfig.get_path("stdlib")) + if (stdlib / "EXTERNALLY-MANAGED").exists(): + cmd.append("--break-system-packages") + cmd += ["-e", str(pkg_dir), "--quiet"] return subprocess.run(cmd, capture_output=True, text=True) diff --git a/tests/test_upgrade.py b/tests/test_upgrade.py index 813b369..cb79967 100644 --- a/tests/test_upgrade.py +++ b/tests/test_upgrade.py @@ -230,3 +230,41 @@ class TestFetch: dest = tmp_path / "out.tar.gz" up_mod._download("http://x/f", "u", "p", dest) assert dest.read_bytes() == payload + + +class TestPipInstallFlags: + def test_uses_break_system_packages_on_externally_managed( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + import sysconfig + + em = tmp_path / "EXTERNALLY-MANAGED" + em.write_text("[externally-managed]\n") + monkeypatch.setattr( + sysconfig, "get_path", lambda name: str(tmp_path) if name == "stdlib" else "" + ) + seen: list[list[str]] = [] + monkeypatch.setattr( + "subprocess.run", + lambda cmd, **kw: seen.append(cmd) + or type("R", (), {"returncode": 0, "stderr": ""})(), + ) + up_mod._pip_install(tmp_path / "pkg") + assert "--break-system-packages" in seen[0] + + def test_skips_flag_without_externally_managed( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + import sysconfig + + monkeypatch.setattr( + sysconfig, "get_path", lambda name: str(tmp_path) if name == "stdlib" else "" + ) + seen: list[list[str]] = [] + monkeypatch.setattr( + "subprocess.run", + lambda cmd, **kw: seen.append(cmd) + or type("R", (), {"returncode": 0, "stderr": ""})(), + ) + up_mod._pip_install(tmp_path / "pkg") + assert "--break-system-packages" not in seen[0]