mirror of
https://github.com/BigMarc/grammargem.git
synced 2026-10-13 17:01:55 +02:00
Add Sparkle auto-update (appcast on grammagem.app)
Installed apps now show an 'Update available' button (menu bar) and can one-click install signed updates. Adds the Sparkle dependency, an UpdaterManager wired into AppState, the menu-bar banner + 'Check for Updates', and Info.plist SUFeedURL (https://grammagem.app/appcast.xml), SUPublicEDKey, and scheduled background checks. Build / release: - build.sh embeds + rpaths Sparkle.framework; sign-app.sh signs it inside-out (XPC services, Updater.app, Autoupdate, framework, then app) for a valid, notarizable signature. - release.sh stamps the version, builds, signs, notarizes, staples, zips, and emits the EdDSA-signed appcast.xml to upload to grammagem.app. - A hidden --update-selftest checks a feed headlessly (CI/build pipeline). Verified: a v0.1.0 build detects a v0.2.0 update against a local signed appcast (UPDATE_FOUND: 0.2.0); the signed app passes codesign --strict. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8a9ffb55af
commit
e8b7dff65d
@ -43,5 +43,19 @@
|
|||||||
<string>© 2026 FounderGem LLC. Built in public.</string>
|
<string>© 2026 FounderGem LLC. Built in public.</string>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
|
|
||||||
|
<!-- Sparkle auto-update. Appcast is served from grammagem.app and updates are
|
||||||
|
EdDSA-signed with the key whose public half is below (private key lives in
|
||||||
|
the release machine's keychain; sign with scripts/release.sh). -->
|
||||||
|
<key>SUFeedURL</key>
|
||||||
|
<string>https://grammagem.app/appcast.xml</string>
|
||||||
|
<key>SUPublicEDKey</key>
|
||||||
|
<string>IwYch+DRgNk3gmCwe86z+SIc4sRdyDf4Z6xGpS+JXDc=</string>
|
||||||
|
<key>SUEnableAutomaticChecks</key>
|
||||||
|
<true/>
|
||||||
|
<key>SUScheduledCheckInterval</key>
|
||||||
|
<integer>86400</integer>
|
||||||
|
<key>SUAutomaticallyUpdate</key>
|
||||||
|
<false/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "1aab35282a23815fc7c2ac7484414660e78b19f68cfbf14f87004cf55bb3ac6b",
|
"originHash" : "f78005d8f2f9a6a31d5c42cc7ce2b46907eb92fce74810060a58056e592ea3a1",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "gzipswift",
|
"identity" : "gzipswift",
|
||||||
@ -28,6 +28,15 @@
|
|||||||
"version" : "2.25.9"
|
"version" : "2.25.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sparkle",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/sparkle-project/Sparkle",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d46d456107feacc80711b21847b82b07bd9fb46e",
|
||||||
|
"version" : "2.9.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-collections",
|
"identity" : "swift-collections",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@ -29,6 +29,8 @@ let package = Package(
|
|||||||
// On-device LLM runtime. Pinned to an exact tag (the LLM libraries still
|
// On-device LLM runtime. Pinned to an exact tag (the LLM libraries still
|
||||||
// ship from this repo at 2.25.9; the package name is "mlx-libraries").
|
// ship from this repo at 2.25.9; the package name is "mlx-libraries").
|
||||||
.package(url: "https://github.com/ml-explore/mlx-swift-examples.git", exact: "2.25.9"),
|
.package(url: "https://github.com/ml-explore/mlx-swift-examples.git", exact: "2.25.9"),
|
||||||
|
// Secure auto-updates (EdDSA-signed appcast served from grammagem.app).
|
||||||
|
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.6.0"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
// C shim exposing libharper_ffi's C ABI (harper-ffi/include/harper.h) to Swift.
|
// C shim exposing libharper_ffi's C ABI (harper-ffi/include/harper.h) to Swift.
|
||||||
@ -39,6 +41,7 @@ let package = Package(
|
|||||||
"CHarper",
|
"CHarper",
|
||||||
.product(name: "MLXLLM", package: "mlx-swift-examples"),
|
.product(name: "MLXLLM", package: "mlx-swift-examples"),
|
||||||
.product(name: "MLXLMCommon", package: "mlx-swift-examples"),
|
.product(name: "MLXLMCommon", package: "mlx-swift-examples"),
|
||||||
|
.product(name: "Sparkle", package: "Sparkle"),
|
||||||
],
|
],
|
||||||
path: "Sources/GrammaGem",
|
path: "Sources/GrammaGem",
|
||||||
linkerSettings: [
|
linkerSettings: [
|
||||||
|
|||||||
@ -27,6 +27,7 @@ final class AppState: ObservableObject {
|
|||||||
let snippets: SnippetStore
|
let snippets: SnippetStore
|
||||||
let exclusions: Exclusions
|
let exclusions: Exclusions
|
||||||
let liveMonitor: LiveMonitor
|
let liveMonitor: LiveMonitor
|
||||||
|
let updater: UpdaterManager
|
||||||
private let capture: TextCapture
|
private let capture: TextCapture
|
||||||
let coordinator: TextReplacementCoordinator
|
let coordinator: TextReplacementCoordinator
|
||||||
|
|
||||||
@ -65,6 +66,7 @@ final class AppState: ObservableObject {
|
|||||||
let excl = Exclusions()
|
let excl = Exclusions()
|
||||||
exclusions = excl
|
exclusions = excl
|
||||||
liveMonitor = LiveMonitor(grammar: engine, detector: detector, exclusions: excl, capture: capture)
|
liveMonitor = LiveMonitor(grammar: engine, detector: detector, exclusions: excl, capture: capture)
|
||||||
|
updater = UpdaterManager()
|
||||||
coordinator = TextReplacementCoordinator(
|
coordinator = TextReplacementCoordinator(
|
||||||
capture: capture, grammar: engine, ai: ai, gate: gate, detector: detector)
|
capture: capture, grammar: engine, ai: ai, gate: gate, detector: detector)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,11 @@ enum GrammaGemMain {
|
|||||||
MLXSelfTest.run()
|
MLXSelfTest.run()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if let i = CommandLine.arguments.firstIndex(of: "--update-selftest") {
|
||||||
|
let feed = i + 1 < CommandLine.arguments.count ? CommandLine.arguments[i + 1] : ""
|
||||||
|
UpdaterSelfTest.run(feedURL: feed)
|
||||||
|
return
|
||||||
|
}
|
||||||
GrammaGemApp.main()
|
GrammaGemApp.main()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,16 +24,22 @@ struct MenuBarLabel: View {
|
|||||||
struct MenuBarContent: View {
|
struct MenuBarContent: View {
|
||||||
@EnvironmentObject private var app: AppState
|
@EnvironmentObject private var app: AppState
|
||||||
@ObservedObject private var monitor: LiveMonitor
|
@ObservedObject private var monitor: LiveMonitor
|
||||||
|
@ObservedObject private var updater: UpdaterManager
|
||||||
@Environment(\.openSettings) private var openSettings
|
@Environment(\.openSettings) private var openSettings
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
_monitor = ObservedObject(wrappedValue: AppState.shared.liveMonitor)
|
_monitor = ObservedObject(wrappedValue: AppState.shared.liveMonitor)
|
||||||
|
_updater = ObservedObject(wrappedValue: AppState.shared.updater)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
header
|
header
|
||||||
|
|
||||||
|
if let version = updater.updateAvailableVersion {
|
||||||
|
updateBanner(version: version)
|
||||||
|
}
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
liveStatus
|
liveStatus
|
||||||
@ -61,6 +67,7 @@ struct MenuBarContent: View {
|
|||||||
actionRow(title: "Open GrammaGem…", shortcut: nil) { app.showMainWindow() }
|
actionRow(title: "Open GrammaGem…", shortcut: nil) { app.showMainWindow() }
|
||||||
actionRow(title: "Manage devices…", shortcut: nil) { app.showMainWindow(select: .devices) }
|
actionRow(title: "Manage devices…", shortcut: nil) { app.showMainWindow(select: .devices) }
|
||||||
actionRow(title: "Page blocker…", shortcut: nil) { app.showMainWindow(select: .exclusions) }
|
actionRow(title: "Page blocker…", shortcut: nil) { app.showMainWindow(select: .exclusions) }
|
||||||
|
actionRow(title: "Check for Updates…", shortcut: nil) { app.updater.checkForUpdates() }
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
@ -83,6 +90,22 @@ struct MenuBarContent: View {
|
|||||||
.frame(width: 290)
|
.frame(width: 290)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shown when Sparkle has found a newer signed build on grammagem.app.
|
||||||
|
private func updateBanner(version: String) -> some View {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: "arrow.down.circle.fill").foregroundStyle(GG.emerald)
|
||||||
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
|
Text("Update available").font(.callout.weight(.semibold))
|
||||||
|
Text("Version \(version) is ready to install").font(.caption).foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Button("Update") { app.updater.checkForUpdates() }
|
||||||
|
.buttonStyle(.borderedProminent).controlSize(.small)
|
||||||
|
}
|
||||||
|
.padding(8)
|
||||||
|
.background(GG.emerald.opacity(0.10), in: RoundedRectangle(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
|
||||||
private var header: some View {
|
private var header: some View {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Image(systemName: "pencil.and.scribble").foregroundStyle(.tint)
|
Image(systemName: "pencil.and.scribble").foregroundStyle(.tint)
|
||||||
|
|||||||
81
Sources/GrammaGem/System/UpdaterManager.swift
Normal file
81
Sources/GrammaGem/System/UpdaterManager.swift
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import Foundation
|
||||||
|
import Sparkle
|
||||||
|
|
||||||
|
/// Wraps Sparkle's updater. It background-checks the appcast on grammagem.app and
|
||||||
|
/// publishes `updateAvailableVersion` when a newer, EdDSA-verified build exists, so
|
||||||
|
/// the menu bar can show an "Update available" button. Triggering an update runs
|
||||||
|
/// Sparkle's verified download + install flow.
|
||||||
|
///
|
||||||
|
/// The updater is only started inside the packaged `.app` (where the Info.plist
|
||||||
|
/// carries `SUFeedURL` + `SUPublicEDKey`); under `swift run`, tests, or the
|
||||||
|
/// self-test binaries it stays inert so nothing crashes.
|
||||||
|
@MainActor
|
||||||
|
final class UpdaterManager: NSObject, ObservableObject {
|
||||||
|
@Published private(set) var updateAvailableVersion: String?
|
||||||
|
|
||||||
|
private var controller: SPUStandardUpdaterController?
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
guard Bundle.main.object(forInfoDictionaryKey: "SUFeedURL") != nil else {
|
||||||
|
return // not the packaged app — no updater
|
||||||
|
}
|
||||||
|
controller = SPUStandardUpdaterController(
|
||||||
|
startingUpdater: true, updaterDelegate: self, userDriverDelegate: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var canCheckForUpdates: Bool { controller?.updater.canCheckForUpdates ?? false }
|
||||||
|
|
||||||
|
/// User-initiated check — shows Sparkle's UI (used by the menu item and the
|
||||||
|
/// "Update" button on the availability banner).
|
||||||
|
func checkForUpdates() {
|
||||||
|
controller?.updater.checkForUpdates()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UpdaterManager: SPUUpdaterDelegate {
|
||||||
|
func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) {
|
||||||
|
updateAvailableVersion = item.displayVersionString
|
||||||
|
}
|
||||||
|
|
||||||
|
func updaterDidNotFindUpdate(_ updater: SPUUpdater) {
|
||||||
|
updateAvailableVersion = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Headless update self-test (build pipeline / CI)
|
||||||
|
|
||||||
|
/// Checks `feedURL` for a newer version with no UI and prints the result, then
|
||||||
|
/// exits. Used to verify the appcast + signature + version-comparison path.
|
||||||
|
enum UpdaterSelfTest {
|
||||||
|
static func run(feedURL: String) {
|
||||||
|
let delegate = UpdateProbeDelegate(feed: feedURL)
|
||||||
|
UpdateProbeDelegate.retained = delegate
|
||||||
|
UpdateProbeDelegate.controller = SPUStandardUpdaterController(
|
||||||
|
startingUpdater: true, updaterDelegate: delegate, userDriverDelegate: nil)
|
||||||
|
UpdateProbeDelegate.controller?.updater.checkForUpdateInformation()
|
||||||
|
RunLoop.main.run() // Sparkle's network check needs the run loop; delegate exits.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class UpdateProbeDelegate: NSObject, SPUUpdaterDelegate {
|
||||||
|
static var controller: SPUStandardUpdaterController?
|
||||||
|
static var retained: UpdateProbeDelegate?
|
||||||
|
let feed: String
|
||||||
|
init(feed: String) { self.feed = feed; super.init() }
|
||||||
|
|
||||||
|
func feedURLString(for updater: SPUUpdater) -> String? { feed.isEmpty ? nil : feed }
|
||||||
|
|
||||||
|
func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) {
|
||||||
|
print("UPDATE_FOUND: \(item.displayVersionString)")
|
||||||
|
exit(0)
|
||||||
|
}
|
||||||
|
func updaterDidNotFindUpdate(_ updater: SPUUpdater) {
|
||||||
|
print("UPDATE_NONE")
|
||||||
|
exit(0)
|
||||||
|
}
|
||||||
|
func updater(_ updater: SPUUpdater, didAbortWithError error: Error) {
|
||||||
|
print("UPDATE_ERROR: \(error.localizedDescription)")
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -45,6 +45,15 @@ MLX_RES="${DIST}/${APP}/Contents/Resources/mlx-swift_Cmlx.bundle/Contents/Resour
|
|||||||
mkdir -p "${MLX_RES}"
|
mkdir -p "${MLX_RES}"
|
||||||
cp "AppSupport/mlx.metallib" "${MLX_RES}/default.metallib"
|
cp "AppSupport/mlx.metallib" "${MLX_RES}/default.metallib"
|
||||||
|
|
||||||
|
# Embed Sparkle.framework (auto-update) and add an rpath so the executable can
|
||||||
|
# resolve it at @executable_path/../Frameworks. (Signed inside-out by sign-app.sh.)
|
||||||
|
echo "==> Embedding Sparkle.framework"
|
||||||
|
SPARKLE_FW="$(find .build -path '*Sparkle.xcframework/macos-arm64*/Sparkle.framework' -type d | head -1)"
|
||||||
|
mkdir -p "${DIST}/${APP}/Contents/Frameworks"
|
||||||
|
cp -R "${SPARKLE_FW}" "${DIST}/${APP}/Contents/Frameworks/"
|
||||||
|
install_name_tool -add_rpath "@executable_path/../Frameworks" \
|
||||||
|
"${DIST}/${APP}/Contents/MacOS/GrammaGem" 2>/dev/null || true
|
||||||
|
|
||||||
echo "==> Built ${DIST}/${APP}"
|
echo "==> Built ${DIST}/${APP}"
|
||||||
lipo -info "${DIST}/${APP}/Contents/MacOS/GrammaGem"
|
lipo -info "${DIST}/${APP}/Contents/MacOS/GrammaGem"
|
||||||
echo " Run with: open \"${DIST}/${APP}\" (grant Accessibility on first launch)"
|
echo " Run with: open \"${DIST}/${APP}\" (grant Accessibility on first launch)"
|
||||||
|
|||||||
@ -1,32 +1,71 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Package a signed build for distribution and update the Sparkle appcast hosted
|
# Cut a GrammaGem release and publish the Sparkle appcast (hosted on grammagem.app).
|
||||||
# on GitHub Releases (zero hosting cost). Run after sign-notarize.sh.
|
|
||||||
#
|
#
|
||||||
# This is a documented scaffold — wire it to your Sparkle EdDSA key + repo.
|
# ./scripts/release.sh <version> e.g. ./scripts/release.sh 0.2.0
|
||||||
|
#
|
||||||
|
# Steps: stamp version -> build -> sign inside-out (incl. Sparkle) -> notarize +
|
||||||
|
# staple -> zip -> generate the EdDSA-signed appcast.xml. Then upload the .zip and
|
||||||
|
# appcast.xml to grammagem.app so every installed app shows "Update available".
|
||||||
|
#
|
||||||
|
# Notarization needs ONE of:
|
||||||
|
# NOTARY_PROFILE (xcrun notarytool store-credentials)
|
||||||
|
# APPLE_ID + TEAM_ID + NOTARY_PASSWORD (Apple ID + app-specific password)
|
||||||
|
# Set SKIP_NOTARIZE=1 for a dry run (un-notarized build; do not ship).
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
APP="$ROOT/dist/GrammaGem.app"
|
cd "$ROOT"
|
||||||
VERSION="${1:?Usage: release.sh <version> e.g. 0.1.0}"
|
|
||||||
DMG="$ROOT/dist/GrammaGem-$VERSION.dmg"
|
|
||||||
|
|
||||||
echo "▸ Creating DMG for $VERSION…"
|
VERSION="${1:?usage: release.sh <version> e.g. release.sh 0.2.0}"
|
||||||
# hdiutil create -volname "GrammaGem" -srcfolder "$APP" -ov -format UDZO "$DMG" # TODO: enable
|
APP="dist/GrammaGem.app"
|
||||||
echo " (DMG creation stubbed — uncomment hdiutil line)"
|
RELDIR="dist/releases"
|
||||||
|
ZIP="${RELDIR}/GrammaGem-${VERSION}.zip"
|
||||||
|
DL_PREFIX="${DL_PREFIX:-https://grammagem.app/releases/}"
|
||||||
|
PB=/usr/libexec/PlistBuddy
|
||||||
|
|
||||||
cat <<'NOTE'
|
# 1. Stamp the marketing version + bump the monotonic build number.
|
||||||
▸ Sparkle appcast (TODO):
|
CUR_BUILD="$($PB -c 'Print :CFBundleVersion' AppSupport/Info.plist)"
|
||||||
1. Sign the update: ./bin/sign_update dist/GrammaGem-<ver>.dmg
|
NEW_BUILD=$((CUR_BUILD + 1))
|
||||||
(Sparkle's sign_update tool, using your EdDSA private key)
|
$PB -c "Set :CFBundleShortVersionString ${VERSION}" AppSupport/Info.plist
|
||||||
2. Add an <item> to appcast.xml with the version, URL (GitHub Releases asset),
|
$PB -c "Set :CFBundleVersion ${NEW_BUILD}" AppSupport/Info.plist
|
||||||
length, and sparkle:edSignature.
|
echo "==> Releasing ${VERSION} (build ${NEW_BUILD})"
|
||||||
3. Upload the DMG + appcast.xml to a GitHub Release.
|
|
||||||
The app's SUFeedURL points at the raw appcast.xml on GitHub — $0 hosting.
|
|
||||||
|
|
||||||
▸ Distribution touchpoints (all $0 marginal cost):
|
# 2. Build + sign (inside-out so the embedded Sparkle.framework is valid).
|
||||||
- Model weights: Hugging Face CDN
|
./scripts/build.sh
|
||||||
- Updates: GitHub Releases + Sparkle
|
./scripts/sign-app.sh "$APP"
|
||||||
- Licensing: Lemon Squeezy License API (% per sale only)
|
|
||||||
NOTE
|
|
||||||
|
|
||||||
echo "✓ Release steps printed for $VERSION"
|
# 3. Zip.
|
||||||
|
mkdir -p "$RELDIR"
|
||||||
|
rm -f "$ZIP"
|
||||||
|
ditto -c -k --keepParent "$APP" "$ZIP"
|
||||||
|
|
||||||
|
# 4. Notarize + staple (the ticket lets Sparkle install it without Gatekeeper prompts).
|
||||||
|
if [ "${SKIP_NOTARIZE:-0}" != "1" ]; then
|
||||||
|
echo "==> Notarizing"
|
||||||
|
if [ -n "${NOTARY_PROFILE:-}" ]; then
|
||||||
|
xcrun notarytool submit "$ZIP" --keychain-profile "$NOTARY_PROFILE" --wait
|
||||||
|
elif [ -n "${APPLE_ID:-}" ] && [ -n "${TEAM_ID:-}" ] && [ -n "${NOTARY_PASSWORD:-}" ]; then
|
||||||
|
xcrun notarytool submit "$ZIP" \
|
||||||
|
--apple-id "$APPLE_ID" --team-id "$TEAM_ID" --password "$NOTARY_PASSWORD" --wait
|
||||||
|
else
|
||||||
|
echo "ERROR: set NOTARY_PROFILE or APPLE_ID+TEAM_ID+NOTARY_PASSWORD (or SKIP_NOTARIZE=1)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
xcrun stapler staple "$APP"
|
||||||
|
rm -f "$ZIP"; ditto -c -k --keepParent "$APP" "$ZIP" # re-zip the stapled app
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. Generate the EdDSA-signed appcast (keeps every zip in RELDIR in its history).
|
||||||
|
GA="$(find .build/artifacts -name generate_appcast -type f | head -1)"
|
||||||
|
[ -n "$GA" ] || { echo "ERROR: generate_appcast not found (run 'swift package resolve')." >&2; exit 1; }
|
||||||
|
"$GA" --download-url-prefix "$DL_PREFIX" "$RELDIR"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "✓ Release ${VERSION} built. Upload these to grammagem.app:"
|
||||||
|
echo " ${ZIP}"
|
||||||
|
echo " -> ${DL_PREFIX}GrammaGem-${VERSION}.zip"
|
||||||
|
echo " ${RELDIR}/appcast.xml"
|
||||||
|
echo " -> https://grammagem.app/appcast.xml (matches Info.plist SUFeedURL)"
|
||||||
|
echo
|
||||||
|
echo " Once uploaded, installed apps show an 'Update available' button within a day"
|
||||||
|
echo " (or immediately via menu bar -> Check for Updates...)."
|
||||||
|
|||||||
35
scripts/sign-app.sh
Executable file
35
scripts/sign-app.sh
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Code-sign GrammaGem.app with Developer ID + Hardened Runtime, signing the
|
||||||
|
# embedded Sparkle.framework's nested helpers INSIDE-OUT first (required for a
|
||||||
|
# valid, notarizable signature).
|
||||||
|
#
|
||||||
|
# Usage: ./scripts/sign-app.sh [path/to/GrammaGem.app]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
DEV_ID="${DEV_ID:-Developer ID Application: Marc Schultheiss (D5KT5B9Z9M)}"
|
||||||
|
ENTITLEMENTS="AppSupport/GrammaGem.entitlements"
|
||||||
|
APP="${1:-dist/GrammaGem.app}"
|
||||||
|
|
||||||
|
sign() { codesign --force --timestamp --options runtime --sign "$DEV_ID" "$@"; }
|
||||||
|
|
||||||
|
FW="${APP}/Contents/Frameworks/Sparkle.framework"
|
||||||
|
if [ -d "$FW" ]; then
|
||||||
|
echo "==> Signing Sparkle.framework (inside-out)"
|
||||||
|
V="${FW}/Versions/B"
|
||||||
|
for xpc in "$V"/XPCServices/*.xpc; do [ -e "$xpc" ] && sign "$xpc"; done
|
||||||
|
[ -e "$V/Updater.app/Contents/MacOS/Autoupdate" ] && sign "$V/Updater.app/Contents/MacOS/Autoupdate" || true
|
||||||
|
[ -e "$V/Updater.app" ] && sign "$V/Updater.app"
|
||||||
|
[ -e "$V/Autoupdate" ] && sign "$V/Autoupdate"
|
||||||
|
sign "$FW"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Signing ${APP}"
|
||||||
|
codesign --force --timestamp --options runtime \
|
||||||
|
--entitlements "$ENTITLEMENTS" --sign "$DEV_ID" "$APP"
|
||||||
|
|
||||||
|
echo "==> Verifying"
|
||||||
|
codesign --verify --strict --verbose=2 "$APP"
|
||||||
|
echo "✓ signed: $APP"
|
||||||
@ -22,12 +22,8 @@ ENTITLEMENTS="AppSupport/GrammaGem.entitlements"
|
|||||||
echo "==> Building universal bundle"
|
echo "==> Building universal bundle"
|
||||||
./scripts/build.sh >/dev/null
|
./scripts/build.sh >/dev/null
|
||||||
|
|
||||||
echo "==> Codesigning (Hardened Runtime)"
|
echo "==> Codesigning (Hardened Runtime, inside-out incl. Sparkle)"
|
||||||
codesign --force --timestamp --options runtime \
|
DEV_ID="$DEV_ID" ./scripts/sign-app.sh "$APP"
|
||||||
--entitlements "$ENTITLEMENTS" \
|
|
||||||
--sign "$DEV_ID" \
|
|
||||||
"$APP"
|
|
||||||
codesign --verify --strict --verbose=2 "$APP"
|
|
||||||
|
|
||||||
echo "==> Zipping"
|
echo "==> Zipping"
|
||||||
ditto -c -k --keepParent "$APP" "$ZIP"
|
ditto -c -k --keepParent "$APP" "$ZIP"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user