#!/bin/bash set -e # ╔═══════════════════════════════════════════════════════════════════════════╗ # ║ HiFastVPN macOS 签名和公证脚本 ║ # ║ ║ # ║ 使用方法: ║ # ║ 1. 修改下面的配置变量 ║ # ║ 2. chmod +x sign_and_notarize.sh ║ # ║ 3. ./sign_and_notarize.sh ║ # ╚═══════════════════════════════════════════════════════════════════════════╝ # ======================== 配置变量(请修改这里)======================== SIGNING_IDENTITY="Developer ID Application: TAW TRADERS SDN. BHD. (NJRRF427XB)" APPLE_ID="speakeloudest@gmail.com" APP_PASSWORD="lvry-umfn-pqgz-lnwk" # App-specific password TEAM_ID="NJRRF427XB" # 路径配置 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" APP_PATH="${PROJECT_DIR}/build/macos/Build/Products/Release/HiFastVPN.app" ENTITLEMENTS="${PROJECT_DIR}/macos/Runner/Release.entitlements" OUTPUT_DIR="${PROJECT_DIR}/dist" BACKGROUND_IMAGE="${SCRIPT_DIR}/assets/dmg_bg.png" # ===================================================================== echo "╔═══════════════════════════════════════════════════════════════════╗" echo "║ HiFastVPN macOS 签名和公证脚本 ║" echo "╚═══════════════════════════════════════════════════════════════════╝" echo "" # 提取版本号 PUBSPEC_PATH="${PROJECT_DIR}/pubspec.yaml" if [ ! -f "${PUBSPEC_PATH}" ]; then echo "❌ 错误: 找不到 pubspec.yaml" exit 1 fi # 提取完整的版本号(例如 1.0.0+103) VERSION=$(grep "^version:" "${PUBSPEC_PATH}" | sed 's/version: *//' | tr -d '[:space:]') if [ -z "${VERSION}" ]; then echo "❌ 错误: 无法提取版本号" exit 1 fi # 更新输出目录为带版本号的子目录 OUTPUT_DIR="${PROJECT_DIR}/dist/${VERSION}" mkdir -p "${OUTPUT_DIR}" echo "📦 版本号: ${VERSION}" echo "📁 项目目录: ${PROJECT_DIR}" echo "📦 应用路径: ${APP_PATH}" echo "📄 Entitlements: ${ENTITLEMENTS}" echo "🖼️ 背景图: ${BACKGROUND_IMAGE}" echo "" # 检查 .app 是否存在 if [ ! -d "${APP_PATH}" ]; then echo "❌ 错误: ${APP_PATH} 不存在" echo "请先运行: flutter build macos --release" exit 1 fi # 检查 entitlements 是否存在 if [ ! -f "${ENTITLEMENTS}" ]; then echo "❌ 错误: ${ENTITLEMENTS} 不存在" exit 1 fi # 检查 create-dmg 是否安装 if ! command -v create-dmg >/dev/null 2>&1; then echo "❌ 错误: create-dmg 未安装(brew install create-dmg)" exit 1 fi # 检查背景图是否存在 if [ ! -f "${BACKGROUND_IMAGE}" ]; then echo "❌ 错误: 背景图不存在" exit 1 fi # 校验背景图尺寸 BG_INFO=$(sips -g pixelWidth -g pixelHeight "${BACKGROUND_IMAGE}") BG_W=$(echo "$BG_INFO" | awk '/pixelWidth/ {print $2}') BG_H=$(echo "$BG_INFO" | awk '/pixelHeight/ {print $2}') if [ "$BG_W" != "1200" ] || [ "$BG_H" != "800" ]; then echo "❌ 背景图必须是 1200x800,当前是 ${BG_W}x${BG_H}" exit 1 fi # ======================== Step 1: 清理 ======================== echo "" echo "🧹 Step 1: 清理多余文件..." # 删除 .DS_Store find "${APP_PATH}" -name ".DS_Store" -delete 2>/dev/null || true find "${APP_PATH}" -name "*.bak" -delete 2>/dev/null || true # 删除 Headers 目录和符号链接 find "${APP_PATH}/Contents/Frameworks" -type l -name "Headers" -delete 2>/dev/null || true find "${APP_PATH}/Contents/Frameworks" -type d -name "Headers" -exec rm -rf {} + 2>/dev/null || true # 删除 Modules 目录 find "${APP_PATH}/Contents/Frameworks" -type d -name "Modules" -exec rm -rf {} + 2>/dev/null || true # 删除 PrivateHeaders find "${APP_PATH}/Contents/Frameworks" -name "PrivateHeaders" -exec rm -rf {} + 2>/dev/null || true echo " ✅ 清理完成" # ======================== Step 2: 移除现有签名 ======================== echo "" echo "🗑️ Step 2: 移除现有签名..." # 移除所有签名(忽略错误) find "${APP_PATH}" -name "*.dylib" -exec codesign --remove-signature {} \; 2>/dev/null || true find "${APP_PATH}" -name "*.framework" -exec codesign --remove-signature {} \; 2>/dev/null || true codesign --remove-signature "${APP_PATH}" 2>/dev/null || true echo " ✅ 移除签名完成" # ======================== Step 3: 签名 dylib 文件 ======================== echo "" echo "🔐 Step 3: 签名 dylib 文件..." find "${APP_PATH}" -name "*.dylib" -print0 | while IFS= read -r -d '' dylib; do echo " → $(basename "$dylib")" codesign --force --sign "${SIGNING_IDENTITY}" \ --options runtime \ --timestamp \ "$dylib" done echo " ✅ dylib 签名完成" # ======================== Step 4: 签名 Framework ======================== echo "" echo "🔐 Step 4: 签名 Framework..." # 签名所有 framework(使用 --deep 确保内部资源被正确处理) for fw in "${APP_PATH}"/Contents/Frameworks/*.framework; do if [ -d "$fw" ]; then fw_name=$(basename "$fw") echo " → ${fw_name}" # 使用 --deep 签名 framework,这样会自动处理内部的资源 codesign --force --sign "${SIGNING_IDENTITY}" \ --options runtime \ --timestamp \ "$fw" fi done echo " ✅ Framework 签名完成" # ======================== Step 5: 签名主应用 ======================== echo "" echo "🔐 Step 5: 签名主应用..." codesign --force --sign "${SIGNING_IDENTITY}" \ --options runtime \ --entitlements "${ENTITLEMENTS}" \ --timestamp \ "${APP_PATH}" echo " ✅ 主应用签名完成" # ======================== Step 6: 验证签名 ======================== echo "" echo "✅ Step 6: 验证签名..." if codesign --verify --deep --strict --verbose=2 "${APP_PATH}" 2>&1; then echo " ✅ 签名验证通过!" else echo " ❌ 签名验证失败!" echo "" echo "详细错误信息:" codesign --verify --deep --strict --verbose=4 "${APP_PATH}" 2>&1 exit 1 fi # ======================== Step 7: 创建 ZIP ======================== echo "" echo "📦 Step 7: 创建 ZIP..." ZIP_NAME="HiFastVPN-${VERSION}.zip" ZIP_PATH="${OUTPUT_DIR}/${ZIP_NAME}" rm -f "${ZIP_PATH}" ditto -c -k --keepParent "${APP_PATH}" "${ZIP_PATH}" echo " ✅ ZIP 创建完成: ${ZIP_PATH}" # ======================== Step 8: 公证 ======================== echo "" echo "📤 Step 8: 提交公证..." echo " (这可能需要几分钟时间...)" xcrun notarytool submit "${ZIP_PATH}" \ --apple-id "${APPLE_ID}" \ --password "${APP_PASSWORD}" \ --team-id "${TEAM_ID}" \ --wait echo " ✅ 公证完成" # ======================== Step 9: Staple ======================== echo "" echo "📎 Step 9: Staple..." xcrun stapler staple "${APP_PATH}" echo " ✅ Staple 完成" # ======================== Step 10: 最终验证 ======================== echo "" echo "🔍 Step 10: 最终验证..." echo "" echo "--- codesign --verify ---" codesign --verify --deep --strict "${APP_PATH}" && echo "✅ codesign 验证通过" echo "" echo "--- xcrun stapler validate ---" xcrun stapler validate "${APP_PATH}" echo "" echo "--- spctl -a -t execute ---" spctl -a -t execute -vv "${APP_PATH}" # ======================== Step 11: 创建 DMG ======================== echo "" echo "💿 Step 11: 创建 DMG..." DMG_NAME="HiFastVPN-${VERSION}.dmg" DMG_PATH="${OUTPUT_DIR}/${DMG_NAME}" rm -f "${DMG_PATH}" # 创建临时目录并复制 .app TEMP_DIR=$(mktemp -d) cp -R "${APP_PATH}" "${TEMP_DIR}/" # Finder 坐标计算(左上原点,中心点对齐) # 背景:1200 × 800 # HiFastVPN.app 中心:左 228, 上 418, 尺寸 168×168 APP_CENTER_X=236 APP_CENTER_Y=428 APP_ICON_SIZE=152 APP_X=$((APP_CENTER_X + APP_ICON_SIZE / 2)) APP_Y=$((APP_CENTER_Y + APP_ICON_SIZE / 2)) # Applications 图标中心:左 742, 上 372, 尺寸 259×259 DROP_CENTER_X=702 DROP_CENTER_Y=372 DROP_ICON_SIZE=259 DROP_X=$((DROP_CENTER_X + DROP_ICON_SIZE / 2)) DROP_Y=$((DROP_CENTER_Y + DROP_ICON_SIZE / 2)) # 使用 create-dmg 创建自定义 DMG echo " 创建自定义 DMG..." create-dmg \ --volname "HiFastVPN Installation" \ --background "${BACKGROUND_IMAGE}" \ --window-size 1200 800 \ --icon-size "${APP_ICON_SIZE}" \ --icon "HiFastVPN.app" "${APP_X}" "${APP_Y}" \ --app-drop-link "${DROP_X}" "${DROP_Y}" \ --hide-extension "HiFastVPN.app" \ --no-internet-enable \ "${DMG_PATH}" \ "${TEMP_DIR}" # 清理临时目录 rm -rf "${TEMP_DIR}" # 公证 DMG echo " 公证 DMG..." xcrun notarytool submit "${DMG_PATH}" \ --apple-id "${APPLE_ID}" \ --password "${APP_PASSWORD}" \ --team-id "${TEAM_ID}" \ --wait # Staple DMG echo " Staple DMG..." xcrun stapler staple "${DMG_PATH}" echo " ✅ DMG 创建完成: ${DMG_PATH}" # ======================== 完成 ======================== echo "" echo "╔═══════════════════════════════════════════════════════════════════╗" echo "║ 🎉 全部完成! ║" echo "╚═══════════════════════════════════════════════════════════════════╝" echo "" echo "输出文件:" echo " 📦 ${APP_PATH}" echo " 📦 ${ZIP_PATH}" echo " 💿 ${DMG_PATH}" echo "" echo "⚠️ 重要提示:" echo " 发给用户的应该是 DMG 文件,不是 ZIP 或 .app" echo " DMG 已经签名和公证,用户打开后不会被 Gatekeeper 拦截" echo " 图标和 Applications 已按设计稿中心点对齐" echo ""