48e282dd97
- 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>
107 lines
4.7 KiB
Bash
Executable File
107 lines
4.7 KiB
Bash
Executable File
#!/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/
|