#!/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" # ── M3 内存裁剪决策记录(tsk_nsobbj_rJdy0)──────────────────────────────────── # NE 进程内存上限实测(MemoryMonitor 打点): # 初始 build tags(with_quic,with_utls,with_clash_api,with_gvisor): # 预估 RSS:~20-25 MB(含 Go runtime ~8MB + gVisor ~4MB + QUIC ~5MB + uTLS ~2MB + 基础~6MB) # 旧设备(≤ A9)15MB 上限下会触顶;新设备(≥ A12)50MB 上限下可运行 # # 若真机测试出现 "NEProvider memory limit exceeded" / jetsam 杀进程,按以下顺序裁减: # Step 1: 去掉 with_gvisor → 改用系统 TUN 网络栈;减约 2-4 MB # 取消注释下一行: # BUILD_TAGS="with_quic,with_utls,with_clash_api" # Step 2: 去掉 with_grpc → 若不需要 gRPC 入站;减约 4 MB # 取消注释下一行: # BUILD_TAGS="with_quic,with_utls,with_clash_api" # 已无 gvisor,此行等效 # Step 3: 去掉 with_quic → 仅在放弃 Hysteria2/QUIC 协议时考虑;减约 3 MB # 取消注释下一行: # BUILD_TAGS="with_utls,with_clash_api" # # 裁减后执行: ./build-ios.sh --force 重出 XCFramework,然后在 Xcode 中 # 重新拖入 dist/ios/Libbox.xcframework(或更新 SPM/Pods 引用)。 # 在此注释中记录最终选定的 build tags 及峰值 RSS(由 MemoryMonitor 汇报)。 # # 当前状态:等待真机 10min 压测结果(MemoryMonitor 每 10s 打点)。 # ── 幂等检查 ───────────────────────────────────────────────────────────────── 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