Files
HY2XS_flamy/tools/build/README.md
T
founder a1c74caa0c fix(build): исключить SIGPIPE из релизных гейтов под pipefail
Поиск с флагом -q прекращает чтение на первом совпадении и закрывает свой конец
канала. Продюсер, которому осталось что писать, получает SIGPIPE и завершается
кодом 141, а `set -o pipefail` делает 141 статусом всей конструкции:

    совпадение НАЙДЕНО -> продюсер оборван -> статус 141 -> «не найдено»

Для утвердительных проверок это ложный FAIL. Для отрицательных — «такой
конструкции в коде нет» — ложный PASS: запрещённая конструкция найдена, а гейт
зелёный. Отрицательными проверками закреплена половина инвариантов приёмки,
включая запрет обхода тестов и запрет `pnpm audit --prod`.

Порог резкий: пока вывод продюсера помещается в буфер канала (64 KiB на Linux),
он не блокируется и успевает завершиться раньше, чем потребитель начнёт читать.
Замер, 60 прогонов на размер: до 60 KiB — 0 отказов, ровно на 64 KiB — 58/60,
от 96 KiB — 60/60. То есть проверка выглядит исправной ровно до первого
источника крупнее буфера, а такие файлы в репозитории уже есть.

- 56 мест переведены на here-string: `grep -q PATTERN <<<"$content"`;
- продюсеры-команды (ss|awk, dpkg-query, /proc/cpuinfo, systemctl
  list-unit-files) сначала читаются в переменную;
- введён code_has: десять отрицательных сканов держались на `|| true` внутри
  code_without_comments, гасившем 141, — то есть на побочном эффекте
  подавления ошибок, а не на заявленном свойстве;
- несуществующий путь в скане больше не означает успех: `2>/dev/null || true`
  превращал опечатку в пустой вывод, а пустой вывод для проверки «этого в коде
  нет» — это PASS. Проверка явная, а не через set -e: в контексте `! code_has`
  bash отключает errexit на весь вызов;
- возврат пайплайна запрещён отдельной приёмкой.
2026-09-01 04:28:54 +05:00

377 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HY2XS production builder
Этот каталог содержит production builder для HY2XS.
Builder собирает один переносимый install-archive:
```text
dist/hy2xs-install-<version>.tar.gz
```
Этот архив переносится на production server, распаковывается и устанавливается через [`install.sh`](../../package/install.sh).
На production server не должно быть сборки из исходников: без `go build`, без `bun install`, без `pnpm install`, без frontend build и без TypeScript transpilation.
## Что где лежит
Основные части проекта:
- [`versions.env`](../../versions.env) — контракт продукта, платформы и toolchain. Единственный источник истины для версий и контрольных сумм.
- [`tools/build/build.sh`](build.sh) — главный entrypoint сборки.
- [`tools/build/lib/versions.sh`](lib/versions.sh) — загрузка `versions.env` и `verify_versions_contract`.
- [`tools/build/lib/deps.sh`](lib/deps.sh) — проверка Debian/amd64, установка build dependencies, установка Go/Bun/Node.js/pnpm.
- [`tools/build/lib/package.sh`](lib/package.sh) — сборка orchestrator, сборка HY2XS admin, создание stage directory и tar.gz архива.
- [`tools/build/lib/verify.sh`](lib/verify.sh) — проверка структуры репозитория и итогового архива.
- [`tools/build/lib/acceptance.sh`](lib/acceptance.sh) — acceptance-проверки production-контракта.
- [`tools/build/lib/hysteria.sh`](lib/hysteria.sh) — разрешение upstream-версии Hysteria и compatibility gate.
- [`tools/build/hysteria-lock.env`](hysteria-lock.env) — fallback-значения для офлайн-сборки (`HYSTERIA_CHANNEL=pinned`).
- [`tools/test/e2e-hysteria.sh`](../test/e2e-hysteria.sh) — end-to-end проверка с реальным клиентом Hysteria.
- [`orchestrator`](../../orchestrator) — TypeScript/Bun install-only orchestrator.
- [`apps`](../../apps) — HY2XS admin: Go backend и Vue frontend.
- [`package`](../../package) — skeleton будущего install package: `install.sh`, templates, systemd units, default config.
- [`dist`](../../dist) — итоговые архивы. Создаётся builder'ом, в git обычно не хранится.
- [`.toolchain`](../../.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`](../../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`](../../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`](../../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-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: сумма фиксирует то, что пришло, каким бы оно ни было.
Формат ассета:
```text
6493dfff…f94 build/hysteria-linux-amd64
f24f63be…189 build/hysteria-linux-amd64-avx
```
Сопоставление идёт по базовому имени и строго на равенство: `build/` — часть
пути, а `hysteria-linux-amd64-avx` — другой артефакт, который не должен
совпасть по префиксу. Разбор вынесен в `parseUpstreamHashes`
([`hysteriaRelease.ts`](../../orchestrator/src/build/hysteriaRelease.ts)) и
покрыт юнит-тестами.
Источник ожидаемой суммы фиксируется в metadata как `hysteria_sha_source`.
Сравнение версий числовое, поэтому `v2.9.10` считается новее `v2.9.2`.
После разрешения обязателен compatibility gate:
```text
скачать бинарник
сверить SHA-256 и `hysteria version`
отрендерить канонический конфиг HY2XS тем же кодом, что и на target
запустить настоящий Hysteria с этим конфигом (gecko и salamander)
только после этого собирать release package
```
При несовместимости сборка останавливается:
```text
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:
```bash
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`.
По умолчанию:
```bash
BUN_FLAVOR=auto
```
Логика:
- если CPU поддерживает AVX2, используется `bun-linux-x64`;
- если CPU не поддерживает AVX2, используется `bun-linux-x64-baseline`.
Поскольку артефакта два, одной контрольной суммы архитектурно недостаточно. В
[`versions.env`](../../versions.env) зафиксированы обе:
```bash
BUN_LINUX_X64_SHA256=<sha256>
BUN_LINUX_X64_BASELINE_SHA256=<sha256>
```
Ожидаемый digest выбирается уже **после** `select_bun_artifact()`.
Можно принудительно задать flavor:
```bash
BUN_FLAVOR=x64 ./tools/build/build.sh
```
или:
```bash
BUN_FLAVOR=x64-baseline ./tools/build/build.sh
```
## Запуск
Из корня репозитория:
```bash
./tools/build/build.sh
```
Версия пакета берётся из `versions.env`, поэтому обычно нужен только build id:
```bash
BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ) ./tools/build/build.sh
```
## Проверка результата
Проверка архива:
```bash
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
```
Проверка обязательных файлов:
```bash
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:
```bash
tar -xOzf dist/hy2xs-install-1.0.0.tar.gz hy2xs-install/metadata/package.env
```
## Когда обновлять orchestrator/bun.lock
Не перегенерируйте [`orchestrator/bun.lock`](../../orchestrator/bun.lock) во время production-сборки.
Обновлять и коммитить [`orchestrator/bun.lock`](../../orchestrator/bun.lock) следует только когда:
- изменился [`orchestrator/package.json`](../../orchestrator/package.json);
- изменился `BUN_VERSION` в [`versions.env`](../../versions.env);
- зависимости оркестратора обновляются осознанно.
При смене `BUN_VERSION` нужно обновить и `packageManager` в
`orchestrator/package.json`, и обе контрольные суммы Bun: иначе
`verify_versions_contract` остановит сборку.
Production builder всегда выполняет:
```bash
bun install --frozen-lockfile
```
Если команда падает, исправьте и закоммитьте lockfile в системе контроля версий. Не убирайте `--frozen-lockfile`.
## Дисциплина lockfile для frontend
Не перегенерируйте frontend lock data во время обычной production-сборки.
Правила управления пакетами frontend:
- [`apps/frontend/package.json`](../../apps/frontend/package.json) объявляет `"packageManager": "pnpm@9.15.9"`;
- builder использует закреплённый pnpm из `PNPM_VERSION` в [`versions.env`](../../versions.env), и `verify_versions_contract` сверяет эти два значения;
- production-путь установки frontend всегда:
```bash
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()`](lib/package.sh):
```bash
--max-old-space-size=2048
```
Способы переопределения:
- изменить значение по умолчанию для этой политики:
```bash
FRONTEND_NODE_OLD_SPACE_SIZE=3072 ./tools/build/build.sh
```
- или передать полный набор собственных Node-опций (если `--max-old-space-size` там уже задан, builder не добавит второй):
```bash
NODE_OPTIONS="--max-old-space-size=3072" ./tools/build/build.sh
```
## Диагностика
Проверить AVX2:
```bash
grep -qw avx2 <<<"$(grep -m1 '^flags' /proc/cpuinfo)" && echo 'CPU has AVX2' || echo 'CPU has NO AVX2; Bun baseline is required'
```
Проверить Bun:
```bash
.toolchain/bun/bin/bun --version
echo "bun_exit=$?"
```
Если есть `Illegal instruction`, удалите старый Bun и пересоберите:
```bash
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
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
```