Files
jiu/scripts/ci/compile-macos.sh
wangjia 48e282dd97 feat(devops): macOS 构建接入 Developer ID 签名 + 公证
- compile-macos.sh:flutter build 后 codesign(hardened runtime + timestamp
  + Release.entitlements)→ notarytool 公证(复用 App Store Connect API key)
  → stapler staple → 重新打包,产出已签名+公证+stapled 的 .app
- 强制策略:缺 MACOS_DEVELOPER_ID_CERT_P12_BASE64 / APPSTORE_API_KEY_P8_BASE64
  时在 flutter build 前 fail-fast,绝不产出未签名包
- deploy-client.yml:build-macos 步骤注入证书与 API key secrets
- 新增 docs/macos-signing.md:Developer ID Application 证书创建与 secret 配置

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 12:56:08 +08:00

107 lines
4.7 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
# compile-macos.sh <tag> — build Flutter macOS desktop, package into dist/
set -euo pipefail
# shellcheck source=scripts/ci/_env.sh
. "$(dirname "$0")/_env.sh"
TAG="$1"
VER="${TAG#client-v}"
echo "==> compile-macos: version=${VER}"
# macOS 通过官网下载 + 应用内自更新分发,未签名/未公证的包会被 Gatekeeper 拦截,
# 因此【强制】签名+公证:缺任一关键 secret 立即报错、流水线红,绝不产出未签名包。
# 在跑昂贵的 flutter build 之前先做这一检查,快速失败。详见 docs/macos-signing.md。
#
# 需要的 CI secrets
# MACOS_DEVELOPER_ID_CERT_P12_BASE64 Developer ID Application 证书 (.p12) 的 base64
# MACOS_DEVELOPER_ID_CERT_PASSWORD .p12 导出密码
# APPSTORE_API_KEY_ID / APPSTORE_API_ISSUER_ID / APPSTORE_API_KEY_P8_BASE64 复用 iOS 的 App Store Connect API key(公证用)
if [ -z "${MACOS_DEVELOPER_ID_CERT_P12_BASE64:-}" ] || [ -z "${APPSTORE_API_KEY_P8_BASE64:-}" ]; then
echo "ERROR: macOS 签名/公证所需 secret 未配置,拒绝产出未签名包。" >&2
echo " 需要 MACOS_DEVELOPER_ID_CERT_P12_BASE64 + APPSTORE_API_KEY_P8_BASE64,详见 docs/macos-signing.md" >&2
exit 1
fi
# Sync Flutter pubspec version (BSD sed on macOS)
sed -i '' "s/^version:.*/version: ${VER}+1/" client/pubspec.yaml
# Build Flutter macOS
cd client
flutter build macos --release \
"--dart-define=BASE_URL=https://jiu.51yanmei.com" \
"--dart-define=PUBLIC_URL=https://jiu.51yanmei.com" \
"--dart-define=APP_VERSION=v${VER}"
cd ..
APP_SRC="client/build/macos/Build/Products/Release/jiu_client.app"
# --- 代码签名 + 公证(Developer ID + Notarization---
# secret 已在脚本开头校验过(缺失会 fail-fast),此处必有证书与 API key。
echo "==> compile-macos: signing + notarizing"
WORK="$(mktemp -d)"
KEYCHAIN="$WORK/jiu-mac.keychain-db"
cleanup_mac() {
security delete-keychain "$KEYCHAIN" 2>/dev/null || true
rm -rf "$WORK"
}
trap cleanup_mac EXIT
# 1. 临时 keychain 导入 Developer ID Application 证书
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"
echo "${MACOS_DEVELOPER_ID_CERT_P12_BASE64}" | base64 --decode > "$WORK/devid.p12"
security import "$WORK/devid.p12" -k "$KEYCHAIN" \
-P "${MACOS_DEVELOPER_ID_CERT_PASSWORD:-}" -T /usr/bin/codesign -T /usr/bin/security
# 允许 codesign 非交互访问私钥
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PWD" "$KEYCHAIN" >/dev/null
# 把临时 keychain 加入搜索列表(保留默认 login keychain
security list-keychains -d user -s "$KEYCHAIN" login.keychain-db
# 解出签名身份 "Developer ID Application: NAME (TEAMID)"
IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN" \
| grep 'Developer ID Application' | head -1 | sed -E 's/.*"(.*)"/\1/')"
if [ -z "$IDENTITY" ]; then
echo "ERROR: Developer ID Application identity not found in keychain" >&2
exit 1
fi
echo "==> compile-macos: signing identity '${IDENTITY}'"
# 2. inside-out 签名:先签嵌套 framework/dylib,再签 app(均启用 hardened runtime + 安全时间戳)
if [ -d "${APP_SRC}/Contents/Frameworks" ]; then
find "${APP_SRC}/Contents/Frameworks" \( -name '*.framework' -o -name '*.dylib' \) -print0 \
| xargs -0 -I{} codesign --force --options runtime --timestamp --sign "$IDENTITY" {}
fi
codesign --force --options runtime --timestamp \
--entitlements client/macos/Runner/Release.entitlements \
--sign "$IDENTITY" "${APP_SRC}"
codesign --verify --deep --strict --verbose=2 "${APP_SRC}"
# 3. 提交公证(复用 App Store Connect API key),阻塞等待结果
ditto -c -k --keepParent "${APP_SRC}" "$WORK/notarize.zip"
echo "${APPSTORE_API_KEY_P8_BASE64}" | base64 --decode > "$WORK/AuthKey.p8"
xcrun notarytool submit "$WORK/notarize.zip" \
--key "$WORK/AuthKey.p8" \
--key-id "${APPSTORE_API_KEY_ID}" \
--issuer "${APPSTORE_API_ISSUER_ID}" \
--wait
# 4. staple 公证票据进 .app(离线也能通过 Gatekeeper),并核验
xcrun stapler staple "${APP_SRC}"
xcrun stapler validate "${APP_SRC}"
spctl -a -vvv -t install "${APP_SRC}" || true # 期望 source=Notarized Developer ID
echo "==> compile-macos: signed + notarized + stapled"
# Package .app bundle into zip(此时 APP_SRC 已签名+stapled
# ditto preserves symlinks, Unix permissions, and extended attributes (code-signing).
# Python's zipfile follows symlinks and drops permissions — do NOT use it for .app bundles.
mkdir -p dist
ditto -c -k --keepParent "${APP_SRC}" dist/jiu-macos-x64.zip
echo "Packaged dist/jiu-macos-x64.zip ($(du -sh dist/jiu-macos-x64.zip | cut -f1))"
echo "==> compile-macos: done — dist/ contents:"
ls -lh dist/