11ca7531a3
Deploy Client / build-android (push) Successful in 1m46s
Deploy Client / build-windows (push) Successful in 1m55s
Deploy Client / build-macos (push) Successful in 3m40s
Deploy Client / build-ios (push) Successful in 3m28s
Deploy Client / release-deploy (push) Successful in 2m14s
iOS build 曾因 flutter build 内隐式 pub get 拉 google_fonts 时 pub.flutter-io.cn socket error(exit 69)而失败。_env.sh 加 flutter_pub_get_retry(5 次重试),四个 compile 脚本在 flutter build 前显式预取,成功后 build 命中缓存不再拉网,减少偶发 网络失败。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
273 lines
15 KiB
Bash
Executable File
273 lines
15 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# compile-macos.sh <tag> — build the Flutter macOS app (with its embedded
|
||
# PacketTunnel System Extension), Developer ID sign + notarize + staple,
|
||
# package into dist/.
|
||
#
|
||
# Mirrors ~/code/jiu/scripts/ci/compile-macos.sh's shape (temp-keychain
|
||
# Developer ID import, inside-out codesign, notarytool submit --wait, staple,
|
||
# ditto zip), but pangolin's macOS app is heavier than jiu's plain window:
|
||
# - it embeds a System Extension (com.pangolin.pangolin.PacketTunnel,
|
||
# `.systemextension` bundle under Contents/Library/SystemExtensions/)
|
||
# that needs ITS OWN Developer ID provisioning profile + entitlements,
|
||
# signed separately, inside-out, before the outer app is signed — see
|
||
# CLAUDE.md "client/ macOS 原生隧道" and
|
||
# docs/macos-sysext-realize-troubleshooting.html for why every one of
|
||
# these steps is load-bearing (wrong order / missing entitlement /
|
||
# unsigned nested item => sysextd silently refuses to load it).
|
||
# - the Xcode project (client/macos/Runner.xcodeproj) is ALREADY configured
|
||
# CODE_SIGN_STYLE=Manual with CODE_SIGN_IDENTITY="Developer ID
|
||
# Application" + PROVISIONING_PROFILE_SPECIFIER set per target (unlike
|
||
# jiu, whose Release config has CODE_SIGN_IDENTITY="-", i.e. unsigned at
|
||
# build time, signed entirely by hand afterward). So `flutter build macos
|
||
# --release` here should already produce a signed .app *if* the identity
|
||
# is in a searched keychain and the profiles are in place. This script
|
||
# still re-runs the explicit inside-out codesign pass below (mirroring
|
||
# scripts/local_test.sh's now-legacy `cmd_sign`) as a defensive,
|
||
# idempotent safety net — resigning with the same identity is harmless,
|
||
# and it removes any dependency on Xcode's automatic nested-item signing
|
||
# behaving identically on a CI runner vs. the maintainer's dev machine.
|
||
# - CFBundleVersion for the PacketTunnel extension is NOT derived from
|
||
# FLUTTER_BUILD_NUMBER (unlike the main Runner target, and unlike iOS
|
||
# where every target uses "$(FLUTTER_BUILD_NUMBER)") — it's a literal
|
||
# CURRENT_PROJECT_VERSION build setting hardcoded in project.pbxproj
|
||
# (currently 53, see CLAUDE.md: "每次构建必递增 CFBundleVersion
|
||
# (CURRENT_PROJECT_VERSION)——否则 sysextd 视为同版本不更新"). This script
|
||
# computes a monotonic build number from the tag version (same
|
||
# major*10000+minor*100+patch formula as compile-android.sh /
|
||
# compile-ios.sh) and sed's every CURRENT_PROJECT_VERSION occurrence in
|
||
# the pbxproj (RunnerTests + PacketTunnel Debug/Release/Profile, 6 total)
|
||
# before building. This edit is NOT committed back to git — each CI run
|
||
# starts from the repo's checked-in value and recomputes deterministically
|
||
# from the tag, so it stays monotonic across releases as long as the
|
||
# version itself increases.
|
||
#
|
||
# Required CI secrets (fail-fast — ANY missing => abort, never ship unsigned):
|
||
# MACOS_DEVELOPER_ID_CERT_P12_BASE64 Developer ID Application 证书(.p12)base64
|
||
# MACOS_DEVELOPER_ID_CERT_PASSWORD .p12 导出密码
|
||
# MACOS_APP_PROVISION_PROFILE_BASE64 "Pangolin App DevID" 描述文件 base64
|
||
# MACOS_SYSEXT_PROVISION_PROFILE_BASE64 "Pangolin PacketTunnel DevID" 描述文件 base64
|
||
# APPSTORE_API_KEY_ID / APPSTORE_API_ISSUER_ID / APPSTORE_API_KEY_P8_BASE64
|
||
# App Store Connect API Key(公证用,notarytool --key/--key-id/--issuer;
|
||
# 不同于 scripts/local_test.sh 本机用的 --keychain-profile pangolin-notary,
|
||
# CI 无法预置 keychain profile,必须走显式 API key 三件套)。
|
||
#
|
||
# Output: dist/pangolin-macos-x64.zip (stable name — deploy-client.sh /
|
||
# release-client.sh key off this exact filename).
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# shellcheck source=scripts/ci/_env.sh
|
||
. "${SCRIPT_DIR}/_env.sh"
|
||
|
||
TAG="${1:?usage: compile-macos.sh <tag>}"
|
||
|
||
ver_file="/tmp/compile_macos_ver.$$"
|
||
ver_from_tag client "$TAG" > "$ver_file"
|
||
VER=""
|
||
read -r VER < "$ver_file" || true # 无结尾换行的 read 退出码,见 compile-android.sh 同注释
|
||
rm -f "$ver_file"
|
||
|
||
# 单调递增 build 号:major*10000 + minor*100 + patch(同 compile-android.sh /
|
||
# compile-ios.sh 公式)。纯参数展开,不用 cut/$();剥掉 patch 段的 `-suffix`
|
||
# (如预发布 tag "48-rc1")以保证后面的算术展开是纯数字。
|
||
MAJOR="${VER%%.*}"
|
||
_REST="${VER#*.}"
|
||
MINOR="${_REST%%.*}"
|
||
PATCH="${_REST#*.}"
|
||
PATCH="${PATCH%%-*}"
|
||
BUILD=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
|
||
|
||
echo "==> compile-macos: tag=${TAG} version=${VER} build=${BUILD}"
|
||
|
||
# ── [0/7] fail-fast:签名/公证所需 secret 一个不少 ──────────────────────────
|
||
# 比 jiu 的 mac 脚本(只查 2 个)更严格:任一缺失都直接报错中止,不产出未签名/
|
||
# 未公证包。用 `${VAR:?msg}` 而非 if-empty,出错信息更精确定位到具体哪个 secret。
|
||
: "${MACOS_DEVELOPER_ID_CERT_P12_BASE64:?缺少 MACOS_DEVELOPER_ID_CERT_P12_BASE64:未配置 Developer ID 证书,构建中止(不产出未签名包)}"
|
||
: "${MACOS_DEVELOPER_ID_CERT_PASSWORD:?缺少 MACOS_DEVELOPER_ID_CERT_PASSWORD}"
|
||
: "${MACOS_APP_PROVISION_PROFILE_BASE64:?缺少 MACOS_APP_PROVISION_PROFILE_BASE64('Pangolin App DevID' 描述文件)}"
|
||
: "${MACOS_SYSEXT_PROVISION_PROFILE_BASE64:?缺少 MACOS_SYSEXT_PROVISION_PROFILE_BASE64('Pangolin PacketTunnel DevID' 描述文件)}"
|
||
: "${APPSTORE_API_KEY_ID:?缺少 APPSTORE_API_KEY_ID(公证用 App Store Connect API Key)}"
|
||
: "${APPSTORE_API_ISSUER_ID:?缺少 APPSTORE_API_ISSUER_ID}"
|
||
: "${APPSTORE_API_KEY_P8_BASE64:?缺少 APPSTORE_API_KEY_P8_BASE64}"
|
||
|
||
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||
API_URL="${PANGOLIN_API_URL:-https://api.yanmeiai.com}"
|
||
SIGN_ID_PREFIX="Developer ID Application"
|
||
TEAM_ID="BYL4KQHMTN"
|
||
APP_BUNDLE_ID="com.pangolin.pangolin"
|
||
SYSEXT_BUNDLE_ID="com.pangolin.pangolin.PacketTunnel"
|
||
APP_GROUP="${TEAM_ID}.com.pangolin.pangolin"
|
||
|
||
WORK="$(mktemp -d)"
|
||
KEYCHAIN="${WORK}/pangolin-mac-ci.keychain-db"
|
||
# 与 scripts/local_test.sh 用的描述文件目录保持一致(Xcode UserData 目录,
|
||
# 而非经典的 ~/Library/MobileDevice/Provisioning Profiles/)——CI 与本机联调
|
||
# 复用同一处,两边的 "Pangolin App DevID"/"Pangolin PacketTunnel DevID" 描述
|
||
# 文件本就应该是同一份,CI 覆盖写入是幂等的。故清理阶段【不删除】这两个描述
|
||
# 文件(只清理临时 keychain/证书),避免影响维护者在同一台 mac runner 上继续
|
||
# 用 local_test.sh 手动联调。
|
||
PROF_DIR="${HOME}/Library/Developer/Xcode/UserData/Provisioning Profiles"
|
||
mkdir -p "$PROF_DIR"
|
||
|
||
cleanup_mac() {
|
||
security delete-keychain "$KEYCHAIN" 2>/dev/null || true
|
||
# 把 keychain 搜索列表还原成默认单项,不把临时 keychain 的痕迹留在这台常驻
|
||
# runner 上(mac-pangolin-2 同时也被人手动用来跑 local_test.sh)。
|
||
security list-keychains -d user -s login.keychain-db 2>/dev/null || true
|
||
rm -rf "$WORK"
|
||
}
|
||
trap cleanup_mac EXIT
|
||
|
||
# ── [1/7] 重建 libbox.xcframework(gitignore 产物,每次换 worktree/CI 都要重建)─
|
||
echo "==> compile-macos: building Libbox.xcframework (apple macos)"
|
||
bash "${REPO_ROOT}/scripts/build-libbox.sh" apple macos
|
||
LIBBOX_FW="${REPO_ROOT}/client/macos/Frameworks/Libbox.xcframework"
|
||
if [ ! -d "$LIBBOX_FW" ]; then
|
||
echo "ERROR: ${LIBBOX_FW} not found after build-libbox.sh — aborting." >&2
|
||
exit 1
|
||
fi
|
||
|
||
# ── [2/7] 递增 CURRENT_PROJECT_VERSION(sysext CFBundleVersion,见头部说明)──
|
||
PBXPROJ="${REPO_ROOT}/client/macos/Runner.xcodeproj/project.pbxproj"
|
||
echo "==> compile-macos: bumping CURRENT_PROJECT_VERSION -> ${BUILD} in project.pbxproj"
|
||
sed -i '' -E "s/CURRENT_PROJECT_VERSION = [0-9]+;/CURRENT_PROJECT_VERSION = ${BUILD};/g" "$PBXPROJ"
|
||
# 同步 pubspec.yaml(主 app 自身的 FLUTTER_BUILD_NUMBER,和 android/windows 脚本一致)。
|
||
sed -i '' "s/^version:.*/version: ${VER}+${BUILD}/" "${REPO_ROOT}/client/pubspec.yaml"
|
||
|
||
# ── [3/7] 解码 2 个 Developer ID 描述文件到 local_test.sh 同款目录 ──────────
|
||
printf '%s' "$MACOS_APP_PROVISION_PROFILE_BASE64" | base64 --decode > "${WORK}/app.provisionprofile"
|
||
printf '%s' "$MACOS_SYSEXT_PROVISION_PROFILE_BASE64" | base64 --decode > "${WORK}/sysext.provisionprofile"
|
||
|
||
# 提取 UUID 用来落盘为规范文件名(Xcode/xcodebuild 靠内容匹配,文件名本身不
|
||
# 强制要求,但用 UUID 命名是 Xcode 自身导入时的惯例,避免与其他描述文件重名冲突)。
|
||
security cms -D -i "${WORK}/app.provisionprofile" > "${WORK}/app_profile.plist"
|
||
/usr/libexec/PlistBuddy -c 'Print :UUID' "${WORK}/app_profile.plist" > "${WORK}/app_uuid.txt"
|
||
APP_PROFILE_UUID=""
|
||
read -r APP_PROFILE_UUID < "${WORK}/app_uuid.txt" || true
|
||
APP_PROFILE="${PROF_DIR}/${APP_PROFILE_UUID}.provisionprofile"
|
||
cp "${WORK}/app.provisionprofile" "$APP_PROFILE"
|
||
|
||
security cms -D -i "${WORK}/sysext.provisionprofile" > "${WORK}/sysext_profile.plist"
|
||
/usr/libexec/PlistBuddy -c 'Print :UUID' "${WORK}/sysext_profile.plist" > "${WORK}/sysext_uuid.txt"
|
||
SYSEXT_PROFILE_UUID=""
|
||
read -r SYSEXT_PROFILE_UUID < "${WORK}/sysext_uuid.txt" || true
|
||
SYSEXT_PROFILE="${PROF_DIR}/${SYSEXT_PROFILE_UUID}.provisionprofile"
|
||
cp "${WORK}/sysext.provisionprofile" "$SYSEXT_PROFILE"
|
||
|
||
echo "==> compile-macos: profiles installed to '${PROF_DIR}' (app=${APP_PROFILE_UUID}, sysext=${SYSEXT_PROFILE_UUID})"
|
||
|
||
# ── [4/7] 临时 keychain 导入 Developer ID Application 证书 ──────────────────
|
||
KEYCHAIN_PWD="ci-temp-$$"
|
||
security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN"
|
||
security set-keychain-settings -lut 21600 "$KEYCHAIN"
|
||
security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN"
|
||
printf '%s' "$MACOS_DEVELOPER_ID_CERT_P12_BASE64" | base64 --decode > "${WORK}/devid.p12"
|
||
security import "${WORK}/devid.p12" -k "$KEYCHAIN" \
|
||
-P "$MACOS_DEVELOPER_ID_CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
|
||
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PWD" "$KEYCHAIN" >/dev/null
|
||
security list-keychains -d user -s "$KEYCHAIN" login.keychain-db
|
||
|
||
security find-identity -v -p codesigning "$KEYCHAIN" > "${WORK}/identities.txt"
|
||
grep "$SIGN_ID_PREFIX" "${WORK}/identities.txt" | head -1 | sed -E 's/.*"(.*)"/\1/' > "${WORK}/identity.txt"
|
||
IDENTITY=""
|
||
read -r IDENTITY < "${WORK}/identity.txt" || true
|
||
if [ -z "$IDENTITY" ]; then
|
||
echo "ERROR: '${SIGN_ID_PREFIX}' identity not found in imported keychain" >&2
|
||
exit 1
|
||
fi
|
||
echo "==> compile-macos: signing identity '${IDENTITY}'"
|
||
|
||
# ── [5/7] flutter build macos --release ─────────────────────────────────────
|
||
# 项目已配 CODE_SIGN_STYLE=Manual + PROVISIONING_PROFILE_SPECIFIER(见头部说
|
||
# 明),本步应已产出 Developer ID 签名 + hardened runtime 的 .app;下一步的
|
||
# inside-out 重签是幂等的安全网,不依赖这一步是否已经"恰好签对"。
|
||
cd "${REPO_ROOT}/client"
|
||
flutter_pub_get_retry # 预取包 + 重试,防 pub.flutter-io.cn 被 GFW 抖断(见 _env.sh)
|
||
flutter build macos --release --dart-define="PANGOLIN_API_URL=${API_URL}"
|
||
cd "$REPO_ROOT"
|
||
|
||
APP="${REPO_ROOT}/client/build/macos/Build/Products/Release/pangolin_vpn.app"
|
||
SE="${APP}/Contents/Library/SystemExtensions/${SYSEXT_BUNDLE_ID}.systemextension"
|
||
[ -d "$APP" ] || { echo "ERROR: build product not found: ${APP}" >&2; exit 1; }
|
||
[ -d "$SE" ] || { echo "ERROR: sysext bundle not found: ${SE}" >&2; exit 1; }
|
||
|
||
# ── [6/7] inside-out 重签(sysext → app frameworks → app),同 local_test.sh cmd_sign ──
|
||
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>${TEAM_ID}.${APP_BUNDLE_ID}</string>
|
||
<key>com.apple.developer.team-identifier</key><string>${TEAM_ID}</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>${APP_GROUP}</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>${TEAM_ID}.${SYSEXT_BUNDLE_ID}</string>
|
||
<key>com.apple.developer.team-identifier</key><string>${TEAM_ID}</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
|
||
|
||
echo "==> compile-macos: inside-out codesign (sysext -> frameworks -> app)"
|
||
xattr -cr "$APP"
|
||
# 从 WORK 临时目录复制(不是 PROF_DIR):flutter build macos 期间 xcodebuild 会
|
||
# 修剪 ~/Library/Developer/Xcode/UserData/Provisioning Profiles,把[3/7]构建前
|
||
# 装进去的 profile 清掉,构建后再从那目录 cp 会扑空(No such file → 本步失败)。
|
||
# WORK 是 mktemp 目录、xcodebuild 不碰,里面的原始 .provisionprofile 全程留存。
|
||
cp "${WORK}/sysext.provisionprofile" "${SE}/Contents/embedded.provisionprofile"
|
||
cp "${WORK}/app.provisionprofile" "${APP}/Contents/embedded.provisionprofile"
|
||
|
||
# sysext first — libbox is linked (not embedded) into the sysext binary
|
||
# itself (see CLAUDE.md), so signing the sysext bundle re-covers its statically
|
||
# linked libbox; no separate Libbox.framework to sign inside it.
|
||
codesign --force --options runtime --timestamp \
|
||
--entitlements "${WORK}/sysext.entitlements" --sign "$IDENTITY" "$SE"
|
||
|
||
if [ -d "${APP}/Contents/Frameworks" ]; then
|
||
for item in "${APP}/Contents/Frameworks/"*; do
|
||
[ -e "$item" ] && codesign --force --options runtime --timestamp --sign "$IDENTITY" "$item"
|
||
done
|
||
fi
|
||
|
||
codesign --force --options runtime --timestamp \
|
||
--entitlements "${WORK}/app.entitlements" --sign "$IDENTITY" "$APP"
|
||
|
||
codesign --verify --deep --strict --verbose=2 "$APP"
|
||
echo "==> compile-macos: signed + verified (${IDENTITY})"
|
||
|
||
# ── [7/7] 公证(notarytool,API key 三件套) + staple + 打包 ──────────────────
|
||
echo "==> compile-macos: notarizing (notarytool submit --wait, ~1-5min)"
|
||
NOTARIZE_ZIP="${WORK}/notarize.zip"
|
||
ditto -c -k --keepParent "$APP" "$NOTARIZE_ZIP"
|
||
printf '%s' "$APPSTORE_API_KEY_P8_BASE64" | base64 --decode > "${WORK}/AuthKey.p8"
|
||
xcrun notarytool submit "$NOTARIZE_ZIP" \
|
||
--key "${WORK}/AuthKey.p8" \
|
||
--key-id "$APPSTORE_API_KEY_ID" \
|
||
--issuer "$APPSTORE_API_ISSUER_ID" \
|
||
--wait
|
||
|
||
xcrun stapler staple "$APP"
|
||
xcrun stapler validate "$APP"
|
||
spctl -a -vvv -t install "$APP" || true # 期望 source=Notarized Developer ID
|
||
echo "==> compile-macos: notarized + stapled"
|
||
|
||
mkdir -p "${REPO_ROOT}/dist"
|
||
ditto -c -k --keepParent "$APP" "${REPO_ROOT}/dist/pangolin-macos-x64.zip"
|
||
|
||
echo "==> compile-macos: done — dist/ contents:"
|
||
ls -lh "${REPO_ROOT}/dist/"
|