From 235dd3e7b81249c500840a4a35b62d0dd4c16ff5 Mon Sep 17 00:00:00 2001 From: Bri Peticca Date: Wed, 5 Nov 2025 15:17:27 -0500 Subject: [PATCH 01/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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):