Files
root 5f6e9f11da
Desktop Client Build / Resolve Build Metadata (push) Successful in 19s
Frontend CI / Frontend (push) Successful in 3m20s
Backend CI / Backend (push) Successful in 16m48s
Desktop Client Build / Build Desktop Client (push) Successful in 24m46s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 37s
feat(desktop): add client release updater
2026-05-25 19:23:49 +08:00

155 lines
6.3 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# 一键:构建 → 签名 → 公证 → 装订 → 验证
# 用法: pnpm sign:mac
set -euo pipefail
cd "$(dirname "$0")/.."
R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; B=$'\033[1m'; N=$'\033[0m'
ok() { echo " ${G}${N} $1"; }
fail() { echo " ${R}${N} $1"; }
step() { echo ""; echo "${B}── $1 ──${N}"; }
LOCK_DIR="${TMPDIR:-/tmp}/geo-rankly-desktop-sign-mac.lock"
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
fail "已有 sign:mac 正在运行,避免多个构建同时写 release/"
echo " 如确认没有进程,可删除: $LOCK_DIR"
exit 1
fi
trap 'rm -rf "$LOCK_DIR"' EXIT
# ── 前置检查(本地/CI 双模式)─────────────────────────────────────
# 本地模式:读 .env.signing,公证走钥匙串 profile
# CI 模式:靠环境变量 CSC_NAME / APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID
NOTARY_ARGS=()
if [ -f .env.signing ] && [ -z "${CI:-}" ]; then
# shellcheck source=/dev/null
set -a; source .env.signing; set +a
: "${CSC_NAME:?.env.signing 缺少 CSC_NAME}"
: "${APPLE_TEAM_ID:?.env.signing 缺少 APPLE_TEAM_ID}"
: "${NOTARY_KEYCHAIN_PROFILE:?.env.signing 缺少 NOTARY_KEYCHAIN_PROFILE}"
NOTARY_ARGS=(--keychain-profile "$NOTARY_KEYCHAIN_PROFILE")
ok "模式: 本地(钥匙串 profile"
elif [ -n "${CSC_NAME:-}" ] && [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_APP_SPECIFIC_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then
NOTARY_ARGS=(--apple-id "$APPLE_ID" --password "$APPLE_APP_SPECIFIC_PASSWORD" --team-id "$APPLE_TEAM_ID")
ok "模式: CI(环境变量)"
else
fail "未找到签名配置"
echo " 本地: 运行 ${B}pnpm sign:setup${N}"
echo " CI: 设置 ${B}CSC_NAME / APPLE_ID / APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID${N} 环境变量"
exit 1
fi
ok "签名身份: $CSC_NAME"
ok "Team ID: $APPLE_TEAM_ID"
CODESIGN_IDENTITY="${CODESIGN_IDENTITY:-$CSC_NAME}"
if [[ ! "$CODESIGN_IDENTITY" =~ ^[0-9A-Fa-f]{40}$ && "$CODESIGN_IDENTITY" != Developer\ ID\ Application:* ]]; then
CODESIGN_IDENTITY="Developer ID Application: $CODESIGN_IDENTITY"
fi
ok "codesign 身份: $CODESIGN_IDENTITY"
# ── 1. 构建 ──────────────────────────────────────────────────────
step "[1/5] 构建 Electron 产物"
pnpm build
ok "构建完成"
# ── 2. 打包 + 签名 ───────────────────────────────────────────────
step "[2/5] 打包 + 签名 (electron-builder)"
# 清理旧产物,避免 spctl 把上次未公证的 dmg 认成本次产物
rm -rf release/mac-* 2>/dev/null || true
rm -f release/*.dmg release/*-mac.zip release/*.blockmap 2>/dev/null || true
pnpm exec electron-builder --mac --arm64
ok "打包完成"
shopt -s nullglob
DMGS=(release/*.dmg)
ZIPS=(release/*-mac.zip)
APPS=(release/mac*/*.app)
shopt -u nullglob
[ "${#DMGS[@]}" -eq 0 ] && { fail "未找到 .dmg 产物"; exit 1; }
[ "${#APPS[@]}" -eq 0 ] && { fail "未找到 .app 产物"; exit 1; }
for DMG in "${DMGS[@]}"; do ok "DMG: $DMG"; done
for ZIP in "${ZIPS[@]}"; do ok "ZIP: $ZIP"; done
for APP in "${APPS[@]}"; do ok "APP: $APP"; done
# 验证签名是否真的盖上了
for APP in "${APPS[@]}"; do
if ! codesign --verify --deep --strict --verbose=2 "$APP" >/dev/null 2>&1; then
fail ".app 签名验证失败,停止公证: $APP"
codesign --verify --deep --strict --verbose=2 "$APP" || true
exit 1
fi
ok ".app 签名校验通过: $APP"
done
# electron-builder 会签 .app,但 DMG 本体在部分配置/版本下不会自动签。
# Gatekeeper 对安装包执行 `spctl -t install` 时要求外层 DMG 也有可用签名。
for DMG in "${DMGS[@]}"; do
echo " 正在签名 DMG: $DMG"
codesign --force --timestamp --sign "$CODESIGN_IDENTITY" "$DMG"
if ! codesign --verify --verbose=2 "$DMG" >/dev/null 2>&1; then
fail "DMG 签名验证失败,停止公证: $DMG"
codesign --verify --verbose=2 "$DMG" || true
exit 1
fi
ok "DMG 签名校验通过: $DMG"
done
# ── 3. 公证 ──────────────────────────────────────────────────────
step "[3/5] 提交公证 (notarytool, 通常 1-5 分钟)"
for DMG in "${DMGS[@]}"; do
echo " 正在提交 DMG: $DMG"
xcrun notarytool submit "$DMG" \
"${NOTARY_ARGS[@]}" \
--wait
ok "DMG 公证通过: $DMG"
done
for ZIP in "${ZIPS[@]}"; do
echo " 正在提交 ZIP(用于自动更新): $ZIP"
xcrun notarytool submit "$ZIP" \
"${NOTARY_ARGS[@]}" \
--wait
ok "ZIP 公证通过: $ZIP"
done
# ── 4. 装订 ticket ───────────────────────────────────────────────
step "[4/5] 装订 ticket (stapler)"
for DMG in "${DMGS[@]}"; do
xcrun stapler staple "$DMG"
ok "DMG ticket 已装订(断网也能验证): $DMG"
done
# zip 不能 staple,但用户解压后里面的 .app 已经被 staple 过
# ── 5. 验证 ──────────────────────────────────────────────────────
step "[5/5] 最终验证 (Gatekeeper)"
for DMG in "${DMGS[@]}"; do
LOG_FILE=$(mktemp "${TMPDIR:-/tmp}/spctl.XXXXXX")
if spctl -a -vv -t install "$DMG" 2>&1 | tee "$LOG_FILE" | grep -q "accepted"; then
ok "Gatekeeper 验证通过: $DMG"
grep -E "source=|origin=" "$LOG_FILE" | sed 's/^/ /'
else
fail "Gatekeeper 拒绝了产物: $DMG"
cat "$LOG_FILE"
rm -f "$LOG_FILE"
exit 1
fi
rm -f "$LOG_FILE"
done
# ── 完成 ────────────────────────────────────────────────────────
echo ""
echo "${G}${B}━━━━━━━━━━ 全部完成 ━━━━━━━━━━${N}"
for DMG in "${DMGS[@]}"; do
SIZE=$(du -h "$DMG" | awk '{print $1}')
echo " 📦 $DMG (${SIZE})"
done
for ZIP in "${ZIPS[@]}"; do
echo " 📦 $ZIP"
done
echo ""
echo " 这份 DMG 可以直接发给任何 macOS 用户,"
echo " 双击安装,首次打开${B}不会${N}弹\"无法验证开发者\"或被 Gatekeeper 拦截。"