feat: Sparkle 2 auto-update
* feat: integrate Sparkle 2 for auto-update Adds the standard pieces for Sparkle-based silent updates: - Package.swift: add Sparkle 2 SPM dep, wired into OpenLessApp. - Sources/OpenLessApp/Updater/UpdaterController.swift: thin wrapper around SPUStandardUpdaterController. Owned by AppDelegate so the updater starts on app launch. - AppDelegate: instantiate UpdaterController before installing the main menu; pass it to ApplicationMenu so the menu item can target it. - ApplicationMenu: add "检查更新…" as the first item in the OpenLess menu when an UpdaterController target is present. - scripts/build-app.sh: copy Sparkle.framework (with Updater.app + XPC services + Autoupdate) from the SPM artifacts dir into Contents/Frameworks/, inject SUFeedURL / SUPublicEDKey / SUEnableAutomaticChecks / SUEnableInstallerLauncherService / SUScheduledCheckInterval into Info.plist, and ad-hoc sign the embedded helpers explicitly (deep sign alone is unreliable for Sparkle's nested XPC bundles). - scripts/release.sh: one-shot release pipeline. ./scripts/release.sh <version> [<build>] [<notes>] Bumps version in build-app.sh, builds, zips, signs the zip with the Keychain-stored EdDSA private key (auto-downloads sign_update from the Sparkle 2.9.1 release on first run), prepends a new <item> to appcast.xml, commits + tags + pushes main, and creates the GitHub release with the zip uploaded. - appcast.xml: empty initial feed file at repo root, served via the raw.githubusercontent.com URL referenced by SUFeedURL. Items are appended automatically by release.sh. Public EdDSA key embedded in the build: iT00+eUw/55obn1suEnWqI7za2pc8mHIFIdRbOWXW1Q= Private key lives only in the local Keychain on the release machine (account: macOS login user; service: https://sparkle-project.org). First-time install still requires the user to remove the quarantine xattr (no Apple Developer ID); subsequent updates flow through Sparkle's installer helper and bypass quarantine. * fix(sparkle): add @executable_path/../Frameworks rpath to OpenLessApp Without this, the SPM-built executable links Sparkle's symbols but ships with no rpath that points at the embedded framework, so dyld fails at launch with: Library not loaded: @rpath/Sparkle.framework/Versions/B/Sparkle …and the user sees the macOS Gatekeeper-styled "请与开发者联系,以确保 OpenLess 可以配合此 macOS 版本使用" error. Inject the rpath via linkerSettings.unsafeFlags so the binary itself knows where to look. --------- Co-authored-by: baiqing <lbx12309@icloud.com>
This commit is contained in:
1 parent
56c3e09983
commit
f38643f17c
6 files changed
+234
-5
No files matched your search
+39
-2
@@ -12,6 +12,12 @@ BUILD_DIR="build"
|
||||
APP_DIR="${BUILD_DIR}/${APP_NAME}.app"
|
||||
BIN_DIR="${APP_DIR}/Contents/MacOS"
|
||||
RES_DIR="${APP_DIR}/Contents/Resources"
|
||||
FRAMEWORKS_DIR="${APP_DIR}/Contents/Frameworks"
|
||||
|
||||
# Sparkle 自动更新:appcast 文件的公开 URL + 锁定签名所用的 EdDSA 公钥。
|
||||
# 公钥永远公开(验证下载内容用),私钥只在本机 Keychain,不进仓库。
|
||||
SPARKLE_FEED_URL="https://raw.githubusercontent.com/appergb/openless/main/appcast.xml"
|
||||
SPARKLE_PUBLIC_KEY="iT00+eUw/55obn1suEnWqI7za2pc8mHIFIdRbOWXW1Q="
|
||||
|
||||
echo "[build-app] generate app icon"
|
||||
swift scripts/generate-app-icon.swift
|
||||
@@ -24,11 +30,19 @@ BIN_SRC=".build/release/${APP_NAME}"
|
||||
|
||||
echo "[build-app] assemble bundle at ${APP_DIR}"
|
||||
rm -rf "${APP_DIR}"
|
||||
mkdir -p "${BIN_DIR}" "${RES_DIR}"
|
||||
mkdir -p "${BIN_DIR}" "${RES_DIR}" "${FRAMEWORKS_DIR}"
|
||||
cp "${BIN_SRC}" "${BIN_DIR}/${APP_NAME}"
|
||||
cp "Resources/AppIcon.icns" "${RES_DIR}/AppIcon.icns"
|
||||
cp "Resources/AppIcon.png" "${RES_DIR}/AppIcon.png"
|
||||
|
||||
# 嵌入 Sparkle.framework(含 Updater.app + XPC services + Autoupdate)。
|
||||
# SPM 把 xcframework 解压到 .build/artifacts/sparkle/Sparkle/Sparkle.xcframework/,
|
||||
# 选 macos-arm64_x86_64 那个 slice。整个 framework 一次性 cp -R 进去。
|
||||
SPARKLE_FRAMEWORK_SRC=".build/artifacts/sparkle/Sparkle/Sparkle.xcframework/macos-arm64_x86_64/Sparkle.framework"
|
||||
[ -d "${SPARKLE_FRAMEWORK_SRC}" ] || { echo "missing ${SPARKLE_FRAMEWORK_SRC} — 跑过 swift build 没?"; exit 1; }
|
||||
echo "[build-app] embed Sparkle.framework"
|
||||
cp -R "${SPARKLE_FRAMEWORK_SRC}" "${FRAMEWORKS_DIR}/Sparkle.framework"
|
||||
|
||||
cat > "${APP_DIR}/Contents/Info.plist" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
@@ -58,11 +72,34 @@ cat > "${APP_DIR}/Contents/Info.plist" <<EOF
|
||||
<string>OpenLess 需要麦克风权限以录制您的语音并转写为文字。</string>
|
||||
<key>NSAppleEventsUsageDescription</key>
|
||||
<string>OpenLess 需要权限以将转写后的文字插入到您当前光标所在的输入框。</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>${SPARKLE_FEED_URL}</string>
|
||||
<key>SUPublicEDKey</key>
|
||||
<string>${SPARKLE_PUBLIC_KEY}</string>
|
||||
<key>SUEnableAutomaticChecks</key>
|
||||
<true/>
|
||||
<key>SUEnableInstallerLauncherService</key>
|
||||
<true/>
|
||||
<key>SUScheduledCheckInterval</key>
|
||||
<integer>3600</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
echo "[build-app] ad-hoc code sign"
|
||||
echo "[build-app] ad-hoc code sign (含 Sparkle helpers)"
|
||||
# 顺序很重要:先签内层 helpers,最后签外层 .app;--deep 在 Sparkle 这种多层
|
||||
# bundle 上不可靠(某些 XPC 会被漏签),所以显式逐个签。
|
||||
SPARKLE_VERSIONS_DIR="${FRAMEWORKS_DIR}/Sparkle.framework/Versions/B"
|
||||
codesign --force --sign - --timestamp=none \
|
||||
"${SPARKLE_VERSIONS_DIR}/XPCServices/Installer.xpc" 2>/dev/null || true
|
||||
codesign --force --sign - --timestamp=none \
|
||||
"${SPARKLE_VERSIONS_DIR}/XPCServices/Downloader.xpc" 2>/dev/null || true
|
||||
codesign --force --sign - --timestamp=none \
|
||||
"${SPARKLE_VERSIONS_DIR}/Updater.app" 2>/dev/null || true
|
||||
codesign --force --sign - --timestamp=none \
|
||||
"${SPARKLE_VERSIONS_DIR}/Autoupdate" 2>/dev/null || true
|
||||
codesign --force --sign - --timestamp=none \
|
||||
"${FRAMEWORKS_DIR}/Sparkle.framework"
|
||||
codesign --force --deep --sign - "${APP_DIR}"
|
||||
|
||||
echo "[build-app] kill old app"
|
||||
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
# 发新版的一条龙脚本。
|
||||
#
|
||||
# 用法:
|
||||
# ./scripts/release.sh <version> [<build_number>] [<release_notes_file>]
|
||||
#
|
||||
# 例:
|
||||
# ./scripts/release.sh 1.0.02 A1004 docs/release-notes/1.0.02.md
|
||||
# ./scripts/release.sh 1.0.02
|
||||
#
|
||||
# 流程:
|
||||
# 1. 改 build-app.sh 里的 APP_VERSION / BUILD_NUMBER
|
||||
# 2. 跑 build-app.sh 出 .app
|
||||
# 3. ditto 打包成 OpenLess-<version>.zip
|
||||
# 4. sign_update zip → 拿 EdDSA 签名 + 长度
|
||||
# 5. 在 appcast.xml 里追加新 <item>
|
||||
# 6. git commit appcast.xml + build-app.sh
|
||||
# 7. git tag v<version> + push main + push tag
|
||||
# 8. gh release create + 上传 zip
|
||||
#
|
||||
# 依赖:
|
||||
# - /tmp/sparkle-tools/bin/sign_update(首次会自动从 Sparkle release 下载)
|
||||
# - gh CLI 已登录
|
||||
# - Keychain 里有 Sparkle EdDSA 私钥(已生成一次性)
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
VERSION="${1:?用法: ./scripts/release.sh <version> [<build_number>] [<release_notes_file>]}"
|
||||
BUILD="${2:-}"
|
||||
NOTES_FILE="${3:-}"
|
||||
|
||||
if [ -z "${BUILD}" ]; then
|
||||
# 默认把 build number 设为 version 的简单变形(去掉点)。用户可显式覆盖。
|
||||
BUILD="$(echo "${VERSION}" | tr -d '.')"
|
||||
fi
|
||||
|
||||
APP_NAME="OpenLess"
|
||||
APP_DIR="build/${APP_NAME}.app"
|
||||
ZIP_NAME="${APP_NAME}-${VERSION}.zip"
|
||||
ZIP_PATH="build/${ZIP_NAME}"
|
||||
APPCAST="appcast.xml"
|
||||
SPARKLE_TOOLS_DIR="/tmp/sparkle-tools"
|
||||
SIGN_UPDATE="${SPARKLE_TOOLS_DIR}/bin/sign_update"
|
||||
SPARKLE_VERSION="2.9.1"
|
||||
|
||||
# 1. 确保 sign_update 工具就位
|
||||
if [ ! -x "${SIGN_UPDATE}" ]; then
|
||||
echo "[release] 下载 Sparkle ${SPARKLE_VERSION} 工具..."
|
||||
mkdir -p "${SPARKLE_TOOLS_DIR}"
|
||||
(cd "${SPARKLE_TOOLS_DIR}" && \
|
||||
gh release download "${SPARKLE_VERSION}" -R sparkle-project/Sparkle \
|
||||
-p "Sparkle-${SPARKLE_VERSION}.tar.xz" --skip-existing && \
|
||||
tar -xJf "Sparkle-${SPARKLE_VERSION}.tar.xz")
|
||||
fi
|
||||
|
||||
# 2. 改 build-app.sh 里的版本
|
||||
echo "[release] 写入版本号 ${VERSION} / ${BUILD}"
|
||||
/usr/bin/sed -i '' "s/^APP_VERSION=.*/APP_VERSION=\"${VERSION}\"/" scripts/build-app.sh
|
||||
/usr/bin/sed -i '' "s/^BUILD_NUMBER=.*/BUILD_NUMBER=\"${BUILD}\"/" scripts/build-app.sh
|
||||
|
||||
# 3. 构建 .app(不重置 TCC,避免本机权限被清掉)
|
||||
echo "[release] 构建 .app"
|
||||
RESET_TCC=0 ./scripts/build-app.sh > /dev/null
|
||||
|
||||
# 4. 打包 zip
|
||||
rm -f "${ZIP_PATH}"
|
||||
ditto -c -k --keepParent "${APP_DIR}" "${ZIP_PATH}"
|
||||
ZIP_SIZE=$(/usr/bin/stat -f%z "${ZIP_PATH}")
|
||||
echo "[release] zip = ${ZIP_PATH} (${ZIP_SIZE} bytes)"
|
||||
|
||||
# 5. 用 Keychain 里的 EdDSA 私钥签 zip
|
||||
SIGN_OUTPUT=$("${SIGN_UPDATE}" "${ZIP_PATH}")
|
||||
# 输出形如:sparkle:edSignature="xxxx" length="1234567"
|
||||
EDSIG=$(echo "${SIGN_OUTPUT}" | /usr/bin/sed -n 's/.*sparkle:edSignature="\([^"]*\)".*/\1/p')
|
||||
[ -n "${EDSIG}" ] || { echo "签名失败,sign_update 输出:${SIGN_OUTPUT}"; exit 1; }
|
||||
echo "[release] EdDSA 签名 OK"
|
||||
|
||||
# 6. 准备 release notes(HTML 片段)
|
||||
if [ -n "${NOTES_FILE}" ] && [ -f "${NOTES_FILE}" ]; then
|
||||
NOTES_HTML="<![CDATA[$(cat "${NOTES_FILE}")]]>"
|
||||
else
|
||||
NOTES_HTML="<![CDATA[<p>OpenLess ${VERSION}</p>]]>"
|
||||
fi
|
||||
|
||||
PUB_DATE=$(/bin/date -u +"%a, %d %b %Y %H:%M:%S +0000")
|
||||
DOWNLOAD_URL="https://github.com/appergb/openless/releases/download/v${VERSION}/${ZIP_NAME}"
|
||||
|
||||
# 7. 在 appcast.xml 的 <channel> 顶部插入新 <item>
|
||||
NEW_ITEM=$(cat <<EOF
|
||||
<item>
|
||||
<title>OpenLess ${VERSION}</title>
|
||||
<pubDate>${PUB_DATE}</pubDate>
|
||||
<sparkle:version>${BUILD}</sparkle:version>
|
||||
<sparkle:shortVersionString>${VERSION}</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description>${NOTES_HTML}</description>
|
||||
<enclosure
|
||||
url="${DOWNLOAD_URL}"
|
||||
length="${ZIP_SIZE}"
|
||||
type="application/octet-stream"
|
||||
sparkle:edSignature="${EDSIG}" />
|
||||
</item>
|
||||
EOF
|
||||
)
|
||||
|
||||
# 用 awk 在 <description>OpenLess update feed</description> 之后插入新 item
|
||||
TMP_APPCAST=$(/usr/bin/mktemp)
|
||||
/usr/bin/awk -v new_item="${NEW_ITEM}" '
|
||||
/<description>OpenLess update feed<\/description>/ {
|
||||
print
|
||||
print ""
|
||||
print new_item
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' "${APPCAST}" > "${TMP_APPCAST}"
|
||||
mv "${TMP_APPCAST}" "${APPCAST}"
|
||||
echo "[release] appcast.xml 已追加 ${VERSION}"
|
||||
|
||||
# 8. git commit + tag + push
|
||||
git add scripts/build-app.sh "${APPCAST}"
|
||||
git commit -m "release: v${VERSION} (build ${BUILD})"
|
||||
git tag "v${VERSION}"
|
||||
git push origin main
|
||||
git push origin "v${VERSION}"
|
||||
|
||||
# 9. gh release create + 上传 zip
|
||||
gh release create "v${VERSION}" "${ZIP_PATH}" \
|
||||
--title "OpenLess ${VERSION}" \
|
||||
--notes-file "${NOTES_FILE:-/dev/stdin}" <<EOF
|
||||
OpenLess ${VERSION} (build ${BUILD}).
|
||||
|
||||
老用户:app 启动后自动检查更新;首次安装见 README。
|
||||
EOF
|
||||
|
||||
echo
|
||||
echo "[release] 完成 ✅"
|
||||
echo " Tag: v${VERSION}"
|
||||
echo " Release: https://github.com/appergb/openless/releases/tag/v${VERSION}"
|
||||
echo " Zip: ${ZIP_PATH} (${ZIP_SIZE} bytes)"
|
||||
echo " Sig: ${EDSIG}"
|
||||
Reference in new issue
Block a user