feat(update): deployment detection and install command builder
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
"""Self-update planner and standalone updater runner.
|
||||
|
||||
This module is STDLIB-ONLY on purpose: the file gets copied into the
|
||||
staging dir and executed after the EvoScientist package has been replaced
|
||||
on disk, so importing anything from EvoScientist would load mixed code.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Mapping, Sequence
|
||||
|
||||
DEPLOY_DOCKER = "docker"
|
||||
DEPLOY_SYSTEMD = "systemd"
|
||||
DEPLOY_UVTOOL = "cli-uvtool"
|
||||
DEPLOY_PIPX = "cli-pipx"
|
||||
DEPLOY_VENV = "cli-venv"
|
||||
|
||||
PID_WAIT_TIMEOUT = 300.0 # seconds waiting for the parent to exit
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SystemProbe:
|
||||
in_container: bool
|
||||
invocation_id: str | None
|
||||
uv_tool_pkg: bool
|
||||
pipx_pkg: bool
|
||||
|
||||
|
||||
def detect_deployment(probe: SystemProbe) -> str:
|
||||
if probe.in_container:
|
||||
return DEPLOY_DOCKER
|
||||
if probe.invocation_id:
|
||||
return DEPLOY_SYSTEMD
|
||||
if probe.uv_tool_pkg:
|
||||
return DEPLOY_UVTOOL
|
||||
if probe.pipx_pkg:
|
||||
return DEPLOY_PIPX
|
||||
return DEPLOY_VENV
|
||||
|
||||
|
||||
def _uv_tool_has_package(environ: Mapping[str, str]) -> bool:
|
||||
uv = shutil.which("uv")
|
||||
if uv is None:
|
||||
return False
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[uv, "tool", "dir"], capture_output=True, text=True, timeout=10
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return False
|
||||
if proc.returncode != 0:
|
||||
return False
|
||||
tool_dir = Path(proc.stdout.strip())
|
||||
return (tool_dir / "evoscientist").is_dir() or (tool_dir / "EvoScientist").is_dir()
|
||||
|
||||
|
||||
def _pipx_has_package() -> bool:
|
||||
home = Path.home()
|
||||
for base in (
|
||||
home / ".local" / "pipx" / "venvs",
|
||||
home / ".local" / "share" / "pipx" / "venvs",
|
||||
):
|
||||
if (base / "evoscientist").is_dir() or (base / "EvoScientist").is_dir():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def probe_system(environ: Mapping[str, str] | None = None) -> SystemProbe:
|
||||
env = os.environ if environ is None else environ
|
||||
in_container = Path("/.dockerenv").exists() or bool(env.get("container"))
|
||||
return SystemProbe(
|
||||
in_container=in_container,
|
||||
invocation_id=env.get("INVOCATION_ID") or None,
|
||||
uv_tool_pkg=False if in_container else _uv_tool_has_package(env),
|
||||
pipx_pkg=False if in_container else _pipx_has_package(),
|
||||
)
|
||||
|
||||
|
||||
def build_install_command(kind: str, artifact: Path, python: str) -> list[str]:
|
||||
if kind in (DEPLOY_UVTOOL, DEPLOY_SYSTEMD):
|
||||
# systemd only decides the restart strategy; the install command for a
|
||||
# systemd + non-uv host is overridden by the caller (build_plan).
|
||||
return ["uv", "tool", "install", "--force", str(artifact)]
|
||||
if kind == DEPLOY_PIPX:
|
||||
return ["pipx", "install", "--force", str(artifact)]
|
||||
return [python, "-m", "pip", "install", "--force-reinstall", str(artifact)]
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Tests for EvoScientist.updater deployment detection and plan building."""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from EvoScientist.updater import (
|
||||
DEPLOY_DOCKER,
|
||||
DEPLOY_PIPX,
|
||||
DEPLOY_SYSTEMD,
|
||||
DEPLOY_UVTOOL,
|
||||
DEPLOY_VENV,
|
||||
SystemProbe,
|
||||
build_install_command,
|
||||
detect_deployment,
|
||||
)
|
||||
|
||||
|
||||
def _probe(**kw):
|
||||
base = dict(in_container=False, invocation_id=None, uv_tool_pkg=False, pipx_pkg=False)
|
||||
return SystemProbe(**{**base, **kw})
|
||||
|
||||
|
||||
def test_docker_wins_over_systemd_marker():
|
||||
assert detect_deployment(_probe(in_container=True, invocation_id="abc")) == DEPLOY_DOCKER
|
||||
|
||||
|
||||
def test_systemd_before_cli():
|
||||
assert detect_deployment(_probe(invocation_id="abc", uv_tool_pkg=True)) == DEPLOY_SYSTEMD
|
||||
|
||||
|
||||
def test_uvtool_pipx_venv_order():
|
||||
assert detect_deployment(_probe(uv_tool_pkg=True, pipx_pkg=True)) == DEPLOY_UVTOOL
|
||||
assert detect_deployment(_probe(pipx_pkg=True)) == DEPLOY_PIPX
|
||||
assert detect_deployment(_probe()) == DEPLOY_VENV
|
||||
|
||||
|
||||
def test_install_commands():
|
||||
artifact = Path("/tmp/updates/v1/EvoScientist-1-py3-none-any.whl")
|
||||
assert build_install_command(DEPLOY_UVTOOL, artifact, "/usr/bin/python3") == [
|
||||
"uv", "tool", "install", "--force", str(artifact),
|
||||
]
|
||||
assert build_install_command(DEPLOY_PIPX, artifact, "/usr/bin/python3") == [
|
||||
"pipx", "install", "--force", str(artifact),
|
||||
]
|
||||
assert build_install_command(DEPLOY_VENV, artifact, "/usr/bin/python3") == [
|
||||
"/usr/bin/python3", "-m", "pip", "install", "--force-reinstall", str(artifact),
|
||||
]
|
||||
systemd_cmd = build_install_command(DEPLOY_SYSTEMD, artifact, "/usr/bin/python3")
|
||||
assert systemd_cmd[0:2] == ["uv", "tool"] or systemd_cmd[0] == "/usr/bin/python3"
|
||||
Reference in New Issue
Block a user