Skip to content

Commit a26f35d

Browse files
committed
Allow overriding the workspace buildsystem per workspace
Merge options prior to determining the buildsystem to use for a workspace, rather than after. Do not search up the directory tree when determining a build system for the given workspaces. Implicit workspaces may search up to the workspace roots, but no further.
1 parent 1cd010d commit a26f35d

File tree

7 files changed

+166
-37
lines changed

7 files changed

+166
-37
lines changed

Documentation/Configuration File.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ The structure of the file is currently not guaranteed to be stable. Options may
4343
- `logLevel: "debug"|"info"|"default"|"error"|"fault"`: The level from which one onwards log messages should be written.
4444
- `privacyLevel: "public"|"private"|"sensitive"`: Whether potentially sensitive information should be redacted. Default is `public`, which redacts potentially sensitive information.
4545
- `inputMirrorDirectory: string`: Write all input received by SourceKit-LSP on stdin to a file in this directory. Useful to record and replay an entire SourceKit-LSP session.
46-
- `defaultWorkspaceType: "buildserver"|"compdb"|"swiftpm"`: Overrides workspace type selection logic.
46+
- `defaultWorkspaceType: "swiftPM"|"compilationDatabase"|"buildServer"`: Overrides workspace type selection logic.
4747
- `generatedFilesPath: string`: Directory in which generated interfaces and macro expansions should be stored.
4848
- `backgroundIndexing: bool`: Explicitly enable or disable background indexing.
4949
- `backgroundPreparationMode: "build"|"noLazy"|"enabled"`: Determines how background indexing should prepare a target. Possible values are:

Sources/BuildSystemIntegration/DetermineBuildSystem.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ import struct TSCBasic.AbsolutePath
3131
#endif
3232

3333
/// Determine which build system should be started to handle the given workspace folder and at which folder that build
34-
/// system's project root is (see `BuiltInBuildSystem.projectRoot(for:options:)`).
34+
/// system's project root is (see `BuiltInBuildSystem.projectRoot(for:options:)`). `onlyConsiderRoot` controls whether
35+
/// paths outside the root should be considered (eg. configuration files in the user's home directory).
3536
///
3637
/// Returns `nil` if no build system can handle this workspace folder.
3738
package func determineBuildSystem(
3839
forWorkspaceFolder workspaceFolder: DocumentURI,
40+
onlyConsiderRoot: Bool,
3941
options: SourceKitLSPOptions
4042
) -> BuildSystemSpec? {
4143
var buildSystemPreference: [WorkspaceType] = [

Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -230,17 +230,12 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem {
230230
guard var path = orLog("Getting realpath for project root", { try path.realpath }) else {
231231
return nil
232232
}
233-
while true {
234-
let packagePath = path.appendingPathComponent("Package.swift")
235-
if (try? String(contentsOf: packagePath, encoding: .utf8))?.contains("PackageDescription") ?? false {
236-
return path
237-
}
238233

239-
if (try? AbsolutePath(validating: path.filePath))?.isRoot ?? true {
240-
break
241-
}
242-
path.deleteLastPathComponent()
234+
let packagePath = path.appendingPathComponent("Package.swift")
235+
if (try? String(contentsOf: packagePath, encoding: .utf8))?.contains("PackageDescription") ?? false {
236+
return path
243237
}
238+
244239
return nil
245240
}
246241

Sources/SKOptions/SourceKitLSPOptions.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
280280
set { logging = newValue }
281281
}
282282

283-
/// Default workspace type (buildserver|compdb|swiftpm). Overrides workspace type selection logic.
283+
/// Default workspace type (swiftPM|compilationDatabase|buildServer). Overrides workspace type selection logic.
284284
public var defaultWorkspaceType: WorkspaceType?
285285
public var generatedFilesPath: String?
286286

@@ -444,6 +444,17 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
444444
)
445445
}
446446

447+
public static func merging(base: SourceKitLSPOptions, workspaceFolder: DocumentURI) -> SourceKitLSPOptions {
448+
return SourceKitLSPOptions.merging(
449+
base: base,
450+
override: SourceKitLSPOptions(
451+
path: workspaceFolder.fileURL?
452+
.appendingPathComponent(".sourcekit-lsp")
453+
.appendingPathComponent("config.json")
454+
)
455+
)
456+
}
457+
447458
public var generatedFilesAbsolutePath: URL {
448459
if let generatedFilesPath {
449460
return URL(fileURLWithPath: generatedFilesPath)

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -209,29 +209,40 @@ package actor SourceKitLSPServer {
209209
guard var url = uri.fileURL?.deletingLastPathComponent() else {
210210
return nil
211211
}
212-
let projectRoots = await self.workspacesAndIsImplicit.filter { !$0.isImplicit }.asyncCompactMap {
212+
213+
// Roots of opened workspaces - only consider explicit here (all implicit must necessarily be subdirectories of
214+
// the explicit workspace roots)
215+
let workspaceRoots = workspacesAndIsImplicit.filter { !$0.isImplicit }.compactMap { $0.workspace.rootUri?.fileURL }
216+
217+
// We want to skip creating another workspace if the project roots are the same. This could happen if an existing
218+
// workspace hasn't reloaded after a new file was added to it (and thus that build system needs to be reloaded).
219+
let projectRoots = await workspacesAndIsImplicit.asyncCompactMap {
213220
await $0.workspace.buildSystemManager.projectRoot
214221
}
215-
let rootURLs = workspacesAndIsImplicit.filter { !$0.isImplicit }.compactMap { $0.workspace.rootUri?.fileURL }
216-
while url.pathComponents.count > 1 && rootURLs.contains(where: { $0.isPrefix(of: url) }) {
222+
223+
while url.pathComponents.count > 1 && workspaceRoots.contains(where: { $0.isPrefix(of: url) }) {
217224
defer {
218225
url.deleteLastPathComponent()
219226
}
220-
// Ignore workspaces that have the same project root as an existing workspace.
221-
// This might happen if there is an existing SwiftPM workspace that hasn't been reloaded after a new file
222-
// was added to it and thus currently doesn't know that it can handle that file. In that case, we shouldn't open
223-
// a new workspace for the same root. Instead, the existing workspace's build system needs to be reloaded.
227+
224228
let uri = DocumentURI(url)
225-
guard let buildSystemSpec = determineBuildSystem(forWorkspaceFolder: uri, options: self.options) else {
229+
let options = SourceKitLSPOptions.merging(base: self.options, workspaceFolder: uri)
230+
231+
// Some build systems consider paths outside of the folder (eg. BSP has settings in the home directory). If we
232+
// allowed those paths, then the very first folder that the file is in would always be its own build system - so
233+
// skip them in that case.
234+
guard let buildSystemSpec = determineBuildSystem(forWorkspaceFolder: uri, onlyConsiderRoot: true, options: options) else {
226235
continue
227236
}
228237
guard !projectRoots.contains(buildSystemSpec.projectRoot) else {
229238
continue
230239
}
240+
241+
// No existing workspace matches this root - create one.
231242
guard
232243
let workspace = await orLog(
233-
"Creating workspace",
234-
{ try await createWorkspace(workspaceFolder: uri, buildSystemSpec: buildSystemSpec) }
244+
"Creating implicit workspace",
245+
{ try await createWorkspace(workspaceFolder: uri, options: options, buildSystemSpec: buildSystemSpec) }
235246
)
236247
else {
237248
continue
@@ -818,26 +829,20 @@ extension SourceKitLSPServer {
818829

819830
/// Creates a workspace at the given `uri`.
820831
///
821-
/// If the build system that was determined for the workspace does not satisfy `condition`, `nil` is returned.
832+
/// A workspace does not necessarily have any buildsystem attached to it, in which case `buildSystemSpec` may be
833+
/// `nil` - consider eg. a top level workspace folder with multiple SwiftPM projects inside it.
822834
private func createWorkspace(
823835
workspaceFolder: DocumentURI,
836+
options: SourceKitLSPOptions,
824837
buildSystemSpec: BuildSystemSpec?
825838
) async throws -> Workspace {
826839
guard let capabilityRegistry = capabilityRegistry else {
827840
struct NoCapabilityRegistryError: Error {}
828841
logger.log("Cannot open workspace before server is initialized")
829842
throw NoCapabilityRegistryError()
830843
}
831-
let testHooks = self.testHooks
832-
let options = SourceKitLSPOptions.merging(
833-
base: self.options,
834-
override: SourceKitLSPOptions(
835-
path: workspaceFolder.fileURL?
836-
.appendingPathComponent(".sourcekit-lsp")
837-
.appendingPathComponent("config.json")
838-
)
839-
)
840-
logger.log("Creating workspace at \(workspaceFolder.forLogging) with options: \(options.forLogging)")
844+
845+
logger.log("Creating workspace at \(workspaceFolder.forLogging)")
841846
logger.logFullObjectInMultipleLogMessages(header: "Options for workspace", options.loggingProxy)
842847

843848
let workspace = await Workspace(
@@ -848,7 +853,7 @@ extension SourceKitLSPServer {
848853
buildSystemSpec: buildSystemSpec,
849854
toolchainRegistry: self.toolchainRegistry,
850855
options: options,
851-
testHooks: testHooks,
856+
testHooks: self.testHooks,
852857
indexTaskScheduler: indexTaskScheduler
853858
)
854859
return workspace
@@ -857,9 +862,12 @@ extension SourceKitLSPServer {
857862
/// Determines the build system for the given workspace folder and creates a `Workspace` that uses this inferred build
858863
/// system.
859864
private func createWorkspaceWithInferredBuildSystem(workspaceFolder: DocumentURI) async throws -> Workspace {
865+
let options = SourceKitLSPOptions.merging(base: self.options, workspaceFolder: workspaceFolder)
866+
let buildSystemSpec = determineBuildSystem(forWorkspaceFolder: workspaceFolder, onlyConsiderRoot: false, options: options)
860867
return try await self.createWorkspace(
861868
workspaceFolder: workspaceFolder,
862-
buildSystemSpec: determineBuildSystem(forWorkspaceFolder: workspaceFolder, options: self.options)
869+
options: options,
870+
buildSystemSpec: buildSystemSpec
863871
)
864872
}
865873

Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -976,9 +976,9 @@ final class SwiftPMBuildSystemTests: XCTestCase {
976976
.appendingPathComponent("pkg")
977977
.appendingPathComponent("Sources")
978978
.appendingPathComponent("lib")
979-
let projectRoot = SwiftPMBuildSystem.projectRoot(for: workspaceRoot, options: .testDefault())
980979

981-
assertEqual(projectRoot, tempDir.appendingPathComponent("pkg", isDirectory: true))
980+
let projectRoot = SwiftPMBuildSystem.projectRoot(for: workspaceRoot, options: .testDefault())
981+
assertNil(projectRoot)
982982
}
983983
}
984984

Tests/SourceKitLSPTests/WorkspaceTests.swift

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import BuildSystemIntegration
1314
import Foundation
1415
import LanguageServerProtocol
1516
import SKLogging
@@ -967,4 +968,116 @@ final class WorkspaceTests: XCTestCase {
967968
["Cannot convert value of type 'Int' to specified type 'String'"]
968969
)
969970
}
971+
972+
func testWorkspaceOptionsOverrideBuildSystem() async throws {
973+
let swiftc = try await unwrap(ToolchainRegistry.forTesting.default?.swiftc)
974+
let sdkArgs =
975+
if let defaultSDKPath {
976+
"""
977+
-sdk '\(defaultSDKPath)'
978+
"""
979+
} else {
980+
""
981+
}
982+
983+
let project = try await MultiFileTestProject(files: [
984+
".sourcekit-lsp/config.json": """
985+
{
986+
"defaultWorkspaceType": "compilationDatabase"
987+
}
988+
""",
989+
"src/Foo.swift": """
990+
#if HAVE_SETTINGS
991+
#error("Have settings")
992+
#endif
993+
""",
994+
"Sources/MyLib/Bar.swift": "",
995+
"build/compile_commands.json": """
996+
[
997+
{
998+
"directory": "$TEST_DIR",
999+
"command": "\(swiftc.filePath) $TEST_DIR/src/Foo.swift -DHAVE_SETTINGS \(sdkArgs)",
1000+
"file": "src/Foo.swift",
1001+
"output": "$TEST_DIR/build/Foo.swift.o"
1002+
},
1003+
]
1004+
""",
1005+
"Package.swift": """
1006+
// swift-tools-version: 5.7
1007+
1008+
import PackageDescription
1009+
1010+
let package = Package(
1011+
name: "MyLib",
1012+
targets: [
1013+
.target(name: "MyLib"),
1014+
]
1015+
)
1016+
""",
1017+
])
1018+
let (uri, _) = try project.openDocument("Foo.swift")
1019+
let diagnostics = try await project.testClient.send(
1020+
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
1021+
)
1022+
XCTAssertEqual(
1023+
diagnostics.fullReport?.items.map(\.message),
1024+
["Have settings"]
1025+
)
1026+
}
1027+
1028+
func testImplicitWorkspaceOptionsOverrideBuildSystem() async throws {
1029+
let swiftc = try await unwrap(ToolchainRegistry.forTesting.default?.swiftc)
1030+
let sdkArgs =
1031+
if let defaultSDKPath {
1032+
"""
1033+
-sdk '\(defaultSDKPath)'
1034+
"""
1035+
} else {
1036+
""
1037+
}
1038+
1039+
let project = try await MultiFileTestProject(files: [
1040+
"projA/.sourcekit-lsp/config.json": """
1041+
{
1042+
"defaultWorkspaceType": "compilationDatabase"
1043+
}
1044+
""",
1045+
"projA/src/Foo.swift": """
1046+
#if HAVE_SETTINGS
1047+
#error("Have settings")
1048+
#endif
1049+
""",
1050+
"projA/Sources/MyLib/Bar.swift": "",
1051+
"projA/build/compile_commands.json": """
1052+
[
1053+
{
1054+
"directory": "$TEST_DIR/projA",
1055+
"command": "\(swiftc.filePath) $TEST_DIR/projA/src/Foo.swift -DHAVE_SETTINGS \(sdkArgs)",
1056+
"file": "src/Foo.swift",
1057+
"output": "$TEST_DIR/projA/build/Foo.swift.o"
1058+
},
1059+
]
1060+
""",
1061+
"projA/Package.swift": """
1062+
// swift-tools-version: 5.7
1063+
1064+
import PackageDescription
1065+
1066+
let package = Package(
1067+
name: "MyLib",
1068+
targets: [
1069+
.target(name: "MyLib"),
1070+
]
1071+
)
1072+
""",
1073+
])
1074+
let (uri, _) = try project.openDocument("Foo.swift")
1075+
let diagnostics = try await project.testClient.send(
1076+
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
1077+
)
1078+
XCTAssertEqual(
1079+
diagnostics.fullReport?.items.map(\.message),
1080+
["Have settings"]
1081+
)
1082+
}
9701083
}

0 commit comments

Comments
 (0)