Skip to content

Commit f228a41

Browse files
committed
Update BSP connection build server config lookup path
Resolves #1695 Adopt `<workspace_root>/.bsp` search locations in addition to `<workspace_root>/`.
1 parent 9e2b1c9 commit f228a41

File tree

2 files changed

+125
-2
lines changed

2 files changed

+125
-2
lines changed

Sources/BuildSystemIntegration/ExternalBuildSystemAdapter.swift

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ struct ExecutableNotFoundError: Error {
5252
let executableName: String
5353
}
5454

55+
enum BuildServerNotFoundError: Error {
56+
case fileNotFound
57+
}
58+
5559
private struct BuildServerConfig: Codable {
5660
/// The name of the build tool.
5761
let name: String
@@ -94,7 +98,7 @@ actor ExternalBuildSystemAdapter {
9498
private var lastRestart: Date?
9599

96100
static package func projectRoot(for workspaceFolder: AbsolutePath, options: SourceKitLSPOptions) -> AbsolutePath? {
97-
guard localFileSystem.isFile(workspaceFolder.appending(component: "buildServer.json")) else {
101+
guard let _ = getConfigPath(for: workspaceFolder) else {
98102
return nil
99103
}
100104
return workspaceFolder
@@ -142,7 +146,10 @@ actor ExternalBuildSystemAdapter {
142146

143147
/// Create a new JSONRPCConnection to the build server.
144148
private func createConnectionToBspServer() async throws -> JSONRPCConnection {
145-
let configPath = projectRoot.appending(component: "buildServer.json")
149+
guard let configPath = ExternalBuildSystemAdapter.getConfigPath(for: self.projectRoot) else {
150+
throw BuildServerNotFoundError.fileNotFound
151+
}
152+
146153
let serverConfig = try BuildServerConfig.load(from: configPath)
147154
var serverPath = try AbsolutePath(validating: serverConfig.argv[0], relativeTo: projectRoot)
148155
var serverArgs = Array(serverConfig.argv[1...])
@@ -178,6 +185,61 @@ actor ExternalBuildSystemAdapter {
178185
).connection
179186
}
180187

188+
private static func getConfigPath(for workspaceFolder: AbsolutePath? = nil) -> AbsolutePath? {
189+
var buildServerConfigLocations: [AbsolutePath?] = []
190+
if let workspaceFolder = workspaceFolder {
191+
buildServerConfigLocations.append(workspaceFolder.appending(component: ".bsp"))
192+
}
193+
194+
#if os(Windows)
195+
let localAppDataPath = AbsolutePath(validatingOrNil: ProcessInfo.processInfo.environment["LOCALAPPDATA"])?.appending(component: "bsp")
196+
let programDataPath = AbsolutePath(validatingOrNil: ProcessInfo.processInfo.environment["PROGRAMDATA"])?.appending(component: "bsp")
197+
198+
buildServerConfigLocations.append(contentsOf: [localAppDataPath, programDataPath])
199+
#else
200+
let xdgDataHomePath = AbsolutePath(validatingOrNil: ProcessInfo.processInfo.environment["XDG_DATA_HOME"])?.appending(component: "bsp")
201+
buildServerConfigLocations.append(xdgDataHomePath)
202+
203+
if let libraryUrl = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first {
204+
let libraryPath = AbsolutePath(validatingOrNil: libraryUrl.absoluteString)?.appending(component: "bsp")
205+
buildServerConfigLocations.append(libraryPath)
206+
}
207+
208+
let xdgDataDirsPath = AbsolutePath(validatingOrNil: ProcessInfo.processInfo.environment["XDG_DATA_DIRS"])?.appending(component: "bsp")
209+
buildServerConfigLocations.append(xdgDataDirsPath)
210+
211+
if let libraryUrl = FileManager.default.urls(for: .applicationSupportDirectory, in: .systemDomainMask).first {
212+
let libraryPath = AbsolutePath(validatingOrNil: libraryUrl.absoluteString)?.appending(component: "bsp")
213+
buildServerConfigLocations.append(libraryPath)
214+
}
215+
#endif
216+
217+
for buildServerConfigLocation in buildServerConfigLocations {
218+
guard let buildServerConfigLocation else {
219+
continue
220+
}
221+
let fileManager = FileManager.default
222+
do {
223+
let items = try fileManager.contentsOfDirectory(atPath: buildServerConfigLocation.pathString)
224+
let jsonFiles = items.filter { $0.hasSuffix(".json") }
225+
226+
if let configFilePath = jsonFiles.sorted().first {
227+
return buildServerConfigLocation.appending(component: configFilePath)
228+
}
229+
} catch {
230+
logger.error("Failed to read build server config file at \(buildServerConfigLocation): \(error)")
231+
}
232+
}
233+
234+
// Backward compatibility
235+
if let workspaceFolder = workspaceFolder,
236+
localFileSystem.isFile(workspaceFolder.appending(component: "buildServer.json")) {
237+
return workspaceFolder.appending(component: "buildServer.json")
238+
}
239+
240+
return nil
241+
}
242+
181243
/// Restart the BSP server after it has crashed.
182244
private func handleBspServerCrash() async throws {
183245
// Set `connectionToBuildServer` to `nil` to indicate that there is currently no BSP server running.

Tests/BuildSystemIntegrationTests/BuildServerBuildSystemTests.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,4 +303,65 @@ final class BuildServerBuildSystemTests: XCTestCase {
303303
}
304304

305305
}
306+
307+
func testBuildServerConfigPath() async throws {
308+
let project = try await BuildServerTestProject(
309+
// The build config file currently lies in `<workspace root>/buildServer.json`.
310+
//
311+
// However, as the current logic prefers existence of `.json files` in
312+
// `<workspace root>/.bsp/`, the only file that exists is `test.json`
313+
// which is not a valid one. Hence, this test will fail.
314+
files: [
315+
"Test.swift": """
316+
#if DEBUG
317+
#error("DEBUG SET")
318+
#else
319+
#error("DEBUG NOT SET")
320+
#endif
321+
""",
322+
".bsp/test.json": "{}"
323+
],
324+
buildServer: """
325+
class BuildServer(AbstractBuildServer):
326+
def workspace_build_targets(self, request: Dict[str, object]) -> Dict[str, object]:
327+
return {
328+
"targets": [
329+
{
330+
"id": {"uri": "bsp://dummy"},
331+
"tags": [],
332+
"languageIds": [],
333+
"dependencies": [],
334+
"capabilities": {},
335+
}
336+
]
337+
}
338+
339+
def buildtarget_sources(self, request: Dict[str, object]) -> Dict[str, object]:
340+
return {
341+
"items": [
342+
{
343+
"target": {"uri": "bsp://dummy"},
344+
"sources": [
345+
{"uri": "file://$TEST_DIR/Test.swift", "kind": 1, "generated": False}
346+
],
347+
}
348+
]
349+
}
350+
351+
def textdocument_sourcekitoptions(self, request: Dict[str, object]) -> Dict[str, object]:
352+
return {
353+
"compilerArguments": ["$TEST_DIR/Test.swift", "-DDEBUG", $SDK_ARGS]
354+
}
355+
"""
356+
)
357+
358+
let (uri, _) = try project.openDocument("Test.swift")
359+
360+
try await repeatUntilExpectedResult {
361+
let diags = try await project.testClient.send(
362+
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
363+
)
364+
return diags.fullReport?.items.map(\.message) == ["DEBUG SET"]
365+
}
366+
}
306367
}

0 commit comments

Comments
 (0)