Files
HY2XS_flamy/tools/build
Crimson ab788725cf fix(env): контракт был шире домена, который принимает systemd
Разбор предыдущего прохода со сверкой по исходникам systemd v257.13 — той самой
линии, что стоит на Debian 13. Тема та же и слоем глубже: контракт, объявленный
шире, чем его принимает чужая сторона. Прошлый проход сделал транспорт lossless
для значений, которые systemd принимает, но не спросил, какие значения он
принимает вообще.

1. Домен значений файла окружения

Перед тем как принять пару, systemd прогоняет ключ и значение через
utf8_is_valid (src/basic/env-file.c, check_utf8ness_and_warn), и отказ там
возвращает -EINVAL — то есть НЕзагруженный EnvironmentFile= и юнит, который не
стартует, а не предупреждение. unichar_is_valid (src/basic/utf8.c) отвергает
суррогаты, U+FDD0..U+FDEF и все code points вида *FFFE/*FFFF, а сам
utf8_is_valid — встроенный NUL и невалидный UTF-8.

Пароль "abcde" + U+FDD0 — шесть символов, восемь байт, ни одного управляющего —
проходил панель, оркестратор, DTO и хеширование, записывался в hy2xs.env, и
после этого админка не поднималась. Тот же класс дефекта, ради уничтожения
которого контракт и существует, только слоем ниже.

Введён IsEnvTransportableText (Go) / isEnvTransportable (TS), повторяющий
множество systemd точно — не шире и не уже. Отдельно отвергаются одиночные
суррогаты: строка JavaScript вправе их содержать, а TextEncoder молча заменяет
непарный суррогат на U+FFFD, то есть без проверки в файл уехал бы ДРУГОЙ
секрет, а не отказ.

Заодно разделены домен транспорта и политика продукта. Проверка отвергала C0 и
DEL с формулировкой «формат управляющих символов не несёт» — неправда: внутри
двойных кавычек перевод строки накапливается как обычный байт и переживает
round-trip. Именно эта подмена и позволила проверке не знать про noncharacters.
Политика HY2XS теперь запрещает категорию Cc целиком (была шире кода ровно на
C1) плюс U+FEFF — последний отдельным решением продукта, а не форматом:
0xFEFF & 0xFFFE это 0xFEFE, и systemd такое значение принимает.

2. Рецепт восстановления выполнял env-файл как код

В docs/operations/12, раздел «Забыт пароль администратора», стояло
`set -a; . /etc/hy2xs/hy2xs.env; set +a`. Строка стала опасной ровно тогда,
когда файл научился нести произвольные значения. Для systemd
HY2XS_ADMIN_INITIAL_PASSWORD="$(...)" — буквальное значение: подстановок в
EnvironmentFile= нет вовсе. Но `.` обрабатывает файл bash, а bash внутри
двойных кавычек выполняет подстановку команд — от root, прямо в рецепте
восстановления доступа. Соседний раздел той же страницы при этом уже правильно
запрещал source/eval для bootstrap-admin.secret: документ запрещал действие и
тут же его предлагал.

Рецепт читает нужные значения как ДАННЫЕ. Поставлен гейт приёмки, запрещающий
возврат source/./eval над этими файлами в командах документации и в скриптах;
гейт смотрит только внутрь ```-блоков, чтобы объяснение, называющее убранную
конструкцию по имени, его не роняло.

3. Отказ приходил после мутаций хоста

Проверка транспорта жила только внутри renderRuntimeEnv, то есть срабатывала на
шаге «write runtime env» — уже после bootstrap оркестратора, установки пакетов
и раскладки файловой системы, — а read-only preflight-install говорил PASS: он
зовёт parseRuntimeEnv и ничего не рендерит. Детерминированно известная ошибка
конфигурации роняла операцию, оставив за собой изменённый хост, что прямо
противоречит контракту PHASE 0.

validateRuntimeEnvTransport вызывается теперь из parseRuntimeEnv и проходит по
ВСЕМ парам runtimeEnvEntries: ограничение принадлежит формату, а не полю
пароля, и HY2XS_ADMIN_CON_PASS сломал бы загрузку юнита так же.

4. Точность порта автомата и его описания

- в состоянии DOUBLE_QUOTE_VALUE_ESCAPE systemd пишет `c != '\n'`, а не
  проверку на любой перевод строки (в VALUE_ESCAPE — наоборот,
  strchr(NEWLINE, c)). Порт съедал и \<LF>, и \<CR>;
- комментарий обещал одно намеренное расхождение с systemd, а их два: кроме
  строки без `=`, HY2XS отказывает и на незакрытой кавычке в конце файла.
  Оба fail-closed и теперь названы оба.

Тесты: граничная таблица во всех слоях дополнена значениями вне домена
(U+FDD0, U+FDEF, U+FFFE, U+FFFF, U+1FFFF, U+10FFFF, невалидный UTF-8),
соседями диапазонов (U+FDCF, U+FDF0, U+FFFD, U+10FFFD), C1 и U+FEFF, одиночным
суррогатом. Добавлены TestEnvTransportDomainMatchesSystemd (домен не шире и не
уже) и TestProductPolicyIsWiderThanTransportDomain (домен и политика
различимы), а также проверки fail-closed порядка: parseRuntimeEnv отвергает
непригодную конфигурацию, проверяются все значения файла, запись и проверка
ходят по одному списку пар.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 23:38:57 +05:00
..

HY2XS production builder

Этот каталог содержит production builder для HY2XS.

Builder собирает один переносимый install-archive:

dist/hy2xs-install-<version>.tar.gz

Этот архив переносится на production server, распаковывается и устанавливается через install.sh.

На production server не должно быть сборки из исходников: без go build, без bun install, без pnpm install, без frontend build и без TypeScript transpilation.

Что где лежит

Основные части проекта:

  • versions.env — контракт продукта, платформы и toolchain. Единственный источник истины для версий и контрольных сумм.
  • tools/build/build.sh — главный entrypoint сборки.
  • tools/build/lib/versions.sh — загрузка versions.env и verify_versions_contract.
  • tools/build/lib/deps.sh — проверка Debian/amd64, установка build dependencies, установка Go/Bun/Node.js/pnpm.
  • tools/build/lib/package.sh — сборка orchestrator, сборка HY2XS admin, создание stage directory и tar.gz архива.
  • tools/build/lib/verify.sh — проверка структуры репозитория и итогового архива.
  • tools/build/lib/acceptance.sh — acceptance-проверки production-контракта.
  • tools/build/lib/hysteria.sh — разрешение upstream-версии Hysteria и compatibility gate.
  • tools/build/hysteria-lock.env — fallback-значения для офлайн-сборки (HYSTERIA_CHANNEL=pinned).
  • tools/test/e2e-hysteria.sh — end-to-end проверка с реальным клиентом Hysteria.
  • orchestrator — TypeScript/Bun install-only orchestrator.
  • apps — HY2XS admin: Go backend и Vue frontend.
  • package — skeleton будущего install package: install.sh, templates, systemd units, default config.
  • dist — итоговые архивы. Создаётся builder'ом, в git обычно не хранится.
  • .toolchain — локальный toolchain builder'а. Создаётся автоматически, в git не хранится.

Требования к build machine

Production build поддерживается только на:

  • Debian 13;
  • amd64 / x86_64;
  • root или пользователь с sudo;
  • доступ в интернет.

Нужен outbound HTTPS/DNS до:

  • Debian apt repositories;
  • go.dev;
  • github.com;
  • nodejs.org;
  • npm registry.

Windows и macOS можно использовать для редактирования исходников, но финальную production-сборку надо делать на Debian 13 amd64.

Что builder делает сам

При запуске builder:

  1. Загружает и валидирует versions.env.
  2. Проверяет, что host соответствует HY2XS_BUILD_* (по умолчанию Debian 13 amd64).
  3. Проверяет структуру репозитория.
  4. Устанавливает недостающие системные build dependencies через apt-get.
  5. Проверяет или скачивает локальные версии Go/Bun/Node.js/pnpm из контракта и сверяет каждый архив с контрольной суммой из versions.env.
  6. Выполняет verify_versions_contract: рассинхрон версий роняет сборку до создания tarball.
  7. Прогоняет тесты и типы оркестратора (bun test, tsc --noEmit).
  8. Разрешает upstream-версию Hysteria, берёт ожидаемый SHA-256 из upstream hashes.txt и сверяет с ним скачанный артефакт.
  9. Проходит compatibility gate: реальный бинарник Hysteria должен принять канонический конфиг HY2XS.
  10. Копирует package skeleton.
  11. Собирает install-only orchestrator в standalone binary.
  12. Собирает frontend и backend HY2XS admin в Linux amd64 binary, проставляя версию админки через ldflags.
  13. Прогоняет go vet и go test для HY2XS admin (после сборки frontend, потому что go:embed all:dist требует готовых ассетов).
  14. Записывает metadata и checksums.
  15. Создаёт dist/hy2xs-install-<version>.tar.gz.
  16. Проверяет архив и прогоняет acceptance-проверки.

Контракт версий

Версии продукта, платформы и toolchain объявлены в корневом versions.env. Собственных значений по умолчанию у deps.sh больше нет: без загруженного контракта сборка падает сразу.

Подход — проверка, а не генерация. profile.ts, package/config/hy2xs.env и packageManager в обоих package.json остаются обычными файлами, чтобы bun test, tsc и go test работали из чистого чекаута до запуска сборки. verify_versions_contract сверяет их с контрактом и роняет сборку при расхождении.

Контракт оркестратора сверяется не grep'ом по исходникам, а выводом orchestrator/tools/print-contract.ts: это доказывает, что в бинарь попало то же значение.

Версия админки приезжает в бинарь через ldflags (-X 'hy2xs-admin/model/constant.Version=v${HY2XS_VERSION}') и проверяется запуском собранного hy2xs-admin version. Захардкоженной константы версии в Go-коде больше нет: она уже успела разъехаться с версией пакета.

Чего в versions.env нет намеренно: прикладных зависимостей (для них есть pnpm-lock.yaml, bun.lock, go.sum) и конкретной версии Hysteria (здесь только политика HYSTERIA_CHANNEL, результат резолва — в hysteria-lock.env).

Версия Hysteria: разрешение и compatibility gate

Builder не хранит версию Hysteria вручную. По умолчанию он определяет последнюю стабильную версию сам и замораживает её в пакете.

Правила разрешения:

  1. канонический upstream — HyNetworks/hysteria;
  2. принимаются только стабильные релизы, без draft и prerelease;
  3. тег должен иметь вид app/vX.Y.Z;
  4. берётся ровно один артефакт hysteria-linux-amd64 и ровно один hashes.txt;
  5. URL используется в том виде, в каком его вернул upstream API, без пересборки строки;
  6. ожидаемый SHA-256 берётся из upstream hashes.txt, и скачанный бинарник сверяется с ним;
  7. разрешённые значения попадают в metadata пакета.

Почему hashes.txt, а не локальный пересчёт

Раньше SHA-256 считался от уже скачанного файла. Это защищает target от последующей подмены, но не доказывает, что builder скачал именно ожидаемый upstream artifact: сумма фиксирует то, что пришло, каким бы оно ни было.

Формат ассета:

6493dfff…f94  build/hysteria-linux-amd64
f24f63be…189  build/hysteria-linux-amd64-avx

Сопоставление идёт по базовому имени и строго на равенство: build/ — часть пути, а hysteria-linux-amd64-avx — другой артефакт, который не должен совпасть по префиксу. Разбор вынесен в parseUpstreamHashes (hysteriaRelease.ts) и покрыт юнит-тестами.

Источник ожидаемой суммы фиксируется в metadata как hysteria_sha_source.

Сравнение версий числовое, поэтому v2.9.10 считается новее v2.9.2.

После разрешения обязателен compatibility gate:

скачать бинарник
      ↓
сверить SHA-256 и `hysteria version`
      ↓
отрендерить канонический конфиг HY2XS тем же кодом, что и на target
      ↓
запустить настоящий Hysteria с этим конфигом (gecko и salamander)
      ↓
только после этого собирать release package

При несовместимости сборка останавливается:

BUILD FAILED: unsupported Hysteria stable v2.13.0

Это осознанное решение: ошибка должна проявиться на build machine, а не на production-сервере.

Переменные:

Переменная По умолчанию Назначение
HYSTERIA_CHANNEL из versions.env (stable) stable — разрешить последнюю стабильную через upstream API; pinned — офлайн-сборка по hysteria-lock.env
HYSTERIA_VERSION_OVERRIDE пусто Закрепить конкретную версию vX.Y.Z
HYSTERIA_COMPAT_GATE true Compatibility gate; для release-сборок обязателен
HYSTERIA_VERIFY_UPSTREAM_HASHES true Сверять артефакт с upstream hashes.txt; отключение — только break-glass
HYSTERIA_WRITE_LOCK false Записать разрешённые значения обратно в hysteria-lock.env
HYSTERIA_GATE_PORT 34443 UDP-порт для временного запуска Hysteria в gate
HYSTERIA_GATE_STATS_PORT 34712 TCP-порт trafficStats в gate
GITHUB_TOKEN пусто Опционально: снимает anonymous rate limit GitHub API

Тесты, проверка типов и проверка зависимостей переменными не управляются: у них нет аварийного выхода. Готовый пакет объявляет об этом полями tests_gate=true и dependency_security_gate=true в metadata/package.env.

Обновить lock-файл под текущий upstream:

HYSTERIA_WRITE_LOCK=true ./tools/build/build.sh

Важное про Bun и старые CPU

Bun имеет два Linux x64 artifact'а:

  • bun-linux-x64 — обычный build;
  • bun-linux-x64-baseline — build для CPU без AVX2.

Если CPU не поддерживает AVX2, обычный Bun может падать с Illegal instruction.

Builder автоматически проверяет /proc/cpuinfo.

По умолчанию:

BUN_FLAVOR=auto

Логика:

  • если CPU поддерживает AVX2, используется bun-linux-x64;
  • если CPU не поддерживает AVX2, используется bun-linux-x64-baseline.

Поскольку артефакта два, одной контрольной суммы архитектурно недостаточно. В versions.env зафиксированы обе:

BUN_LINUX_X64_SHA256=<sha256>
BUN_LINUX_X64_BASELINE_SHA256=<sha256>

Ожидаемый digest выбирается уже после select_bun_artifact().

Можно принудительно задать flavor:

BUN_FLAVOR=x64 ./tools/build/build.sh

или:

BUN_FLAVOR=x64-baseline ./tools/build/build.sh

Запуск

Из корня репозитория:

./tools/build/build.sh

Версия пакета берётся из versions.env, поэтому обычно нужен только build id:

BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ) ./tools/build/build.sh

Проверка результата

Проверка архива:

ls -lh dist/hy2xs-install-1.0.0.tar.gz
sha256sum dist/hy2xs-install-1.0.0.tar.gz | tee dist/hy2xs-install-1.0.0.tar.gz.sha256

Проверка обязательных файлов:

tar -tzf dist/hy2xs-install-1.0.0.tar.gz | grep -E '^(hy2xs-install/install.sh|hy2xs-install/orchestrator/hy2xs-orchestrator|hy2xs-install/ui/hy2xs-admin/hy2xs-admin|hy2xs-install/metadata/checksums.txt)$'

Проверка metadata:

tar -xOzf dist/hy2xs-install-1.0.0.tar.gz hy2xs-install/metadata/package.env

Когда обновлять orchestrator/bun.lock

Не перегенерируйте orchestrator/bun.lock во время production-сборки.

Обновлять и коммитить orchestrator/bun.lock следует только когда:

  • изменился orchestrator/package.json;
  • изменился BUN_VERSION в versions.env;
  • зависимости оркестратора обновляются осознанно.

При смене BUN_VERSION нужно обновить и packageManager в orchestrator/package.json, и обе контрольные суммы Bun: иначе verify_versions_contract остановит сборку.

Production builder всегда выполняет:

bun install --frozen-lockfile

Если команда падает, исправьте и закоммитьте lockfile в системе контроля версий. Не убирайте --frozen-lockfile.

Дисциплина lockfile для frontend

Не перегенерируйте frontend lock data во время обычной production-сборки.

Правила управления пакетами frontend:

  • apps/frontend/package.json объявляет "packageManager": "pnpm@9.15.9";
  • builder использует закреплённый pnpm из PNPM_VERSION в versions.env, и verify_versions_contract сверяет эти два значения;
  • production-путь установки frontend всегда:
pnpm install --frozen-lockfile

Если frozen install падает, обновите зависимости осознанно в системе контроля версий и закоммитьте изменения lockfile. Не убирайте --frozen-lockfile из сборочного потока.

Полезные переменные

  • BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ)
  • BUN_FLAVOR=auto|x64|x64-baseline
  • FRONTEND_NODE_OLD_SPACE_SIZE=2048 (default memory limit for frontend build step)
  • TOOLCHAIN_DIR=/custom/path/.toolchain
  • VERIFY_TOOLCHAIN_CHECKSUMS=true
  • VERSIONS_ENV_FILE=/custom/path/versions.env

PACKAGE_VERSION берётся из versions.env (HY2XS_VERSION); переопределять его вручную нужно только для отладочных сборок, и verify_versions_contract такую сборку отклонит.

Контрольные суммы toolchain больше не передаются через окружение: они объявлены в versions.env. Раньше воспроизводимая сборка в чистой Debian-среде требовала предварительного знания четырёх SHA-256 и не запускалась одной командой.

Политика памяти при сборке frontend

Builder задаёт безопасный лимит heap для Node.js внутри bundle_ui():

--max-old-space-size=2048

Способы переопределения:

  • изменить значение по умолчанию для этой политики:
FRONTEND_NODE_OLD_SPACE_SIZE=3072 ./tools/build/build.sh
  • или передать полный набор собственных Node-опций (если --max-old-space-size там уже задан, builder не добавит второй):
NODE_OPTIONS="--max-old-space-size=3072" ./tools/build/build.sh

Диагностика

Проверить AVX2:

grep -qw avx2 <<<"$(grep -m1 '^flags' /proc/cpuinfo)" && echo 'CPU has AVX2' || echo 'CPU has NO AVX2; Bun baseline is required'

Проверить Bun:

.toolchain/bun/bin/bun --version
echo "bun_exit=$?"

Если есть Illegal instruction, удалите старый Bun и пересоберите:

rm -rf .toolchain/bun .toolchain/bun-tmp
rm -f .toolchain/downloads/bun-linux-x64-*.zip
rm -f .toolchain/downloads/bun-linux-x64-baseline-*.zip
./tools/build/build.sh

Проверить shell syntax:

bash -n tools/build/build.sh
bash -n tools/build/lib/common.sh
bash -n tools/build/lib/versions.sh
bash -n tools/build/lib/deps.sh
bash -n tools/build/lib/hysteria.sh
bash -n tools/build/lib/package.sh
bash -n tools/build/lib/verify.sh
bash -n tools/build/lib/acceptance.sh