#!/usr/bin/env bash # # local_test.sh — macOS 客户端本地联调一条龙(Developer ID 重签 + 装 + 跑)。 # # 背景:flutter build 出来的包是 Apple Development 签名;要让 System Extension # 能加载(且不依赖 SIP/dev mode),需重签成 Developer ID。本脚本封装这套流程。 # # 子命令: # build flutter build macos --release(注入联调节点地址) # sign Developer ID 内向外重签整个 app + PacketTunnel sysext(嵌 DevID profile) # copy 覆盖安装到 /Applications # run 直跑 /Applications 版本(绕 Gatekeeper,实时输出日志,并打印测试账号) # all 依次执行 build → sign → copy → run # # 用法: scripts/local_test.sh all | scripts/local_test.sh build ... set -euo pipefail # ─────────── 配置(按需改)─────────── API_URL="http://103.119.13.48:8080" # 联调控制面;发版改这里或走默认 SIGN_ID="Developer ID Application: Yanmei (beijing) Technology Co., Ltd (BYL4KQHMTN)" APP_PROFILE_NAME="Pangolin App DevID" # 主 app 的 Developer ID 描述文件名 SE_PROFILE_NAME="Pangolin PacketTunnel DevID" # PacketTunnel 的描述文件名 TEST_EMAIL="wang880812@gmail.com" # 联调测试账号 TEST_PASSWORD="pangolin2026" NOTARY_PROFILE="pangolin-notary" # notarytool 凭据(已存入钥匙串) # 注:DevID profile 已含 system-extension.install + NE(-systemextension 变体); # app/sysext entitlements 与之对齐(见 write_entitlements)。 # ─────────── 路径推导 ─────────── SRC="${BASH_SOURCE[0]}" DIR="${SRC%/*}"; [ "$DIR" = "$SRC" ] && DIR="." cd "$DIR/.."; REPO_ROOT="$PWD" CLIENT="$REPO_ROOT/client" APP="$CLIENT/build/macos/Build/Products/Release/pangolin_vpn.app" SE="$APP/Contents/Library/SystemExtensions/PacketTunnel.systemextension" LIBFW="$SE/Contents/Frameworks/Libbox.framework" PROF_DIR="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles" WORK="${TMPDIR:-/tmp}/pangolin_local_test"; mkdir -p "$WORK" log(){ printf '\033[1;33m== %s ==\033[0m\n' "$*"; } die(){ printf '\033[1;31m❌ %s\033[0m\n' "$*" >&2; exit 1; } # 按 Name 在描述文件目录里定位 .provisionprofile(结果写入全局 FOUND)。 FOUND="" find_profile(){ local want="$1" f for f in "$PROF_DIR"/*.provisionprofile; do [ -e "$f" ] || continue if security cms -D -i "$f" 2>/dev/null | grep -q ">$want<"; then FOUND="$f"; return 0 fi done return 1 } write_entitlements(){ cat > "$WORK/app.entitlements" <<'PLIST' com.apple.application-identifierBYL4KQHMTN.com.pangolin.pangolin com.apple.developer.team-identifierBYL4KQHMTN com.apple.developer.system-extension.install com.apple.developer.networking.networkextension packet-tunnel-provider-systemextension com.apple.security.app-sandbox com.apple.security.network.client com.apple.security.network.server keychain-access-groups BYL4KQHMTN.com.pangolin.pangolin PLIST cat > "$WORK/sysext.entitlements" <<'PLIST' com.apple.application-identifierBYL4KQHMTN.com.pangolin.pangolin.PacketTunnel com.apple.developer.team-identifierBYL4KQHMTN com.apple.developer.networking.networkextension packet-tunnel-provider-systemextension com.apple.security.app-sandbox com.apple.security.application-groups group.com.pangolin.pangolin PLIST } cs(){ codesign --force --options runtime --timestamp "$@"; } cmd_build(){ log "构建 release(API=$API_URL)" cd "$CLIENT" flutter build macos --release --dart-define="PANGOLIN_API_URL=$API_URL" [ -d "$APP" ] || die "构建产物不存在: $APP" } cmd_sign(){ [ -d "$APP" ] || die "先 build:找不到 $APP" log "Developer ID 重签" write_entitlements find_profile "$APP_PROFILE_NAME" || die "找不到描述文件: $APP_PROFILE_NAME" local app_prof="$FOUND" find_profile "$SE_PROFILE_NAME" || die "找不到描述文件: $SE_PROFILE_NAME" local se_prof="$FOUND" echo " app profile : $app_prof" echo " sysext prof : $se_prof" xattr -cr "$APP" cp "$app_prof" "$APP/Contents/embedded.provisionprofile" cp "$se_prof" "$SE/Contents/embedded.provisionprofile" # 内向外:Libbox 真二进制 → Libbox.framework → sysext → app 各 framework → app 主体 cs -s "$SIGN_ID" "$LIBFW/Versions/A/Libbox" cs -s "$SIGN_ID" "$LIBFW" cs --entitlements "$WORK/sysext.entitlements" -s "$SIGN_ID" "$SE" local item for item in "$APP/Contents/Frameworks/"*; do [ -e "$item" ] && cs -s "$SIGN_ID" "$item" done cs --entitlements "$WORK/app.entitlements" -s "$SIGN_ID" "$APP" codesign --verify --deep --strict "$APP" || die "深度校验失败" echo " ✅ 重签 + 校验通过($SIGN_ID)" } cmd_notarize(){ [ -d "$APP" ] || die "先 build/sign:找不到 $APP" log "公证(notarytool submit --wait,通常 1-5 分钟)" local zip="$WORK/pangolin_vpn.zip" rm -f "$zip" ditto -c -k --keepParent "$APP" "$zip" xcrun notarytool submit "$zip" --keychain-profile "$NOTARY_PROFILE" --wait log "staple 票据到 app" xcrun stapler staple "$APP" xcrun stapler validate "$APP" && echo " ✅ 已公证 + staple" } cmd_copy(){ [ -d "$APP" ] || die "先 build/sign:找不到 $APP" log "安装到 /Applications" osascript -e 'quit app "pangolin_vpn"' 2>/dev/null || true pkill -x pangolin_vpn 2>/dev/null || true sleep 1 rm -rf /Applications/pangolin_vpn.app cp -R "$APP" /Applications/ echo " ✅ /Applications/pangolin_vpn.app" } cmd_run(){ log "启动 /Applications/pangolin_vpn.app(直跑,日志见下)" echo "" echo " 测试账号: $TEST_EMAIL" echo " 测试密码: $TEST_PASSWORD" echo "" exec /Applications/pangolin_vpn.app/Contents/MacOS/pangolin_vpn } case "${1:-}" in build) cmd_build ;; sign) cmd_sign ;; # 旧:手动 Developer ID 重签(现已由 Xcode 工程配置直接签,通常不需要) notarize) cmd_notarize ;; copy) cmd_copy ;; run) cmd_run ;; # Xcode 工程已配 Developer ID 手动签名(Release 配置),build 即出 DevID 包,无需 sign。 all) cmd_build; cmd_notarize; cmd_copy; cmd_run ;; *) echo "用法: $0 {all|build|sign|notarize|copy|run}"; exit 2 ;; esac