feat(update): deployment detection and install command builder

This commit is contained in:
m4
2026-08-12 16:24:17 +08:00
parent 174f03b92d
commit 40b896bcb5
2 changed files with 147 additions and 0 deletions
+95
View File
@@ -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)]
+52
View File
@@ -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"