Files
geo/apps/desktop-client/scripts/sign-mac.sh
T
root a9af6c634c
Backend CI / Backend (push) Successful in 13m6s
Deployment Config CI / Deployment Config (push) Successful in 17s
Frontend CI / Frontend (push) Successful in 2m24s
chore(desktop-client): add macOS code-signing and notarization scaffolding
- Move the build config out of package.json into electron-builder.yml so
  the mac block can carry hardenedRuntime, entitlements, universal arch,
  and usage-description Info.plist keys. Drop identity:null (which
  forces unsigned builds) and let CSC_NAME or the keychain decide.
- Add entitlements plist files and three helper scripts: sign-setup.sh
  (one-time keychain prep with notarytool store-credentials),
  sign-mac.sh (sign + notarize + staple, supports keychain or CI env
  vars), and ci-import-cert.sh (CI keychain bootstrap).
- Ignore .env.signing so local credentials don't sneak into the repo,
  and pin @emnapi/{core,runtime} as devDependencies so universal builds
  resolve them deterministically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:02:14 +08:00

111 lines
4.8 KiB
Bash
Executable File
Raw 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}"; }
# ── 前置检查(本地/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"
# ── 1. 构建 ──────────────────────────────────────────────────────
step "[1/5] 构建 Electron 产物"
pnpm build
ok "构建完成"
# ── 2. 打包 + 签名 ───────────────────────────────────────────────
step "[2/5] 打包 + 签名 (electron-builder)"
# 清理旧产物,避免 spctl 把上次未公证的 dmg 认成本次产物
rm -f release/*.dmg release/*-mac.zip release/*.blockmap 2>/dev/null || true
pnpm exec electron-builder --mac --arm64
ok "打包完成"
DMG=$(ls -t release/*.dmg 2>/dev/null | head -n1 || true)
ZIP=$(ls -t release/*-mac.zip 2>/dev/null | head -n1 || true)
APP=$(ls -td release/mac*/*.app 2>/dev/null | head -n1 || true)
[ -z "$DMG" ] && { fail "未找到 .dmg 产物"; exit 1; }
ok "DMG: $DMG"
[ -n "$ZIP" ] && ok "ZIP: $ZIP"
[ -n "$APP" ] && ok "APP: $APP"
# 验证签名是否真的盖上了
if [ -n "$APP" ]; then
if ! codesign --verify --deep --strict --verbose=2 "$APP" >/dev/null 2>&1; then
fail ".app 签名验证失败,停止公证"
codesign --verify --deep --strict --verbose=2 "$APP" || true
exit 1
fi
ok ".app 签名校验通过"
fi
# ── 3. 公证 ──────────────────────────────────────────────────────
step "[3/5] 提交公证 (notarytool, 通常 1-5 分钟)"
echo " 正在提交 DMG..."
xcrun notarytool submit "$DMG" \
"${NOTARY_ARGS[@]}" \
--wait
ok "DMG 公证通过"
if [ -n "$ZIP" ]; then
echo " 正在提交 ZIP(用于自动更新)..."
xcrun notarytool submit "$ZIP" \
--keychain-profile "$NOTARY_KEYCHAIN_PROFILE" \
--wait
ok "ZIP 公证通过"
fi
# ── 4. 装订 ticket ───────────────────────────────────────────────
step "[4/5] 装订 ticket (stapler)"
xcrun stapler staple "$DMG"
ok "DMG ticket 已装订(断网也能验证)"
# zip 不能 staple,但用户解压后里面的 .app 已经被 staple 过
# ── 5. 验证 ──────────────────────────────────────────────────────
step "[5/5] 最终验证 (Gatekeeper)"
if spctl -a -vv -t install "$DMG" 2>&1 | tee /tmp/spctl.log | grep -q "accepted"; then
ok "Gatekeeper 验证通过"
grep -E "source=|origin=" /tmp/spctl.log | sed 's/^/ /'
else
fail "Gatekeeper 拒绝了产物"
cat /tmp/spctl.log
exit 1
fi
# ── 完成 ────────────────────────────────────────────────────────
SIZE=$(du -h "$DMG" | awk '{print $1}')
echo ""
echo "${G}${B}━━━━━━━━━━ 全部完成 ━━━━━━━━━━${N}"
echo " 📦 $DMG (${SIZE})"
[ -n "$ZIP" ] && echo " 📦 $ZIP"
echo ""
echo " 这份 DMG 可以直接发给任何 macOS 用户,"
echo " 双击安装,首次打开${B}不会${N}弹\"无法验证开发者\"或被 Gatekeeper 拦截。"