Skip to content

Commit ed098f5

Browse files
authored
Merge pull request #1934 from ahoppen/multi-toolchain-support
Support opening documents within the same workspace with sourcekitd/clangd from different toolchains
2 parents c67c06e + 98b1294 commit ed098f5

18 files changed

+391
-114
lines changed

Sources/BuildSystemIntegration/BuildSystemManager.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ private extension BuildSystemSpec {
202202
) { connectionToSourceKitLSP in
203203
try JSONCompilationDatabaseBuildSystem(
204204
configPath: configPath,
205+
toolchainRegistry: toolchainRegistry,
205206
connectionToSourceKitLSP: connectionToSourceKitLSP
206207
)
207208
}

Sources/BuildSystemIntegration/BuildTargetIdentifierExtensions.swift

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,14 @@ package enum BuildDestinationIdentifier {
4343
}
4444
}
4545

46+
// MARK: BuildTargetIdentifier for SwiftPM
47+
4648
extension BuildTargetIdentifier {
4749
/// - Important: *For testing only*
48-
package init(target: String, destination: BuildDestinationIdentifier) throws {
50+
package static func createSwiftPM(
51+
target: String,
52+
destination: BuildDestinationIdentifier
53+
) throws -> BuildTargetIdentifier {
4954
var components = URLComponents()
5055
components.scheme = "swiftpm"
5156
components.host = "target"
@@ -67,12 +72,12 @@ extension BuildTargetIdentifier {
6772
throw FailedToConvertSwiftBuildTargetToUrlError(target: target, destination: destination.id)
6873
}
6974

70-
self.init(uri: URI(url))
75+
return BuildTargetIdentifier(uri: URI(url))
7176
}
7277

73-
fileprivate static let forPackageManifest = BuildTargetIdentifier(uri: try! URI(string: "swiftpm://package-manifest"))
78+
static let forPackageManifest = BuildTargetIdentifier(uri: try! URI(string: "swiftpm://package-manifest"))
7479

75-
fileprivate var targetProperties: (target: String, runDestination: String) {
80+
var swiftpmTargetProperties: (target: String, runDestination: String) {
7681
get throws {
7782
struct InvalidTargetIdentifierError: Swift.Error, CustomStringConvertible {
7883
var target: BuildTargetIdentifier
@@ -96,6 +101,57 @@ extension BuildTargetIdentifier {
96101
}
97102
}
98103

104+
// MARK: BuildTargetIdentifier for CompileCommands
105+
106+
extension BuildTargetIdentifier {
107+
/// - Important: *For testing only*
108+
package static func createCompileCommands(compiler: String) throws -> BuildTargetIdentifier {
109+
var components = URLComponents()
110+
components.scheme = "compilecommands"
111+
components.host = "target"
112+
components.queryItems = [URLQueryItem(name: "compiler", value: compiler)]
113+
114+
struct FailedToConvertSwiftBuildTargetToUrlError: Swift.Error, CustomStringConvertible {
115+
var compiler: String
116+
117+
var description: String {
118+
return "Failed to generate URL for compiler: \(compiler)"
119+
}
120+
}
121+
122+
guard let url = components.url else {
123+
throw FailedToConvertSwiftBuildTargetToUrlError(compiler: compiler)
124+
}
125+
126+
return BuildTargetIdentifier(uri: URI(url))
127+
}
128+
129+
var compileCommandsCompiler: String {
130+
get throws {
131+
struct InvalidTargetIdentifierError: Swift.Error, CustomStringConvertible {
132+
var target: BuildTargetIdentifier
133+
134+
var description: String {
135+
return "Invalid target identifier \(target)"
136+
}
137+
}
138+
guard let components = URLComponents(url: self.uri.arbitrarySchemeURL, resolvingAgainstBaseURL: false) else {
139+
throw InvalidTargetIdentifierError(target: self)
140+
}
141+
guard components.scheme == "compilecommands", components.host == "target" else {
142+
throw InvalidTargetIdentifierError(target: self)
143+
}
144+
let compiler = components.queryItems?.last(where: { $0.name == "compiler" })?.value
145+
146+
guard let compiler else {
147+
throw InvalidTargetIdentifierError(target: self)
148+
}
149+
150+
return compiler
151+
}
152+
}
153+
}
154+
99155
#if compiler(>=6)
100156
extension BuildTargetIdentifier: CustomLogStringConvertible {
101157
package var description: String {

Sources/BuildSystemIntegration/CompilationDatabase.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,13 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
#if compiler(>=6)
14-
package import BuildServerProtocol
1514
package import Foundation
1615
package import LanguageServerProtocol
1716
import LanguageServerProtocolExtensions
1817
import SKLogging
1918
import SwiftExtensions
2019
import TSCExtensions
2120
#else
22-
import BuildServerProtocol
2321
import Foundation
2422
import LanguageServerProtocol
2523
import LanguageServerProtocolExtensions
@@ -164,12 +162,6 @@ package struct JSONCompilationDatabase: Equatable, Codable {
164162
return []
165163
}
166164

167-
package var sourceItems: [SourceItem] {
168-
return commands.map {
169-
SourceItem(uri: $0.uri, kind: .file, generated: false)
170-
}
171-
}
172-
173165
private mutating func add(_ command: CompilationDatabaseCompileCommand) {
174166
let uri = command.uri
175167
pathToCommands[uri, default: []].append(commands.count)

Sources/BuildSystemIntegration/JSONCompilationDatabaseBuildSystem.swift

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,27 @@ import SwiftExtensions
1717
package import BuildServerProtocol
1818
package import Foundation
1919
package import LanguageServerProtocol
20+
package import ToolchainRegistry
2021
#else
2122
import BuildServerProtocol
2223
import Foundation
2324
import LanguageServerProtocol
25+
import ToolchainRegistry
2426
#endif
2527

28+
fileprivate extension CompilationDatabaseCompileCommand {
29+
/// The first entry in the command line identifies the compiler that should be used to compile the file and can thus
30+
/// be used to infer the toolchain.
31+
///
32+
/// Note that this compiler does not necessarily need to exist on disk. Eg. tools may just use `clang` as the compiler
33+
/// without specifying a path.
34+
///
35+
/// The absence of a compiler means we have an empty command line, which should never happen.
36+
var compiler: String? {
37+
return commandLine.first
38+
}
39+
}
40+
2641
/// A `BuildSystem` that provides compiler arguments from a `compile_commands.json` file.
2742
package actor JSONCompilationDatabaseBuildSystem: BuiltInBuildSystem {
2843
package static let dbName: String = "compile_commands.json"
@@ -36,6 +51,8 @@ package actor JSONCompilationDatabaseBuildSystem: BuiltInBuildSystem {
3651
}
3752
}
3853

54+
private let toolchainRegistry: ToolchainRegistry
55+
3956
private let connectionToSourceKitLSP: any Connection
4057

4158
package let configPath: URL
@@ -73,34 +90,55 @@ package actor JSONCompilationDatabaseBuildSystem: BuiltInBuildSystem {
7390

7491
package init(
7592
configPath: URL,
93+
toolchainRegistry: ToolchainRegistry,
7694
connectionToSourceKitLSP: any Connection
7795
) throws {
7896
self.compdb = try JSONCompilationDatabase(file: configPath)
97+
self.toolchainRegistry = toolchainRegistry
7998
self.connectionToSourceKitLSP = connectionToSourceKitLSP
80-
8199
self.configPath = configPath
82100
}
83101

84102
package func buildTargets(request: WorkspaceBuildTargetsRequest) async throws -> WorkspaceBuildTargetsResponse {
85-
return WorkspaceBuildTargetsResponse(targets: [
86-
BuildTarget(
87-
id: .dummy,
103+
let compilers = Set(compdb.commands.compactMap(\.compiler)).sorted { $0 < $1 }
104+
let targets = try await compilers.asyncMap { compiler in
105+
let toolchainUri: URI? =
106+
if let toolchainPath = await toolchainRegistry.toolchain(withCompiler: URL(fileURLWithPath: compiler))?.path {
107+
URI(toolchainPath)
108+
} else {
109+
nil
110+
}
111+
return BuildTarget(
112+
id: try BuildTargetIdentifier.createCompileCommands(compiler: compiler),
88113
displayName: nil,
89114
baseDirectory: nil,
90115
tags: [.test],
91116
capabilities: BuildTargetCapabilities(),
92117
// Be conservative with the languages that might be used in the target. SourceKit-LSP doesn't use this property.
93118
languageIds: [.c, .cpp, .objective_c, .objective_cpp, .swift],
94-
dependencies: []
119+
dependencies: [],
120+
dataKind: .sourceKit,
121+
data: SourceKitBuildTarget(toolchain: toolchainUri).encodeToLSPAny()
95122
)
96-
])
123+
}
124+
return WorkspaceBuildTargetsResponse(targets: targets)
97125
}
98126

99127
package func buildTargetSources(request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse {
100-
guard request.targets.contains(.dummy) else {
101-
return BuildTargetSourcesResponse(items: [])
128+
let items = request.targets.compactMap { (target) -> SourcesItem? in
129+
guard let targetCompiler = orLog("Compiler for target", { try target.compileCommandsCompiler }) else {
130+
return nil
131+
}
132+
let commandsWithRequestedCompilers = compdb.commands.lazy.filter { command in
133+
return targetCompiler == command.compiler
134+
}
135+
let sources = commandsWithRequestedCompilers.map {
136+
SourceItem(uri: $0.uri, kind: .file, generated: false)
137+
}
138+
return SourcesItem(target: target, sources: Array(sources))
102139
}
103-
return BuildTargetSourcesResponse(items: [SourcesItem(target: .dummy, sources: compdb.sourceItems)])
140+
141+
return BuildTargetSourcesResponse(items: items)
104142
}
105143

106144
package func didChangeWatchedFiles(notification: OnWatchedFilesDidChangeNotification) {
@@ -116,12 +154,16 @@ package actor JSONCompilationDatabaseBuildSystem: BuiltInBuildSystem {
116154
package func sourceKitOptions(
117155
request: TextDocumentSourceKitOptionsRequest
118156
) async throws -> TextDocumentSourceKitOptionsResponse? {
119-
guard let cmd = compdb[request.textDocument.uri].first else {
157+
let targetCompiler = try request.target.compileCommandsCompiler
158+
let command = compdb[request.textDocument.uri].filter {
159+
$0.compiler == targetCompiler
160+
}.first
161+
guard let command else {
120162
return nil
121163
}
122164
return TextDocumentSourceKitOptionsResponse(
123-
compilerArguments: Array(cmd.commandLine.dropFirst()),
124-
workingDirectory: cmd.directory
165+
compilerArguments: Array(command.commandLine.dropFirst()),
166+
workingDirectory: command.directory
125167
)
126168
}
127169

Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -75,32 +75,10 @@ extension BuildDestinationIdentifier {
7575

7676
extension BuildTargetIdentifier {
7777
fileprivate init(_ buildTarget: any SwiftBuildTarget) throws {
78-
try self.init(target: buildTarget.name, destination: BuildDestinationIdentifier(buildTarget.destination))
79-
}
80-
81-
fileprivate static let forPackageManifest = BuildTargetIdentifier(uri: try! URI(string: "swiftpm://package-manifest"))
82-
83-
fileprivate var targetProperties: (target: String, runDestination: String) {
84-
get throws {
85-
struct InvalidTargetIdentifierError: Swift.Error, CustomStringConvertible {
86-
var target: BuildTargetIdentifier
87-
88-
var description: String {
89-
return "Invalid target identifier \(target)"
90-
}
91-
}
92-
guard let components = URLComponents(url: self.uri.arbitrarySchemeURL, resolvingAgainstBaseURL: false) else {
93-
throw InvalidTargetIdentifierError(target: self)
94-
}
95-
let target = components.queryItems?.last(where: { $0.name == "target" })?.value
96-
let runDestination = components.queryItems?.last(where: { $0.name == "destination" })?.value
97-
98-
guard let target, let runDestination else {
99-
throw InvalidTargetIdentifierError(target: self)
100-
}
101-
102-
return (target, runDestination)
103-
}
78+
self = try Self.createSwiftPM(
79+
target: buildTarget.name,
80+
destination: BuildDestinationIdentifier(buildTarget.destination)
81+
)
10482
}
10583
}
10684

@@ -604,7 +582,7 @@ package actor SwiftPMBuildSystem: BuiltInBuildSystem {
604582
"--package-path", try projectRoot.filePath,
605583
"--scratch-path", self.swiftPMWorkspace.location.scratchDirectory.pathString,
606584
"--disable-index-store",
607-
"--target", try target.targetProperties.target,
585+
"--target", try target.swiftpmTargetProperties.target,
608586
]
609587
if options.swiftPMOrDefault.disableSandbox ?? false {
610588
arguments += ["--disable-sandbox"]

Sources/SKTestSupport/MultiFileTestProject.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,20 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import SwiftExtensions
14+
1315
#if compiler(>=6)
1416
package import Foundation
1517
package import LanguageServerProtocol
1618
package import SKOptions
1719
package import SourceKitLSP
18-
import SwiftExtensions
20+
package import ToolchainRegistry
1921
#else
2022
import Foundation
2123
import LanguageServerProtocol
2224
import SKOptions
2325
import SourceKitLSP
24-
import SwiftExtensions
26+
import ToolchainRegistry
2527
#endif
2628

2729
/// The location of a test file within test workspace.
@@ -137,6 +139,7 @@ package class MultiFileTestProject {
137139
initializationOptions: LSPAny? = nil,
138140
capabilities: ClientCapabilities = ClientCapabilities(),
139141
options: SourceKitLSPOptions = .testDefault(),
142+
toolchainRegistry: ToolchainRegistry = .forTesting,
140143
hooks: Hooks = Hooks(),
141144
enableBackgroundIndexing: Bool = false,
142145
usePullDiagnostics: Bool = true,
@@ -152,6 +155,7 @@ package class MultiFileTestProject {
152155
hooks: hooks,
153156
initializationOptions: initializationOptions,
154157
capabilities: capabilities,
158+
toolchainRegistry: toolchainRegistry,
155159
usePullDiagnostics: usePullDiagnostics,
156160
enableBackgroundIndexing: enableBackgroundIndexing,
157161
workspaceFolders: workspaces(scratchDirectory),

Sources/SKTestSupport/TestSourceKitLSPClient.swift

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,26 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
#if compiler(>=6)
1413
import Foundation
1514
import InProcessClient
16-
package import LanguageServerProtocol
17-
import LanguageServerProtocolJSONRPC
1815
import LanguageServerProtocolExtensions
19-
package import SKOptions
16+
import LanguageServerProtocolJSONRPC
2017
import SKUtilities
2118
import SourceKitD
22-
package import SourceKitLSP
2319
import SwiftExtensions
2420
import SwiftSyntax
25-
import ToolchainRegistry
2621
import XCTest
22+
23+
#if compiler(>=6)
24+
package import LanguageServerProtocol
25+
package import SKOptions
26+
package import SourceKitLSP
27+
package import ToolchainRegistry
2728
#else
28-
import Foundation
29-
import InProcessClient
3029
import LanguageServerProtocol
31-
import LanguageServerProtocolJSONRPC
32-
import LanguageServerProtocolExtensions
3330
import SKOptions
34-
import SKUtilities
35-
import SourceKitD
3631
import SourceKitLSP
37-
import SwiftExtensions
38-
import SwiftSyntax
3932
import ToolchainRegistry
40-
import XCTest
4133
#endif
4234

4335
extension SourceKitLSPOptions {
@@ -150,6 +142,7 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable {
150142
initialize: Bool = true,
151143
initializationOptions: LSPAny? = nil,
152144
capabilities: ClientCapabilities = ClientCapabilities(),
145+
toolchainRegistry: ToolchainRegistry = .forTesting,
153146
usePullDiagnostics: Bool = true,
154147
enableBackgroundIndexing: Bool = false,
155148
workspaceFolders: [WorkspaceFolder]? = nil,
@@ -172,7 +165,7 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable {
172165
self.serverToClientConnection = serverToClientConnection
173166
server = SourceKitLSPServer(
174167
client: serverToClientConnection,
175-
toolchainRegistry: ToolchainRegistry.forTesting,
168+
toolchainRegistry: toolchainRegistry,
176169
options: options,
177170
hooks: hooks,
178171
onExit: {

0 commit comments

Comments
 (0)