Files
pangolin/app/kernel/fetch-desktop-bin.sh
wangjia 38be40a107
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 25s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 22s
ci-pangolin / Lint — shellcheck (pull_request) Successful in 8s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 30s
ci-pangolin / Flutter — analyze + test (pull_request) Failing after 18s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 4s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 4s
ci-pangolin / Go — build + test (pull_request) Successful in 14s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Successful in 16s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Successful in 4m52s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Successful in 23s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Failing after 13m12s
fix(client/ci): sing-box/wintun 境内镜像到 NAS(修 Windows 构建 GFW 失败)
根因:Windows CI 从 GitHub release 下 sing-box 压缩包被 GFW 限速超时(github.com
网页可达但 release 资产 CDN 被掐),build-windows 卡在 fetch-desktop-bin 早期。

修:
- 二进制镜像到 NAS Gitea generic 包(v1.13.12/sing-box-*-windows-amd64.zip +
  wintun-0.14.1.zip),上传前后均 sha256 校验。
- fetch-desktop-bin.sh 加 DESKTOP_BIN_MIRROR(+可选 _TOKEN):设了先试镜像、
  命中即用、未设/失败回退 GitHub/wintun.net;镜像文件照走现有 sha pin 校验(防
  投毒/损坏)。修一处 macOS bash3.2 空数组 set -u unbound 坑。
- deploy-client.yml build-windows env 指向 NAS 镜像基址 + 复用现有 FORGEJO_TOKEN
  secret(匿名 GET 也行,token 仅备将来私有)。无新 secret、无硬编码、无入库二进制。

回归(mac bash3.2):无 mirror 走 GitHub ✓ / mirror 走 NAS 命中 ✓ / bogus mirror
回退 ✓。Windows 端到端待下个 client-v* tag 触发 CI 观察。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 09:53:38 +08:00

272 lines
13 KiB
Bash
Raw Permalink 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.
#!/usr/bin/env bash
# fetch-desktop-bin.sh — 从 sing-box GitHub Release 下载桌面平台二进制
#
# 用法: ./fetch-desktop-bin.sh <os> <arch> [--force]
# os : linux | darwin | windows
# arch : amd64 | arm64
# --force 忽略已有产物,强制重新下载
#
# 产物目录: dist/desktop/<os>-<arch>/
# Linux/macOS : sing-box
# Windows : sing-box.exe + wintun.dll
#
# SHA256 校验: 从官方 Release 下载 checksums.txt 后逐项验证。
# 校验通过后,checksums.txt 缓存于产物目录,可离线复核。
#
# Wintun (Windows 专用):
# 版本由 VERSION 中 WINTUN_VERSION 控制。
# 从 https://www.wintun.net/builds/wintun-<ver>.zip 下载,
# 校验 zip 的 SHA256(见下方 WINTUN_SHA256 变量),
# 提取对应 arch 的 wintun.dll 到产物目录。
#
# 幂等: 产物已存在且 SHA256 校验通过则跳过下载(传 --force 强制重下)
#
# 境内镜像(可选,解决 CI 从 GitHub release 被 GFW 限速的问题):
# DESKTOP_BIN_MIRROR 镜像基址;设了则 archive 与 wintun.zip 先试
# ${DESKTOP_BIN_MIRROR}/<文件名>,命中即用,失败/未设
# 则回退官方 GitHub / wintun.net。镜像下载的文件照样走
# 下面的 SHA256 校验(防投毒/损坏)。
# 例:http://192.168.3.200:3000/api/packages/wangjia/generic/desktop-bin/v1.13.12
# DESKTOP_BIN_MIRROR_TOKEN 可选;镜像需鉴权时作 `Authorization: token <值>`。
# 不设则匿名 GETNAS Gitea generic 包默认可匿名读)。
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ── 加载版本锚点 ─────────────────────────────────────────────────────────────
# shellcheck source=VERSION
. "${SCRIPT_DIR}/VERSION"
# ── Wintun ZIP SHA256(对应 WINTUN_VERSION=0.14.1)─────────────────────────
# 更新 WINTUN_VERSION 时须同步更新此值:
# curl -sL https://www.wintun.net/builds/wintun-<ver>.zip | sha256sum
WINTUN_SHA256="07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51"
# ── 参数解析 ─────────────────────────────────────────────────────────────────
if [[ $# -lt 2 ]]; then
printf 'Usage: %s <os> <arch> [--force]\n' "$0" >&2
printf ' os : linux | darwin | windows\n' >&2
printf ' arch: amd64 | arm64\n' >&2
exit 1
fi
TARGET_OS="$1"
TARGET_ARCH="$2"
FORCE=false
shift 2
for arg in "$@"; do
case "${arg}" in
--force) FORCE=true ;;
*) printf 'Unknown argument: %s\n' "${arg}" >&2; exit 1 ;;
esac
done
# ── 参数验证 ─────────────────────────────────────────────────────────────────
case "${TARGET_OS}" in
linux|darwin|windows) ;;
*) printf '✗ 不支持的 OS: %s(支持: linux | darwin | windows\n' "${TARGET_OS}" >&2; exit 1 ;;
esac
case "${TARGET_ARCH}" in
amd64|arm64) ;;
*) printf '✗ 不支持的 arch: %s(支持: amd64 | arm64\n' "${TARGET_ARCH}" >&2; exit 1 ;;
esac
# ── 路径与文件名配置 ─────────────────────────────────────────────────────────
DIST_DIR="${SCRIPT_DIR}/dist/desktop/${TARGET_OS}-${TARGET_ARCH}"
CACHE_DIR="${SCRIPT_DIR}/.build/cache"
VERSION_BARE="${SINGBOX_VERSION#v}" # 去掉前缀 v(如 v1.13.12 → 1.13.12
if [[ "${TARGET_OS}" == "windows" ]]; then
ARCHIVE_EXT="zip"
BIN_NAME="sing-box.exe"
else
ARCHIVE_EXT="tar.gz"
BIN_NAME="sing-box"
fi
ARCHIVE_STEM="sing-box-${VERSION_BARE}-${TARGET_OS}-${TARGET_ARCH}"
ARCHIVE_FILE="${ARCHIVE_STEM}.${ARCHIVE_EXT}"
GITHUB_BASE="https://github.com/SagerNet/sing-box/releases/download/${SINGBOX_VERSION}"
ARCHIVE_URL="${GITHUB_BASE}/${ARCHIVE_FILE}"
# ── sing-box 压缩包 SHA256(内置 pin)──────────────────────────────────────────
# sing-box 自 v1.13 起的 Release 不再发布 checksums.txt,故改为脚本内置各压缩包
# 的 SHA256。更新 SINGBOX_VERSION 时必须同步更新下表(case 键含版本号,未更新会
# 命中 *) 分支并报错,以防用错版本的哈希):
# curl -fsSL "${GITHUB_BASE}/<archive>" | shasum -a 256
EXPECTED_HASH=""
case "${ARCHIVE_FILE}" in
sing-box-1.13.12-windows-amd64.zip) EXPECTED_HASH="e93fc531134eb1beb4efa3c74990a24e48456098a31c03b60d5ddf17f223cf98" ;;
sing-box-1.13.12-darwin-arm64.tar.gz) EXPECTED_HASH="43eef86f0ea4a79c3696974f397a963c46a457ee46d1ffac9aa913944a5fc986" ;;
sing-box-1.13.12-darwin-amd64.tar.gz) EXPECTED_HASH="f3275316451bf1983bc059599c69c8ed0232d53a619d15cfd535f95cc9a4477a" ;;
sing-box-1.13.12-linux-amd64.tar.gz) EXPECTED_HASH="1540533adb3df24f5ad5f14b5c7ca3dbc2401b10a1c1eb278fcadcada47ec6c4" ;;
esac
OUT_BIN="${DIST_DIR}/${BIN_NAME}"
# ── 工具检查 ─────────────────────────────────────────────────────────────────
_need() {
if ! command -v "$1" >/dev/null 2>&1; then
printf '✗ 依赖未找到: %s\n' "$1" >&2; exit 1
fi
}
_need curl
_need sha256sum || _need shasum # macOS 用 shasumLinux 用 sha256sum
# 兼容 macOS (shasum -a 256) 和 Linux (sha256sum)
_sha256() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | awk '{print $1}'
else
shasum -a 256 "$1" | awk '{print $1}'
fi
}
# ── 下载助手:镜像优先,回退官方源 ─────────────────────────────────────────────
# 用法: _download_with_mirror <文件名> <官方回退URL> <输出路径>
# 若设了 DESKTOP_BIN_MIRROR:先试 ${DESKTOP_BIN_MIRROR}/<文件名>(带可选 token
# header),命中即返回;未设 / 镜像失败则回退官方 <回退URL>。
# SHA256 校验由调用方在下载后统一执行——镜像来的文件同样要过校验。
_download_with_mirror() {
local filename="$1" fallback_url="$2" out="$3"
if [[ -n "${DESKTOP_BIN_MIRROR:-}" ]]; then
local mirror_url="${DESKTOP_BIN_MIRROR%/}/${filename}"
local -a auth=()
if [[ -n "${DESKTOP_BIN_MIRROR_TOKEN:-}" ]]; then
auth=(-H "Authorization: token ${DESKTOP_BIN_MIRROR_TOKEN}")
fi
printf '==> [mirror] 尝试 %s…\n' "${mirror_url}"
# ${auth[@]+...} 兜住空数组 + set -umacOS bash 3.2 下 "${auth[@]}" 在数组
# 为空时会报 "unbound variable",此写法数组空则整体展开为空。
if curl -fSL --retry 5 --retry-delay 3 --retry-all-errors --connect-timeout 15 \
${auth[@]+"${auth[@]}"} -o "${out}" "${mirror_url}"; then
printf ' ✓ [mirror] 命中\n'
return 0
fi
printf ' ! [mirror] 未命中,回退官方源\n' >&2
fi
printf '==> 下载 %s…\n' "${fallback_url}"
curl -fSL --retry 8 --retry-delay 5 --retry-connrefused --retry-all-errors --connect-timeout 20 \
-o "${out}" "${fallback_url}"
}
# ── 幂等检查(已有产物则跳过;--force 强制重下)────────────────────────────────
if [[ "${FORCE}" == false && -f "${OUT_BIN}" ]]; then
printf '✓ %s 已存在,跳过下载\n' "${OUT_BIN}"
printf ' 当前 SHA256: %s\n' "$(_sha256 "${OUT_BIN}")"
printf ' (传 --force 强制重新下载并校验压缩包)\n'
exit 0
fi
# ── 创建目录 ─────────────────────────────────────────────────────────────────
mkdir -p "${DIST_DIR}"
mkdir -p "${CACHE_DIR}"
ARCHIVE_CACHE="${CACHE_DIR}/${ARCHIVE_FILE}"
# 未在内置 pin 表中找到该压缩包的 SHA256(多半是 SINGBOX_VERSION 升级后没同步哈希)
if [[ -z "${EXPECTED_HASH}" ]]; then
printf '✗ 脚本内未内置 %s 的 SHA256\n' "${ARCHIVE_FILE}" >&2
printf ' 升级 SINGBOX_VERSION 后需在本脚本 case 表补该压缩包哈希:\n' >&2
printf ' curl -fsSL "%s" | shasum -a 256\n' "${ARCHIVE_URL}" >&2
exit 1
fi
# ── 下载二进制压缩包(镜像优先,回退 GitHub Release)──────────────────────────
_download_with_mirror "${ARCHIVE_FILE}" "${ARCHIVE_URL}" "${ARCHIVE_CACHE}"
# ── SHA256 校验(对压缩包,比对内置 pin)──────────────────────────────────────
printf '==> 校验 SHA256…\n'
ACTUAL_HASH="$(_sha256 "${ARCHIVE_CACHE}")"
if [[ "${ACTUAL_HASH}" != "${EXPECTED_HASH}" ]]; then
printf '✗ SHA256 不匹配!\n' >&2
printf ' 期望: %s\n' "${EXPECTED_HASH}" >&2
printf ' 实际: %s\n' "${ACTUAL_HASH}" >&2
rm -f "${ARCHIVE_CACHE}"
exit 1
fi
printf ' ✓ SHA256 匹配: %s\n' "${ACTUAL_HASH}"
# ── 解压二进制 ────────────────────────────────────────────────────────────────
printf '==> 解压 → %s\n' "${DIST_DIR}"
EXTRACT_TMP="${CACHE_DIR}/extract-${ARCHIVE_STEM}"
rm -rf "${EXTRACT_TMP}"
mkdir -p "${EXTRACT_TMP}"
if [[ "${ARCHIVE_EXT}" == "zip" ]]; then
if ! command -v unzip >/dev/null 2>&1; then
printf '✗ unzip 未找到\n' >&2; exit 1
fi
unzip -q "${ARCHIVE_CACHE}" -d "${EXTRACT_TMP}"
else
tar -xzf "${ARCHIVE_CACHE}" -C "${EXTRACT_TMP}"
fi
# sing-box 压缩包内层目录名与 ARCHIVE_STEM 相同
INNER_DIR="${EXTRACT_TMP}/${ARCHIVE_STEM}"
if [[ ! -f "${INNER_DIR}/${BIN_NAME}" ]]; then
printf '✗ 压缩包中未找到 %s/%s\n' "${ARCHIVE_STEM}" "${BIN_NAME}" >&2
exit 1
fi
cp "${INNER_DIR}/${BIN_NAME}" "${OUT_BIN}"
chmod 755 "${OUT_BIN}"
printf '\n✓ sing-box 二进制: %s\n' "${OUT_BIN}"
printf ' 版本验证: %s version\n' "${OUT_BIN}"
# ── Windows 额外处理: wintun.dll ─────────────────────────────────────────────
if [[ "${TARGET_OS}" == "windows" ]]; then
printf '\n==> Windows: 下载 wintun.dll (v%s)…\n' "${WINTUN_VERSION}"
WINTUN_ZIP_NAME="wintun-${WINTUN_VERSION}.zip"
WINTUN_URL="https://www.wintun.net/builds/${WINTUN_ZIP_NAME}"
WINTUN_ZIP_CACHE="${CACHE_DIR}/${WINTUN_ZIP_NAME}"
WINTUN_OUT="${DIST_DIR}/wintun.dll"
if [[ "${FORCE}" == false && -f "${WINTUN_OUT}" ]]; then
printf '✓ wintun.dll 已存在,跳过(传 --force 重新下载)\n'
else
# 镜像优先,回退 wintun.net
_download_with_mirror "${WINTUN_ZIP_NAME}" "${WINTUN_URL}" "${WINTUN_ZIP_CACHE}"
printf '==> 校验 wintun.zip SHA256…\n'
WINTUN_ACTUAL="$(_sha256 "${WINTUN_ZIP_CACHE}")"
if [[ "${WINTUN_ACTUAL}" != "${WINTUN_SHA256}" ]]; then
printf '✗ wintun.zip SHA256 不匹配!\n' >&2
printf ' 期望: %s\n' "${WINTUN_SHA256}" >&2
printf ' 实际: %s\n' "${WINTUN_ACTUAL}" >&2
printf ' 如果 WINTUN_VERSION 已更新,请同步更新脚本中的 WINTUN_SHA256\n' >&2
rm -f "${WINTUN_ZIP_CACHE}"
exit 1
fi
printf ' ✓ wintun.zip SHA256 匹配\n'
# 提取对应 arch 的 wintun.dll
# wintun zip 内路径: wintun/bin/<arch>/wintun.dll
WINTUN_EXTRACT="${CACHE_DIR}/extract-wintun"
rm -rf "${WINTUN_EXTRACT}"
mkdir -p "${WINTUN_EXTRACT}"
unzip -q "${WINTUN_ZIP_CACHE}" -d "${WINTUN_EXTRACT}"
WINTUN_DLL_SRC="${WINTUN_EXTRACT}/wintun/bin/${TARGET_ARCH}/wintun.dll"
if [[ ! -f "${WINTUN_DLL_SRC}" ]]; then
printf '✗ wintun zip 中未找到 %s arch 的 wintun.dll\n' "${TARGET_ARCH}" >&2
exit 1
fi
cp "${WINTUN_DLL_SRC}" "${WINTUN_OUT}"
printf '✓ wintun.dll: %s\n' "${WINTUN_OUT}"
fi
fi
# ── 最终输出摘要 ─────────────────────────────────────────────────────────────
printf '\n=== 产物清单: %s ===\n' "${DIST_DIR}"
ls -lh "${DIST_DIR}/"