#!/bin/bash # 发布一个可分发的版本:Developer ID 签名 → 公证 → 装订 → DMG(签名、公证、装订)→ 在线升级包与版本清单 → Homebrew cask。 # # ./Scripts/release.sh # # 需要钥匙串里的 Developer ID Application 证书和 notarytool 凭据。凭据按 Apple ID 与团队保存, # 默认沿用 QuotaBar 的 “QuotaBar” 凭据;也可以单独保存一份: # xcrun notarytool store-credentials OpenStats --apple-id you@example.com --team-id # NOTARY_PROFILE=OpenStats ./Scripts/release.sh # # SKIP_NOTARIZE=1 只生成未公证的 DMG 供本机测试——不要分发,别的 Mac 上 Gatekeeper 会拒绝打开。 set -euo pipefail cd "$(dirname "$0")/.." # 安装包放在官网(仓库是私有的,GitHub Release 无法匿名下载) DOWNLOAD_BASE="${DOWNLOAD_BASE:-https://getopenstats.com/download}" DIST="${DIST:-dist}" NOTARY_PROFILE="${NOTARY_PROFILE:-QuotaBar}" SIGN_ID="${SIGN_ID:-$(security find-identity -v -p codesigning 2>/dev/null \ | grep 'Developer ID Application' | head -1 | sed -E 's/.*"(.*)".*/\1/' || true)}" VERSION="$(sed -nE 's/^ *MARKETING_VERSION: *"?([0-9.]+)"?.*/\1/p' project.yml | head -1)" BUILD="$(sed -nE 's/^ *CURRENT_PROJECT_VERSION: *"?([0-9]+)"?.*/\1/p' project.yml | head -1)" APP="build/DerivedData/Build/Products/Release/OpenStats.app" DMG_NAME="OpenStats-${VERSION}.dmg" ZIP_NAME="OpenStats-${VERSION}.zip" # 在线升级的更新摘要取自该版本的更新日志:发版前把 “## 未发布” 改成 “## 版本 · 日期” grep -q "^## ${VERSION} · " CHANGELOG.md \ || { echo "error: CHANGELOG.md 里没有 “## ${VERSION} · 日期” 标题,先把 “## 未发布” 改成正式版本。" >&2; exit 1; } if [ -z "$SIGN_ID" ]; then echo "error: 钥匙串里没有 Developer ID Application 证书,无法发布。" >&2 exit 1 fi TEAM_ID="$(echo "$SIGN_ID" | sed -nE 's/.*\(([A-Z0-9]+)\)$/\1/p')" echo "版本 ${VERSION} · 签名身份:${SIGN_ID}" # ---- 构建 -------------------------------------------------------------------- make build CONFIG=Release SIGN_ID="$SIGN_ID" codesign --verify --deep --strict --verbose=2 "$APP" for binary in "$APP" "$APP/Contents/MacOS/OpenStatsHelper"; do details="$(codesign -dvv "$binary" 2>&1)" echo "$details" | grep -q "TeamIdentifier=${TEAM_ID}" \ || { echo "error: $binary 未使用团队 ${TEAM_ID} 签名" >&2; exit 1; } echo "$details" | grep -q "Timestamp=" \ || { echo "error: $binary 缺少安全时间戳" >&2; exit 1; } echo "$details" | grep -Eq "flags=.*runtime" \ || { echo "error: $binary 未启用 Hardened Runtime" >&2; exit 1; } done notarize() { echo "提交公证:$(basename "$1")(通常需要几分钟)…" xcrun notarytool submit "$1" --keychain-profile "$NOTARY_PROFILE" --wait } # ---- 公证 App ---------------------------------------------------------------- WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT if [ "${SKIP_NOTARIZE:-0}" != "1" ]; then # ditto 而不是 zip:保留包内的符号链接与扩展属性 ditto -c -k --keepParent "$APP" "$WORK/OpenStats.zip" notarize "$WORK/OpenStats.zip" xcrun stapler staple "$APP" xcrun stapler validate "$APP" # 拒绝发布 Gatekeeper 在用户机器上仍会拒绝的包 if ! spctl -a -vv "$APP" 2>&1 | grep -q accepted; then echo "error: Gatekeeper 仍然拒绝该 App,停止发布。" >&2 spctl -a -vv "$APP" || true exit 1 fi fi # ---- DMG --------------------------------------------------------------------- rm -rf "$DIST" mkdir -p "$DIST" "$WORK/dmg" ditto "$APP" "$WORK/dmg/OpenStats.app" ln -s /Applications "$WORK/dmg/Applications" hdiutil create -volname "OpenStats ${VERSION}" -srcfolder "$WORK/dmg" -ov -format UDZO "$DIST/$DMG_NAME" >/dev/null codesign --force --timestamp --sign "$SIGN_ID" "$DIST/$DMG_NAME" if [ "${SKIP_NOTARIZE:-0}" != "1" ]; then notarize "$DIST/$DMG_NAME" xcrun stapler staple "$DIST/$DMG_NAME" spctl -a -t open --context context:primary-signature -vv "$DIST/$DMG_NAME" fi SHA256="$(shasum -a 256 "$DIST/$DMG_NAME" | cut -d' ' -f1)" # ---- 在线升级 ------------------------------------------------------------------ # 应用内升级下载已装订票据的 .app 压缩包,版本清单里带 sha256 与更新摘要 ditto -c -k --keepParent "$APP" "$DIST/$ZIP_NAME" python3 Scripts/appcast.py "$VERSION" "$BUILD" "$DIST/$ZIP_NAME" "$DIST/$DMG_NAME" "$DOWNLOAD_BASE" > "$DIST/appcast.json" # ---- Homebrew cask ------------------------------------------------------------ cat > "$DIST/openstats.rb" <