Merge branch 'main' into worktree-monitor-debounce
# Conflicts: # scripts/vpn_test.py
This commit is contained in:
+154
-52
@@ -1,65 +1,167 @@
|
||||
#!/usr/bin/env bash
|
||||
# local_test.sh —— 一键构建并运行【release】版 macOS 客户端,连真实节点做端到端验证。
|
||||
#
|
||||
# 用真正的 release 包(不是 dev 模拟),前台运行以便直接看内核(sing-box)日志。
|
||||
# 改完代码直接跑此脚本即可。
|
||||
# local_test.sh — macOS 客户端本地联调一条龙(Developer ID 重签 + 装 + 跑)。
|
||||
#
|
||||
# bash scripts/local_test.sh # 构建 release 并运行
|
||||
# bash scripts/local_test.sh --no-build # 跳过构建,直接跑上次的产物(快速重跑)
|
||||
# 背景:flutter build 出来的包是 Apple Development 签名;要让 System Extension
|
||||
# 能加载(且不依赖 SIP/dev mode),需重签成 Developer ID。本脚本封装这套流程。
|
||||
#
|
||||
# 可选环境变量:
|
||||
# PANGOLIN_API_URL 控制面地址(默认连 racknerd 节点)
|
||||
# SINGBOX_BIN sing-box 路径(默认 brew 的 /opt/homebrew/bin/sing-box)
|
||||
# 子命令:
|
||||
# build flutter build macos --release(注入联调节点地址)
|
||||
# sign Developer ID 内向外重签整个 app + PacketTunnel sysext(嵌 DevID profile)
|
||||
# copy 覆盖安装到 /Applications
|
||||
# run 直跑 /Applications 版本(绕 Gatekeeper,实时输出日志,并打印测试账号)
|
||||
# all 依次执行 build → sign → copy → run
|
||||
#
|
||||
# 前置:flutter、sing-box(brew install sing-box)、已在 Xcode 配好签名团队。
|
||||
# TUN 需 root → 脚本会装一条 NOPASSWD sudoers(一次性,可能让你输一次电脑密码)。
|
||||
# 正式版会用 SMJobBless 特权 Helper 取代 sudo(BACKLOG-11D-HELPER)。
|
||||
# 用法: scripts/local_test.sh all | scripts/local_test.sh build ...
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
# ─────────── 配置(按需改)───────────
|
||||
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"
|
||||
API_URL="${PANGOLIN_API_URL:-http://107.172.55.251:8080}"
|
||||
SINGBOX="${SINGBOX_BIN:-/opt/homebrew/bin/sing-box}"
|
||||
NO_BUILD=0
|
||||
[ "${1:-}" = "--no-build" ] && NO_BUILD=1
|
||||
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"
|
||||
|
||||
command -v flutter >/dev/null 2>&1 || { echo "✗ 需要 flutter"; exit 1; }
|
||||
[ -x "$SINGBOX" ] || { echo "✗ 找不到 sing-box: $SINGBOX(brew install sing-box,或设 SINGBOX_BIN)"; exit 1; }
|
||||
log(){ printf '\033[1;33m== %s ==\033[0m\n' "$*"; }
|
||||
die(){ printf '\033[1;31m❌ %s\033[0m\n' "$*" >&2; exit 1; }
|
||||
|
||||
# ── 1. TUN 免密 sudoers(已生效则跳过,避免反复要密码)─────────────────────────
|
||||
echo "==> [1/3] sing-box 免密 sudo(TUN 需 root)"
|
||||
if sudo -n "$SINGBOX" version >/dev/null 2>&1; then
|
||||
echo " 已生效,跳过(无需密码)"
|
||||
else
|
||||
SUDO_FILE=/etc/sudoers.d/pangolin-singbox
|
||||
WANT="$(whoami) ALL=(root) NOPASSWD: $SINGBOX"
|
||||
TMP="$(mktemp)"
|
||||
printf '%s\n' "$WANT" > "$TMP"
|
||||
if sudo visudo -cf "$TMP" >/dev/null 2>&1; then
|
||||
sudo install -m 440 -o root -g wheel "$TMP" "$SUDO_FILE"
|
||||
echo " 已写入 $SUDO_FILE: $WANT"
|
||||
else
|
||||
echo "✗ sudoers 语法校验失败"; rm -f "$TMP"; exit 1
|
||||
fi
|
||||
rm -f "$TMP"
|
||||
fi
|
||||
# 按 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
|
||||
}
|
||||
|
||||
# ── 2. 构建 release(--no-build 跳过,直接跑上次产物)─────────────────────────
|
||||
if [ "$NO_BUILD" = "1" ]; then
|
||||
echo "==> [2/3] 跳过构建(--no-build)"
|
||||
else
|
||||
echo "==> [2/3] flutter build macos --release (API=$API_URL)"
|
||||
( cd "$CLIENT" && flutter build macos --release --dart-define=PANGOLIN_API_URL="$API_URL" )
|
||||
fi
|
||||
write_entitlements(){
|
||||
cat > "$WORK/app.entitlements" <<'PLIST'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict>
|
||||
<key>com.apple.application-identifier</key><string>BYL4KQHMTN.com.pangolin.pangolin</string>
|
||||
<key>com.apple.developer.team-identifier</key><string>BYL4KQHMTN</string>
|
||||
<key>com.apple.developer.system-extension.install</key><true/>
|
||||
<key>com.apple.developer.networking.networkextension</key>
|
||||
<array><string>packet-tunnel-provider-systemextension</string></array>
|
||||
<key>com.apple.security.app-sandbox</key><false/>
|
||||
<key>com.apple.security.network.client</key><true/>
|
||||
<key>com.apple.security.network.server</key><true/>
|
||||
<key>keychain-access-groups</key>
|
||||
<array><string>BYL4KQHMTN.com.pangolin.pangolin</string></array>
|
||||
</dict></plist>
|
||||
PLIST
|
||||
cat > "$WORK/sysext.entitlements" <<'PLIST'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict>
|
||||
<key>com.apple.application-identifier</key><string>BYL4KQHMTN.com.pangolin.pangolin.PacketTunnel</string>
|
||||
<key>com.apple.developer.team-identifier</key><string>BYL4KQHMTN</string>
|
||||
<key>com.apple.developer.networking.networkextension</key>
|
||||
<array><string>packet-tunnel-provider-systemextension</string></array>
|
||||
<key>com.apple.security.app-sandbox</key><true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array><string>group.com.pangolin.pangolin</string></array>
|
||||
</dict></plist>
|
||||
PLIST
|
||||
}
|
||||
|
||||
APP="$(ls -d "$CLIENT"/build/macos/Build/Products/Release/*.app 2>/dev/null | head -1)"
|
||||
[ -n "$APP" ] || { echo "✗ 未找到构建产物 .app"; exit 1; }
|
||||
EXE="$APP/Contents/MacOS/$(basename "$APP" .app)"
|
||||
cs(){ codesign --force --options runtime --timestamp "$@"; }
|
||||
|
||||
# ── 3. 前台运行(日志直出)────────────────────────────────────────────────────
|
||||
echo "==> [3/3] 运行 release 包(前台,日志直出;Ctrl+C 退出)"
|
||||
echo " app : $APP"
|
||||
echo " 内核: 客户端会解析到 $SINGBOX(已加进默认查找路径)"
|
||||
echo " 连上后另开终端验证出口: curl https://api.ipify.org → 期望 ${API_URL#http://}"
|
||||
echo "------------------------------------------------------------------"
|
||||
exec "$EXE"
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user