From dd721fafc85f08b145cafba4c700f262cccfdaeb Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Wed, 22 Jul 2026 11:59:08 +0800 Subject: [PATCH] =?UTF-8?q?fix(scripts):=20local=5Ftest.sh=20ipad=20?= =?UTF-8?q?=E6=94=B9=E8=B5=B0=E5=85=AC=E5=8F=B8=E5=88=86=E5=8F=91=E8=AF=81?= =?UTF-8?q?=E4=B9=A6(ad-hoc),=E8=A3=85=E6=9C=BA=E5=89=8D=E6=A0=B8=E9=AA=8C?= =?UTF-8?q?=E7=AD=BE=E5=90=8D=E4=B8=BB=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原 `ipad` 子命令用 `flutter run --release` 装真机,走的是 Xcode 工程的**开发** 签名。Apple 只把 Development 证书签发给团队里的**个人成员**(不存在组织版), 于是装到机上「设置→通用→VPN 与设备管理」里开发者显示成个人姓名,而不是 Yanmei (beijing) Technology Co., Ltd。 改为 ad-hoc 分发(Xcode 15+ 的 method=release-testing)+ 自动签名: - 新增 write_ios_export_options() 生成 ExportOptions.plist(teamID=BYL4KQHMTN); - `flutter build ipa --release --export-options-plist=…` 导出时由 Xcode 用 **Apple Distribution: Yanmei…** 重签; - 装机前解包 IPA 逐字比对签名主体(IOS_EXPECT_SIGNER),不符即中止 —— 防止 以后无声退回个人证书; - 用 devicectl 安装;仅当覆盖安装失败(旧包签名主体不同)才卸载重装,不无条件 先卸(卸载会清空 app 数据要重新登录); - 构建失败时提示新设备注册命令:光给 -allowProvisioningUpdates 不够,必须同时 给 -allowProvisioningDeviceRegistration。 另外两处顺手修: - 取 IPA 路径原写 `ls … | head -1`,在 set -o pipefail 下 head 提前退出会让 ls 吃 SIGPIPE、管道返回 141,被 set -e 当失败终止脚本(实测复现,退出码 141)。 改用数组 glob。签名主体提取的 `codesign | grep -m1` 同一个坑,改成整段捕获 后用 awk 读完全部输入。 - API_URL 支持 env 覆盖(默认值不变),便于装生产地址的包做验证。 验证:bash -n 通过;shellcheck 无新增告警; API_URL=https://api.yanmeiai.com bash scripts/local_test.sh ipad 无字天书 端到端 exit=0,日志确认"签名核验通过: Apple Distribution: Yanmei…"并装机成功。 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01P9G7E3wmAYL9KeYCVZVsqu --- scripts/local_test.sh | 151 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 3 deletions(-) diff --git a/scripts/local_test.sh b/scripts/local_test.sh index 78e3d2e..3f3040a 100755 --- a/scripts/local_test.sh +++ b/scripts/local_test.sh @@ -12,11 +12,20 @@ # run 直跑 /Applications 版本(绕 Gatekeeper,实时输出日志,并打印测试账号) # all 依次执行 build → sign → copy → run # -# 用法: scripts/local_test.sh all | scripts/local_test.sh build ... +# ── iOS / iPad 真机(USB 连接;与上面 macOS 流程完全独立)── +# ios-devices 列出已连接的 iOS 物理设备(拿 device id) +# ipad [id|名字] iOS/iPad 真机装机:ad-hoc 导出(**公司**分发证书 Apple +# Distribution: Yanmei…)→ 核验签名主体 → devicectl 装。 +# 仅一台 iOS 设备时可省参数;iPhone/iPad 同连时须指定 id 或名字子串。 +# 注:不用 `flutter run --release` —— 那走开发签名,是**个人**证书。 +# +# 用法: scripts/local_test.sh all | scripts/local_test.sh ipad ... set -euo pipefail # ─────────── 配置(按需改)─────────── -API_URL="http://103.119.13.48:8080" # 联调控制面;发版改这里或走默认 +# 联调控制面。默认指向联调节点;要装生产地址的包用 env 覆盖: +# API_URL=https://api.yanmeiai.com scripts/local_test.sh ipad +API_URL="${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 的描述文件名 @@ -29,6 +38,12 @@ IP_SVC="https://api.ipify.org" # 返回纯文本公网 IP # 注:DevID profile 已含 system-extension.install + NE(-systemextension 变体); # app/sysext entitlements 与之对齐(见 write_entitlements)。 +# ── iOS/iPad 真机分发(ad-hoc,**公司**证书)──────────────────────────────── +TEAM_ID="BYL4KQHMTN" # Yanmei (beijing) Technology Co., Ltd +IOS_BUNDLE_ID="com.pangolin.pangolinVpn" +# 期望的签名主体。装机前逐字核对,不符即中止 —— 防止悄悄退回个人开发证书。 +IOS_EXPECT_SIGNER="Apple Distribution: Yanmei (beijing) Technology Co., Ltd (${TEAM_ID})" + # ─────────── 路径推导 ─────────── SRC="${BASH_SOURCE[0]}" DIR="${SRC%/*}"; [ "$DIR" = "$SRC" ] && DIR="." @@ -235,6 +250,134 @@ cmd_ks_status(){ 2>/dev/null | tail -20 || echo " (无,确认已连接过一次)" } +# ─────────── iOS / iPad 真机安装 ─────────── +# 与 macOS 流程独立:iOS 用 Network Extension(非 System Extension),不需要 Developer ID +# 重签 / 公证;flutter build ios --release 由 Xcode 工程配置签名(Team=BYL4KQHMTN,自动 +# 签名会把已连接设备注册进描述文件),再 flutter install 走 USB 直接装。 + +# 列出已连接的物理 iOS 设备(输出 "idname" 每行一台;模拟器/无线设备排除)。 +list_ios_devices(){ + flutter devices --machine 2>/dev/null | python3 -c ' +import sys, json +try: + devs = json.load(sys.stdin) +except Exception: + devs = [] +for d in devs: + if str(d.get("targetPlatform", "")).startswith("ios") and not d.get("emulator", False): + print((d.get("id", "") or "") + "\t" + (d.get("name", "") or "")) +' +} + +cmd_ios_devices(){ + log "已连接的 iOS 物理设备" + local list; list="$(list_ios_devices)" + [ -n "$list" ] || die "没检测到 iOS 设备。用数据线连上 iPad、解锁并在弹窗点「信任此电脑」后重试。" + printf '%s\n' "$list" | while IFS=$'\t' read -r id name; do printf ' %-42s %s\n' "$id" "$name"; done +} + +# 解析目标设备到全局 DEVICE_ID:优先用传入的 id/名字子串;否则仅一台 iOS 设备时自动选它, +# 多台(如 iPhone+iPad 同连)则列出并要求显式指定,绝不瞎猜。 +resolve_ios_device(){ + local want="${1:-}" list + list="$(list_ios_devices)" + [ -n "$list" ] || die "没检测到 iOS 设备。用数据线连上 iPad、解锁并在弹窗点「信任此电脑」后重试。" + if [ -n "$want" ]; then + DEVICE_ID="$(printf '%s\n' "$list" | grep -iF "$want" | head -1 | cut -f1)" + [ -n "$DEVICE_ID" ] || die "找不到匹配「$want」的 iOS 设备。先跑 ios-devices 看可用列表。" + return 0 + fi + local n; n="$(printf '%s\n' "$list" | grep -c . || true)" + if [ "$n" = 1 ]; then + DEVICE_ID="$(printf '%s\n' "$list" | head -1 | cut -f1)" + else + printf '%s\n' "$list" | while IFS=$'\t' read -r id name; do printf ' %-42s %s\n' "$id" "$name" >&2; done + die "检测到多台 iOS 设备(如上)。请指定:scripts/local_test.sh ipad <设备id或名字子串>" + fi +} + +# ad-hoc 导出选项。**不要**改回 development/automatic: +# Apple 只把 Development 证书签发给团队里的**个人成员**(没有组织版),走开发签名 +# 装机后 iPad「设置→通用→VPN 与设备管理」里开发者会显示成个人姓名。要显示公司名 +# 必须用分发证书,而能装到自有真机的分发方式就是 ad-hoc(Xcode 15+ 叫 release-testing)。 +write_ios_export_options(){ + cat > "$WORK/ExportOptions-adhoc.plist" < + + + + methodrelease-testing + teamID${TEAM_ID} + signingStyleautomatic + stripSwiftSymbols + uploadSymbols + compileBitcode + + +PLIST +} + +cmd_ipad(){ + command -v flutter >/dev/null || die "找不到 flutter。" + local DEVICE_ID="" + resolve_ios_device "${1:-}" + write_ios_export_options + cd "$CLIENT" + echo "" + echo " 目标设备 : $DEVICE_ID" + echo " 签名主体 : $IOS_EXPECT_SIGNER" + echo " 联调 API : $API_URL" + echo " 测试账号 : $TEST_EMAIL / $TEST_PASSWORD" + echo " ad-hoc 分发签名无需在「VPN 与设备管理」信任;首次启动要联网让 iOS 校验证书。" + echo " VPN 配置在系统弹窗点「允许」。" + echo "" + + log "flutter build ipa --release(ad-hoc / 公司分发证书)" + # 新设备第一次装会因描述文件不含其 UDID 失败(0xe8008012)。自动签名要真正去 + # **注册设备**,光给 -allowProvisioningUpdates 不够,必须同时给 + # -allowProvisioningDeviceRegistration —— 由 Xcode 在 archive 阶段完成。 + flutter build ipa --release \ + --export-options-plist="$WORK/ExportOptions-adhoc.plist" \ + --dart-define="PANGOLIN_API_URL=$API_URL" \ + || die "IPA 构建失败。若报设备未注册,跑一次: + cd $CLIENT/ios && xcodebuild -workspace Runner.xcworkspace -scheme Runner \\ + -configuration Release -destination \"id=$DEVICE_ID\" \\ + -allowProvisioningUpdates -allowProvisioningDeviceRegistration build" + + # 用 glob 取,不要 `ls | head` —— set -o pipefail 下 head 先退出会让 ls 吃 SIGPIPE, + # 管道返回 141,再被 set -e 当失败直接终止脚本(实测踩过)。 + local IPA IPAS=() + IPAS=("$CLIENT"/build/ios/ipa/*.ipa) + IPA="${IPAS[0]}" + [ -f "$IPA" ] || die "没找到 IPA(build/ios/ipa/*.ipa)。" + + # ── 装机前核验签名主体,防止无声退回个人开发证书 ── + local VDIR="$WORK/ipa_verify"; rm -rf "$VDIR"; mkdir -p "$VDIR" + ( cd "$VDIR" && unzip -q "$IPA" ) || die "IPA 解包失败。" + # 同理避开提前退出的管道消费者(grep -m1 / head):先整段捕获,再在内存里取第一条 + # Authority(叶子证书)。awk 读完全部输入,不会给上游制造 SIGPIPE。 + local AUTH CS_OUT + CS_OUT="$(codesign -dv --verbose=4 "$VDIR/Payload/Runner.app" 2>&1 || true)" + AUTH="$(printf '%s\n' "$CS_OUT" | awk -F'Authority=' '/^Authority=/ && !seen {print $2; seen=1}')" + [ "$AUTH" = "$IOS_EXPECT_SIGNER" ] \ + || die "签名主体不符,拒绝安装。 + 期望: $IOS_EXPECT_SIGNER + 实际: ${AUTH:-<空>}" + log "签名核验通过:$AUTH" + + log "装到设备 $DEVICE_ID" + # 同 bundle id 的旧包若是**别的签名主体**(如早先的 Apple Development 个人证书), + # iOS 拒绝覆盖安装。仅在这种情况下才卸载重装 —— 卸载会清空 app 数据(需重新登录), + # 所以不无条件先卸。 + if ! xcrun devicectl device install app --device "$DEVICE_ID" "$IPA"; then + log "覆盖安装失败(多半是旧包签名主体不同),卸载旧包后重装 —— app 数据会清空,需重新登录" + xcrun devicectl device uninstall app --device "$DEVICE_ID" "$IOS_BUNDLE_ID" || true + xcrun devicectl device install app --device "$DEVICE_ID" "$IPA" \ + || die "安装失败。" + fi + log "已安装 $IOS_BUNDLE_ID → $DEVICE_ID;在设备上点开「穿山甲」即可(首次需联网校验证书)" +} + case "${1:-}" in build) cmd_build ;; sign) cmd_sign ;; # 旧:手动 Developer ID 重签(现已由 Xcode 工程配置直接签,通常不需要) @@ -246,5 +389,7 @@ case "${1:-}" in ks-baseline) cmd_ks_baseline ;; # ① VPN 断开下记录真实公网 IP ks-test) cmd_ks_test ;; # ② VPN 连接(killswitch 开)下杀扩展做漏测 ks-status) cmd_ks_status ;; # 看 VPN 状态 + killswitch 配置打点日志 - *) echo "用法: $0 {all|build|sign|notarize|copy|run|ks-baseline|ks-test|ks-status}"; exit 2 ;; + ios-devices) cmd_ios_devices ;; # 列出已连接的 iOS 物理设备(拿 device id) + ipad|ios) cmd_ipad "${2:-}" ;; # 构建 iOS release 并装到 iPad(可传设备 id/名字子串) + *) echo "用法: $0 {all|build|sign|notarize|copy|run|ks-baseline|ks-test|ks-status|ios-devices|ipad [id]}"; exit 2 ;; esac