diff --git a/Sources/SWBBuildService/BuildDescriptionMessages.swift b/Sources/SWBBuildService/BuildDescriptionMessages.swift index 917cba2a..2b91a2e2 100644 --- a/Sources/SWBBuildService/BuildDescriptionMessages.swift +++ b/Sources/SWBBuildService/BuildDescriptionMessages.swift @@ -125,7 +125,8 @@ struct BuildDescriptionConfiguredTargetsMsg: MessageHandler { identifier: ConfiguredTargetIdentifier(rawGUID: configuredTarget.guid.stringValue, targetGUID: TargetGUID(rawValue: configuredTarget.target.guid)), name: configuredTarget.target.name, dependencies: Set(dependencyRelationships?.flatMap(\.targetDependencies).compactMap { configuredTargetIdentifiersByGUID[$0.guid] } ?? []), - toolchain: toolchain + toolchain: toolchain, + artifactInfo: buildDescription.artifactInfoPerTarget[configuredTarget] ) } return BuildDescriptionConfiguredTargetsResponse(configuredTargets: targetInfos) diff --git a/Sources/SWBCore/SpecImplementations/ProductTypes.swift b/Sources/SWBCore/SpecImplementations/ProductTypes.swift index 75df0d4a..4bf20e07 100644 --- a/Sources/SWBCore/SpecImplementations/ProductTypes.swift +++ b/Sources/SWBCore/SpecImplementations/ProductTypes.swift @@ -69,6 +69,11 @@ public class ProductTypeSpec : Spec, SpecType, @unchecked Sendable { return nil } + public func artifactInfo(in scope: MacroEvaluationScope) -> ArtifactInfo? { + // Customization point for subclasses + return nil + } + /// Whether this product type supports having compiler sanitizer libraries embedded in it. public let canEmbedCompilerSanitizerLibraries: Bool @@ -529,6 +534,11 @@ public class FrameworkProductTypeSpec : BundleProductTypeSpec, @unchecked Sendab return descriptors } + public override func artifactInfo(in scope: MacroEvaluationScope) -> ArtifactInfo? { + let path = scope.evaluate(BuiltinMacros.TARGET_BUILD_DIR).join(scope.evaluate(BuiltinMacros.FULL_PRODUCT_NAME)) + return ArtifactInfo(kind: .framework, path: path) + } + /* /// Build setting expressions to evaluate to determine how to create symbolic links for the product structure. static let productStructureSymlinkBuildSettings = [SymlinkDescriptor]([ @@ -815,6 +825,12 @@ public final class DynamicLibraryProductTypeSpec : LibraryProductTypeSpec, @unch } return ([], []) } + + public override func artifactInfo(in scope: MacroEvaluationScope) -> ArtifactInfo? { + let path = scope.evaluate(BuiltinMacros.TARGET_BUILD_DIR).join(scope.evaluate(BuiltinMacros.FULL_PRODUCT_NAME)) + return ArtifactInfo(kind: .dynamicLibrary, path: path) + } + } public final class StaticLibraryProductTypeSpec : LibraryProductTypeSpec, @unchecked Sendable { @@ -827,12 +843,22 @@ public final class StaticLibraryProductTypeSpec : LibraryProductTypeSpec, @unche public override var supportsDefinesModule: Bool { return true } + + public override func artifactInfo(in scope: MacroEvaluationScope) -> ArtifactInfo? { + let path = scope.evaluate(BuiltinMacros.TARGET_BUILD_DIR).join(scope.evaluate(BuiltinMacros.FULL_PRODUCT_NAME)) + return ArtifactInfo(kind: .staticLibrary, path: path) + } } public final class ToolProductTypeSpec : StandaloneExecutableProductTypeSpec, @unchecked Sendable { class public override var className: String { return "PBXToolProductType" } + + public override func artifactInfo(in scope: MacroEvaluationScope) -> ArtifactInfo? { + let path = scope.evaluate(BuiltinMacros.TARGET_BUILD_DIR).join(scope.evaluate(BuiltinMacros.FULL_PRODUCT_NAME)) + return ArtifactInfo(kind: .executable, path: path) + } } /// Describes a symbolic link to create. diff --git a/Sources/SWBProtocol/BuildDescriptionMessages.swift b/Sources/SWBProtocol/BuildDescriptionMessages.swift index b154512f..6ec15f8a 100644 --- a/Sources/SWBProtocol/BuildDescriptionMessages.swift +++ b/Sources/SWBProtocol/BuildDescriptionMessages.swift @@ -76,11 +76,14 @@ public struct BuildDescriptionConfiguredTargetsResponse: Message, SerializableCo /// `nil` if the toolchain for this target could not be determined due to an error. public let toolchain: Path? - public init(identifier: ConfiguredTargetIdentifier, name: String, dependencies: Set, toolchain: Path?) { + public let artifactInfo: ArtifactInfo? + + public init(identifier: ConfiguredTargetIdentifier, name: String, dependencies: Set, toolchain: Path?, artifactInfo: ArtifactInfo?) { self.identifier = identifier self.name = name self.dependencies = dependencies self.toolchain = toolchain + self.artifactInfo = artifactInfo } } diff --git a/Sources/SWBTaskExecution/BuildDescription.swift b/Sources/SWBTaskExecution/BuildDescription.swift index 460e79c4..02eceeba 100644 --- a/Sources/SWBTaskExecution/BuildDescription.swift +++ b/Sources/SWBTaskExecution/BuildDescription.swift @@ -141,6 +141,9 @@ package final class BuildDescription: Serializable, Sendable, Encodable, Cacheab private let rootPathsPerTarget: [ConfiguredTarget: [Path]] private let moduleCachePathsPerTarget: [ConfiguredTarget: [Path]] + /// Describes the artifact produced by a target. Not all targets will have artifact info. + package let artifactInfoPerTarget: [ConfiguredTarget: ArtifactInfo] + /// A description of a CAS for validation, including how it is configured /// and which llvm-cas should be used to validate it. package struct CASValidationInfo { @@ -203,13 +206,14 @@ package final class BuildDescription: Serializable, Sendable, Encodable, Cacheab package let emitFrontendCommandLines: Bool /// Load a build description from the given path. - fileprivate init(inDir dir: Path, signature: BuildDescriptionSignature, taskStore: FrozenTaskStore, allOutputPaths: Set, rootPathsPerTarget: [ConfiguredTarget: [Path]], moduleCachePathsPerTarget: [ConfiguredTarget: [Path]], casValidationInfos: [CASValidationInfo], settingsPerTarget: [ConfiguredTarget: Settings], enableStaleFileRemoval: Bool = true, taskActionMap: [String: TaskAction.Type], targetTaskCounts: [ConfiguredTarget: Int], moduleSessionFilePath: Path?, diagnostics: [ConfiguredTarget?: [Diagnostic]], fs: any FSProxy, invalidationPaths: [Path], recursiveSearchPathResults: [RecursiveSearchPathResolver.CachedResult], copiedPathMap: [String: String], targetDependencies: [TargetDependencyRelationship], definingTargetsByModuleName: [String: OrderedSet], capturedBuildInfo: CapturedBuildInfo?, bypassActualTasks: Bool, targetsBuildInParallel: Bool, emitFrontendCommandLines: Bool) throws { + fileprivate init(inDir dir: Path, signature: BuildDescriptionSignature, taskStore: FrozenTaskStore, allOutputPaths: Set, rootPathsPerTarget: [ConfiguredTarget: [Path]], moduleCachePathsPerTarget: [ConfiguredTarget: [Path]], artifactInfoPerTarget: [ConfiguredTarget: ArtifactInfo], casValidationInfos: [CASValidationInfo], settingsPerTarget: [ConfiguredTarget: Settings], enableStaleFileRemoval: Bool = true, taskActionMap: [String: TaskAction.Type], targetTaskCounts: [ConfiguredTarget: Int], moduleSessionFilePath: Path?, diagnostics: [ConfiguredTarget?: [Diagnostic]], fs: any FSProxy, invalidationPaths: [Path], recursiveSearchPathResults: [RecursiveSearchPathResolver.CachedResult], copiedPathMap: [String: String], targetDependencies: [TargetDependencyRelationship], definingTargetsByModuleName: [String: OrderedSet], capturedBuildInfo: CapturedBuildInfo?, bypassActualTasks: Bool, targetsBuildInParallel: Bool, emitFrontendCommandLines: Bool) throws { self.dir = dir self.signature = signature self.taskStore = taskStore self.allOutputPaths = allOutputPaths self.rootPathsPerTarget = rootPathsPerTarget self.moduleCachePathsPerTarget = moduleCachePathsPerTarget + self.artifactInfoPerTarget = artifactInfoPerTarget self.casValidationInfos = casValidationInfos self.dependencyValidationPerTarget = settingsPerTarget.mapValues { $0.globalScope.evaluate(BuiltinMacros.VALIDATE_DEPENDENCIES) } self.taskActionMap = taskActionMap @@ -331,13 +335,14 @@ package final class BuildDescription: Serializable, Sendable, Encodable, Cacheab package func serialize(to serializer: T) { guard serializer.delegate is BuildDescriptionSerializerDelegate else { fatalError("delegate must be a BuildDescriptionSerializerDelegate") } - serializer.beginAggregate(20) + serializer.beginAggregate(21) serializer.serialize(dir) serializer.serialize(signature) // Serialize the tasks first so we can index into this array during deserialization. serializer.serialize(allOutputPaths) serializer.serialize(rootPathsPerTarget) serializer.serialize(moduleCachePathsPerTarget) + serializer.serialize(artifactInfoPerTarget) serializer.serialize(dependencyValidationPerTarget) serializer.beginAggregate(taskActionMap.count) for (tool, taskActionClass) in taskActionMap.sorted(byKey: <) { @@ -371,12 +376,13 @@ package final class BuildDescription: Serializable, Sendable, Encodable, Cacheab // Check that we have the appropriate delegate. guard let delegate = deserializer.delegate as? BuildDescriptionDeserializerDelegate else { throw DeserializerError.invalidDelegate("delegate must be a BuildDescriptionDeserializerDelegate") } - try deserializer.beginAggregate(20) + try deserializer.beginAggregate(21) self.dir = try deserializer.deserialize() self.signature = try deserializer.deserialize() self.allOutputPaths = try deserializer.deserialize() self.rootPathsPerTarget = try deserializer.deserialize() self.moduleCachePathsPerTarget = try deserializer.deserialize() + self.artifactInfoPerTarget = try deserializer.deserialize() self.dependencyValidationPerTarget = try deserializer.deserialize() var taskActionMap = [String: TaskAction.Type]() let taskActionMapCount = try deserializer.beginAggregate() @@ -546,6 +552,8 @@ package final class BuildDescriptionBuilder { // The map of module cache path per configured target. private let moduleCachePathsPerTarget: [ConfiguredTarget: [Path]] + private let artifactInfoPerTarget: [ConfiguredTarget: ArtifactInfo] + /// The set of all CAS directories and their corresponding CASOptions. private let casValidationInfos: [BuildDescription.CASValidationInfo] @@ -566,7 +574,7 @@ package final class BuildDescriptionBuilder { /// - Parameters: /// - path: The path of a directory to store the build description to. /// - bypassActualTasks: If enabled, replace tasks with fake ones (`/usr/bin/true`). - init(path: Path, signature: BuildDescriptionSignature, buildCommand: BuildCommand, taskAdditionalInputs: [Ref: NodeList], mutatedNodes: Set>, mutatingTasks: [Ref: MutatingTaskInfo], bypassActualTasks: Bool, targetsBuildInParallel: Bool, emitFrontendCommandLines: Bool, moduleSessionFilePath: Path?, invalidationPaths: [Path], recursiveSearchPathResults: [RecursiveSearchPathResolver.CachedResult], copiedPathMap: [String: String], outputPathsPerTarget: [ConfiguredTarget?: [Path]], allOutputPaths: Set, rootPathsPerTarget: [ConfiguredTarget: [Path]], moduleCachePathsPerTarget: [ConfiguredTarget: [Path]], casValidationInfos: [BuildDescription.CASValidationInfo], staleFileRemovalIdentifierPerTarget: [ConfiguredTarget?: String], settingsPerTarget: [ConfiguredTarget: Settings], targetDependencies: [TargetDependencyRelationship], definingTargetsByModuleName: [String: OrderedSet], workspace: Workspace, capturedBuildInfo: CapturedBuildInfo?) { + init(path: Path, signature: BuildDescriptionSignature, buildCommand: BuildCommand, taskAdditionalInputs: [Ref: NodeList], mutatedNodes: Set>, mutatingTasks: [Ref: MutatingTaskInfo], bypassActualTasks: Bool, targetsBuildInParallel: Bool, emitFrontendCommandLines: Bool, moduleSessionFilePath: Path?, invalidationPaths: [Path], recursiveSearchPathResults: [RecursiveSearchPathResolver.CachedResult], copiedPathMap: [String: String], outputPathsPerTarget: [ConfiguredTarget?: [Path]], allOutputPaths: Set, rootPathsPerTarget: [ConfiguredTarget: [Path]], moduleCachePathsPerTarget: [ConfiguredTarget: [Path]], artifactInfoPerTarget: [ConfiguredTarget: ArtifactInfo], casValidationInfos: [BuildDescription.CASValidationInfo], staleFileRemovalIdentifierPerTarget: [ConfiguredTarget?: String], settingsPerTarget: [ConfiguredTarget: Settings], targetDependencies: [TargetDependencyRelationship], definingTargetsByModuleName: [String: OrderedSet], workspace: Workspace, capturedBuildInfo: CapturedBuildInfo?) { self.path = path self.signature = signature self.taskAdditionalInputs = taskAdditionalInputs @@ -583,6 +591,7 @@ package final class BuildDescriptionBuilder { self.allOutputPaths = allOutputPaths self.rootPathsPerTarget = rootPathsPerTarget self.moduleCachePathsPerTarget = moduleCachePathsPerTarget + self.artifactInfoPerTarget = artifactInfoPerTarget self.casValidationInfos = casValidationInfos self.staleFileRemovalIdentifierPerTarget = staleFileRemovalIdentifierPerTarget self.settingsPerTarget = settingsPerTarget @@ -696,7 +705,7 @@ package final class BuildDescriptionBuilder { // Create the build description. let buildDescription: BuildDescription do { - buildDescription = try BuildDescription(inDir: path, signature: signature, taskStore: frozenTaskStore, allOutputPaths: allOutputPaths, rootPathsPerTarget: rootPathsPerTarget, moduleCachePathsPerTarget: moduleCachePathsPerTarget, casValidationInfos: casValidationInfos, settingsPerTarget: settingsPerTarget, taskActionMap: taskActionMap, targetTaskCounts: targetTaskCounts, moduleSessionFilePath: moduleSessionFilePath, diagnostics: diagnosticsEngines.mapValues { engine in engine.diagnostics }, fs: fs, invalidationPaths: invalidationPaths, recursiveSearchPathResults: recursiveSearchPathResults, copiedPathMap: copiedPathMap, targetDependencies: targetDependencies, definingTargetsByModuleName: definingTargetsByModuleName, capturedBuildInfo: capturedBuildInfo, bypassActualTasks: bypassActualTasks, targetsBuildInParallel: targetsBuildInParallel, emitFrontendCommandLines: emitFrontendCommandLines) + buildDescription = try BuildDescription(inDir: path, signature: signature, taskStore: frozenTaskStore, allOutputPaths: allOutputPaths, rootPathsPerTarget: rootPathsPerTarget, moduleCachePathsPerTarget: moduleCachePathsPerTarget, artifactInfoPerTarget: artifactInfoPerTarget, casValidationInfos: casValidationInfos, settingsPerTarget: settingsPerTarget, taskActionMap: taskActionMap, targetTaskCounts: targetTaskCounts, moduleSessionFilePath: moduleSessionFilePath, diagnostics: diagnosticsEngines.mapValues { engine in engine.diagnostics }, fs: fs, invalidationPaths: invalidationPaths, recursiveSearchPathResults: recursiveSearchPathResults, copiedPathMap: copiedPathMap, targetDependencies: targetDependencies, definingTargetsByModuleName: definingTargetsByModuleName, capturedBuildInfo: capturedBuildInfo, bypassActualTasks: bypassActualTasks, targetsBuildInParallel: targetsBuildInParallel, emitFrontendCommandLines: emitFrontendCommandLines) } catch { throw StubError.error("unable to create build description: \(error)") @@ -1030,7 +1039,7 @@ extension BuildDescription { // FIXME: Bypass actual tasks should go away, eventually. // // FIXME: This layering isn't working well, we are plumbing a bunch of stuff through here just because we don't want to talk to TaskConstruction. - static package func construct(workspace: Workspace, tasks: [any PlannedTask], path: Path, signature: BuildDescriptionSignature, buildCommand: BuildCommand, diagnostics: [ConfiguredTarget?: [Diagnostic]] = [:], indexingInfo: [(forTarget: ConfiguredTarget?, path: Path, indexingInfo: any SourceFileIndexingInfo)] = [], fs: any FSProxy = localFS, bypassActualTasks: Bool = false, targetsBuildInParallel: Bool = true, emitFrontendCommandLines: Bool = false, moduleSessionFilePath: Path? = nil, invalidationPaths: [Path] = [], recursiveSearchPathResults: [RecursiveSearchPathResolver.CachedResult] = [], copiedPathMap: [String: String] = [:], rootPathsPerTarget: [ConfiguredTarget:[Path]] = [:], moduleCachePathsPerTarget: [ConfiguredTarget: [Path]] = [:], casValidationInfos: [BuildDescription.CASValidationInfo] = [], staleFileRemovalIdentifierPerTarget: [ConfiguredTarget?: String] = [:], settingsPerTarget: [ConfiguredTarget: Settings] = [:], delegate: any BuildDescriptionConstructionDelegate, targetDependencies: [TargetDependencyRelationship] = [], definingTargetsByModuleName: [String: OrderedSet], capturedBuildInfo: CapturedBuildInfo?, userPreferences: UserPreferences) async throws -> BuildDescription? { + static package func construct(workspace: Workspace, tasks: [any PlannedTask], path: Path, signature: BuildDescriptionSignature, buildCommand: BuildCommand, diagnostics: [ConfiguredTarget?: [Diagnostic]] = [:], indexingInfo: [(forTarget: ConfiguredTarget?, path: Path, indexingInfo: any SourceFileIndexingInfo)] = [], fs: any FSProxy = localFS, bypassActualTasks: Bool = false, targetsBuildInParallel: Bool = true, emitFrontendCommandLines: Bool = false, moduleSessionFilePath: Path? = nil, invalidationPaths: [Path] = [], recursiveSearchPathResults: [RecursiveSearchPathResolver.CachedResult] = [], copiedPathMap: [String: String] = [:], rootPathsPerTarget: [ConfiguredTarget:[Path]] = [:], moduleCachePathsPerTarget: [ConfiguredTarget: [Path]] = [:], artifactInfoPerTarget: [ConfiguredTarget: ArtifactInfo] = [:], casValidationInfos: [BuildDescription.CASValidationInfo] = [], staleFileRemovalIdentifierPerTarget: [ConfiguredTarget?: String] = [:], settingsPerTarget: [ConfiguredTarget: Settings] = [:], delegate: any BuildDescriptionConstructionDelegate, targetDependencies: [TargetDependencyRelationship] = [], definingTargetsByModuleName: [String: OrderedSet], capturedBuildInfo: CapturedBuildInfo?, userPreferences: UserPreferences) async throws -> BuildDescription? { var diagnostics = diagnostics // We operate on the sorted tasks here to ensure that the list of task additional inputs is deterministic. @@ -1293,7 +1302,7 @@ extension BuildDescription { } // Create the builder. - let builder = BuildDescriptionBuilder(path: path, signature: signature, buildCommand: buildCommand, taskAdditionalInputs: taskAdditionalInputs, mutatedNodes: Set(mutableNodes.keys), mutatingTasks: mutatingTasks, bypassActualTasks: bypassActualTasks, targetsBuildInParallel: targetsBuildInParallel, emitFrontendCommandLines: emitFrontendCommandLines, moduleSessionFilePath: moduleSessionFilePath, invalidationPaths: invalidationPaths, recursiveSearchPathResults: recursiveSearchPathResults, copiedPathMap: copiedPathMap, outputPathsPerTarget: outputPathsPerTarget, allOutputPaths: Set(producers.keys.map { $0.instance.path }), rootPathsPerTarget: rootPathsPerTarget, moduleCachePathsPerTarget: moduleCachePathsPerTarget, casValidationInfos: casValidationInfos, staleFileRemovalIdentifierPerTarget: staleFileRemovalIdentifierPerTarget, settingsPerTarget: settingsPerTarget, targetDependencies: targetDependencies, definingTargetsByModuleName: definingTargetsByModuleName, workspace: workspace, capturedBuildInfo: capturedBuildInfo) + let builder = BuildDescriptionBuilder(path: path, signature: signature, buildCommand: buildCommand, taskAdditionalInputs: taskAdditionalInputs, mutatedNodes: Set(mutableNodes.keys), mutatingTasks: mutatingTasks, bypassActualTasks: bypassActualTasks, targetsBuildInParallel: targetsBuildInParallel, emitFrontendCommandLines: emitFrontendCommandLines, moduleSessionFilePath: moduleSessionFilePath, invalidationPaths: invalidationPaths, recursiveSearchPathResults: recursiveSearchPathResults, copiedPathMap: copiedPathMap, outputPathsPerTarget: outputPathsPerTarget, allOutputPaths: Set(producers.keys.map { $0.instance.path }), rootPathsPerTarget: rootPathsPerTarget, moduleCachePathsPerTarget: moduleCachePathsPerTarget, artifactInfoPerTarget: artifactInfoPerTarget, casValidationInfos: casValidationInfos, staleFileRemovalIdentifierPerTarget: staleFileRemovalIdentifierPerTarget, settingsPerTarget: settingsPerTarget, targetDependencies: targetDependencies, definingTargetsByModuleName: definingTargetsByModuleName, workspace: workspace, capturedBuildInfo: capturedBuildInfo) for (target, diagnostics) in diagnostics { let engine = builder.diagnosticsEngines.getOrInsert(target, { DiagnosticsEngine() }) for diag in diagnostics { diff --git a/Sources/SWBTaskExecution/BuildDescriptionManager.swift b/Sources/SWBTaskExecution/BuildDescriptionManager.swift index 248bca9e..d0a2a29d 100644 --- a/Sources/SWBTaskExecution/BuildDescriptionManager.swift +++ b/Sources/SWBTaskExecution/BuildDescriptionManager.swift @@ -183,6 +183,7 @@ package final class BuildDescriptionManager: Sendable { var settingsPerTarget = [ConfiguredTarget:Settings]() var rootPathsPerTarget = [ConfiguredTarget:[Path]]() var moduleCachePathsPerTarget = [ConfiguredTarget: [Path]]() + var artifactInfoPerTarget = [ConfiguredTarget: ArtifactInfo]() var casValidationInfos: OrderedSet = [] let buildGraph = planRequest.buildGraph @@ -205,6 +206,8 @@ package final class BuildDescriptionManager: Sendable { settings.globalScope.evaluate(BuiltinMacros.CLANG_EXPLICIT_MODULES_OUTPUT_PATH), ] + artifactInfoPerTarget[target] = settings.productType?.artifactInfo(in: settings.globalScope) + if shouldValidateCAS, settings.globalScope.evaluate(BuiltinMacros.CLANG_ENABLE_COMPILE_CACHE) || settings.globalScope.evaluate(BuiltinMacros.SWIFT_ENABLE_COMPILE_CACHE) { // FIXME: currently we only handle the compiler cache here, because the plugin configuration for the generic CAS is not configured by build settings. for purpose in [CASOptions.Purpose.compiler(.c)] { @@ -249,7 +252,7 @@ package final class BuildDescriptionManager: Sendable { } // Create the build description. - return try await BuildDescription.construct(workspace: buildGraph.workspaceContext.workspace, tasks: plan.tasks, path: path, signature: signature, buildCommand: planRequest.buildRequest.buildCommand, diagnostics: planningDiagnostics, indexingInfo: [], fs: fs, bypassActualTasks: bypassActualTasks, targetsBuildInParallel: buildGraph.targetsBuildInParallel, emitFrontendCommandLines: plan.emitFrontendCommandLines, moduleSessionFilePath: planRequest.workspaceContext.getModuleSessionFilePath(planRequest.buildRequest.parameters), invalidationPaths: plan.invalidationPaths, recursiveSearchPathResults: plan.recursiveSearchPathResults, copiedPathMap: plan.copiedPathMap, rootPathsPerTarget: rootPathsPerTarget, moduleCachePathsPerTarget: moduleCachePathsPerTarget, casValidationInfos: casValidationInfos.elements, staleFileRemovalIdentifierPerTarget: staleFileRemovalIdentifierPerTarget, settingsPerTarget: settingsPerTarget, delegate: delegate, targetDependencies: buildGraph.targetDependenciesByGuid, definingTargetsByModuleName: definingTargetsByModuleName, capturedBuildInfo: capturedBuildInfo, userPreferences: buildGraph.workspaceContext.userPreferences) + return try await BuildDescription.construct(workspace: buildGraph.workspaceContext.workspace, tasks: plan.tasks, path: path, signature: signature, buildCommand: planRequest.buildRequest.buildCommand, diagnostics: planningDiagnostics, indexingInfo: [], fs: fs, bypassActualTasks: bypassActualTasks, targetsBuildInParallel: buildGraph.targetsBuildInParallel, emitFrontendCommandLines: plan.emitFrontendCommandLines, moduleSessionFilePath: planRequest.workspaceContext.getModuleSessionFilePath(planRequest.buildRequest.parameters), invalidationPaths: plan.invalidationPaths, recursiveSearchPathResults: plan.recursiveSearchPathResults, copiedPathMap: plan.copiedPathMap, rootPathsPerTarget: rootPathsPerTarget, moduleCachePathsPerTarget: moduleCachePathsPerTarget, artifactInfoPerTarget: artifactInfoPerTarget, casValidationInfos: casValidationInfos.elements, staleFileRemovalIdentifierPerTarget: staleFileRemovalIdentifierPerTarget, settingsPerTarget: settingsPerTarget, delegate: delegate, targetDependencies: buildGraph.targetDependenciesByGuid, definingTargetsByModuleName: definingTargetsByModuleName, capturedBuildInfo: capturedBuildInfo, userPreferences: buildGraph.workspaceContext.userPreferences) } /// Encapsulates the two ways `getNewOrCachedBuildDescription` can be called, whether we want to retrieve or create a build description based on a plan or whether we have an explicit build description ID that we want to retrieve and we don't need to create a new one. diff --git a/Sources/SWBTestSupport/TaskExecutionTestSupport.swift b/Sources/SWBTestSupport/TaskExecutionTestSupport.swift index 3b9e2088..2bd45e9f 100644 --- a/Sources/SWBTestSupport/TaskExecutionTestSupport.swift +++ b/Sources/SWBTestSupport/TaskExecutionTestSupport.swift @@ -94,8 +94,8 @@ package struct TestManifest: Sendable { extension BuildDescription { /// Convenience testing method which omits the `capturedBuildInfo:` parameter. - static package func construct(workspace: Workspace, tasks: [any PlannedTask], path: Path, signature: BuildDescriptionSignature, buildCommand: BuildCommand, diagnostics: [ConfiguredTarget?: [Diagnostic]] = [:], indexingInfo: [(forTarget: ConfiguredTarget?, path: Path, indexingInfo: any SourceFileIndexingInfo)] = [], fs: any FSProxy = localFS, bypassActualTasks: Bool = false, moduleSessionFilePath: Path? = nil, invalidationPaths: [Path] = [], recursiveSearchPathResults: [RecursiveSearchPathResolver.CachedResult] = [], copiedPathMap: [String: String] = [:], rootPathsPerTarget: [ConfiguredTarget:[Path]] = [:], moduleCachePathsPerTarget: [ConfiguredTarget: [Path]] = [:], casValidationInfos: [BuildDescription.CASValidationInfo] = [], staleFileRemovalIdentifierPerTarget: [ConfiguredTarget: String] = [:], settingsPerTarget: [ConfiguredTarget: Settings] = [:], delegate: any BuildDescriptionConstructionDelegate, targetDependencies: [TargetDependencyRelationship] = [], definingTargetsByModuleName: [String: OrderedSet] = [:]) async throws -> BuildDescription? { - return try await construct(workspace: workspace, tasks: tasks, path: path, signature: signature, buildCommand: buildCommand, diagnostics: diagnostics, indexingInfo: indexingInfo, fs: fs, bypassActualTasks: bypassActualTasks, moduleSessionFilePath: moduleSessionFilePath, invalidationPaths: invalidationPaths, recursiveSearchPathResults: recursiveSearchPathResults, copiedPathMap: copiedPathMap, rootPathsPerTarget: rootPathsPerTarget, moduleCachePathsPerTarget: moduleCachePathsPerTarget, casValidationInfos: casValidationInfos, staleFileRemovalIdentifierPerTarget: staleFileRemovalIdentifierPerTarget, settingsPerTarget: settingsPerTarget, delegate: delegate, targetDependencies: targetDependencies, definingTargetsByModuleName: definingTargetsByModuleName, capturedBuildInfo: nil, userPreferences: .defaultForTesting) + static package func construct(workspace: Workspace, tasks: [any PlannedTask], path: Path, signature: BuildDescriptionSignature, buildCommand: BuildCommand, diagnostics: [ConfiguredTarget?: [Diagnostic]] = [:], indexingInfo: [(forTarget: ConfiguredTarget?, path: Path, indexingInfo: any SourceFileIndexingInfo)] = [], fs: any FSProxy = localFS, bypassActualTasks: Bool = false, moduleSessionFilePath: Path? = nil, invalidationPaths: [Path] = [], recursiveSearchPathResults: [RecursiveSearchPathResolver.CachedResult] = [], copiedPathMap: [String: String] = [:], rootPathsPerTarget: [ConfiguredTarget:[Path]] = [:], moduleCachePathsPerTarget: [ConfiguredTarget: [Path]] = [:], artifactInfoPerTarget: [ConfiguredTarget: ArtifactInfo] = [:], casValidationInfos: [BuildDescription.CASValidationInfo] = [], staleFileRemovalIdentifierPerTarget: [ConfiguredTarget: String] = [:], settingsPerTarget: [ConfiguredTarget: Settings] = [:], delegate: any BuildDescriptionConstructionDelegate, targetDependencies: [TargetDependencyRelationship] = [], definingTargetsByModuleName: [String: OrderedSet] = [:]) async throws -> BuildDescription? { + return try await construct(workspace: workspace, tasks: tasks, path: path, signature: signature, buildCommand: buildCommand, diagnostics: diagnostics, indexingInfo: indexingInfo, fs: fs, bypassActualTasks: bypassActualTasks, moduleSessionFilePath: moduleSessionFilePath, invalidationPaths: invalidationPaths, recursiveSearchPathResults: recursiveSearchPathResults, copiedPathMap: copiedPathMap, rootPathsPerTarget: rootPathsPerTarget, moduleCachePathsPerTarget: moduleCachePathsPerTarget, artifactInfoPerTarget: artifactInfoPerTarget, casValidationInfos: casValidationInfos, staleFileRemovalIdentifierPerTarget: staleFileRemovalIdentifierPerTarget, settingsPerTarget: settingsPerTarget, delegate: delegate, targetDependencies: targetDependencies, definingTargetsByModuleName: definingTargetsByModuleName, capturedBuildInfo: nil, userPreferences: .defaultForTesting) } } diff --git a/Sources/SWBUtil/ArtifactInfo.swift b/Sources/SWBUtil/ArtifactInfo.swift new file mode 100644 index 00000000..e8e76550 --- /dev/null +++ b/Sources/SWBUtil/ArtifactInfo.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +///. Describes high-level information about an artifact produced by a particular ConfiguredTarget, suitable for consumption by build tools like SwiftPM command plugins. +public struct ArtifactInfo: Equatable, Hashable, Sendable, SerializableCodable { + public enum Kind: Equatable, Hashable, Sendable, SerializableCodable { + case executable + case staticLibrary + case dynamicLibrary + case framework + } + + public let kind: Kind + public let path: Path + + public init(kind: Kind, path: Path) { + self.kind = kind + self.path = path + } +} diff --git a/Sources/SWBUtil/CMakeLists.txt b/Sources/SWBUtil/CMakeLists.txt index 681fc86e..8121b52d 100644 --- a/Sources/SWBUtil/CMakeLists.txt +++ b/Sources/SWBUtil/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(SWBUtil Architecture.swift ArgumentSplitting.swift Array.swift + ArtifactInfo.swift AsyncCache.swift AsyncFlatteningSequence.swift AsyncIteratorProtocol.swift diff --git a/Sources/SwiftBuild/SWBBuildServiceSession.swift b/Sources/SwiftBuild/SWBBuildServiceSession.swift index 6fadaa40..c5423b89 100644 --- a/Sources/SwiftBuild/SWBBuildServiceSession.swift +++ b/Sources/SwiftBuild/SWBBuildServiceSession.swift @@ -181,6 +181,11 @@ public final class SWBBuildServiceSession: Sendable { try await createBuildOperation(request: request, delegate: delegate, onlyCreateBuildDescription: false, retainBuildDescription: false) } + @_disfavoredOverload + public func createBuildOperation(request: SWBBuildRequest, delegate: any SWBPlanningOperationDelegate, retainBuildDescription: Bool) async throws -> SWBBuildOperation { + try await createBuildOperation(request: request, delegate: delegate, onlyCreateBuildDescription: false, retainBuildDescription: retainBuildDescription) + } + @_disfavoredOverload public func createBuildOperationForBuildDescriptionOnly(request: SWBBuildRequest, delegate: any SWBPlanningOperationDelegate) async throws -> SWBBuildOperation { try await createBuildOperation(request: request, delegate: delegate, onlyCreateBuildDescription: true, retainBuildDescription: false) diff --git a/Sources/SwiftBuild/SWBConfiguredTargetInfo.swift b/Sources/SwiftBuild/SWBConfiguredTargetInfo.swift index 55b41e36..98aa9f76 100644 --- a/Sources/SwiftBuild/SWBConfiguredTargetInfo.swift +++ b/Sources/SwiftBuild/SWBConfiguredTargetInfo.swift @@ -11,8 +11,34 @@ //===----------------------------------------------------------------------===// import SWBProtocol +import SWBUtil -public struct SWBConfiguredTargetInfo { +public struct SWBArtifactInfo: Sendable { + public enum Kind: Sendable { + case executable + case staticLibrary + case dynamicLibrary + case framework + } + public var kind: Kind + public var path: String + + init(_ info: ArtifactInfo) { + switch info.kind { + case .executable: + self.kind = .executable + case .staticLibrary: + self.kind = .staticLibrary + case .dynamicLibrary: + self.kind = .dynamicLibrary + case .framework: + self.kind = .framework + } + self.path = info.path.str + } +} + +public struct SWBConfiguredTargetInfo: Sendable { /// The GUID of this configured target public let identifier: SWBConfiguredTargetIdentifier @@ -27,11 +53,14 @@ public struct SWBConfiguredTargetInfo { /// `nil` if the toolchain for this target could not be determined due to an error. public let toolchain: AbsolutePath? - public init(identifier: SWBConfiguredTargetIdentifier, name: String, dependencies: Set, toolchain: AbsolutePath?) { + public let artifactInfo: SWBArtifactInfo? + + public init(identifier: SWBConfiguredTargetIdentifier, name: String, dependencies: Set, toolchain: AbsolutePath?, artifactInfo: SWBArtifactInfo?) { self.identifier = identifier self.name = name self.dependencies = dependencies self.toolchain = toolchain + self.artifactInfo = artifactInfo } init(_ configuredTargetInfo: BuildDescriptionConfiguredTargetsResponse.ConfiguredTargetInfo) { @@ -39,7 +68,8 @@ public struct SWBConfiguredTargetInfo { identifier: SWBConfiguredTargetIdentifier(configuredTargetIdentifier: configuredTargetInfo.identifier), name: configuredTargetInfo.name, dependencies: Set(configuredTargetInfo.dependencies.map { SWBConfiguredTargetIdentifier(configuredTargetIdentifier: $0) }), - toolchain: AbsolutePath(configuredTargetInfo.toolchain) + toolchain: AbsolutePath(configuredTargetInfo.toolchain), + artifactInfo: configuredTargetInfo.artifactInfo.map { SWBArtifactInfo($0) } ) } } diff --git a/Tests/SWBTaskExecutionTests/BuildDescriptionTests.swift b/Tests/SWBTaskExecutionTests/BuildDescriptionTests.swift index 2b86165b..8cf74b36 100644 --- a/Tests/SWBTaskExecutionTests/BuildDescriptionTests.swift +++ b/Tests/SWBTaskExecutionTests/BuildDescriptionTests.swift @@ -810,8 +810,8 @@ fileprivate struct BuildDescriptionTests: CoreBasedTests { } private extension BuildDescription { - static func construct(workspace: Workspace, tasks: [any PlannedTask], path: Path, signature: BuildDescriptionSignature, buildCommand: BuildCommand? = nil, diagnostics: [ConfiguredTarget?: [Diagnostic]] = [:], indexingInfo: [(forTarget: ConfiguredTarget?, path: Path, indexingInfo: any SourceFileIndexingInfo)] = [], fs: any FSProxy = localFS, bypassActualTasks: Bool = false, moduleSessionFilePath: Path? = nil, invalidationPaths: [Path] = [], recursiveSearchPathResults: [RecursiveSearchPathResolver.CachedResult] = [], copiedPathMap: [String: String] = [:], rootPathsPerTarget: [ConfiguredTarget:[Path]] = [:], moduleCachePathsPerTarget: [ConfiguredTarget: [Path]] = [:], staleFileRemovalIdentifierPerTarget: [ConfiguredTarget: String] = [:], settingsPerTarget: [ConfiguredTarget: Settings] = [:], targetDependencies: [TargetDependencyRelationship] = [], definingTargetsByModuleName: [String: OrderedSet] = [:]) async throws -> BuildDescriptionDiagnosticResults? { - let buildDescription = try await construct(workspace: workspace, tasks: tasks, path: path, signature: signature, buildCommand: buildCommand ?? .build(style: .buildOnly, skipDependencies: false), diagnostics: diagnostics, indexingInfo: indexingInfo, fs: fs, bypassActualTasks: bypassActualTasks, moduleSessionFilePath: moduleSessionFilePath, invalidationPaths: invalidationPaths, recursiveSearchPathResults: recursiveSearchPathResults, copiedPathMap: copiedPathMap, rootPathsPerTarget: rootPathsPerTarget, moduleCachePathsPerTarget: moduleCachePathsPerTarget, staleFileRemovalIdentifierPerTarget: staleFileRemovalIdentifierPerTarget, settingsPerTarget: settingsPerTarget, delegate: MockTestBuildDescriptionConstructionDelegate(), targetDependencies: targetDependencies, definingTargetsByModuleName: definingTargetsByModuleName, capturedBuildInfo: nil, userPreferences: .defaultForTesting) + static func construct(workspace: Workspace, tasks: [any PlannedTask], path: Path, signature: BuildDescriptionSignature, buildCommand: BuildCommand? = nil, diagnostics: [ConfiguredTarget?: [Diagnostic]] = [:], indexingInfo: [(forTarget: ConfiguredTarget?, path: Path, indexingInfo: any SourceFileIndexingInfo)] = [], fs: any FSProxy = localFS, bypassActualTasks: Bool = false, moduleSessionFilePath: Path? = nil, invalidationPaths: [Path] = [], recursiveSearchPathResults: [RecursiveSearchPathResolver.CachedResult] = [], copiedPathMap: [String: String] = [:], rootPathsPerTarget: [ConfiguredTarget:[Path]] = [:], moduleCachePathsPerTarget: [ConfiguredTarget: [Path]] = [:], artifactInfoPerTarget: [ConfiguredTarget: ArtifactInfo] = [:], staleFileRemovalIdentifierPerTarget: [ConfiguredTarget: String] = [:], settingsPerTarget: [ConfiguredTarget: Settings] = [:], targetDependencies: [TargetDependencyRelationship] = [], definingTargetsByModuleName: [String: OrderedSet] = [:]) async throws -> BuildDescriptionDiagnosticResults? { + let buildDescription = try await construct(workspace: workspace, tasks: tasks, path: path, signature: signature, buildCommand: buildCommand ?? .build(style: .buildOnly, skipDependencies: false), diagnostics: diagnostics, indexingInfo: indexingInfo, fs: fs, bypassActualTasks: bypassActualTasks, moduleSessionFilePath: moduleSessionFilePath, invalidationPaths: invalidationPaths, recursiveSearchPathResults: recursiveSearchPathResults, copiedPathMap: copiedPathMap, rootPathsPerTarget: rootPathsPerTarget, moduleCachePathsPerTarget: moduleCachePathsPerTarget, artifactInfoPerTarget: artifactInfoPerTarget, staleFileRemovalIdentifierPerTarget: staleFileRemovalIdentifierPerTarget, settingsPerTarget: settingsPerTarget, delegate: MockTestBuildDescriptionConstructionDelegate(), targetDependencies: targetDependencies, definingTargetsByModuleName: definingTargetsByModuleName, capturedBuildInfo: nil, userPreferences: .defaultForTesting) return buildDescription.map { BuildDescriptionDiagnosticResults(buildDescription: $0, workspace: workspace) } ?? nil } } diff --git a/Tests/SwiftBuildTests/InspectBuildDescriptionTests.swift b/Tests/SwiftBuildTests/InspectBuildDescriptionTests.swift index de98291d..ab205ff5 100644 --- a/Tests/SwiftBuildTests/InspectBuildDescriptionTests.swift +++ b/Tests/SwiftBuildTests/InspectBuildDescriptionTests.swift @@ -74,6 +74,100 @@ fileprivate struct InspectBuildDescriptionTests { } } + @Test(.requireSDKs(.macOS)) + func artifacts() 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, + buildConfigurations: [ + .init("Debug", buildSettings: [ + "PRODUCT_NAME": "$(TARGET_NAME)" + ]) + ], + buildPhases: [TestSourcesBuildPhase([TestBuildFile("Foo.swift")])], + ) + + let staticLibraryTarget = TestStandardTarget( + "MyStaticLibrary", + type: .staticLibrary, + buildConfigurations: [ + .init("Debug", buildSettings: [ + "PRODUCT_NAME": "$(TARGET_NAME)" + ]) + ], + buildPhases: [TestSourcesBuildPhase([TestBuildFile("Foo.swift")])], + ) + + let dynamicLibraryTarget = TestStandardTarget( + "MyDynamicLibrary", + type: .dynamicLibrary, + buildConfigurations: [ + .init("Debug", buildSettings: [ + "PRODUCT_NAME": "$(TARGET_NAME)" + ]) + ], + buildPhases: [TestSourcesBuildPhase([TestBuildFile("Foo.swift")])], + ) + + let executableTarget = TestStandardTarget( + "MyExecutable", + type: .commandLineTool, + buildConfigurations: [ + .init("Debug", buildSettings: [ + "PRODUCT_NAME": "$(TARGET_NAME)" + ]) + ], + buildPhases: [TestSourcesBuildPhase([TestBuildFile("Foo.swift")])], + ) + + let project = TestProject( + "Test", + groupTree: TestGroup("Test", children: [TestFile("Foo.swift")]), + targets: [frameworkTarget, staticLibraryTarget, dynamicLibraryTarget, executableTarget] + ) + + 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: frameworkTarget.guid, parameters: buildParameters)) + request.add(target: SWBConfiguredTarget(guid: staticLibraryTarget.guid, parameters: buildParameters)) + request.add(target: SWBConfiguredTarget(guid: dynamicLibraryTarget.guid, parameters: buildParameters)) + request.add(target: SWBConfiguredTarget(guid: executableTarget.guid, parameters: buildParameters)) + + let buildDescriptionID = try await testSession.session.createBuildDescription(buildRequest: request) + let targetInfos = try await testSession.session.configuredTargets(buildDescription: buildDescriptionID, buildRequest: request) + + let frameworkTargetInfo = try #require(targetInfos.filter { $0.name == "MyFramework" }.only) + #expect(frameworkTargetInfo.artifactInfo?.kind == .framework) + #expect(frameworkTargetInfo.artifactInfo?.path.hasSuffix("MyFramework.framework") == true) + + let staticLibraryTargetInfo = try #require(targetInfos.filter { $0.name == "MyStaticLibrary" }.only) + #expect(staticLibraryTargetInfo.artifactInfo?.kind == .staticLibrary) + #expect(staticLibraryTargetInfo.artifactInfo?.path.hasSuffix("MyStaticLibrary.a") == true) + + let dynamicLibraryTargetInfo = try #require(targetInfos.filter { $0.name == "MyDynamicLibrary" }.only) + #expect(dynamicLibraryTargetInfo.artifactInfo?.kind == .dynamicLibrary) + #expect(dynamicLibraryTargetInfo.artifactInfo?.path.hasSuffix("MyDynamicLibrary.dylib") == true) + + let executableTargetInfo = try #require(targetInfos.filter { $0.name == "MyExecutable" }.only) + #expect(executableTargetInfo.artifactInfo?.kind == .executable) + #expect(executableTargetInfo.artifactInfo?.path.hasSuffix("MyExecutable") == true) + } + } + } + @Test(.requireSDKs(.macOS)) func configuredTargetSources() async throws { try await withTemporaryDirectory { (temporaryDirectory: NamedTemporaryDirectory) in