Skip to content

Commit aaf2926

Browse files
committed
experimental-build-server should provide compiler arguments for header files
1 parent a88e0e0 commit aaf2926

File tree

7 files changed

+173
-21
lines changed

7 files changed

+173
-21
lines changed

Sources/SwiftBuildSupport/PIFBuilder.swift

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ import var TSCBasic.stdoutStream
2525

2626
import enum SwiftBuild.ProjectModel
2727

28+
public struct PIFGenerationResult {
29+
public var pif: String
30+
public var accompanyingMetadata: [PackagePIFBuilder.ModuleOrProduct]
31+
}
32+
2833
fileprivate func memoize<T>(to cache: inout T?, build: () async throws -> T) async rethrows -> T {
2934
if let value = cache {
3035
return value
@@ -153,14 +158,14 @@ public final class PIFBuilder {
153158
printPIFManifestGraphviz: Bool = false,
154159
buildParameters: BuildParameters,
155160
hostBuildParameters: BuildParameters
156-
) async throws -> String {
161+
) async throws -> PIFGenerationResult {
157162
let encoder = prettyPrint ? JSONEncoder.makeWithDefaults() : JSONEncoder()
158163

159164
if !preservePIFModelStructure {
160165
encoder.userInfo[.encodeForSwiftBuild] = true
161166
}
162167

163-
let topLevelObject = try await self.constructPIF(buildParameters: buildParameters, hostBuildParameters: hostBuildParameters)
168+
let (topLevelObject, modulesAndProducts) = try await self.constructPIF(buildParameters: buildParameters, hostBuildParameters: hostBuildParameters)
164169

165170
// Sign the PIF objects before encoding it for Swift Build.
166171
try PIF.sign(workspace: topLevelObject.workspace)
@@ -178,10 +183,10 @@ public final class PIFBuilder {
178183
throw PIFGenerationError.printedPIFManifestGraphviz
179184
}
180185

181-
return pifString
186+
return PIFGenerationResult(pif: pifString, accompanyingMetadata: modulesAndProducts)
182187
}
183188

184-
private var cachedPIF: PIF.TopLevelObject?
189+
private var cachedPIF: (PIF.TopLevelObject, [PackagePIFBuilder.ModuleOrProduct])?
185190

186191
/// Compute the available build tools, and their destination build path for host for each plugin.
187192
private func availableBuildPluginTools(
@@ -444,7 +449,7 @@ public final class PIFBuilder {
444449
package func constructPIF(
445450
buildParameters: BuildParameters,
446451
hostBuildParameters: BuildParameters
447-
) async throws -> PIF.TopLevelObject {
452+
) async throws -> (PIF.TopLevelObject, [PackagePIFBuilder.ModuleOrProduct]) {
448453
return try await memoize(to: &self.cachedPIF) {
449454
let rootPackages = self.graph.rootPackages
450455
guard !rootPackages.isEmpty else {
@@ -453,8 +458,10 @@ public final class PIFBuilder {
453458

454459
let packagesAndPIFBuilders = try await makePIFBuilders(buildParameters: buildParameters, hostBuildParameters: hostBuildParameters)
455460

461+
var modulesAndProducts: [PackagePIFBuilder.ModuleOrProduct] = []
456462
let packagesAndPIFProjects = try packagesAndPIFBuilders.map { (package, pifBuilder, _) in
457-
try pifBuilder.build()
463+
let builtModulesAndProducts = try pifBuilder.build()
464+
modulesAndProducts.append(contentsOf: builtModulesAndProducts)
458465
let pifProject: ProjectModel.Project = pifBuilder.pifProject
459466
return (package, pifProject)
460467
}
@@ -479,7 +486,7 @@ public final class PIFBuilder {
479486
path: try getCommonParentDirectory(paths: rootPackagesPaths),
480487
projects: pifProjects
481488
)
482-
return PIF.TopLevelObject(workspace: workspace)
489+
return (PIF.TopLevelObject(workspace: workspace), modulesAndProducts)
483490
}
484491
}
485492

@@ -555,7 +562,7 @@ public final class PIFBuilder {
555562
addLocalRpaths: Bool,
556563
materializeStaticArchiveProductsForRootPackages: Bool,
557564
createDynamicVariantsForLibraryProducts: Bool
558-
) async throws -> String {
565+
) async throws -> PIFGenerationResult {
559566
let parameters = PIFBuilderParameters(
560567
buildParameters,
561568
supportedSwiftVersions: [],

Sources/SwiftBuildSupport/SwiftBuildSystem.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1259,17 +1259,20 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
12591259
}
12601260
}
12611261

1262-
public func generatePIF(preserveStructure: Bool) async throws -> String {
1262+
public func generatePIFAndAccompanyingMetadata(preserveStructure: Bool) async throws -> PIFGenerationResult {
12631263
pifBuilder = .init()
12641264
packageGraph = .init()
12651265
let pifBuilder = try await getPIFBuilder()
1266-
let pif = try await pifBuilder.generatePIF(
1266+
return try await pifBuilder.generatePIF(
12671267
preservePIFModelStructure: preserveStructure,
12681268
printPIFManifestGraphviz: buildParameters.printPIFManifestGraphviz,
12691269
buildParameters: buildParameters,
12701270
hostBuildParameters: hostBuildParameters
12711271
)
1272-
return pif
1272+
}
1273+
1274+
public func generatePIF(preserveStructure: Bool) async throws -> String {
1275+
return try await generatePIFAndAccompanyingMetadata(preserveStructure: preserveStructure).pif
12731276
}
12741277

12751278
public func writePIF(buildParameters: BuildParameters) async throws {

Sources/SwiftPMBuildServer/SwiftPMBuildServer.swift

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ public actor SwiftPMBuildServer: QueueBasedMessageHandler {
111111
}
112112
}
113113
var state: ServerState = .waitingForInitializeRequest
114+
115+
private var headersByTargetGUID: [String: Set<Basics.AbsolutePath>] = [:]
116+
114117
/// Allows customization of server exit behavior.
115118
var exitHandler: (Int) -> Void
116119

@@ -213,6 +216,26 @@ public actor SwiftPMBuildServer: QueueBasedMessageHandler {
213216
await logToClient(.warning, "SwiftPM build server processed target sources request for unexpected target '\(target)'")
214217
}
215218
}
219+
220+
// Add entries for the target's headers, which are not currently represented in the PIF.
221+
for index in sourcesResponse.items.indices {
222+
guard let targetGUID = sourcesResponse.items[index].target.targetGUID else {
223+
logToClient(.warning, "Unable to determine target GUID for \(sourcesResponse.items[index].target) when looking up headers")
224+
continue
225+
}
226+
let headers = self.headersByTargetGUID[targetGUID] ?? []
227+
for header in headers {
228+
sourcesResponse.items[index].sources.append(
229+
SourceItem(
230+
uri: DocumentURI(header.asURL),
231+
kind: .file,
232+
generated: false,
233+
dataKind: .sourceKit,
234+
data: SourceKitSourceItemData(kind: .header).encodeToLSPAny()
235+
)
236+
)
237+
}
238+
}
216239
return sourcesResponse
217240
}
218241
case let request as RequestAndReply<InitializeBuildRequest>:
@@ -221,9 +244,15 @@ public actor SwiftPMBuildServer: QueueBasedMessageHandler {
221244
await request.reply {
222245
if request.params.target.isSwiftPMBuildServerTargetID {
223246
return try await manifestSourceKitOptions(request: request.params)
224-
} else {
225-
return try await connectionToUnderlyingBuildServer.send(request.params)
226247
}
248+
if let targetGUID = request.params.target.targetGUID,
249+
let headers = headersByTargetGUID[targetGUID],
250+
let requestPath = try? request.params.textDocument.uri.fileURL?.filePath,
251+
headers.contains(requestPath),
252+
let response = try await self.headerSourceKitOptions(request: request.params){
253+
return response
254+
}
255+
return try await connectionToUnderlyingBuildServer.send(request.params)
227256
}
228257
case let request as RequestAndReply<WorkspaceBuildTargetsRequest>:
229258
await request.reply {
@@ -314,6 +343,79 @@ public actor SwiftPMBuildServer: QueueBasedMessageHandler {
314343
return TextDocumentSourceKitOptionsResponse(compilerArguments: compilerArgs)
315344
}
316345

346+
/// If the requested file is a known header, returns compiler arguments derived from a substitute source file
347+
private func headerSourceKitOptions(
348+
request: TextDocumentSourceKitOptionsRequest
349+
) async throws -> TextDocumentSourceKitOptionsResponse? {
350+
guard let fileURL = request.textDocument.uri.fileURL,
351+
let filePath = try? fileURL.filePath else {
352+
return nil
353+
}
354+
355+
var substituteSourceFile: URI? = nil
356+
let sourcesResponse = try await connectionToUnderlyingBuildServer.send(BuildTargetSourcesRequest(targets: [request.target]))
357+
for sourcesItem in sourcesResponse.items {
358+
for sourceFile in sourcesItem.sources {
359+
let language = SourceKitSourceItemData(fromLSPAny: sourceFile.data)?.language
360+
switch language {
361+
case .c, .cpp, .objective_c, .objective_cpp, nil:
362+
// SourceKit-LSP historically chose the first source file of a C-family target as the substitute.
363+
// Here, we specifically look for the first C/C++/ObjC/ObjC++ file so this is futureproof against
364+
// mixed Swift/C-family targets. However, we may also want to consider if e.g. a .hpp header should
365+
// use a C++ source file over a C source file if a target has both.
366+
substituteSourceFile = sourceFile.uri
367+
default:
368+
break
369+
}
370+
}
371+
}
372+
373+
guard let substituteSourceFile, let substituteSourceFilePath = try? substituteSourceFile.fileURL?.filePath else {
374+
logToClient(.info, "Unable to find a substitute source file for '\(filePath)'")
375+
return nil
376+
}
377+
logToClient(.info, "Getting compiler arguments for '\(filePath)' using substitute file '\(substituteSourceFilePath)'")
378+
379+
let substituteRequest = TextDocumentSourceKitOptionsRequest(
380+
textDocument: TextDocumentIdentifier(substituteSourceFile),
381+
target: request.target,
382+
language: request.language
383+
)
384+
guard let substituteResponse = try await connectionToUnderlyingBuildServer.send(substituteRequest) else {
385+
return nil
386+
}
387+
388+
// Replace the substitute file path with the header path
389+
// It's possible the arguments use relative paths while the `originalFile` given
390+
// is an absolute/real path value. We guess based on suffixes instead of hitting
391+
// the file system. Copied from SourceKit-LSP
392+
var arguments = substituteResponse.compilerArguments
393+
let substituteBasename = substituteSourceFilePath.basename
394+
if let index = arguments.lastIndex(where: {
395+
$0.hasSuffix(substituteBasename) && substituteSourceFilePath.pathString.hasSuffix($0)
396+
}) {
397+
arguments[index] = filePath.pathString
398+
}
399+
400+
return TextDocumentSourceKitOptionsResponse(
401+
compilerArguments: arguments,
402+
workingDirectory: substituteResponse.workingDirectory,
403+
data: substituteResponse.data
404+
)
405+
}
406+
407+
private func rebuildHeaderMapping(pifAccompanyingMetadata: [PackagePIFBuilder.ModuleOrProduct]) async {
408+
var headers: [String: Set<Basics.AbsolutePath>] = [:]
409+
for moduleOrProduct in pifAccompanyingMetadata {
410+
guard let pifTarget = moduleOrProduct.pifTarget else { continue }
411+
let guid = pifTarget.id.value
412+
if !moduleOrProduct.headerFiles.isEmpty {
413+
headers[guid] = moduleOrProduct.headerFiles
414+
}
415+
}
416+
self.headersByTargetGUID = headers
417+
}
418+
317419
private func shutdown() -> VoidResponse {
318420
state = .shutdown
319421
return VoidResponse()
@@ -345,7 +447,9 @@ public actor SwiftPMBuildServer: QueueBasedMessageHandler {
345447
public func scheduleRegeneratingBuildDescription() {
346448
packageLoadingQueue.async { [buildSystem] in
347449
do {
348-
try await buildSystem.writePIF(buildParameters: buildSystem.buildParameters)
450+
let result = try await buildSystem.generatePIFAndAccompanyingMetadata(preserveStructure: false)
451+
try localFileSystem.writeIfChanged(path: buildSystem.buildParameters.pifManifest, string: result.pif)
452+
await self.rebuildHeaderMapping(pifAccompanyingMetadata: result.accompanyingMetadata)
349453
self.connectionToUnderlyingBuildServer.send(OnWatchedFilesDidChangeNotification(changes: [
350454
.init(uri: .init(buildSystem.buildParameters.pifManifest.asURL), type: .changed)
351455
]))
@@ -365,5 +469,13 @@ extension BuildTargetIdentifier {
365469
var isSwiftPMBuildServerTargetID: Bool {
366470
uri.scheme == Self.swiftPMBuildServerTargetScheme
367471
}
472+
473+
var targetGUID: String? {
474+
guard let components = URLComponents(url: uri.arbitrarySchemeURL, resolvingAgainstBaseURL: false),
475+
let value = components.queryItems?.last(where: { $0.name == "targetGUID" })?.value else {
476+
return nil
477+
}
478+
return value
479+
}
368480
}
369481
#endif

Tests/SwiftBuildSupportTests/CGenPIFTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ import SwiftBuild
199199
destination: .host,
200200
buildSystemKind: .swiftbuild,
201201
)
202-
)
202+
).0
203203
}
204204

205205
/// This is more to test out that the setup routines provide a good test environment

Tests/SwiftBuildSupportTests/PIFBuilderTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ fileprivate func withGeneratedPIF(
9999
fileSystem: localFileSystem,
100100
observabilityScope: observabilitySystem.topScope
101101
)
102-
let pif = try await builder.constructPIF(
102+
let (pif, _) = try await builder.constructPIF(
103103
buildParameters: buildParameters,
104104
hostBuildParameters: hostBuildParameters
105105
)
@@ -391,7 +391,7 @@ struct PIFBuilderTests {
391391
)
392392

393393
// Act
394-
let pif = try await pifBuilder.constructPIF(
394+
let (pif, _) = try await pifBuilder.constructPIF(
395395
buildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild),
396396
hostBuildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild)
397397
)
@@ -826,7 +826,7 @@ struct PIFBuilderTests {
826826
fileSystem: fs,
827827
observabilityScope: observability.topScope
828828
)
829-
let pif = try await pifBuilder.constructPIF(
829+
let (pif, _) = try await pifBuilder.constructPIF(
830830
buildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild),
831831
hostBuildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild)
832832
)
@@ -965,7 +965,7 @@ struct PIFBuilderTests {
965965
observabilityScope: observability.topScope
966966
)
967967

968-
let pif = try await pifBuilder.constructPIF(
968+
let (pif, _) = try await pifBuilder.constructPIF(
969969
buildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild),
970970
hostBuildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild)
971971
)

Tests/SwiftBuildSupportTests/PrebuiltsPIFTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ struct PrebuiltsPIFTests {
173173
fileSystem: fs,
174174
observabilityScope: observability.topScope
175175
)
176-
let pif = try await pifBuilder.constructPIF(
176+
let (pif, _) = try await pifBuilder.constructPIF(
177177
buildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild),
178178
hostBuildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild)
179179
)
@@ -373,7 +373,7 @@ struct PrebuiltsPIFTests {
373373
fileSystem: fs,
374374
observabilityScope: observability.topScope
375375
)
376-
let pif = try await pifBuilder.constructPIF(
376+
let (pif, _) = try await pifBuilder.constructPIF(
377377
buildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild),
378378
hostBuildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild)
379379
)

Tests/SwiftPMBuildServerTests/BuildServerTests.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,36 @@ struct SwiftPMBuildServerTests {
177177
}
178178
}
179179

180+
@Test
181+
func compilerArgsCTarget() async throws {
182+
try await withSwiftPMBSP(fixtureName: "CFamilyTargets/CLibrarySources") { connection, _, _ in
183+
let targetResponse = try await connection.send(WorkspaceBuildTargetsRequest())
184+
let cLibrarySources = try #require(targetResponse.targets.first(where: { $0.displayName == "CLibrarySources" }))
185+
let targetID = cLibrarySources.id
186+
187+
let sourcesResponse = try await connection.send(BuildTargetSourcesRequest(targets: [targetID]))
188+
let sources = try #require(sourcesResponse.items.only?.sources)
189+
let sourceFile = try #require(sources.first(where: { $0.uri.fileURL?.lastPathComponent == "Foo.c" }))
190+
#expect(sourceFile.kind == .file)
191+
192+
let headerFile = try #require(sources.first(where: { $0.uri.fileURL?.lastPathComponent == "Foo.h" }))
193+
#expect(headerFile.kind == .file)
194+
#expect(headerFile.sourceKitData?.kind == .header)
195+
196+
_ = try await connection.send(BuildTargetPrepareRequest(targets: [targetID]))
197+
198+
// Verify compiler arguments for the source file
199+
let sourceSettingsResponse = try #require(try await connection.send(TextDocumentSourceKitOptionsRequest(textDocument: TextDocumentIdentifier(sourceFile.uri), target: targetID, language: .c)))
200+
#expect(sourceSettingsResponse.compilerArguments.contains(where: { $0.hasSuffix("Foo.c") }))
201+
try await AsyncProcess.checkNonZeroExit(arguments: [UserToolchain.default.getClangCompiler().pathString, "-fsyntax-only"] + sourceSettingsResponse.compilerArguments)
202+
203+
// Verify compiler arguments for the header file
204+
let headerSettingsResponse = try #require(try await connection.send(TextDocumentSourceKitOptionsRequest(textDocument: TextDocumentIdentifier(headerFile.uri), target: targetID, language: .c)))
205+
#expect(headerSettingsResponse.compilerArguments.contains(where: { $0.hasSuffix("Foo.h") }))
206+
try await AsyncProcess.checkNonZeroExit(arguments: [UserToolchain.default.getClangCompiler().pathString, "-fsyntax-only"] + headerSettingsResponse.compilerArguments)
207+
}
208+
}
209+
180210
@Test
181211
func manifestArgs() async throws {
182212
try await withSwiftPMBSP(fixtureName: "Miscellaneous/VersionSpecificManifest") { connection, _, _ in

0 commit comments

Comments
 (0)