Skip to content

Commit 0f72152

Browse files
authored
Merge pull request #761 from ahoppen/bsp-requests
Add requests that can be used to implement a BSP server based on swift-builds build description
2 parents f59c406 + b9e4b1e commit 0f72152

24 files changed

+1044
-11
lines changed

Sources/SWBApplePlatform/CoreMLCompiler.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ fileprivate struct CoreMLTaskPayload: TaskPayload, Encodable {
4848

4949
/// The indexing info for a CoreML file. This will be sent to the client in a property list format described below.
5050
fileprivate enum CoreMLIndexingInfo: Serializable, SourceFileIndexingInfo, Encodable {
51+
public var compilerArguments: [String]? { nil }
52+
public var indexOutputFile: String? { nil }
53+
public var language: IndexingInfoLanguage? { nil }
54+
5155
/// Setting up the code generation task did not generate an error. It might have been disabled, which is treated as success.
5256
case success(generatedFilePaths: [Path]?, languageToGenerate: String, notice: String?)
5357
/// Setting up the code generation task failed, and we use the index to propagate an error back to Xcode.

Sources/SWBApplePlatform/IntentsCompiler.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ fileprivate struct IntentsTaskPayload: TaskPayload, Encodable {
4343

4444
/// The indexing info for an Intents file. This will be sent to the client in a property list format described below.
4545
fileprivate enum IntentsIndexingInfo: Serializable, SourceFileIndexingInfo, Encodable {
46+
public var compilerArguments: [String]? { nil }
47+
public var indexOutputFile: String? { nil }
48+
public var language: IndexingInfoLanguage? { nil }
49+
4650
/// Setting up the code generation task did not generate an error. It might have been disabled, which is treated as success.
4751
case success(generatedFilePaths: [Path])
4852
/// Setting up the code generation task failed, and we use the index to propagate an error back to Xcode.

Sources/SWBApplePlatform/MetalCompiler.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ struct MetalSourceFileIndexingInfo: SourceFileIndexingInfo {
2020
let commandLine: [ByteString]
2121
let builtProductsDir: Path
2222
let toolchains: [String]
23+
var compilerArguments: [String]? { commandLine.map { $0.asString } }
24+
var indexOutputFile: String? { outputFile.str }
25+
public var language: IndexingInfoLanguage? { .metal }
2326

2427
init(outputFile: Path, commandLine: [ByteString], builtProductsDir: Path, toolchains: [String]) {
2528
self.outputFile = outputFile
@@ -49,7 +52,7 @@ struct MetalSourceFileIndexingInfo: SourceFileIndexingInfo {
4952

5053
extension OutputPathIndexingInfo {
5154
fileprivate init(task: any ExecutableTask, payload: MetalIndexingPayload) {
52-
self.init(outputFile: Path(task.commandLine[payload.outputFileIndex].asString))
55+
self.init(outputFile: Path(task.commandLine[payload.outputFileIndex].asString), language: .metal)
5356
}
5457
}
5558

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SWBBuildSystem
14+
import SWBCore
15+
import SWBProtocol
16+
import SWBServiceCore
17+
import SWBTaskConstruction
18+
import SWBTaskExecution
19+
import SWBUtil
20+
21+
// MARK: - Retrieve build description
22+
23+
/// Message that contains enough information to load a build description
24+
private protocol BuildDescriptionMessage: SessionMessage {
25+
/// The ID of the build description from which to load the configured targets
26+
var buildDescriptionID: BuildDescriptionID { get }
27+
28+
/// The build request that was used to generate the build description with the given ID.
29+
var request: BuildRequestMessagePayload { get }
30+
}
31+
32+
extension BuildDescriptionConfiguredTargetsRequest: BuildDescriptionMessage {}
33+
extension BuildDescriptionConfiguredTargetSourcesRequest: BuildDescriptionMessage {}
34+
extension IndexBuildSettingsRequest: BuildDescriptionMessage {}
35+
36+
fileprivate extension Request {
37+
struct BuildDescriptionDoesNotExistError: Error {}
38+
39+
func buildDescription(for message: some BuildDescriptionMessage) async throws -> BuildDescription {
40+
return try await buildRequestAndDescription(for: message).description
41+
}
42+
43+
func buildRequestAndDescription(for message: some BuildDescriptionMessage) async throws -> (request: BuildRequest, description: BuildDescription) {
44+
let session = try self.session(for: message)
45+
guard let workspaceContext = session.workspaceContext else {
46+
throw MsgParserError.missingWorkspaceContext
47+
}
48+
let buildRequest = try BuildRequest(from: message.request, workspace: workspaceContext.workspace)
49+
let buildRequestContext = BuildRequestContext(workspaceContext: workspaceContext)
50+
let clientDelegate = ClientExchangeDelegate(request: self, session: session)
51+
let operation = IndexingOperation(workspace: workspaceContext.workspace)
52+
let buildDescription = try await session.buildDescriptionManager.getNewOrCachedBuildDescription(
53+
.cachedOnly(
54+
message.buildDescriptionID,
55+
request: buildRequest,
56+
buildRequestContext: buildRequestContext,
57+
workspaceContext: workspaceContext
58+
), clientDelegate: clientDelegate, constructionDelegate: operation
59+
)?.buildDescription
60+
61+
guard let buildDescription else {
62+
throw BuildDescriptionDoesNotExistError()
63+
}
64+
return (buildRequest, buildDescription)
65+
}
66+
}
67+
68+
// MARK: - Message handlers
69+
70+
struct BuildDescriptionConfiguredTargetsMsg: MessageHandler {
71+
/// Compute the toolchains that can handle all Swift and clang compilation tasks in the given target.
72+
private func toolchainIDs(in configuredTarget: ConfiguredTarget, of buildDescription: BuildDescription) -> [String]? {
73+
var toolchains: [String]?
74+
75+
for task in buildDescription.taskStore.tasksForTarget(configuredTarget) {
76+
let targetToolchains: [String]? =
77+
switch task.payload {
78+
case let payload as SwiftTaskPayload: payload.indexingPayload.toolchains
79+
case let payload as ClangTaskPayload: payload.indexingPayload?.toolchains
80+
default: nil
81+
}
82+
guard let targetToolchains else {
83+
continue
84+
}
85+
if let unwrappedToolchains = toolchains {
86+
toolchains = unwrappedToolchains.filter { targetToolchains.contains($0) }
87+
} else {
88+
toolchains = targetToolchains
89+
}
90+
}
91+
92+
return toolchains
93+
}
94+
95+
func handle(request: Request, message: BuildDescriptionConfiguredTargetsRequest) async throws -> BuildDescriptionConfiguredTargetsResponse {
96+
let buildDescription = try await request.buildDescription(for: message)
97+
98+
let dependencyRelationships = Dictionary(
99+
buildDescription.targetDependencies.map { (ConfiguredTarget.GUID(id: $0.target.guid), [$0]) },
100+
uniquingKeysWith: { $0 + $1 }
101+
)
102+
103+
let session = try request.session(for: message)
104+
105+
let targetInfos = buildDescription.allConfiguredTargets.map { configuredTarget in
106+
let toolchain: Path?
107+
if let toolchainID = toolchainIDs(in: configuredTarget, of: buildDescription)?.first {
108+
toolchain = session.core.toolchainRegistry.lookup(toolchainID)?.path
109+
if toolchain == nil {
110+
log("Unable to find path for toolchain with identifier \(toolchainID)", isError: true)
111+
}
112+
} else {
113+
log("Unable to find toolchain for \(configuredTarget)", isError: true)
114+
toolchain = nil
115+
}
116+
117+
let dependencyRelationships = dependencyRelationships[configuredTarget.guid]
118+
return BuildDescriptionConfiguredTargetsResponse.ConfiguredTargetInfo(
119+
guid: ConfiguredTargetGUID(configuredTarget.guid.stringValue),
120+
target: TargetGUID(rawValue: configuredTarget.target.guid),
121+
name: configuredTarget.target.name,
122+
dependencies: Set(dependencyRelationships?.flatMap(\.targetDependencies).map { ConfiguredTargetGUID($0.guid) } ?? []),
123+
toolchain: toolchain
124+
)
125+
}
126+
return BuildDescriptionConfiguredTargetsResponse(configuredTargets: targetInfos)
127+
}
128+
}
129+
130+
fileprivate extension SourceLanguage {
131+
init?(_ language: IndexingInfoLanguage?) {
132+
switch language {
133+
case nil: return nil
134+
case .c: self = .c
135+
case .cpp: self = .cpp
136+
case .metal: self = .metal
137+
case .objectiveC: self = .objectiveC
138+
case .objectiveCpp: self = .objectiveCpp
139+
case .swift: self = .swift
140+
}
141+
}
142+
}
143+
144+
struct BuildDescriptionConfiguredTargetSourcesMsg: MessageHandler {
145+
private struct UnknownConfiguredTargetIDError: Error, CustomStringConvertible {
146+
let configuredTarget: ConfiguredTargetGUID
147+
var description: String { "Unknown configured target: \(configuredTarget)" }
148+
}
149+
150+
typealias SourceFileInfo = BuildDescriptionConfiguredTargetSourcesResponse.SourceFileInfo
151+
typealias ConfiguredTargetSourceFilesInfo = BuildDescriptionConfiguredTargetSourcesResponse.ConfiguredTargetSourceFilesInfo
152+
153+
func handle(request: Request, message: BuildDescriptionConfiguredTargetSourcesRequest) async throws -> BuildDescriptionConfiguredTargetSourcesResponse {
154+
let buildDescription = try await request.buildDescription(for: message)
155+
156+
let configuredTargetsByID = Dictionary(
157+
buildDescription.allConfiguredTargets.map { ($0.guid, $0) }
158+
) { lhs, rhs in
159+
log("Found conflicting targets for the same ID: \(lhs.guid)", isError: true)
160+
return lhs
161+
}
162+
163+
let indexingInfoInput = TaskGenerateIndexingInfoInput(requestedSourceFile: nil, outputPathOnly: true, enableIndexBuildArena: false)
164+
let sourcesItems = try message.configuredTargets.map { configuredTargetGuid in
165+
guard let target = configuredTargetsByID[ConfiguredTarget.GUID(id: configuredTargetGuid.rawValue)] else {
166+
throw UnknownConfiguredTargetIDError(configuredTarget: configuredTargetGuid)
167+
}
168+
let sourceFiles = buildDescription.taskStore.tasksForTarget(target).flatMap { task in
169+
task.generateIndexingInfo(input: indexingInfoInput).compactMap { (entry) -> SourceFileInfo? in
170+
return SourceFileInfo(
171+
path: entry.path,
172+
language: SourceLanguage(entry.indexingInfo.language),
173+
outputPath: entry.indexingInfo.indexOutputFile
174+
)
175+
}
176+
}
177+
return ConfiguredTargetSourceFilesInfo(configuredTarget: configuredTargetGuid, sourceFiles: sourceFiles)
178+
}
179+
return BuildDescriptionConfiguredTargetSourcesResponse(targetSourceFileInfos: sourcesItems)
180+
}
181+
}
182+
183+
struct IndexBuildSettingsMsg: MessageHandler {
184+
private struct AmbiguousIndexingInfoError: Error, CustomStringConvertible {
185+
var description: String { "Found multiple indexing informations for the same source file" }
186+
}
187+
188+
private struct FailedToGetCompilerArgumentsError: Error {}
189+
190+
func handle(request: Request, message: IndexBuildSettingsRequest) async throws -> IndexBuildSettingsResponse {
191+
let (buildRequest, buildDescription) = try await request.buildRequestAndDescription(for: message)
192+
193+
let configuredTarget = buildDescription.allConfiguredTargets.filter { $0.guid.stringValue == message.configuredTarget.rawValue }.only
194+
195+
let indexingInfoInput = TaskGenerateIndexingInfoInput(
196+
requestedSourceFile: message.file,
197+
outputPathOnly: false,
198+
enableIndexBuildArena: buildRequest.enableIndexBuildArena
199+
)
200+
// First find all the tasks that declare the requested source file as an input file. This should narrow the list
201+
// of targets down significantly.
202+
let taskForSourceFile = buildDescription.taskStore.tasksForTarget(configuredTarget)
203+
.filter { $0.inputPaths.contains(message.file) }
204+
// Now get the indexing info for the targets that might be relevant and perform another check to ensure they
205+
// actually represent the requested source file.
206+
let indexingInfos =
207+
taskForSourceFile
208+
.flatMap { $0.generateIndexingInfo(input: indexingInfoInput) }
209+
.filter({ $0.path == message.file })
210+
guard let indexingInfo = indexingInfos.only else {
211+
throw AmbiguousIndexingInfoError()
212+
}
213+
guard let compilerArguments = indexingInfo.indexingInfo.compilerArguments else {
214+
throw FailedToGetCompilerArgumentsError()
215+
}
216+
return IndexBuildSettingsResponse(compilerArguments: compilerArguments)
217+
}
218+
}

Sources/SWBBuildService/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ See http://swift.org/CONTRIBUTORS.txt for Swift project authors
1010

1111
add_library(SWBBuildService
1212
BuildDependencyInfo.swift
13+
BuildDescriptionMessages.swift
1314
BuildOperationMessages.swift
1415
BuildService.swift
1516
BuildServiceEntryPoint.swift

Sources/SWBBuildService/Messages.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,6 +1614,10 @@ package struct ServiceMessageHandlers: ServiceExtension {
16141614
service.registerMessageHandler(ComputeDependencyGraphMsg.self)
16151615
service.registerMessageHandler(DumpBuildDependencyInfoMsg.self)
16161616

1617+
service.registerMessageHandler(BuildDescriptionConfiguredTargetsMsg.self)
1618+
service.registerMessageHandler(BuildDescriptionConfiguredTargetSourcesMsg.self)
1619+
service.registerMessageHandler(IndexBuildSettingsMsg.self)
1620+
16171621
service.registerMessageHandler(MacroEvaluationMsg.self)
16181622
service.registerMessageHandler(AllExportedMacrosAndValuesMsg.self)
16191623
service.registerMessageHandler(BuildSettingsEditorInfoMsg.self)

Sources/SWBCore/ConfiguredTarget.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public final class ConfiguredTarget: Hashable, CustomStringConvertible, Serializ
8282
public struct GUID: Hashable, Sendable, Comparable, CustomStringConvertible {
8383
public let stringValue: String
8484

85-
fileprivate init(id: String) {
85+
public init(id: String) {
8686
self.stringValue = id
8787
}
8888

Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,15 @@ public struct ClangPrefixInfo: Serializable, Hashable, Encodable, Sendable {
9393
}
9494

9595
/// The minimal data we need to serialize to reconstruct `ClangSourceFileIndexingInfo` from `generateIndexingInfo`
96-
fileprivate struct ClangIndexingPayload: Serializable, Encodable, Sendable {
96+
public struct ClangIndexingPayload: Serializable, Encodable, Sendable {
9797
let sourceFileIndex: Int
9898
let outputFileIndex: Int
9999
let sourceLanguageIndex: Int
100100
let builtProductsDir: Path
101101
let assetSymbolIndexPath: Path
102102
let workingDir: Path
103103
let prefixInfo: ClangPrefixInfo?
104-
let toolchains: [String]
104+
public let toolchains: [String]
105105
let responseFileAttachmentPaths: [Path: Path]
106106

107107
init(sourceFileIndex: Int,
@@ -128,7 +128,7 @@ fileprivate struct ClangIndexingPayload: Serializable, Encodable, Sendable {
128128
return Path(task.commandLine[self.sourceFileIndex].asString)
129129
}
130130

131-
func serialize<T: Serializer>(to serializer: T) {
131+
public func serialize<T: Serializer>(to serializer: T) {
132132
serializer.serializeAggregate(9) {
133133
serializer.serialize(sourceFileIndex)
134134
serializer.serialize(outputFileIndex)
@@ -142,7 +142,7 @@ fileprivate struct ClangIndexingPayload: Serializable, Encodable, Sendable {
142142
}
143143
}
144144

145-
init(from deserializer: any Deserializer) throws {
145+
public init(from deserializer: any Deserializer) throws {
146146
try deserializer.beginAggregate(9)
147147
self.sourceFileIndex = try deserializer.deserialize()
148148
self.outputFileIndex = try deserializer.deserialize()
@@ -165,6 +165,19 @@ public struct ClangSourceFileIndexingInfo: SourceFileIndexingInfo {
165165
let assetSymbolIndexPath: Path
166166
let prefixInfo: ClangPrefixInfo?
167167
let toolchains: [String]
168+
public var compilerArguments: [String]? {
169+
var result = commandLine.map { $0.asString }
170+
// commandLine does not contain the `-index-unit-output-path` but we want to return it from the
171+
// `IndexBuildSettingsRequest`, so add it.
172+
if !result.contains(where: { $0 == "-o" || $0 == "-index-unit-output-path" }), let indexOutputFile {
173+
result += ["-index-unit-output-path", indexOutputFile]
174+
}
175+
return result
176+
}
177+
public var indexOutputFile: String? { outputFile.str }
178+
public var language: IndexingInfoLanguage? {
179+
return IndexingInfoLanguage(compilerArgumentLanguage: sourceLanguage)
180+
}
168181

169182
init(outputFile: Path, sourceLanguage: ByteString, commandLine: [ByteString], builtProductsDir: Path, assetSymbolIndexPath: Path, prefixInfo: ClangPrefixInfo?, toolchains: [String]) {
170183
self.outputFile = outputFile
@@ -280,6 +293,7 @@ public struct ClangSourceFileIndexingInfo: SourceFileIndexingInfo {
280293
extension OutputPathIndexingInfo {
281294
fileprivate init(task: any ExecutableTask, payload: ClangIndexingPayload) {
282295
self.outputFile = Path(task.commandLine[payload.outputFileIndex].asString)
296+
self.language = IndexingInfoLanguage(compilerArgumentLanguage: task.commandLine[payload.sourceLanguageIndex].asByteString)
283297
}
284298
}
285299

@@ -422,7 +436,7 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd
422436
public let serializedDiagnosticsPath: Path?
423437

424438
/// Additional information used to answer indexing queries. Not all clang tasks will need to provide indexing info (for example, precompilation tasks don't).
425-
fileprivate let indexingPayload: ClangIndexingPayload?
439+
public let indexingPayload: ClangIndexingPayload?
426440

427441
/// Additional information used by explicit modules support.
428442
public let explicitModulesPayload: ClangExplicitModulesPayload?
@@ -2120,3 +2134,15 @@ public final class ClangModuleVerifierSpec: ClangCompilerSpec, SpecImplementatio
21202134
private func ==(lhs: ClangCompilerSpec.DataCache.ConstantFlagsKey, rhs: ClangCompilerSpec.DataCache.ConstantFlagsKey) -> Bool {
21212135
return ObjectIdentifier(lhs.scope) == ObjectIdentifier(rhs.scope) && lhs.inputFileType == rhs.inputFileType
21222136
}
2137+
2138+
private extension IndexingInfoLanguage {
2139+
init?(compilerArgumentLanguage: ByteString) {
2140+
switch compilerArgumentLanguage {
2141+
case "c": self = .c
2142+
case "c++": self = .cpp
2143+
case "objective-c": self = .objectiveC
2144+
case "objective-c++": self = .objectiveCpp
2145+
default: return nil
2146+
}
2147+
}
2148+
}

0 commit comments

Comments
 (0)