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
4 changes: 4 additions & 0 deletions Sources/SWBApplePlatform/CoreMLCompiler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ fileprivate struct CoreMLTaskPayload: TaskPayload, Encodable {

/// The indexing info for a CoreML file. This will be sent to the client in a property list format described below.
fileprivate enum CoreMLIndexingInfo: Serializable, SourceFileIndexingInfo, Encodable {
public var compilerArguments: [String]? { nil }
public var indexOutputFile: String? { nil }
public var language: IndexingInfoLanguage? { nil }

/// Setting up the code generation task did not generate an error. It might have been disabled, which is treated as success.
case success(generatedFilePaths: [Path]?, languageToGenerate: String, notice: String?)
/// Setting up the code generation task failed, and we use the index to propagate an error back to Xcode.
Expand Down
4 changes: 4 additions & 0 deletions Sources/SWBApplePlatform/IntentsCompiler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ fileprivate struct IntentsTaskPayload: TaskPayload, Encodable {

/// The indexing info for an Intents file. This will be sent to the client in a property list format described below.
fileprivate enum IntentsIndexingInfo: Serializable, SourceFileIndexingInfo, Encodable {
public var compilerArguments: [String]? { nil }
public var indexOutputFile: String? { nil }
public var language: IndexingInfoLanguage? { nil }

/// Setting up the code generation task did not generate an error. It might have been disabled, which is treated as success.
case success(generatedFilePaths: [Path])
/// Setting up the code generation task failed, and we use the index to propagate an error back to Xcode.
Expand Down
5 changes: 4 additions & 1 deletion Sources/SWBApplePlatform/MetalCompiler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ struct MetalSourceFileIndexingInfo: SourceFileIndexingInfo {
let commandLine: [ByteString]
let builtProductsDir: Path
let toolchains: [String]
var compilerArguments: [String]? { commandLine.map { $0.asString } }
var indexOutputFile: String? { outputFile.str }
public var language: IndexingInfoLanguage? { .metal }

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

extension OutputPathIndexingInfo {
fileprivate init(task: any ExecutableTask, payload: MetalIndexingPayload) {
self.init(outputFile: Path(task.commandLine[payload.outputFileIndex].asString))
self.init(outputFile: Path(task.commandLine[payload.outputFileIndex].asString), language: .metal)
}
}

Expand Down
218 changes: 218 additions & 0 deletions Sources/SWBBuildService/BuildDescriptionMessages.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SWBBuildSystem
import SWBCore
import SWBProtocol
import SWBServiceCore
import SWBTaskConstruction
import SWBTaskExecution
import SWBUtil

// MARK: - Retrieve build description

/// Message that contains enough information to load a build description
private protocol BuildDescriptionMessage: SessionMessage {
/// The ID of the build description from which to load the configured targets
var buildDescriptionID: BuildDescriptionID { get }

/// The build request that was used to generate the build description with the given ID.
var request: BuildRequestMessagePayload { get }
}

extension BuildDescriptionConfiguredTargetsRequest: BuildDescriptionMessage {}
extension BuildDescriptionConfiguredTargetSourcesRequest: BuildDescriptionMessage {}
extension IndexBuildSettingsRequest: BuildDescriptionMessage {}

fileprivate extension Request {
struct BuildDescriptionDoesNotExistError: Error {}

func buildDescription(for message: some BuildDescriptionMessage) async throws -> BuildDescription {
return try await buildRequestAndDescription(for: message).description
}

func buildRequestAndDescription(for message: some BuildDescriptionMessage) async throws -> (request: BuildRequest, description: BuildDescription) {
let session = try self.session(for: message)
guard let workspaceContext = session.workspaceContext else {
throw MsgParserError.missingWorkspaceContext
}
let buildRequest = try BuildRequest(from: message.request, workspace: workspaceContext.workspace)
let buildRequestContext = BuildRequestContext(workspaceContext: workspaceContext)
let clientDelegate = ClientExchangeDelegate(request: self, session: session)
let operation = IndexingOperation(workspace: workspaceContext.workspace)
let buildDescription = try await session.buildDescriptionManager.getNewOrCachedBuildDescription(
.cachedOnly(
message.buildDescriptionID,
request: buildRequest,
buildRequestContext: buildRequestContext,
workspaceContext: workspaceContext
), clientDelegate: clientDelegate, constructionDelegate: operation
)?.buildDescription

guard let buildDescription else {
throw BuildDescriptionDoesNotExistError()
}
return (buildRequest, buildDescription)
}
}

// MARK: - Message handlers

struct BuildDescriptionConfiguredTargetsMsg: MessageHandler {
/// Compute the toolchains that can handle all Swift and clang compilation tasks in the given target.
private func toolchainIDs(in configuredTarget: ConfiguredTarget, of buildDescription: BuildDescription) -> [String]? {
var toolchains: [String]?

for task in buildDescription.taskStore.tasksForTarget(configuredTarget) {
let targetToolchains: [String]? =
switch task.payload {
case let payload as SwiftTaskPayload: payload.indexingPayload.toolchains
case let payload as ClangTaskPayload: payload.indexingPayload?.toolchains
default: nil
}
guard let targetToolchains else {
continue
}
if let unwrappedToolchains = toolchains {
toolchains = unwrappedToolchains.filter { targetToolchains.contains($0) }
} else {
toolchains = targetToolchains
}
}

return toolchains
}

func handle(request: Request, message: BuildDescriptionConfiguredTargetsRequest) async throws -> BuildDescriptionConfiguredTargetsResponse {
let buildDescription = try await request.buildDescription(for: message)

let dependencyRelationships = Dictionary(
buildDescription.targetDependencies.map { (ConfiguredTarget.GUID(id: $0.target.guid), [$0]) },
uniquingKeysWith: { $0 + $1 }
)

let session = try request.session(for: message)

let targetInfos = buildDescription.allConfiguredTargets.map { configuredTarget in
let toolchain: Path?
if let toolchainID = toolchainIDs(in: configuredTarget, of: buildDescription)?.first {
toolchain = session.core.toolchainRegistry.lookup(toolchainID)?.path
if toolchain == nil {
log("Unable to find path for toolchain with identifier \(toolchainID)", isError: true)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ahoppen We don't conventionally have this level of logging in the product; it seems like debug printing that should've been removed.

}
} else {
log("Unable to find toolchain for \(configuredTarget)", isError: true)
toolchain = nil
}

let dependencyRelationships = dependencyRelationships[configuredTarget.guid]
return BuildDescriptionConfiguredTargetsResponse.ConfiguredTargetInfo(
guid: ConfiguredTargetGUID(configuredTarget.guid.stringValue),
target: TargetGUID(rawValue: configuredTarget.target.guid),
name: configuredTarget.target.name,
dependencies: Set(dependencyRelationships?.flatMap(\.targetDependencies).map { ConfiguredTargetGUID($0.guid) } ?? []),
toolchain: toolchain
)
}
return BuildDescriptionConfiguredTargetsResponse(configuredTargets: targetInfos)
}
}

fileprivate extension SourceLanguage {
init?(_ language: IndexingInfoLanguage?) {
switch language {
case nil: return nil
case .c: self = .c
case .cpp: self = .cpp
case .metal: self = .metal
case .objectiveC: self = .objectiveC
case .objectiveCpp: self = .objectiveCpp
case .swift: self = .swift
}
}
}

struct BuildDescriptionConfiguredTargetSourcesMsg: MessageHandler {
private struct UnknownConfiguredTargetIDError: Error, CustomStringConvertible {
let configuredTarget: ConfiguredTargetGUID
var description: String { "Unknown configured target: \(configuredTarget)" }
}

typealias SourceFileInfo = BuildDescriptionConfiguredTargetSourcesResponse.SourceFileInfo
typealias ConfiguredTargetSourceFilesInfo = BuildDescriptionConfiguredTargetSourcesResponse.ConfiguredTargetSourceFilesInfo

func handle(request: Request, message: BuildDescriptionConfiguredTargetSourcesRequest) async throws -> BuildDescriptionConfiguredTargetSourcesResponse {
let buildDescription = try await request.buildDescription(for: message)

let configuredTargetsByID = Dictionary(
buildDescription.allConfiguredTargets.map { ($0.guid, $0) }
) { lhs, rhs in
log("Found conflicting targets for the same ID: \(lhs.guid)", isError: true)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ahoppen These type of scenarios should throw an error, not "pick one and log"; it tends to lead to unpredictable and nondeterministic behavior

return lhs
}

let indexingInfoInput = TaskGenerateIndexingInfoInput(requestedSourceFile: nil, outputPathOnly: true, enableIndexBuildArena: false)
let sourcesItems = try message.configuredTargets.map { configuredTargetGuid in
guard let target = configuredTargetsByID[ConfiguredTarget.GUID(id: configuredTargetGuid.rawValue)] else {
throw UnknownConfiguredTargetIDError(configuredTarget: configuredTargetGuid)
}
let sourceFiles = buildDescription.taskStore.tasksForTarget(target).flatMap { task in
task.generateIndexingInfo(input: indexingInfoInput).compactMap { (entry) -> SourceFileInfo? in
return SourceFileInfo(
path: entry.path,
language: SourceLanguage(entry.indexingInfo.language),
outputPath: entry.indexingInfo.indexOutputFile
)
}
}
return ConfiguredTargetSourceFilesInfo(configuredTarget: configuredTargetGuid, sourceFiles: sourceFiles)
}
return BuildDescriptionConfiguredTargetSourcesResponse(targetSourceFileInfos: sourcesItems)
}
}

struct IndexBuildSettingsMsg: MessageHandler {
private struct AmbiguousIndexingInfoError: Error, CustomStringConvertible {
var description: String { "Found multiple indexing informations for the same source file" }
}

private struct FailedToGetCompilerArgumentsError: Error {}

func handle(request: Request, message: IndexBuildSettingsRequest) async throws -> IndexBuildSettingsResponse {
let (buildRequest, buildDescription) = try await request.buildRequestAndDescription(for: message)

let configuredTarget = buildDescription.allConfiguredTargets.filter { $0.guid.stringValue == message.configuredTarget.rawValue }.only

let indexingInfoInput = TaskGenerateIndexingInfoInput(
requestedSourceFile: message.file,
outputPathOnly: false,
enableIndexBuildArena: buildRequest.enableIndexBuildArena
)
// First find all the tasks that declare the requested source file as an input file. This should narrow the list
// of targets down significantly.
let taskForSourceFile = buildDescription.taskStore.tasksForTarget(configuredTarget)
.filter { $0.inputPaths.contains(message.file) }
// Now get the indexing info for the targets that might be relevant and perform another check to ensure they
// actually represent the requested source file.
let indexingInfos =
taskForSourceFile
.flatMap { $0.generateIndexingInfo(input: indexingInfoInput) }
.filter({ $0.path == message.file })
guard let indexingInfo = indexingInfos.only else {
throw AmbiguousIndexingInfoError()
}
guard let compilerArguments = indexingInfo.indexingInfo.compilerArguments else {
throw FailedToGetCompilerArgumentsError()
}
return IndexBuildSettingsResponse(compilerArguments: compilerArguments)
}
}
1 change: 1 addition & 0 deletions Sources/SWBBuildService/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ See http://swift.org/CONTRIBUTORS.txt for Swift project authors

add_library(SWBBuildService
BuildDependencyInfo.swift
BuildDescriptionMessages.swift
BuildOperationMessages.swift
BuildService.swift
BuildServiceEntryPoint.swift
Expand Down
4 changes: 4 additions & 0 deletions Sources/SWBBuildService/Messages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1614,6 +1614,10 @@ package struct ServiceMessageHandlers: ServiceExtension {
service.registerMessageHandler(ComputeDependencyGraphMsg.self)
service.registerMessageHandler(DumpBuildDependencyInfoMsg.self)

service.registerMessageHandler(BuildDescriptionConfiguredTargetsMsg.self)
service.registerMessageHandler(BuildDescriptionConfiguredTargetSourcesMsg.self)
service.registerMessageHandler(IndexBuildSettingsMsg.self)

service.registerMessageHandler(MacroEvaluationMsg.self)
service.registerMessageHandler(AllExportedMacrosAndValuesMsg.self)
service.registerMessageHandler(BuildSettingsEditorInfoMsg.self)
Expand Down
2 changes: 1 addition & 1 deletion Sources/SWBCore/ConfiguredTarget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public final class ConfiguredTarget: Hashable, CustomStringConvertible, Serializ
public struct GUID: Hashable, Sendable, Comparable, CustomStringConvertible {
public let stringValue: String

fileprivate init(id: String) {
public init(id: String) {
self.stringValue = id
}

Expand Down
36 changes: 31 additions & 5 deletions Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,15 @@ public struct ClangPrefixInfo: Serializable, Hashable, Encodable, Sendable {
}

/// The minimal data we need to serialize to reconstruct `ClangSourceFileIndexingInfo` from `generateIndexingInfo`
fileprivate struct ClangIndexingPayload: Serializable, Encodable, Sendable {
public struct ClangIndexingPayload: Serializable, Encodable, Sendable {
let sourceFileIndex: Int
let outputFileIndex: Int
let sourceLanguageIndex: Int
let builtProductsDir: Path
let assetSymbolIndexPath: Path
let workingDir: Path
let prefixInfo: ClangPrefixInfo?
let toolchains: [String]
public let toolchains: [String]
let responseFileAttachmentPaths: [Path: Path]

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

func serialize<T: Serializer>(to serializer: T) {
public func serialize<T: Serializer>(to serializer: T) {
serializer.serializeAggregate(9) {
serializer.serialize(sourceFileIndex)
serializer.serialize(outputFileIndex)
Expand All @@ -142,7 +142,7 @@ fileprivate struct ClangIndexingPayload: Serializable, Encodable, Sendable {
}
}

init(from deserializer: any Deserializer) throws {
public init(from deserializer: any Deserializer) throws {
try deserializer.beginAggregate(9)
self.sourceFileIndex = try deserializer.deserialize()
self.outputFileIndex = try deserializer.deserialize()
Expand All @@ -165,6 +165,19 @@ public struct ClangSourceFileIndexingInfo: SourceFileIndexingInfo {
let assetSymbolIndexPath: Path
let prefixInfo: ClangPrefixInfo?
let toolchains: [String]
public var compilerArguments: [String]? {
var result = commandLine.map { $0.asString }
// commandLine does not contain the `-index-unit-output-path` but we want to return it from the
// `IndexBuildSettingsRequest`, so add it.
if !result.contains(where: { $0 == "-o" || $0 == "-index-unit-output-path" }), let indexOutputFile {
result += ["-index-unit-output-path", indexOutputFile]
}
return result
}
public var indexOutputFile: String? { outputFile.str }
public var language: IndexingInfoLanguage? {
return IndexingInfoLanguage(compilerArgumentLanguage: sourceLanguage)
}

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

Expand Down Expand Up @@ -422,7 +436,7 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd
public let serializedDiagnosticsPath: Path?

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

/// Additional information used by explicit modules support.
public let explicitModulesPayload: ClangExplicitModulesPayload?
Expand Down Expand Up @@ -2120,3 +2134,15 @@ public final class ClangModuleVerifierSpec: ClangCompilerSpec, SpecImplementatio
private func ==(lhs: ClangCompilerSpec.DataCache.ConstantFlagsKey, rhs: ClangCompilerSpec.DataCache.ConstantFlagsKey) -> Bool {
return ObjectIdentifier(lhs.scope) == ObjectIdentifier(rhs.scope) && lhs.inputFileType == rhs.inputFileType
}

private extension IndexingInfoLanguage {
init?(compilerArgumentLanguage: ByteString) {
switch compilerArgumentLanguage {
case "c": self = .c
case "c++": self = .cpp
case "objective-c": self = .objectiveC
case "objective-c++": self = .objectiveCpp
default: return nil
}
}
}
Loading