a9af6c634c
- 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>
111 lines
4.8 KiB
Bash
Executable File
111 lines
4.8 KiB
Bash
Executable File
#!/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 拦截。"
|