Auto-update server-only Macs and replace Sparkle's update UI
This commit is contained in:
@@ -22,6 +22,8 @@
|
||||
<string>350+qbxFCpm+LhbRlCxtBHST+FTz6o19jhGOSs8D0XU=</string>
|
||||
<key>SUEnableAutomaticChecks</key>
|
||||
<true/>
|
||||
<key>SUAutomaticallyUpdate</key>
|
||||
<true/>
|
||||
<key>NSAppleEventsUsageDescription</key>
|
||||
<string>byre's agents drive Orion on this Mac through AppleScript when a task asks them to.</string>
|
||||
</dict>
|
||||
|
||||
@@ -32,5 +32,13 @@ let package = Package(
|
||||
swiftSettings: [.strictMemorySafety()],
|
||||
linkerSettings: [.unsafeFlags(["-Xlinker", "-rpath", "-Xlinker", "@executable_path/../Frameworks"])]
|
||||
),
|
||||
.executableTarget(
|
||||
name: "byre-update",
|
||||
dependencies: [
|
||||
.product(name: "Sparkle", package: "Sparkle"),
|
||||
],
|
||||
swiftSettings: [.strictMemorySafety()],
|
||||
linkerSettings: [.unsafeFlags(["-Xlinker", "-rpath", "-Xlinker", "@executable_path/../Frameworks"])]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1,16 +1,108 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
import Sparkle
|
||||
|
||||
@MainActor
|
||||
final class Updater {
|
||||
final class Updater: NSObject, SPUUserDriver, SPUUpdaterDelegate {
|
||||
static let shared = Updater()
|
||||
private lazy var controller = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil)
|
||||
private var updater: SPUUpdater?
|
||||
private var now = false
|
||||
private var staged: (item: SUAppcastItem, install: () -> Void)?
|
||||
|
||||
func start() {
|
||||
_ = controller
|
||||
let updater = SPUUpdater(hostBundle: .main, applicationBundle: .main, userDriver: self, delegate: self)
|
||||
self.updater = updater
|
||||
do {
|
||||
try updater.start()
|
||||
} catch {
|
||||
tell("Updates are off", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func check() {
|
||||
controller.checkForUpdates(nil)
|
||||
if let staged {
|
||||
if ask(staged.item) {
|
||||
staged.install()
|
||||
}
|
||||
return
|
||||
}
|
||||
updater?.checkForUpdates()
|
||||
}
|
||||
|
||||
private func ask(_ item: SUAppcastItem) -> Bool {
|
||||
let current = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Byre \(item.displayVersionString) is available"
|
||||
alert.informativeText = "You have \(current)."
|
||||
alert.addButton(withTitle: "Update Now")
|
||||
alert.addButton(withTitle: "Update on Quit")
|
||||
NSApp.activate()
|
||||
return alert.runModal() == .alertFirstButtonReturn
|
||||
}
|
||||
|
||||
private func tell(_ message: String, _ info: String) {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = message
|
||||
alert.informativeText = info
|
||||
NSApp.activate()
|
||||
alert.runModal()
|
||||
}
|
||||
|
||||
func updater(_ updater: SPUUpdater, willInstallUpdateOnQuit item: SUAppcastItem, immediateInstallationBlock immediateInstallHandler: @escaping () -> Void) -> Bool {
|
||||
staged = (item, immediateInstallHandler)
|
||||
if ask(item) {
|
||||
immediateInstallHandler()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func show(_ request: SPUUpdatePermissionRequest, reply: @escaping (SUUpdatePermissionResponse) -> Void) {
|
||||
reply(SUUpdatePermissionResponse(automaticUpdateChecks: true, sendSystemProfile: false))
|
||||
}
|
||||
|
||||
func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) {}
|
||||
|
||||
func showUpdateFound(with appcastItem: SUAppcastItem, state: SPUUserUpdateState, reply: @escaping (SPUUserUpdateChoice) -> Void) {
|
||||
now = ask(appcastItem)
|
||||
if !now && state.stage == .installing {
|
||||
reply(.dismiss)
|
||||
return
|
||||
}
|
||||
reply(.install)
|
||||
}
|
||||
|
||||
func showUpdateReleaseNotes(with downloadData: SPUDownloadData) {}
|
||||
|
||||
func showUpdateReleaseNotesFailedToDownloadWithError(_ error: any Error) {}
|
||||
|
||||
func showUpdateNotFoundWithError(_ error: any Error, acknowledgement: @escaping () -> Void) {
|
||||
tell(error.localizedDescription, (error as NSError).localizedRecoverySuggestion ?? "")
|
||||
acknowledgement()
|
||||
}
|
||||
|
||||
func showUpdaterError(_ error: any Error, acknowledgement: @escaping () -> Void) {
|
||||
tell("Update failed", error.localizedDescription)
|
||||
acknowledgement()
|
||||
}
|
||||
|
||||
func showDownloadInitiated(cancellation: @escaping () -> Void) {}
|
||||
|
||||
func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) {}
|
||||
|
||||
func showDownloadDidReceiveData(ofLength length: UInt64) {}
|
||||
|
||||
func showDownloadDidStartExtractingUpdate() {}
|
||||
|
||||
func showExtractionReceivedProgress(_ progress: Double) {}
|
||||
|
||||
func showReady(toInstallAndRelaunch reply: @escaping (SPUUserUpdateChoice) -> Void) {
|
||||
reply(now ? .install : .dismiss)
|
||||
}
|
||||
|
||||
func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) {}
|
||||
|
||||
func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) {
|
||||
acknowledgement()
|
||||
}
|
||||
|
||||
func dismissUpdateInstallation() {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import AppKit
|
||||
import Sparkle
|
||||
|
||||
func log(_ message: String) {
|
||||
FileHandle.standardError.write(Data((message + "\n").utf8))
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class Driver: NSObject, SPUUserDriver {
|
||||
func show(_ request: SPUUpdatePermissionRequest, reply: @escaping (SUUpdatePermissionResponse) -> Void) {
|
||||
reply(SUUpdatePermissionResponse(automaticUpdateChecks: true, sendSystemProfile: false))
|
||||
}
|
||||
|
||||
func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) {}
|
||||
|
||||
func showUpdateFound(with appcastItem: SUAppcastItem, state: SPUUserUpdateState, reply: @escaping (SPUUserUpdateChoice) -> Void) {
|
||||
log("installing \(appcastItem.displayVersionString) (\(appcastItem.versionString))")
|
||||
reply(.install)
|
||||
}
|
||||
|
||||
func showUpdateReleaseNotes(with downloadData: SPUDownloadData) {}
|
||||
|
||||
func showUpdateReleaseNotesFailedToDownloadWithError(_ error: any Error) {}
|
||||
|
||||
func showUpdateNotFoundWithError(_ error: any Error, acknowledgement: @escaping () -> Void) {
|
||||
log("no update: \(error.localizedDescription)")
|
||||
exit(0)
|
||||
}
|
||||
|
||||
func showUpdaterError(_ error: any Error, acknowledgement: @escaping () -> Void) {
|
||||
log("update failed: \(error.localizedDescription)")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
func showDownloadInitiated(cancellation: @escaping () -> Void) {}
|
||||
|
||||
func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) {}
|
||||
|
||||
func showDownloadDidReceiveData(ofLength length: UInt64) {}
|
||||
|
||||
func showDownloadDidStartExtractingUpdate() {}
|
||||
|
||||
func showExtractionReceivedProgress(_ progress: Double) {}
|
||||
|
||||
func showReady(toInstallAndRelaunch reply: @escaping (SPUUserUpdateChoice) -> Void) {
|
||||
reply(.install)
|
||||
}
|
||||
|
||||
func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) {
|
||||
log("installing")
|
||||
}
|
||||
|
||||
func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) {
|
||||
log("installed")
|
||||
exit(0)
|
||||
}
|
||||
|
||||
func dismissUpdateInstallation() {
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
let app = Bundle.main.executableURL!.resolvingSymlinksInPath().deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
|
||||
if NSWorkspace.shared.runningApplications.contains(where: { $0.bundleURL?.standardizedFileURL == app.standardizedFileURL }) {
|
||||
log("\(app.path) is running; its own updater handles updates")
|
||||
exit(0)
|
||||
}
|
||||
let bundle = Bundle(url: app)!
|
||||
let driver = Driver()
|
||||
let updater = SPUUpdater(hostBundle: bundle, applicationBundle: bundle, userDriver: driver, delegate: nil)
|
||||
do {
|
||||
try updater.start()
|
||||
} catch {
|
||||
log("start updater: \(error.localizedDescription)")
|
||||
exit(1)
|
||||
}
|
||||
updater.checkForUpdates()
|
||||
RunLoop.main.run()
|
||||
+4
-1
@@ -19,9 +19,10 @@ swift build --package-path "$2/mac" --configuration release --no-color-diagnosti
|
||||
swift build --package-path "$2/hands" --configuration release --no-color-diagnostics
|
||||
cp "$2/mac/.build/release/Byre" "$3"
|
||||
cp "$2/hands/.build/release/byre-hands" "$4"
|
||||
cp "$2/mac/.build/release/byre-update" "$6"
|
||||
rm -rf "$5"
|
||||
ditto "$2/mac/.build/release/Sparkle.framework" "$5"
|
||||
' sh "$root" "$shared" "$build/Byre" "$build/byre-hands" "$build/Sparkle.framework"
|
||||
' sh "$root" "$shared" "$build/Byre" "$build/byre-hands" "$build/Sparkle.framework" "$build/byre-update"
|
||||
go -C "$root" build -trimpath -o "$build/server/byre" ./cmd/byre
|
||||
if [ ! "$icns" -nt "$svg" ]; then
|
||||
iconset=$build/byre.iconset
|
||||
@@ -40,6 +41,7 @@ mkdir -p "$app/Contents/MacOS" "$app/Contents/Helpers" "$app/Contents/Frameworks
|
||||
cp "$build/Byre" "$app/Contents/MacOS/Byre"
|
||||
cp "$build/server/byre" "$app/Contents/Helpers/byre"
|
||||
cp "$build/byre-hands" "$app/Contents/Helpers/byre-hands"
|
||||
cp "$build/byre-update" "$app/Contents/Helpers/byre-update"
|
||||
ditto "$build/Sparkle.framework" "$app/Contents/Frameworks/Sparkle.framework"
|
||||
cp "$mac/Info.plist" "$app/Contents/Info.plist"
|
||||
plutil -insert ByreCommit -string "$(git -C "$root" rev-parse HEAD)" "$app/Contents/Info.plist"
|
||||
@@ -58,5 +60,6 @@ sign --identifier run.fc.byre --entitlements "$mac/automation.entitlements" "$ap
|
||||
sign "$sparkle/Versions/B/Autoupdate"
|
||||
sign "$sparkle/Versions/B/Updater.app"
|
||||
sign "$sparkle"
|
||||
sign --identifier run.fc.byre.update "$app/Contents/Helpers/byre-update"
|
||||
sign --entitlements "$mac/byre.entitlements" "$app"
|
||||
echo "$app"
|
||||
|
||||
Reference in New Issue
Block a user