Skip to content

Commit 6a86c64

Browse files
committed
feat: add auto-updates
1 parent 3c72ff4 commit 6a86c64

File tree

7 files changed

+111
-7
lines changed

7 files changed

+111
-7
lines changed

Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import NetworkExtension
33
import os
44
import SDWebImageSVGCoder
55
import SDWebImageSwiftUI
6+
import Sparkle
67
import SwiftUI
78
import UserNotifications
89
import VPNLib
@@ -26,6 +27,7 @@ struct DesktopApp: App {
2627
.environmentObject(appDelegate.vpn)
2728
.environmentObject(appDelegate.state)
2829
.environmentObject(appDelegate.helper)
30+
.environmentObject(appDelegate.autoUpdater)
2931
}
3032
.windowResizability(.contentSize)
3133
Window("Coder File Sync", id: Windows.fileSync.rawValue) {
@@ -47,11 +49,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4749
let urlHandler: URLHandler
4850
let notifDelegate: NotifDelegate
4951
let helper: HelperService
52+
let autoUpdater: UpdaterService
5053

5154
override init() {
5255
notifDelegate = NotifDelegate()
5356
vpn = CoderVPNService()
5457
helper = HelperService()
58+
autoUpdater = UpdaterService()
5559
let state = AppState(onChange: vpn.configureTunnelProviderProtocol)
5660
vpn.onStart = {
5761
// We don't need this to have finished before the VPN actually starts

Coder-Desktop/Coder-Desktop/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,7 @@
3535
<string>Ae2oQLTcx89/a73XrpOt+IVvqdo+fMTjo3UKEm77VdA=</string>
3636
<key>CommitHash</key>
3737
<string>$(GIT_COMMIT_HASH)</string>
38+
<key>SUFeedURL</key>
39+
<string>https://releases.coder.com/coder-desktop/mac/appcast.xml</string>
3840
</dict>
3941
</plist>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import Sparkle
2+
import SwiftUI
3+
4+
final class UpdaterService: NSObject, ObservableObject {
5+
private lazy var inner: SPUStandardUpdaterController = .init(
6+
startingUpdater: true,
7+
updaterDelegate: self,
8+
userDriverDelegate: self,
9+
)
10+
private var updater: SPUUpdater!
11+
@Published var canCheckForUpdates = true
12+
13+
@Published var autoCheckForUpdates: Bool = false {
14+
didSet {
15+
if autoCheckForUpdates != oldValue {
16+
updater.automaticallyChecksForUpdates = autoCheckForUpdates
17+
}
18+
}
19+
}
20+
21+
@Published var updateChannel: UpdateChannel {
22+
didSet {
23+
UserDefaults.standard.set(updateChannel.rawValue, forKey: Self.updateChannelKey)
24+
}
25+
}
26+
27+
static let updateChannelKey = "updateChannel"
28+
29+
override init() {
30+
updateChannel = UserDefaults.standard.string(forKey: Self.updateChannelKey)
31+
.flatMap { UpdateChannel(rawValue: $0) } ?? .stable
32+
super.init()
33+
updater = inner.updater
34+
updater.publisher(for: \.canCheckForUpdates).assign(to: &$canCheckForUpdates)
35+
}
36+
37+
func checkForUpdates() {
38+
guard canCheckForUpdates else { return }
39+
updater.checkForUpdates()
40+
}
41+
}
42+
43+
enum UpdateChannel: String, CaseIterable, Identifiable {
44+
case stable
45+
case preview
46+
47+
var name: String {
48+
switch self {
49+
case .stable:
50+
"Stable"
51+
case .preview:
52+
"Preview"
53+
}
54+
}
55+
56+
var id: String { rawValue }
57+
}
58+
59+
extension UpdaterService: SPUUpdaterDelegate {
60+
func allowedChannels(for _: SPUUpdater) -> Set<String> {
61+
// There's currently no point in subscribing to both channels, as
62+
// preview >= stable
63+
[updateChannel.rawValue]
64+
}
65+
}
66+
67+
extension UpdaterService: SUVersionDisplay {
68+
func formatUpdateVersion(
69+
fromUpdate update: SUAppcastItem,
70+
andBundleDisplayVersion inOutBundleDisplayVersion: AutoreleasingUnsafeMutablePointer<NSString>,
71+
withBundleVersion bundleVersion: String
72+
) -> String {
73+
// Replace CFBundleShortVersionString with CFBundleVersion, as the
74+
// latter shows build numbers.
75+
inOutBundleDisplayVersion.pointee = bundleVersion as NSString
76+
// This is already CFBundleVersion, as that's the only version in the
77+
// appcast.
78+
return update.displayVersionString
79+
}
80+
}
81+
82+
extension UpdaterService: SPUStandardUserDriverDelegate {
83+
func standardUserDriverRequestsVersionDisplayer() -> (any SUVersionDisplay)? {
84+
self
85+
}
86+
}

Coder-Desktop/Coder-Desktop/VPN/VPNSystemExtension.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
174174
actionForReplacingExtension existing: OSSystemExtensionProperties,
175175
withExtension extension: OSSystemExtensionProperties
176176
) -> OSSystemExtensionRequest.ReplacementAction {
177-
logger.info("Replacing \(request.identifier) v\(existing.bundleVersion) with v\(`extension`.bundleVersion)")
177+
logger.info("Replacing \(request.identifier) \(existing.bundleVersion) with \(`extension`.bundleVersion)")
178178
// This is counterintuitive, but this function is only called if the
179179
// versions are the same in a dev environment.
180180
// In a release build, this only gets called when the version string is

Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import SwiftUI
33

44
struct GeneralTab: View {
55
@EnvironmentObject var state: AppState
6+
@EnvironmentObject var updater: UpdaterService
67
var body: some View {
78
Form {
89
Section {
@@ -18,10 +19,20 @@ struct GeneralTab: View {
1819
Text("Start Coder Connect on launch")
1920
}
2021
}
22+
Section {
23+
Toggle(isOn: $updater.autoCheckForUpdates) {
24+
Text("Automatically check for updates")
25+
}
26+
Picker("Update channel", selection: $updater.updateChannel) {
27+
ForEach(UpdateChannel.allCases) { channel in
28+
Text(channel.name).tag(channel)
29+
}
30+
}
31+
HStack {
32+
Spacer()
33+
Button("Check for updates") { updater.checkForUpdates() }.disabled(!updater.canCheckForUpdates)
34+
}
35+
}
2136
}.formStyle(.grouped)
2237
}
2338
}
24-
25-
#Preview {
26-
GeneralTab()
27-
}

Coder-Desktop/project.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ options:
1111

1212
settings:
1313
base:
14-
MARKETING_VERSION: ${MARKETING_VERSION} # Sets the version number.
15-
CURRENT_PROJECT_VERSION: ${CURRENT_PROJECT_VERSION} # Sets the build number.
14+
MARKETING_VERSION: ${MARKETING_VERSION} # Sets CFBundleShortVersionString
15+
CURRENT_PROJECT_VERSION: ${CURRENT_PROJECT_VERSION} # CFBundleVersion
1616
GIT_COMMIT_HASH: ${GIT_COMMIT_HASH}
1717

1818
ALWAYS_SEARCH_USER_PATHS: NO

scripts/update-cask.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ cask "coder-desktop${SUFFIX}" do
101101
name "Coder Desktop"
102102
desc "Native desktop client for Coder"
103103
homepage "https://github.com/coder/coder-desktop-macos"
104+
auto_updates true
104105
105106
conflicts_with cask: "coder/coder/${CONFLICTS_WITH}"
106107
depends_on macos: ">= :sonoma"

0 commit comments

Comments
 (0)