Подготовить HY2XS к production-сборке

This commit is contained in:
2026-04-25 23:13:12 +05:00
commit 84a4e94567
277 changed files with 26513 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
# HY2XS production builder
## Назначение
`tools/build/build.sh` собирает один переносимый install package HY2XS для production-развёртывания.
Итоговый артефакт создаётся в `dist/hy2xs-install-<version>.tar.gz` и предназначен для установки на чистый Debian 12 amd64 без target-side сборки.
## Поддерживаемая среда сборки
Builder поддерживает только:
- Debian 12;
- amd64 / x86_64;
- bash;
- доступ к интернету для установки build-зависимостей и toolchain.
Windows/macOS не являются production build host. На Windows можно править исходники, но финальную сборку нужно выполнять на Debian 12 amd64.
## Что builder делает сам
При запуске builder:
1. Проверяет ОС и архитектуру build host.
2. Проверяет структуру репозитория.
3. Доставляет отсутствующие системные build-зависимости через `apt-get`.
4. Проверяет и при необходимости скачивает локальный toolchain:
- Go `1.21.13`;
- Bun `1.1.45`;
- Node.js `20.19.0`;
- pnpm `9.15.9`.
5. Собирает install-only orchestrator под Linux amd64.
6. Собирает bundled HY2XS admin под Linux amd64.
7. Формирует metadata и checksums.
8. Создаёт архив install package.
9. Проверяет состав итогового архива.
## Запуск
Из корня репозитория:
```bash
./tools/build/build.sh
```
С явной версией пакета:
```bash
PACKAGE_VERSION=0.1.0 ./tools/build/build.sh
```
С явным build id:
```bash
PACKAGE_VERSION=0.1.0 BUILD_ID=prod-20260425-001 ./tools/build/build.sh
```
## Локальный toolchain
Builder ставит управляемый toolchain в `.toolchain/` и не требует ручной установки Go/Bun/Node/pnpm в систему.
Если нужная версия уже установлена глобально, builder может использовать её. Если версия не совпадает, будет скачана локальная версия.
## Важные ограничения
- Builder не ставит HY2XS на сервер.
- Builder не выполняет target install.
- Builder не собирает ничего на target machine.
- Builder не вендорит Hysteria2 binary в пакет: Hysteria2 скачивается install layer'ом с official upstream.
- Итоговый package не должен содержать build scripts, `.toolchain` или временные каталоги.
## Результат
После успешной сборки появится архив:
```bash
dist/hy2xs-install-<version>.tar.gz
```
Его нужно перенести на target Debian 12 amd64, распаковать и запустить `install.sh` от root.
## Диагностика
Если сборка падает:
1. Проверьте, что host — Debian 12 amd64.
2. Проверьте доступ к `go.dev`, `github.com`, `nodejs.org`, npm registry и apt repositories.
3. Удалите `.toolchain/` и повторите запуск, если toolchain скачался повреждённым.
4. Проверьте lock-файлы `orchestrator/bun.lock`, `apps/frontend/pnpm-lock.yaml`, `apps/go.sum`.
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
BUILD_DIR="$ROOT_DIR/tools/build"
# shellcheck source=tools/build/lib/common.sh
. "$BUILD_DIR/lib/common.sh"
# shellcheck source=tools/build/lib/deps.sh
. "$BUILD_DIR/lib/deps.sh"
# shellcheck source=tools/build/lib/verify.sh
. "$BUILD_DIR/lib/verify.sh"
# shellcheck source=tools/build/lib/package.sh
. "$BUILD_DIR/lib/package.sh"
main() {
cd "$ROOT_DIR"
require_linux_debian12_amd64
require_repo_layout
ensure_build_dependencies
ensure_toolchain
PACKAGE_VERSION="${PACKAGE_VERSION:-0.1.0}"
BUILD_ID="${BUILD_ID:-$(date -u +%Y%m%dT%H%M%SZ)}"
log_step "Preparing package stage"
prepare_stage "$PACKAGE_VERSION" "$BUILD_ID"
log_step "Building install-only orchestrator"
build_orchestrator
log_step "Bundling HY2XS admin fork"
bundle_ui
log_step "Writing metadata"
write_metadata "$PACKAGE_VERSION" "$BUILD_ID"
log_step "Creating final archive"
create_archive "$PACKAGE_VERSION"
log_step "Verifying final archive"
verify_archive "$PACKAGE_VERSION"
log_info "Built dist/hy2xs-install-${PACKAGE_VERSION}.tar.gz"
}
main "$@"
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
log_info() {
printf '[hy2xs-build] %s\n' "$*"
}
log_step() {
printf '\n[hy2xs-build] ==> %s\n' "$*"
}
fail() {
printf '[hy2xs-build] ERROR: %s\n' "$*" >&2
exit 1
}
require_tool() {
local tool="$1"
command -v "$tool" >/dev/null 2>&1 || fail "required tool not found: $tool"
}
version_ge() {
[ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -n 1)" = "$2" ]
}
copy_dir_contents() {
local src="$1"
local dst="$2"
mkdir -p "$dst"
if [ -d "$src" ]; then
cp -a "$src"/. "$dst"/
else
fail "directory not found: $src"
fi
}
download_file() {
local url="$1"
local dst="$2"
if command -v curl >/dev/null 2>&1; then
curl -fsSL "$url" -o "$dst"
elif command -v wget >/dev/null 2>&1; then
wget -qO "$dst" "$url"
else
fail "curl or wget is required to download $url"
fi
}
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env bash
set -euo pipefail
GO_REQUIRED="${GO_REQUIRED:-1.21.13}"
BUN_REQUIRED="${BUN_REQUIRED:-1.1.45}"
NODE_REQUIRED="${NODE_REQUIRED:-20.19.0}"
PNPM_REQUIRED="${PNPM_REQUIRED:-9.15.9}"
TOOLCHAIN_DIR="${TOOLCHAIN_DIR:-$ROOT_DIR/.toolchain}"
require_linux_debian12_amd64() {
[ "$(uname -s)" = "Linux" ] || fail "production builder supports only Linux Debian 12 amd64"
local arch
arch="$(uname -m)"
[ "$arch" = "x86_64" ] || [ "$arch" = "amd64" ] || fail "production builder supports only amd64, got: $arch"
[ -f /etc/os-release ] || fail "missing /etc/os-release"
# shellcheck disable=SC1091
. /etc/os-release
[ "${ID:-}" = "debian" ] || fail "production builder supports only Debian 12, got: ${ID:-unknown}"
[ "${VERSION_ID:-}" = "12" ] || fail "production builder supports only Debian 12, got version: ${VERSION_ID:-unknown}"
}
apt_install_missing() {
local missing=()
local pkg
for pkg in "$@"; do
if ! dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q 'install ok installed'; then
missing+=("$pkg")
fi
done
if [ "${#missing[@]}" -eq 0 ]; then
return 0
fi
log_info "Installing missing build packages: ${missing[*]}"
if [ "$(id -u)" = "0" ]; then
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${missing[@]}"
elif command -v sudo >/dev/null 2>&1; then
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${missing[@]}"
else
fail "missing packages (${missing[*]}) and neither root nor sudo is available"
fi
}
ensure_build_dependencies() {
apt_install_missing \
ca-certificates curl unzip tar xz-utils git build-essential pkg-config \
bash coreutils findutils grep sed gawk openssl
require_tool curl
require_tool tar
require_tool xz
require_tool unzip
require_tool sha256sum
require_tool find
require_tool install
require_tool sort
require_tool xargs
require_tool grep
require_tool sed
}
go_version() {
"$1" version | sed -E 's/^go version go([0-9.]+).*/\1/'
}
ensure_go() {
local managed="$TOOLCHAIN_DIR/go/bin/go"
if [ -x "$managed" ] && [ "$(go_version "$managed")" = "$GO_REQUIRED" ]; then
GO_BIN="$managed"
elif command -v go >/dev/null 2>&1 && [ "$(go_version "$(command -v go)")" = "$GO_REQUIRED" ]; then
GO_BIN="$(command -v go)"
else
log_info "Installing Go $GO_REQUIRED into $TOOLCHAIN_DIR/go"
mkdir -p "$TOOLCHAIN_DIR/downloads"
local archive="$TOOLCHAIN_DIR/downloads/go${GO_REQUIRED}.linux-amd64.tar.gz"
download_file "https://go.dev/dl/go${GO_REQUIRED}.linux-amd64.tar.gz" "$archive"
rm -rf "$TOOLCHAIN_DIR/go"
tar -C "$TOOLCHAIN_DIR" -xzf "$archive"
GO_BIN="$managed"
fi
export GO_BIN
export GOROOT="$TOOLCHAIN_DIR/go"
export PATH="$(dirname "$GO_BIN"):$PATH"
export GOTOOLCHAIN=local
[ "$(go_version "$GO_BIN")" = "$GO_REQUIRED" ] || fail "Go version mismatch: required $GO_REQUIRED, got $($GO_BIN version)"
}
ensure_bun() {
local managed="$TOOLCHAIN_DIR/bun/bin/bun"
if [ -x "$managed" ] && [ "$($managed --version)" = "$BUN_REQUIRED" ]; then
BUN_BIN="$managed"
elif command -v bun >/dev/null 2>&1 && [ "$(bun --version)" = "$BUN_REQUIRED" ]; then
BUN_BIN="$(command -v bun)"
else
log_info "Installing Bun $BUN_REQUIRED into $TOOLCHAIN_DIR/bun"
mkdir -p "$TOOLCHAIN_DIR/downloads" "$TOOLCHAIN_DIR/bun"
local archive="$TOOLCHAIN_DIR/downloads/bun-linux-x64-${BUN_REQUIRED}.zip"
download_file "https://github.com/oven-sh/bun/releases/download/bun-v${BUN_REQUIRED}/bun-linux-x64.zip" "$archive"
rm -rf "$TOOLCHAIN_DIR/bun-tmp" "$TOOLCHAIN_DIR/bun"
mkdir -p "$TOOLCHAIN_DIR/bun-tmp"
unzip -q "$archive" -d "$TOOLCHAIN_DIR/bun-tmp"
mkdir -p "$TOOLCHAIN_DIR/bun/bin"
install -m 0755 "$TOOLCHAIN_DIR/bun-tmp/bun-linux-x64/bun" "$managed"
rm -rf "$TOOLCHAIN_DIR/bun-tmp"
BUN_BIN="$managed"
fi
export BUN_BIN
export PATH="$(dirname "$BUN_BIN"):$PATH"
[ "$($BUN_BIN --version)" = "$BUN_REQUIRED" ] || fail "Bun version mismatch: required $BUN_REQUIRED, got $($BUN_BIN --version)"
}
node_version() {
"$1" --version | sed 's/^v//'
}
ensure_node() {
local managed="$TOOLCHAIN_DIR/node/bin/node"
if [ -x "$managed" ] && [ "$(node_version "$managed")" = "$NODE_REQUIRED" ]; then
NODE_BIN="$managed"
elif command -v node >/dev/null 2>&1 && [ "$(node_version "$(command -v node)")" = "$NODE_REQUIRED" ]; then
NODE_BIN="$(command -v node)"
else
log_info "Installing Node.js $NODE_REQUIRED into $TOOLCHAIN_DIR/node"
mkdir -p "$TOOLCHAIN_DIR/downloads"
local archive="$TOOLCHAIN_DIR/downloads/node-v${NODE_REQUIRED}-linux-x64.tar.xz"
download_file "https://nodejs.org/dist/v${NODE_REQUIRED}/node-v${NODE_REQUIRED}-linux-x64.tar.xz" "$archive"
rm -rf "$TOOLCHAIN_DIR/node" "$TOOLCHAIN_DIR/node-v${NODE_REQUIRED}-linux-x64"
tar -C "$TOOLCHAIN_DIR" -xJf "$archive"
mv "$TOOLCHAIN_DIR/node-v${NODE_REQUIRED}-linux-x64" "$TOOLCHAIN_DIR/node"
NODE_BIN="$managed"
fi
export NODE_BIN
export PATH="$(dirname "$NODE_BIN"):$PATH"
[ "$(node_version "$NODE_BIN")" = "$NODE_REQUIRED" ] || fail "Node.js version mismatch: required $NODE_REQUIRED, got $($NODE_BIN --version)"
}
ensure_pnpm() {
local managed="$TOOLCHAIN_DIR/pnpm/bin/pnpm"
if [ -x "$managed" ] && [ "$($managed --version)" = "$PNPM_REQUIRED" ]; then
PNPM_BIN="$managed"
else
log_info "Installing pnpm $PNPM_REQUIRED into $TOOLCHAIN_DIR/pnpm"
mkdir -p "$TOOLCHAIN_DIR/pnpm/bin"
corepack enable --install-directory "$TOOLCHAIN_DIR/pnpm/bin"
corepack prepare "pnpm@$PNPM_REQUIRED" --activate
if command -v pnpm >/dev/null 2>&1; then
cp "$(command -v pnpm)" "$managed"
chmod 0755 "$managed"
fi
PNPM_BIN="$(command -v pnpm)"
fi
export PNPM_BIN
[ "$($PNPM_BIN --version)" = "$PNPM_REQUIRED" ] || fail "pnpm version mismatch: required $PNPM_REQUIRED, got $($PNPM_BIN --version)"
}
ensure_toolchain() {
ensure_go
ensure_bun
ensure_node
ensure_pnpm
log_info "Go: $($GO_BIN version)"
log_info "Bun: $($BUN_BIN --version)"
log_info "Node.js: $($NODE_BIN --version)"
log_info "pnpm: $($PNPM_BIN --version)"
}
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
set -euo pipefail
STAGE_DIR="tools/build/output/hy2xs-install"
ADMIN_BUILD_DIR="tools/build/output/hy2xs-admin-build"
prepare_stage() {
local version="$1"
local build_id="$2"
rm -rf "$STAGE_DIR"
mkdir -p "$STAGE_DIR" "dist"
copy_dir_contents "package" "$STAGE_DIR"
rm -rf "$STAGE_DIR/orchestrator" "$STAGE_DIR/ui" "$STAGE_DIR/metadata"
mkdir -p "$STAGE_DIR/orchestrator" "$STAGE_DIR/ui/hy2xs-admin" "$STAGE_DIR/metadata"
printf '%s\n' "$version" >"$STAGE_DIR/metadata/package.version"
printf '%s\n' "$build_id" >"$STAGE_DIR/metadata/package.build_id"
}
build_orchestrator() {
(
cd orchestrator
"$BUN_BIN" install --frozen-lockfile
"$BUN_BIN" build src/cli.ts --compile --target=bun-linux-x64 --outfile ../"$STAGE_DIR"/orchestrator/hy2xs-orchestrator
)
chmod 0755 "$STAGE_DIR/orchestrator/hy2xs-orchestrator"
}
bundle_ui() {
local ui_src="${UI_SRC:-apps}"
rm -rf "$ADMIN_BUILD_DIR"
mkdir -p "$ADMIN_BUILD_DIR"
(
cd "$ui_src/frontend"
"$PNPM_BIN" install --frozen-lockfile
"$PNPM_BIN" run build:prod
)
(
cd "$ui_src"
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GOTOOLCHAIN=local "$GO_BIN" mod download
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GOTOOLCHAIN=local "$GO_BIN" build -trimpath -ldflags "-s -w" -o "../$ADMIN_BUILD_DIR/hy2xs-admin" .
)
install -m 0755 "$ADMIN_BUILD_DIR/hy2xs-admin" "$STAGE_DIR/ui/hy2xs-admin/hy2xs-admin"
if [ -f "$ui_src/docs/sql/h_ui_db.sql" ]; then
mkdir -p "$STAGE_DIR/ui/hy2xs-admin/docs/sql"
install -m 0644 "$ui_src/docs/sql/h_ui_db.sql" "$STAGE_DIR/ui/hy2xs-admin/docs/sql/h_ui_db.sql"
fi
}
write_metadata() {
local version="$1"
local build_id="$2"
{
printf 'name=HY2XS\n'
printf 'version=%s\n' "$version"
printf 'build_id=%s\n' "$build_id"
printf 'build_host_os=debian12\n'
printf 'build_host_arch=amd64\n'
printf 'target_os=linux\n'
printf 'target_arch=amd64\n'
printf 'orchestrator_stack=Bun+TypeScript\n'
printf 'go_version=%s\n' "$($GO_BIN version)"
printf 'bun_version=%s\n' "$($BUN_BIN --version)"
printf 'node_version=%s\n' "$($NODE_BIN --version)"
printf 'pnpm_version=%s\n' "$($PNPM_BIN --version)"
printf 'hysteria_source=official-upstream\n'
printf 'hysteria_target=linux-amd64\n'
} >"$STAGE_DIR/metadata/package.env"
(
cd "$STAGE_DIR"
find . -type f ! -path './metadata/checksums.txt' -print0 \
| sort -z \
| xargs -0 sha256sum >metadata/checksums.txt
)
}
create_archive() {
local version="$1"
local archive="dist/hy2xs-install-${version}.tar.gz"
rm -f "$archive"
tar -C "tools/build/output" -czf "$archive" "hy2xs-install"
}
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail
require_repo_layout() {
[ -d "orchestrator/src" ] || fail "missing orchestrator/src"
[ -f "orchestrator/package.json" ] || fail "missing orchestrator/package.json"
[ -f "orchestrator/bun.lock" ] || fail "missing orchestrator/bun.lock"
[ -f "package/install.sh" ] || fail "missing package/install.sh"
[ -d "package/templates" ] || fail "missing package/templates"
[ -f "package/templates/env/post-install.env.tpl" ] || fail "missing package/templates/env/post-install.env.tpl"
[ -d "package/systemd" ] || fail "missing package/systemd"
[ -d "apps" ] || fail "missing apps HY2XS admin source"
[ -f "apps/go.mod" ] || fail "missing apps/go.mod"
[ -f "apps/go.sum" ] || fail "missing apps/go.sum"
[ -f "apps/frontend/package.json" ] || fail "missing apps/frontend/package.json"
[ -f "apps/frontend/pnpm-lock.yaml" ] || fail "missing apps/frontend/pnpm-lock.yaml"
}
verify_archive() {
local version="$1"
local archive="dist/hy2xs-install-${version}.tar.gz"
[ -f "$archive" ] || fail "archive was not created: $archive"
local listing
listing="$(tar -tzf "$archive")"
printf '%s\n' "$listing" | grep -q '^hy2xs-install/install.sh$' || fail "archive missing install.sh"
printf '%s\n' "$listing" | grep -q '^hy2xs-install/orchestrator/hy2xs-orchestrator$' || fail "archive missing orchestrator artifact"
printf '%s\n' "$listing" | grep -q '^hy2xs-install/ui/hy2xs-admin/hy2xs-admin$' || fail "archive missing bundled UI binary"
printf '%s\n' "$listing" | grep -q '^hy2xs-install/systemd/hysteria-server.service$' || fail "archive missing hysteria systemd unit"
printf '%s\n' "$listing" | grep -q '^hy2xs-install/systemd/hy2xs-admin.service$' || fail "archive missing admin systemd unit"
printf '%s\n' "$listing" | grep -q '^hy2xs-install/templates/hysteria/config.yaml.tpl$' || fail "archive missing Hysteria config template"
printf '%s\n' "$listing" | grep -q '^hy2xs-install/templates/env/post-install.env.tpl$' || fail "archive missing post-install env template"
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/checksums.txt$' || fail "archive missing checksums"
}