diff --git a/Sources/SWBApplePlatform/CoreMLCompiler.swift b/Sources/SWBApplePlatform/CoreMLCompiler.swift index 17225c0a..1fd193d2 100644 --- a/Sources/SWBApplePlatform/CoreMLCompiler.swift +++ b/Sources/SWBApplePlatform/CoreMLCompiler.swift @@ -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. diff --git a/Sources/SWBApplePlatform/IntentsCompiler.swift b/Sources/SWBApplePlatform/IntentsCompiler.swift index 6d879dd1..ccffd2ff 100644 --- a/Sources/SWBApplePlatform/IntentsCompiler.swift +++ b/Sources/SWBApplePlatform/IntentsCompiler.swift @@ -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. diff --git a/Sources/SWBApplePlatform/MetalCompiler.swift b/Sources/SWBApplePlatform/MetalCompiler.swift index 9830085d..133ff839 100644 --- a/Sources/SWBApplePlatform/MetalCompiler.swift +++ b/Sources/SWBApplePlatform/MetalCompiler.swift @@ -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 @@ -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) } } diff --git a/Sources/SWBBuildService/BuildDescriptionMessages.swift b/Sources/SWBBuildService/BuildDescriptionMessages.swift new file mode 100644 index 00000000..e6c4a3e5 --- /dev/null +++ b/Sources/SWBBuildService/BuildDescriptionMessages.swift @@ -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) + } + } 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) + 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) + } +} diff --git a/Sources/SWBBuildService/CMakeLists.txt b/Sources/SWBBuildService/CMakeLists.txt index ac623f70..43b9ac05 100644 --- a/Sources/SWBBuildService/CMakeLists.txt +++ b/Sources/SWBBuildService/CMakeLists.txt @@ -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 diff --git a/Sources/SWBBuildService/Messages.swift b/Sources/SWBBuildService/Messages.swift index ded6b880..ab5b43b8 100644 --- a/Sources/SWBBuildService/Messages.swift +++ b/Sources/SWBBuildService/Messages.swift @@ -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) diff --git a/Sources/SWBCore/ConfiguredTarget.swift b/Sources/SWBCore/ConfiguredTarget.swift index 4f93f80e..5ab5c0c5 100644 --- a/Sources/SWBCore/ConfiguredTarget.swift +++ b/Sources/SWBCore/ConfiguredTarget.swift @@ -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 } diff --git a/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift index 279b0b3b..26e43bb9 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift @@ -93,7 +93,7 @@ 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 @@ -101,7 +101,7 @@ fileprivate struct ClangIndexingPayload: Serializable, Encodable, Sendable { let assetSymbolIndexPath: Path let workingDir: Path let prefixInfo: ClangPrefixInfo? - let toolchains: [String] + public let toolchains: [String] let responseFileAttachmentPaths: [Path: Path] init(sourceFileIndex: Int, @@ -128,7 +128,7 @@ fileprivate struct ClangIndexingPayload: Serializable, Encodable, Sendable { return Path(task.commandLine[self.sourceFileIndex].asString) } - func serialize(to serializer: T) { + public func serialize(to serializer: T) { serializer.serializeAggregate(9) { serializer.serialize(sourceFileIndex) serializer.serialize(outputFileIndex) @@ -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() @@ -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 @@ -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) } } @@ -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? @@ -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 + } + } +} diff --git a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift index 44b99a76..9effd838 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift @@ -95,7 +95,18 @@ public struct SwiftSourceFileIndexingInfo: SourceFileIndexingInfo { let builtProductsDir: Path let assetSymbolIndexPath: Path let toolchains: [String] - let outputFile: Path + public let outputFile: Path + 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? { .swift } public init(task: any ExecutableTask, payload: SwiftIndexingPayload, outputFile: Path, enableIndexBuildArena: Bool, integratedDriver: Bool) { self.commandLine = Self.indexingCommandLine(commandLine: task.commandLine.map(\.asByteString), payload: payload, enableIndexBuildArena: enableIndexBuildArena, integratedDriver: integratedDriver) @@ -3267,7 +3278,7 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi objectFileDir: payload.indexingPayload.objectFileDir, fileExtension: ".o") let indexingInfo: any SourceFileIndexingInfo if input.outputPathOnly { - indexingInfo = OutputPathIndexingInfo(outputFile: outputFile) + indexingInfo = OutputPathIndexingInfo(outputFile: outputFile, language: .swift) } else { indexingInfo = SwiftSourceFileIndexingInfo(task: task, payload: payload.indexingPayload, outputFile: outputFile, enableIndexBuildArena: input.enableIndexBuildArena, integratedDriver: payload.driverPayload != nil) } diff --git a/Sources/SWBCore/TaskGeneration.swift b/Sources/SWBCore/TaskGeneration.swift index 7ef96613..53db7891 100644 --- a/Sources/SWBCore/TaskGeneration.swift +++ b/Sources/SWBCore/TaskGeneration.swift @@ -1020,18 +1020,46 @@ extension DependencyDataStyle: Serializable { public protocol TaskPayload: Serializable, Sendable { } +public enum IndexingInfoLanguage { + case c + case cpp + case metal + case objectiveC + case objectiveCpp + case swift +} + /// Defines the indexing information to be sent back to the client for a particular source file. /// /// This protocol includes `PropertyListItemConvertible` to describe how the info is packaged up to send back to the client. Someday we hope to transition this to sending the info in a strongly typed format. public protocol SourceFileIndexingInfo: PropertyListItemConvertible { + /// The compiler arguments that should be used to index this source file. + /// + /// `nil` if this indexing info does not supply compiler arguments to index a source file. + var compilerArguments: [String]? { get } + + /// The output path that is used for indexing, ie. the value of the `-index-unit-output-path` or `-o` option in + /// the source file's build settings. + /// + /// This is a `String` and not a `Path` because th index output path may be a fake path that is relative to the + /// build directory and has no relation to actual files on disks. + /// + /// `nil` if this indexing info does not produce an output file. + var indexOutputFile: String? { get } + + var language: IndexingInfoLanguage? { get } } /// The `SourceFileIndexingInfo` info returned when only output paths are requested. public struct OutputPathIndexingInfo: SourceFileIndexingInfo { public let outputFile: Path + public var compilerArguments: [String]? { nil } + public var indexOutputFile: String? { outputFile.str } + public let language: IndexingInfoLanguage? - public init(outputFile: Path) { + public init(outputFile: Path, language: IndexingInfoLanguage?) { self.outputFile = outputFile + self.language = language } /// The indexing info is packaged and sent to the client in a property list format. diff --git a/Sources/SWBProtocol/BuildAction.swift b/Sources/SWBProtocol/BuildAction.swift index 11a37542..443bc4f0 100644 --- a/Sources/SWBProtocol/BuildAction.swift +++ b/Sources/SWBProtocol/BuildAction.swift @@ -86,7 +86,7 @@ public enum BuildAction: String, Serializable, Codable, CaseIterable, Comparable } /// Opaque token used to uniquely identify a build description. -public struct BuildDescriptionID: Hashable, Sendable { +public struct BuildDescriptionID: Hashable, Sendable, SerializableCodable { public let rawValue: String public init(_ value: String) { diff --git a/Sources/SWBProtocol/BuildDescriptionMessages.swift b/Sources/SWBProtocol/BuildDescriptionMessages.swift new file mode 100644 index 00000000..36cace79 --- /dev/null +++ b/Sources/SWBProtocol/BuildDescriptionMessages.swift @@ -0,0 +1,227 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +public import SWBUtil + +// MARK: Support types + +public struct ConfiguredTargetGUID: Hashable, Sendable, Codable { + public var rawValue: String + + public init(_ rawValue: String) { + self.rawValue = rawValue + } +} + +/// The language of a source file +public enum SourceLanguage: Hashable, Sendable, Codable { + case c + case cpp + case metal + case objectiveC + case objectiveCpp + case swift +} + +// MARK: Requests + +/// Get the configured targets inside a pre-generated build description, their dependencies and some supplementary +/// information about the targets. +public struct BuildDescriptionConfiguredTargetsRequest: SessionMessage, RequestMessage, SerializableCodable, Equatable { + public typealias ResponseMessage = BuildDescriptionConfiguredTargetsResponse + + public static let name = "BUILD_DESCRIPTION_CONFIGURED_TARGETS_REQUEST" + + public let sessionHandle: String + + /// The ID of the build description from which to load the configured targets + public let buildDescriptionID: BuildDescriptionID + + /// The build request that was used to generate the build description with the given ID. + public let request: BuildRequestMessagePayload + + public init(sessionHandle: String, buildDescriptionID: BuildDescriptionID, request: BuildRequestMessagePayload) { + self.sessionHandle = sessionHandle + self.buildDescriptionID = buildDescriptionID + self.request = request + } +} + +public struct BuildDescriptionConfiguredTargetsResponse: Message, SerializableCodable, Equatable { + public static let name = "BUILD_DESCRIPTION_CONFIGURED_TARGETS_RESPONSE" + + public struct ConfiguredTargetInfo: SerializableCodable, Equatable, Sendable { + /// The GUID of this configured target + public let guid: ConfiguredTargetGUID + + /// The GUID of the target from which this configured target was created + public let target: TargetGUID + + /// A name of the target that may be displayed to the user + public let name: String + + /// The configured targets that this target depends on + public let dependencies: Set + + /// The path of the toolchain that should be used to build this target. + /// + /// `nil` if the toolchain for this target could not be determined due to an error. + public let toolchain: Path? + + public init(guid: ConfiguredTargetGUID, target: TargetGUID, name: String, dependencies: Set, toolchain: Path?) { + self.guid = guid + self.target = target + self.name = name + self.dependencies = dependencies + self.toolchain = toolchain + } + } + + public let configuredTargets: [ConfiguredTargetInfo] + + public init(configuredTargets: [ConfiguredTargetInfo]) { + self.configuredTargets = configuredTargets + } +} + +/// Get information about the source files in a list of configured targets. +public struct BuildDescriptionConfiguredTargetSourcesRequest: SessionMessage, RequestMessage, SerializableCodable, Equatable { + public typealias ResponseMessage = BuildDescriptionConfiguredTargetSourcesResponse + + public static let name = "BUILD_DESCRIPTION_CONFIGURED_TARGET_SOURCES_REQUEST" + + public var sessionHandle: String + + /// The ID of the build description in which the configured targets reside + public let buildDescriptionID: BuildDescriptionID + + /// The build request that was used to generate the build description with the given ID. + public let request: BuildRequestMessagePayload + + /// The configured targets for which to load source file information + public let configuredTargets: [ConfiguredTargetGUID] + + public init(sessionHandle: String, buildDescriptionID: BuildDescriptionID, request: BuildRequestMessagePayload, configuredTargets: [ConfiguredTargetGUID]) { + self.sessionHandle = sessionHandle + self.buildDescriptionID = buildDescriptionID + self.request = request + self.configuredTargets = configuredTargets + } +} + +public struct BuildDescriptionConfiguredTargetSourcesResponse: Message, SerializableCodable, Equatable { + public static let name = "BUILD_DESCRIPTION_CONFIGURED_TARGET_SOURCES_RESPONSE" + + public struct SourceFileInfo: SerializableCodable, Equatable, Sendable { + /// The path of the source file on disk + public let path: Path + + /// The language of the source file. + /// + /// `nil` if the language could not be determined due to an error. + public let language: SourceLanguage? + + /// The output path that is used for indexing, ie. the value of the `-index-unit-output-path` or `-o` option in + /// the source file's build settings. + /// + /// This is a `String` and not a `Path` because th index output path may be a fake path that is relative to the + /// build directory and has no relation to actual files on disks. + /// + /// May be `nil` if the output path could not be determined due to an error. + public let indexOutputPath: String? + + public init(path: Path, language: SourceLanguage?, outputPath: String?) { + self.path = path + self.language = language + self.indexOutputPath = outputPath + } + } + + public struct ConfiguredTargetSourceFilesInfo: SerializableCodable, Equatable, Sendable { + /// The configured target to which this info belongs + public let configuredTarget: ConfiguredTargetGUID + + /// Information about the source files in this source file + public let sourceFiles: [SourceFileInfo] + + public init(configuredTarget: ConfiguredTargetGUID, sourceFiles: [SourceFileInfo]) { + self.configuredTarget = configuredTarget + self.sourceFiles = sourceFiles + } + } + + /// For each requested configured target, the response contains one entry in this array + public let targetSourceFileInfos: [ConfiguredTargetSourceFilesInfo] + + public init(targetSourceFileInfos: [ConfiguredTargetSourceFilesInfo]) { + self.targetSourceFileInfos = targetSourceFileInfos + } +} + +/// Load the build settings that should be used to index a source file in a given configured target +public struct IndexBuildSettingsRequest: SessionMessage, RequestMessage, SerializableCodable, Equatable { + public typealias ResponseMessage = IndexBuildSettingsResponse + + public static let name = "INDEX_BUILD_SETTINGS_REQUEST" + + public var sessionHandle: String + + /// The ID of the build description in which the configured targets reside + public let buildDescriptionID: BuildDescriptionID + + /// The build request that was used to generate the build description with the given ID. + public let request: BuildRequestMessagePayload + + /// The configured target in whose context the build settings of the source file should be loaded + public let configuredTarget: ConfiguredTargetGUID + + /// The path of the source file for which the build settings should be loaded + public let file: Path + + public init( + sessionHandle: String, + buildDescriptionID: BuildDescriptionID, + request: BuildRequestMessagePayload, + configuredTarget: ConfiguredTargetGUID, + file: Path + ) { + self.sessionHandle = sessionHandle + self.buildDescriptionID = buildDescriptionID + self.request = request + self.configuredTarget = configuredTarget + self.file = file + } +} + +public struct IndexBuildSettingsResponse: Message, SerializableCodable, Equatable { + public static let name = "INDEX_BUILD_SETTINGS_RESPONSE" + + /// The arguments that should be passed to the compiler to index the source file. + /// + /// This does not include the path to the compiler executable itself. + public let compilerArguments: [String] + + public init(compilerArguments: [String]) { + self.compilerArguments = compilerArguments + } +} + +// MARK: Registering messages + +let buildDescriptionMessages: [any Message.Type] = [ + BuildDescriptionConfiguredTargetsRequest.self, + BuildDescriptionConfiguredTargetsResponse.self, + BuildDescriptionConfiguredTargetSourcesRequest.self, + BuildDescriptionConfiguredTargetSourcesResponse.self, + IndexBuildSettingsRequest.self, + IndexBuildSettingsResponse.self, +] diff --git a/Sources/SWBProtocol/CMakeLists.txt b/Sources/SWBProtocol/CMakeLists.txt index 9208d4b2..8fdbe81d 100644 --- a/Sources/SWBProtocol/CMakeLists.txt +++ b/Sources/SWBProtocol/CMakeLists.txt @@ -11,6 +11,7 @@ See http://swift.org/CONTRIBUTORS.txt for Swift project authors add_library(SWBProtocol AsyncSequence.swift BuildAction.swift + BuildDescriptionMessages.swift BuildOperationMessages.swift BuildSettingsInfoMessageTypes.swift ClientExchangeMessages.swift diff --git a/Sources/SWBProtocol/Message.swift b/Sources/SWBProtocol/Message.swift index c32a01ad..dce784c0 100644 --- a/Sources/SWBProtocol/Message.swift +++ b/Sources/SWBProtocol/Message.swift @@ -1218,6 +1218,7 @@ public struct IPCMessage: Serializable, Sendable { + localizationMessageTypes + dependencyClosureMessageTypes + dependencyGraphMessageTypes + + buildDescriptionMessages /// Reverse name mapping. static let messageNameToID: [String: any Message.Type] = { diff --git a/Sources/SwiftBuild/CMakeLists.txt b/Sources/SwiftBuild/CMakeLists.txt index 45970977..9a1bbea2 100644 --- a/Sources/SwiftBuild/CMakeLists.txt +++ b/Sources/SwiftBuild/CMakeLists.txt @@ -31,6 +31,7 @@ add_library(SwiftBuild ProjectModel/TargetDependency.swift ProjectModel/Targets.swift SWBBuildAction.swift + SWBBuildDescriptionID.swift SWBBuildOperation.swift SWBBuildOperationBacktraceFrame.swift SWBBuildParameters.swift @@ -41,6 +42,9 @@ add_library(SwiftBuild SWBBuildServiceSession.swift SWBChannel.swift SWBClientExchangeSupport.swift + SWBConfiguredTargetGUID.swift + SWBConfiguredTargetInfo.swift + SWBConfiguredTargetSourceFilesInfo.swift SWBDocumentationSupport.swift SWBIndexingSupport.swift SWBLocalizationSupport.swift @@ -49,6 +53,7 @@ add_library(SwiftBuild SWBProductPlannerSupport.swift SWBPropertyList.swift SWBProvisioningTaskInputs.swift + SWBSourceLanguage.swift SWBSystemInfo.swift SWBTargetGUID.swift SWBTerminal.swift diff --git a/Sources/SwiftBuild/ConsoleCommands/SWBServiceConsoleBuildCommandProtocol.swift b/Sources/SwiftBuild/ConsoleCommands/SWBServiceConsoleBuildCommandProtocol.swift index 28283569..46f97977 100644 --- a/Sources/SwiftBuild/ConsoleCommands/SWBServiceConsoleBuildCommandProtocol.swift +++ b/Sources/SwiftBuild/ConsoleCommands/SWBServiceConsoleBuildCommandProtocol.swift @@ -925,6 +925,17 @@ public struct AbsolutePath: Hashable, Equatable, Sendable { self.pathString = path } + init(_ path: Path) { + self.pathString = path.str + } + + init?(_ path: Path?) { + guard let path else { + return nil + } + self.init(path) + } + public static let root = try! AbsolutePath(validating: Path.root.str) } diff --git a/Sources/SwiftBuild/SWBBuildDescriptionID.swift b/Sources/SwiftBuild/SWBBuildDescriptionID.swift new file mode 100644 index 00000000..33b766ca --- /dev/null +++ b/Sources/SwiftBuild/SWBBuildDescriptionID.swift @@ -0,0 +1,32 @@ +//===----------------------------------------------------------------------===// +// +// 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 SWBProtocol + +/// Opaque token used to uniquely identify a build description. +public struct SWBBuildDescriptionID: Hashable, Sendable { + public let rawValue: String + + public init(_ value: String) { + self.rawValue = value + } + + init(_ buildDescriptionID: BuildDescriptionID) { + self.rawValue = buildDescriptionID.rawValue + } +} + +extension BuildDescriptionID { + init(_ buildDescriptionID: SWBBuildDescriptionID) { + self.init(buildDescriptionID.rawValue) + } +} diff --git a/Sources/SwiftBuild/SWBBuildServiceSession.swift b/Sources/SwiftBuild/SWBBuildServiceSession.swift index 07b29c65..c96d6c7e 100644 --- a/Sources/SwiftBuild/SWBBuildServiceSession.swift +++ b/Sources/SwiftBuild/SWBBuildServiceSession.swift @@ -388,6 +388,35 @@ public final class SWBBuildServiceSession: Sendable { return result } + public func configuredTargets(buildDescription: SWBBuildDescriptionID, buildRequest: SWBBuildRequest) async throws -> [SWBConfiguredTargetInfo] { + let response = try await service.send(request: BuildDescriptionConfiguredTargetsRequest(sessionHandle: uid, buildDescriptionID: BuildDescriptionID(buildDescription), request: buildRequest.messagePayloadRepresentation)) + return response.configuredTargets.map { SWBConfiguredTargetInfo($0) } + } + + public func sources(of configuredTargets: [SWBConfiguredTargetGUID], buildDescription: SWBBuildDescriptionID, buildRequest: SWBBuildRequest) async throws -> [SWBConfiguredTargetSourceFilesInfo] { + let response = try await service.send( + request: BuildDescriptionConfiguredTargetSourcesRequest( + sessionHandle: uid, + buildDescriptionID: BuildDescriptionID(buildDescription), + request: buildRequest.messagePayloadRepresentation, + configuredTargets: configuredTargets.map { ConfiguredTargetGUID($0) } + ) + ) + return response.targetSourceFileInfos.map { SWBConfiguredTargetSourceFilesInfo($0) } + } + + public func indexCompilerArguments(of file: AbsolutePath, in configuredTarget: SWBConfiguredTargetGUID, buildDescription: SWBBuildDescriptionID, buildRequest: SWBBuildRequest) async throws -> [String] { + let buildSettings = try await service.send( + request: IndexBuildSettingsRequest( + sessionHandle: uid, + buildDescriptionID: BuildDescriptionID(buildDescription), + request: buildRequest.messagePayloadRepresentation, + configuredTarget: ConfiguredTargetGUID(configuredTarget), + file: Path(file.pathString) + ) + ) + return buildSettings.compilerArguments + } // MARK: Macro evaluation diff --git a/Sources/SwiftBuild/SWBConfiguredTargetGUID.swift b/Sources/SwiftBuild/SWBConfiguredTargetGUID.swift new file mode 100644 index 00000000..4b193169 --- /dev/null +++ b/Sources/SwiftBuild/SWBConfiguredTargetGUID.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// 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 SWBProtocol + +public struct SWBConfiguredTargetGUID: RawRepresentable, Hashable, Sendable { + public var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + init(_ guid: ConfiguredTargetGUID) { + self.init(rawValue: guid.rawValue) + } +} + +extension ConfiguredTargetGUID { + init(_ guid: SWBConfiguredTargetGUID) { + self.init(guid.rawValue) + } +} diff --git a/Sources/SwiftBuild/SWBConfiguredTargetInfo.swift b/Sources/SwiftBuild/SWBConfiguredTargetInfo.swift new file mode 100644 index 00000000..410924c1 --- /dev/null +++ b/Sources/SwiftBuild/SWBConfiguredTargetInfo.swift @@ -0,0 +1,50 @@ +//===----------------------------------------------------------------------===// +// +// 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 SWBProtocol + +public struct SWBConfiguredTargetInfo { + /// The GUID of this configured target + public let guid: SWBConfiguredTargetGUID + + /// The GUID of the target from which this configured target was created + public let target: SWBTargetGUID + + /// A name of the target that may be displayed to the user + public let name: String + + /// The configured targets that this target depends on + public let dependencies: Set + + /// The path of the toolchain that should be used to build this target. + /// + /// `nil` if the toolchain for this target could not be determined due to an error. + public let toolchain: AbsolutePath? + + public init(guid: SWBConfiguredTargetGUID, target: SWBTargetGUID, name: String, dependencies: Set, toolchain: AbsolutePath?) { + self.guid = guid + self.target = target + self.name = name + self.dependencies = dependencies + self.toolchain = toolchain + } + + init(_ configuredTargetInfo: BuildDescriptionConfiguredTargetsResponse.ConfiguredTargetInfo) { + self.init( + guid: SWBConfiguredTargetGUID(configuredTargetInfo.guid), + target: SWBTargetGUID(configuredTargetInfo.target), + name: configuredTargetInfo.name, + dependencies: Set(configuredTargetInfo.dependencies.map { SWBConfiguredTargetGUID($0) }), + toolchain: AbsolutePath(configuredTargetInfo.toolchain) + ) + } +} diff --git a/Sources/SwiftBuild/SWBConfiguredTargetSourceFilesInfo.swift b/Sources/SwiftBuild/SWBConfiguredTargetSourceFilesInfo.swift new file mode 100644 index 00000000..d87f5927 --- /dev/null +++ b/Sources/SwiftBuild/SWBConfiguredTargetSourceFilesInfo.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// 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 SWBProtocol + +public struct SWBConfiguredTargetSourceFilesInfo: Equatable, Sendable { + public struct SourceFileInfo: Equatable, Sendable { + /// The path of the source file on disk + public let path: AbsolutePath + + /// The language of the source file. + /// + /// `nil` if the language could not be determined due to an error. + public let language: SWBSourceLanguage? + + /// The output path that is used for indexing, ie. the value of the `-index-unit-output-path` or `-o` option in + /// the source file's build settings. + /// + /// This is a `String` and not a `Path` because th index output path may be a fake path that is relative to the + /// build directory and has no relation to actual files on disks. + /// + /// May be `nil` if the output path could not be determined due to an error. + public let indexOutputPath: String? + + public init(path: AbsolutePath, language: SWBSourceLanguage? = nil, indexOutputPath: String? = nil) { + self.path = path + self.language = language + self.indexOutputPath = indexOutputPath + } + + init(_ sourceFileInfo: BuildDescriptionConfiguredTargetSourcesResponse.SourceFileInfo) { + self.path = AbsolutePath(sourceFileInfo.path) + self.language = SWBSourceLanguage(sourceFileInfo.language) + self.indexOutputPath = sourceFileInfo.indexOutputPath + } + } + + /// The configured target to which this info belongs + public let configuredTarget: SWBConfiguredTargetGUID + + /// Information about the source files in this source file + public let sourceFiles: [SourceFileInfo] + + public init(configuredTarget: SWBConfiguredTargetGUID, sourceFiles: [SWBConfiguredTargetSourceFilesInfo.SourceFileInfo]) { + self.configuredTarget = configuredTarget + self.sourceFiles = sourceFiles + } + + init(_ sourceFilesInfo: BuildDescriptionConfiguredTargetSourcesResponse.ConfiguredTargetSourceFilesInfo) { + self.configuredTarget = SWBConfiguredTargetGUID(sourceFilesInfo.configuredTarget) + self.sourceFiles = sourceFilesInfo.sourceFiles.map { SourceFileInfo($0) } + } +} diff --git a/Sources/SwiftBuild/SWBSourceLanguage.swift b/Sources/SwiftBuild/SWBSourceLanguage.swift new file mode 100644 index 00000000..edf5d75f --- /dev/null +++ b/Sources/SwiftBuild/SWBSourceLanguage.swift @@ -0,0 +1,40 @@ +//===----------------------------------------------------------------------===// +// +// 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 SWBProtocol + +public enum SWBSourceLanguage: Hashable, Sendable { + case c + case cpp + case metal + case objectiveC + case objectiveCpp + case swift + + init?(_ language: SourceLanguage?) { + guard let language else { + return nil + } + self.init(language) + } + + init(_ language: SourceLanguage) { + switch language { + case .c: self = .c + case .cpp: self = .cpp + case .metal: self = .metal + case .objectiveC: self = .objectiveC + case .objectiveCpp: self = .objectiveCpp + case .swift: self = .swift + } + } +} diff --git a/Sources/SwiftBuild/SWBTargetGUID.swift b/Sources/SwiftBuild/SWBTargetGUID.swift index 922c00c4..c70f433f 100644 --- a/Sources/SwiftBuild/SWBTargetGUID.swift +++ b/Sources/SwiftBuild/SWBTargetGUID.swift @@ -10,10 +10,16 @@ // //===----------------------------------------------------------------------===// +import SWBProtocol + public struct SWBTargetGUID: RawRepresentable, Hashable, Sendable { public var rawValue: String public init(rawValue: String) { self.rawValue = rawValue } + + init(_ guid: TargetGUID) { + self.init(rawValue: guid.rawValue) + } } diff --git a/Tests/SwiftBuildTests/InspectBuildDescriptionTests.swift b/Tests/SwiftBuildTests/InspectBuildDescriptionTests.swift new file mode 100644 index 00000000..e4fde56a --- /dev/null +++ b/Tests/SwiftBuildTests/InspectBuildDescriptionTests.swift @@ -0,0 +1,239 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation +import Testing +import SwiftBuild +import SwiftBuildTestSupport +import SWBTestSupport +@_spi(Testing) import SWBUtil +import SWBProtocol +import SWBCore + +// These tests use the old model, ie. the index build arena is disabled. +@Suite(.requireHostOS(.macOS)) +fileprivate struct InspectBuildDescriptionTests { + @Test + func configuredTargets() async throws { + try await withTemporaryDirectory { (temporaryDirectory: NamedTemporaryDirectory) in + try await withAsyncDeferrable { deferrable in + let tmpDir = temporaryDirectory.path + let testSession = try await TestSWBSession(temporaryDirectory: temporaryDirectory) + await deferrable.addBlock { + await #expect(throws: Never.self) { + try await testSession.close() + } + } + + let frameworkTarget = TestStandardTarget( + "MyFramework", + type: .framework, + buildPhases: [TestSourcesBuildPhase([TestBuildFile("MyFramework.swift")])], + ) + + let appTarget = TestStandardTarget( + "MyApp", + type: .application, + buildPhases: [TestSourcesBuildPhase([TestBuildFile("MyApp.swift")])], + dependencies: [TestTargetDependency("MyFramework")] + ) + + let project = TestProject( + "Test", + groupTree: TestGroup("Test", children: [TestFile("MyFramework.swift"), TestFile("MyApp.swift")]), + targets: [frameworkTarget, appTarget] + ) + + try await testSession.sendPIF(TestWorkspace("Test", sourceRoot: tmpDir, projects: [project])) + + let activeRunDestination = SWBRunDestinationInfo.macOS + let buildParameters = SWBBuildParameters(configuration: "Debug", activeRunDestination: activeRunDestination) + var request = SWBBuildRequest() + request.add(target: SWBConfiguredTarget(guid: appTarget.guid, parameters: buildParameters)) + + let buildDescriptionID = try await testSession.session.createBuildDescription(buildRequest: request) + let targetInfos = try await testSession.session.configuredTargets(buildDescription: buildDescriptionID, buildRequest: request) + + #expect(Set(targetInfos.map(\.name)) == ["MyFramework", "MyApp"]) + let frameworkTargetInfo = try #require(targetInfos.filter { $0.name == "MyFramework" }.only) + #expect(frameworkTargetInfo.dependencies == []) + #expect(frameworkTargetInfo.toolchain != nil) + let appTargetInfo = try #require(targetInfos.filter { $0.name == "MyApp" }.only) + #expect(appTargetInfo.dependencies == [frameworkTargetInfo.guid]) + #expect(appTargetInfo.toolchain != nil) + } + } + } + + @Test + func configuredTargetSources() async throws { + try await withTemporaryDirectory { (temporaryDirectory: NamedTemporaryDirectory) in + try await withAsyncDeferrable { deferrable in + let tmpDir = temporaryDirectory.path + let testSession = try await TestSWBSession(temporaryDirectory: temporaryDirectory) + await deferrable.addBlock { + await #expect(throws: Never.self) { + try await testSession.close() + } + } + + let appTarget = TestStandardTarget( + "MyApp", + type: .application, + buildPhases: [TestSourcesBuildPhase([TestBuildFile("MyApp.swift")])] + ) + + let otherAppTarget = TestStandardTarget( + "MyOtherApp", + type: .application, + buildPhases: [TestSourcesBuildPhase([TestBuildFile("MyOtherApp.swift")])] + ) + + let project = TestProject( + "Test", + groupTree: TestGroup("Test", children: [TestFile("MyApp.swift"), TestFile("MyOtherApp.swift")]), + targets: [appTarget, otherAppTarget] + ) + + try await testSession.sendPIF(TestWorkspace("Test", sourceRoot: tmpDir, projects: [project])) + + let activeRunDestination = SWBRunDestinationInfo.macOS + var buildParameters = SWBBuildParameters(configuration: "Debug", activeRunDestination: activeRunDestination) + buildParameters.overrides = SWBSettingsOverrides() + buildParameters.overrides.commandLine = SWBSettingsTable() + // Set `ONLY_ACTIVE_ARCH`, otherwise we get two entries for each Swift file, one for each architecture + // with a different output path. + buildParameters.overrides.commandLine!.set(value: "YES", for: "ONLY_ACTIVE_ARCH") + var request = SWBBuildRequest() + request.add(target: SWBConfiguredTarget(guid: appTarget.guid, parameters: buildParameters)) + request.add(target: SWBConfiguredTarget(guid: otherAppTarget.guid, parameters: buildParameters)) + + let buildDescriptionID = try await testSession.session.createBuildDescription(buildRequest: request) + let targetInfos = try await testSession.session.configuredTargets(buildDescription: buildDescriptionID, buildRequest: request) + let appTargetInfo = try #require(targetInfos.filter { $0.name == "MyApp" }.only) + let otherAppTargetInfo = try #require(targetInfos.filter { $0.name == "MyOtherApp" }.only) + + let appSources = try #require(await testSession.session.sources(of: [appTargetInfo.guid], buildDescription: buildDescriptionID, buildRequest: request).only) + #expect(appSources.configuredTarget == appTargetInfo.guid) + print(appSources.sourceFiles) + let myAppFile = try #require(appSources.sourceFiles.only) + #expect(myAppFile.path.pathString.hasSuffix("MyApp.swift")) + #expect(myAppFile.language == .swift) + #expect(myAppFile.indexOutputPath != nil) + + let combinedSources = try await testSession.session.sources( + of: [appTargetInfo.guid, otherAppTargetInfo.guid], + buildDescription: buildDescriptionID, + buildRequest: request + ) + #expect(Set(combinedSources.map(\.configuredTarget)) == [appTargetInfo.guid, otherAppTargetInfo.guid]) + #expect(Set(combinedSources.flatMap(\.sourceFiles).map { URL(filePath: $0.path.pathString).lastPathComponent }) == ["MyApp.swift", "MyOtherApp.swift"]) + + let emptyTargetListInfos = try await testSession.session.sources(of: [], buildDescription: buildDescriptionID, buildRequest: request) + #expect(emptyTargetListInfos == []) + + await #expect(throws: (any Error).self) { + try await testSession.session.sources( + of: [SWBConfiguredTargetGUID(rawValue: "does-not-exist")], + buildDescription: buildDescriptionID, + buildRequest: request + ) + } + } + } + } + + @Test + func indexCompilerArguments() async throws { + try await withTemporaryDirectory { (temporaryDirectory: NamedTemporaryDirectory) in + try await withAsyncDeferrable { deferrable in + let tmpDir = temporaryDirectory.path + let testSession = try await TestSWBSession(temporaryDirectory: temporaryDirectory) + await deferrable.addBlock { + await #expect(throws: Never.self) { + try await testSession.close() + } + } + + let appTarget = TestStandardTarget( + "MyApp", + type: .application, + buildConfigurations: [TestBuildConfiguration("Debug", buildSettings: ["SWIFT_ACTIVE_COMPILATION_CONDITIONS": "MY_APP"])], + buildPhases: [TestSourcesBuildPhase([TestBuildFile("MyApp.swift")])] + ) + + let otherAppTarget = TestStandardTarget( + "MyOtherApp", + type: .application, + buildConfigurations: [TestBuildConfiguration("Debug", buildSettings: ["SWIFT_ACTIVE_COMPILATION_CONDITIONS": "MY_OTHER_APP"])], + buildPhases: [TestSourcesBuildPhase([TestBuildFile("MyApp.swift")])] + ) + + let project = TestProject( + "Test", + groupTree: TestGroup("Test", children: [TestFile("MyApp.swift")]), + targets: [appTarget, otherAppTarget] + ) + + try await testSession.sendPIF(TestWorkspace("Test", sourceRoot: tmpDir, projects: [project])) + + let activeRunDestination = SWBRunDestinationInfo.macOS + var buildParameters = SWBBuildParameters(configuration: "Debug", activeRunDestination: activeRunDestination) + buildParameters.overrides = SWBSettingsOverrides() + buildParameters.overrides.commandLine = SWBSettingsTable() + // Set `ONLY_ACTIVE_ARCH`, otherwise we get two entries for each Swift file, one for each architecture + // with a different output path. + buildParameters.overrides.commandLine!.set(value: "YES", for: "ONLY_ACTIVE_ARCH") + var request = SWBBuildRequest() + request.add(target: SWBConfiguredTarget(guid: appTarget.guid, parameters: buildParameters)) + request.add(target: SWBConfiguredTarget(guid: otherAppTarget.guid, parameters: buildParameters)) + + let buildDescriptionID = try await testSession.session.createBuildDescription(buildRequest: request) + let targetInfos = try await testSession.session.configuredTargets(buildDescription: buildDescriptionID, buildRequest: request) + let appTargetInfo = try #require(targetInfos.filter { $0.name == "MyApp" }.only) + let otherAppTargetInfo = try #require(targetInfos.filter { $0.name == "MyOtherApp" }.only) + let appSources = try #require(await testSession.session.sources(of: [appTargetInfo.guid], buildDescription: buildDescriptionID, buildRequest: request).only) + let myAppFile = try #require(Set(appSources.sourceFiles.map(\.path)).filter { $0.pathString.hasSuffix("MyApp.swift") }.only) + + let appIndexSettings = try await testSession.session.indexCompilerArguments(of: myAppFile, in: appTargetInfo.guid, buildDescription: buildDescriptionID, buildRequest: request) + #expect(appIndexSettings.contains("-DMY_APP")) + #expect(!appIndexSettings.contains("-DMY_OTHER_APP")) + let otherAppIndexSettings = try await testSession.session.indexCompilerArguments(of: myAppFile, in: otherAppTargetInfo.guid, buildDescription: buildDescriptionID, buildRequest: request) + #expect(otherAppIndexSettings.contains("-DMY_OTHER_APP")) + } + } + } +} + +fileprivate extension SWBBuildServiceSession { + func createBuildDescription(buildRequest: SWBBuildRequest) async throws -> SWBBuildDescriptionID { + var buildDescriptionID: SWBBuildDescriptionID? + let buildDescriptionOperation = try await self.createBuildOperationForBuildDescriptionOnly( + request: buildRequest, + delegate: TestBuildOperationDelegate() + ) + for try await event in try await buildDescriptionOperation.start() { + guard case .reportBuildDescription(let info) = event else { + continue + } + guard buildDescriptionID == nil else { + Issue.record("Received multiple build description IDs") + continue + } + buildDescriptionID = SWBBuildDescriptionID(info.buildDescriptionID) + } + guard let buildDescriptionID else { + throw StubError.error("Failed to get build description ID") + } + return buildDescriptionID + } +}