From 235dd3e7b81249c500840a4a35b62d0dd4c16ff5 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Wed, 5 Nov 2025 15:17:27 -0500 Subject: [PATCH 01/24] Forray into capturing swift compiler output logs * Built tentative test class SwiftBuildSystemOutputParser to handle the compiler output specifically * Added a handleDiagnostic method to possibly substitute the emitEvent local scope implementation of handling a SwiftBuildMessage diagnostic --- .../SwiftBuildSupport/SwiftBuildSystem.swift | 229 +++++++++++++++++- 1 file changed, 227 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index a0d710c29bc..576b391c1b8 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -30,9 +30,19 @@ import protocol TSCBasic.OutputByteStream import func TSCBasic.withTemporaryFile import enum TSCUtility.Diagnostics +import class TSCUtility.JSONMessageStreamingParser +import protocol TSCUtility.JSONMessageStreamingParserDelegate +import struct TSCBasic.RegEx import var TSCBasic.stdoutStream +import class Build.SwiftCompilerOutputParser +import protocol Build.SwiftCompilerOutputParserDelegate +import struct Build.SwiftCompilerMessage + +// TODO bp +import class SWBCore.SwiftCommandOutputParser + import Foundation import SWBBuildService import SwiftBuild @@ -174,6 +184,176 @@ private final class PlanningOperationDelegate: SWBPlanningOperationDelegate, Sen } } +private final class SWBOutputDelegate: SPMBuildCore.BuildSystemDelegate { + func buildSystem(_ buildSystem: BuildSystem, willStartCommand command: BuildSystemCommand) { + print("SWBDELEGATE will start command") + } + func buildSystem(_ buildSystem: BuildSystem, didStartCommand command: BuildSystemCommand) { + print("SWBDELEGATE did start command") + + } + func buildSystem(_ buildSystem: BuildSystem, didUpdateTaskProgress text: String) { + print("SWBDELEGATE did Update Task Progress") + + } + func buildSystem(_ buildSystem: BuildSystem, didFinishCommand command: BuildSystemCommand) { + print("SWBDELEGATE did finish command") + + } + func buildSystemDidDetectCycleInRules(_ buildSystem: BuildSystem) { + print("SWBDELEGATE detect cycle") + + } + func buildSystem(_ buildSystem: BuildSystem, didFinishWithResult success: Bool) { + print("SWBDELEGATE did finish with result: \(success)") + } + func buildSystemDidCancel(_ buildSystem: BuildSystem) { + print("SWBDELEGATE did cancel") + } + +} + +extension SwiftCompilerMessage { + fileprivate var verboseProgressText: String? { + switch kind { + case .began(let info): + ([info.commandExecutable] + info.commandArguments).joined(separator: " ") + case .skipped, .finished, .abnormal, .signalled, .unparsableOutput: + nil + } + } + + fileprivate var standardOutput: String? { + switch kind { + case .finished(let info), + .abnormal(let info), + .signalled(let info): + info.output + case .unparsableOutput(let output): + output + case .skipped, .began: + nil + } + } +} + + +extension SwiftBuildSystemOutputParser: SwiftCompilerOutputParserDelegate { + func swiftCompilerOutputParser(_ parser: Build.SwiftCompilerOutputParser, didParse message: Build.SwiftCompilerMessage) { + // TODO bp + if self.logLevel.isVerbose { + if let text = message.verboseProgressText { + self.outputStream.send("\(text)\n") + self.outputStream.flush() + } + } else if !self.logLevel.isQuiet { +// self.taskTracker.swiftCompilerDidOutputMessage(message, targetName: parser.targetName) +// self.updateProgress() + } + + if let output = message.standardOutput { + // first we want to print the output so users have it handy + if !self.logLevel.isVerbose { +// self.progressAnimation.clear() + } + + self.outputStream.send(output) + self.outputStream.flush() + + // next we want to try and scoop out any errors from the output (if reasonable size, otherwise this + // will be very slow), so they can later be passed to the advice provider in case of failure. + if output.utf8.count < 1024 * 10 { + let regex = try! RegEx(pattern: #".*(error:[^\n]*)\n.*"#, options: .dotMatchesLineSeparators) + for match in regex.matchGroups(in: output) { + self.errorMessagesByTarget[parser.targetName] = ( + self.errorMessagesByTarget[parser.targetName] ?? [] + ) + [match[0]] + } + } + } + + } + + func swiftCompilerOutputParser(_ parser: Build.SwiftCompilerOutputParser, didFailWith error: any Error) { + // TODO bp + print("failed parsing with error: \(error.localizedDescription)") + } +} + +/// Parser for SwiftBuild output. +final class SwiftBuildSystemOutputParser { +// typealias Message = SwiftBuildMessage + private var buildSystem: SPMBuildCore.BuildSystem +// private var parser: SwiftCompilerOutputParser? + private let observabilityScope: ObservabilityScope + private let outputStream: OutputByteStream +// private let progressAnimation: ProgressAnimationProtocol + private let logLevel: Basics.Diagnostic.Severity + private var swiftOutputParser: SwiftCompilerOutputParser? + private var errorMessagesByTarget: [String: [String]] = [:] + + public init( + buildSystem: SPMBuildCore.BuildSystem, + observabilityScope: ObservabilityScope, + outputStream: OutputByteStream, + logLevel: Basics.Diagnostic.Severity, +// progressAnimation: ProgressAnimationProtocol +// swiftOutputParser: SwiftCompilerOutputParser? + ) + { + self.buildSystem = buildSystem + self.observabilityScope = observabilityScope + self.outputStream = outputStream + self.logLevel = logLevel +// self.progressAnimation = progressAnimation +// self.swiftOutputParser = swiftOutputParser + self.swiftOutputParser = nil + let outputParser: SwiftCompilerOutputParser? = { [weak self] in + guard let self else { return nil } + return .init(targetName: "", delegate: self) + }() + + self.swiftOutputParser = outputParser + } + + func parse(bytes data: Data) { + swiftOutputParser?.parse(bytes: data) + } + + func handleDiagnostic(_ diagnostic: SwiftBuildMessage.DiagnosticInfo) { + let fixItsDescription = if diagnostic.fixIts.hasContent { + ": " + diagnostic.fixIts.map { String(describing: $0) }.joined(separator: ", ") + } else { + "" + } + let message = if let locationDescription = diagnostic.location.userDescription { + "\(locationDescription) \(diagnostic.message)\(fixItsDescription)" + } else { + "\(diagnostic.message)\(fixItsDescription)" + } + let severity: Diagnostic.Severity = switch diagnostic.kind { + case .error: .error + case .warning: .warning + case .note: .info + case .remark: .debug + } + self.observabilityScope.emit(severity: severity, message: "\(message)\n") + + for childDiagnostic in diagnostic.childDiagnostics { + handleDiagnostic(childDiagnostic) + } + } + +// func swiftCompilerOutputParser(_ parser: Build.SwiftCompilerOutputParser, didParse message: Build.SwiftCompilerMessage) { +// print("Attempting to parse output:") +// print(parser.targetName) +// } +// +// func swiftCompilerOutputParser(_ parser: Build.SwiftCompilerOutputParser, didFailWith error: any Error) { +// print("Parser encountered error: \(error.localizedDescription)") +// } +} + public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { private let buildParameters: BuildParameters private let packageGraphLoader: () async throws -> ModulesGraph @@ -190,6 +370,9 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { /// The delegate used by the build system. public weak var delegate: SPMBuildCore.BuildSystemDelegate? + /// A build message delegate to capture diagnostic output captured from the compiler. + private var buildMessageParser: SwiftBuildSystemOutputParser? + /// Configuration for building and invoking plugins. private let pluginConfiguration: PluginConfiguration @@ -254,7 +437,13 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { self.fileSystem = fileSystem self.observabilityScope = observabilityScope.makeChildScope(description: "Swift Build System") self.pluginConfiguration = pluginConfiguration - self.delegate = delegate + self.delegate = delegate //?? SWBOutputDelegate() + self.buildMessageParser = .init( + buildSystem: self, + observabilityScope: observabilityScope, + outputStream: outputStream, + logLevel: logLevel + ) } private func createREPLArguments( @@ -532,6 +721,17 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { } } + // TODO bp create swiftcompileroutputparser here? +// self.buildMessageParser = .init( +// buildSystem: self, +// observabilityScope: self.observabilityScope, +// outputStream: self.outputStream, +// logLevel: self.logLevel +// // progressAnimation: progressAnimation +// ) +// let parser = SwiftCompilerOutputParser(targetName: pifTargetName, delegate: self.delegate) + // TODO bp: see BuildOperation, and where LLBuildTracker is created and used. + var replArguments: CLIArguments? var artifacts: [(String, PluginInvocationBuildResult.BuiltArtifact)]? return try await withService(connectionMode: .inProcessStatic(swiftbuildServiceEntryPoint)) { service in @@ -580,6 +780,19 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { throw error } +// for target in configuredTargets { +// let outputParser = SwiftBuildSystemOutputParser( +// buildSystem: self, +// observabilityScope:self.observabilityScope, +// outputStream: self.outputStream, +// logLevel: self.logLevel, +// progressAnimation: progressAnimation, +// swiftOutputParser: .init( +// targetName: target, +// delegate: self) +// ) +// } + let request = try await self.makeBuildRequest(session: session, configuredTargets: configuredTargets, derivedDataPath: derivedDataPath, symbolGraphOptions: symbolGraphOptions) struct BuildState { @@ -665,6 +878,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { emitInfoAsDiagnostic(info: info) case .output(let info): +// let parsedOutputText = self.parseOutput(info) self.observabilityScope.emit(info: "\(String(decoding: info.data, as: UTF8.self))") case .taskStarted(let info): try buildState.started(task: info) @@ -712,7 +926,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { let operation = try await session.createBuildOperation( request: request, - delegate: PlanningOperationDelegate(), + delegate: PlanningOperationDelegate(), // TODO bp possibly enhance this delegate? retainBuildDescription: true ) @@ -813,6 +1027,16 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { } } + private func parseOutput(_ info: SwiftBuildMessage.OutputInfo) -> String { + var result = "" + let data = info.data + // let parser = SwiftCompilerOutputParser() + // use json parser to parse bytes +// self.buildMessageParser?.parse(bytes: data) + + return result + } + private func makeRunDestination() -> SwiftBuild.SWBRunDestinationInfo { let platformName: String let sdkName: String @@ -954,6 +1178,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { + buildParameters.flags.swiftCompilerFlags.map { $0.shellEscaped() } ).joined(separator: " ") + // TODO bp passing -v flag via verboseFlag here; investigate linker messages settings["OTHER_LDFLAGS"] = ( verboseFlag + // clang will be invoked to link so the verbose flag is valid for it ["$(inherited)"] From f9dfdf39bf2713aa5ab08784d0f81ff50986a200 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Tue, 11 Nov 2025 15:35:53 -0500 Subject: [PATCH 02/24] Revert usage of JSON parser; selectively emit DiagnosticInfo * the flag `appendToOutputStream` helps us to determine whether a diagnostic is to be emitted or whether we'll be emitting the compiler output via OutputInfo * separate the emitEvent method into the SwiftBuildSystemMessageHandler --- .../SwiftBuildSupport/SwiftBuildSystem.swift | 594 +++++++++--------- 1 file changed, 291 insertions(+), 303 deletions(-) diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index 576b391c1b8..8fd3804f0f0 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -184,154 +184,83 @@ private final class PlanningOperationDelegate: SWBPlanningOperationDelegate, Sen } } -private final class SWBOutputDelegate: SPMBuildCore.BuildSystemDelegate { - func buildSystem(_ buildSystem: BuildSystem, willStartCommand command: BuildSystemCommand) { - print("SWBDELEGATE will start command") - } - func buildSystem(_ buildSystem: BuildSystem, didStartCommand command: BuildSystemCommand) { - print("SWBDELEGATE did start command") - - } - func buildSystem(_ buildSystem: BuildSystem, didUpdateTaskProgress text: String) { - print("SWBDELEGATE did Update Task Progress") - - } - func buildSystem(_ buildSystem: BuildSystem, didFinishCommand command: BuildSystemCommand) { - print("SWBDELEGATE did finish command") - - } - func buildSystemDidDetectCycleInRules(_ buildSystem: BuildSystem) { - print("SWBDELEGATE detect cycle") - - } - func buildSystem(_ buildSystem: BuildSystem, didFinishWithResult success: Bool) { - print("SWBDELEGATE did finish with result: \(success)") - } - func buildSystemDidCancel(_ buildSystem: BuildSystem) { - print("SWBDELEGATE did cancel") - } - -} - -extension SwiftCompilerMessage { - fileprivate var verboseProgressText: String? { - switch kind { - case .began(let info): - ([info.commandExecutable] + info.commandArguments).joined(separator: " ") - case .skipped, .finished, .abnormal, .signalled, .unparsableOutput: - nil - } - } - - fileprivate var standardOutput: String? { - switch kind { - case .finished(let info), - .abnormal(let info), - .signalled(let info): - info.output - case .unparsableOutput(let output): - output - case .skipped, .began: - nil - } - } -} - - -extension SwiftBuildSystemOutputParser: SwiftCompilerOutputParserDelegate { - func swiftCompilerOutputParser(_ parser: Build.SwiftCompilerOutputParser, didParse message: Build.SwiftCompilerMessage) { - // TODO bp - if self.logLevel.isVerbose { - if let text = message.verboseProgressText { - self.outputStream.send("\(text)\n") - self.outputStream.flush() - } - } else if !self.logLevel.isQuiet { -// self.taskTracker.swiftCompilerDidOutputMessage(message, targetName: parser.targetName) -// self.updateProgress() - } - - if let output = message.standardOutput { - // first we want to print the output so users have it handy - if !self.logLevel.isVerbose { -// self.progressAnimation.clear() - } - - self.outputStream.send(output) - self.outputStream.flush() - - // next we want to try and scoop out any errors from the output (if reasonable size, otherwise this - // will be very slow), so they can later be passed to the advice provider in case of failure. - if output.utf8.count < 1024 * 10 { - let regex = try! RegEx(pattern: #".*(error:[^\n]*)\n.*"#, options: .dotMatchesLineSeparators) - for match in regex.matchGroups(in: output) { - self.errorMessagesByTarget[parser.targetName] = ( - self.errorMessagesByTarget[parser.targetName] ?? [] - ) + [match[0]] - } - } - } - - } - - func swiftCompilerOutputParser(_ parser: Build.SwiftCompilerOutputParser, didFailWith error: any Error) { - // TODO bp - print("failed parsing with error: \(error.localizedDescription)") - } -} - -/// Parser for SwiftBuild output. -final class SwiftBuildSystemOutputParser { -// typealias Message = SwiftBuildMessage +/// Handler for SwiftBuildMessage events sent by the active SWBService. +final class SwiftBuildSystemMessageHandler { private var buildSystem: SPMBuildCore.BuildSystem -// private var parser: SwiftCompilerOutputParser? private let observabilityScope: ObservabilityScope private let outputStream: OutputByteStream -// private let progressAnimation: ProgressAnimationProtocol private let logLevel: Basics.Diagnostic.Severity - private var swiftOutputParser: SwiftCompilerOutputParser? - private var errorMessagesByTarget: [String: [String]] = [:] + private var buildState: BuildState = .init() + private var delegate: SPMBuildCore.BuildSystemDelegate? + + let progressAnimation: ProgressAnimationProtocol + var serializedDiagnosticPathsByTargetName: [String: [Basics.AbsolutePath]] = [:] public init( buildSystem: SPMBuildCore.BuildSystem, observabilityScope: ObservabilityScope, outputStream: OutputByteStream, - logLevel: Basics.Diagnostic.Severity, -// progressAnimation: ProgressAnimationProtocol -// swiftOutputParser: SwiftCompilerOutputParser? + logLevel: Basics.Diagnostic.Severity ) { self.buildSystem = buildSystem self.observabilityScope = observabilityScope self.outputStream = outputStream self.logLevel = logLevel -// self.progressAnimation = progressAnimation -// self.swiftOutputParser = swiftOutputParser - self.swiftOutputParser = nil - let outputParser: SwiftCompilerOutputParser? = { [weak self] in - guard let self else { return nil } - return .init(targetName: "", delegate: self) - }() - - self.swiftOutputParser = outputParser + self.progressAnimation = ProgressAnimation.ninja( + stream: self.outputStream, + verbose: self.logLevel.isVerbose + ) } - func parse(bytes data: Data) { - swiftOutputParser?.parse(bytes: data) + struct BuildState { + private var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:] + private var activeTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskStartedInfo] = [:] + + mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws { + if activeTasks[task.taskID] != nil { + throw Diagnostics.fatalError + } + activeTasks[task.taskID] = task + } + + mutating func completed(task: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo) throws -> SwiftBuild.SwiftBuildMessage.TaskStartedInfo { + guard let task = activeTasks[task.taskID] else { + throw Diagnostics.fatalError + } + return task + } + + mutating func started(target: SwiftBuild.SwiftBuildMessage.TargetStartedInfo) throws { + if targetsByID[target.targetID] != nil { + throw Diagnostics.fatalError + } + targetsByID[target.targetID] = target + } + + mutating func target(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws -> SwiftBuild.SwiftBuildMessage.TargetStartedInfo? { + guard let id = task.targetID else { + return nil + } + guard let target = targetsByID[id] else { + throw Diagnostics.fatalError + } + return target + } } - func handleDiagnostic(_ diagnostic: SwiftBuildMessage.DiagnosticInfo) { - let fixItsDescription = if diagnostic.fixIts.hasContent { - ": " + diagnostic.fixIts.map { String(describing: $0) }.joined(separator: ", ") + private func emitInfoAsDiagnostic(info: SwiftBuildMessage.DiagnosticInfo) { + let fixItsDescription = if info.fixIts.hasContent { + ": " + info.fixIts.map { String(describing: $0) }.joined(separator: ", ") } else { "" } - let message = if let locationDescription = diagnostic.location.userDescription { - "\(locationDescription) \(diagnostic.message)\(fixItsDescription)" + let message = if let locationDescription = info.location.userDescription { + "\(locationDescription) \(info.message)\(fixItsDescription)" } else { - "\(diagnostic.message)\(fixItsDescription)" + "\(info.message)\(fixItsDescription)" } - let severity: Diagnostic.Severity = switch diagnostic.kind { + let severity: Diagnostic.Severity = switch info.kind { case .error: .error case .warning: .warning case .note: .info @@ -339,19 +268,88 @@ final class SwiftBuildSystemOutputParser { } self.observabilityScope.emit(severity: severity, message: "\(message)\n") - for childDiagnostic in diagnostic.childDiagnostics { - handleDiagnostic(childDiagnostic) + for childDiagnostic in info.childDiagnostics { + emitInfoAsDiagnostic(info: childDiagnostic) } } -// func swiftCompilerOutputParser(_ parser: Build.SwiftCompilerOutputParser, didParse message: Build.SwiftCompilerMessage) { -// print("Attempting to parse output:") -// print(parser.targetName) -// } -// -// func swiftCompilerOutputParser(_ parser: Build.SwiftCompilerOutputParser, didFailWith error: any Error) { -// print("Parser encountered error: \(error.localizedDescription)") -// } + func emitEvent( + _ message: SwiftBuild.SwiftBuildMessage + ) throws { + guard !self.logLevel.isQuiet else { return } + switch message { + case .buildCompleted(let info): + progressAnimation.complete(success: info.result == .ok) + if info.result == .cancelled { + self.delegate?.buildSystemDidCancel(self.buildSystem) + } else { + self.delegate?.buildSystem(self.buildSystem, didFinishWithResult: info.result == .ok) + } + case .didUpdateProgress(let progressInfo): + var step = Int(progressInfo.percentComplete) + if step < 0 { step = 0 } + let message = if let targetName = progressInfo.targetName { + "\(targetName) \(progressInfo.message)" + } else { + "\(progressInfo.message)" + } + progressAnimation.update(step: step, total: 100, text: message) + self.delegate?.buildSystem(self.buildSystem, didUpdateTaskProgress: message) + case .diagnostic(let info): + if info.appendToOutputStream { + emitInfoAsDiagnostic(info: info) + } + case .output(let info): + let parsedOutput = String(decoding: info.data, as: UTF8.self) + if parsedOutput.contains("error: ") { + self.observabilityScope.emit(severity: .error, message: parsedOutput) + } else { + self.observabilityScope.emit(info: parsedOutput) + } + // Parse the output to extract diagnostics with code snippets + case .taskStarted(let info): + try buildState.started(task: info) + + if let commandLineDisplay = info.commandLineDisplayString { + self.observabilityScope.emit(info: "\(info.executionDescription)\n\(commandLineDisplay)") + } else { + self.observabilityScope.emit(info: "\(info.executionDescription)") + } + + if self.logLevel.isVerbose { + if let commandLineDisplay = info.commandLineDisplayString { + self.outputStream.send("\(info.executionDescription)\n\(commandLineDisplay)") + } else { + self.outputStream.send("\(info.executionDescription)") + } + } + let targetInfo = try buildState.target(for: info) + self.delegate?.buildSystem(self.buildSystem, willStartCommand: BuildSystemCommand(info, targetInfo: targetInfo)) + self.delegate?.buildSystem(self.buildSystem, didStartCommand: BuildSystemCommand(info, targetInfo: targetInfo)) + case .taskComplete(let info): + let startedInfo = try buildState.completed(task: info) + if info.result != .success { + self.observabilityScope.emit(severity: .error, message: "\(startedInfo.ruleInfo) failed with a nonzero exit code. Command line: \(startedInfo.commandLineDisplayString ?? "")") + } + let targetInfo = try buildState.target(for: startedInfo) + self.delegate?.buildSystem(self.buildSystem, didFinishCommand: BuildSystemCommand(startedInfo, targetInfo: targetInfo)) + if let targetName = targetInfo?.targetName { + serializedDiagnosticPathsByTargetName[targetName, default: []].append(contentsOf: startedInfo.serializedDiagnosticsPaths.compactMap { + try? Basics.AbsolutePath(validating: $0.pathString) + }) + } + case .targetStarted(let info): + try buildState.started(target: info) + case .planningOperationStarted, .planningOperationCompleted, .reportBuildDescription, .reportPathMap, .preparedForIndex, .backtraceFrame, .buildStarted, .preparationComplete, .targetUpToDate, .targetComplete, .taskUpToDate: + break + case .buildDiagnostic, .targetDiagnostic, .taskDiagnostic: + break // deprecated + case .buildOutput, .targetOutput, .taskOutput: + break // deprecated + @unknown default: + break + } + } } public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { @@ -370,9 +368,6 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { /// The delegate used by the build system. public weak var delegate: SPMBuildCore.BuildSystemDelegate? - /// A build message delegate to capture diagnostic output captured from the compiler. - private var buildMessageParser: SwiftBuildSystemOutputParser? - /// Configuration for building and invoking plugins. private let pluginConfiguration: PluginConfiguration @@ -437,13 +432,13 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { self.fileSystem = fileSystem self.observabilityScope = observabilityScope.makeChildScope(description: "Swift Build System") self.pluginConfiguration = pluginConfiguration - self.delegate = delegate //?? SWBOutputDelegate() - self.buildMessageParser = .init( - buildSystem: self, - observabilityScope: observabilityScope, - outputStream: outputStream, - logLevel: logLevel - ) + self.delegate = delegate +// self.buildMessageParser = .init( +// buildSystem: self, +// observabilityScope: observabilityScope, +// outputStream: outputStream, +// logLevel: logLevel +// ) } private func createREPLArguments( @@ -721,28 +716,19 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { } } - // TODO bp create swiftcompileroutputparser here? -// self.buildMessageParser = .init( -// buildSystem: self, -// observabilityScope: self.observabilityScope, -// outputStream: self.outputStream, -// logLevel: self.logLevel -// // progressAnimation: progressAnimation -// ) -// let parser = SwiftCompilerOutputParser(targetName: pifTargetName, delegate: self.delegate) - // TODO bp: see BuildOperation, and where LLBuildTracker is created and used. - var replArguments: CLIArguments? var artifacts: [(String, PluginInvocationBuildResult.BuiltArtifact)]? return try await withService(connectionMode: .inProcessStatic(swiftbuildServiceEntryPoint)) { service in let derivedDataPath = self.buildParameters.dataPath - let progressAnimation = ProgressAnimation.ninja( - stream: self.outputStream, - verbose: self.logLevel.isVerbose + let buildMessageHandler = SwiftBuildSystemMessageHandler( + buildSystem: self, + observabilityScope: self.observabilityScope, + outputStream: self.outputStream, + logLevel: self.logLevel ) - var serializedDiagnosticPathsByTargetName: [String: [Basics.AbsolutePath]] = [:] +// var serializedDiagnosticPathsByTargetName: [String: [Basics.AbsolutePath]] = [:] do { try await withSession(service: service, name: self.buildParameters.pifManifest.pathString, toolchainPath: self.buildParameters.toolchain.toolchainDir, packageManagerResourcesDirectory: self.packageManagerResourcesDirectory) { session, _ in self.outputStream.send("Building for \(self.buildParameters.configuration == .debug ? "debugging" : "production")...\n") @@ -795,143 +781,155 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { let request = try await self.makeBuildRequest(session: session, configuredTargets: configuredTargets, derivedDataPath: derivedDataPath, symbolGraphOptions: symbolGraphOptions) - struct BuildState { - private var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:] - private var activeTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskStartedInfo] = [:] - - mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws { - if activeTasks[task.taskID] != nil { - throw Diagnostics.fatalError - } - activeTasks[task.taskID] = task - } - - mutating func completed(task: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo) throws -> SwiftBuild.SwiftBuildMessage.TaskStartedInfo { - guard let task = activeTasks[task.taskID] else { - throw Diagnostics.fatalError - } - return task - } - - mutating func started(target: SwiftBuild.SwiftBuildMessage.TargetStartedInfo) throws { - if targetsByID[target.targetID] != nil { - throw Diagnostics.fatalError - } - targetsByID[target.targetID] = target - } - - mutating func target(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws -> SwiftBuild.SwiftBuildMessage.TargetStartedInfo? { - guard let id = task.targetID else { - return nil - } - guard let target = targetsByID[id] else { - throw Diagnostics.fatalError - } - return target - } - } - - func emitEvent(_ message: SwiftBuild.SwiftBuildMessage, buildState: inout BuildState) throws { - guard !self.logLevel.isQuiet else { return } - switch message { - case .buildCompleted(let info): - progressAnimation.complete(success: info.result == .ok) - if info.result == .cancelled { - self.delegate?.buildSystemDidCancel(self) - } else { - self.delegate?.buildSystem(self, didFinishWithResult: info.result == .ok) - } - case .didUpdateProgress(let progressInfo): - var step = Int(progressInfo.percentComplete) - if step < 0 { step = 0 } - let message = if let targetName = progressInfo.targetName { - "\(targetName) \(progressInfo.message)" - } else { - "\(progressInfo.message)" - } - progressAnimation.update(step: step, total: 100, text: message) - self.delegate?.buildSystem(self, didUpdateTaskProgress: message) - case .diagnostic(let info): - func emitInfoAsDiagnostic(info: SwiftBuildMessage.DiagnosticInfo) { - let fixItsDescription = if info.fixIts.hasContent { - ": " + info.fixIts.map { String(describing: $0) }.joined(separator: ", ") - } else { - "" - } - let message = if let locationDescription = info.location.userDescription { - "\(locationDescription) \(info.message)\(fixItsDescription)" - } else { - "\(info.message)\(fixItsDescription)" - } - let severity: Diagnostic.Severity = switch info.kind { - case .error: .error - case .warning: .warning - case .note: .info - case .remark: .debug - } - self.observabilityScope.emit(severity: severity, message: "\(message)\n") - - for childDiagnostic in info.childDiagnostics { - emitInfoAsDiagnostic(info: childDiagnostic) - } - } - - emitInfoAsDiagnostic(info: info) - case .output(let info): -// let parsedOutputText = self.parseOutput(info) - self.observabilityScope.emit(info: "\(String(decoding: info.data, as: UTF8.self))") - case .taskStarted(let info): - try buildState.started(task: info) - - if let commandLineDisplay = info.commandLineDisplayString { - self.observabilityScope.emit(info: "\(info.executionDescription)\n\(commandLineDisplay)") - } else { - self.observabilityScope.emit(info: "\(info.executionDescription)") - } - - if self.logLevel.isVerbose { - if let commandLineDisplay = info.commandLineDisplayString { - self.outputStream.send("\(info.executionDescription)\n\(commandLineDisplay)") - } else { - self.outputStream.send("\(info.executionDescription)") - } - } - let targetInfo = try buildState.target(for: info) - self.delegate?.buildSystem(self, willStartCommand: BuildSystemCommand(info, targetInfo: targetInfo)) - self.delegate?.buildSystem(self, didStartCommand: BuildSystemCommand(info, targetInfo: targetInfo)) - case .taskComplete(let info): - let startedInfo = try buildState.completed(task: info) - if info.result != .success { - self.observabilityScope.emit(severity: .error, message: "\(startedInfo.ruleInfo) failed with a nonzero exit code. Command line: \(startedInfo.commandLineDisplayString ?? "")") - } - let targetInfo = try buildState.target(for: startedInfo) - self.delegate?.buildSystem(self, didFinishCommand: BuildSystemCommand(startedInfo, targetInfo: targetInfo)) - if let targetName = targetInfo?.targetName { - serializedDiagnosticPathsByTargetName[targetName, default: []].append(contentsOf: startedInfo.serializedDiagnosticsPaths.compactMap { - try? Basics.AbsolutePath(validating: $0.pathString) - }) - } - case .targetStarted(let info): - try buildState.started(target: info) - case .planningOperationStarted, .planningOperationCompleted, .reportBuildDescription, .reportPathMap, .preparedForIndex, .backtraceFrame, .buildStarted, .preparationComplete, .targetUpToDate, .targetComplete, .taskUpToDate: - break - case .buildDiagnostic, .targetDiagnostic, .taskDiagnostic: - break // deprecated - case .buildOutput, .targetOutput, .taskOutput: - break // deprecated - @unknown default: - break - } - } +// struct BuildState { +// private var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:] +// private var activeTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskStartedInfo] = [:] +// +// mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws { +// if activeTasks[task.taskID] != nil { +// throw Diagnostics.fatalError +// } +// activeTasks[task.taskID] = task +// } +// +// mutating func completed(task: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo) throws -> SwiftBuild.SwiftBuildMessage.TaskStartedInfo { +// guard let task = activeTasks[task.taskID] else { +// throw Diagnostics.fatalError +// } +// return task +// } +// +// mutating func started(target: SwiftBuild.SwiftBuildMessage.TargetStartedInfo) throws { +// if targetsByID[target.targetID] != nil { +// throw Diagnostics.fatalError +// } +// targetsByID[target.targetID] = target +// } +// +// mutating func target(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws -> SwiftBuild.SwiftBuildMessage.TargetStartedInfo? { +// guard let id = task.targetID else { +// return nil +// } +// guard let target = targetsByID[id] else { +// throw Diagnostics.fatalError +// } +// return target +// } +// } +// +// func emitEvent(_ message: SwiftBuild.SwiftBuildMessage, buildState: inout BuildState) throws { +// guard !self.logLevel.isQuiet else { return } +// switch message { +// case .buildCompleted(let info): +// progressAnimation.complete(success: info.result == .ok) +// if info.result == .cancelled { +// self.delegate?.buildSystemDidCancel(self) +// } else { +// self.delegate?.buildSystem(self, didFinishWithResult: info.result == .ok) +// } +// case .didUpdateProgress(let progressInfo): +// var step = Int(progressInfo.percentComplete) +// if step < 0 { step = 0 } +// let message = if let targetName = progressInfo.targetName { +// "\(targetName) \(progressInfo.message)" +// } else { +// "\(progressInfo.message)" +// } +// progressAnimation.update(step: step, total: 100, text: message) +// self.delegate?.buildSystem(self, didUpdateTaskProgress: message) +// case .diagnostic(let info): +// func emitInfoAsDiagnostic(info: SwiftBuildMessage.DiagnosticInfo) { +// let fixItsDescription = if info.fixIts.hasContent { +// ": " + info.fixIts.map { String(describing: $0) }.joined(separator: ", ") +// } else { +// "" +// } +// let message = if let locationDescription = info.location.userDescription { +// "\(locationDescription) \(info.message)\(fixItsDescription)" +// } else { +// "\(info.message)\(fixItsDescription)" +// } +// let severity: Diagnostic.Severity = switch info.kind { +// case .error: .error +// case .warning: .warning +// case .note: .info +// case .remark: .debug +// } +// self.observabilityScope.emit(severity: severity, message: "\(message)\n") +// +// for childDiagnostic in info.childDiagnostics { +// emitInfoAsDiagnostic(info: childDiagnostic) +// } +// } +// +// // If we've flagged this diagnostic to be appended to +// // the output stream, then do so. Otherwise, this +// // diagnostic message will be omitted from output as +// // it should have already been emitted as a +// // SwiftBuildMessage.OutputInfo. +// if info.appendToOutputStream { +// emitInfoAsDiagnostic(info: info) +// } +// +// case .output(let info): +// let parsedOutput = String(decoding: info.data, as: UTF8.self) +// // Error diagnostic message found! Emit. +// if parsedOutput.contains("error") { +// self.observabilityScope.emit(severity: .error, message: parsedOutput) +// } else { +// self.observabilityScope.emit(info: "\(String(decoding: info.data, as: UTF8.self))") +// } +// case .taskStarted(let info): +// try buildState.started(task: info) +// +// if let commandLineDisplay = info.commandLineDisplayString { +// self.observabilityScope.emit(info: "\(info.executionDescription)\n\(commandLineDisplay)") +// } else { +// self.observabilityScope.emit(info: "\(info.executionDescription)") +// } +// +// if self.logLevel.isVerbose { +// if let commandLineDisplay = info.commandLineDisplayString { +// self.outputStream.send("\(info.executionDescription)\n\(commandLineDisplay)") +// } else { +// self.outputStream.send("\(info.executionDescription)") +// } +// } +// let targetInfo = try buildState.target(for: info) +// self.delegate?.buildSystem(self, willStartCommand: BuildSystemCommand(info, targetInfo: targetInfo)) +// self.delegate?.buildSystem(self, didStartCommand: BuildSystemCommand(info, targetInfo: targetInfo)) +// case .taskComplete(let info): +// let startedInfo = try buildState.completed(task: info) +// if info.result != .success { +// self.observabilityScope.emit(severity: .error, message: "\(startedInfo.ruleInfo) failed with a nonzero exit code. Command line: \(startedInfo.commandLineDisplayString ?? "")") +// } +// let targetInfo = try buildState.target(for: startedInfo) +// self.delegate?.buildSystem(self, didFinishCommand: BuildSystemCommand(startedInfo, targetInfo: targetInfo)) +// if let targetName = targetInfo?.targetName { +// serializedDiagnosticPathsByTargetName[targetName, default: []].append(contentsOf: startedInfo.serializedDiagnosticsPaths.compactMap { +// try? Basics.AbsolutePath(validating: $0.pathString) +// }) +// } +// case .targetStarted(let info): +// try buildState.started(target: info) +// case .planningOperationStarted, .planningOperationCompleted, .reportBuildDescription, .reportPathMap, .preparedForIndex, .backtraceFrame, .buildStarted, .preparationComplete, .targetUpToDate, .targetComplete, .taskUpToDate: +// break +// case .buildDiagnostic, .targetDiagnostic, .taskDiagnostic: +// break // deprecated +// case .buildOutput, .targetOutput, .taskOutput: +// break // deprecated +// @unknown default: +// break +// } +// } let operation = try await session.createBuildOperation( request: request, - delegate: PlanningOperationDelegate(), // TODO bp possibly enhance this delegate? + delegate: PlanningOperationDelegate(), retainBuildDescription: true ) var buildDescriptionID: SWBBuildDescriptionID? = nil - var buildState = BuildState() for try await event in try await operation.start() { if case .reportBuildDescription(let info) = event { if buildDescriptionID != nil { @@ -939,7 +937,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { } buildDescriptionID = SWBBuildDescriptionID(info.buildDescriptionID) } - try emitEvent(event, buildState: &buildState) + try buildMessageHandler.emitEvent(event) } await operation.waitForCompletion() @@ -947,8 +945,8 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { switch operation.state { case .succeeded: guard !self.logLevel.isQuiet else { return } - progressAnimation.update(step: 100, total: 100, text: "") - progressAnimation.complete(success: true) + buildMessageHandler.progressAnimation.update(step: 100, total: 100, text: "") + buildMessageHandler.progressAnimation.complete(success: true) let duration = ContinuousClock.Instant.now - buildStartTime let formattedDuration = duration.formatted(.units(allowed: [.seconds], fractionalPart: .show(length: 2, rounded: .up))) self.outputStream.send("Build complete! (\(formattedDuration))\n") @@ -1015,7 +1013,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { } return BuildResult( - serializedDiagnosticPathsByTargetName: .success(serializedDiagnosticPathsByTargetName), + serializedDiagnosticPathsByTargetName: .success(buildMessageHandler.serializedDiagnosticPathsByTargetName), symbolGraph: SymbolGraphResult( outputLocationForTarget: { target, buildParameters in return ["\(buildParameters.triple.archName)", "\(target).symbolgraphs"] @@ -1027,16 +1025,6 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { } } - private func parseOutput(_ info: SwiftBuildMessage.OutputInfo) -> String { - var result = "" - let data = info.data - // let parser = SwiftCompilerOutputParser() - // use json parser to parse bytes -// self.buildMessageParser?.parse(bytes: data) - - return result - } - private func makeRunDestination() -> SwiftBuild.SWBRunDestinationInfo { let platformName: String let sdkName: String From b1ff2310d4ab861714db9264e76166f3765dd33e Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Thu, 13 Nov 2025 13:33:17 -0500 Subject: [PATCH 03/24] Implement per-task-buffer of Data output --- Package.swift | 3 + .../SwiftBuildSupport/SwiftBuildSystem.swift | 91 +++++++++++++++++-- 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/Package.swift b/Package.swift index aa32ba1e384..7cb5f053caa 100644 --- a/Package.swift +++ b/Package.swift @@ -1165,6 +1165,9 @@ if !shouldUseSwiftBuildFramework { package.dependencies += [ .package(url: "https://github.com/swiftlang/swift-build.git", branch: relatedDependenciesBranch), ] +// package.dependencies += [ +// .package(path: "../swift-build") +// ] } else { package.dependencies += [ .package(path: "../swift-build"), diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index 8fd3804f0f0..8a19950ae29 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -184,6 +184,26 @@ private final class PlanningOperationDelegate: SWBPlanningOperationDelegate, Sen } } +extension SwiftBuildMessage.LocationContext { + var taskID: Int? { + switch self { + case .task(let id, _), .globalTask(let id): + return id + case .target, .global: + return nil + } + } + + var targetID: Int? { + switch self { + case .task(_, let id), .target(let id): + return id + case .global, .globalTask: + return nil + } + } +} + /// Handler for SwiftBuildMessage events sent by the active SWBService. final class SwiftBuildSystemMessageHandler { private var buildSystem: SPMBuildCore.BuildSystem @@ -196,6 +216,8 @@ final class SwiftBuildSystemMessageHandler { let progressAnimation: ProgressAnimationProtocol var serializedDiagnosticPathsByTargetName: [String: [Basics.AbsolutePath]] = [:] + private var unprocessedDiagnostics: [SwiftBuildMessage.DiagnosticInfo] = [] + public init( buildSystem: SPMBuildCore.BuildSystem, observabilityScope: ObservabilityScope, @@ -216,6 +238,7 @@ final class SwiftBuildSystemMessageHandler { struct BuildState { private var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:] private var activeTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskStartedInfo] = [:] + private var taskBuffer: [Int: Data] = [:] mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws { if activeTasks[task.taskID] != nil { @@ -247,6 +270,19 @@ final class SwiftBuildSystemMessageHandler { } return target } + + mutating func appendToBuffer(taskID id: Int, data: Data) { + taskBuffer[id, default: .init()].append(data) + } + + func taskBuffer(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) -> Data? { + guard let data = taskBuffer[task.taskID] else { +// throw Diagnostics.fatalError + return nil + } + + return data + } } private func emitInfoAsDiagnostic(info: SwiftBuildMessage.DiagnosticInfo) { @@ -273,9 +309,41 @@ final class SwiftBuildSystemMessageHandler { } } - func emitEvent( - _ message: SwiftBuild.SwiftBuildMessage - ) throws { + struct ParsedDiagnostic { + var filePath: String + var col: Int + var line: Int + var kind: Diagnostic.Severity + var message: String + var codeSnippet: String? + var fixIts: [String] = [] + } + + private func parseCompilerOutput(_ info: Data) -> [ParsedDiagnostic] { + // How to separate the string by diagnostic chunk: + // 1. Decode into string + // 2. Determine how many diagnostic message there are; + // a. Determine a parsing method + // 3. Serialize into a ParsedDiagnostic, with matching DiagnosticInfo elsewhere + let decodedString = String(decoding: info, as: UTF8.self) + var diagnostics: [ParsedDiagnostic] = [] + + var diagnosticUserDescriptions = unprocessedDiagnostics.compactMap({ info in + info.location.userDescription + }) + // For each Diagnostic.Severity, there is a logLabel property that we can + // use to search upon in the given Data blob + // This can give us san estimate of how many diagnostics there are. + for desc in diagnosticUserDescriptions { + let doesContain = decodedString.contains(desc) + print("does contain diagnostic location \(doesContain)") + print("loc: \(desc)") + } + + return diagnostics + } + + func emitEvent(_ message: SwiftBuild.SwiftBuildMessage) throws { guard !self.logLevel.isQuiet else { return } switch message { case .buildCompleted(let info): @@ -298,15 +366,17 @@ final class SwiftBuildSystemMessageHandler { case .diagnostic(let info): if info.appendToOutputStream { emitInfoAsDiagnostic(info: info) + } else { + unprocessedDiagnostics.append(info) } case .output(let info): - let parsedOutput = String(decoding: info.data, as: UTF8.self) - if parsedOutput.contains("error: ") { - self.observabilityScope.emit(severity: .error, message: parsedOutput) - } else { - self.observabilityScope.emit(info: parsedOutput) + // TODO bp possible bug with location context re: locationContext2 nil properties + // Grab the taskID to append to buffer-per-task storage + guard let taskID = info.locationContext.taskID else { + return } - // Parse the output to extract diagnostics with code snippets + + buildState.appendToBuffer(taskID: taskID, data: info.data) case .taskStarted(let info): try buildState.started(task: info) @@ -328,6 +398,9 @@ final class SwiftBuildSystemMessageHandler { self.delegate?.buildSystem(self.buildSystem, didStartCommand: BuildSystemCommand(info, targetInfo: targetInfo)) case .taskComplete(let info): let startedInfo = try buildState.completed(task: info) + if let buffer = buildState.taskBuffer(for: startedInfo) { + let parsedOutput = parseCompilerOutput(buffer) + } if info.result != .success { self.observabilityScope.emit(severity: .error, message: "\(startedInfo.ruleInfo) failed with a nonzero exit code. Command line: \(startedInfo.commandLineDisplayString ?? "")") } From ac6b81bdceeab018b6221cb14efd5d55556166ce Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Thu, 13 Nov 2025 14:10:10 -0500 Subject: [PATCH 04/24] Fallback to locationContext if locationContext2 properties are nil --- .../SwiftBuildSupport/SwiftBuildSystem.swift | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index 8a19950ae29..e233795422f 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -238,13 +238,15 @@ final class SwiftBuildSystemMessageHandler { struct BuildState { private var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:] private var activeTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskStartedInfo] = [:] - private var taskBuffer: [Int: Data] = [:] + private var taskBuffer: [String: Data] = [:] + private var taskIDToSignature: [Int: String] = [:] mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws { if activeTasks[task.taskID] != nil { throw Diagnostics.fatalError } activeTasks[task.taskID] = task + taskIDToSignature[task.taskID] = task.taskSignature } mutating func completed(task: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo) throws -> SwiftBuild.SwiftBuildMessage.TaskStartedInfo { @@ -271,13 +273,20 @@ final class SwiftBuildSystemMessageHandler { return target } - mutating func appendToBuffer(taskID id: Int, data: Data) { - taskBuffer[id, default: .init()].append(data) + func taskSignature(for id: Int) -> String? { + if let signature = taskIDToSignature[id] { + return signature + } + return nil + } + + mutating func appendToBuffer(_ task: String, data: Data) { + taskBuffer[task, default: .init()].append(data) } - func taskBuffer(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) -> Data? { - guard let data = taskBuffer[task.taskID] else { -// throw Diagnostics.fatalError + func dataBuffer(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) -> Data? { + guard let data = taskBuffer[task.taskSignature] else { + // If there is no available buffer, simply return nil. return nil } @@ -370,13 +379,19 @@ final class SwiftBuildSystemMessageHandler { unprocessedDiagnostics.append(info) } case .output(let info): - // TODO bp possible bug with location context re: locationContext2 nil properties // Grab the taskID to append to buffer-per-task storage - guard let taskID = info.locationContext.taskID else { + guard let taskSignature = info.locationContext2.taskSignature else { + // If we cannot find the task signature from the locationContext2, + // use deprecated locationContext instead. + if let taskID = info.locationContext.taskID, + let taskSignature = buildState.taskSignature(for: taskID) { + buildState.appendToBuffer(taskSignature, data: info.data) + } + return } - buildState.appendToBuffer(taskID: taskID, data: info.data) + buildState.appendToBuffer(taskSignature, data: info.data) case .taskStarted(let info): try buildState.started(task: info) @@ -398,7 +413,7 @@ final class SwiftBuildSystemMessageHandler { self.delegate?.buildSystem(self.buildSystem, didStartCommand: BuildSystemCommand(info, targetInfo: targetInfo)) case .taskComplete(let info): let startedInfo = try buildState.completed(task: info) - if let buffer = buildState.taskBuffer(for: startedInfo) { + if let buffer = buildState.dataBuffer(for: startedInfo) { let parsedOutput = parseCompilerOutput(buffer) } if info.result != .success { From b81bfcae7ea40ca59fca180b334d364036edad37 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Thu, 13 Nov 2025 16:43:12 -0500 Subject: [PATCH 05/24] Cleanup; add descriptions related to redundant task output --- Sources/SwiftBuildSupport/SwiftBuildSystem.swift | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index 3b01ad95ea9..5eac6a1d7f0 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -30,19 +30,9 @@ import protocol TSCBasic.OutputByteStream import func TSCBasic.withTemporaryFile import enum TSCUtility.Diagnostics -import class TSCUtility.JSONMessageStreamingParser -import protocol TSCUtility.JSONMessageStreamingParserDelegate -import struct TSCBasic.RegEx import var TSCBasic.stdoutStream -import class Build.SwiftCompilerOutputParser -import protocol Build.SwiftCompilerOutputParserDelegate -import struct Build.SwiftCompilerMessage - -// TODO bp -import class SWBCore.SwiftCommandOutputParser - import Foundation import SWBBuildService import SwiftBuild @@ -294,6 +284,7 @@ final class SwiftBuildSystemMessageHandler { } private func emitInfoAsDiagnostic(info: SwiftBuildMessage.DiagnosticInfo) { + // Assure that we haven't already emitted this diagnostic. let fixItsDescription = if info.fixIts.hasContent { ": " + info.fixIts.map { String(describing: $0) }.joined(separator: ", ") } else { @@ -322,10 +313,14 @@ final class SwiftBuildSystemMessageHandler { guard !self.tasksEmitted.contains(info.taskSignature) else { return } + // Assure we have a data buffer to decode. guard let buffer = buildState.dataBuffer(for: info) else { return } + // Fetch the task signature for a SwiftBuildMessage.DiagnosticInfo, + // falling back to using the deprecated `locationContext` if we fail + // to find it through the `locationContext2`. func getTaskSignature(from info: SwiftBuildMessage.DiagnosticInfo) -> String? { if let taskSignature = info.locationContext2.taskSignature { return taskSignature @@ -346,6 +341,7 @@ final class SwiftBuildSystemMessageHandler { self.observabilityScope.emit(info: decodedOutput) } + // Record that we've emitted the output for a given task signature. self.tasksEmitted.insert(info.taskSignature) } From f3aaabf0c8b47b053c0613fb163e16ad19837085 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Thu, 20 Nov 2025 13:46:13 -0500 Subject: [PATCH 06/24] attempt to parse decoded string into individual diagnostics --- .../SwiftBuildSupport/SwiftBuildSystem.swift | 143 +++++++++++++++--- 1 file changed, 125 insertions(+), 18 deletions(-) diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index 5eac6a1d7f0..acf38473bbe 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -308,6 +308,91 @@ final class SwiftBuildSystemMessageHandler { } } + /// Represents a parsed diagnostic segment from compiler output + private struct ParsedDiagnostic { + /// The file path if present + let filePath: String? + /// The line number if present + let line: Int? + /// The column number if present + let column: Int? + /// The severity (error, warning, note, remark) + let severity: String + /// The diagnostic message text + let message: String + /// The full text including any multi-line context (code snippets, carets, etc.) + let fullText: String + + /// Parse severity string to Diagnostic.Severity + func toDiagnosticSeverity() -> Basics.Diagnostic.Severity { + switch severity.lowercased() { + case "error": return .error + case "warning": return .warning + case "note": return .info + case "remark": return .debug + default: return .info + } + } + } + + /// Split compiler output into individual diagnostic segments + /// Format: /path/to/file.swift:line:column: severity: message + private func splitIntoDiagnostics(_ output: String) -> [ParsedDiagnostic] { + var diagnostics: [ParsedDiagnostic] = [] + + // Regex pattern to match diagnostic lines + // Matches: path:line:column: severity: message (path is required) + // The path must contain at least one character and line must be present + let diagnosticPattern = #"^(.+?):(\d+):(?:(\d+):)?\s*(error|warning|note|remark):\s*(.*)$"# + guard let regex = try? NSRegularExpression(pattern: diagnosticPattern, options: [.anchorsMatchLines]) else { + return [] + } + + let nsString = output as NSString + let matches = regex.matches(in: output, options: [], range: NSRange(location: 0, length: nsString.length)) + + // Process each match and gather full text including subsequent lines + for (index, match) in matches.enumerated() { + let matchRange = match.range + + // Extract components + let filePathRange = match.range(at: 1) + let lineRange = match.range(at: 2) + let columnRange = match.range(at: 3) + let severityRange = match.range(at: 4) + let messageRange = match.range(at: 5) + + let filePath = nsString.substring(with: filePathRange) + let line = Int(nsString.substring(with: lineRange)) + let column = columnRange.location != NSNotFound ? Int(nsString.substring(with: columnRange)) : nil + let severity = nsString.substring(with: severityRange) + let message = nsString.substring(with: messageRange) + + // Determine the full text range (from this diagnostic to the next one, or end) + let startLocation = matchRange.location + let endLocation: Int + if index + 1 < matches.count { + endLocation = matches[index + 1].range.location + } else { + endLocation = nsString.length + } + + let fullTextRange = NSRange(location: startLocation, length: endLocation - startLocation) + let fullText = nsString.substring(with: fullTextRange).trimmingCharacters(in: .whitespacesAndNewlines) + + diagnostics.append(ParsedDiagnostic( + filePath: filePath, + line: line, + column: column, + severity: severity, + message: message, + fullText: fullText + )) + } + + return diagnostics + } + private func emitDiagnosticCompilerOutput(_ info: SwiftBuildMessage.TaskStartedInfo) { // Don't redundantly emit tasks. guard !self.tasksEmitted.contains(info.taskSignature) else { @@ -318,27 +403,49 @@ final class SwiftBuildSystemMessageHandler { return } - // Fetch the task signature for a SwiftBuildMessage.DiagnosticInfo, - // falling back to using the deprecated `locationContext` if we fail - // to find it through the `locationContext2`. - func getTaskSignature(from info: SwiftBuildMessage.DiagnosticInfo) -> String? { - if let taskSignature = info.locationContext2.taskSignature { - return taskSignature - } else if let taskID = info.locationContext.taskID, - let taskSignature = self.buildState.taskSignature(for: taskID) - { - return taskSignature + // Decode the buffer to a string + let decodedOutput = String(decoding: buffer, as: UTF8.self) + + // Split the output into individual diagnostic segments + let parsedDiagnostics = splitIntoDiagnostics(decodedOutput) + + if parsedDiagnostics.isEmpty { + // No structured diagnostics found - emit as-is based on task signature matching + // Fetch the task signature for a SwiftBuildMessage.DiagnosticInfo + func getTaskSignature(from info: SwiftBuildMessage.DiagnosticInfo) -> String? { + if let taskSignature = info.locationContext2.taskSignature { + return taskSignature + } else if let taskID = info.locationContext.taskID, + let taskSignature = self.buildState.taskSignature(for: taskID) + { + return taskSignature + } + return nil } - return nil - } - // Ensure that this info matches with the location context of the - // DiagnosticInfo. Otherwise, it should be emitted with "info" severity. - let decodedOutput = String(decoding: buffer, as: UTF8.self) - if unprocessedDiagnostics.compactMap(getTaskSignature).contains(where: { $0 == info.taskSignature }) { - self.observabilityScope.emit(error: decodedOutput) + // Use existing logic as fallback + if unprocessedDiagnostics.compactMap(getTaskSignature).contains(where: { $0 == info.taskSignature }) { + self.observabilityScope.emit(error: decodedOutput) + } else { + self.observabilityScope.emit(info: decodedOutput) + } } else { - self.observabilityScope.emit(info: decodedOutput) + // Process each parsed diagnostic derived from the decodedOutput + for parsedDiag in parsedDiagnostics { + let severity = parsedDiag.toDiagnosticSeverity() + + // Use the appropriate emit method based on severity + switch severity { + case .error: + self.observabilityScope.emit(error: parsedDiag.fullText) + case .warning: + self.observabilityScope.emit(warning: parsedDiag.fullText) + case .info: + self.observabilityScope.emit(info: parsedDiag.fullText) + case .debug: + self.observabilityScope.emit(severity: .debug, message: parsedDiag.fullText) + } + } } // Record that we've emitted the output for a given task signature. From c48e606afdcb5c61b55bca074f3e8a5818306676 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Thu, 20 Nov 2025 13:57:18 -0500 Subject: [PATCH 07/24] cleanup --- Package.swift | 3 --- Sources/SwiftBuildSupport/SwiftBuildSystem.swift | 1 - 2 files changed, 4 deletions(-) diff --git a/Package.swift b/Package.swift index 7cb5f053caa..aa32ba1e384 100644 --- a/Package.swift +++ b/Package.swift @@ -1165,9 +1165,6 @@ if !shouldUseSwiftBuildFramework { package.dependencies += [ .package(url: "https://github.com/swiftlang/swift-build.git", branch: relatedDependenciesBranch), ] -// package.dependencies += [ -// .package(path: "../swift-build") -// ] } else { package.dependencies += [ .package(path: "../swift-build"), diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index acf38473bbe..4cece7d7491 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -1211,7 +1211,6 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { + buildParameters.flags.swiftCompilerFlags.map { $0.shellEscaped() } ).joined(separator: " ") - // TODO bp passing -v flag via verboseFlag here; investigate linker messages settings["OTHER_LDFLAGS"] = ( verboseFlag + // clang will be invoked to link so the verbose flag is valid for it ["$(inherited)"] From f14600cf73a7126fca7ca66eeb419726a52aa499 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Fri, 21 Nov 2025 15:38:02 -0500 Subject: [PATCH 08/24] Revert diagnostic parsing and emit directly to outputStream --- .../SwiftBuildSupport/SwiftBuildSystem.swift | 134 +----------------- 1 file changed, 4 insertions(+), 130 deletions(-) diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index 4cece7d7491..a631b0f6c1e 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -196,7 +196,7 @@ extension SwiftBuildMessage.LocationContext { } /// Handler for SwiftBuildMessage events sent by the SWBBuildOperation. -final class SwiftBuildSystemMessageHandler { +public final class SwiftBuildSystemMessageHandler { private let observabilityScope: ObservabilityScope private let outputStream: OutputByteStream private let logLevel: Basics.Diagnostic.Severity @@ -252,7 +252,7 @@ final class SwiftBuildSystemMessageHandler { targetsByID[target.targetID] = target } - mutating func target(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws -> SwiftBuild.SwiftBuildMessage.TargetStartedInfo? { + func target(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws -> SwiftBuild.SwiftBuildMessage.TargetStartedInfo? { guard let id = task.targetID else { return nil } @@ -308,91 +308,6 @@ final class SwiftBuildSystemMessageHandler { } } - /// Represents a parsed diagnostic segment from compiler output - private struct ParsedDiagnostic { - /// The file path if present - let filePath: String? - /// The line number if present - let line: Int? - /// The column number if present - let column: Int? - /// The severity (error, warning, note, remark) - let severity: String - /// The diagnostic message text - let message: String - /// The full text including any multi-line context (code snippets, carets, etc.) - let fullText: String - - /// Parse severity string to Diagnostic.Severity - func toDiagnosticSeverity() -> Basics.Diagnostic.Severity { - switch severity.lowercased() { - case "error": return .error - case "warning": return .warning - case "note": return .info - case "remark": return .debug - default: return .info - } - } - } - - /// Split compiler output into individual diagnostic segments - /// Format: /path/to/file.swift:line:column: severity: message - private func splitIntoDiagnostics(_ output: String) -> [ParsedDiagnostic] { - var diagnostics: [ParsedDiagnostic] = [] - - // Regex pattern to match diagnostic lines - // Matches: path:line:column: severity: message (path is required) - // The path must contain at least one character and line must be present - let diagnosticPattern = #"^(.+?):(\d+):(?:(\d+):)?\s*(error|warning|note|remark):\s*(.*)$"# - guard let regex = try? NSRegularExpression(pattern: diagnosticPattern, options: [.anchorsMatchLines]) else { - return [] - } - - let nsString = output as NSString - let matches = regex.matches(in: output, options: [], range: NSRange(location: 0, length: nsString.length)) - - // Process each match and gather full text including subsequent lines - for (index, match) in matches.enumerated() { - let matchRange = match.range - - // Extract components - let filePathRange = match.range(at: 1) - let lineRange = match.range(at: 2) - let columnRange = match.range(at: 3) - let severityRange = match.range(at: 4) - let messageRange = match.range(at: 5) - - let filePath = nsString.substring(with: filePathRange) - let line = Int(nsString.substring(with: lineRange)) - let column = columnRange.location != NSNotFound ? Int(nsString.substring(with: columnRange)) : nil - let severity = nsString.substring(with: severityRange) - let message = nsString.substring(with: messageRange) - - // Determine the full text range (from this diagnostic to the next one, or end) - let startLocation = matchRange.location - let endLocation: Int - if index + 1 < matches.count { - endLocation = matches[index + 1].range.location - } else { - endLocation = nsString.length - } - - let fullTextRange = NSRange(location: startLocation, length: endLocation - startLocation) - let fullText = nsString.substring(with: fullTextRange).trimmingCharacters(in: .whitespacesAndNewlines) - - diagnostics.append(ParsedDiagnostic( - filePath: filePath, - line: line, - column: column, - severity: severity, - message: message, - fullText: fullText - )) - } - - return diagnostics - } - private func emitDiagnosticCompilerOutput(_ info: SwiftBuildMessage.TaskStartedInfo) { // Don't redundantly emit tasks. guard !self.tasksEmitted.contains(info.taskSignature) else { @@ -406,47 +321,8 @@ final class SwiftBuildSystemMessageHandler { // Decode the buffer to a string let decodedOutput = String(decoding: buffer, as: UTF8.self) - // Split the output into individual diagnostic segments - let parsedDiagnostics = splitIntoDiagnostics(decodedOutput) - - if parsedDiagnostics.isEmpty { - // No structured diagnostics found - emit as-is based on task signature matching - // Fetch the task signature for a SwiftBuildMessage.DiagnosticInfo - func getTaskSignature(from info: SwiftBuildMessage.DiagnosticInfo) -> String? { - if let taskSignature = info.locationContext2.taskSignature { - return taskSignature - } else if let taskID = info.locationContext.taskID, - let taskSignature = self.buildState.taskSignature(for: taskID) - { - return taskSignature - } - return nil - } - - // Use existing logic as fallback - if unprocessedDiagnostics.compactMap(getTaskSignature).contains(where: { $0 == info.taskSignature }) { - self.observabilityScope.emit(error: decodedOutput) - } else { - self.observabilityScope.emit(info: decodedOutput) - } - } else { - // Process each parsed diagnostic derived from the decodedOutput - for parsedDiag in parsedDiagnostics { - let severity = parsedDiag.toDiagnosticSeverity() - - // Use the appropriate emit method based on severity - switch severity { - case .error: - self.observabilityScope.emit(error: parsedDiag.fullText) - case .warning: - self.observabilityScope.emit(warning: parsedDiag.fullText) - case .info: - self.observabilityScope.emit(info: parsedDiag.fullText) - case .debug: - self.observabilityScope.emit(severity: .debug, message: parsedDiag.fullText) - } - } - } + // Emit to output stream. + outputStream.send(decodedOutput) // Record that we've emitted the output for a given task signature. self.tasksEmitted.insert(info.taskSignature) @@ -515,9 +391,7 @@ final class SwiftBuildSystemMessageHandler { let startedInfo = try buildState.completed(task: info) // If we've captured the compiler output with formatted diagnostics, emit them. -// if let buffer = buildState.dataBuffer(for: startedInfo) { emitDiagnosticCompilerOutput(startedInfo) -// } if info.result != .success { self.observabilityScope.emit(severity: .error, message: "\(startedInfo.ruleInfo) failed with a nonzero exit code. Command line: \(startedInfo.commandLineDisplayString ?? "")") From 359331afc02882949264b51134b8e8bd0a2732d0 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Fri, 21 Nov 2025 16:01:02 -0500 Subject: [PATCH 09/24] Address PR comments * Remove taskStarted outputStream emissions * Propagate exception if unable to find path --- Sources/SwiftBuildSupport/SwiftBuildSystem.swift | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index a631b0f6c1e..c58cde24ef4 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -377,13 +377,6 @@ public final class SwiftBuildSystemMessageHandler { self.observabilityScope.emit(info: "\(info.executionDescription)") } - if self.logLevel.isVerbose { - if let commandLineDisplay = info.commandLineDisplayString { - self.outputStream.send("\(info.executionDescription)\n\(commandLineDisplay)") - } else { - self.outputStream.send("\(info.executionDescription)") - } - } let targetInfo = try buildState.target(for: info) buildSystem.delegate?.buildSystem(buildSystem, willStartCommand: BuildSystemCommand(info, targetInfo: targetInfo)) buildSystem.delegate?.buildSystem(buildSystem, didStartCommand: BuildSystemCommand(info, targetInfo: targetInfo)) @@ -399,8 +392,8 @@ public final class SwiftBuildSystemMessageHandler { let targetInfo = try buildState.target(for: startedInfo) buildSystem.delegate?.buildSystem(buildSystem, didFinishCommand: BuildSystemCommand(startedInfo, targetInfo: targetInfo)) if let targetName = targetInfo?.targetName { - serializedDiagnosticPathsByTargetName[targetName, default: []].append(contentsOf: startedInfo.serializedDiagnosticsPaths.compactMap { - try? Basics.AbsolutePath(validating: $0.pathString) + try serializedDiagnosticPathsByTargetName[targetName, default: []].append(contentsOf: startedInfo.serializedDiagnosticsPaths.compactMap { + try Basics.AbsolutePath(validating: $0.pathString) }) } if buildSystem.enableTaskBacktraces { From 56f0a4502d6b00396fd2041e768861316ee52c48 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Mon, 24 Nov 2025 16:21:30 -0500 Subject: [PATCH 10/24] implement generic print method for observability scope --- Sources/Basics/Observability.swift | 14 ++++++++++++++ .../SwiftCommandObservabilityHandler.swift | 6 ++++++ Sources/SwiftBuildSupport/SwiftBuildSystem.swift | 5 +++-- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Sources/Basics/Observability.swift b/Sources/Basics/Observability.swift index 404d42a85c4..24d965e5002 100644 --- a/Sources/Basics/Observability.swift +++ b/Sources/Basics/Observability.swift @@ -56,6 +56,10 @@ public class ObservabilitySystem { func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic) { self.underlying(scope, diagnostic) } + + func print(message: String) { + self.diagnosticsHandler.print(message: message) + } } public static var NOOP: ObservabilityScope { @@ -128,6 +132,10 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol, Sendable, Cus return parent?.errorsReportedInAnyScope ?? false } + public func print(message: String) { + self.diagnosticsHandler.print(message: message) + } + // DiagnosticsEmitterProtocol public func emit(_ diagnostic: Diagnostic) { var diagnostic = diagnostic @@ -150,6 +158,10 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol, Sendable, Cus self.underlying.handleDiagnostic(scope: scope, diagnostic: diagnostic) } + public func print(message: String) { + self.underlying.print(message: message) + } + var errorsReported: Bool { self._errorsReported.get() ?? false } @@ -160,6 +172,8 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol, Sendable, Cus public protocol DiagnosticsHandler: Sendable { func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic) + + func print(message: String) } /// Helper protocol to share default behavior. diff --git a/Sources/CoreCommands/SwiftCommandObservabilityHandler.swift b/Sources/CoreCommands/SwiftCommandObservabilityHandler.swift index aa49fffcc94..ffcbfbbae6a 100644 --- a/Sources/CoreCommands/SwiftCommandObservabilityHandler.swift +++ b/Sources/CoreCommands/SwiftCommandObservabilityHandler.swift @@ -116,6 +116,12 @@ public struct SwiftCommandObservabilityHandler: ObservabilityHandlerProvider { } } + func print(message: String) { + self.queue.async(group: self.sync) { + self.write(message) + } + } + // for raw output reporting func print(_ output: String, verbose: Bool) { self.queue.async(group: self.sync) { diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index c58cde24ef4..089b4110399 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -227,6 +227,7 @@ public final class SwiftBuildSystemMessageHandler { private var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:] private var activeTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskStartedInfo] = [:] private var taskBuffer: [String: Data] = [:] +// private var taskBuffer: [SwiftBuildMessage.LocationContext: Data] = [:] private var taskIDToSignature: [Int: String] = [:] var collectedBacktraceFrames = SWBBuildOperationCollectedBacktraceFrames() @@ -284,7 +285,6 @@ public final class SwiftBuildSystemMessageHandler { } private func emitInfoAsDiagnostic(info: SwiftBuildMessage.DiagnosticInfo) { - // Assure that we haven't already emitted this diagnostic. let fixItsDescription = if info.fixIts.hasContent { ": " + info.fixIts.map { String(describing: $0) }.joined(separator: ", ") } else { @@ -322,7 +322,8 @@ public final class SwiftBuildSystemMessageHandler { let decodedOutput = String(decoding: buffer, as: UTF8.self) // Emit to output stream. - outputStream.send(decodedOutput) +// outputStream.send(decodedOutput) + observabilityScope.print(message: decodedOutput) // Record that we've emitted the output for a given task signature. self.tasksEmitted.insert(info.taskSignature) From dd1505a5a987d2c2dc7db2f7a96869d87087b2a3 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Tue, 25 Nov 2025 16:45:22 -0500 Subject: [PATCH 11/24] Introduce model to store data buffer per task type The TaskDataBuffer introduces some extra complexity in its handling of various buffers we'd actually like to track and emit, rather than ignore. We will keep multiple buffers depending on the information we have available i.e. whether there's an existing task signature buffer, taskID buffer, targetID buffer, or simply a global buffer. --- .../SwiftBuildSupport/SwiftBuildSystem.swift | 151 +++++++++++++++--- 1 file changed, 126 insertions(+), 25 deletions(-) diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index 089b4110399..ee854df79ac 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -198,7 +198,6 @@ extension SwiftBuildMessage.LocationContext { /// Handler for SwiftBuildMessage events sent by the SWBBuildOperation. public final class SwiftBuildSystemMessageHandler { private let observabilityScope: ObservabilityScope - private let outputStream: OutputByteStream private let logLevel: Basics.Diagnostic.Severity private var buildState: BuildState = .init() private var tasksEmitted: Set = [] @@ -215,10 +214,9 @@ public final class SwiftBuildSystemMessageHandler { ) { self.observabilityScope = observabilityScope - self.outputStream = outputStream self.logLevel = logLevel self.progressAnimation = ProgressAnimation.ninja( - stream: self.outputStream, + stream: outputStream, verbose: self.logLevel.isVerbose ) } @@ -226,11 +224,104 @@ public final class SwiftBuildSystemMessageHandler { struct BuildState { private var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:] private var activeTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskStartedInfo] = [:] - private var taskBuffer: [String: Data] = [:] -// private var taskBuffer: [SwiftBuildMessage.LocationContext: Data] = [:] + private var taskDataBuffer: TaskDataBuffer = [:] private var taskIDToSignature: [Int: String] = [:] var collectedBacktraceFrames = SWBBuildOperationCollectedBacktraceFrames() + struct TaskDataBuffer: ExpressibleByDictionaryLiteral { + typealias Key = String + typealias Value = Data + + // Default taskSignature -> Data buffer + private var storage: [Key: Value] = [:] + + // Others + private var taskIDDataBuffer: [Int: Data] = [:] + private var globalDataBuffer: Data = Data() + private var targetIDDataBuffer: [Int: Data] = [:] + + subscript(key: String) -> Value? { + self.storage[key] + } + + subscript(key: String, default defaultValue: Value) -> Value { + get { self.storage[key] ?? defaultValue } + set { self.storage[key] = newValue } + } + + subscript(key: SwiftBuildMessage.LocationContext, default defaultValue: Value) -> Value { + get { + if let taskID = key.taskID, let result = self.taskIDDataBuffer[taskID] { + return result + // ask for build state to fetch taskSignature for a given id? + } else if let targetID = key.targetID, let result = self.targetIDDataBuffer[targetID] { + return result + } else if !self.globalDataBuffer.isEmpty { + return self.globalDataBuffer + } else { + return defaultValue + } + } + + set { + if let taskID = key.taskID { + self.taskIDDataBuffer[taskID] = newValue + if let targetID = key.targetID { + self.targetIDDataBuffer[targetID] = newValue + } + } else if let targetID = key.targetID { + self.targetIDDataBuffer[targetID] = newValue + } else { + self.globalDataBuffer = newValue + } + } + } + + subscript(key: SwiftBuildMessage.LocationContext2) -> Value? { + get { + if let taskSignature = key.taskSignature { + return self.storage[taskSignature] + } else if let targetID = key.targetID { + return self.targetIDDataBuffer[targetID] + } + + return nil + } + + set { + if let taskSignature = key.taskSignature { + self.storage[taskSignature] = newValue + } else if let targetID = key.targetID { + self.targetIDDataBuffer[targetID] = newValue + } + } + } + + subscript(task: SwiftBuildMessage.TaskStartedInfo) -> Value? { + get { + guard let result = self.storage[task.taskSignature] else { + // Default to checking targetID and taskID. + if let result = self.taskIDDataBuffer[task.taskID] { + return result + } else if let targetID = task.targetID, + let result = self.targetIDDataBuffer[targetID] { + return result + } + + return nil + } + + return result + } + } + + init(dictionaryLiteral elements: (String, Data)...) { + for (key, value) in elements { + self.storage[key] = value + } + } + } + mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws { if activeTasks[task.taskID] != nil { throw Diagnostics.fatalError @@ -270,16 +361,36 @@ public final class SwiftBuildSystemMessageHandler { return nil } - mutating func appendToBuffer(_ task: String, data: Data) { - taskBuffer[task, default: .init()].append(data) + mutating func appendToBuffer(_ info: SwiftBuildMessage.OutputInfo) { + // Attempt to key by taskSignature; at times this may not be possible, + // in which case we'd need to fall back to using LocationContext. + guard let taskSignature = info.locationContext2.taskSignature else { + // If we cannot find the task signature from the locationContext2, + // use deprecated locationContext instead to find task signature. + // If this fails to find an associated task signature, track + // relevant IDs from the location context in the task buffer. + if let taskID = info.locationContext.taskID, + let taskSignature = self.taskSignature(for: taskID) { + self.taskDataBuffer[taskSignature, default: .init()].append(info.data) + self.taskDataBuffer[info.locationContext, default: .init()].append(info.data) + } else { + self.taskDataBuffer[info.locationContext, default: .init()].append(info.data) + } + + return + } + + self.taskDataBuffer[taskSignature, default: .init()].append(info.data) } func dataBuffer(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) -> Data? { - guard let data = taskBuffer[task.taskSignature] else { - // If there is no available buffer, simply return nil. - return nil + guard let data = taskDataBuffer[task.taskSignature] else { + // Fallback to checking taskID and targetID. + return taskDataBuffer[task] } + // todo bp "-fno-color-diagnostics", + return data } } @@ -321,8 +432,9 @@ public final class SwiftBuildSystemMessageHandler { // Decode the buffer to a string let decodedOutput = String(decoding: buffer, as: UTF8.self) - // Emit to output stream. -// outputStream.send(decodedOutput) + // Emit message. + // Note: This is a temporary workaround until we can re-architect + // how we'd like to format and handle diagnostic output. observabilityScope.print(message: decodedOutput) // Record that we've emitted the output for a given task signature. @@ -356,19 +468,8 @@ public final class SwiftBuildSystemMessageHandler { unprocessedDiagnostics.append(info) } case .output(let info): - // Grab the taskID to append to buffer-per-task storage - guard let taskSignature = info.locationContext2.taskSignature else { - // If we cannot find the task signature from the locationContext2, - // use deprecated locationContext instead. - if let taskID = info.locationContext.taskID, - let taskSignature = buildState.taskSignature(for: taskID) { - buildState.appendToBuffer(taskSignature, data: info.data) - } - - return - } - - buildState.appendToBuffer(taskSignature, data: info.data) + // Append to buffer-per-task storage + buildState.appendToBuffer(info) case .taskStarted(let info): try buildState.started(task: info) From 08306e2ab0c0666c0f5dfde28912cd55c3527177 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Tue, 25 Nov 2025 16:54:59 -0500 Subject: [PATCH 12/24] minor changes to TaskDataBuffer + cleanup --- .../SwiftBuildSupport/SwiftBuildSystem.swift | 64 ++++++++++--------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index ee854df79ac..5598310fcb4 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -232,32 +232,33 @@ public final class SwiftBuildSystemMessageHandler { typealias Key = String typealias Value = Data - // Default taskSignature -> Data buffer - private var storage: [Key: Value] = [:] - - // Others - private var taskIDDataBuffer: [Int: Data] = [:] - private var globalDataBuffer: Data = Data() - private var targetIDDataBuffer: [Int: Data] = [:] + private var taskSignatureBuffer: [Key: Value] = [:] + private var taskIDBuffer: [Int: Data] = [:] + private var targetIDBuffer: [Int: Data] = [:] + private var globalBuffer: Data = Data() subscript(key: String) -> Value? { - self.storage[key] + self.taskSignatureBuffer[key] } subscript(key: String, default defaultValue: Value) -> Value { - get { self.storage[key] ?? defaultValue } - set { self.storage[key] = newValue } + get { self.taskSignatureBuffer[key] ?? defaultValue } + set { self.taskSignatureBuffer[key] = newValue } } subscript(key: SwiftBuildMessage.LocationContext, default defaultValue: Value) -> Value { get { - if let taskID = key.taskID, let result = self.taskIDDataBuffer[taskID] { + // Check each ID kind and try to fetch the associated buffer. + // If unable to get a non-nil result, then follow through to the + // next check. + if let taskID = key.taskID, + let result = self.taskIDBuffer[taskID] { return result - // ask for build state to fetch taskSignature for a given id? - } else if let targetID = key.targetID, let result = self.targetIDDataBuffer[targetID] { + } else if let targetID = key.targetID, + let result = self.targetIDBuffer[targetID] { return result - } else if !self.globalDataBuffer.isEmpty { - return self.globalDataBuffer + } else if !self.globalBuffer.isEmpty { + return self.globalBuffer } else { return defaultValue } @@ -265,14 +266,14 @@ public final class SwiftBuildSystemMessageHandler { set { if let taskID = key.taskID { - self.taskIDDataBuffer[taskID] = newValue + self.taskIDBuffer[taskID] = newValue if let targetID = key.targetID { - self.targetIDDataBuffer[targetID] = newValue + self.targetIDBuffer[targetID] = newValue } } else if let targetID = key.targetID { - self.targetIDDataBuffer[targetID] = newValue + self.targetIDBuffer[targetID] = newValue } else { - self.globalDataBuffer = newValue + self.globalBuffer = newValue } } } @@ -280,9 +281,9 @@ public final class SwiftBuildSystemMessageHandler { subscript(key: SwiftBuildMessage.LocationContext2) -> Value? { get { if let taskSignature = key.taskSignature { - return self.storage[taskSignature] + return self.taskSignatureBuffer[taskSignature] } else if let targetID = key.targetID { - return self.targetIDDataBuffer[targetID] + return self.targetIDBuffer[targetID] } return nil @@ -290,25 +291,26 @@ public final class SwiftBuildSystemMessageHandler { set { if let taskSignature = key.taskSignature { - self.storage[taskSignature] = newValue + self.taskSignatureBuffer[taskSignature] = newValue } else if let targetID = key.targetID { - self.targetIDDataBuffer[targetID] = newValue + self.targetIDBuffer[targetID] = newValue } } } subscript(task: SwiftBuildMessage.TaskStartedInfo) -> Value? { get { - guard let result = self.storage[task.taskSignature] else { + guard let result = self.taskSignatureBuffer[task.taskSignature] else { // Default to checking targetID and taskID. - if let result = self.taskIDDataBuffer[task.taskID] { + if let result = self.taskIDBuffer[task.taskID] { return result } else if let targetID = task.targetID, - let result = self.targetIDDataBuffer[targetID] { + let result = self.targetIDBuffer[targetID] { return result } - return nil + // Return global buffer if none of the above are found. + return self.globalBuffer } return result @@ -317,7 +319,7 @@ public final class SwiftBuildSystemMessageHandler { init(dictionaryLiteral elements: (String, Data)...) { for (key, value) in elements { - self.storage[key] = value + self.taskSignatureBuffer[key] = value } } } @@ -372,11 +374,11 @@ public final class SwiftBuildSystemMessageHandler { if let taskID = info.locationContext.taskID, let taskSignature = self.taskSignature(for: taskID) { self.taskDataBuffer[taskSignature, default: .init()].append(info.data) - self.taskDataBuffer[info.locationContext, default: .init()].append(info.data) - } else { - self.taskDataBuffer[info.locationContext, default: .init()].append(info.data) } + // TODO bp: extra tracking for taskIDs/targetIDs/possible global buffers + self.taskDataBuffer[info.locationContext, default: .init()].append(info.data) + return } From 424492f8a8bdb3ed397a7299b7ec58c58fb1686c Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Thu, 27 Nov 2025 14:32:13 -0500 Subject: [PATCH 13/24] Modify emission of command line display strings If there is no command line display strings available for a failed task, we should demote this message to info-level to avoid cluttering the output stream with messages that may not be incredibly helpful. To consider here: if this is the only error, we should be able to expose this as an error and perhaps omit "". --- .../SwiftBuildSupport/SwiftBuildSystem.swift | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index 5598310fcb4..98c3b498241 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -206,6 +206,7 @@ public final class SwiftBuildSystemMessageHandler { var serializedDiagnosticPathsByTargetName: [String: [Basics.AbsolutePath]] = [:] private var unprocessedDiagnostics: [SwiftBuildMessage.DiagnosticInfo] = [] + private var failedTasks: [SwiftBuildMessage.TaskCompleteInfo] = [] public init( observabilityScope: ObservabilityScope, @@ -224,6 +225,7 @@ public final class SwiftBuildSystemMessageHandler { struct BuildState { private var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:] private var activeTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskStartedInfo] = [:] + private var completedTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo] = [:] private var taskDataBuffer: TaskDataBuffer = [:] private var taskIDToSignature: [Int: String] = [:] var collectedBacktraceFrames = SWBBuildOperationCollectedBacktraceFrames() @@ -333,10 +335,14 @@ public final class SwiftBuildSystemMessageHandler { } mutating func completed(task: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo) throws -> SwiftBuild.SwiftBuildMessage.TaskStartedInfo { - guard let task = activeTasks[task.taskID] else { + guard let startedTaskInfo = activeTasks[task.taskID] else { throw Diagnostics.fatalError } - return task + if completedTasks[task.taskID] != nil { + throw Diagnostics.fatalError + } + self.completedTasks[task.taskID] = task + return startedTaskInfo } mutating func started(target: SwiftBuild.SwiftBuildMessage.TargetStartedInfo) throws { @@ -443,6 +449,28 @@ public final class SwiftBuildSystemMessageHandler { self.tasksEmitted.insert(info.taskSignature) } + private func handleFailedTask( + _ info: SwiftBuildMessage.TaskCompleteInfo, + _ startedInfo: SwiftBuildMessage.TaskStartedInfo + ) { + guard info.result != .success else { + return + } + + // Track failed tasks. + self.failedTasks.append(info) + + let message = "\(startedInfo.ruleInfo) failed with a nonzero exit code." + // If we have the command line display string available, then we should continue to emit + // this as an error. Otherwise, this doesn't give enough information to the user for it + // to be useful so we can demote it to an info-level log. + if let cmdLineDisplayStr = startedInfo.commandLineDisplayString { + self.observabilityScope.emit(severity: .error, message: "\(message) Command line: \(cmdLineDisplayStr)") + } else { + self.observabilityScope.emit(severity: .info, message: message) + } + } + func emitEvent(_ message: SwiftBuild.SwiftBuildMessage, _ buildSystem: SwiftBuildSystem) throws { guard !self.logLevel.isQuiet else { return } switch message { @@ -487,12 +515,13 @@ public final class SwiftBuildSystemMessageHandler { case .taskComplete(let info): let startedInfo = try buildState.completed(task: info) - // If we've captured the compiler output with formatted diagnostics, emit them. + // Handler for failed tasks, if applicable. + handleFailedTask(info, startedInfo) + + // If we've captured the compiler output with formatted diagnostics keyed by + // this task's signature, emit them. emitDiagnosticCompilerOutput(startedInfo) - if info.result != .success { - self.observabilityScope.emit(severity: .error, message: "\(startedInfo.ruleInfo) failed with a nonzero exit code. Command line: \(startedInfo.commandLineDisplayString ?? "")") - } let targetInfo = try buildState.target(for: startedInfo) buildSystem.delegate?.buildSystem(buildSystem, didFinishCommand: BuildSystemCommand(startedInfo, targetInfo: targetInfo)) if let targetName = targetInfo?.targetName { From 4074c597497634362696b95bdaf473f5d2368bee Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Thu, 27 Nov 2025 16:49:17 -0500 Subject: [PATCH 14/24] cleanup; stronger assertions for redundant task output --- .../SwiftBuildSupport/SwiftBuildSystem.swift | 122 +++++++++++------- 1 file changed, 78 insertions(+), 44 deletions(-) diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index 98c3b498241..7478f5dc32a 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -200,13 +200,18 @@ public final class SwiftBuildSystemMessageHandler { private let observabilityScope: ObservabilityScope private let logLevel: Basics.Diagnostic.Severity private var buildState: BuildState = .init() - private var tasksEmitted: Set = [] let progressAnimation: ProgressAnimationProtocol var serializedDiagnosticPathsByTargetName: [String: [Basics.AbsolutePath]] = [:] + /// Tracks the diagnostics that we have not yet emitted. private var unprocessedDiagnostics: [SwiftBuildMessage.DiagnosticInfo] = [] - private var failedTasks: [SwiftBuildMessage.TaskCompleteInfo] = [] + /// Tracks the task IDs for failed tasks. + private var failedTasks: [Int] = [] + /// Tracks the tasks by their signature for which we have already emitted output. + private var tasksEmitted: Set = [] + /// Tracks the tasks by their ID for which we have already emitted output. + private var taskIDsEmitted: Set = [] public init( observabilityScope: ObservabilityScope, @@ -226,29 +231,28 @@ public final class SwiftBuildSystemMessageHandler { private var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:] private var activeTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskStartedInfo] = [:] private var completedTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo] = [:] - private var taskDataBuffer: TaskDataBuffer = [:] + private var taskDataBuffer: TaskDataBuffer = .init() private var taskIDToSignature: [Int: String] = [:] var collectedBacktraceFrames = SWBBuildOperationCollectedBacktraceFrames() - struct TaskDataBuffer: ExpressibleByDictionaryLiteral { - typealias Key = String - typealias Value = Data - - private var taskSignatureBuffer: [Key: Value] = [:] + /// Rich model to store data buffers for a given `SwiftBuildMessage.LocationContext` or + /// a `SwiftBuildMessage.LocationContext2`. + struct TaskDataBuffer { + private var taskSignatureBuffer: [String: Data] = [:] private var taskIDBuffer: [Int: Data] = [:] private var targetIDBuffer: [Int: Data] = [:] private var globalBuffer: Data = Data() - subscript(key: String) -> Value? { + subscript(key: String) -> Data? { self.taskSignatureBuffer[key] } - subscript(key: String, default defaultValue: Value) -> Value { + subscript(key: String, default defaultValue: Data) -> Data { get { self.taskSignatureBuffer[key] ?? defaultValue } set { self.taskSignatureBuffer[key] = newValue } } - subscript(key: SwiftBuildMessage.LocationContext, default defaultValue: Value) -> Value { + subscript(key: SwiftBuildMessage.LocationContext, default defaultValue: Data) -> Data { get { // Check each ID kind and try to fetch the associated buffer. // If unable to get a non-nil result, then follow through to the @@ -280,7 +284,7 @@ public final class SwiftBuildSystemMessageHandler { } } - subscript(key: SwiftBuildMessage.LocationContext2) -> Value? { + subscript(key: SwiftBuildMessage.LocationContext2) -> Data? { get { if let taskSignature = key.taskSignature { return self.taskSignatureBuffer[taskSignature] @@ -300,7 +304,7 @@ public final class SwiftBuildSystemMessageHandler { } } - subscript(task: SwiftBuildMessage.TaskStartedInfo) -> Value? { + subscript(task: SwiftBuildMessage.TaskStartedInfo) -> Data? { get { guard let result = self.taskSignatureBuffer[task.taskSignature] else { // Default to checking targetID and taskID. @@ -318,12 +322,6 @@ public final class SwiftBuildSystemMessageHandler { return result } } - - init(dictionaryLiteral elements: (String, Data)...) { - for (key, value) in elements { - self.taskSignatureBuffer[key] = value - } - } } mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws { @@ -382,7 +380,6 @@ public final class SwiftBuildSystemMessageHandler { self.taskDataBuffer[taskSignature, default: .init()].append(info.data) } - // TODO bp: extra tracking for taskIDs/targetIDs/possible global buffers self.taskDataBuffer[info.locationContext, default: .init()].append(info.data) return @@ -397,8 +394,6 @@ public final class SwiftBuildSystemMessageHandler { return taskDataBuffer[task] } - // todo bp "-fno-color-diagnostics", - return data } } @@ -428,10 +423,13 @@ public final class SwiftBuildSystemMessageHandler { } private func emitDiagnosticCompilerOutput(_ info: SwiftBuildMessage.TaskStartedInfo) { - // Don't redundantly emit tasks. + // Don't redundantly emit task output. guard !self.tasksEmitted.contains(info.taskSignature) else { return } + guard hasUnprocessedDiagnostics(info) else { + return + } // Assure we have a data buffer to decode. guard let buffer = buildState.dataBuffer(for: info) else { return @@ -441,34 +439,83 @@ public final class SwiftBuildSystemMessageHandler { let decodedOutput = String(decoding: buffer, as: UTF8.self) // Emit message. - // Note: This is a temporary workaround until we can re-architect - // how we'd like to format and handle diagnostic output. observabilityScope.print(message: decodedOutput) // Record that we've emitted the output for a given task signature. self.tasksEmitted.insert(info.taskSignature) + self.taskIDsEmitted.insert(info.taskID) + } + + private func hasUnprocessedDiagnostics(_ info: SwiftBuildMessage.TaskStartedInfo) -> Bool { + let diagnosticTaskSignature = unprocessedDiagnostics.compactMap(\.locationContext2.taskSignature) + let diagnosticTaskIDs = unprocessedDiagnostics.compactMap(\.locationContext.taskID) + + return diagnosticTaskSignature.contains(info.taskSignature) || diagnosticTaskIDs.contains(info.taskID) } - private func handleFailedTask( + private func handleTaskOutput( + _ info: SwiftBuildMessage.TaskCompleteInfo, + _ startedInfo: SwiftBuildMessage.TaskStartedInfo, + _ enableTaskBacktraces: Bool + ) throws { + if info.result != .success { + emitFailedTaskOutput(info, startedInfo) + } else if let data = buildState.dataBuffer(for: startedInfo), !tasksEmitted.contains(startedInfo.taskSignature) { + let decodedOutput = String(decoding: data, as: UTF8.self) + if !decodedOutput.isEmpty { + observabilityScope.emit(info: decodedOutput) + } + } + + // Handle task backtraces, if applicable. + if enableTaskBacktraces { + if let id = SWBBuildOperationBacktraceFrame.Identifier(taskSignatureData: Data(startedInfo.taskSignature.utf8)), + let backtrace = SWBTaskBacktrace(from: id, collectedFrames: buildState.collectedBacktraceFrames) { + let formattedBacktrace = backtrace.renderTextualRepresentation() + if !formattedBacktrace.isEmpty { + self.observabilityScope.emit(info: "Task backtrace:\n\(formattedBacktrace)") + } + } + } + } + + private func emitFailedTaskOutput( _ info: SwiftBuildMessage.TaskCompleteInfo, _ startedInfo: SwiftBuildMessage.TaskStartedInfo ) { + // Assure that the task has failed. guard info.result != .success else { return } + // Don't redundantly emit task output. + guard !tasksEmitted.contains(startedInfo.taskSignature) else { + return + } // Track failed tasks. - self.failedTasks.append(info) + self.failedTasks.append(info.taskID) + + // Check for existing diagnostics with matching taskID/taskSignature. + // If we've captured the compiler output with formatted diagnostics keyed by + // this task's signature, emit them. + // Note that this is a workaround instead of emitting directly from a `DiagnosticInfo` + // message, as here we receive the formatted code snippet directly from the compiler. + emitDiagnosticCompilerOutput(startedInfo) let message = "\(startedInfo.ruleInfo) failed with a nonzero exit code." - // If we have the command line display string available, then we should continue to emit - // this as an error. Otherwise, this doesn't give enough information to the user for it - // to be useful so we can demote it to an info-level log. + // If we have the command line display string available, then we + // should continue to emit this as an error. Otherwise, this doesn't + // give enough information to the user for it to be useful so we can + // demote it to an info-level log. if let cmdLineDisplayStr = startedInfo.commandLineDisplayString { self.observabilityScope.emit(severity: .error, message: "\(message) Command line: \(cmdLineDisplayStr)") } else { self.observabilityScope.emit(severity: .info, message: message) } + + // Track that we have emitted output for this task. + tasksEmitted.insert(startedInfo.taskSignature) + taskIDsEmitted.insert(info.taskID) } func emitEvent(_ message: SwiftBuild.SwiftBuildMessage, _ buildSystem: SwiftBuildSystem) throws { @@ -516,11 +563,7 @@ public final class SwiftBuildSystemMessageHandler { let startedInfo = try buildState.completed(task: info) // Handler for failed tasks, if applicable. - handleFailedTask(info, startedInfo) - - // If we've captured the compiler output with formatted diagnostics keyed by - // this task's signature, emit them. - emitDiagnosticCompilerOutput(startedInfo) + try handleTaskOutput(info, startedInfo, buildSystem.enableTaskBacktraces) let targetInfo = try buildState.target(for: startedInfo) buildSystem.delegate?.buildSystem(buildSystem, didFinishCommand: BuildSystemCommand(startedInfo, targetInfo: targetInfo)) @@ -529,15 +572,6 @@ public final class SwiftBuildSystemMessageHandler { try Basics.AbsolutePath(validating: $0.pathString) }) } - if buildSystem.enableTaskBacktraces { - if let id = SWBBuildOperationBacktraceFrame.Identifier(taskSignatureData: Data(startedInfo.taskSignature.utf8)), - let backtrace = SWBTaskBacktrace(from: id, collectedFrames: buildState.collectedBacktraceFrames) { - let formattedBacktrace = backtrace.renderTextualRepresentation() - if !formattedBacktrace.isEmpty { - self.observabilityScope.emit(info: "Task backtrace:\n\(formattedBacktrace)") - } - } - } case .targetStarted(let info): try buildState.started(target: info) case .backtraceFrame(let info): From 05ee0430e2c579c08bfef8aaf45b6d63eacd03f0 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Tue, 2 Dec 2025 12:16:48 -0500 Subject: [PATCH 15/24] Fix protocol adherence errors --- Sources/Basics/Observability.swift | 12 ++++++------ .../SwiftCommandObservabilityHandler.swift | 2 +- Sources/_InternalTestSupport/Observability.swift | 4 ++++ Tests/BasicsTests/ObservabilitySystemTests.swift | 4 ++++ 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Sources/Basics/Observability.swift b/Sources/Basics/Observability.swift index 24d965e5002..e254d2cc5b2 100644 --- a/Sources/Basics/Observability.swift +++ b/Sources/Basics/Observability.swift @@ -57,8 +57,8 @@ public class ObservabilitySystem { self.underlying(scope, diagnostic) } - func print(message: String) { - self.diagnosticsHandler.print(message: message) + func printToOutput(message: String) { + self.diagnosticsHandler.printToOutput(message: message) } } @@ -133,7 +133,7 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol, Sendable, Cus } public func print(message: String) { - self.diagnosticsHandler.print(message: message) + self.diagnosticsHandler.printToOutput(message: message) } // DiagnosticsEmitterProtocol @@ -158,8 +158,8 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol, Sendable, Cus self.underlying.handleDiagnostic(scope: scope, diagnostic: diagnostic) } - public func print(message: String) { - self.underlying.print(message: message) + public func printToOutput(message: String) { + self.underlying.printToOutput(message: message) } var errorsReported: Bool { @@ -173,7 +173,7 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol, Sendable, Cus public protocol DiagnosticsHandler: Sendable { func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic) - func print(message: String) + func printToOutput(message: String) } /// Helper protocol to share default behavior. diff --git a/Sources/CoreCommands/SwiftCommandObservabilityHandler.swift b/Sources/CoreCommands/SwiftCommandObservabilityHandler.swift index ffcbfbbae6a..f726d9d317e 100644 --- a/Sources/CoreCommands/SwiftCommandObservabilityHandler.swift +++ b/Sources/CoreCommands/SwiftCommandObservabilityHandler.swift @@ -116,7 +116,7 @@ public struct SwiftCommandObservabilityHandler: ObservabilityHandlerProvider { } } - func print(message: String) { + func printToOutput(message: String) { self.queue.async(group: self.sync) { self.write(message) } diff --git a/Sources/_InternalTestSupport/Observability.swift b/Sources/_InternalTestSupport/Observability.swift index 2d934abf8dc..ae68e3df7ad 100644 --- a/Sources/_InternalTestSupport/Observability.swift +++ b/Sources/_InternalTestSupport/Observability.swift @@ -75,6 +75,10 @@ public struct TestingObservability { self.diagnostics.append(diagnostic) } + func printToOutput(message: String) { + print(message) + } + var hasErrors: Bool { self.diagnostics.get().hasErrors } diff --git a/Tests/BasicsTests/ObservabilitySystemTests.swift b/Tests/BasicsTests/ObservabilitySystemTests.swift index c7ae9a1b582..51a50fa64ed 100644 --- a/Tests/BasicsTests/ObservabilitySystemTests.swift +++ b/Tests/BasicsTests/ObservabilitySystemTests.swift @@ -309,6 +309,10 @@ struct ObservabilitySystemTest { func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic) { self._diagnostics.append(diagnostic) } + + func printToOutput(message: String) { + print(message) + } } } From 252286b633f5d5b66430f613963e6b6647ddec89 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Tue, 2 Dec 2025 16:52:02 -0500 Subject: [PATCH 16/24] Create test suite for SwiftBuildSystemMessageHandler --- .../SwiftBuildSupport/SwiftBuildSystem.swift | 53 +- .../SwiftBuildSystemMessageHandlerTests.swift | 569 ++++++++++++++++++ 2 files changed, 609 insertions(+), 13 deletions(-) create mode 100644 Tests/SwiftBuildSupportTests/SwiftBuildSystemMessageHandlerTests.swift diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index 7478f5dc32a..3858c75e6c5 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -200,6 +200,10 @@ public final class SwiftBuildSystemMessageHandler { private let observabilityScope: ObservabilityScope private let logLevel: Basics.Diagnostic.Severity private var buildState: BuildState = .init() + private let enableBacktraces: Bool + private let buildDelegate: SPMBuildCore.BuildSystemDelegate? + + public typealias BuildSystemCallback = (SwiftBuildSystem) -> Void let progressAnimation: ProgressAnimationProtocol var serializedDiagnosticPathsByTargetName: [String: [Basics.AbsolutePath]] = [:] @@ -216,7 +220,9 @@ public final class SwiftBuildSystemMessageHandler { public init( observabilityScope: ObservabilityScope, outputStream: OutputByteStream, - logLevel: Basics.Diagnostic.Severity + logLevel: Basics.Diagnostic.Severity, + enableBacktraces: Bool = false, + buildDelegate: SPMBuildCore.BuildSystemDelegate? = nil ) { self.observabilityScope = observabilityScope @@ -225,6 +231,8 @@ public final class SwiftBuildSystemMessageHandler { stream: outputStream, verbose: self.logLevel.isVerbose ) + self.enableBacktraces = enableBacktraces + self.buildDelegate = buildDelegate } struct BuildState { @@ -518,15 +526,22 @@ public final class SwiftBuildSystemMessageHandler { taskIDsEmitted.insert(info.taskID) } - func emitEvent(_ message: SwiftBuild.SwiftBuildMessage, _ buildSystem: SwiftBuildSystem) throws { - guard !self.logLevel.isQuiet else { return } + public func emitEvent(_ message: SwiftBuild.SwiftBuildMessage) throws -> BuildSystemCallback? { + var callback: BuildSystemCallback? = nil + + guard !self.logLevel.isQuiet else { return callback } + switch message { case .buildCompleted(let info): progressAnimation.complete(success: info.result == .ok) if info.result == .cancelled { - buildSystem.delegate?.buildSystemDidCancel(buildSystem) + callback = { [weak self] buildSystem in + self?.buildDelegate?.buildSystemDidCancel(buildSystem) + } } else { - buildSystem.delegate?.buildSystem(buildSystem, didFinishWithResult: info.result == .ok) + callback = { [weak self] buildSystem in + self?.buildDelegate?.buildSystem(buildSystem, didFinishWithResult: info.result == .ok) + } } case .didUpdateProgress(let progressInfo): var step = Int(progressInfo.percentComplete) @@ -537,7 +552,9 @@ public final class SwiftBuildSystemMessageHandler { "\(progressInfo.message)" } progressAnimation.update(step: step, total: 100, text: message) - buildSystem.delegate?.buildSystem(buildSystem, didUpdateTaskProgress: message) + callback = { [weak self] buildSystem in + self?.buildDelegate?.buildSystem(buildSystem, didUpdateTaskProgress: message) + } case .diagnostic(let info): if info.appendToOutputStream { emitInfoAsDiagnostic(info: info) @@ -557,16 +574,20 @@ public final class SwiftBuildSystemMessageHandler { } let targetInfo = try buildState.target(for: info) - buildSystem.delegate?.buildSystem(buildSystem, willStartCommand: BuildSystemCommand(info, targetInfo: targetInfo)) - buildSystem.delegate?.buildSystem(buildSystem, didStartCommand: BuildSystemCommand(info, targetInfo: targetInfo)) + callback = { [weak self] buildSystem in + self?.buildDelegate?.buildSystem(buildSystem, willStartCommand: BuildSystemCommand(info, targetInfo: targetInfo)) + self?.buildDelegate?.buildSystem(buildSystem, didStartCommand: BuildSystemCommand(info, targetInfo: targetInfo)) + } case .taskComplete(let info): let startedInfo = try buildState.completed(task: info) // Handler for failed tasks, if applicable. - try handleTaskOutput(info, startedInfo, buildSystem.enableTaskBacktraces) + try handleTaskOutput(info, startedInfo, self.enableBacktraces) let targetInfo = try buildState.target(for: startedInfo) - buildSystem.delegate?.buildSystem(buildSystem, didFinishCommand: BuildSystemCommand(startedInfo, targetInfo: targetInfo)) + callback = { [weak self] buildSystem in + self?.buildDelegate?.buildSystem(buildSystem, didFinishCommand: BuildSystemCommand(startedInfo, targetInfo: targetInfo)) + } if let targetName = targetInfo?.targetName { try serializedDiagnosticPathsByTargetName[targetName, default: []].append(contentsOf: startedInfo.serializedDiagnosticsPaths.compactMap { try Basics.AbsolutePath(validating: $0.pathString) @@ -575,7 +596,7 @@ public final class SwiftBuildSystemMessageHandler { case .targetStarted(let info): try buildState.started(target: info) case .backtraceFrame(let info): - if buildSystem.enableTaskBacktraces { + if self.enableBacktraces { buildState.collectedBacktraceFrames.add(frame: info) } case .planningOperationStarted, .planningOperationCompleted, .reportBuildDescription, .reportPathMap, .preparedForIndex, .buildStarted, .preparationComplete, .targetUpToDate, .targetComplete, .taskUpToDate: @@ -587,6 +608,8 @@ public final class SwiftBuildSystemMessageHandler { @unknown default: break } + + return callback } } @@ -960,7 +983,9 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { let buildMessageHandler = SwiftBuildSystemMessageHandler( observabilityScope: self.observabilityScope, outputStream: self.outputStream, - logLevel: self.logLevel + logLevel: self.logLevel, + enableBacktraces: self.enableTaskBacktraces, + buildDelegate: self.delegate ) do { @@ -1016,7 +1041,9 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { } buildDescriptionID = SWBBuildDescriptionID(info.buildDescriptionID) } - try buildMessageHandler.emitEvent(event, self) + if let delegateCallback = try buildMessageHandler.emitEvent(event) { + delegateCallback(self) + } } await operation.waitForCompletion() diff --git a/Tests/SwiftBuildSupportTests/SwiftBuildSystemMessageHandlerTests.swift b/Tests/SwiftBuildSupportTests/SwiftBuildSystemMessageHandlerTests.swift new file mode 100644 index 00000000000..3eb4d561046 --- /dev/null +++ b/Tests/SwiftBuildSupportTests/SwiftBuildSystemMessageHandlerTests.swift @@ -0,0 +1,569 @@ +//===----------------------------------------------------------------------===// +// +// 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 Basics +import Foundation +import struct SWBUtil.AbsolutePath +import Testing +import SwiftBuild +import SwiftBuildSupport + +import TSCBasic +import _InternalTestSupport + +@Suite +struct SwiftBuildSystemMessageHandlerTests { + private func createMessageHandler( + _ logLevel: Basics.Diagnostic.Severity = .warning + ) -> (handler: SwiftBuildSystemMessageHandler, outputStream: BufferedOutputByteStream, observability: TestingObservability) { + let observability = ObservabilitySystem.makeForTesting() + let outputStream = BufferedOutputByteStream() + + let handler = SwiftBuildSystemMessageHandler( + observabilityScope: observability.topScope, + outputStream: outputStream, + logLevel: logLevel + ) + + return (handler, outputStream, observability) + } + + @Test + func testNoDiagnosticsReported() throws { + let (messageHandler, outputStream, observability) = createMessageHandler() + + let events: [SwiftBuildMessage] = [ + .taskStarted(.mock()), + .taskComplete(.mock()), + .buildCompleted(.mock()) + ] + + for event in events { + _ = try messageHandler.emitEvent(event) + } + + // Check output stream + let output = outputStream.bytes.description + #expect(!output.contains("error")) + + // Check observability diagnostics + expectNoDiagnostics(observability.diagnostics) + } + + @Test + func testSimpleDiagnosticReported() throws { + let (messageHandler, outputStream, observability) = createMessageHandler() + + let events: [SwiftBuildMessage] = [ + .taskStarted(.mock( + taskID: 1, + taskSignature: "mock-diagnostic" + )), + .diagnostic(.mock( + kind: .error, + locationContext: .mock( + taskID: 1, + ), + locationContext2: .mock( + taskSignature: "mock-diagnostic" + ), + message: "Simple diagnostic", + appendToOutputStream: true) + ), + .taskComplete(.mock(taskID: 1)) // Handler only emits when a task is completed. + ] + + for event in events { + _ = try messageHandler.emitEvent(event) + } + + #expect(observability.hasErrorDiagnostics) + + try expectDiagnostics(observability.diagnostics) { result in + result.check(diagnostic: "Simple diagnostic", severity: .error) + } + } +} + + +extension SwiftBuildMessage.TaskStartedInfo { + package static func mock( + taskID: Int = 1, + targetID: Int? = nil, + taskSignature: String = "taskStartedSignature", + parentTaskID: Int? = nil, + ruleInfo: String = "ruleInfo", + interestingPath: AbsolutePath? = nil, + commandLineDisplayString: String? = nil, + executionDescription: String = "", + serializedDiagnosticsPaths: [AbsolutePath] = [] + ) -> SwiftBuildMessage.TaskStartedInfo { + // Use JSON encoding/decoding as a workaround for lack of public initializer. Have it match the custom CodingKeys as described in + struct MockData: Encodable { + let id: Int + let targetID: Int? + let signature: String + let parentID: Int? + let ruleInfo: String + let interestingPath: String? + let commandLineDisplayString: String? + let executionDescription: String + let serializedDiagnosticsPaths: [String] + } + + let mockData = MockData( + id: taskID, + targetID: targetID, + signature: taskSignature, + parentID: parentTaskID, + ruleInfo: ruleInfo, + interestingPath: interestingPath?.path.str, + commandLineDisplayString: commandLineDisplayString, + executionDescription: executionDescription, + serializedDiagnosticsPaths: serializedDiagnosticsPaths.map { $0.path.str } + ) + + let data = try! JSONEncoder().encode(mockData) + return try! JSONDecoder().decode(SwiftBuildMessage.TaskStartedInfo.self, from: data) + } +} + +// MARK: - DiagnosticInfo + +extension SwiftBuildMessage.DiagnosticInfo { + package static func mock( + kind: Kind = .warning, + location: Location = .unknown, + locationContext: SwiftBuildMessage.LocationContext = .mock(), + locationContext2: SwiftBuildMessage.LocationContext2 = .mock(), + component: Component = .default, + message: String = "Test diagnostic message", + optionName: String? = nil, + appendToOutputStream: Bool = false, + childDiagnostics: [SwiftBuildMessage.DiagnosticInfo] = [], + sourceRanges: [SourceRange] = [], + fixIts: [FixIt] = [] + ) -> SwiftBuildMessage.DiagnosticInfo { + struct MockData: Encodable { + let kind: Kind + let location: Location + let locationContext: SwiftBuildMessage.LocationContext + let locationContext2: SwiftBuildMessage.LocationContext2 + let component: Component + let message: String + let optionName: String? + let appendToOutputStream: Bool + let childDiagnostics: [SwiftBuildMessage.DiagnosticInfo] + let sourceRanges: [SourceRange] + let fixIts: [FixIt] + } + + let mockData = MockData( + kind: kind, + location: location, + locationContext: locationContext, + locationContext2: locationContext2, + component: component, + message: message, + optionName: optionName, + appendToOutputStream: appendToOutputStream, + childDiagnostics: childDiagnostics, + sourceRanges: sourceRanges, + fixIts: fixIts + ) + + let data = try! JSONEncoder().encode(mockData) + return try! JSONDecoder().decode(SwiftBuildMessage.DiagnosticInfo.self, from: data) + } +} + +extension SwiftBuildMessage.LocationContext { + package static func mock( + taskID: Int = 1, + targetID: Int = 1 + ) -> Self { + return .task(taskID: taskID, targetID: targetID) + } + + package static func mockTarget(targetID: Int = 1) -> Self { + return .target(targetID: targetID) + } + + package static func mockGlobalTask(taskID: Int = 1) -> Self { + return .globalTask(taskID: taskID) + } + + package static func mockGlobal() -> Self { + return .global + } +} + +extension SwiftBuildMessage.DiagnosticInfo.Location { + package static func mockPath( + _ path: String = "/mock/file.swift", + line: Int = 10, + column: Int? = 5 + ) -> Self { + return .path(path, fileLocation: .textual(line: line, column: column)) + } + + package static func mockPathOnly(_ path: String = "/mock/file.swift") -> Self { + return .path(path, fileLocation: nil) + } + + package static func mockObject( + path: String = "/mock/file.swift", + identifier: String = "mock-identifier" + ) -> Self { + return .path(path, fileLocation: .object(identifier: identifier)) + } + + package static func mockBuildSettings(names: [String] = ["MOCK_SETTING"]) -> Self { + return .buildSettings(names: names) + } + + package static func mockBuildFiles( + buildFileGUIDs: [String] = ["BUILD_FILE_1"], + buildPhaseGUIDs: [String] = ["BUILD_PHASE_1"], + targetGUID: String = "TARGET_GUID" + ) -> Self { + let buildFiles = zip(buildFileGUIDs, buildPhaseGUIDs).map { + SwiftBuildMessage.DiagnosticInfo.Location.BuildFileAndPhase.mock( + buildFileGUID: $0, + buildPhaseGUID: $1 + ) + } + return .buildFiles(buildFiles, targetGUID: targetGUID) + } +} + +extension SwiftBuildMessage.DiagnosticInfo.Location.BuildFileAndPhase { + package static func mock( + buildFileGUID: String = "BUILD_FILE_GUID", + buildPhaseGUID: String = "BUILD_PHASE_GUID" + ) -> Self { + struct MockData: Encodable { + let buildFileGUID: String + let buildPhaseGUID: String + } + + let mockData = MockData( + buildFileGUID: buildFileGUID, + buildPhaseGUID: buildPhaseGUID + ) + + let data = try! JSONEncoder().encode(mockData) + return try! JSONDecoder().decode(SwiftBuildMessage.DiagnosticInfo.Location.BuildFileAndPhase.self, from: data) + } +} + +extension SwiftBuildMessage.DiagnosticInfo.SourceRange { + package static func mock( + path: String = "/mock/file.swift", + startLine: Int = 10, + startColumn: Int = 5, + endLine: Int = 10, + endColumn: Int = 20 + ) -> Self { + struct MockData: Encodable { + let path: String + let startLine: Int + let startColumn: Int + let endLine: Int + let endColumn: Int + } + + let mockData = MockData( + path: path, + startLine: startLine, + startColumn: startColumn, + endLine: endLine, + endColumn: endColumn + ) + + let data = try! JSONEncoder().encode(mockData) + return try! JSONDecoder().decode(SwiftBuildMessage.DiagnosticInfo.SourceRange.self, from: data) + } +} + +extension SwiftBuildMessage.DiagnosticInfo.FixIt { + package static func mock( + sourceRange: SwiftBuildMessage.DiagnosticInfo.SourceRange = .mock(), + textToInsert: String = "fix text" + ) -> Self { + struct MockData: Encodable { + let sourceRange: SwiftBuildMessage.DiagnosticInfo.SourceRange + let textToInsert: String + } + + let mockData = MockData( + sourceRange: sourceRange, + textToInsert: textToInsert + ) + + let data = try! JSONEncoder().encode(mockData) + return try! JSONDecoder().decode(SwiftBuildMessage.DiagnosticInfo.FixIt.self, from: data) + } +} + +extension SwiftBuildMessage.LocationContext2 { + package static func mock( + targetID: Int? = nil, + taskSignature: String? = nil + ) -> Self { + struct MockData: Encodable { + let targetID: Int? + let taskSignature: String? + } + + let mockData = MockData( + targetID: targetID, + taskSignature: taskSignature + ) + + let data = try! JSONEncoder().encode(mockData) + return try! JSONDecoder().decode(SwiftBuildMessage.LocationContext2.self, from: data) + } +} + +// MARK: - TaskCompleteInfo + +extension SwiftBuildMessage.TaskCompleteInfo { + package static func mock( + taskID: Int = 1, + taskSignature: String = "mock-task-signature", + result: Result = .success, + signalled: Bool = false, + metrics: Metrics? = nil + ) -> SwiftBuildMessage.TaskCompleteInfo { + struct MockData: Encodable { + let id: Int + let signature: String + let result: Result + let signalled: Bool + let metrics: Metrics? + } + + let mockData = MockData( + id: taskID, + signature: taskSignature, + result: result, + signalled: signalled, + metrics: metrics + ) + + let data = try! JSONEncoder().encode(mockData) + return try! JSONDecoder().decode(SwiftBuildMessage.TaskCompleteInfo.self, from: data) + } +} + +extension SwiftBuildMessage.TaskCompleteInfo.Metrics { + package static func mock( + utime: UInt64 = 100, + stime: UInt64 = 50, + maxRSS: UInt64 = 1024000, + wcStartTime: UInt64 = 1000000, + wcDuration: UInt64 = 150 + ) -> Self { + struct MockData: Encodable { + let utime: UInt64 + let stime: UInt64 + let maxRSS: UInt64 + let wcStartTime: UInt64 + let wcDuration: UInt64 + } + + let mockData = MockData( + utime: utime, + stime: stime, + maxRSS: maxRSS, + wcStartTime: wcStartTime, + wcDuration: wcDuration + ) + + let data = try! JSONEncoder().encode(mockData) + return try! JSONDecoder().decode(SwiftBuildMessage.TaskCompleteInfo.Metrics.self, from: data) + } +} + +// MARK: - TargetStartedInfo + +extension SwiftBuildMessage.TargetStartedInfo { + package static func mock( + targetID: Int = 1, + targetGUID: String = "MOCK_TARGET_GUID", + targetName: String = "MockTarget", + type: Kind = .native, + projectName: String = "MockProject", + projectPath: String = "/mock/project.xcodeproj", + projectIsPackage: Bool = false, + projectNameIsUniqueInWorkspace: Bool = true, + configurationName: String = "Debug", + configurationIsDefault: Bool = true, + sdkroot: String? = "macosx" + ) -> SwiftBuildMessage.TargetStartedInfo { + struct MockData: Encodable { + let id: Int + let guid: String + let name: String + let type: Kind + let projectName: String + let projectPath: String + let projectIsPackage: Bool + let projectNameIsUniqueInWorkspace: Bool + let configurationName: String + let configurationIsDefault: Bool + let sdkroot: String? + } + + let mockData = MockData( + id: targetID, + guid: targetGUID, + name: targetName, + type: type, + projectName: projectName, + projectPath: projectPath, + projectIsPackage: projectIsPackage, + projectNameIsUniqueInWorkspace: projectNameIsUniqueInWorkspace, + configurationName: configurationName, + configurationIsDefault: configurationIsDefault, + sdkroot: sdkroot + ) + + let data = try! JSONEncoder().encode(mockData) + return try! JSONDecoder().decode(SwiftBuildMessage.TargetStartedInfo.self, from: data) + } +} + +// MARK: - BuildStartedInfo + +extension SwiftBuildMessage.BuildStartedInfo { + package static func mock( + baseDirectory: String = "/mock/base", + derivedDataPath: String? = "/mock/derived-data" + ) -> SwiftBuildMessage.BuildStartedInfo { + struct MockData: Encodable { + let baseDirectory: String + let derivedDataPath: String? + } + + let mockData = MockData( + baseDirectory: baseDirectory, + derivedDataPath: derivedDataPath + ) + + let data = try! JSONEncoder().encode(mockData) + return try! JSONDecoder().decode(SwiftBuildMessage.BuildStartedInfo.self, from: data) + } +} + +// MARK: - BuildCompletedInfo + +extension SwiftBuildMessage.BuildCompletedInfo { + package static func mock( + result: Result = .ok, + metrics: SwiftBuildMessage.BuildOperationMetrics? = nil + ) -> SwiftBuildMessage.BuildCompletedInfo { + struct MockData: Encodable { + let result: Result + let metrics: SwiftBuildMessage.BuildOperationMetrics? + } + + let mockData = MockData( + result: result, + metrics: metrics + ) + + let data = try! JSONEncoder().encode(mockData) + return try! JSONDecoder().decode(SwiftBuildMessage.BuildCompletedInfo.self, from: data) + } +} + +extension SwiftBuildMessage.BuildOperationMetrics { + package static func mock( + counters: [String: Int] = ["totalTasks": 10], + taskCounters: [String: [String: Int]] = ["CompileSwift": ["count": 5]] + ) -> Self { + struct MockData: Encodable { + let counters: [String: Int] + let taskCounters: [String: [String: Int]] + } + + let mockData = MockData( + counters: counters, + taskCounters: taskCounters + ) + + let data = try! JSONEncoder().encode(mockData) + return try! JSONDecoder().decode(SwiftBuildMessage.BuildOperationMetrics.self, from: data) + } +} + +// MARK: - TaskOutputInfo + +extension SwiftBuildMessage.TaskOutputInfo { + package static func mock( + taskID: Int = 1, + data: String = "Mock task output" + ) -> SwiftBuildMessage.TaskOutputInfo { + struct MockData: Encodable { + let taskID: Int + let data: String + } + + let mockData = MockData( + taskID: taskID, + data: data + ) + + let data = try! JSONEncoder().encode(mockData) + return try! JSONDecoder().decode(SwiftBuildMessage.TaskOutputInfo.self, from: data) + } +} + +// MARK: - BuildDiagnosticInfo + +extension SwiftBuildMessage.BuildDiagnosticInfo { + package static func mock( + message: String = "Mock build diagnostic" + ) -> SwiftBuildMessage.BuildDiagnosticInfo { + struct MockData: Encodable { + let message: String + } + + let mockData = MockData( + message: message + ) + + let data = try! JSONEncoder().encode(mockData) + return try! JSONDecoder().decode(SwiftBuildMessage.BuildDiagnosticInfo.self, from: data) + } +} + +// MARK: - TargetCompleteInfo + +extension SwiftBuildMessage.TargetCompleteInfo { + package static func mock( + targetID: Int = 1 + ) -> SwiftBuildMessage.TargetCompleteInfo { + struct MockData: Encodable { + let id: Int + } + + let mockData = MockData( + id: targetID + ) + + let data = try! JSONEncoder().encode(mockData) + return try! JSONDecoder().decode(SwiftBuildMessage.TargetCompleteInfo.self, from: data) + } +} From a658f58c06657b756a1d6d7119a34d2ae4f770ea Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Thu, 4 Dec 2025 13:59:47 -0500 Subject: [PATCH 17/24] Modify test mocks using exposed memberwise inits --- .../SwiftBuildSystemMessageHandlerTests.swift | 663 ++++++------------ 1 file changed, 215 insertions(+), 448 deletions(-) diff --git a/Tests/SwiftBuildSupportTests/SwiftBuildSystemMessageHandlerTests.swift b/Tests/SwiftBuildSupportTests/SwiftBuildSystemMessageHandlerTests.swift index 3eb4d561046..f69f3cbebec 100644 --- a/Tests/SwiftBuildSupportTests/SwiftBuildSystemMessageHandlerTests.swift +++ b/Tests/SwiftBuildSupportTests/SwiftBuildSystemMessageHandlerTests.swift @@ -14,12 +14,14 @@ import Basics import Foundation import struct SWBUtil.AbsolutePath import Testing +@_spi(Testing) import SwiftBuild import SwiftBuildSupport import TSCBasic import _InternalTestSupport + @Suite struct SwiftBuildSystemMessageHandlerTests { private func createMessageHandler( @@ -42,9 +44,9 @@ struct SwiftBuildSystemMessageHandlerTests { let (messageHandler, outputStream, observability) = createMessageHandler() let events: [SwiftBuildMessage] = [ - .taskStarted(.mock()), - .taskComplete(.mock()), - .buildCompleted(.mock()) + .taskStartedInfo(), + .taskCompleteInfo(), + .buildCompletedInfo() ] for event in events { @@ -61,25 +63,12 @@ struct SwiftBuildSystemMessageHandlerTests { @Test func testSimpleDiagnosticReported() throws { - let (messageHandler, outputStream, observability) = createMessageHandler() + let (messageHandler, _, observability) = createMessageHandler() let events: [SwiftBuildMessage] = [ - .taskStarted(.mock( - taskID: 1, - taskSignature: "mock-diagnostic" - )), - .diagnostic(.mock( - kind: .error, - locationContext: .mock( - taskID: 1, - ), - locationContext2: .mock( - taskSignature: "mock-diagnostic" - ), - message: "Simple diagnostic", - appendToOutputStream: true) - ), - .taskComplete(.mock(taskID: 1)) // Handler only emits when a task is completed. + .taskStartedInfo(taskSignature: "simple-diagnostic"), + .diagnosticInfo(locationContext2: .init(taskSignature: "simple-diagnostic"), message: "Simple diagnostic", appendToOutputStream: true), + .taskCompleteInfo(taskSignature: "simple-diagnostic") // Handler only emits when a task is completed. ] for event in events { @@ -92,478 +81,256 @@ struct SwiftBuildSystemMessageHandlerTests { result.check(diagnostic: "Simple diagnostic", severity: .error) } } -} + @Test + func testManyDiagnosticsReported() throws { + let (messageHandler, _, observability) = createMessageHandler() -extension SwiftBuildMessage.TaskStartedInfo { - package static func mock( - taskID: Int = 1, - targetID: Int? = nil, - taskSignature: String = "taskStartedSignature", - parentTaskID: Int? = nil, - ruleInfo: String = "ruleInfo", - interestingPath: AbsolutePath? = nil, - commandLineDisplayString: String? = nil, - executionDescription: String = "", - serializedDiagnosticsPaths: [AbsolutePath] = [] - ) -> SwiftBuildMessage.TaskStartedInfo { - // Use JSON encoding/decoding as a workaround for lack of public initializer. Have it match the custom CodingKeys as described in - struct MockData: Encodable { - let id: Int - let targetID: Int? - let signature: String - let parentID: Int? - let ruleInfo: String - let interestingPath: String? - let commandLineDisplayString: String? - let executionDescription: String - let serializedDiagnosticsPaths: [String] - } - - let mockData = MockData( - id: taskID, - targetID: targetID, - signature: taskSignature, - parentID: parentTaskID, - ruleInfo: ruleInfo, - interestingPath: interestingPath?.path.str, - commandLineDisplayString: commandLineDisplayString, - executionDescription: executionDescription, - serializedDiagnosticsPaths: serializedDiagnosticsPaths.map { $0.path.str } - ) - - let data = try! JSONEncoder().encode(mockData) - return try! JSONDecoder().decode(SwiftBuildMessage.TaskStartedInfo.self, from: data) - } -} - -// MARK: - DiagnosticInfo + let events: [SwiftBuildMessage] = [ + .taskStartedInfo(taskID: 1, taskSignature: "simple-diagnostic"), + .diagnosticInfo( + locationContext2: .init(taskSignature: "simple-diagnostic"), + message: "Simple diagnostic", + appendToOutputStream: true + ), + .taskStartedInfo(taskID: 2, taskSignature: "another-diagnostic"), + .taskStartedInfo(taskID: 3, taskSignature: "warning-diagnostic"), + .diagnosticInfo( + kind: .warning, + locationContext2: .init(taskSignature: "warning-diagnostic"), + message: "Warning diagnostic", + appendToOutputStream: true + ), + .taskCompleteInfo(taskID: 1, taskSignature: "simple-diagnostic"), + .diagnosticInfo( + kind: .warning, + locationContext2: .init(taskSignature: "warning-diagnostic"), + message: "Another warning diagnostic", + appendToOutputStream: true + ), + .taskCompleteInfo(taskID: 3, taskSignature: "warning-diagnostic"), + .diagnosticInfo( + kind: .note, + locationContext2: .init(taskSignature: "another-diagnostic"), + message: "Another diagnostic", + appendToOutputStream: true + ), + .taskCompleteInfo(taskID: 2, taskSignature: "another-diagnostic") + ] -extension SwiftBuildMessage.DiagnosticInfo { - package static func mock( - kind: Kind = .warning, - location: Location = .unknown, - locationContext: SwiftBuildMessage.LocationContext = .mock(), - locationContext2: SwiftBuildMessage.LocationContext2 = .mock(), - component: Component = .default, - message: String = "Test diagnostic message", - optionName: String? = nil, - appendToOutputStream: Bool = false, - childDiagnostics: [SwiftBuildMessage.DiagnosticInfo] = [], - sourceRanges: [SourceRange] = [], - fixIts: [FixIt] = [] - ) -> SwiftBuildMessage.DiagnosticInfo { - struct MockData: Encodable { - let kind: Kind - let location: Location - let locationContext: SwiftBuildMessage.LocationContext - let locationContext2: SwiftBuildMessage.LocationContext2 - let component: Component - let message: String - let optionName: String? - let appendToOutputStream: Bool - let childDiagnostics: [SwiftBuildMessage.DiagnosticInfo] - let sourceRanges: [SourceRange] - let fixIts: [FixIt] + for event in events { + _ = try messageHandler.emitEvent(event) } - let mockData = MockData( - kind: kind, - location: location, - locationContext: locationContext, - locationContext2: locationContext2, - component: component, - message: message, - optionName: optionName, - appendToOutputStream: appendToOutputStream, - childDiagnostics: childDiagnostics, - sourceRanges: sourceRanges, - fixIts: fixIts - ) - - let data = try! JSONEncoder().encode(mockData) - return try! JSONDecoder().decode(SwiftBuildMessage.DiagnosticInfo.self, from: data) - } -} - -extension SwiftBuildMessage.LocationContext { - package static func mock( - taskID: Int = 1, - targetID: Int = 1 - ) -> Self { - return .task(taskID: taskID, targetID: targetID) - } - - package static func mockTarget(targetID: Int = 1) -> Self { - return .target(targetID: targetID) - } - - package static func mockGlobalTask(taskID: Int = 1) -> Self { - return .globalTask(taskID: taskID) - } - - package static func mockGlobal() -> Self { - return .global - } -} - -extension SwiftBuildMessage.DiagnosticInfo.Location { - package static func mockPath( - _ path: String = "/mock/file.swift", - line: Int = 10, - column: Int? = 5 - ) -> Self { - return .path(path, fileLocation: .textual(line: line, column: column)) - } - - package static func mockPathOnly(_ path: String = "/mock/file.swift") -> Self { - return .path(path, fileLocation: nil) - } - - package static func mockObject( - path: String = "/mock/file.swift", - identifier: String = "mock-identifier" - ) -> Self { - return .path(path, fileLocation: .object(identifier: identifier)) - } - - package static func mockBuildSettings(names: [String] = ["MOCK_SETTING"]) -> Self { - return .buildSettings(names: names) - } + #expect(observability.hasErrorDiagnostics) - package static func mockBuildFiles( - buildFileGUIDs: [String] = ["BUILD_FILE_1"], - buildPhaseGUIDs: [String] = ["BUILD_PHASE_1"], - targetGUID: String = "TARGET_GUID" - ) -> Self { - let buildFiles = zip(buildFileGUIDs, buildPhaseGUIDs).map { - SwiftBuildMessage.DiagnosticInfo.Location.BuildFileAndPhase.mock( - buildFileGUID: $0, - buildPhaseGUID: $1 - ) + try expectDiagnostics(observability.diagnostics) { result in + result.check(diagnostic: "Simple diagnostic", severity: .error) + result.check(diagnostic: "Another diagnostic", severity: .debug) + result.check(diagnostic: "Another warning diagnostic", severity: .warning) + result.check(diagnostic: "Warning diagnostic", severity: .warning) } - return .buildFiles(buildFiles, targetGUID: targetGUID) } -} -extension SwiftBuildMessage.DiagnosticInfo.Location.BuildFileAndPhase { - package static func mock( - buildFileGUID: String = "BUILD_FILE_GUID", - buildPhaseGUID: String = "BUILD_PHASE_GUID" - ) -> Self { - struct MockData: Encodable { - let buildFileGUID: String - let buildPhaseGUID: String - } + @Test + func testCompilerOutputDiagnosticsWithoutDuplicatedLogging() throws { + let (messageHandler, outputStream, observability) = createMessageHandler() - let mockData = MockData( - buildFileGUID: buildFileGUID, - buildPhaseGUID: buildPhaseGUID + let simpleDiagnosticString: String = "[error]: Simple diagnostic\n" + let simpleOutputInfo: SwiftBuildMessage = .outputInfo( + data: data(simpleDiagnosticString), + locationContext: .task(taskID: 1, targetID: 1), + locationContext2: .init(targetID: 1, taskSignature: "simple-diagnostic") ) - let data = try! JSONEncoder().encode(mockData) - return try! JSONDecoder().decode(SwiftBuildMessage.DiagnosticInfo.Location.BuildFileAndPhase.self, from: data) - } -} - -extension SwiftBuildMessage.DiagnosticInfo.SourceRange { - package static func mock( - path: String = "/mock/file.swift", - startLine: Int = 10, - startColumn: Int = 5, - endLine: Int = 10, - endColumn: Int = 20 - ) -> Self { - struct MockData: Encodable { - let path: String - let startLine: Int - let startColumn: Int - let endLine: Int - let endColumn: Int - } - - let mockData = MockData( - path: path, - startLine: startLine, - startColumn: startColumn, - endLine: endLine, - endColumn: endColumn + let warningDiagnosticString: String = "[warning]: Warning diagnostic\n" + let warningOutputInfo: SwiftBuildMessage = .outputInfo( + data: data(warningDiagnosticString), + locationContext: .task(taskID: 3, targetID: 1), + locationContext2: .init(targetID: 1, taskSignature: "warning-diagnostic") ) - let data = try! JSONEncoder().encode(mockData) - return try! JSONDecoder().decode(SwiftBuildMessage.DiagnosticInfo.SourceRange.self, from: data) - } -} - -extension SwiftBuildMessage.DiagnosticInfo.FixIt { - package static func mock( - sourceRange: SwiftBuildMessage.DiagnosticInfo.SourceRange = .mock(), - textToInsert: String = "fix text" - ) -> Self { - struct MockData: Encodable { - let sourceRange: SwiftBuildMessage.DiagnosticInfo.SourceRange - let textToInsert: String - } + let anotherDiagnosticString = "[note]: Another diagnostic\n" + let anotherOutputInfo: SwiftBuildMessage = .outputInfo( + data: data(anotherDiagnosticString), + locationContext: .task(taskID: 2, targetID: 1), + locationContext2: .init(targetID: 1, taskSignature: "another-diagnostic") + ) - let mockData = MockData( - sourceRange: sourceRange, - textToInsert: textToInsert + let anotherWarningDiagnosticString: String = "[warning]: Another warning diagnostic\n" + let anotherWarningOutputInfo: SwiftBuildMessage = .outputInfo( + data: data(anotherWarningDiagnosticString), + locationContext: .task(taskID: 3, targetID: 1), + locationContext2: .init(targetID: 1, taskSignature: "warning-diagnostic") ) - let data = try! JSONEncoder().encode(mockData) - return try! JSONDecoder().decode(SwiftBuildMessage.DiagnosticInfo.FixIt.self, from: data) - } -} + let events: [SwiftBuildMessage] = [ + .taskStartedInfo(taskID: 1, taskSignature: "simple-diagnostic"), + .diagnosticInfo( + locationContext2: .init(taskSignature: "simple-diagnostic"), + message: "Simple diagnostic", + appendToOutputStream: true + ), + .taskStartedInfo(taskID: 2, taskSignature: "another-diagnostic"), + .taskStartedInfo(taskID: 3, taskSignature: "warning-diagnostic"), + .diagnosticInfo( + kind: .warning, + locationContext2: .init(taskSignature: "warning-diagnostic"), + message: "Warning diagnostic", + appendToOutputStream: true + ), + anotherWarningOutputInfo, + simpleOutputInfo, + .taskCompleteInfo(taskID: 1, taskSignature: "simple-diagnostic"), + .diagnosticInfo( + kind: .warning, + locationContext2: .init(taskSignature: "warning-diagnostic"), + message: "Another warning diagnostic", + appendToOutputStream: true + ), + warningOutputInfo, + .taskCompleteInfo(taskID: 3, taskSignature: "warning-diagnostic"), + .diagnosticInfo( + kind: .note, + locationContext2: .init(taskSignature: "another-diagnostic"), + message: "Another diagnostic", + appendToOutputStream: true + ), + anotherOutputInfo, + .taskCompleteInfo(taskID: 2, taskSignature: "another-diagnostic") + ] -extension SwiftBuildMessage.LocationContext2 { - package static func mock( - targetID: Int? = nil, - taskSignature: String? = nil - ) -> Self { - struct MockData: Encodable { - let targetID: Int? - let taskSignature: String? + for event in events { + _ = try messageHandler.emitEvent(event) } - let mockData = MockData( - targetID: targetID, - taskSignature: taskSignature - ) - - let data = try! JSONEncoder().encode(mockData) - return try! JSONDecoder().decode(SwiftBuildMessage.LocationContext2.self, from: data) + // TODO bp this output stream will not contain the bytes of textual output; + // must augment the print use-case within the observability scope to fetch + // that data to complete this assertion. + let outputText = outputStream.bytes.description + #expect(outputText.contains("error")) } } -// MARK: - TaskCompleteInfo +private func data(_ message: String) -> Data { + Data(message.utf8) +} -extension SwiftBuildMessage.TaskCompleteInfo { - package static func mock( +/// Convenience inits for testing +extension SwiftBuildMessage { + /// SwiftBuildMessage.TaskStartedInfo + package static func taskStartedInfo( taskID: Int = 1, + targetID: Int? = nil, taskSignature: String = "mock-task-signature", - result: Result = .success, - signalled: Bool = false, - metrics: Metrics? = nil - ) -> SwiftBuildMessage.TaskCompleteInfo { - struct MockData: Encodable { - let id: Int - let signature: String - let result: Result - let signalled: Bool - let metrics: Metrics? - } - - let mockData = MockData( - id: taskID, - signature: taskSignature, - result: result, - signalled: signalled, - metrics: metrics + parentTaskID: Int? = nil, + ruleInfo: String = "mock-rule", + interestingPath: SwiftBuild.AbsolutePath? = nil, + commandLineDisplayString: String? = nil, + executionDescription: String = "execution description", + serializedDiagnosticsPath: [SwiftBuild.AbsolutePath] = [] + ) -> SwiftBuildMessage { + .taskStarted( + .init( + taskID: taskID, + targetID: targetID, + taskSignature: taskSignature, + parentTaskID: parentTaskID, + ruleInfo: ruleInfo, + interestingPath: interestingPath, + commandLineDisplayString: commandLineDisplayString, + executionDescription: executionDescription, + serializedDiagnosticsPaths: serializedDiagnosticsPath + ) ) - - let data = try! JSONEncoder().encode(mockData) - return try! JSONDecoder().decode(SwiftBuildMessage.TaskCompleteInfo.self, from: data) } -} -extension SwiftBuildMessage.TaskCompleteInfo.Metrics { - package static func mock( - utime: UInt64 = 100, - stime: UInt64 = 50, - maxRSS: UInt64 = 1024000, - wcStartTime: UInt64 = 1000000, - wcDuration: UInt64 = 150 - ) -> Self { - struct MockData: Encodable { - let utime: UInt64 - let stime: UInt64 - let maxRSS: UInt64 - let wcStartTime: UInt64 - let wcDuration: UInt64 - } - - let mockData = MockData( - utime: utime, - stime: stime, - maxRSS: maxRSS, - wcStartTime: wcStartTime, - wcDuration: wcDuration + /// SwiftBuildMessage.TaskCompletedInfo + package static func taskCompleteInfo( + taskID: Int = 1, + taskSignature: String = "mock-task-signature", + result: TaskCompleteInfo.Result = .success, + signalled: Bool = false, + metrics: TaskCompleteInfo.Metrics? = nil + ) -> SwiftBuildMessage { + .taskComplete( + .init( + taskID: taskID, + taskSignature: taskSignature, + result: result, + signalled: signalled, + metrics: metrics + ) ) - - let data = try! JSONEncoder().encode(mockData) - return try! JSONDecoder().decode(SwiftBuildMessage.TaskCompleteInfo.Metrics.self, from: data) } -} - -// MARK: - TargetStartedInfo - -extension SwiftBuildMessage.TargetStartedInfo { - package static func mock( - targetID: Int = 1, - targetGUID: String = "MOCK_TARGET_GUID", - targetName: String = "MockTarget", - type: Kind = .native, - projectName: String = "MockProject", - projectPath: String = "/mock/project.xcodeproj", - projectIsPackage: Bool = false, - projectNameIsUniqueInWorkspace: Bool = true, - configurationName: String = "Debug", - configurationIsDefault: Bool = true, - sdkroot: String? = "macosx" - ) -> SwiftBuildMessage.TargetStartedInfo { - struct MockData: Encodable { - let id: Int - let guid: String - let name: String - let type: Kind - let projectName: String - let projectPath: String - let projectIsPackage: Bool - let projectNameIsUniqueInWorkspace: Bool - let configurationName: String - let configurationIsDefault: Bool - let sdkroot: String? - } - let mockData = MockData( - id: targetID, - guid: targetGUID, - name: targetName, - type: type, - projectName: projectName, - projectPath: projectPath, - projectIsPackage: projectIsPackage, - projectNameIsUniqueInWorkspace: projectNameIsUniqueInWorkspace, - configurationName: configurationName, - configurationIsDefault: configurationIsDefault, - sdkroot: sdkroot + /// SwiftBuildMessage.DiagnosticInfo + package static func diagnosticInfo( + kind: DiagnosticInfo.Kind = .error, + location: DiagnosticInfo.Location = .unknown, + locationContext: LocationContext = .task(taskID: 1, targetID: 1), + locationContext2: LocationContext2 = .init(), + component: DiagnosticInfo.Component = .default, + message: String = "Mock diagnostic message.", + optionName: String? = nil, + appendToOutputStream: Bool = false, + childDiagnostics: [DiagnosticInfo] = [], + sourceRanges: [DiagnosticInfo.SourceRange] = [], + fixIts: [SwiftBuildMessage.DiagnosticInfo.FixIt] = [] + ) -> SwiftBuildMessage { + .diagnostic( + .init( + kind: kind, + location: location, + locationContext: locationContext, + locationContext2: locationContext2, + component: component, + message: message, + optionName: optionName, + appendToOutputStream: appendToOutputStream, + childDiagnostics: childDiagnostics, + sourceRanges: sourceRanges, + fixIts: fixIts + ) ) - - let data = try! JSONEncoder().encode(mockData) - return try! JSONDecoder().decode(SwiftBuildMessage.TargetStartedInfo.self, from: data) } -} -// MARK: - BuildStartedInfo - -extension SwiftBuildMessage.BuildStartedInfo { - package static func mock( - baseDirectory: String = "/mock/base", - derivedDataPath: String? = "/mock/derived-data" + /// SwiftBuildMessage.BuildStartedInfo + package static func buildStartedInfo( + baseDirectory: SwiftBuild.AbsolutePath, + derivedDataPath: SwiftBuild.AbsolutePath? = nil ) -> SwiftBuildMessage.BuildStartedInfo { - struct MockData: Encodable { - let baseDirectory: String - let derivedDataPath: String? - } - - let mockData = MockData( + .init( baseDirectory: baseDirectory, derivedDataPath: derivedDataPath ) - - let data = try! JSONEncoder().encode(mockData) - return try! JSONDecoder().decode(SwiftBuildMessage.BuildStartedInfo.self, from: data) } -} - -// MARK: - BuildCompletedInfo - -extension SwiftBuildMessage.BuildCompletedInfo { - package static func mock( - result: Result = .ok, - metrics: SwiftBuildMessage.BuildOperationMetrics? = nil - ) -> SwiftBuildMessage.BuildCompletedInfo { - struct MockData: Encodable { - let result: Result - let metrics: SwiftBuildMessage.BuildOperationMetrics? - } - - let mockData = MockData( - result: result, - metrics: metrics - ) - - let data = try! JSONEncoder().encode(mockData) - return try! JSONDecoder().decode(SwiftBuildMessage.BuildCompletedInfo.self, from: data) - } -} - -extension SwiftBuildMessage.BuildOperationMetrics { - package static func mock( - counters: [String: Int] = ["totalTasks": 10], - taskCounters: [String: [String: Int]] = ["CompileSwift": ["count": 5]] - ) -> Self { - struct MockData: Encodable { - let counters: [String: Int] - let taskCounters: [String: [String: Int]] - } - - let mockData = MockData( - counters: counters, - taskCounters: taskCounters - ) - - let data = try! JSONEncoder().encode(mockData) - return try! JSONDecoder().decode(SwiftBuildMessage.BuildOperationMetrics.self, from: data) - } -} - -// MARK: - TaskOutputInfo - -extension SwiftBuildMessage.TaskOutputInfo { - package static func mock( - taskID: Int = 1, - data: String = "Mock task output" - ) -> SwiftBuildMessage.TaskOutputInfo { - struct MockData: Encodable { - let taskID: Int - let data: String - } - - let mockData = MockData( - taskID: taskID, - data: data - ) - - let data = try! JSONEncoder().encode(mockData) - return try! JSONDecoder().decode(SwiftBuildMessage.TaskOutputInfo.self, from: data) - } -} - -// MARK: - BuildDiagnosticInfo - -extension SwiftBuildMessage.BuildDiagnosticInfo { - package static func mock( - message: String = "Mock build diagnostic" - ) -> SwiftBuildMessage.BuildDiagnosticInfo { - struct MockData: Encodable { - let message: String - } - let mockData = MockData( - message: message + /// SwiftBuildMessage.BuildCompleteInfo + package static func buildCompletedInfo( + result: BuildCompletedInfo.Result = .ok, + metrics: BuildOperationMetrics? = nil + ) -> SwiftBuildMessage { + .buildCompleted( + .init( + result: result, + metrics: metrics + ) ) - - let data = try! JSONEncoder().encode(mockData) - return try! JSONDecoder().decode(SwiftBuildMessage.BuildDiagnosticInfo.self, from: data) } -} - -// MARK: - TargetCompleteInfo -extension SwiftBuildMessage.TargetCompleteInfo { - package static func mock( - targetID: Int = 1 - ) -> SwiftBuildMessage.TargetCompleteInfo { - struct MockData: Encodable { - let id: Int - } - - let mockData = MockData( - id: targetID + /// SwiftBuildMessage.OutputInfo + package static func outputInfo( + data: Data, + locationContext: LocationContext = .task(taskID: 1, targetID: 1), + locationContext2: LocationContext2 = .init(targetID: 1, taskSignature: "mock-task-signature") + ) -> SwiftBuildMessage { + .output( + .init( + data: data, + locationContext: locationContext, + locationContext2: locationContext2 + ) ) - - let data = try! JSONEncoder().encode(mockData) - return try! JSONDecoder().decode(SwiftBuildMessage.TargetCompleteInfo.self, from: data) } } From 7a00f357b14b62f642ebf60bea8e797eb8a02525 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Thu, 4 Dec 2025 16:36:50 -0500 Subject: [PATCH 18/24] Address PR comments * Defer emission of diagnostics to completed task event * Remove global data buffer from TaskDataBuffer * Remove emits to observabilityScope for taskStarted events * Move SwiftBuildSystemMessageHandler to its own file * Rename printToOutput to print(_ output:verbose:) * Remove unprocessedDiagnostics parameter from message handler * Remove taskIDsEmitted in favour of using tasksEmitted --- Sources/Basics/Observability.swift | 14 +- Sources/SwiftBuildSupport/CMakeLists.txt | 3 +- .../SwiftBuildSupport/SwiftBuildSystem.swift | 456 --------------- .../SwiftBuildSystemMessageHandler.swift | 533 ++++++++++++++++++ .../_InternalTestSupport/Observability.swift | 8 +- .../ObservabilitySystemTests.swift | 6 +- 6 files changed, 551 insertions(+), 469 deletions(-) create mode 100644 Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift diff --git a/Sources/Basics/Observability.swift b/Sources/Basics/Observability.swift index e254d2cc5b2..7d915fd1d1f 100644 --- a/Sources/Basics/Observability.swift +++ b/Sources/Basics/Observability.swift @@ -57,8 +57,8 @@ public class ObservabilitySystem { self.underlying(scope, diagnostic) } - func printToOutput(message: String) { - self.diagnosticsHandler.printToOutput(message: message) + func print(_ output: String, verbose: Bool) { + self.diagnosticsHandler.print(output, verbose: verbose) } } @@ -132,8 +132,8 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol, Sendable, Cus return parent?.errorsReportedInAnyScope ?? false } - public func print(message: String) { - self.diagnosticsHandler.printToOutput(message: message) + public func print(_ output: String, verbose: Bool) { + self.diagnosticsHandler.print(output, verbose: verbose) } // DiagnosticsEmitterProtocol @@ -158,8 +158,8 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol, Sendable, Cus self.underlying.handleDiagnostic(scope: scope, diagnostic: diagnostic) } - public func printToOutput(message: String) { - self.underlying.printToOutput(message: message) + public func print(_ output: String, verbose: Bool) { + self.underlying.print(output, verbose: verbose) } var errorsReported: Bool { @@ -173,7 +173,7 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol, Sendable, Cus public protocol DiagnosticsHandler: Sendable { func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic) - func printToOutput(message: String) + func print(_ output: String, verbose: Bool) } /// Helper protocol to share default behavior. diff --git a/Sources/SwiftBuildSupport/CMakeLists.txt b/Sources/SwiftBuildSupport/CMakeLists.txt index ca9e15f938b..43956e93f4b 100644 --- a/Sources/SwiftBuildSupport/CMakeLists.txt +++ b/Sources/SwiftBuildSupport/CMakeLists.txt @@ -18,7 +18,8 @@ add_library(SwiftBuildSupport STATIC PIF.swift PIFBuilder.swift PluginConfiguration.swift - SwiftBuildSystem.swift) + SwiftBuildSystem.swift + SwiftBuildSystemMessageHandler.swift) target_link_libraries(SwiftBuildSupport PUBLIC Build DriverSupport diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index 7478f5dc32a..1eb9ef77326 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -174,422 +174,6 @@ private final class PlanningOperationDelegate: SWBPlanningOperationDelegate, Sen } } -/// Convenience extensions to extract taskID and targetID from the LocationContext. -extension SwiftBuildMessage.LocationContext { - var taskID: Int? { - switch self { - case .task(let id, _), .globalTask(let id): - return id - case .target, .global: - return nil - } - } - - var targetID: Int? { - switch self { - case .task(_, let id), .target(let id): - return id - case .global, .globalTask: - return nil - } - } -} - -/// Handler for SwiftBuildMessage events sent by the SWBBuildOperation. -public final class SwiftBuildSystemMessageHandler { - private let observabilityScope: ObservabilityScope - private let logLevel: Basics.Diagnostic.Severity - private var buildState: BuildState = .init() - - let progressAnimation: ProgressAnimationProtocol - var serializedDiagnosticPathsByTargetName: [String: [Basics.AbsolutePath]] = [:] - - /// Tracks the diagnostics that we have not yet emitted. - private var unprocessedDiagnostics: [SwiftBuildMessage.DiagnosticInfo] = [] - /// Tracks the task IDs for failed tasks. - private var failedTasks: [Int] = [] - /// Tracks the tasks by their signature for which we have already emitted output. - private var tasksEmitted: Set = [] - /// Tracks the tasks by their ID for which we have already emitted output. - private var taskIDsEmitted: Set = [] - - public init( - observabilityScope: ObservabilityScope, - outputStream: OutputByteStream, - logLevel: Basics.Diagnostic.Severity - ) - { - self.observabilityScope = observabilityScope - self.logLevel = logLevel - self.progressAnimation = ProgressAnimation.ninja( - stream: outputStream, - verbose: self.logLevel.isVerbose - ) - } - - struct BuildState { - private var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:] - private var activeTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskStartedInfo] = [:] - private var completedTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo] = [:] - private var taskDataBuffer: TaskDataBuffer = .init() - private var taskIDToSignature: [Int: String] = [:] - var collectedBacktraceFrames = SWBBuildOperationCollectedBacktraceFrames() - - /// Rich model to store data buffers for a given `SwiftBuildMessage.LocationContext` or - /// a `SwiftBuildMessage.LocationContext2`. - struct TaskDataBuffer { - private var taskSignatureBuffer: [String: Data] = [:] - private var taskIDBuffer: [Int: Data] = [:] - private var targetIDBuffer: [Int: Data] = [:] - private var globalBuffer: Data = Data() - - subscript(key: String) -> Data? { - self.taskSignatureBuffer[key] - } - - subscript(key: String, default defaultValue: Data) -> Data { - get { self.taskSignatureBuffer[key] ?? defaultValue } - set { self.taskSignatureBuffer[key] = newValue } - } - - subscript(key: SwiftBuildMessage.LocationContext, default defaultValue: Data) -> Data { - get { - // Check each ID kind and try to fetch the associated buffer. - // If unable to get a non-nil result, then follow through to the - // next check. - if let taskID = key.taskID, - let result = self.taskIDBuffer[taskID] { - return result - } else if let targetID = key.targetID, - let result = self.targetIDBuffer[targetID] { - return result - } else if !self.globalBuffer.isEmpty { - return self.globalBuffer - } else { - return defaultValue - } - } - - set { - if let taskID = key.taskID { - self.taskIDBuffer[taskID] = newValue - if let targetID = key.targetID { - self.targetIDBuffer[targetID] = newValue - } - } else if let targetID = key.targetID { - self.targetIDBuffer[targetID] = newValue - } else { - self.globalBuffer = newValue - } - } - } - - subscript(key: SwiftBuildMessage.LocationContext2) -> Data? { - get { - if let taskSignature = key.taskSignature { - return self.taskSignatureBuffer[taskSignature] - } else if let targetID = key.targetID { - return self.targetIDBuffer[targetID] - } - - return nil - } - - set { - if let taskSignature = key.taskSignature { - self.taskSignatureBuffer[taskSignature] = newValue - } else if let targetID = key.targetID { - self.targetIDBuffer[targetID] = newValue - } - } - } - - subscript(task: SwiftBuildMessage.TaskStartedInfo) -> Data? { - get { - guard let result = self.taskSignatureBuffer[task.taskSignature] else { - // Default to checking targetID and taskID. - if let result = self.taskIDBuffer[task.taskID] { - return result - } else if let targetID = task.targetID, - let result = self.targetIDBuffer[targetID] { - return result - } - - // Return global buffer if none of the above are found. - return self.globalBuffer - } - - return result - } - } - } - - mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws { - if activeTasks[task.taskID] != nil { - throw Diagnostics.fatalError - } - activeTasks[task.taskID] = task - taskIDToSignature[task.taskID] = task.taskSignature - } - - mutating func completed(task: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo) throws -> SwiftBuild.SwiftBuildMessage.TaskStartedInfo { - guard let startedTaskInfo = activeTasks[task.taskID] else { - throw Diagnostics.fatalError - } - if completedTasks[task.taskID] != nil { - throw Diagnostics.fatalError - } - self.completedTasks[task.taskID] = task - return startedTaskInfo - } - - mutating func started(target: SwiftBuild.SwiftBuildMessage.TargetStartedInfo) throws { - if targetsByID[target.targetID] != nil { - throw Diagnostics.fatalError - } - targetsByID[target.targetID] = target - } - - func target(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws -> SwiftBuild.SwiftBuildMessage.TargetStartedInfo? { - guard let id = task.targetID else { - return nil - } - guard let target = targetsByID[id] else { - throw Diagnostics.fatalError - } - return target - } - - func taskSignature(for id: Int) -> String? { - if let signature = taskIDToSignature[id] { - return signature - } - return nil - } - - mutating func appendToBuffer(_ info: SwiftBuildMessage.OutputInfo) { - // Attempt to key by taskSignature; at times this may not be possible, - // in which case we'd need to fall back to using LocationContext. - guard let taskSignature = info.locationContext2.taskSignature else { - // If we cannot find the task signature from the locationContext2, - // use deprecated locationContext instead to find task signature. - // If this fails to find an associated task signature, track - // relevant IDs from the location context in the task buffer. - if let taskID = info.locationContext.taskID, - let taskSignature = self.taskSignature(for: taskID) { - self.taskDataBuffer[taskSignature, default: .init()].append(info.data) - } - - self.taskDataBuffer[info.locationContext, default: .init()].append(info.data) - - return - } - - self.taskDataBuffer[taskSignature, default: .init()].append(info.data) - } - - func dataBuffer(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) -> Data? { - guard let data = taskDataBuffer[task.taskSignature] else { - // Fallback to checking taskID and targetID. - return taskDataBuffer[task] - } - - return data - } - } - - private func emitInfoAsDiagnostic(info: SwiftBuildMessage.DiagnosticInfo) { - let fixItsDescription = if info.fixIts.hasContent { - ": " + info.fixIts.map { String(describing: $0) }.joined(separator: ", ") - } else { - "" - } - let message = if let locationDescription = info.location.userDescription { - "\(locationDescription) \(info.message)\(fixItsDescription)" - } else { - "\(info.message)\(fixItsDescription)" - } - let severity: Diagnostic.Severity = switch info.kind { - case .error: .error - case .warning: .warning - case .note: .info - case .remark: .debug - } - self.observabilityScope.emit(severity: severity, message: "\(message)\n") - - for childDiagnostic in info.childDiagnostics { - emitInfoAsDiagnostic(info: childDiagnostic) - } - } - - private func emitDiagnosticCompilerOutput(_ info: SwiftBuildMessage.TaskStartedInfo) { - // Don't redundantly emit task output. - guard !self.tasksEmitted.contains(info.taskSignature) else { - return - } - guard hasUnprocessedDiagnostics(info) else { - return - } - // Assure we have a data buffer to decode. - guard let buffer = buildState.dataBuffer(for: info) else { - return - } - - // Decode the buffer to a string - let decodedOutput = String(decoding: buffer, as: UTF8.self) - - // Emit message. - observabilityScope.print(message: decodedOutput) - - // Record that we've emitted the output for a given task signature. - self.tasksEmitted.insert(info.taskSignature) - self.taskIDsEmitted.insert(info.taskID) - } - - private func hasUnprocessedDiagnostics(_ info: SwiftBuildMessage.TaskStartedInfo) -> Bool { - let diagnosticTaskSignature = unprocessedDiagnostics.compactMap(\.locationContext2.taskSignature) - let diagnosticTaskIDs = unprocessedDiagnostics.compactMap(\.locationContext.taskID) - - return diagnosticTaskSignature.contains(info.taskSignature) || diagnosticTaskIDs.contains(info.taskID) - } - - private func handleTaskOutput( - _ info: SwiftBuildMessage.TaskCompleteInfo, - _ startedInfo: SwiftBuildMessage.TaskStartedInfo, - _ enableTaskBacktraces: Bool - ) throws { - if info.result != .success { - emitFailedTaskOutput(info, startedInfo) - } else if let data = buildState.dataBuffer(for: startedInfo), !tasksEmitted.contains(startedInfo.taskSignature) { - let decodedOutput = String(decoding: data, as: UTF8.self) - if !decodedOutput.isEmpty { - observabilityScope.emit(info: decodedOutput) - } - } - - // Handle task backtraces, if applicable. - if enableTaskBacktraces { - if let id = SWBBuildOperationBacktraceFrame.Identifier(taskSignatureData: Data(startedInfo.taskSignature.utf8)), - let backtrace = SWBTaskBacktrace(from: id, collectedFrames: buildState.collectedBacktraceFrames) { - let formattedBacktrace = backtrace.renderTextualRepresentation() - if !formattedBacktrace.isEmpty { - self.observabilityScope.emit(info: "Task backtrace:\n\(formattedBacktrace)") - } - } - } - } - - private func emitFailedTaskOutput( - _ info: SwiftBuildMessage.TaskCompleteInfo, - _ startedInfo: SwiftBuildMessage.TaskStartedInfo - ) { - // Assure that the task has failed. - guard info.result != .success else { - return - } - // Don't redundantly emit task output. - guard !tasksEmitted.contains(startedInfo.taskSignature) else { - return - } - - // Track failed tasks. - self.failedTasks.append(info.taskID) - - // Check for existing diagnostics with matching taskID/taskSignature. - // If we've captured the compiler output with formatted diagnostics keyed by - // this task's signature, emit them. - // Note that this is a workaround instead of emitting directly from a `DiagnosticInfo` - // message, as here we receive the formatted code snippet directly from the compiler. - emitDiagnosticCompilerOutput(startedInfo) - - let message = "\(startedInfo.ruleInfo) failed with a nonzero exit code." - // If we have the command line display string available, then we - // should continue to emit this as an error. Otherwise, this doesn't - // give enough information to the user for it to be useful so we can - // demote it to an info-level log. - if let cmdLineDisplayStr = startedInfo.commandLineDisplayString { - self.observabilityScope.emit(severity: .error, message: "\(message) Command line: \(cmdLineDisplayStr)") - } else { - self.observabilityScope.emit(severity: .info, message: message) - } - - // Track that we have emitted output for this task. - tasksEmitted.insert(startedInfo.taskSignature) - taskIDsEmitted.insert(info.taskID) - } - - func emitEvent(_ message: SwiftBuild.SwiftBuildMessage, _ buildSystem: SwiftBuildSystem) throws { - guard !self.logLevel.isQuiet else { return } - switch message { - case .buildCompleted(let info): - progressAnimation.complete(success: info.result == .ok) - if info.result == .cancelled { - buildSystem.delegate?.buildSystemDidCancel(buildSystem) - } else { - buildSystem.delegate?.buildSystem(buildSystem, didFinishWithResult: info.result == .ok) - } - case .didUpdateProgress(let progressInfo): - var step = Int(progressInfo.percentComplete) - if step < 0 { step = 0 } - let message = if let targetName = progressInfo.targetName { - "\(targetName) \(progressInfo.message)" - } else { - "\(progressInfo.message)" - } - progressAnimation.update(step: step, total: 100, text: message) - buildSystem.delegate?.buildSystem(buildSystem, didUpdateTaskProgress: message) - case .diagnostic(let info): - if info.appendToOutputStream { - emitInfoAsDiagnostic(info: info) - } else { - unprocessedDiagnostics.append(info) - } - case .output(let info): - // Append to buffer-per-task storage - buildState.appendToBuffer(info) - case .taskStarted(let info): - try buildState.started(task: info) - - if let commandLineDisplay = info.commandLineDisplayString { - self.observabilityScope.emit(info: "\(info.executionDescription)\n\(commandLineDisplay)") - } else { - self.observabilityScope.emit(info: "\(info.executionDescription)") - } - - let targetInfo = try buildState.target(for: info) - buildSystem.delegate?.buildSystem(buildSystem, willStartCommand: BuildSystemCommand(info, targetInfo: targetInfo)) - buildSystem.delegate?.buildSystem(buildSystem, didStartCommand: BuildSystemCommand(info, targetInfo: targetInfo)) - case .taskComplete(let info): - let startedInfo = try buildState.completed(task: info) - - // Handler for failed tasks, if applicable. - try handleTaskOutput(info, startedInfo, buildSystem.enableTaskBacktraces) - - let targetInfo = try buildState.target(for: startedInfo) - buildSystem.delegate?.buildSystem(buildSystem, didFinishCommand: BuildSystemCommand(startedInfo, targetInfo: targetInfo)) - if let targetName = targetInfo?.targetName { - try serializedDiagnosticPathsByTargetName[targetName, default: []].append(contentsOf: startedInfo.serializedDiagnosticsPaths.compactMap { - try Basics.AbsolutePath(validating: $0.pathString) - }) - } - case .targetStarted(let info): - try buildState.started(target: info) - case .backtraceFrame(let info): - if buildSystem.enableTaskBacktraces { - buildState.collectedBacktraceFrames.add(frame: info) - } - case .planningOperationStarted, .planningOperationCompleted, .reportBuildDescription, .reportPathMap, .preparedForIndex, .buildStarted, .preparationComplete, .targetUpToDate, .targetComplete, .taskUpToDate: - break - case .buildDiagnostic, .targetDiagnostic, .taskDiagnostic: - break // deprecated - case .buildOutput, .targetOutput, .taskOutput: - break // deprecated - @unknown default: - break - } - } -} - public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { private let buildParameters: BuildParameters private let packageGraphLoader: () async throws -> ModulesGraph @@ -1486,46 +1070,6 @@ extension String { } } -fileprivate extension SwiftBuild.SwiftBuildMessage.DiagnosticInfo.Location { - var userDescription: String? { - switch self { - case .path(let path, let fileLocation): - switch fileLocation { - case .textual(let line, let column): - var description = "\(path):\(line)" - if let column { description += ":\(column)" } - return description - case .object(let identifier): - return "\(path):\(identifier)" - case .none: - return path - } - - case .buildSettings(let names): - return names.joined(separator: ", ") - - case .buildFiles(let buildFiles, let targetGUID): - return "\(targetGUID): " + buildFiles.map { String(describing: $0) }.joined(separator: ", ") - - case .unknown: - return nil - } - } -} - -fileprivate extension BuildSystemCommand { - init(_ taskStartedInfo: SwiftBuildMessage.TaskStartedInfo, targetInfo: SwiftBuildMessage.TargetStartedInfo?) { - self = .init( - name: taskStartedInfo.executionDescription, - targetName: targetInfo?.targetName, - description: taskStartedInfo.commandLineDisplayString ?? "", - serializedDiagnosticPaths: taskStartedInfo.serializedDiagnosticsPaths.compactMap { - try? Basics.AbsolutePath(validating: $0.pathString) - } - ) - } -} - fileprivate extension Triple { var deploymentTargetSettingName: String? { switch (self.os, self.environment) { diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift b/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift new file mode 100644 index 00000000000..120c0181a91 --- /dev/null +++ b/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift @@ -0,0 +1,533 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +@_spi(SwiftPMInternal) +import Basics +import Foundation +@_spi(SwiftPMInternal) +import SPMBuildCore +import enum TSCUtility.Diagnostics +import SWBBuildService +import SwiftBuild +import protocol TSCBasic.OutputByteStream + + +/// Handler for SwiftBuildMessage events sent by the SWBBuildOperation. +public final class SwiftBuildSystemMessageHandler { + private let observabilityScope: ObservabilityScope + private let logLevel: Basics.Diagnostic.Severity + private var buildState: BuildState = .init() + + let progressAnimation: ProgressAnimationProtocol + // TODO bp key by ID; must map to target name when passed to BuildResult + var serializedDiagnosticPathsByTargetName: [String: [Basics.AbsolutePath]] = [:] + + /// Tracks the task IDs for failed tasks. + private var failedTasks: [Int] = [] + /// Tracks the tasks by their signature for which we have already emitted output. + private var tasksEmitted: Set = [] + + public init( + observabilityScope: ObservabilityScope, + outputStream: OutputByteStream, + logLevel: Basics.Diagnostic.Severity + ) + { + self.observabilityScope = observabilityScope + self.logLevel = logLevel + self.progressAnimation = ProgressAnimation.ninja( + stream: outputStream, + verbose: self.logLevel.isVerbose + ) + } + + struct BuildState { + private var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:] + private var activeTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskStartedInfo] = [:] + private var completedTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo] = [:] + private var completedTargets: [Int: SwiftBuild.SwiftBuildMessage.TargetCompleteInfo] = [:] + private var taskDataBuffer: TaskDataBuffer = .init() + private var diagnosticsBuffer: TaskDiagnosticBuffer = .init() + private var taskIDToSignature: [Int: String] = [:] + var collectedBacktraceFrames = SWBBuildOperationCollectedBacktraceFrames() + + struct TaskDiagnosticBuffer { + private var diagnosticSignatureBuffer: [String: [SwiftBuildMessage.DiagnosticInfo]] = [:] + private var diagnosticIDBuffer: [Int: [SwiftBuildMessage.DiagnosticInfo]] = [:] + + subscript(key: SwiftBuildMessage.LocationContext2) -> [SwiftBuildMessage.DiagnosticInfo]? { + guard let taskSignature = key.taskSignature else { + return nil + } + return self.diagnosticSignatureBuffer[taskSignature] + } + + subscript(key: SwiftBuildMessage.LocationContext2, default defaultValue: [SwiftBuildMessage.DiagnosticInfo]) -> [SwiftBuildMessage.DiagnosticInfo] { + get { self[key] ?? defaultValue } + set { + self[key, default: defaultValue] + } + } + + subscript(key: SwiftBuildMessage.LocationContext) -> [SwiftBuildMessage.DiagnosticInfo]? { + guard let taskID = key.taskID else { + return nil + } + + return self.diagnosticIDBuffer[taskID] + } + + subscript(key: SwiftBuildMessage.LocationContext, default defaultValue: [SwiftBuildMessage.DiagnosticInfo]) -> [SwiftBuildMessage.DiagnosticInfo] { + get { self[key] ?? defaultValue } + } + + subscript(key: String) -> [SwiftBuildMessage.DiagnosticInfo] { + get { self.diagnosticSignatureBuffer[key] ?? [] } + set { self.diagnosticSignatureBuffer[key] = newValue } + } + + subscript(key: Int) -> [SwiftBuildMessage.DiagnosticInfo] { + get { self.diagnosticIDBuffer[key] ?? [] } + set { self.diagnosticIDBuffer[key] = newValue } + } + + } + /// Rich model to store data buffers for a given `SwiftBuildMessage.LocationContext` or + /// a `SwiftBuildMessage.LocationContext2`. + struct TaskDataBuffer { + private var taskSignatureBuffer: [String: Data] = [:] + private var taskIDBuffer: [Int: Data] = [:] + + subscript(key: String) -> Data? { + self.taskSignatureBuffer[key] + } + + subscript(key: String, default defaultValue: Data) -> Data { + get { self.taskSignatureBuffer[key] ?? defaultValue } + set { self.taskSignatureBuffer[key] = newValue } + } + + subscript(key: SwiftBuildMessage.LocationContext, default defaultValue: Data) -> Data { + get { + // Check each ID kind and try to fetch the associated buffer. + // If unable to get a non-nil result, then follow through to the + // next check. + if let taskID = key.taskID, + let result = self.taskIDBuffer[taskID] { + return result + } else { + return defaultValue + } + } + + set { + if let taskID = key.taskID { + self.taskIDBuffer[taskID] = newValue + } + } + } + + subscript(key: SwiftBuildMessage.LocationContext2) -> Data? { + get { + if let taskSignature = key.taskSignature { + return self.taskSignatureBuffer[taskSignature] + } + + return nil + } + + set { + if let taskSignature = key.taskSignature { + self.taskSignatureBuffer[taskSignature] = newValue + } + } + } + + subscript(task: SwiftBuildMessage.TaskStartedInfo) -> Data? { + get { + guard let result = self.taskSignatureBuffer[task.taskSignature] else { + // Default to checking targetID and taskID. + if let result = self.taskIDBuffer[task.taskID] { + return result + } + + return nil + } + + return result + } + } + } + + mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws { + if activeTasks[task.taskID] != nil { + throw Diagnostics.fatalError + } + activeTasks[task.taskID] = task + taskIDToSignature[task.taskID] = task.taskSignature + } + + mutating func completed(task: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo) throws -> SwiftBuild.SwiftBuildMessage.TaskStartedInfo { + guard let startedTaskInfo = activeTasks[task.taskID] else { + throw Diagnostics.fatalError + } + if completedTasks[task.taskID] != nil { + throw Diagnostics.fatalError + } + // Track completed task, remove from active tasks. + self.completedTasks[task.taskID] = task + self.activeTasks[task.taskID] = nil + + return startedTaskInfo + } + + mutating func started(target: SwiftBuild.SwiftBuildMessage.TargetStartedInfo) throws { + if targetsByID[target.targetID] != nil { + throw Diagnostics.fatalError + } + targetsByID[target.targetID] = target + } + + mutating func completed(target: SwiftBuild.SwiftBuildMessage.TargetCompleteInfo) throws -> SwiftBuild.SwiftBuildMessage.TargetStartedInfo { + guard let targetStartedInfo = targetsByID[target.targetID] else { + throw Diagnostics.fatalError + } + + targetsByID[target.targetID] = nil + completedTargets[target.targetID] = target + return targetStartedInfo + } + + func target(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws -> SwiftBuild.SwiftBuildMessage.TargetStartedInfo? { + guard let id = task.targetID else { + return nil + } + guard let target = targetsByID[id] else { + throw Diagnostics.fatalError + } + return target + } + + func taskSignature(for id: Int) -> String? { + if let signature = taskIDToSignature[id] { + return signature + } + return nil + } + + mutating func appendToBuffer(_ info: SwiftBuildMessage.OutputInfo) { + // Attempt to key by taskSignature; at times this may not be possible, + // in which case we'd need to fall back to using LocationContext. + guard let taskSignature = info.locationContext2.taskSignature else { + // If we cannot find the task signature from the locationContext2, + // use deprecated locationContext instead to find task signature. + // If this fails to find an associated task signature, track + // relevant IDs from the location context in the task buffer. + if let taskID = info.locationContext.taskID, + let taskSignature = self.taskSignature(for: taskID) { + self.taskDataBuffer[taskSignature, default: .init()].append(info.data) + } + + self.taskDataBuffer[info.locationContext, default: .init()].append(info.data) + + return + } + + self.taskDataBuffer[taskSignature, default: .init()].append(info.data) + } + + func dataBuffer(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) -> Data? { + guard let data = taskDataBuffer[task.taskSignature] else { + // Fallback to checking taskID and targetID. + return taskDataBuffer[task] + } + + return data + } + + mutating func appendDiagnostic(_ info: SwiftBuildMessage.DiagnosticInfo) { + guard let taskID = info.locationContext.taskID else { + return + } + + diagnosticsBuffer[taskID].append(info) + } + + func diagnostics(for task: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo) -> [SwiftBuildMessage.DiagnosticInfo] { + return diagnosticsBuffer[task.taskID] + } + } + + private func emitInfoAsDiagnostic(info: SwiftBuildMessage.DiagnosticInfo) { + let fixItsDescription = if info.fixIts.hasContent { + ": " + info.fixIts.map { String(describing: $0) }.joined(separator: ", ") + } else { + "" + } + let message = if let locationDescription = info.location.userDescription { + "\(locationDescription) \(info.message)\(fixItsDescription)" + } else { + "\(info.message)\(fixItsDescription)" + } + let severity: Diagnostic.Severity = switch info.kind { + case .error: .error + case .warning: .warning + case .note: .info + case .remark: .debug + } + self.observabilityScope.emit(severity: severity, message: "\(message)\n") + + for childDiagnostic in info.childDiagnostics { + emitInfoAsDiagnostic(info: childDiagnostic) + } + } + + private func emitDiagnosticCompilerOutput(_ info: SwiftBuildMessage.TaskStartedInfo) { + // Don't redundantly emit task output. + guard !self.tasksEmitted.contains(info.taskSignature) else { + return + } + // Assure we have a data buffer to decode. + guard let buffer = buildState.dataBuffer(for: info) else { + return + } + + // Decode the buffer to a string + let decodedOutput = String(decoding: buffer, as: UTF8.self) + + // Emit message. + observabilityScope.print(decodedOutput, verbose: self.logLevel.isVerbose) + + // Record that we've emitted the output for a given task signature. + self.tasksEmitted.insert(info.taskSignature) + } + + private func handleTaskOutput( + _ info: SwiftBuildMessage.TaskCompleteInfo, + _ startedInfo: SwiftBuildMessage.TaskStartedInfo, + _ enableTaskBacktraces: Bool + ) throws { + if info.result != .success { + let diagnostics = self.buildState.diagnostics(for: info) + if diagnostics.isEmpty { + // Handle diagnostic via textual compiler output. + emitFailedTaskOutput(info, startedInfo) + } else { + // Handle diagnostic via diagnostic info struct. + diagnostics.forEach({ emitInfoAsDiagnostic(info: $0) }) + } + } else if let data = buildState.dataBuffer(for: startedInfo), !tasksEmitted.contains(startedInfo.taskSignature) { + let decodedOutput = String(decoding: data, as: UTF8.self) + if !decodedOutput.isEmpty { + observabilityScope.emit(info: decodedOutput) + } + } + + // Handle task backtraces, if applicable. + if enableTaskBacktraces { + if let id = SWBBuildOperationBacktraceFrame.Identifier(taskSignatureData: Data(startedInfo.taskSignature.utf8)), + let backtrace = SWBTaskBacktrace(from: id, collectedFrames: buildState.collectedBacktraceFrames) { + let formattedBacktrace = backtrace.renderTextualRepresentation() + if !formattedBacktrace.isEmpty { + self.observabilityScope.emit(info: "Task backtrace:\n\(formattedBacktrace)") + } + } + } + } + + private func emitFailedTaskOutput( + _ info: SwiftBuildMessage.TaskCompleteInfo, + _ startedInfo: SwiftBuildMessage.TaskStartedInfo + ) { + // Assure that the task has failed. + guard info.result != .success else { + return + } + // Don't redundantly emit task output. + guard !tasksEmitted.contains(startedInfo.taskSignature) else { + return + } + + // Track failed tasks. + self.failedTasks.append(info.taskID) + + // Check for existing diagnostics with matching taskID/taskSignature. + // If we've captured the compiler output with formatted diagnostics keyed by + // this task's signature, emit them. + // Note that this is a workaround instead of emitting directly from a `DiagnosticInfo` + // message, as here we receive the formatted code snippet directly from the compiler. + emitDiagnosticCompilerOutput(startedInfo) + + let message = "\(startedInfo.ruleInfo) failed with a nonzero exit code." + // If we have the command line display string available, then we + // should continue to emit this as an error. Otherwise, this doesn't + // give enough information to the user for it to be useful so we can + // demote it to an info-level log. + if let cmdLineDisplayStr = startedInfo.commandLineDisplayString { + self.observabilityScope.emit(severity: .error, message: "\(message) Command line: \(cmdLineDisplayStr)") + } else { + self.observabilityScope.emit(severity: .info, message: message) + } + + // Track that we have emitted output for this task. + tasksEmitted.insert(startedInfo.taskSignature) + } + + func emitEvent(_ message: SwiftBuild.SwiftBuildMessage, _ buildSystem: SwiftBuildSystem) throws { + guard !self.logLevel.isQuiet else { return } + switch message { + case .buildCompleted(let info): + progressAnimation.complete(success: info.result == .ok) + if info.result == .cancelled { + buildSystem.delegate?.buildSystemDidCancel(buildSystem) + } else { + buildSystem.delegate?.buildSystem(buildSystem, didFinishWithResult: info.result == .ok) + } + case .didUpdateProgress(let progressInfo): + var step = Int(progressInfo.percentComplete) + if step < 0 { step = 0 } + let message = if let targetName = progressInfo.targetName { + "\(targetName) \(progressInfo.message)" + } else { + "\(progressInfo.message)" + } + progressAnimation.update(step: step, total: 100, text: message) + buildSystem.delegate?.buildSystem(buildSystem, didUpdateTaskProgress: message) + case .diagnostic(let info): + // If this is representative of a global/target diagnostic + // then we can emit immediately. + // Otherwise, defer emission of diagnostic to matching taskCompleted event. + if info.locationContext.isGlobal || info.locationContext.isTarget { + emitInfoAsDiagnostic(info: info) + } else if info.appendToOutputStream { + buildState.appendDiagnostic(info) + } + case .output(let info): + // Append to buffer-per-task storage + buildState.appendToBuffer(info) + case .taskStarted(let info): + try buildState.started(task: info) + + let targetInfo = try buildState.target(for: info) + buildSystem.delegate?.buildSystem(buildSystem, willStartCommand: BuildSystemCommand(info, targetInfo: targetInfo)) + buildSystem.delegate?.buildSystem(buildSystem, didStartCommand: BuildSystemCommand(info, targetInfo: targetInfo)) + case .taskComplete(let info): + let startedInfo = try buildState.completed(task: info) + + // Handler for failed tasks, if applicable. + try handleTaskOutput(info, startedInfo, buildSystem.enableTaskBacktraces) + + let targetInfo = try buildState.target(for: startedInfo) + buildSystem.delegate?.buildSystem(buildSystem, didFinishCommand: BuildSystemCommand(startedInfo, targetInfo: targetInfo)) + if let targetName = targetInfo?.targetName { + try serializedDiagnosticPathsByTargetName[targetName, default: []].append(contentsOf: startedInfo.serializedDiagnosticsPaths.compactMap { + try Basics.AbsolutePath(validating: $0.pathString) + }) + } + case .targetStarted(let info): + try buildState.started(target: info) + case .backtraceFrame(let info): + if buildSystem.enableTaskBacktraces { + buildState.collectedBacktraceFrames.add(frame: info) + } + case .targetComplete(let info): + _ = try buildState.completed(target: info) + case .planningOperationStarted, .planningOperationCompleted, .reportBuildDescription, .reportPathMap, .preparedForIndex, .buildStarted, .preparationComplete, .targetUpToDate, .taskUpToDate: + break + case .buildDiagnostic, .targetDiagnostic, .taskDiagnostic: + break // deprecated + case .buildOutput, .targetOutput, .taskOutput: + break // deprecated + @unknown default: + break + } + } +} + +fileprivate extension SwiftBuild.SwiftBuildMessage.DiagnosticInfo.Location { + var userDescription: String? { + switch self { + case .path(let path, let fileLocation): + switch fileLocation { + case .textual(let line, let column): + var description = "\(path):\(line)" + if let column { description += ":\(column)" } + return description + case .object(let identifier): + return "\(path):\(identifier)" + case .none: + return path + } + + case .buildSettings(let names): + return names.joined(separator: ", ") + + case .buildFiles(let buildFiles, let targetGUID): + return "\(targetGUID): " + buildFiles.map { String(describing: $0) }.joined(separator: ", ") + + case .unknown: + return nil + } + } +} + +fileprivate extension BuildSystemCommand { + init(_ taskStartedInfo: SwiftBuildMessage.TaskStartedInfo, targetInfo: SwiftBuildMessage.TargetStartedInfo?) { + self = .init( + name: taskStartedInfo.executionDescription, + targetName: targetInfo?.targetName, + description: taskStartedInfo.commandLineDisplayString ?? "", + serializedDiagnosticPaths: taskStartedInfo.serializedDiagnosticsPaths.compactMap { + try? Basics.AbsolutePath(validating: $0.pathString) + } + ) + } +} + +/// Convenience extensions to extract taskID and targetID from the LocationContext. +extension SwiftBuildMessage.LocationContext { + var taskID: Int? { + switch self { + case .task(let id, _), .globalTask(let id): + return id + case .target, .global: + return nil + } + } + + var targetID: Int? { + switch self { + case .task(_, let id), .target(let id): + return id + case .global, .globalTask: + return nil + } + } + + var isGlobal: Bool { + switch self { + case .global, .globalTask: + return true + case .task, .target: + return false + } + } + + var isTarget: Bool { + switch self { + case .target: + return true + case .global, .globalTask, .task: + return false + } + } +} diff --git a/Sources/_InternalTestSupport/Observability.swift b/Sources/_InternalTestSupport/Observability.swift index ae68e3df7ad..4d92d05a114 100644 --- a/Sources/_InternalTestSupport/Observability.swift +++ b/Sources/_InternalTestSupport/Observability.swift @@ -70,13 +70,15 @@ public struct TestingObservability { // TODO: do something useful with scope func handleDiagnostic(scope: ObservabilityScope, diagnostic: Basics.Diagnostic) { if self.verbose { - print(diagnostic.description) + Swift.print(diagnostic.description) } self.diagnostics.append(diagnostic) } - func printToOutput(message: String) { - print(message) + func print(_ output: String, verbose: Bool) { + if verbose { + Swift.print(output) + } } var hasErrors: Bool { diff --git a/Tests/BasicsTests/ObservabilitySystemTests.swift b/Tests/BasicsTests/ObservabilitySystemTests.swift index 51a50fa64ed..34e5143ae53 100644 --- a/Tests/BasicsTests/ObservabilitySystemTests.swift +++ b/Tests/BasicsTests/ObservabilitySystemTests.swift @@ -310,8 +310,10 @@ struct ObservabilitySystemTest { self._diagnostics.append(diagnostic) } - func printToOutput(message: String) { - print(message) + func print(_ output: String, verbose: Bool) { + if verbose { + Swift.print(output) + } } } } From e88ab9639330dd635cd1e3c46e56a118b8213d62 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Fri, 5 Dec 2025 15:42:28 -0500 Subject: [PATCH 19/24] Fix check on global task for LocationContext --- .../SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift b/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift index 120c0181a91..284a1fd4750 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift @@ -515,9 +515,9 @@ extension SwiftBuildMessage.LocationContext { var isGlobal: Bool { switch self { - case .global, .globalTask: + case .global: return true - case .task, .target: + case .task, .target, .globalTask: return false } } From 5f0911d3a555062fa1ec3032787bee7c9813ef34 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Fri, 5 Dec 2025 15:48:01 -0500 Subject: [PATCH 20/24] Track serialized diagnostic path by targetID Modified the map to track these by targetID rather than name; since the `BuildResult` is expecting a map by targetName, another computed property representing the same map by targetName has also been added --- .../SwiftBuildSystemMessageHandler.swift | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift b/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift index 284a1fd4750..f5eafce2a1d 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift @@ -28,8 +28,14 @@ public final class SwiftBuildSystemMessageHandler { private var buildState: BuildState = .init() let progressAnimation: ProgressAnimationProtocol - // TODO bp key by ID; must map to target name when passed to BuildResult - var serializedDiagnosticPathsByTargetName: [String: [Basics.AbsolutePath]] = [:] + var serializedDiagnosticPathsByTargetID: [Int: [Basics.AbsolutePath]] = [:] + var serializedDiagnosticPathsByTargetName: [String: [Basics.AbsolutePath]] { + serializedDiagnosticPathsByTargetID.reduce(into: [:]) { result, entry in + if let name = buildState.targetsByID[entry.key]?.targetName { + result[name, default: []].append(contentsOf: entry.value) + } + } + } /// Tracks the task IDs for failed tasks. private var failedTasks: [Int] = [] @@ -51,7 +57,7 @@ public final class SwiftBuildSystemMessageHandler { } struct BuildState { - private var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:] + internal var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:] private var activeTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskStartedInfo] = [:] private var completedTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo] = [:] private var completedTargets: [Int: SwiftBuild.SwiftBuildMessage.TargetCompleteInfo] = [:] @@ -428,8 +434,8 @@ public final class SwiftBuildSystemMessageHandler { let targetInfo = try buildState.target(for: startedInfo) buildSystem.delegate?.buildSystem(buildSystem, didFinishCommand: BuildSystemCommand(startedInfo, targetInfo: targetInfo)) - if let targetName = targetInfo?.targetName { - try serializedDiagnosticPathsByTargetName[targetName, default: []].append(contentsOf: startedInfo.serializedDiagnosticsPaths.compactMap { + if let targetID = targetInfo?.targetID { + try serializedDiagnosticPathsByTargetID[targetID, default: []].append(contentsOf: startedInfo.serializedDiagnosticsPaths.compactMap { try Basics.AbsolutePath(validating: $0.pathString) }) } From fd21a42333ba1fc8b3733f984148a932e23d2491 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Mon, 8 Dec 2025 17:16:02 -0500 Subject: [PATCH 21/24] Add FIXME + richer model to track emitted tasks --- .../SwiftBuildSystemMessageHandler.swift | 523 ++++++++++-------- 1 file changed, 303 insertions(+), 220 deletions(-) diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift b/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift index f5eafce2a1d..17a3ae0bc4b 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift @@ -29,6 +29,10 @@ public final class SwiftBuildSystemMessageHandler { let progressAnimation: ProgressAnimationProtocol var serializedDiagnosticPathsByTargetID: [Int: [Basics.AbsolutePath]] = [:] + // FIXME: This eventually gets passed into the BuildResult, which expects a + // dictionary of [String: [AbsolutePath]]. Eventually, we should refactor it + // to accept a dictionary keyed by a unique identifier (possibly `ResolvedModule.ID`), + // and instead use the above dictionary keyed by target ID. var serializedDiagnosticPathsByTargetName: [String: [Basics.AbsolutePath]] { serializedDiagnosticPathsByTargetID.reduce(into: [:]) { result, entry in if let name = buildState.targetsByID[entry.key]?.targetName { @@ -40,7 +44,7 @@ public final class SwiftBuildSystemMessageHandler { /// Tracks the task IDs for failed tasks. private var failedTasks: [Int] = [] /// Tracks the tasks by their signature for which we have already emitted output. - private var tasksEmitted: Set = [] + private var tasksEmitted: EmittedTasks = .init() public init( observabilityScope: ObservabilityScope, @@ -56,223 +60,6 @@ public final class SwiftBuildSystemMessageHandler { ) } - struct BuildState { - internal var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:] - private var activeTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskStartedInfo] = [:] - private var completedTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo] = [:] - private var completedTargets: [Int: SwiftBuild.SwiftBuildMessage.TargetCompleteInfo] = [:] - private var taskDataBuffer: TaskDataBuffer = .init() - private var diagnosticsBuffer: TaskDiagnosticBuffer = .init() - private var taskIDToSignature: [Int: String] = [:] - var collectedBacktraceFrames = SWBBuildOperationCollectedBacktraceFrames() - - struct TaskDiagnosticBuffer { - private var diagnosticSignatureBuffer: [String: [SwiftBuildMessage.DiagnosticInfo]] = [:] - private var diagnosticIDBuffer: [Int: [SwiftBuildMessage.DiagnosticInfo]] = [:] - - subscript(key: SwiftBuildMessage.LocationContext2) -> [SwiftBuildMessage.DiagnosticInfo]? { - guard let taskSignature = key.taskSignature else { - return nil - } - return self.diagnosticSignatureBuffer[taskSignature] - } - - subscript(key: SwiftBuildMessage.LocationContext2, default defaultValue: [SwiftBuildMessage.DiagnosticInfo]) -> [SwiftBuildMessage.DiagnosticInfo] { - get { self[key] ?? defaultValue } - set { - self[key, default: defaultValue] - } - } - - subscript(key: SwiftBuildMessage.LocationContext) -> [SwiftBuildMessage.DiagnosticInfo]? { - guard let taskID = key.taskID else { - return nil - } - - return self.diagnosticIDBuffer[taskID] - } - - subscript(key: SwiftBuildMessage.LocationContext, default defaultValue: [SwiftBuildMessage.DiagnosticInfo]) -> [SwiftBuildMessage.DiagnosticInfo] { - get { self[key] ?? defaultValue } - } - - subscript(key: String) -> [SwiftBuildMessage.DiagnosticInfo] { - get { self.diagnosticSignatureBuffer[key] ?? [] } - set { self.diagnosticSignatureBuffer[key] = newValue } - } - - subscript(key: Int) -> [SwiftBuildMessage.DiagnosticInfo] { - get { self.diagnosticIDBuffer[key] ?? [] } - set { self.diagnosticIDBuffer[key] = newValue } - } - - } - /// Rich model to store data buffers for a given `SwiftBuildMessage.LocationContext` or - /// a `SwiftBuildMessage.LocationContext2`. - struct TaskDataBuffer { - private var taskSignatureBuffer: [String: Data] = [:] - private var taskIDBuffer: [Int: Data] = [:] - - subscript(key: String) -> Data? { - self.taskSignatureBuffer[key] - } - - subscript(key: String, default defaultValue: Data) -> Data { - get { self.taskSignatureBuffer[key] ?? defaultValue } - set { self.taskSignatureBuffer[key] = newValue } - } - - subscript(key: SwiftBuildMessage.LocationContext, default defaultValue: Data) -> Data { - get { - // Check each ID kind and try to fetch the associated buffer. - // If unable to get a non-nil result, then follow through to the - // next check. - if let taskID = key.taskID, - let result = self.taskIDBuffer[taskID] { - return result - } else { - return defaultValue - } - } - - set { - if let taskID = key.taskID { - self.taskIDBuffer[taskID] = newValue - } - } - } - - subscript(key: SwiftBuildMessage.LocationContext2) -> Data? { - get { - if let taskSignature = key.taskSignature { - return self.taskSignatureBuffer[taskSignature] - } - - return nil - } - - set { - if let taskSignature = key.taskSignature { - self.taskSignatureBuffer[taskSignature] = newValue - } - } - } - - subscript(task: SwiftBuildMessage.TaskStartedInfo) -> Data? { - get { - guard let result = self.taskSignatureBuffer[task.taskSignature] else { - // Default to checking targetID and taskID. - if let result = self.taskIDBuffer[task.taskID] { - return result - } - - return nil - } - - return result - } - } - } - - mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws { - if activeTasks[task.taskID] != nil { - throw Diagnostics.fatalError - } - activeTasks[task.taskID] = task - taskIDToSignature[task.taskID] = task.taskSignature - } - - mutating func completed(task: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo) throws -> SwiftBuild.SwiftBuildMessage.TaskStartedInfo { - guard let startedTaskInfo = activeTasks[task.taskID] else { - throw Diagnostics.fatalError - } - if completedTasks[task.taskID] != nil { - throw Diagnostics.fatalError - } - // Track completed task, remove from active tasks. - self.completedTasks[task.taskID] = task - self.activeTasks[task.taskID] = nil - - return startedTaskInfo - } - - mutating func started(target: SwiftBuild.SwiftBuildMessage.TargetStartedInfo) throws { - if targetsByID[target.targetID] != nil { - throw Diagnostics.fatalError - } - targetsByID[target.targetID] = target - } - - mutating func completed(target: SwiftBuild.SwiftBuildMessage.TargetCompleteInfo) throws -> SwiftBuild.SwiftBuildMessage.TargetStartedInfo { - guard let targetStartedInfo = targetsByID[target.targetID] else { - throw Diagnostics.fatalError - } - - targetsByID[target.targetID] = nil - completedTargets[target.targetID] = target - return targetStartedInfo - } - - func target(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws -> SwiftBuild.SwiftBuildMessage.TargetStartedInfo? { - guard let id = task.targetID else { - return nil - } - guard let target = targetsByID[id] else { - throw Diagnostics.fatalError - } - return target - } - - func taskSignature(for id: Int) -> String? { - if let signature = taskIDToSignature[id] { - return signature - } - return nil - } - - mutating func appendToBuffer(_ info: SwiftBuildMessage.OutputInfo) { - // Attempt to key by taskSignature; at times this may not be possible, - // in which case we'd need to fall back to using LocationContext. - guard let taskSignature = info.locationContext2.taskSignature else { - // If we cannot find the task signature from the locationContext2, - // use deprecated locationContext instead to find task signature. - // If this fails to find an associated task signature, track - // relevant IDs from the location context in the task buffer. - if let taskID = info.locationContext.taskID, - let taskSignature = self.taskSignature(for: taskID) { - self.taskDataBuffer[taskSignature, default: .init()].append(info.data) - } - - self.taskDataBuffer[info.locationContext, default: .init()].append(info.data) - - return - } - - self.taskDataBuffer[taskSignature, default: .init()].append(info.data) - } - - func dataBuffer(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) -> Data? { - guard let data = taskDataBuffer[task.taskSignature] else { - // Fallback to checking taskID and targetID. - return taskDataBuffer[task] - } - - return data - } - - mutating func appendDiagnostic(_ info: SwiftBuildMessage.DiagnosticInfo) { - guard let taskID = info.locationContext.taskID else { - return - } - - diagnosticsBuffer[taskID].append(info) - } - - func diagnostics(for task: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo) -> [SwiftBuildMessage.DiagnosticInfo] { - return diagnosticsBuffer[task.taskID] - } - } - private func emitInfoAsDiagnostic(info: SwiftBuildMessage.DiagnosticInfo) { let fixItsDescription = if info.fixIts.hasContent { ": " + info.fixIts.map { String(describing: $0) }.joined(separator: ", ") @@ -314,7 +101,7 @@ public final class SwiftBuildSystemMessageHandler { observabilityScope.print(decodedOutput, verbose: self.logLevel.isVerbose) // Record that we've emitted the output for a given task signature. - self.tasksEmitted.insert(info.taskSignature) + self.tasksEmitted.insert(info) } private func handleTaskOutput( @@ -385,7 +172,7 @@ public final class SwiftBuildSystemMessageHandler { } // Track that we have emitted output for this task. - tasksEmitted.insert(startedInfo.taskSignature) + tasksEmitted.insert(startedInfo) } func emitEvent(_ message: SwiftBuild.SwiftBuildMessage, _ buildSystem: SwiftBuildSystem) throws { @@ -459,6 +246,302 @@ public final class SwiftBuildSystemMessageHandler { } } +// MARK: SwiftBuildSystemMessageHandler.BuildState + +extension SwiftBuildSystemMessageHandler { + struct BuildState { + internal var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:] + private var activeTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskStartedInfo] = [:] + private var completedTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo] = [:] + private var completedTargets: [Int: SwiftBuild.SwiftBuildMessage.TargetCompleteInfo] = [:] + private var taskDataBuffer: TaskDataBuffer = .init() + private var diagnosticsBuffer: TaskDiagnosticBuffer = .init() + private var taskIDToSignature: [Int: String] = [:] + var collectedBacktraceFrames = SWBBuildOperationCollectedBacktraceFrames() + + mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws { + if activeTasks[task.taskID] != nil { + throw Diagnostics.fatalError + } + activeTasks[task.taskID] = task + taskIDToSignature[task.taskID] = task.taskSignature + } + + mutating func completed(task: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo) throws -> SwiftBuild.SwiftBuildMessage.TaskStartedInfo { + guard let startedTaskInfo = activeTasks[task.taskID] else { + throw Diagnostics.fatalError + } + if completedTasks[task.taskID] != nil { + throw Diagnostics.fatalError + } + // Track completed task, remove from active tasks. + self.completedTasks[task.taskID] = task + self.activeTasks[task.taskID] = nil + + return startedTaskInfo + } + + mutating func started(target: SwiftBuild.SwiftBuildMessage.TargetStartedInfo) throws { + if targetsByID[target.targetID] != nil { + throw Diagnostics.fatalError + } + targetsByID[target.targetID] = target + } + + mutating func completed(target: SwiftBuild.SwiftBuildMessage.TargetCompleteInfo) throws -> SwiftBuild.SwiftBuildMessage.TargetStartedInfo { + guard let targetStartedInfo = targetsByID[target.targetID] else { + throw Diagnostics.fatalError + } + + targetsByID[target.targetID] = nil + completedTargets[target.targetID] = target + return targetStartedInfo + } + + func target(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws -> SwiftBuild.SwiftBuildMessage.TargetStartedInfo? { + guard let id = task.targetID else { + return nil + } + guard let target = targetsByID[id] else { + throw Diagnostics.fatalError + } + return target + } + + func taskSignature(for id: Int) -> String? { + if let signature = taskIDToSignature[id] { + return signature + } + return nil + } + + mutating func appendToBuffer(_ info: SwiftBuildMessage.OutputInfo) { + // Attempt to key by taskSignature; at times this may not be possible, + // in which case we'd need to fall back to using LocationContext. + guard let taskSignature = info.locationContext2.taskSignature else { + // If we cannot find the task signature from the locationContext2, + // use deprecated locationContext instead to find task signature. + // If this fails to find an associated task signature, track + // relevant IDs from the location context in the task buffer. + if let taskID = info.locationContext.taskID, + let taskSignature = self.taskSignature(for: taskID) { + self.taskDataBuffer[taskSignature, default: .init()].append(info.data) + } + + self.taskDataBuffer[info.locationContext, default: .init()].append(info.data) + + return + } + + self.taskDataBuffer[taskSignature, default: .init()].append(info.data) + } + + func dataBuffer(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) -> Data? { + guard let data = taskDataBuffer[task.taskSignature] else { + // Fallback to checking taskID and targetID. + return taskDataBuffer[task] + } + + return data + } + + mutating func appendDiagnostic(_ info: SwiftBuildMessage.DiagnosticInfo) { + guard let taskID = info.locationContext.taskID else { + return + } + + diagnosticsBuffer[taskID].append(info) + } + + func diagnostics(for task: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo) -> [SwiftBuildMessage.DiagnosticInfo] { + return diagnosticsBuffer[task.taskID] + } + } +} + +// MARK: - SwiftBuildSystemMessageHandler.BuildState.TaskDataBuffer + +extension SwiftBuildSystemMessageHandler.BuildState { + /// Rich model to store data buffers for a given `SwiftBuildMessage.LocationContext` or + /// a `SwiftBuildMessage.LocationContext2`. + struct TaskDataBuffer { + private var taskSignatureBuffer: [String: Data] = [:] + private var taskIDBuffer: [Int: Data] = [:] + + subscript(key: String) -> Data? { + self.taskSignatureBuffer[key] + } + + subscript(key: String, default defaultValue: Data) -> Data { + get { self.taskSignatureBuffer[key] ?? defaultValue } + set { self.taskSignatureBuffer[key] = newValue } + } + + subscript(key: SwiftBuildMessage.LocationContext, default defaultValue: Data) -> Data { + get { + // Check each ID kind and try to fetch the associated buffer. + // If unable to get a non-nil result, then follow through to the + // next check. + if let taskID = key.taskID, + let result = self.taskIDBuffer[taskID] { + return result + } else { + return defaultValue + } + } + + set { + if let taskID = key.taskID { + self.taskIDBuffer[taskID] = newValue + } + } + } + + subscript(key: SwiftBuildMessage.LocationContext2) -> Data? { + get { + if let taskSignature = key.taskSignature { + return self.taskSignatureBuffer[taskSignature] + } + + return nil + } + + set { + if let taskSignature = key.taskSignature { + self.taskSignatureBuffer[taskSignature] = newValue + } + } + } + + subscript(task: SwiftBuildMessage.TaskStartedInfo) -> Data? { + get { + guard let result = self.taskSignatureBuffer[task.taskSignature] else { + // Default to checking targetID and taskID. + if let result = self.taskIDBuffer[task.taskID] { + return result + } + + return nil + } + + return result + } + } + } + +} + +// MARK: - SwiftBuildSystemMessageHandler.BuildState. + +extension SwiftBuildSystemMessageHandler.BuildState { + struct TaskDiagnosticBuffer { + private var diagnosticSignatureBuffer: [String: [SwiftBuildMessage.DiagnosticInfo]] = [:] + private var diagnosticIDBuffer: [Int: [SwiftBuildMessage.DiagnosticInfo]] = [:] + + subscript(key: SwiftBuildMessage.LocationContext2) -> [SwiftBuildMessage.DiagnosticInfo]? { + guard let taskSignature = key.taskSignature else { + return nil + } + return self.diagnosticSignatureBuffer[taskSignature] + } + + subscript(key: SwiftBuildMessage.LocationContext2, default defaultValue: [SwiftBuildMessage.DiagnosticInfo]) -> [SwiftBuildMessage.DiagnosticInfo] { + get { self[key] ?? defaultValue } + set { + self[key, default: defaultValue] + } + } + + subscript(key: SwiftBuildMessage.LocationContext) -> [SwiftBuildMessage.DiagnosticInfo]? { + guard let taskID = key.taskID else { + return nil + } + + return self.diagnosticIDBuffer[taskID] + } + + subscript(key: SwiftBuildMessage.LocationContext, default defaultValue: [SwiftBuildMessage.DiagnosticInfo]) -> [SwiftBuildMessage.DiagnosticInfo] { + get { self[key] ?? defaultValue } + } + + subscript(key: String) -> [SwiftBuildMessage.DiagnosticInfo] { + get { self.diagnosticSignatureBuffer[key] ?? [] } + set { self.diagnosticSignatureBuffer[key] = newValue } + } + + subscript(key: Int) -> [SwiftBuildMessage.DiagnosticInfo] { + get { self.diagnosticIDBuffer[key] ?? [] } + set { self.diagnosticIDBuffer[key] = newValue } + } + + } +} + +// MARK: - SwiftBuildSystemMessageHandler.EmittedTasks + +extension SwiftBuildSystemMessageHandler { + struct EmittedTasks: Collection { + public typealias Index = Set.Index + public typealias Element = Set.Element + var startIndex: Set.Index { + self.storage.startIndex + } + var endIndex: Set.Index { + self.storage.endIndex + } + + private var storage: Set = [] + + public init() { } + + mutating func insert(_ task: TaskInfo) { + storage.insert(task) + } + + subscript(position: Index) -> Element { + return storage[position] + } + + func index(after i: Set.Index) -> Set.Index { + return storage.index(after: i) + } + + func contains(_ task: TaskInfo) -> Bool { + return storage.contains(task) + } + + public func contains(_ taskID: Int) -> Bool { + return storage.contains(where: { $0.taskID == taskID }) + } + + public func contains(_ taskSignature: String) -> Bool { + return storage.contains(where: { $0.taskSignature == taskSignature }) + } + + public mutating func insert(_ startedTaskInfo: SwiftBuildMessage.TaskStartedInfo) { + self.storage.insert(.init(startedTaskInfo)) + } + } + + struct TaskInfo: Hashable { + let taskID: Int + let taskSignature: String + + public init(_ startedTaskInfo: SwiftBuildMessage.TaskStartedInfo) { + self.taskID = startedTaskInfo.taskID + self.taskSignature = startedTaskInfo.taskSignature + } + + public static func ==(lhs: Self, rhs: String) -> Bool { + return lhs.taskSignature == rhs + } + + public static func ==(lhs: Self, rhs: Int) -> Bool { + return lhs.taskID == rhs + } + } +} + fileprivate extension SwiftBuild.SwiftBuildMessage.DiagnosticInfo.Location { var userDescription: String? { switch self { From 4e2926798f057bee2ff6da26d1d34003424102d7 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Tue, 9 Dec 2025 12:06:16 -0500 Subject: [PATCH 22/24] Add documentation --- .../SwiftBuildSystemMessageHandler.swift | 299 +++++++++++++----- 1 file changed, 212 insertions(+), 87 deletions(-) diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift b/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift index 17a3ae0bc4b..6c43fafbfe3 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift @@ -249,16 +249,29 @@ public final class SwiftBuildSystemMessageHandler { // MARK: SwiftBuildSystemMessageHandler.BuildState extension SwiftBuildSystemMessageHandler { + /// Manages the state of an active build operation, tracking targets, tasks, buffers, and backtrace frames. + /// This struct maintains the complete state model for build operations, coordinating data between + /// different phases of the build lifecycle. struct BuildState { + // Targets internal var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:] + private var completedTargets: [Int: SwiftBuild.SwiftBuildMessage.TargetCompleteInfo] = [:] + + // Tasks private var activeTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskStartedInfo] = [:] private var completedTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo] = [:] - private var completedTargets: [Int: SwiftBuild.SwiftBuildMessage.TargetCompleteInfo] = [:] + private var taskIDToSignature: [Int: String] = [:] + + // Per-task buffers private var taskDataBuffer: TaskDataBuffer = .init() private var diagnosticsBuffer: TaskDiagnosticBuffer = .init() - private var taskIDToSignature: [Int: String] = [:] - var collectedBacktraceFrames = SWBBuildOperationCollectedBacktraceFrames() + // Backtrace frames + internal var collectedBacktraceFrames = SWBBuildOperationCollectedBacktraceFrames() + + /// Registers the start of a build task, validating that the task hasn't already been started. + /// - Parameter task: The task start information containing task ID and signature + /// - Throws: Fatal error if the task is already active mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws { if activeTasks[task.taskID] != nil { throw Diagnostics.fatalError @@ -267,6 +280,10 @@ extension SwiftBuildSystemMessageHandler { taskIDToSignature[task.taskID] = task.taskSignature } + /// Marks a task as completed and removes it from active tracking. + /// - Parameter task: The task completion information + /// - Returns: The original task start information for the completed task + /// - Throws: Fatal error if the task was not started or already completed mutating func completed(task: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo) throws -> SwiftBuild.SwiftBuildMessage.TaskStartedInfo { guard let startedTaskInfo = activeTasks[task.taskID] else { throw Diagnostics.fatalError @@ -281,6 +298,9 @@ extension SwiftBuildSystemMessageHandler { return startedTaskInfo } + /// Registers the start of a build target, validating that the target hasn't already been started. + /// - Parameter target: The target start information containing target ID and name + /// - Throws: Fatal error if the target is already active mutating func started(target: SwiftBuild.SwiftBuildMessage.TargetStartedInfo) throws { if targetsByID[target.targetID] != nil { throw Diagnostics.fatalError @@ -288,6 +308,10 @@ extension SwiftBuildSystemMessageHandler { targetsByID[target.targetID] = target } + /// Marks a target as completed and removes it from active tracking. + /// - Parameter target: The target completion information + /// - Returns: The original target start information for the completed target + /// - Throws: Fatal error if the target was not started mutating func completed(target: SwiftBuild.SwiftBuildMessage.TargetCompleteInfo) throws -> SwiftBuild.SwiftBuildMessage.TargetStartedInfo { guard let targetStartedInfo = targetsByID[target.targetID] else { throw Diagnostics.fatalError @@ -298,6 +322,10 @@ extension SwiftBuildSystemMessageHandler { return targetStartedInfo } + /// Retrieves the target information associated with a given task. + /// - Parameter task: The task start information to look up the target for + /// - Returns: The target start information if the task has an associated target, nil otherwise + /// - Throws: Fatal error if the target ID exists but no matching target is found func target(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws -> SwiftBuild.SwiftBuildMessage.TargetStartedInfo? { guard let id = task.targetID else { return nil @@ -308,75 +336,50 @@ extension SwiftBuildSystemMessageHandler { return target } + /// Retrieves the task signature for a given task ID. + /// - Parameter id: The task ID to look up + /// - Returns: The task signature string if found, nil otherwise func taskSignature(for id: Int) -> String? { if let signature = taskIDToSignature[id] { return signature } return nil } - - mutating func appendToBuffer(_ info: SwiftBuildMessage.OutputInfo) { - // Attempt to key by taskSignature; at times this may not be possible, - // in which case we'd need to fall back to using LocationContext. - guard let taskSignature = info.locationContext2.taskSignature else { - // If we cannot find the task signature from the locationContext2, - // use deprecated locationContext instead to find task signature. - // If this fails to find an associated task signature, track - // relevant IDs from the location context in the task buffer. - if let taskID = info.locationContext.taskID, - let taskSignature = self.taskSignature(for: taskID) { - self.taskDataBuffer[taskSignature, default: .init()].append(info.data) - } - - self.taskDataBuffer[info.locationContext, default: .init()].append(info.data) - - return - } - - self.taskDataBuffer[taskSignature, default: .init()].append(info.data) - } - - func dataBuffer(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) -> Data? { - guard let data = taskDataBuffer[task.taskSignature] else { - // Fallback to checking taskID and targetID. - return taskDataBuffer[task] - } - - return data - } - - mutating func appendDiagnostic(_ info: SwiftBuildMessage.DiagnosticInfo) { - guard let taskID = info.locationContext.taskID else { - return - } - - diagnosticsBuffer[taskID].append(info) - } - - func diagnostics(for task: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo) -> [SwiftBuildMessage.DiagnosticInfo] { - return diagnosticsBuffer[task.taskID] - } } } // MARK: - SwiftBuildSystemMessageHandler.BuildState.TaskDataBuffer extension SwiftBuildSystemMessageHandler.BuildState { - /// Rich model to store data buffers for a given `SwiftBuildMessage.LocationContext` or - /// a `SwiftBuildMessage.LocationContext2`. + /// Manages data buffers for build tasks, supporting multiple indexing strategies. + /// This buffer system stores output data from tasks using both task signatures and task IDs, + /// providing flexible access patterns for different build message types and legacy support. struct TaskDataBuffer { private var taskSignatureBuffer: [String: Data] = [:] private var taskIDBuffer: [Int: Data] = [:] + /// Retrieves data for a task signature key. + /// - Parameter key: The task signature string + /// - Returns: The associated data buffer, or nil if not found subscript(key: String) -> Data? { self.taskSignatureBuffer[key] } + /// Retrieves or sets data for a task signature key with a default value. + /// - Parameters: + /// - key: The task signature string + /// - defaultValue: The default data to return/store if no value exists + /// - Returns: The stored data buffer or the default value subscript(key: String, default defaultValue: Data) -> Data { get { self.taskSignatureBuffer[key] ?? defaultValue } set { self.taskSignatureBuffer[key] = newValue } } + /// Retrieves or sets data using a LocationContext for task identification. + /// - Parameters: + /// - key: The location context containing task or target ID information + /// - defaultValue: The default data to return/store if no value exists + /// - Returns: The stored data buffer or the default value subscript(key: SwiftBuildMessage.LocationContext, default defaultValue: Data) -> Data { get { // Check each ID kind and try to fetch the associated buffer. @@ -397,6 +400,9 @@ extension SwiftBuildSystemMessageHandler.BuildState { } } + /// Retrieves or sets data using a LocationContext2 for task identification. + /// - Parameter key: The location context containing task signature information + /// - Returns: The associated data buffer, or nil if not found subscript(key: SwiftBuildMessage.LocationContext2) -> Data? { get { if let taskSignature = key.taskSignature { @@ -413,6 +419,9 @@ extension SwiftBuildSystemMessageHandler.BuildState { } } + /// Retrieves data for a specific task using TaskStartedInfo. + /// - Parameter task: The task start information containing signature and ID + /// - Returns: The associated data buffer, or nil if not found subscript(task: SwiftBuildMessage.TaskStartedInfo) -> Data? { get { guard let result = self.taskSignatureBuffer[task.taskSignature] else { @@ -429,15 +438,56 @@ extension SwiftBuildSystemMessageHandler.BuildState { } } + /// Appends output data to the appropriate task buffer based on location context information. + /// - Parameter info: The output info containing data and location context for storage + mutating func appendToBuffer(_ info: SwiftBuildMessage.OutputInfo) { + // Attempt to key by taskSignature; at times this may not be possible, + // in which case we'd need to fall back to using LocationContext. + guard let taskSignature = info.locationContext2.taskSignature else { + // If we cannot find the task signature from the locationContext2, + // use deprecated locationContext instead to find task signature. + // If this fails to find an associated task signature, track + // relevant IDs from the location context in the task buffer. + if let taskID = info.locationContext.taskID, + let taskSignature = self.taskSignature(for: taskID) { + self.taskDataBuffer[taskSignature, default: .init()].append(info.data) + } + + self.taskDataBuffer[info.locationContext, default: .init()].append(info.data) + + return + } + + self.taskDataBuffer[taskSignature, default: .init()].append(info.data) + } + + /// Retrieves the accumulated data buffer for a specific task. + /// - Parameter task: The task start information to look up data for + /// - Returns: The accumulated data buffer for the task, or nil if no data exists + func dataBuffer(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) -> Data? { + guard let data = taskDataBuffer[task.taskSignature] else { + // Fallback to checking taskID and targetID. + return taskDataBuffer[task] + } + + return data + } + } -// MARK: - SwiftBuildSystemMessageHandler.BuildState. +// MARK: - SwiftBuildSystemMessageHandler.BuildState.TaskDiagnosticBuffer extension SwiftBuildSystemMessageHandler.BuildState { + /// Manages diagnostic information buffers for build tasks, organized by task signatures and IDs. + /// This buffer system collects diagnostic messages during task execution for later retrieval + /// and structured reporting of build errors, warnings, and other diagnostic information. struct TaskDiagnosticBuffer { private var diagnosticSignatureBuffer: [String: [SwiftBuildMessage.DiagnosticInfo]] = [:] private var diagnosticIDBuffer: [Int: [SwiftBuildMessage.DiagnosticInfo]] = [:] + /// Retrieves diagnostic information using LocationContext2 for task identification. + /// - Parameter key: The location context containing task signature information + /// - Returns: Array of diagnostic info for the task, or nil if not found subscript(key: SwiftBuildMessage.LocationContext2) -> [SwiftBuildMessage.DiagnosticInfo]? { guard let taskSignature = key.taskSignature else { return nil @@ -445,6 +495,11 @@ extension SwiftBuildSystemMessageHandler.BuildState { return self.diagnosticSignatureBuffer[taskSignature] } + /// Retrieves or sets diagnostic information using LocationContext2 with a default value. + /// - Parameters: + /// - key: The location context containing task signature information + /// - defaultValue: The default diagnostic array to return if no value exists + /// - Returns: Array of diagnostic info for the task, or the default value subscript(key: SwiftBuildMessage.LocationContext2, default defaultValue: [SwiftBuildMessage.DiagnosticInfo]) -> [SwiftBuildMessage.DiagnosticInfo] { get { self[key] ?? defaultValue } set { @@ -452,6 +507,9 @@ extension SwiftBuildSystemMessageHandler.BuildState { } } + /// Retrieves diagnostic information using LocationContext for task identification. + /// - Parameter key: The location context containing task ID information + /// - Returns: Array of diagnostic info for the task, or nil if not found subscript(key: SwiftBuildMessage.LocationContext) -> [SwiftBuildMessage.DiagnosticInfo]? { guard let taskID = key.taskID else { return nil @@ -460,26 +518,56 @@ extension SwiftBuildSystemMessageHandler.BuildState { return self.diagnosticIDBuffer[taskID] } + /// Retrieves diagnostic information using LocationContext with a default value. + /// - Parameters: + /// - key: The location context containing task ID information + /// - defaultValue: The default diagnostic array to return if no value exists + /// - Returns: Array of diagnostic info for the task, or the default value subscript(key: SwiftBuildMessage.LocationContext, default defaultValue: [SwiftBuildMessage.DiagnosticInfo]) -> [SwiftBuildMessage.DiagnosticInfo] { get { self[key] ?? defaultValue } } + /// Retrieves or sets diagnostic information using a task signature string. + /// - Parameter key: The task signature string + /// - Returns: Array of diagnostic info for the task signature subscript(key: String) -> [SwiftBuildMessage.DiagnosticInfo] { get { self.diagnosticSignatureBuffer[key] ?? [] } set { self.diagnosticSignatureBuffer[key] = newValue } } + /// Retrieves or sets diagnostic information using a task ID. + /// - Parameter key: The task ID + /// - Returns: Array of diagnostic info for the task ID subscript(key: Int) -> [SwiftBuildMessage.DiagnosticInfo] { get { self.diagnosticIDBuffer[key] ?? [] } set { self.diagnosticIDBuffer[key] = newValue } } + } + /// Appends a diagnostic message to the appropriate diagnostic buffer. + /// - Parameter info: The diagnostic information to store, containing location context for identification + mutating func appendDiagnostic(_ info: SwiftBuildMessage.DiagnosticInfo) { + guard let taskID = info.locationContext.taskID else { + return + } + + diagnosticsBuffer[taskID].append(info) + } + + /// Retrieves all diagnostic information for a completed task. + /// - Parameter task: The task completion information containing the task ID + /// - Returns: Array of diagnostic info associated with the task + func diagnostics(for task: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo) -> [SwiftBuildMessage.DiagnosticInfo] { + return diagnosticsBuffer[task.taskID] } } // MARK: - SwiftBuildSystemMessageHandler.EmittedTasks extension SwiftBuildSystemMessageHandler { + /// A collection that tracks tasks for which output has already been emitted to prevent duplicate output. + /// This struct ensures that task output is only displayed once during the build process, improving + /// the readability and accuracy of build logs by avoiding redundant messaging. struct EmittedTasks: Collection { public typealias Index = Set.Index public typealias Element = Set.Element @@ -494,6 +582,8 @@ extension SwiftBuildSystemMessageHandler { public init() { } + /// Inserts a task info into the emitted tasks collection. + /// - Parameter task: The task information to mark as emitted mutating func insert(_ task: TaskInfo) { storage.insert(task) } @@ -506,42 +596,116 @@ extension SwiftBuildSystemMessageHandler { return storage.index(after: i) } + /// Checks if a specific task info has been marked as emitted. + /// - Parameter task: The task information to check + /// - Returns: True if the task has already been emitted, false otherwise func contains(_ task: TaskInfo) -> Bool { return storage.contains(task) } + /// Checks if a task with the given ID has been marked as emitted. + /// - Parameter taskID: The task ID to check + /// - Returns: True if a task with this ID has already been emitted, false otherwise public func contains(_ taskID: Int) -> Bool { return storage.contains(where: { $0.taskID == taskID }) } + /// Checks if a task with the given signature has been marked as emitted. + /// - Parameter taskSignature: The task signature to check + /// - Returns: True if a task with this signature has already been emitted, false otherwise public func contains(_ taskSignature: String) -> Bool { return storage.contains(where: { $0.taskSignature == taskSignature }) } + /// Convenience method to insert a task using TaskStartedInfo. + /// - Parameter startedTaskInfo: The task start information to mark as emitted public mutating func insert(_ startedTaskInfo: SwiftBuildMessage.TaskStartedInfo) { self.storage.insert(.init(startedTaskInfo)) } } + /// Represents essential identifying information for a build task. + /// This struct encapsulates both the numeric task ID and string task signature, + /// providing efficient lookup and comparison capabilities for task tracking. struct TaskInfo: Hashable { let taskID: Int let taskSignature: String + /// Initializes TaskInfo from TaskStartedInfo. + /// - Parameter startedTaskInfo: The task start information containing ID and signature public init(_ startedTaskInfo: SwiftBuildMessage.TaskStartedInfo) { self.taskID = startedTaskInfo.taskID self.taskSignature = startedTaskInfo.taskSignature } + /// Compares TaskInfo with a task signature string. + /// - Parameters: + /// - lhs: The TaskInfo instance + /// - rhs: The task signature string to compare + /// - Returns: True if the TaskInfo's signature matches the string public static func ==(lhs: Self, rhs: String) -> Bool { return lhs.taskSignature == rhs } + /// Compares TaskInfo with a task ID integer. + /// - Parameters: + /// - lhs: The TaskInfo instance + /// - rhs: The task ID integer to compare + /// - Returns: True if the TaskInfo's ID matches the integer public static func ==(lhs: Self, rhs: Int) -> Bool { return lhs.taskID == rhs } } } +/// Convenience extensions to extract taskID and targetID from the LocationContext. +extension SwiftBuildMessage.LocationContext { + /// Extracts the task ID from the location context. + /// - Returns: The task ID if the context represents a task or global task, nil otherwise + var taskID: Int? { + switch self { + case .task(let id, _), .globalTask(let id): + return id + case .target, .global: + return nil + } + } + + /// Extracts the target ID from the location context. + /// - Returns: The target ID if the context represents a task or target, nil otherwise + var targetID: Int? { + switch self { + case .task(_, let id), .target(let id): + return id + case .global, .globalTask: + return nil + } + } + + /// Determines if the location context represents a global scope. + /// - Returns: True if the context is global, false otherwise + var isGlobal: Bool { + switch self { + case .global: + return true + case .task, .target, .globalTask: + return false + } + } + + /// Determines if the location context represents a target scope. + /// - Returns: True if the context is target-specific, false otherwise + var isTarget: Bool { + switch self { + case .target: + return true + case .global, .globalTask, .task: + return false + } + } +} + + fileprivate extension SwiftBuild.SwiftBuildMessage.DiagnosticInfo.Location { var userDescription: String? { switch self { @@ -581,42 +745,3 @@ fileprivate extension BuildSystemCommand { ) } } - -/// Convenience extensions to extract taskID and targetID from the LocationContext. -extension SwiftBuildMessage.LocationContext { - var taskID: Int? { - switch self { - case .task(let id, _), .globalTask(let id): - return id - case .target, .global: - return nil - } - } - - var targetID: Int? { - switch self { - case .task(_, let id), .target(let id): - return id - case .global, .globalTask: - return nil - } - } - - var isGlobal: Bool { - switch self { - case .global: - return true - case .task, .target, .globalTask: - return false - } - } - - var isTarget: Bool { - switch self { - case .target: - return true - case .global, .globalTask, .task: - return false - } - } -} From f5dd65c9cb5a4bba8f9d1dec78f4f7f20bcc9c93 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Tue, 9 Dec 2025 12:26:11 -0500 Subject: [PATCH 23/24] remove old print method --- Sources/CoreCommands/SwiftCommandObservabilityHandler.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Sources/CoreCommands/SwiftCommandObservabilityHandler.swift b/Sources/CoreCommands/SwiftCommandObservabilityHandler.swift index f726d9d317e..aa49fffcc94 100644 --- a/Sources/CoreCommands/SwiftCommandObservabilityHandler.swift +++ b/Sources/CoreCommands/SwiftCommandObservabilityHandler.swift @@ -116,12 +116,6 @@ public struct SwiftCommandObservabilityHandler: ObservabilityHandlerProvider { } } - func printToOutput(message: String) { - self.queue.async(group: self.sync) { - self.write(message) - } - } - // for raw output reporting func print(_ output: String, verbose: Bool) { self.queue.async(group: self.sync) { From 30fc79ae5027caad87b429fdfb1ecc177028f648 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Tue, 9 Dec 2025 16:45:04 -0500 Subject: [PATCH 24/24] Create new testing observability system that can be supplied an output stream Some minor fixes to how task output is handled in the SwiftBuildSystemMessageHandler. --- .../SwiftBuildSystemMessageHandler.swift | 42 ++++++++++--------- .../_InternalTestSupport/Observability.swift | 23 ++++++++-- .../SwiftBuildSystemMessageHandlerTests.swift | 35 ++++++++++++---- 3 files changed, 70 insertions(+), 30 deletions(-) diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift b/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift index 22b56fb5993..882029a4973 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift @@ -54,7 +54,7 @@ public final class SwiftBuildSystemMessageHandler { observabilityScope: ObservabilityScope, outputStream: OutputByteStream, logLevel: Basics.Diagnostic.Severity, - enableBacktraces: Bool, + enableBacktraces: Bool = false, buildDelegate: SPMBuildCore.BuildSystemDelegate? = nil ) { @@ -98,7 +98,7 @@ public final class SwiftBuildSystemMessageHandler { return } // Assure we have a data buffer to decode. - guard let buffer = buildState.dataBuffer(for: info) else { + guard let buffer = buildState.dataBuffer(for: info), !buffer.isEmpty else { return } @@ -108,7 +108,7 @@ public final class SwiftBuildSystemMessageHandler { // Emit message. observabilityScope.print(decodedOutput, verbose: self.logLevel.isVerbose) - // Record that we've emitted the output for a given task signature. + // Record that we've emitted the output for a given task. self.tasksEmitted.insert(info) } @@ -117,20 +117,19 @@ public final class SwiftBuildSystemMessageHandler { _ startedInfo: SwiftBuildMessage.TaskStartedInfo, _ enableTaskBacktraces: Bool ) throws { - if info.result != .success { - let diagnostics = self.buildState.diagnostics(for: info) - if diagnostics.isEmpty { - // Handle diagnostic via textual compiler output. - emitFailedTaskOutput(info, startedInfo) - } else { - // Handle diagnostic via diagnostic info struct. - diagnostics.forEach({ emitInfoAsDiagnostic(info: $0) }) - } - } else if let data = buildState.dataBuffer(for: startedInfo), !tasksEmitted.contains(startedInfo.taskSignature) { - let decodedOutput = String(decoding: data, as: UTF8.self) - if !decodedOutput.isEmpty { - observabilityScope.emit(info: decodedOutput) - } + guard info.result == .success else { + emitFailedTaskOutput(info, startedInfo) + return + } + + // Handle diagnostics, if applicable. + let diagnostics = self.buildState.diagnostics(for: info) + if !diagnostics.isEmpty { + // Emit diagnostics using the `DiagnosticInfo` model. + diagnostics.forEach({ emitInfoAsDiagnostic(info: $0) }) + } else { + // Emit diagnostics through textual compiler output. + emitDiagnosticCompilerOutput(startedInfo) } // Handle task backtraces, if applicable. @@ -166,7 +165,12 @@ public final class SwiftBuildSystemMessageHandler { // this task's signature, emit them. // Note that this is a workaround instead of emitting directly from a `DiagnosticInfo` // message, as here we receive the formatted code snippet directly from the compiler. - emitDiagnosticCompilerOutput(startedInfo) + let diagnosticsBuffer = buildState.diagnostics(for: info) + if !diagnosticsBuffer.isEmpty { + diagnosticsBuffer.forEach({ emitInfoAsDiagnostic(info: $0) }) + } else { + emitDiagnosticCompilerOutput(startedInfo) + } let message = "\(startedInfo.ruleInfo) failed with a nonzero exit code." // If we have the command line display string available, then we @@ -263,7 +267,7 @@ public final class SwiftBuildSystemMessageHandler { @unknown default: break } - + return callback } } diff --git a/Sources/_InternalTestSupport/Observability.swift b/Sources/_InternalTestSupport/Observability.swift index 4d92d05a114..8463805ad24 100644 --- a/Sources/_InternalTestSupport/Observability.swift +++ b/Sources/_InternalTestSupport/Observability.swift @@ -16,6 +16,7 @@ import func XCTest.XCTAssertEqual import func XCTest.XCTFail import struct TSCBasic.StringError +import class TSCBasic.BufferedOutputByteStream import TSCTestSupport import Testing @@ -26,6 +27,12 @@ extension ObservabilitySystem { let observabilitySystem = ObservabilitySystem(collector) return TestingObservability(collector: collector, topScope: observabilitySystem.topScope) } + + public static func makeForTesting(verbose: Bool = true, outputStream: BufferedOutputByteStream) -> TestingObservability { + let collector = TestingObservability.Collector(verbose: verbose, outputStream: outputStream) + let observabilitySystem = ObservabilitySystem(collector) + return TestingObservability(collector: collector, topScope: observabilitySystem.topScope) + } } public struct TestingObservability { @@ -62,22 +69,32 @@ public struct TestingObservability { private let verbose: Bool let diagnostics = ThreadSafeArrayStore() + private let outputStream: BufferedOutputByteStream? - init(verbose: Bool) { + init(verbose: Bool, outputStream: BufferedOutputByteStream? = nil) { self.verbose = verbose + self.outputStream = outputStream } // TODO: do something useful with scope func handleDiagnostic(scope: ObservabilityScope, diagnostic: Basics.Diagnostic) { if self.verbose { - Swift.print(diagnostic.description) + if let outputStream { + outputStream.write(diagnostic.description) + } else { + Swift.print(diagnostic.description) + } } self.diagnostics.append(diagnostic) } func print(_ output: String, verbose: Bool) { if verbose { - Swift.print(output) + if let outputStream { + outputStream.write(output) + } else { + Swift.print(output) + } } } diff --git a/Tests/SwiftBuildSupportTests/SwiftBuildSystemMessageHandlerTests.swift b/Tests/SwiftBuildSupportTests/SwiftBuildSystemMessageHandlerTests.swift index f69f3cbebec..48c43b288d5 100644 --- a/Tests/SwiftBuildSupportTests/SwiftBuildSystemMessageHandlerTests.swift +++ b/Tests/SwiftBuildSupportTests/SwiftBuildSystemMessageHandlerTests.swift @@ -27,8 +27,8 @@ struct SwiftBuildSystemMessageHandlerTests { private func createMessageHandler( _ logLevel: Basics.Diagnostic.Severity = .warning ) -> (handler: SwiftBuildSystemMessageHandler, outputStream: BufferedOutputByteStream, observability: TestingObservability) { - let observability = ObservabilitySystem.makeForTesting() let outputStream = BufferedOutputByteStream() + let observability = ObservabilitySystem.makeForTesting(outputStream: outputStream) let handler = SwiftBuildSystemMessageHandler( observabilityScope: observability.topScope, @@ -68,7 +68,7 @@ struct SwiftBuildSystemMessageHandlerTests { let events: [SwiftBuildMessage] = [ .taskStartedInfo(taskSignature: "simple-diagnostic"), .diagnosticInfo(locationContext2: .init(taskSignature: "simple-diagnostic"), message: "Simple diagnostic", appendToOutputStream: true), - .taskCompleteInfo(taskSignature: "simple-diagnostic") // Handler only emits when a task is completed. + .taskCompleteInfo(taskSignature: "simple-diagnostic", result: .failed) // Handler only emits when a task is completed. ] for event in events { @@ -101,21 +101,21 @@ struct SwiftBuildSystemMessageHandlerTests { message: "Warning diagnostic", appendToOutputStream: true ), - .taskCompleteInfo(taskID: 1, taskSignature: "simple-diagnostic"), + .taskCompleteInfo(taskID: 1, taskSignature: "simple-diagnostic", result: .failed), .diagnosticInfo( kind: .warning, locationContext2: .init(taskSignature: "warning-diagnostic"), message: "Another warning diagnostic", appendToOutputStream: true ), - .taskCompleteInfo(taskID: 3, taskSignature: "warning-diagnostic"), + .taskCompleteInfo(taskID: 3, taskSignature: "warning-diagnostic", result: .success), .diagnosticInfo( kind: .note, locationContext2: .init(taskSignature: "another-diagnostic"), message: "Another diagnostic", appendToOutputStream: true ), - .taskCompleteInfo(taskID: 2, taskSignature: "another-diagnostic") + .taskCompleteInfo(taskID: 2, taskSignature: "another-diagnostic", result: .failed) ] for event in events { @@ -204,12 +204,31 @@ struct SwiftBuildSystemMessageHandlerTests { _ = try messageHandler.emitEvent(event) } - // TODO bp this output stream will not contain the bytes of textual output; - // must augment the print use-case within the observability scope to fetch - // that data to complete this assertion. let outputText = outputStream.bytes.description #expect(outputText.contains("error")) } + + @Test + func testDiagnosticOutputWhenOnlyWarnings() throws { + let (messageHandler, outputStream, observability) = createMessageHandler() + + let events: [SwiftBuildMessage] = [ + .taskStartedInfo(taskID: 1, taskSignature: "simple-warning-diagnostic"), + .diagnosticInfo( + kind: .warning, + locationContext2: .init(taskSignature: "simple-warning-diagnostic"), + message: "Simple warning diagnostic", + appendToOutputStream: true + ), + .taskCompleteInfo(taskID: 1, taskSignature: "simple-diagnostic", result: .success) + ] + + for event in events { + _ = try messageHandler.emitEvent(event) + } + + #expect(observability.hasWarningDiagnostics) + } } private func data(_ message: String) -> Data {