Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions Sources/SwiftBuildSupport/PIFBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ import var TSCBasic.stdoutStream

import enum SwiftBuild.ProjectModel

public struct PIFGenerationResult {
public var pif: String
public var accompanyingMetadata: [PackagePIFBuilder.ModuleOrProduct]
}

fileprivate func memoize<T>(to cache: inout T?, build: () async throws -> T) async rethrows -> T {
if let value = cache {
return value
Expand Down Expand Up @@ -153,14 +158,14 @@ public final class PIFBuilder {
printPIFManifestGraphviz: Bool = false,
buildParameters: BuildParameters,
hostBuildParameters: BuildParameters
) async throws -> String {
) async throws -> PIFGenerationResult {
let encoder = prettyPrint ? JSONEncoder.makeWithDefaults() : JSONEncoder()

if !preservePIFModelStructure {
encoder.userInfo[.encodeForSwiftBuild] = true
}

let topLevelObject = try await self.constructPIF(buildParameters: buildParameters, hostBuildParameters: hostBuildParameters)
let (topLevelObject, modulesAndProducts) = try await self.constructPIF(buildParameters: buildParameters, hostBuildParameters: hostBuildParameters)

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

return pifString
return PIFGenerationResult(pif: pifString, accompanyingMetadata: modulesAndProducts)
}

private var cachedPIF: PIF.TopLevelObject?
private var cachedPIF: (PIF.TopLevelObject, [PackagePIFBuilder.ModuleOrProduct])?

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

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

var modulesAndProducts: [PackagePIFBuilder.ModuleOrProduct] = []
let packagesAndPIFProjects = try packagesAndPIFBuilders.map { (package, pifBuilder, _) in
try pifBuilder.build()
let builtModulesAndProducts = try pifBuilder.build()
modulesAndProducts.append(contentsOf: builtModulesAndProducts)
let pifProject: ProjectModel.Project = pifBuilder.pifProject
return (package, pifProject)
}
Expand All @@ -479,7 +486,7 @@ public final class PIFBuilder {
path: try getCommonParentDirectory(paths: rootPackagesPaths),
projects: pifProjects
)
return PIF.TopLevelObject(workspace: workspace)
return (PIF.TopLevelObject(workspace: workspace), modulesAndProducts)
}
}

Expand Down Expand Up @@ -555,7 +562,7 @@ public final class PIFBuilder {
addLocalRpaths: Bool,
materializeStaticArchiveProductsForRootPackages: Bool,
createDynamicVariantsForLibraryProducts: Bool
) async throws -> String {
) async throws -> PIFGenerationResult {
let parameters = PIFBuilderParameters(
buildParameters,
supportedSwiftVersions: [],
Expand Down
9 changes: 6 additions & 3 deletions Sources/SwiftBuildSupport/SwiftBuildSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1259,17 +1259,20 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
}
}

public func generatePIF(preserveStructure: Bool) async throws -> String {
public func generatePIFAndAccompanyingMetadata(preserveStructure: Bool) async throws -> PIFGenerationResult {
pifBuilder = .init()
packageGraph = .init()
let pifBuilder = try await getPIFBuilder()
let pif = try await pifBuilder.generatePIF(
return try await pifBuilder.generatePIF(
preservePIFModelStructure: preserveStructure,
printPIFManifestGraphviz: buildParameters.printPIFManifestGraphviz,
buildParameters: buildParameters,
hostBuildParameters: hostBuildParameters
)
return pif
}

public func generatePIF(preserveStructure: Bool) async throws -> String {
return try await generatePIFAndAccompanyingMetadata(preserveStructure: preserveStructure).pif
}

public func writePIF(buildParameters: BuildParameters) async throws {
Expand Down
118 changes: 115 additions & 3 deletions Sources/SwiftPMBuildServer/SwiftPMBuildServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ public actor SwiftPMBuildServer: QueueBasedMessageHandler {
}
}
var state: ServerState = .waitingForInitializeRequest

private var headersByTargetGUID: [String: Set<Basics.AbsolutePath>] = [:]

/// Allows customization of server exit behavior.
var exitHandler: (Int) -> Void

Expand Down Expand Up @@ -213,6 +216,26 @@ public actor SwiftPMBuildServer: QueueBasedMessageHandler {
await logToClient(.warning, "SwiftPM build server processed target sources request for unexpected target '\(target)'")
}
}

// Add entries for the target's headers, which are not currently represented in the PIF.
for index in sourcesResponse.items.indices {
guard let targetGUID = sourcesResponse.items[index].target.targetGUID else {
logToClient(.warning, "Unable to determine target GUID for \(sourcesResponse.items[index].target) when looking up headers")
continue
}
let headers = self.headersByTargetGUID[targetGUID] ?? []
for header in headers {
sourcesResponse.items[index].sources.append(
SourceItem(
uri: DocumentURI(header.asURL),
kind: .file,
generated: false,
dataKind: .sourceKit,
data: SourceKitSourceItemData(kind: .header).encodeToLSPAny()
)
)
}
}
return sourcesResponse
}
case let request as RequestAndReply<InitializeBuildRequest>:
Expand All @@ -221,9 +244,15 @@ public actor SwiftPMBuildServer: QueueBasedMessageHandler {
await request.reply {
if request.params.target.isSwiftPMBuildServerTargetID {
return try await manifestSourceKitOptions(request: request.params)
} else {
return try await connectionToUnderlyingBuildServer.send(request.params)
}
if let targetGUID = request.params.target.targetGUID,
let headers = headersByTargetGUID[targetGUID],
let requestPath = try? request.params.textDocument.uri.fileURL?.filePath,
headers.contains(requestPath),
let response = try await self.headerSourceKitOptions(request: request.params){
return response
}
return try await connectionToUnderlyingBuildServer.send(request.params)
}
case let request as RequestAndReply<WorkspaceBuildTargetsRequest>:
await request.reply {
Expand Down Expand Up @@ -314,6 +343,79 @@ public actor SwiftPMBuildServer: QueueBasedMessageHandler {
return TextDocumentSourceKitOptionsResponse(compilerArguments: compilerArgs)
}

/// If the requested file is a known header, returns compiler arguments derived from a substitute source file
private func headerSourceKitOptions(
request: TextDocumentSourceKitOptionsRequest
) async throws -> TextDocumentSourceKitOptionsResponse? {
guard let fileURL = request.textDocument.uri.fileURL,
let filePath = try? fileURL.filePath else {
return nil
}

var substituteSourceFile: URI? = nil
let sourcesResponse = try await connectionToUnderlyingBuildServer.send(BuildTargetSourcesRequest(targets: [request.target]))
for sourcesItem in sourcesResponse.items {
for sourceFile in sourcesItem.sources {
let language = SourceKitSourceItemData(fromLSPAny: sourceFile.data)?.language
switch language {
case .c, .cpp, .objective_c, .objective_cpp, nil:
// SourceKit-LSP historically chose the first source file of a C-family target as the substitute.
// Here, we specifically look for the first C/C++/ObjC/ObjC++ file so this is futureproof against
// mixed Swift/C-family targets. However, we may also want to consider if e.g. a .hpp header should
// use a C++ source file over a C source file if a target has both.
substituteSourceFile = sourceFile.uri
default:
break
}
}
}

guard let substituteSourceFile, let substituteSourceFilePath = try? substituteSourceFile.fileURL?.filePath else {
logToClient(.info, "Unable to find a substitute source file for '\(filePath)'")
return nil
}
logToClient(.info, "Getting compiler arguments for '\(filePath)' using substitute file '\(substituteSourceFilePath)'")

let substituteRequest = TextDocumentSourceKitOptionsRequest(
textDocument: TextDocumentIdentifier(substituteSourceFile),
target: request.target,
language: request.language
)
guard let substituteResponse = try await connectionToUnderlyingBuildServer.send(substituteRequest) else {
return nil
}

// Replace the substitute file path with the header path
// It's possible the arguments use relative paths while the `originalFile` given
// is an absolute/real path value. We guess based on suffixes instead of hitting
// the file system. Copied from SourceKit-LSP
var arguments = substituteResponse.compilerArguments
let substituteBasename = substituteSourceFilePath.basename
if let index = arguments.lastIndex(where: {
$0.hasSuffix(substituteBasename) && substituteSourceFilePath.pathString.hasSuffix($0)
}) {
arguments[index] = filePath.pathString
}

return TextDocumentSourceKitOptionsResponse(
compilerArguments: arguments,
workingDirectory: substituteResponse.workingDirectory,
data: substituteResponse.data
)
}

private func rebuildHeaderMapping(pifAccompanyingMetadata: [PackagePIFBuilder.ModuleOrProduct]) async {
var headers: [String: Set<Basics.AbsolutePath>] = [:]
for moduleOrProduct in pifAccompanyingMetadata {
guard let pifTarget = moduleOrProduct.pifTarget else { continue }
let guid = pifTarget.id.value
if !moduleOrProduct.headerFiles.isEmpty {
headers[guid] = moduleOrProduct.headerFiles
}
}
self.headersByTargetGUID = headers
}

private func shutdown() -> VoidResponse {
state = .shutdown
return VoidResponse()
Expand Down Expand Up @@ -345,7 +447,9 @@ public actor SwiftPMBuildServer: QueueBasedMessageHandler {
public func scheduleRegeneratingBuildDescription() {
packageLoadingQueue.async { [buildSystem] in
do {
try await buildSystem.writePIF(buildParameters: buildSystem.buildParameters)
let result = try await buildSystem.generatePIFAndAccompanyingMetadata(preserveStructure: false)
try localFileSystem.writeIfChanged(path: buildSystem.buildParameters.pifManifest, string: result.pif)
await self.rebuildHeaderMapping(pifAccompanyingMetadata: result.accompanyingMetadata)
self.connectionToUnderlyingBuildServer.send(OnWatchedFilesDidChangeNotification(changes: [
.init(uri: .init(buildSystem.buildParameters.pifManifest.asURL), type: .changed)
]))
Expand All @@ -365,5 +469,13 @@ extension BuildTargetIdentifier {
var isSwiftPMBuildServerTargetID: Bool {
uri.scheme == Self.swiftPMBuildServerTargetScheme
}

var targetGUID: String? {
guard let components = URLComponents(url: uri.arbitrarySchemeURL, resolvingAgainstBaseURL: false),
let value = components.queryItems?.last(where: { $0.name == "targetGUID" })?.value else {
return nil
}
return value
}
}
#endif
2 changes: 1 addition & 1 deletion Tests/SwiftBuildSupportTests/CGenPIFTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ import SwiftBuild
destination: .host,
buildSystemKind: .swiftbuild,
)
)
).0
}

/// This is more to test out that the setup routines provide a good test environment
Expand Down
8 changes: 4 additions & 4 deletions Tests/SwiftBuildSupportTests/PIFBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ fileprivate func withGeneratedPIF(
fileSystem: localFileSystem,
observabilityScope: observabilitySystem.topScope
)
let pif = try await builder.constructPIF(
let (pif, _) = try await builder.constructPIF(
buildParameters: buildParameters,
hostBuildParameters: hostBuildParameters
)
Expand Down Expand Up @@ -391,7 +391,7 @@ struct PIFBuilderTests {
)

// Act
let pif = try await pifBuilder.constructPIF(
let (pif, _) = try await pifBuilder.constructPIF(
buildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild),
hostBuildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild)
)
Expand Down Expand Up @@ -826,7 +826,7 @@ struct PIFBuilderTests {
fileSystem: fs,
observabilityScope: observability.topScope
)
let pif = try await pifBuilder.constructPIF(
let (pif, _) = try await pifBuilder.constructPIF(
buildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild),
hostBuildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild)
)
Expand Down Expand Up @@ -965,7 +965,7 @@ struct PIFBuilderTests {
observabilityScope: observability.topScope
)

let pif = try await pifBuilder.constructPIF(
let (pif, _) = try await pifBuilder.constructPIF(
buildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild),
hostBuildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild)
)
Expand Down
4 changes: 2 additions & 2 deletions Tests/SwiftBuildSupportTests/PrebuiltsPIFTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ struct PrebuiltsPIFTests {
fileSystem: fs,
observabilityScope: observability.topScope
)
let pif = try await pifBuilder.constructPIF(
let (pif, _) = try await pifBuilder.constructPIF(
buildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild),
hostBuildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild)
)
Expand Down Expand Up @@ -373,7 +373,7 @@ struct PrebuiltsPIFTests {
fileSystem: fs,
observabilityScope: observability.topScope
)
let pif = try await pifBuilder.constructPIF(
let (pif, _) = try await pifBuilder.constructPIF(
buildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild),
hostBuildParameters: mockBuildParameters(destination: .host, buildSystemKind: .swiftbuild)
)
Expand Down
30 changes: 30 additions & 0 deletions Tests/SwiftPMBuildServerTests/BuildServerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,36 @@ struct SwiftPMBuildServerTests {
}
}

@Test
func compilerArgsCTarget() async throws {
try await withSwiftPMBSP(fixtureName: "CFamilyTargets/CLibrarySources") { connection, _, _ in
let targetResponse = try await connection.send(WorkspaceBuildTargetsRequest())
let cLibrarySources = try #require(targetResponse.targets.first(where: { $0.displayName == "CLibrarySources" }))
let targetID = cLibrarySources.id

let sourcesResponse = try await connection.send(BuildTargetSourcesRequest(targets: [targetID]))
let sources = try #require(sourcesResponse.items.only?.sources)
let sourceFile = try #require(sources.first(where: { $0.uri.fileURL?.lastPathComponent == "Foo.c" }))
#expect(sourceFile.kind == .file)

let headerFile = try #require(sources.first(where: { $0.uri.fileURL?.lastPathComponent == "Foo.h" }))
#expect(headerFile.kind == .file)
#expect(headerFile.sourceKitData?.kind == .header)

_ = try await connection.send(BuildTargetPrepareRequest(targets: [targetID]))

// Verify compiler arguments for the source file
let sourceSettingsResponse = try #require(try await connection.send(TextDocumentSourceKitOptionsRequest(textDocument: TextDocumentIdentifier(sourceFile.uri), target: targetID, language: .c)))
#expect(sourceSettingsResponse.compilerArguments.contains(where: { $0.hasSuffix("Foo.c") }))
try await AsyncProcess.checkNonZeroExit(arguments: [UserToolchain.default.getClangCompiler().pathString, "-fsyntax-only"] + sourceSettingsResponse.compilerArguments)

// Verify compiler arguments for the header file
let headerSettingsResponse = try #require(try await connection.send(TextDocumentSourceKitOptionsRequest(textDocument: TextDocumentIdentifier(headerFile.uri), target: targetID, language: .c)))
#expect(headerSettingsResponse.compilerArguments.contains(where: { $0.hasSuffix("Foo.h") }))
try await AsyncProcess.checkNonZeroExit(arguments: [UserToolchain.default.getClangCompiler().pathString, "-fsyntax-only"] + headerSettingsResponse.compilerArguments)
}
}

@Test
func manifestArgs() async throws {
try await withSwiftPMBSP(fixtureName: "Miscellaneous/VersionSpecificManifest") { connection, _, _ in
Expand Down
Loading