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
200 lines
9.8 KiB
Bash
Executable File
200 lines
9.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# compile-ios.sh <tag> — build a signed iOS IPA (app + PacketTunnel Network
|
|
# Extension) and upload it to TestFlight (App Store Connect).
|
|
#
|
|
# Mirrors ~/code/jiu/scripts/ci/compile-ios.sh's shape (temp-keychain dist
|
|
# cert import, provisioning-profile install by UUID, ExportOptions.plist,
|
|
# `flutter build ipa`, `xcrun altool --upload-app`), adapted for pangolin:
|
|
# - pangolin's iOS app ships a NEPacketTunnelProvider **app extension**
|
|
# (client/ios/PacketTunnel/, bundle com.pangolin.pangolinVpn.PacketTunnel)
|
|
# alongside the main app (com.pangolin.pangolinVpn) — jiu has no
|
|
# extension at all, just the one app target. So this script needs TWO
|
|
# distribution provisioning profiles (app + extension), not one, and the
|
|
# ExportOptions.plist provisioningProfiles dict needs both bundle-id ->
|
|
# profile-name mappings.
|
|
# - pangolin embeds sing-box as a native core (gomobile-built
|
|
# Libbox.xcframework, io.nekohasekai.libbox) — jiu's Flutter app has no
|
|
# embedded core. This script MUST run `scripts/build-libbox.sh apple ios`
|
|
# before `flutter build ipa`, or the IPA links no VPN kernel (mirrors the
|
|
# equivalent step in compile-android.sh / compile-macos.sh).
|
|
# - Team ID BYL4KQHMTN is hardcoded as a script constant rather than a
|
|
# secret (unlike jiu's IOS_TEAM_ID secret): it's already public inside
|
|
# this repo (CLAUDE.md, client/ios/Runner.xcodeproj/project.pbxproj
|
|
# DEVELOPMENT_TEAM, scripts/local_test.sh SIGN_ID) — not sensitive, no
|
|
# reason to add another secret for it.
|
|
# - APPSTORE_API_KEY_ID / APPSTORE_API_ISSUER_ID / APPSTORE_API_KEY_P8_BASE64
|
|
# are shared with compile-macos.sh's notarization step (same App Store
|
|
# Connect API key serves both notarization and TestFlight upload).
|
|
#
|
|
# Required CI secrets (missing ANY of them => SKIP gracefully, exit 0 — does
|
|
# NOT fail the pipeline; Apple iOS distribution needs an enrolled account +
|
|
# certs that may not exist yet, unlike macOS Developer ID which is a hard
|
|
# fail-fast in compile-macos.sh):
|
|
# IOS_DIST_CERT_P12_BASE64 Apple Distribution 证书(.p12)base64
|
|
# IOS_DIST_CERT_PASSWORD .p12 导出密码
|
|
# IOS_APP_PROVISIONING_PROFILE_BASE64 App Store 类型描述文件(主 app,
|
|
# com.pangolin.pangolinVpn)base64
|
|
# IOS_PACKETTUNNEL_PROVISIONING_PROFILE_BASE64 App Store 类型描述文件(扩展,
|
|
# com.pangolin.pangolinVpn.PacketTunnel)base64
|
|
# APPSTORE_API_KEY_ID / APPSTORE_API_ISSUER_ID / APPSTORE_API_KEY_P8_BASE64
|
|
# App Store Connect API Key(上传 TestFlight 用;与 compile-macos.sh 公证共用)。
|
|
#
|
|
# No dist/ artifact is produced (matches jiu) — the IPA goes straight to
|
|
# TestFlight via altool, nothing is uploaded to the Forgejo release.
|
|
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-ios.sh <tag>}"
|
|
|
|
ver_file="/tmp/compile_ios_ver.$$"
|
|
ver_from_tag client "$TAG" > "$ver_file"
|
|
VER=""
|
|
read -r VER < "$ver_file" || true # 无结尾换行的 read 退出码,见 compile-android.sh 同注释
|
|
rm -f "$ver_file"
|
|
|
|
# CFBundleVersion(两个 target 均为 "$(FLUTTER_BUILD_NUMBER)",见
|
|
# client/ios/Runner.xcodeproj/project.pbxproj —— 与 macOS 的 PacketTunnel 硬编码
|
|
# CURRENT_PROJECT_VERSION 不同,这里走 flutter build --build-number 走标准路径)。
|
|
# 必须单调递增,否则 TestFlight 拒绝重复上传(同 jiu 注释),公式与
|
|
# compile-android.sh / compile-macos.sh 一致。
|
|
MAJOR="${VER%%.*}"
|
|
_REST="${VER#*.}"
|
|
MINOR="${_REST%%.*}"
|
|
PATCH="${_REST#*.}"
|
|
PATCH="${PATCH%%-*}"
|
|
BUILD=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
|
|
|
|
echo "==> compile-ios: tag=${TAG} version=${VER} build=${BUILD}"
|
|
|
|
# ── 凭证缺失则优雅跳过(不阻塞流水线;见头部说明)─────────────────────────
|
|
if [ -z "${IOS_DIST_CERT_P12_BASE64:-}" ] || \
|
|
[ -z "${IOS_DIST_CERT_PASSWORD:-}" ] || \
|
|
[ -z "${IOS_APP_PROVISIONING_PROFILE_BASE64:-}" ] || \
|
|
[ -z "${IOS_PACKETTUNNEL_PROVISIONING_PROFILE_BASE64:-}" ] || \
|
|
[ -z "${APPSTORE_API_KEY_ID:-}" ] || \
|
|
[ -z "${APPSTORE_API_ISSUER_ID:-}" ] || \
|
|
[ -z "${APPSTORE_API_KEY_P8_BASE64:-}" ]; then
|
|
echo "==> compile-ios: SKIP — iOS signing secrets not fully configured yet (not a failure)"
|
|
exit 0
|
|
fi
|
|
|
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
|
API_URL="${PANGOLIN_API_URL:-https://api.yanmeiai.com}"
|
|
TEAM_ID="BYL4KQHMTN"
|
|
APP_BUNDLE_ID="com.pangolin.pangolinVpn"
|
|
EXT_BUNDLE_ID="com.pangolin.pangolinVpn.PacketTunnel"
|
|
|
|
WORK="$(mktemp -d)"
|
|
KEYCHAIN="${WORK}/pangolin-ios-ci.keychain-db"
|
|
PROFILES_DIR="${HOME}/Library/MobileDevice/Provisioning Profiles"
|
|
mkdir -p "$PROFILES_DIR"
|
|
|
|
cleanup_ios() {
|
|
security delete-keychain "$KEYCHAIN" 2>/dev/null || true
|
|
security list-keychains -d user -s login.keychain-db 2>/dev/null || true
|
|
[ -n "${APP_PROFILE_UUID:-}" ] && rm -f "${PROFILES_DIR}/${APP_PROFILE_UUID}.mobileprovision" 2>/dev/null || true
|
|
[ -n "${EXT_PROFILE_UUID:-}" ] && rm -f "${PROFILES_DIR}/${EXT_PROFILE_UUID}.mobileprovision" 2>/dev/null || true
|
|
rm -rf "$WORK"
|
|
}
|
|
trap cleanup_ios EXIT
|
|
|
|
# ── [1/6] 重建 libbox.xcframework(apple ios;gitignore 产物,每次都要重建) ──
|
|
echo "==> compile-ios: building Libbox.xcframework (apple ios)"
|
|
bash "${REPO_ROOT}/scripts/build-libbox.sh" apple ios
|
|
LIBBOX_FW="${REPO_ROOT}/client/ios/Frameworks/Libbox.xcframework"
|
|
if [ ! -d "$LIBBOX_FW" ]; then
|
|
echo "ERROR: ${LIBBOX_FW} not found after build-libbox.sh — aborting." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ── [2/6] 临时 keychain 导入 Apple Distribution 证书 ─────────────────────────
|
|
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' "$IOS_DIST_CERT_P12_BASE64" | base64 --decode > "${WORK}/dist.p12"
|
|
security import "${WORK}/dist.p12" -k "$KEYCHAIN" \
|
|
-P "$IOS_DIST_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
|
|
|
|
# ── [3/6] 安装 2 个 App Store 类型描述文件(app + PacketTunnel 扩展) ─────────
|
|
printf '%s' "$IOS_APP_PROVISIONING_PROFILE_BASE64" | base64 --decode > "${WORK}/app.mobileprovision"
|
|
security cms -D -i "${WORK}/app.mobileprovision" > "${WORK}/app_profile.plist"
|
|
/usr/libexec/PlistBuddy -c 'Print :Name' "${WORK}/app_profile.plist" > "${WORK}/app_name.txt"
|
|
/usr/libexec/PlistBuddy -c 'Print :UUID' "${WORK}/app_profile.plist" > "${WORK}/app_uuid.txt"
|
|
APP_PROFILE_NAME=""
|
|
read -r APP_PROFILE_NAME < "${WORK}/app_name.txt" || true
|
|
APP_PROFILE_UUID=""
|
|
read -r APP_PROFILE_UUID < "${WORK}/app_uuid.txt" || true
|
|
cp "${WORK}/app.mobileprovision" "${PROFILES_DIR}/${APP_PROFILE_UUID}.mobileprovision"
|
|
echo "==> compile-ios: app profile '${APP_PROFILE_NAME}' (${APP_PROFILE_UUID})"
|
|
|
|
printf '%s' "$IOS_PACKETTUNNEL_PROVISIONING_PROFILE_BASE64" | base64 --decode > "${WORK}/ext.mobileprovision"
|
|
security cms -D -i "${WORK}/ext.mobileprovision" > "${WORK}/ext_profile.plist"
|
|
/usr/libexec/PlistBuddy -c 'Print :Name' "${WORK}/ext_profile.plist" > "${WORK}/ext_name.txt"
|
|
/usr/libexec/PlistBuddy -c 'Print :UUID' "${WORK}/ext_profile.plist" > "${WORK}/ext_uuid.txt"
|
|
EXT_PROFILE_NAME=""
|
|
read -r EXT_PROFILE_NAME < "${WORK}/ext_name.txt" || true
|
|
EXT_PROFILE_UUID=""
|
|
read -r EXT_PROFILE_UUID < "${WORK}/ext_uuid.txt" || true
|
|
cp "${WORK}/ext.mobileprovision" "${PROFILES_DIR}/${EXT_PROFILE_UUID}.mobileprovision"
|
|
echo "==> compile-ios: PacketTunnel profile '${EXT_PROFILE_NAME}' (${EXT_PROFILE_UUID})"
|
|
|
|
# ── [4/6] App Store Connect API Key(供 altool 上传使用)──────────────────────
|
|
API_KEYS_DIR="${HOME}/.appstoreconnect/private_keys"
|
|
mkdir -p "$API_KEYS_DIR"
|
|
printf '%s' "$APPSTORE_API_KEY_P8_BASE64" | base64 --decode > "${API_KEYS_DIR}/AuthKey_${APPSTORE_API_KEY_ID}.p8"
|
|
|
|
# ── [5/6] 同步版本号 + 生成 ExportOptions.plist(manual 签名,两个 bundle 都映射)──
|
|
sed -i '' "s/^version:.*/version: ${VER}+${BUILD}/" "${REPO_ROOT}/client/pubspec.yaml"
|
|
|
|
cat > "${WORK}/ExportOptions.plist" <<EOF
|
|
<?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>method</key><string>app-store</string>
|
|
<key>teamID</key><string>${TEAM_ID}</string>
|
|
<key>signingStyle</key><string>manual</string>
|
|
<key>uploadBitcode</key><false/>
|
|
<key>uploadSymbols</key><true/>
|
|
<key>provisioningProfiles</key>
|
|
<dict>
|
|
<key>${APP_BUNDLE_ID}</key><string>${APP_PROFILE_NAME}</string>
|
|
<key>${EXT_BUNDLE_ID}</key><string>${EXT_PROFILE_NAME}</string>
|
|
</dict>
|
|
</dict>
|
|
</plist>
|
|
EOF
|
|
|
|
# ── [6/6] flutter build ipa + 上传 TestFlight ───────────────────────────────
|
|
cd "${REPO_ROOT}/client"
|
|
flutter_pub_get_retry # 预取包 + 重试,防 pub.flutter-io.cn 被 GFW 抖断(见 _env.sh)
|
|
flutter build ipa --release \
|
|
--build-name="${VER}" \
|
|
--build-number="${BUILD}" \
|
|
--export-options-plist="${WORK}/ExportOptions.plist" \
|
|
"--dart-define=PANGOLIN_API_URL=${API_URL}"
|
|
cd "$REPO_ROOT"
|
|
|
|
IPA=""
|
|
for cand in "${REPO_ROOT}"/client/build/ios/ipa/*.ipa; do
|
|
if [ -f "$cand" ]; then IPA="$cand"; break; fi
|
|
done
|
|
if [ -z "$IPA" ]; then
|
|
echo "ERROR: IPA not found under client/build/ios/ipa/" >&2
|
|
exit 1
|
|
fi
|
|
echo "==> compile-ios: built ${IPA}"
|
|
|
|
echo "==> compile-ios: uploading to TestFlight"
|
|
xcrun altool --upload-app -f "$IPA" -t ios \
|
|
--apiKey "$APPSTORE_API_KEY_ID" \
|
|
--apiIssuer "$APPSTORE_API_ISSUER_ID"
|
|
|
|
echo "==> compile-ios: done — uploaded build ${BUILD} to TestFlight"
|