diff --git a/installer/HermesHubSetup.cs b/installer/HermesHubSetup.cs index c2b70f1..f4f4521 100644 --- a/installer/HermesHubSetup.cs +++ b/installer/HermesHubSetup.cs @@ -18,7 +18,7 @@ namespace HermesHubSetup // Подставляется сборщиком из фактического git-коммита. Раньше здесь // жил зашитый "8cddc9f", то есть манифест сообщал неправду о том, из // какого кода собран установщик. - public const string BuildCommit = "8daeafa"; + public const string BuildCommit = "9641957"; public const string MIN_HERMES_VERSION = "0.20.0"; public const string MAX_TESTED_HERMES = "0.20.4"; diff --git a/installer/install-linux.sh b/installer/install-linux.sh index b77c45c..3f9556d 100644 --- a/installer/install-linux.sh +++ b/installer/install-linux.sh @@ -11,6 +11,27 @@ HUB_VERSION="0.1.3" DEFAULT_HERMES_HOME="$HOME/.hermes" HERMES_HOME="${HERMES_HOME:-$DEFAULT_HERMES_HOME}" +# Установка пользовательская: всё ложится в $HOME/.hermes и $HOME/.local/bin, +# службы не ставятся, root не нужен. Под sudo домашним каталогом становится +# /root, венв Hermes там не находится, установщик сваливается на системный +# python — а он в Ubuntu 24.04 закрыт для pip (PEP 668). В итоге установка +# уходит в /root/.hermes, где владелец её не видит, и падает на проверке. +# Молча ставить не туда нельзя, поэтому отказываемся сразу и по делу. +if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ] && [ -z "${HERMES_ALLOW_ROOT:-}" ]; then + echo "❌ Установщик запущен через sudo." >&2 + echo "" >&2 + echo " Hermes Hub ставится в домашний каталог пользователя, а под sudo" >&2 + echo " это /root — туда, где ни аккаунты, ни Hermes Agent не лежат." >&2 + echo "" >&2 + # $0 здесь — распакованная копия во временном каталоге, называть её + # владельцу бессмысленно: этого файла через минуту не будет. + echo " Запустите тот же установщик от своего имени, без sudo." >&2 + echo "" >&2 + echo " Если установка в /root действительно нужна, задайте" >&2 + echo " HERMES_ALLOW_ROOT=1." >&2 + exit 3 +fi + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" @@ -84,6 +105,8 @@ if [ -f "$HERMES_HOME/hermes-agent/venv/bin/python3" ]; then PYTHON_BIN="$HERMES_HOME/hermes-agent/venv/bin/python3" elif [ -f "$HERMES_HOME/hermes-agent/venv/bin/python" ]; then PYTHON_BIN="$HERMES_HOME/hermes-agent/venv/bin/python" +elif [ -x "$HERMES_HOME/venv/bin/python3" ]; then + PYTHON_BIN="$HERMES_HOME/venv/bin/python3" elif command -v python3 >/dev/null 2>&1; then PYTHON_BIN="$(command -v python3)" elif command -v python >/dev/null 2>&1; then @@ -111,12 +134,48 @@ echo "[2/6] Verifying Python dependencies..." DEPS_OK=true "$PYTHON_BIN" -c "import fastapi, uvicorn, pydantic, psutil, yaml; print('DEPS_OK')" >/dev/null 2>&1 || DEPS_OK=false +HUB_DEPS="fastapi uvicorn pydantic psutil pyyaml" + if [ "$DEPS_OK" != "true" ]; then - echo " Installing required packages (fastapi, uvicorn, pydantic, psutil, pyyaml)..." - "$PYTHON_BIN" -m pip install --no-warn-script-location -q fastapi uvicorn pydantic psutil pyyaml || { - echo "⚠️ Warning: pip install returned non-zero code. Trying with --user..." - "$PYTHON_BIN" -m pip install --user --no-warn-script-location -q fastapi uvicorn pydantic psutil pyyaml || true - } + # Системный python в Debian и Ubuntu помечен как externally-managed + # (PEP 668) и отклоняет pip install — и обычный, и с --user. Обходить это + # через --break-system-packages нельзя: имя флага не преувеличивает, так + # ломают питон всей машины. Правильный ответ — собственный venv. + NEED_VENV=false + if [ -f "/usr/lib/python$PY_VER/EXTERNALLY-MANAGED" ] || [ -f "/usr/lib/python3/EXTERNALLY-MANAGED" ]; then + case "$PYTHON_BIN" in + */venv/bin/*) : ;; + *) NEED_VENV=true ;; + esac + fi + + if [ "$NEED_VENV" = "true" ]; then + echo " Системный Python защищён от изменений (PEP 668)." + echo " Создаю отдельное окружение: $HERMES_HOME/venv" + if ! "$PYTHON_BIN" -m venv "$HERMES_HOME/venv" 2>/dev/null; then + echo "❌ Не удалось создать виртуальное окружение." >&2 + echo " Установите пакет python3-venv:" >&2 + echo " sudo apt install python3-venv" >&2 + exit 11 + fi + PYTHON_BIN="$HERMES_HOME/venv/bin/python3" + echo " Using Python: $PYTHON_BIN" + fi + + echo " Installing required packages ($HUB_DEPS)..." + # shellcheck disable=SC2086 + if ! "$PYTHON_BIN" -m pip install --no-warn-script-location -q $HUB_DEPS; then + echo "❌ Не удалось установить зависимости через $PYTHON_BIN." >&2 + echo " Установка прервана: без них хаб не запустится." >&2 + exit 12 + fi +fi + +# Проверяем результат, а не код возврата pip: установка «прошла», а модуля нет — +# именно так предыдущая сборка дошла до проверки и упала на ней. +if ! "$PYTHON_BIN" -c "import fastapi, uvicorn, pydantic, psutil, yaml" >/dev/null 2>&1; then + echo "❌ Зависимости не импортируются даже после установки ($PYTHON_BIN)." >&2 + exit 13 fi # 3. Mirror Plugin Files to ~/.hermes/plugins/antigravity-provider (with cleanup of stale files) diff --git a/launcher/HermesHub.exe b/launcher/HermesHub.exe index 1f154d0..cd5b237 100644 Binary files a/launcher/HermesHub.exe and b/launcher/HermesHub.exe differ diff --git a/launcher/HermesHubWeb.exe b/launcher/HermesHubWeb.exe index fcf1375..c395499 100644 Binary files a/launcher/HermesHubWeb.exe and b/launcher/HermesHubWeb.exe differ diff --git a/launcher/hermes-hub-web.sh b/launcher/hermes-hub-web.sh index 4b424d8..a759ec2 100644 --- a/launcher/hermes-hub-web.sh +++ b/launcher/hermes-hub-web.sh @@ -16,6 +16,11 @@ if [ -f "$HERMES_HOME/hermes-agent/venv/bin/python3" ]; then PYTHON_BIN="$HERMES_HOME/hermes-agent/venv/bin/python3" elif [ -f "$HERMES_HOME/hermes-agent/venv/bin/python" ]; then PYTHON_BIN="$HERMES_HOME/hermes-agent/venv/bin/python" +elif [ -x "$HERMES_HOME/venv/bin/python3" ]; then + # Окружение, созданное установщиком, когда системный python закрыт + # правилом PEP 668. Без этой ветки запуск уходил бы на /usr/bin/python3, + # где зависимостей нет и быть не может. + PYTHON_BIN="$HERMES_HOME/venv/bin/python3" elif command -v python3 >/dev/null 2>&1; then PYTHON_BIN="$(command -v python3)" elif command -v python >/dev/null 2>&1; then diff --git a/tests/test_installer.py b/tests/test_installer.py index ff77072..d8e7d28 100644 --- a/tests/test_installer.py +++ b/tests/test_installer.py @@ -72,3 +72,50 @@ def test_silent_installer_fails_without_hermes(tmp_path): res = subprocess.run([str(SETUP_EXE), "/silent"], env=env, capture_output=True, text=True) assert res.returncode != 0, f"Expected failure for missing Hermes, got {res.returncode}" + + +# ── Установка на Linux: sudo и защищённый системный Python ── +# +# Установка на сервере владельца упала так: запуск через sudo увёл всё в +# /root/.hermes, венв Hermes там не нашёлся, установщик взял /usr/bin/python3, +# а он в Ubuntu 24.04 помечен EXTERNALLY-MANAGED и отклоняет pip. Проверка +# после установки честно упала на «No module named 'fastapi'». + +def _linux_installer_text() -> str: + root = Path(__file__).resolve().parent.parent + return (root / "installer" / "install-linux.sh").read_text(encoding="utf-8") + + +def test_linux_installer_refuses_sudo(): + text = _linux_installer_text() + assert "SUDO_USER" in text, "запуск через sudo обязан распознаваться" + assert "HERMES_ALLOW_ROOT" in text, "должен быть осознанный способ обойти отказ" + assert "exit 3" in text + + +def test_linux_installer_creates_own_venv_when_system_python_is_managed(): + text = _linux_installer_text() + assert "EXTERNALLY-MANAGED" in text, "PEP 668 должен распознаваться" + assert "-m venv" in text, "ответ на PEP 668 — собственное окружение" + # --break-system-packages ломает питон всей машины: имя флага не + # преувеличивает. Упоминание в пояснении допустимо, применение — нет, + # поэтому смотрим только на исполняемые строки. + code_lines = [ + line for line in text.splitlines() if not line.lstrip().startswith("#") + ] + assert not any("--break-system-packages" in line for line in code_lines) + + +def test_linux_installer_fails_loudly_on_missing_deps(): + """Раньше pip падал, а установка продолжалась — до отказа на проверке.""" + text = _linux_installer_text() + assert "exit 12" in text and "exit 13" in text + assert "|| true" not in text.split("Installing required packages")[1][:400] + + +def test_linux_launcher_knows_about_installer_venv(): + root = Path(__file__).resolve().parent.parent + launcher = (root / "launcher" / "hermes-hub-web.sh").read_text(encoding="utf-8") + assert '$HERMES_HOME/venv/bin/python3' in launcher, ( + "иначе запуск уйдёт на системный python, где зависимостей нет" + )