fix(installer): установка на Linux падала на защищённом системном Python
Установка на сервере владельца прервалась на проверке: «No module named 'fastapi'». Причин две, и обе в установщике. Первая: запуск через sudo. Установка пользовательская — всё ложится в $HOME/.hermes и $HOME/.local/bin, root не нужен. Под sudo домашним каталогом становится /root, венв Hermes там не находится, и установщик уходит на системный python. Копия при этом ложится в /root/.hermes, где владелец её не видит. Теперь запуск через sudo распознаётся и отклоняется с объяснением; осознанный обход остаётся через HERMES_ALLOW_ROOT=1. Вторая: системный python в Ubuntu 24.04 помечен EXTERNALLY-MANAGED (PEP 668) и отклоняет pip install — и обычный, и с --user. Установщик обе неудачи проглатывал (|| true) и продолжал работу до отказа на проверке. Теперь при защищённом системном python создаётся собственное окружение $HERMES_HOME/venv, а неудача установки зависимостей прекращает установку с внятным кодом возврата. --break-system-packages не применяется: имя флага не преувеличивает. Пусковик научен той же ветке — иначе запуск уходил бы на системный python, где зависимостей нет и быть не может. Проверено исполнением на сервере под непривилегированным пользователем: окружение создано, зависимости установлены, HERMES_HUB_LINUX_VERIFY_OK, запуск через sudo отклонён с кодом 3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
9641957d5b
commit
001cd1f91d
6 changed files with 117 additions and 6 deletions
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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, где зависимостей нет"
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue