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-коммита. Раньше здесь
|
// Подставляется сборщиком из фактического git-коммита. Раньше здесь
|
||||||
// жил зашитый "8cddc9f", то есть манифест сообщал неправду о том, из
|
// жил зашитый "8cddc9f", то есть манифест сообщал неправду о том, из
|
||||||
// какого кода собран установщик.
|
// какого кода собран установщик.
|
||||||
public const string BuildCommit = "8daeafa";
|
public const string BuildCommit = "9641957";
|
||||||
public const string MIN_HERMES_VERSION = "0.20.0";
|
public const string MIN_HERMES_VERSION = "0.20.0";
|
||||||
public const string MAX_TESTED_HERMES = "0.20.4";
|
public const string MAX_TESTED_HERMES = "0.20.4";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,27 @@ HUB_VERSION="0.1.3"
|
||||||
DEFAULT_HERMES_HOME="$HOME/.hermes"
|
DEFAULT_HERMES_HOME="$HOME/.hermes"
|
||||||
HERMES_HOME="${HERMES_HOME:-$DEFAULT_HERMES_HOME}"
|
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)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && 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"
|
PYTHON_BIN="$HERMES_HOME/hermes-agent/venv/bin/python3"
|
||||||
elif [ -f "$HERMES_HOME/hermes-agent/venv/bin/python" ]; then
|
elif [ -f "$HERMES_HOME/hermes-agent/venv/bin/python" ]; then
|
||||||
PYTHON_BIN="$HERMES_HOME/hermes-agent/venv/bin/python"
|
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
|
elif command -v python3 >/dev/null 2>&1; then
|
||||||
PYTHON_BIN="$(command -v python3)"
|
PYTHON_BIN="$(command -v python3)"
|
||||||
elif command -v python >/dev/null 2>&1; then
|
elif command -v python >/dev/null 2>&1; then
|
||||||
|
|
@ -111,12 +134,48 @@ echo "[2/6] Verifying Python dependencies..."
|
||||||
DEPS_OK=true
|
DEPS_OK=true
|
||||||
"$PYTHON_BIN" -c "import fastapi, uvicorn, pydantic, psutil, yaml; print('DEPS_OK')" >/dev/null 2>&1 || DEPS_OK=false
|
"$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
|
if [ "$DEPS_OK" != "true" ]; then
|
||||||
echo " Installing required packages (fastapi, uvicorn, pydantic, psutil, pyyaml)..."
|
# Системный python в Debian и Ubuntu помечен как externally-managed
|
||||||
"$PYTHON_BIN" -m pip install --no-warn-script-location -q fastapi uvicorn pydantic psutil pyyaml || {
|
# (PEP 668) и отклоняет pip install — и обычный, и с --user. Обходить это
|
||||||
echo "⚠️ Warning: pip install returned non-zero code. Trying with --user..."
|
# через --break-system-packages нельзя: имя флага не преувеличивает, так
|
||||||
"$PYTHON_BIN" -m pip install --user --no-warn-script-location -q fastapi uvicorn pydantic psutil pyyaml || true
|
# ломают питон всей машины. Правильный ответ — собственный 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
|
fi
|
||||||
|
|
||||||
# 3. Mirror Plugin Files to ~/.hermes/plugins/antigravity-provider (with cleanup of stale files)
|
# 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"
|
PYTHON_BIN="$HERMES_HOME/hermes-agent/venv/bin/python3"
|
||||||
elif [ -f "$HERMES_HOME/hermes-agent/venv/bin/python" ]; then
|
elif [ -f "$HERMES_HOME/hermes-agent/venv/bin/python" ]; then
|
||||||
PYTHON_BIN="$HERMES_HOME/hermes-agent/venv/bin/python"
|
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
|
elif command -v python3 >/dev/null 2>&1; then
|
||||||
PYTHON_BIN="$(command -v python3)"
|
PYTHON_BIN="$(command -v python3)"
|
||||||
elif command -v python >/dev/null 2>&1; then
|
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)
|
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}"
|
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