Skip to content

Commit 7af7b57

Browse files
committed
Support downloading Codeium language server
1 parent d04bb34 commit 7af7b57

File tree

8 files changed

+413
-37
lines changed

8 files changed

+413
-37
lines changed

Core/Package.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ let package = Package(
127127
"GitHubCopilotService",
128128
"CodeiumService",
129129
"SuggestionModel",
130-
"LaunchAgentManager"
130+
"LaunchAgentManager",
131131
]
132132
),
133133

@@ -256,7 +256,13 @@ let package = Package(
256256

257257
.target(
258258
name: "CodeiumService",
259-
dependencies: ["LanguageClient", "SuggestionModel", "Preferences", "KeychainAccess"]
259+
dependencies: [
260+
"LanguageClient",
261+
"SuggestionModel",
262+
"Preferences",
263+
"KeychainAccess",
264+
"Terminal"
265+
]
260266
),
261267
]
262268
)

Core/Sources/CodeiumService/CodeiumAuthService.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ public final class CodeiumAuthService {
77
let keychain: Keychain = {
88
let info = Bundle.main.infoDictionary
99
return Keychain(
10-
service: info?["BUNDLE_IDENTIFIER_BASE"] as! String,
11-
accessGroup: "\(info?["APP_ID_PREFIX"] as! String)\(info?["BUNDLE_IDENTIFIER_BASE"] as! String).Shared"
10+
service: info?["BUNDLE_IDENTIFIER_BASE"] as? String ?? "",
11+
accessGroup: "\(info?["APP_ID_PREFIX"] as? String ?? "")\(info?["BUNDLE_IDENTIFIER_BASE"] as? String ?? "").Shared"
1212
)
1313
}()
1414

@@ -19,10 +19,6 @@ public final class CodeiumAuthService {
1919
public func signIn(token: String) async throws {
2020
let key = try await generate(token: token)
2121
let info = Bundle.main.infoDictionary
22-
let keychain = Keychain(
23-
service: info?["BUNDLE_IDENTIFIER_BASE"] as! String,
24-
accessGroup: "\(info?["APP_ID_PREFIX"] as! String)\(info?["BUNDLE_IDENTIFIER_BASE"] as! String).Shared"
25-
)
2622
try keychain.set(key, key: codeiumKeyKey)
2723
}
2824

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import Foundation
2+
import Terminal
3+
4+
public struct CodeiumInstallationManager {
5+
private static var isInstalling = false
6+
let latestSupportedVersion: String = "1.2.9"
7+
8+
public init() {}
9+
10+
public enum InstallationStatus {
11+
case notInstalled
12+
case installed(String)
13+
case outdated(current: String, latest: String)
14+
case unsupported(current: String, latest: String)
15+
}
16+
17+
public func checkInstallation() -> InstallationStatus {
18+
guard let urls = try? CodeiumSuggestionService.createFoldersIfNeeded()
19+
else { return .notInstalled }
20+
let executableFolderURL = urls.executableURL
21+
let binaryURL = executableFolderURL.appendingPathComponent("language_server")
22+
let versionFileURL = executableFolderURL.appendingPathComponent("version")
23+
24+
if !FileManager.default.fileExists(atPath: binaryURL.path) {
25+
return .notInstalled
26+
}
27+
28+
if FileManager.default.fileExists(atPath: versionFileURL.path),
29+
let versionData = try? Data(contentsOf: versionFileURL),
30+
let version = String(data: versionData, encoding: .utf8)
31+
{
32+
switch version.compare(latestSupportedVersion) {
33+
case .orderedAscending:
34+
return .outdated(current: version, latest: latestSupportedVersion)
35+
case .orderedSame:
36+
return .installed(version)
37+
case .orderedDescending:
38+
return .unsupported(current: version, latest: latestSupportedVersion)
39+
}
40+
}
41+
42+
return .outdated(current: "Unknown", latest: latestSupportedVersion)
43+
}
44+
45+
public enum InstallationStep {
46+
case downloading
47+
case uninstalling
48+
case decompressing
49+
case done
50+
}
51+
52+
public func installLatestVersion() -> AsyncThrowingStream<InstallationStep, Error> {
53+
AsyncThrowingStream<InstallationStep, Error> { continuation in
54+
Task {
55+
guard !CodeiumInstallationManager.isInstalling else {
56+
continuation.finish(throwing: CodeiumError.languageServiceIsInstalling)
57+
return
58+
}
59+
CodeiumInstallationManager.isInstalling = true
60+
defer { CodeiumInstallationManager.isInstalling = false }
61+
do {
62+
continuation.yield(.downloading)
63+
let urls = try CodeiumSuggestionService.createFoldersIfNeeded()
64+
let urlString =
65+
"https://github.com/Exafunction/codeium/releases/download/language-server-v\(latestSupportedVersion)/language_server_macos_\(isAppleSilicon() ? "arm" : "x64").gz"
66+
guard let url = URL(string: urlString) else { return }
67+
68+
// download
69+
let (fileURL, _) = try await URLSession.shared.download(from: url)
70+
let targetURL = urls.executableURL.appendingPathComponent("language_server")
71+
.appendingPathExtension("gz")
72+
try FileManager.default.copyItem(at: fileURL, to: targetURL)
73+
defer { try? FileManager.default.removeItem(at: targetURL) }
74+
75+
continuation.yield(.uninstalling)
76+
try await uninstall()
77+
78+
continuation.yield(.decompressing)
79+
// extract file
80+
let terminal = Terminal()
81+
_ = try await terminal.runCommand(
82+
"/usr/bin/gunzip",
83+
arguments: [targetURL.path],
84+
environment: [:]
85+
)
86+
87+
// update permission 755
88+
try FileManager.default.setAttributes(
89+
[.posixPermissions: 0o755],
90+
ofItemAtPath: targetURL.deletingPathExtension().path
91+
)
92+
93+
// create version file
94+
let data = latestSupportedVersion.data(using: .utf8)
95+
FileManager.default.createFile(
96+
atPath: urls.executableURL.appendingPathComponent("version").path,
97+
contents: data
98+
)
99+
100+
continuation.yield(.done)
101+
continuation.finish()
102+
} catch {
103+
continuation.finish(throwing: error)
104+
}
105+
}
106+
}
107+
}
108+
109+
public func uninstall() async throws {
110+
guard let urls = try? CodeiumSuggestionService.createFoldersIfNeeded()
111+
else { return }
112+
let executableFolderURL = urls.executableURL
113+
let binaryURL = executableFolderURL.appendingPathComponent("language_server")
114+
let versionFileURL = executableFolderURL.appendingPathComponent("version")
115+
if FileManager.default.fileExists(atPath: binaryURL.path) {
116+
try FileManager.default.removeItem(at: binaryURL)
117+
}
118+
if FileManager.default.fileExists(atPath: versionFileURL.path) {
119+
try FileManager.default.removeItem(at: versionFileURL)
120+
}
121+
}
122+
}
123+
124+
func isAppleSilicon() -> Bool {
125+
var result = false
126+
#if arch(arm64)
127+
result = true
128+
#endif
129+
return result
130+
}
131+

Core/Sources/CodeiumService/CodeiumLanguageServer.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ final class CodeiumLanguageServer {
106106

107107
deinit {
108108
process.terminationHandler = nil
109-
process.terminate()
109+
if process.isRunning {
110+
process.terminate()
111+
}
110112
transport.close()
111113
}
112114

@@ -141,6 +143,7 @@ extension CodeiumLanguageServer: CodeiumLSP {
141143
do {
142144
let error = try JSONDecoder().decode(CodeiumResponseError.self, from: data)
143145
Logger.codeium.error(error.message)
146+
throw CancellationError()
144147
} catch {
145148
Logger.codeium.error(error.localizedDescription)
146149
throw error

Core/Sources/CodeiumService/CodeiumService.swift

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,17 @@ public protocol CodeiumSuggestionServiceType {
2222

2323
enum CodeiumError: Error, LocalizedError {
2424
case languageServerNotInstalled
25+
case languageServerOutdated
26+
case languageServiceIsInstalling
2527

2628
var errorDescription: String? {
2729
switch self {
2830
case .languageServerNotInstalled:
29-
return "Language server is not installed."
31+
return "Language server is not installed. Please install it in the host app."
32+
case .languageServerOutdated:
33+
return "Language server is outdated. Please update it in the host app."
34+
case .languageServiceIsInstalling:
35+
return "Language service is installing. Please wait."
3036
}
3137
}
3238
}
@@ -44,7 +50,7 @@ public class CodeiumSuggestionService {
4450
let supportURL: URL
4551

4652
let authService = CodeiumAuthService()
47-
53+
4854
var xcodeVersion = "14.0"
4955

5056
init(designatedServer: CodeiumLSP) {
@@ -61,9 +67,6 @@ public class CodeiumSuggestionService {
6167
let urls = try CodeiumSuggestionService.createFoldersIfNeeded()
6268
languageServerURL = urls.executableURL.appendingPathComponent("language_server")
6369
supportURL = urls.supportURL
64-
guard FileManager.default.fileExists(atPath: languageServerURL.path) else {
65-
throw CodeiumError.languageServerNotInstalled
66-
}
6770
Task {
6871
try await setupServerIfNeeded()
6972
}
@@ -72,6 +75,18 @@ public class CodeiumSuggestionService {
7275
@discardableResult
7376
func setupServerIfNeeded() async throws -> CodeiumLSP {
7477
if let server { return server }
78+
79+
let binaryManager = CodeiumInstallationManager()
80+
let installationStatus = binaryManager.checkInstallation()
81+
switch installationStatus {
82+
case .installed, .unsupported:
83+
break
84+
case .notInstalled:
85+
throw CodeiumError.languageServerNotInstalled
86+
case .outdated:
87+
throw CodeiumError.languageServerOutdated
88+
}
89+
7590
let metadata = try getMetadata()
7691
xcodeVersion = (try? await getXcodeVersion()) ?? xcodeVersion
7792
let tempFolderURL = FileManager.default.temporaryDirectory
@@ -112,8 +127,8 @@ public class CodeiumSuggestionService {
112127
}
113128
}
114129

115-
server.start()
116130
self.server = server
131+
server.start()
117132
return server
118133
}
119134

@@ -322,3 +337,4 @@ func getXcodeVersion() async throws -> String {
322337
}
323338
}
324339
}
340+

0 commit comments

Comments
 (0)