#!/usr/bin/env bash # fetch-desktop-bin.sh — 从 sing-box GitHub Release 下载桌面平台二进制 # # 用法: ./fetch-desktop-bin.sh [--force] # os : linux | darwin | windows # arch : amd64 | arm64 # --force 忽略已有产物,强制重新下载 # # 产物目录: dist/desktop/-/ # 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-.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 <值>`。 # 不设则匿名 GET(NAS 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-.zip | sha256sum WINTUN_SHA256="07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51" # ── 参数解析 ───────────────────────────────────────────────────────────────── if [[ $# -lt 2 ]]; then printf 'Usage: %s [--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}/" | 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 用 shasum,Linux 用 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 -u:macOS 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//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}/"