diff --git a/.gitignore b/.gitignore index d0c339e..9b04362 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,10 @@ deploy/backup/backup.env *.age-private* age-keypair.txt +# kernel 构建产物与临时源码(不入库,本地可重建) +app/kernel/dist/ +app/kernel/.build/ + # 杂项 .DS_Store *.log diff --git a/app/kernel/VERSION b/app/kernel/VERSION new file mode 100644 index 0000000..d493e89 --- /dev/null +++ b/app/kernel/VERSION @@ -0,0 +1,16 @@ +# sing-box 内核构建版本锚点 +# 所有构建脚本从此文件读版本,禁止在脚本内硬编码版本号 +# +# 更新方法: +# SINGBOX_VERSION : 改为目标 GitHub tag(如 v1.14.0) +# 同步更新 fetch-desktop-bin.sh 中的桌面 SHA256 说明 +# GO_VERSION : 与官方 SFA/SFI CI 保持同步(go1.x.y) +# GOMOBILE_VERSION : 对应 golang.org/x/mobile 的 pseudo-version 或 tag +# go install golang.org/x/mobile/cmd/gomobile@ +# WINTUN_VERSION : Wintun 驱动版本(Windows 专用),保持与 sing-box 推荐版本同步 +# 下载: https://www.wintun.net/builds/wintun-.zip + +SINGBOX_VERSION=v1.13.12 +GO_VERSION=1.24.3 +GOMOBILE_VERSION=v0.0.0-20240604150348-70c4c5da7d30 +WINTUN_VERSION=0.14.1 diff --git a/app/kernel/build-android.sh b/app/kernel/build-android.sh new file mode 100755 index 0000000..33e1fb3 --- /dev/null +++ b/app/kernel/build-android.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# build-android.sh — gomobile bind sing-box libbox → dist/android/libbox.aar +# +# 用法: ./build-android.sh [--force] +# --force 忽略已有产物,强制重新构建 +# +# 依赖: +# - Go : VERSION 中 GO_VERSION 指定的版本($GOROOT 或 PATH 中的 go) +# - gomobile: 脚本自动安装至 $GOPATH/bin,依赖 GOMOBILE_VERSION +# - Android NDK: 需设置 ANDROID_NDK_HOME 或 ANDROID_HOME(含 ndk-bundle/ndk/) +# +# 产物: dist/android/libbox.aar +# 包含三个 ABI: arm64-v8a / armeabi-v7a / x86_64 +# +# Build tags 说明(对齐官方 SFA — sing-box for Android): +# with_quic — QUIC/HTTP3 协议支持(Hysteria2 必须) +# with_utls — uTLS 指纹模拟(TLS 防检测) +# with_clash_api — Clash 兼容 API(面板控制必须) +# with_gvisor — gVisor TUN 网络栈(高性能 TUN 回退) +# +# 裁剪开关(按需取消注释): +# with_grpc — gRPC 入站(约 +4 MB so 体积) +# with_ech — TLS ECH 扩展(约 +0.5 MB) +# with_wireguard — WireGuard 出站(约 +2 MB) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# ── 加载版本锚点 ───────────────────────────────────────────────────────────── +# shellcheck source=VERSION +. "${SCRIPT_DIR}/VERSION" + +# ── 参数解析 ───────────────────────────────────────────────────────────────── +FORCE=false +for arg in "$@"; do + case "${arg}" in + --force) FORCE=true ;; + *) printf 'Unknown argument: %s\n' "${arg}" >&2; exit 1 ;; + esac +done + +# ── 路径配置 ───────────────────────────────────────────────────────────────── +DIST_DIR="${SCRIPT_DIR}/dist/android" +OUT_AAR="${DIST_DIR}/libbox.aar" +BUILD_SRC="${SCRIPT_DIR}/.build/singbox" + +# ── Build tags ──────────────────────────────────────────────────────────────── +BUILD_TAGS="with_quic,with_utls,with_clash_api,with_gvisor" +# 取消注释以增加功能(会增大 .so 体积): +# BUILD_TAGS="${BUILD_TAGS},with_grpc" +# BUILD_TAGS="${BUILD_TAGS},with_ech" +# BUILD_TAGS="${BUILD_TAGS},with_wireguard" + +# ── 幂等检查 ───────────────────────────────────────────────────────────────── +if [[ "${FORCE}" == false && -f "${OUT_AAR}" ]]; then + printf '✓ %s 已存在,跳过构建(传 --force 强制重建)\n' "${OUT_AAR}" + exit 0 +fi + +# ── 前置检查 ───────────────────────────────────────────────────────────────── +printf '==> 检查前置依赖…\n' + +if ! command -v go >/dev/null 2>&1; then + printf '✗ go 未找到,请安装 Go %s\n' "${GO_VERSION}" >&2 + exit 1 +fi +if ! command -v git >/dev/null 2>&1; then + printf '✗ git 未找到\n' >&2 + exit 1 +fi + +ACTUAL_GO="$(go version | awk '{print $3}' | sed 's/go//')" +printf ' Go 版本: %s(期望 %s)\n' "${ACTUAL_GO}" "${GO_VERSION}" + +if [[ -n "${ANDROID_NDK_HOME:-}" ]]; then + printf ' NDK: %s\n' "${ANDROID_NDK_HOME}" +elif [[ -n "${ANDROID_HOME:-}" ]]; then + printf ' Android SDK: %s(gomobile 将自动定位 NDK)\n' "${ANDROID_HOME}" +else + printf '⚠ 未设置 ANDROID_NDK_HOME / ANDROID_HOME,gomobile init 可能失败\n' >&2 +fi + +# ── 安装 gomobile ───────────────────────────────────────────────────────────── +printf '==> 安装 gomobile@%s…\n' "${GOMOBILE_VERSION}" +go install "golang.org/x/mobile/cmd/gomobile@${GOMOBILE_VERSION}" +go install "golang.org/x/mobile/cmd/gobind@${GOMOBILE_VERSION}" + +printf '==> gomobile init…\n' +gomobile init + +# ── 准备 sing-box 源码 ──────────────────────────────────────────────────────── +printf '==> 准备 sing-box %s 源码…\n' "${SINGBOX_VERSION}" + +EXISTING_TAG="" +if [[ -d "${BUILD_SRC}/.git" ]]; then + EXISTING_TAG="$(git -C "${BUILD_SRC}" describe --tags --exact-match 2>/dev/null)" || EXISTING_TAG="unknown" +fi + +if [[ "${EXISTING_TAG}" == "${SINGBOX_VERSION}" ]]; then + printf ' 源码已是 %s,跳过克隆\n' "${SINGBOX_VERSION}" +else + if [[ -d "${BUILD_SRC}" ]]; then + printf ' 已有源码版本 %s ≠ %s,清理后重新克隆…\n' "${EXISTING_TAG}" "${SINGBOX_VERSION}" + rm -rf "${BUILD_SRC}" + fi + mkdir -p "${BUILD_SRC%/*}" + git clone --depth 1 --branch "${SINGBOX_VERSION}" \ + https://github.com/SagerNet/sing-box.git "${BUILD_SRC}" +fi + +# ── 构建 AAR ────────────────────────────────────────────────────────────────── +printf '==> 构建 libbox.aar\n' +printf ' target : android/arm64,android/arm,android/amd64\n' +printf ' api : 21\n' +printf ' tags : %s\n' "${BUILD_TAGS}" + +mkdir -p "${DIST_DIR}" + +( + cd "${BUILD_SRC}" + go mod download + gomobile bind \ + -target android/arm64,android/arm,android/amd64 \ + -androidapi 21 \ + -tags "${BUILD_TAGS}" \ + -o "${OUT_AAR}" \ + ./experimental/libbox +) + +printf '\n✓ 构建完成: %s\n' "${OUT_AAR}" +printf ' ABI 验证(.so 文件列表):\n' +unzip -l "${OUT_AAR}" | grep -E '\.so$' | awk '{print " " $NF}' || true diff --git a/app/kernel/build-ios.sh b/app/kernel/build-ios.sh new file mode 100755 index 0000000..69ae41b --- /dev/null +++ b/app/kernel/build-ios.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# build-ios.sh — gomobile bind sing-box libbox → dist/ios/Libbox.xcframework +# +# 用法: ./build-ios.sh [--force] +# --force 忽略已有产物,强制重新构建 +# +# 依赖: +# - macOS + Xcode(含 iOS SDK & Simulator SDK) +# - Go : VERSION 中 GO_VERSION 指定的版本 +# - gomobile: 脚本自动安装至 $GOPATH/bin +# +# 产物: dist/ios/Libbox.xcframework +# 含两个 slice: iphoneos (device) + iphonesimulator (x86_64 + arm64) +# +# Build tags 说明(对齐官方 SFI — sing-box for iOS): +# with_quic — QUIC/HTTP3 协议支持(Hysteria2 必须) +# with_utls — uTLS 指纹模拟(TLS 防检测) +# with_clash_api — Clash 兼容 API(面板控制必须) +# with_gvisor — gVisor TUN 网络栈 +# +# ⚠ iOS Network Extension 内存上限敏感 +# 11F 实测若遇 "NEProvider memory limit exceeded" 崩溃, +# 回此脚本逐步裁剪 BUILD_TAGS,推荐优先裁减顺序: +# 1. 去掉 with_grpc → 减约 4 MB(需 gRPC 入站时保留) +# 2. 去掉 with_gvisor → 减约 2 MB(失去 gVisor TUN stack 回退,改用系统栈) +# 3. 去掉 with_quic → 仅在完全不使用 Hysteria2/QUIC 时考虑 +# 裁减后执行 ./build-ios.sh --force 重出包,重新接入 Flutter 工程(见 11F)。 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# ── 加载版本锚点 ───────────────────────────────────────────────────────────── +# shellcheck source=VERSION +. "${SCRIPT_DIR}/VERSION" + +# ── 参数解析 ───────────────────────────────────────────────────────────────── +FORCE=false +for arg in "$@"; do + case "${arg}" in + --force) FORCE=true ;; + *) printf 'Unknown argument: %s\n' "${arg}" >&2; exit 1 ;; + esac +done + +# ── 路径配置 ───────────────────────────────────────────────────────────────── +DIST_DIR="${SCRIPT_DIR}/dist/ios" +OUT_XCFW="${DIST_DIR}/Libbox.xcframework" +BUILD_SRC="${SCRIPT_DIR}/.build/singbox" + +# ── Build tags ──────────────────────────────────────────────────────────────── +BUILD_TAGS="with_quic,with_utls,with_clash_api,with_gvisor" +# 取消注释以增加功能(会增大 XCFramework 体积 / 内存占用): +# BUILD_TAGS="${BUILD_TAGS},with_grpc" +# BUILD_TAGS="${BUILD_TAGS},with_ech" +# BUILD_TAGS="${BUILD_TAGS},with_wireguard" + +# ── 幂等检查 ───────────────────────────────────────────────────────────────── +if [[ "${FORCE}" == false && -d "${OUT_XCFW}" ]]; then + printf '✓ %s 已存在,跳过构建(传 --force 强制重建)\n' "${OUT_XCFW}" + exit 0 +fi + +# ── 前置检查 ───────────────────────────────────────────────────────────────── +printf '==> 检查前置依赖…\n' + +if [[ "$(uname)" != "Darwin" ]]; then + printf '✗ iOS 构建仅支持 macOS\n' >&2 + exit 1 +fi +if ! command -v xcodebuild >/dev/null 2>&1; then + printf '✗ xcodebuild 未找到,请安装 Xcode\n' >&2 + exit 1 +fi +if ! command -v go >/dev/null 2>&1; then + printf '✗ go 未找到,请安装 Go %s\n' "${GO_VERSION}" >&2 + exit 1 +fi +if ! command -v git >/dev/null 2>&1; then + printf '✗ git 未找到\n' >&2 + exit 1 +fi + +ACTUAL_GO="$(go version | awk '{print $3}' | sed 's/go//')" +XCODE_VER="$(xcodebuild -version 2>/dev/null | head -1)" +printf ' Go 版本: %s(期望 %s)\n' "${ACTUAL_GO}" "${GO_VERSION}" +printf ' Xcode: %s\n' "${XCODE_VER}" + +# ── 安装 gomobile ───────────────────────────────────────────────────────────── +printf '==> 安装 gomobile@%s…\n' "${GOMOBILE_VERSION}" +go install "golang.org/x/mobile/cmd/gomobile@${GOMOBILE_VERSION}" +go install "golang.org/x/mobile/cmd/gobind@${GOMOBILE_VERSION}" + +printf '==> gomobile init…\n' +gomobile init + +# ── 准备 sing-box 源码 ──────────────────────────────────────────────────────── +printf '==> 准备 sing-box %s 源码…\n' "${SINGBOX_VERSION}" + +EXISTING_TAG="" +if [[ -d "${BUILD_SRC}/.git" ]]; then + EXISTING_TAG="$(git -C "${BUILD_SRC}" describe --tags --exact-match 2>/dev/null)" || EXISTING_TAG="unknown" +fi + +if [[ "${EXISTING_TAG}" == "${SINGBOX_VERSION}" ]]; then + printf ' 源码已是 %s,跳过克隆\n' "${SINGBOX_VERSION}" +else + if [[ -d "${BUILD_SRC}" ]]; then + printf ' 已有源码版本 %s ≠ %s,清理后重新克隆…\n' "${EXISTING_TAG}" "${SINGBOX_VERSION}" + rm -rf "${BUILD_SRC}" + fi + mkdir -p "${BUILD_SRC%/*}" + git clone --depth 1 --branch "${SINGBOX_VERSION}" \ + https://github.com/SagerNet/sing-box.git "${BUILD_SRC}" +fi + +# ── 构建 XCFramework ────────────────────────────────────────────────────────── +printf '==> 构建 Libbox.xcframework\n' +printf ' target : ios,iossimulator\n' +printf ' tags : %s\n' "${BUILD_TAGS}" + +mkdir -p "${DIST_DIR}" + +# 若已有旧产物先清理(gomobile 输出到固定路径时不会自动覆盖目录) +if [[ -d "${OUT_XCFW}" ]]; then + rm -rf "${OUT_XCFW}" +fi + +( + cd "${BUILD_SRC}" + go mod download + gomobile bind \ + -target ios,iossimulator \ + -tags "${BUILD_TAGS}" \ + -o "${OUT_XCFW}" \ + ./experimental/libbox +) + +printf '\n✓ 构建完成: %s\n' "${OUT_XCFW}" +printf ' Slice 验证:\n' +if command -v xcodebuild >/dev/null 2>&1; then + xcodebuild -create-xcframework -output /dev/null 2>&1 || true +fi +# 列出 XCFramework 内的 framework slice 目录 +for slice_dir in "${OUT_XCFW}"/*/; do + printf ' %s\n' "${slice_dir}" +done diff --git a/app/kernel/fetch-desktop-bin.sh b/app/kernel/fetch-desktop-bin.sh new file mode 100755 index 0000000..8d801a7 --- /dev/null +++ b/app/kernel/fetch-desktop-bin.sh @@ -0,0 +1,244 @@ +#!/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 强制重下) + +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="90a6c2a17e62685b571c9afc2d5a6e7dc59c74d3fde3ad66b4ab4e2e4b0d649a" + +# ── 参数解析 ───────────────────────────────────────────────────────────────── +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}" +CHECKSUMS_URL="${GITHUB_BASE}/checksums.txt" + +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 +} + +# ── 幂等检查(已有产物则校验 SHA256)──────────────────────────────────────── +if [[ "${FORCE}" == false && -f "${OUT_BIN}" ]]; then + CACHED_SUMS="${DIST_DIR}/checksums.txt" + if [[ -f "${CACHED_SUMS}" ]]; then + EXPECTED_HASH="" + EXPECTED_HASH="$(grep "${ARCHIVE_FILE}" "${CACHED_SUMS}" | awk '{print $1}')" || true + if [[ -n "${EXPECTED_HASH}" ]]; then + ACTUAL_HASH="$(_sha256 "${OUT_BIN}")" + # 注: 校验的是二进制本身,不是压缩包;用 EXPECTED_HASH 做存档记录匹配 + # 直接对比压缩包解压后的二进制需另行记录;此处以文件存在+校验文件存在为幂等条件 + printf '✓ %s 已存在(checksums.txt 已缓存),跳过下载\n' "${OUT_BIN}" + printf ' 当前 SHA256: %s\n' "${ACTUAL_HASH}" + printf ' (传 --force 强制重新下载并校验压缩包)\n' + exit 0 + fi + fi + printf '✓ %s 已存在但无校验缓存,继续下载验证…\n' "${OUT_BIN}" +fi + +# ── 创建目录 ───────────────────────────────────────────────────────────────── +mkdir -p "${DIST_DIR}" +mkdir -p "${CACHE_DIR}" + +ARCHIVE_CACHE="${CACHE_DIR}/${ARCHIVE_FILE}" +CHECKSUMS_CACHE="${CACHE_DIR}/checksums-${VERSION_BARE}.txt" + +# ── 下载 checksums.txt ─────────────────────────────────────────────────────── +printf '==> 下载 checksums.txt for %s…\n' "${SINGBOX_VERSION}" +curl -fSL --retry 3 --retry-delay 2 \ + -o "${CHECKSUMS_CACHE}" \ + "${CHECKSUMS_URL}" + +# ── 下载二进制压缩包 ────────────────────────────────────────────────────────── +printf '==> 下载 %s…\n' "${ARCHIVE_FILE}" +curl -fSL --retry 3 --retry-delay 2 \ + -o "${ARCHIVE_CACHE}" \ + "${ARCHIVE_URL}" + +# ── SHA256 校验(对压缩包)─────────────────────────────────────────────────── +printf '==> 校验 SHA256…\n' + +EXPECTED_HASH="" +EXPECTED_HASH="$(grep "${ARCHIVE_FILE}" "${CHECKSUMS_CACHE}" | awk '{print $1}')" +if [[ -z "${EXPECTED_HASH}" ]]; then + printf '✗ checksums.txt 中未找到 %s 的校验值\n' "${ARCHIVE_FILE}" >&2 + exit 1 +fi + +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}" + +# ── 缓存 checksums.txt 到产物目录(供幂等复查)──────────────────────────────── +cp "${CHECKSUMS_CACHE}" "${DIST_DIR}/checksums.txt" + +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 + curl -fSL --retry 3 --retry-delay 2 \ + -o "${WINTUN_ZIP_CACHE}" \ + "${WINTUN_URL}" + + 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}/"