The nightly build is now a distinct app called "cmux NIGHTLY" with bundle ID com.cmuxterm.app.nightly, allowing side-by-side installation with the stable release. The nightly appcast URL is baked into the app's Info.plist by CI, so no in-app channel switching is needed. - Nightly workflow: rename app to "cmux NIGHTLY", set bundle ID to com.cmuxterm.app.nightly, hardcode nightly Sparkle feed URL, publish DMG as cmux-nightly-macos.dmg - Remove "Receive Nightly Builds" toggle from settings - Remove UpdateChannelSettings enum and simplify feed URL resolution to just use SUFeedURL from Info.plist - Remove UpdateChannelSettingsTests (no longer applicable)
97 lines
4.1 KiB
Swift
97 lines
4.1 KiB
Swift
import Sparkle
|
|
import Cocoa
|
|
|
|
extension UpdateDriver: SPUUpdaterDelegate {
|
|
func feedURLString(for updater: SPUUpdater) -> String? {
|
|
#if DEBUG
|
|
let env = ProcessInfo.processInfo.environment
|
|
if let override = env["CMUX_UI_TEST_FEED_URL"], !override.isEmpty {
|
|
UpdateTestURLProtocol.registerIfNeeded()
|
|
recordFeedURLString(override, usedFallback: false)
|
|
return override
|
|
}
|
|
#endif
|
|
// The feed URL is baked into Info.plist at build time:
|
|
// - Stable releases use the stable appcast URL
|
|
// - cmux NIGHTLY has the nightly appcast URL injected by CI
|
|
let feedURL = Bundle.main.object(forInfoDictionaryKey: "SUFeedURL") as? String
|
|
let isNightly = feedURL?.contains("/nightly/") == true
|
|
UpdateLogStore.shared.append("update channel: \(isNightly ? "nightly" : "stable")")
|
|
let usedFallback = feedURL == nil || feedURL?.isEmpty == true
|
|
recordFeedURLString(feedURL ?? "https://github.com/manaflow-ai/cmux/releases/latest/download/appcast.xml", usedFallback: usedFallback)
|
|
return feedURL
|
|
}
|
|
|
|
/// Called when an update is scheduled to install silently,
|
|
/// which occurs when automatic download is enabled.
|
|
func updater(_ updater: SPUUpdater, willInstallUpdateOnQuit item: SUAppcastItem, immediateInstallationBlock immediateInstallHandler: @escaping () -> Void) -> Bool {
|
|
viewModel.state = .installing(.init(
|
|
isAutoUpdate: true,
|
|
retryTerminatingApplication: immediateInstallHandler,
|
|
dismiss: { [weak viewModel] in
|
|
viewModel?.state = .idle
|
|
}
|
|
))
|
|
return true
|
|
}
|
|
|
|
func updater(_ updater: SPUUpdater, didFinishLoading appcast: SUAppcast) {
|
|
let count = appcast.items.count
|
|
let firstVersion = appcast.items.first?.displayVersionString ?? ""
|
|
if firstVersion.isEmpty {
|
|
UpdateLogStore.shared.append("appcast loaded (items=\(count))")
|
|
} else {
|
|
UpdateLogStore.shared.append("appcast loaded (items=\(count), first=\(firstVersion))")
|
|
}
|
|
}
|
|
|
|
func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) {
|
|
let version = item.displayVersionString
|
|
let fileURL = item.fileURL?.absoluteString ?? ""
|
|
if fileURL.isEmpty {
|
|
UpdateLogStore.shared.append("valid update found: \(version)")
|
|
} else {
|
|
UpdateLogStore.shared.append("valid update found: \(version) (\(fileURL))")
|
|
}
|
|
}
|
|
|
|
func updaterDidNotFindUpdate(_ updater: SPUUpdater, error: Error) {
|
|
let nsError = error as NSError
|
|
let reasonValue = (nsError.userInfo[SPUNoUpdateFoundReasonKey] as? NSNumber)?.intValue
|
|
let reason = reasonValue.map { SPUNoUpdateFoundReason(rawValue: OSStatus($0)) } ?? nil
|
|
let reasonText = reason.map(describeNoUpdateFoundReason) ?? "unknown"
|
|
let userInitiated = (nsError.userInfo[SPUNoUpdateFoundUserInitiatedKey] as? NSNumber)?.boolValue ?? false
|
|
let latestItem = nsError.userInfo[SPULatestAppcastItemFoundKey] as? SUAppcastItem
|
|
let latestVersion = latestItem?.displayVersionString ?? ""
|
|
if latestVersion.isEmpty {
|
|
UpdateLogStore.shared.append("no update found (reason=\(reasonText), userInitiated=\(userInitiated))")
|
|
} else {
|
|
UpdateLogStore.shared.append("no update found (reason=\(reasonText), userInitiated=\(userInitiated), latest=\(latestVersion))")
|
|
}
|
|
}
|
|
|
|
func updaterWillRelaunchApplication(_ updater: SPUUpdater) {
|
|
TerminalController.shared.stop()
|
|
NSApp.invalidateRestorableState()
|
|
for window in NSApp.windows {
|
|
window.invalidateRestorableState()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func describeNoUpdateFoundReason(_ reason: SPUNoUpdateFoundReason) -> String {
|
|
switch reason {
|
|
case .unknown:
|
|
return "unknown"
|
|
case .onLatestVersion:
|
|
return "onLatestVersion"
|
|
case .onNewerThanLatestVersion:
|
|
return "onNewerThanLatestVersion"
|
|
case .systemIsTooOld:
|
|
return "systemIsTooOld"
|
|
case .systemIsTooNew:
|
|
return "systemIsTooNew"
|
|
@unknown default:
|
|
return "unknown"
|
|
}
|
|
}
|