#!/usr/bin/env bash set -euo pipefail # Проверка среды разработки против versions.env. # # Зачем. # # versions.env — единый контракт «продукт / платформа / toolchain», и сборка # релиза его соблюдает: она скачивает Go, Node и Bun ровно тех версий, что # записаны здесь, сверяя контрольные суммы. Машина разработчика при этом не # проверялась НИКАК, поэтому расхождение обнаруживалось только на Debian, внутри # release-сборки, и выглядело как «у меня работало». # # Расхождение не гипотетическое: локальный Go 1.25 собирал проект успешно (`go` # в go.mod — языковой baseline, а не выбор компилятора), а релизный бинарь # компилировался на 1.26.7 и наследовал ЕЁ stdlib. То есть проверялся не тот # код, который уезжает в production. # # Скрипт ничего не устанавливает и не меняет: он отвечает на один вопрос — # совпадает ли эта машина с контрактом сборки. # # Использование: # ./tools/dev/doctor.sh проверить и вернуть код выхода # ./tools/dev/doctor.sh --quiet только итог ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" cd "$ROOT_DIR" QUIET=false for arg in "$@"; do case "$arg" in --quiet) QUIET=true ;; -h|--help) printf 'usage: %s [--quiet]\n' "$0" exit 0 ;; *) printf 'unknown argument: %s\n' "$arg" >&2 exit 2 ;; esac done [ -f versions.env ] || { printf 'versions.env не найден: doctor запускается из репозитория HY2XS\n' >&2 exit 2 } # shellcheck disable=SC1091 . ./versions.env FAILURES=0 say() { [ "$QUIET" = true ] || printf '%s\n' "$1" } # Версия извлекается из вывода самого инструмента: спрашивать `которая версия # записана в PATH` бессмысленно, отвечать должен тот бинарь, который реально # запустится. tool_version() { local command="$1" shift command -v "$command" >/dev/null 2>&1 || { printf 'not found' return 0 } "$command" "$@" 2>/dev/null | head -n1 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n1 } check() { local label="$1" required="$2" found="$3" hint="$4" if [ -z "$found" ]; then found="unknown" fi say "" say "$label:" say " required: $required" say " found: $found" if [ "$found" = "$required" ]; then say " OK" return 0 fi say " FAIL — $hint" FAILURES=$((FAILURES + 1)) } say "HY2XS development environment" say "contract: versions.env (HY2XS $HY2XS_VERSION, release line $HY2XS_RELEASE_LINE)" # Go попадает в production-артефакт целиком: его stdlib компилируется в # hy2xs-admin. Несовпадение здесь — не удобство, а разная поверхность атаки у # разработчика и у релиза. check "Go" "$GO_VERSION" "$(tool_version go version)" \ "локальный Go собирает не ту stdlib, что уедет в релиз; поставьте $GO_VERSION" # Node живёт только на build-хосте (сборка frontend-ассетов), но собирает код, # который уезжает в production. check "Node" "$NODE_VERSION" "$(tool_version node --version)" \ "frontend-ассеты собираются другой версией Node; поставьте $NODE_VERSION" check "pnpm" "$PNPM_VERSION" "$(tool_version pnpm --version)" \ "разрешение зависимостей frontend может разойтись с lock-файлом; поставьте $PNPM_VERSION" # Bun физически входит в скомпилированный оркестратор (`bun build --compile`), # поэтому его версия — часть артефакта, а не инструмента. check "Bun" "$BUN_VERSION" "$(tool_version bun --version)" \ "runtime Bun входит в бинарь оркестратора; поставьте $BUN_VERSION" # go.mod обязан называть компилятор явно: директива `go` этого не делает. DECLARED_TOOLCHAIN="$(grep -E '^toolchain[[:space:]]+go[0-9]' apps/go.mod | head -n1 | awk '{print $2}' || true)" check "apps/go.mod toolchain" "go$GO_VERSION" "${DECLARED_TOOLCHAIN:-отсутствует}" \ "директива toolchain разошлась с versions.env; сборка отвергнет это в verify_go_toolchain_contract" say "" if [ "$FAILURES" -eq 0 ]; then say "Итог: среда разработки совпадает с контрактом сборки." exit 0 fi say "Итог: расхождений — $FAILURES. Локальные проверки идут не по тому toolchain," say "которым собирается релиз." exit 1