支持 Intel 芯片:分别打包 Apple 芯片版与 Intel 版,官网按芯片提供两个下载按钮

- Makefile 支持 ARCH=arm64 / x86_64,release.sh 两种芯片各自构建、签名、公证,生成 DMG 与升级包
- 版本清单顶层仍是 Apple 芯片版(兼容 0.3.0),intel 字段放 Intel 版;应用按芯片挑包,安装前核对架构
- Homebrew cask 按 arch 选择安装包
- 官网下载区改为 Apple 芯片 / Intel 两张卡片,README 安装段落与常见问题同步
This commit is contained in:
gentpan committed 2026-09-15 16:21:29 +05:00
1 parent 5de9f22221
commit 40b7e41cbf
16 files changed
+319 -172

No files matched your search

+16 -6
View File
@@ -1,7 +1,10 @@
#!/usr/bin/env python3
"""生成应用内在线升级读取的版本清单 appcast.json。
Scripts/appcast.py <版本> <build> <zip 路径> <dmg 路径> <下载地址前缀> > dist/appcast.json
Scripts/appcast.py <版本> <build> <下载地址前缀> <Apple 芯片 zip> <Apple 芯片 dmg> <Intel zip> <Intel dmg> > dist/appcast.json
顶层的 url / sha256 / size / dmg 是 Apple 芯片版:0.3.0 只有 Apple 芯片版,也只认顶层字段。
Intel 版放在 intel 字段里,0.3.1 起的 Intel 版应用读它。
更新摘要取自 CHANGELOG.md 中该版本的条目:每条取冒号或句号之前的部分(过短时带上冒号后的内容),最多 10 条。
"""
@@ -50,8 +53,17 @@ def sha256(path: str) -> str:
return digest.hexdigest()
def asset(base: str, zip_path: str, dmg_path: str) -> dict:
return {
"url": f"{base}/{os.path.basename(zip_path)}",
"sha256": sha256(zip_path),
"size": os.path.getsize(zip_path),
"dmg": f"{base}/{os.path.basename(dmg_path)}",
}
def main() -> None:
version, build, zip_path, dmg_path, base = sys.argv[1:6]
version, build, base, arm_zip, arm_dmg, intel_zip, intel_dmg = sys.argv[1:8]
root = os.path.join(os.path.dirname(__file__), "..")
with open(os.path.join(root, "CHANGELOG.md"), encoding="utf-8") as handle:
date, notes = release_notes(handle.read(), version)
@@ -60,12 +72,10 @@ def main() -> None:
"build": build,
"date": date,
"minimumSystem": "14.0",
"url": f"{base}/{os.path.basename(zip_path)}",
"sha256": sha256(zip_path),
"size": os.path.getsize(zip_path),
"dmg": f"{base}/{os.path.basename(dmg_path)}",
**asset(base, arm_zip, arm_dmg),
"notes": notes,
"changelog": "https://getopenstats.com/#changelog",
"intel": asset(base, intel_zip, intel_dmg),
}
json.dump(feed, sys.stdout, ensure_ascii=False, indent=2)
sys.stdout.write("\n")
+2 -2
View File
@@ -28,8 +28,8 @@ fi
mv "$SOURCE" "$TARGET"
"$LSREGISTER" -f "$TARGET"
# 编译目录里残留的旧产物(Debug / Release 另一种配置)也清掉
for stale in build/DerivedData/Build/Products/*/OpenStats.app; do
# 编译目录里残留的旧产物(Debug / Release 另一种配置、发布时另一种芯片的版本)也清掉
for stale in build/DerivedData*/Build/Products/*/OpenStats.app; do
[ -d "$stale" ] || continue
"$LSREGISTER" -u "$stale" >/dev/null 2>&1 || true
rm -rf "$stale"
+24 -12
View File
@@ -1,5 +1,5 @@
#!/bin/bash
# 发布已公证的版本:DMG 与在线升级包上传到官网 /download/,最后更新版本清单 appcast.json(已安装的应用据此提示升级),
# 发布已公证的版本:Apple 芯片版与 Intel 版的 DMG、在线升级包上传到官网 /download/,最后更新版本清单 appcast.json(已安装的应用据此提示升级),
# cask 提交到 gentpan/homebrew-tap,并校验线上文件。
#
# ./Scripts/release.sh # 先打包、公证
@@ -12,25 +12,35 @@ KEY="${SITE_KEY:-$HOME/.ssh/gentpan.pem}"
ROOT="${SITE_ROOT:-/var/www/getopenstats.com}"
TAP="${TAP:-gentpan/homebrew-tap}"
VERSION="$(sed -nE 's/^ *MARKETING_VERSION: *"?([0-9.]+)"?.*/\1/p' project.yml | head -1)"
DMG="dist/OpenStats-${VERSION}.dmg"
ZIP="dist/OpenStats-${VERSION}.zip"
CHIPS=(AppleSilicon Intel)
APPCAST="dist/appcast.json"
CASK="dist/openstats.rb"
SSH=(ssh -i "$KEY" -o BatchMode=yes "$HOST")
for file in "$DMG" "$ZIP" "$APPCAST" "$CASK"; do
for chip in "${CHIPS[@]}"; do
for ext in dmg zip; do
file="dist/OpenStats-${VERSION}-${chip}.${ext}"
[ -f "$file" ] || { echo "缺少 $file,先运行 ./Scripts/release.sh" >&2; exit 1; }
done
done
for file in "$APPCAST" "$CASK"; do
[ -f "$file" ] || { echo "缺少 $file,先运行 ./Scripts/release.sh" >&2; exit 1; }
done
python3 - "$APPCAST" "$VERSION" "$ZIP" <<'PY' || { echo "$APPCAST 与 $ZIP 不一致" >&2; exit 1; }
python3 - "$APPCAST" "$VERSION" "dist/OpenStats-${VERSION}-AppleSilicon.zip" "dist/OpenStats-${VERSION}-Intel.zip" <<'PY' \
|| { echo "$APPCAST 与升级包不一致" >&2; exit 1; }
import hashlib, json, sys
feed = json.load(open(sys.argv[1]))
digest = hashlib.sha256(open(sys.argv[3], "rb").read()).hexdigest()
assert feed["version"] == sys.argv[2] and feed["sha256"] == digest and feed["notes"], feed
digest = lambda path: hashlib.sha256(open(path, "rb").read()).hexdigest()
assert feed["version"] == sys.argv[2] and feed["notes"], feed
assert feed["sha256"] == digest(sys.argv[3]) and feed["url"].endswith("-AppleSilicon.zip"), feed
assert feed["intel"]["sha256"] == digest(sys.argv[4]) and feed["intel"]["url"].endswith("-Intel.zip"), feed
PY
grep -q "version \"${VERSION}\"" "$CASK" || { echo "$CASK 的版本不是 ${VERSION}" >&2; exit 1; }
xcrun stapler validate "$DMG" >/dev/null || { echo "$DMG 没有装订公证票据" >&2; exit 1; }
SHA="$(shasum -a 256 "$DMG" | cut -d' ' -f1)"
grep -q "$SHA" "$CASK" || { echo "$CASK 里的 sha256 与 DMG 不一致" >&2; exit 1; }
for chip in "${CHIPS[@]}"; do
dmg="dist/OpenStats-${VERSION}-${chip}.dmg"
xcrun stapler validate "$dmg" >/dev/null || { echo "$dmg 没有装订公证票据" >&2; exit 1; }
grep -q "$(shasum -a 256 "$dmg" | cut -d' ' -f1)" "$CASK" || { echo "$CASK 里的 sha256 与 $dmg 不一致" >&2; exit 1; }
done
# 上传:先传临时名再改名,下载中途不会拿到半个文件
"${SSH[@]}" "sudo install -d -o \$(id -un) -m 755 $ROOT/download"
@@ -44,8 +54,10 @@ upload() {
[ "$online" = "$(shasum -a 256 "$1" | cut -d' ' -f1)" ] || { echo "线上 ${name} 校验不一致:$online" >&2; exit 1; }
echo "✅ https://getopenstats.com/download/${name}"
}
upload "$DMG"
upload "$ZIP"
for chip in "${CHIPS[@]}"; do
upload "dist/OpenStats-${VERSION}-${chip}.dmg"
upload "dist/OpenStats-${VERSION}-${chip}.zip"
done
# 版本清单最后发布:安装包都已就位后,已安装的应用才会看到新版本
upload "$APPCAST"
+94 -65
View File
@@ -1,5 +1,6 @@
#!/bin/bash
# 发布一个可分发的版本:Developer ID 签名 → 公证 → 装订 → DMG(签名、公证、装订)→ 在线升级包与版本清单 → Homebrew cask。
# 发布一个可分发的版本:Apple 芯片版与 Intel 版各自 Developer ID 签名 → 公证 → 装订 → DMG(签名、公证、装订)
# → 在线升级包 → 版本清单 → Homebrew cask。
#
# ./Scripts/release.sh
#
@@ -12,16 +13,17 @@
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)"
APP="build/DerivedData/Build/Products/Release/OpenStats.app"
DMG_NAME="OpenStats-${VERSION}.dmg"
ZIP_NAME="OpenStats-${VERSION}.zip"
# 编译架构与文件名里的芯片名:OpenStats-0.3.1-AppleSilicon.dmg、OpenStats-0.3.1-Intel.dmg
ARCHS=(arm64 x86_64)
chip() { [ "$1" = arm64 ] && echo AppleSilicon || echo Intel; }
app_for() { echo "build/DerivedData-$1/Build/Products/Release/OpenStats.app"; }
# 在线升级的更新摘要取自该版本的更新日志:发版前把 “## 未发布” 改成 “## 版本 · 日期”
grep -q "^## ${VERSION} · " CHANGELOG.md \
@@ -34,78 +36,102 @@ fi
TEAM_ID="$(echo "$SIGN_ID" | sed -nE 's/.*\(([A-Z0-9]+)\)$/\1/p')"
echo "版本 ${VERSION} · 签名身份:${SIGN_ID}"
# ---- 构建 --------------------------------------------------------------------
make build CONFIG=Release INSTALL=0 SIGN_ID="$SIGN_ID"
# make build 会把构建号加一,构建完再读,版本清单里的 build 才与安装包一致
BUILD="$(sed -nE 's/^ *CURRENT_PROJECT_VERSION: *"?([0-9]+)"?.*/\1/p' project.yml | head -1)"
codesign --verify --deep --strict --verbose=2 "$APP"
for binary in "$APP" "$APP/Contents/MacOS/OpenStatsHelper" "$APP/Contents/PlugIns/OpenStatsWidget.appex"; 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"
mkdir -p "$DIST"
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
# 两种芯片共用一个构建号:先加一次,各自构建时不再加
./Scripts/version.sh build >/dev/null
BUILD="$(sed -nE 's/^ *CURRENT_PROJECT_VERSION: *"?([0-9]+)"?.*/\1/p' project.yml | head -1)"
SHA256="$(shasum -a 256 "$DIST/$DMG_NAME" | cut -d' ' -f1)"
for arch in "${ARCHS[@]}"; do
APP="$(app_for "$arch")"
NAME="OpenStats-${VERSION}-$(chip "$arch")"
echo
echo "==== $(chip "$arch")(${arch})===="
# ---- 在线升级 ------------------------------------------------------------------
# ---- 构建 ------------------------------------------------------------------
# 应用内升级下载已装订票据的 .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"
rm -rf "$APP"
make build CONFIG=Release INSTALL=0 BUMP=0 ARCH="$arch" SIGN_ID="$SIGN_ID"
for binary in "$APP/Contents/MacOS/OpenStats" "$APP/Contents/MacOS/OpenStatsHelper" \
"$APP/Contents/PlugIns/OpenStatsWidget.appex/Contents/MacOS/OpenStatsWidget"; do
[ "$(lipo -archs "$binary")" = "$arch" ] \
|| { echo "error: $binary 的架构是 $(lipo -archs "$binary"),应当只有 $arch" >&2; exit 1; }
done
codesign --verify --deep --strict --verbose=2 "$APP"
for binary in "$APP" "$APP/Contents/MacOS/OpenStatsHelper" "$APP/Contents/PlugIns/OpenStatsWidget.appex"; 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
# ---- 公证 App --------------------------------------------------------------
if [ "${SKIP_NOTARIZE:-0}" != "1" ]; then
# ditto 而不是 zip:保留包内的符号链接与扩展属性
ditto -c -k --keepParent "$APP" "$WORK/$NAME-notarize.zip"
notarize "$WORK/$NAME-notarize.zip"
xcrun stapler staple "$APP"
xcrun stapler validate "$APP"
# 拒绝发布 Gatekeeper 在用户机器上仍会拒绝的包(Intel 版的签名与公证票据同样能在本机校验)
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 "$WORK/dmg"
mkdir -p "$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/$NAME.dmg" >/dev/null
codesign --force --timestamp --sign "$SIGN_ID" "$DIST/$NAME.dmg"
if [ "${SKIP_NOTARIZE:-0}" != "1" ]; then
notarize "$DIST/$NAME.dmg"
xcrun stapler staple "$DIST/$NAME.dmg"
spctl -a -t open --context context:primary-signature -vv "$DIST/$NAME.dmg"
fi
# ---- 在线升级 ----------------------------------------------------------------
# 应用内升级下载已装订票据的 .app 压缩包
ditto -c -k --keepParent "$APP" "$DIST/$NAME.zip"
done
# 版本清单:顶层是 Apple 芯片版(0.3.0 只认顶层字段),intel 字段是 Intel 版
python3 Scripts/appcast.py "$VERSION" "$BUILD" "$DOWNLOAD_BASE" \
"$DIST/OpenStats-${VERSION}-AppleSilicon.zip" "$DIST/OpenStats-${VERSION}-AppleSilicon.dmg" \
"$DIST/OpenStats-${VERSION}-Intel.zip" "$DIST/OpenStats-${VERSION}-Intel.dmg" > "$DIST/appcast.json"
# ---- Homebrew cask ------------------------------------------------------------
SHA_ARM="$(shasum -a 256 "$DIST/OpenStats-${VERSION}-AppleSilicon.dmg" | cut -d' ' -f1)"
SHA_INTEL="$(shasum -a 256 "$DIST/OpenStats-${VERSION}-Intel.dmg" | cut -d' ' -f1)"
cat > "$DIST/openstats.rb" <<CASK
cask "openstats" do
version "${VERSION}"
sha256 "${SHA256}"
arch arm: "AppleSilicon", intel: "Intel"
url "${DOWNLOAD_BASE}/OpenStats-#{version}.dmg"
version "${VERSION}"
sha256 arm: "${SHA_ARM}",
intel: "${SHA_INTEL}"
url "${DOWNLOAD_BASE}/OpenStats-#{version}-#{arch}.dmg"
name "OpenStats"
desc "Menu bar system monitor with fan control, keep-awake and cleanup"
homepage "https://getopenstats.com"
@@ -123,16 +149,19 @@ cask "openstats" do
end
CASK
# 本机只保留一份:把刚公证的发布版装到 /Applications,编译目录里不留副本
./Scripts/install_local.sh "$APP"
# 本机只保留一份:把本机芯片的发布版装到 /Applications,另一种芯片的编译产物由安装脚本一并清掉
native="$(uname -m)"
./Scripts/install_local.sh "$(app_for "$native")"
echo
echo "✅ ${DIST}/${DMG_NAME}"
echo " SHA-256 ${SHA256}"
echo " 在线升级:${DIST}/${ZIP_NAME} · ${DIST}/appcast.json"
for arch in "${ARCHS[@]}"; do
NAME="OpenStats-${VERSION}-$(chip "$arch")"
echo "✅ ${DIST}/${NAME}.dmg SHA-256 $(shasum -a 256 "$DIST/$NAME.dmg" | cut -d' ' -f1)"
done
echo " 在线升级:${DIST}/*.zip · ${DIST}/appcast.json"
echo " Homebrew cask:${DIST}/openstats.rb"
if [ "${SKIP_NOTARIZE:-0}" = "1" ]; then
echo "⚠️ 未公证,仅供本机测试。"
else
echo "下一步:./Scripts/publish_release.sh 上传 DMG 到官网并更新 gentpan/homebrew-tap。"
echo "下一步:./Scripts/publish_release.sh 上传安装包到官网并更新 gentpan/homebrew-tap。"
fi