Skip to content

Commit 10af69e

Browse files
authored
Capture code snippet from diagnostic compiler output (#9362)
This PR refactors diagnostic handling in the Swift build system by introducing a dedicated message handler and per-task output buffering to properly parse and emit compiler diagnostics individually. ### **Key Changes** **SwiftBuildSystemMessageHandler** - Introduced a new dedicated handler class to process `SwiftBuildMessage` events from the build operation - Moved message handling logic out of inline nested functions for better organization and testability - Maintains build state, progress animation, and diagnostic processing in a single cohesive component **Per-Task Data Buffering** - Added `taskDataBuffer` struct in `BuildState` to capture compiler output per task signature - New `TaskDataBuffer` struct allows for using `LocationContext` or `LocationContext2` as a subscript key to fetch the appropriate data buffer for a task, defaulting to the global buffer if no associated task or target can be identified. - Task output is accumulated in the buffer as `.output` messages arrive - Buffer contents are processed when tasks complete, ensuring all output is captured before parsing - Failed tasks with no useful or apparent message will be demoted to an info log level to avoid creating too much noise on the output. **Per-Task Diagnostic Buffering** - Added `diagnosticBuffer` property to the `BuildState` to track diagnostics to emit once we receive a `taskComplete` event - A check is done to ascertain whether the diagnostic info we receive is a global/target diagnostic, and if so we emit the diagnostic immediately; all other diagnostics are accumulated in the buffer to be emitted once the associated task is completed. **EmittedTasks** - Helper struct for the message handler to track which task's messages have already been emitted - Handles both taskIDs as well as taskSignatures ### **Test Suite** **SwiftBuildSystemMessageHandlerTests** - New test suite created to assert that the diagnostic output is formatted and emitted as expected. - Uses the initializers for the nested `SwiftBuildMessage` info structs that are exposed for testing purposes only
1 parent ef8b21d commit 10af69e

File tree

10 files changed

+1237
-199
lines changed

10 files changed

+1237
-199
lines changed

Sources/Basics/Observability.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ public class ObservabilitySystem {
5656
func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic) {
5757
self.underlying(scope, diagnostic)
5858
}
59+
60+
func print(_ output: String, verbose: Bool) {
61+
self.diagnosticsHandler.print(output, verbose: verbose)
62+
}
5963
}
6064

6165
public static var NOOP: ObservabilityScope {
@@ -128,6 +132,10 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol, Sendable, Cus
128132
return parent?.errorsReportedInAnyScope ?? false
129133
}
130134

135+
public func print(_ output: String, verbose: Bool) {
136+
self.diagnosticsHandler.print(output, verbose: verbose)
137+
}
138+
131139
// DiagnosticsEmitterProtocol
132140
public func emit(_ diagnostic: Diagnostic) {
133141
var diagnostic = diagnostic
@@ -150,6 +158,10 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol, Sendable, Cus
150158
self.underlying.handleDiagnostic(scope: scope, diagnostic: diagnostic)
151159
}
152160

161+
public func print(_ output: String, verbose: Bool) {
162+
self.underlying.print(output, verbose: verbose)
163+
}
164+
153165
var errorsReported: Bool {
154166
self._errorsReported.get() ?? false
155167
}
@@ -160,6 +172,8 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol, Sendable, Cus
160172

161173
public protocol DiagnosticsHandler: Sendable {
162174
func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic)
175+
176+
func print(_ output: String, verbose: Bool)
163177
}
164178

165179
/// Helper protocol to share default behavior.

Sources/SwiftBuildSupport/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ add_library(SwiftBuildSupport STATIC
1818
PIF.swift
1919
PIFBuilder.swift
2020
PluginConfiguration.swift
21-
SwiftBuildSystem.swift)
21+
SwiftBuildSystem.swift
22+
SwiftBuildSystemMessageHandler.swift)
2223
target_link_libraries(SwiftBuildSupport PUBLIC
2324
Build
2425
DriverSupport

Sources/SwiftBuildSupport/SwiftBuildSystem.swift

Lines changed: 16 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,10 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
242242

243243
public var hasIntegratedAPIDigesterSupport: Bool { true }
244244

245+
public var enableTaskBacktraces: Bool {
246+
self.buildParameters.outputParameters.enableTaskBacktraces
247+
}
248+
245249
public init(
246250
buildParameters: BuildParameters,
247251
packageGraphLoader: @escaping () async throws -> ModulesGraph,
@@ -546,12 +550,14 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
546550
return try await withService(connectionMode: .inProcessStatic(swiftbuildServiceEntryPoint)) { service in
547551
let derivedDataPath = self.buildParameters.dataPath
548552

549-
let progressAnimation = ProgressAnimation.ninja(
550-
stream: self.outputStream,
551-
verbose: self.logLevel.isVerbose
553+
let buildMessageHandler = SwiftBuildSystemMessageHandler(
554+
observabilityScope: self.observabilityScope,
555+
outputStream: self.outputStream,
556+
logLevel: self.logLevel,
557+
enableBacktraces: self.enableTaskBacktraces,
558+
buildDelegate: self.delegate
552559
)
553560

554-
var serializedDiagnosticPathsByTargetName: [String: [Basics.AbsolutePath]] = [:]
555561
do {
556562
try await withSession(service: service, name: self.buildParameters.pifManifest.pathString, toolchain: self.buildParameters.toolchain, packageManagerResourcesDirectory: self.packageManagerResourcesDirectory) { session, _ in
557563
self.outputStream.send("Building for \(self.buildParameters.configuration == .debug ? "debugging" : "production")...\n")
@@ -591,173 +597,32 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
591597

592598
let request = try await self.makeBuildRequest(session: session, configuredTargets: configuredTargets, derivedDataPath: derivedDataPath, symbolGraphOptions: symbolGraphOptions)
593599

594-
struct BuildState {
595-
private var targetsByID: [Int: SwiftBuild.SwiftBuildMessage.TargetStartedInfo] = [:]
596-
private var activeTasks: [Int: SwiftBuild.SwiftBuildMessage.TaskStartedInfo] = [:]
597-
var collectedBacktraceFrames = SWBBuildOperationCollectedBacktraceFrames()
598-
599-
mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws {
600-
if activeTasks[task.taskID] != nil {
601-
throw Diagnostics.fatalError
602-
}
603-
activeTasks[task.taskID] = task
604-
}
605-
606-
mutating func completed(task: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo) throws -> SwiftBuild.SwiftBuildMessage.TaskStartedInfo {
607-
guard let task = activeTasks[task.taskID] else {
608-
throw Diagnostics.fatalError
609-
}
610-
return task
611-
}
612-
613-
mutating func started(target: SwiftBuild.SwiftBuildMessage.TargetStartedInfo) throws {
614-
if targetsByID[target.targetID] != nil {
615-
throw Diagnostics.fatalError
616-
}
617-
targetsByID[target.targetID] = target
618-
}
619-
620-
mutating func target(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws -> SwiftBuild.SwiftBuildMessage.TargetStartedInfo? {
621-
guard let id = task.targetID else {
622-
return nil
623-
}
624-
guard let target = targetsByID[id] else {
625-
throw Diagnostics.fatalError
626-
}
627-
return target
628-
}
629-
}
630-
631-
func emitEvent(_ message: SwiftBuild.SwiftBuildMessage, buildState: inout BuildState) throws {
632-
guard !self.logLevel.isQuiet else { return }
633-
switch message {
634-
case .buildCompleted(let info):
635-
progressAnimation.complete(success: info.result == .ok)
636-
if info.result == .cancelled {
637-
self.delegate?.buildSystemDidCancel(self)
638-
} else {
639-
self.delegate?.buildSystem(self, didFinishWithResult: info.result == .ok)
640-
}
641-
case .didUpdateProgress(let progressInfo):
642-
var step = Int(progressInfo.percentComplete)
643-
if step < 0 { step = 0 }
644-
let message = if let targetName = progressInfo.targetName {
645-
"\(targetName) \(progressInfo.message)"
646-
} else {
647-
"\(progressInfo.message)"
648-
}
649-
progressAnimation.update(step: step, total: 100, text: message)
650-
self.delegate?.buildSystem(self, didUpdateTaskProgress: message)
651-
case .diagnostic(let info):
652-
func emitInfoAsDiagnostic(info: SwiftBuildMessage.DiagnosticInfo) {
653-
let fixItsDescription = if info.fixIts.hasContent {
654-
": " + info.fixIts.map { String(describing: $0) }.joined(separator: ", ")
655-
} else {
656-
""
657-
}
658-
let message = if let locationDescription = info.location.userDescription {
659-
"\(locationDescription) \(info.message)\(fixItsDescription)"
660-
} else {
661-
"\(info.message)\(fixItsDescription)"
662-
}
663-
let severity: Diagnostic.Severity = switch info.kind {
664-
case .error: .error
665-
case .warning: .warning
666-
case .note: .info
667-
case .remark: .debug
668-
}
669-
self.observabilityScope.emit(severity: severity, message: "\(message)\n")
670-
671-
for childDiagnostic in info.childDiagnostics {
672-
emitInfoAsDiagnostic(info: childDiagnostic)
673-
}
674-
}
675-
676-
emitInfoAsDiagnostic(info: info)
677-
case .output(let info):
678-
self.observabilityScope.emit(info: "\(String(decoding: info.data, as: UTF8.self))")
679-
case .taskStarted(let info):
680-
try buildState.started(task: info)
681-
682-
if let commandLineDisplay = info.commandLineDisplayString {
683-
self.observabilityScope.emit(info: "\(info.executionDescription)\n\(commandLineDisplay)")
684-
} else {
685-
self.observabilityScope.emit(info: "\(info.executionDescription)")
686-
}
687-
688-
if self.logLevel.isVerbose {
689-
if let commandLineDisplay = info.commandLineDisplayString {
690-
self.outputStream.send("\(info.executionDescription)\n\(commandLineDisplay)")
691-
} else {
692-
self.outputStream.send("\(info.executionDescription)")
693-
}
694-
}
695-
let targetInfo = try buildState.target(for: info)
696-
self.delegate?.buildSystem(self, willStartCommand: BuildSystemCommand(info, targetInfo: targetInfo))
697-
self.delegate?.buildSystem(self, didStartCommand: BuildSystemCommand(info, targetInfo: targetInfo))
698-
case .taskComplete(let info):
699-
let startedInfo = try buildState.completed(task: info)
700-
if info.result != .success {
701-
self.observabilityScope.emit(severity: .error, message: "\(startedInfo.ruleInfo) failed with a nonzero exit code. Command line: \(startedInfo.commandLineDisplayString ?? "<no command line>")")
702-
}
703-
let targetInfo = try buildState.target(for: startedInfo)
704-
self.delegate?.buildSystem(self, didFinishCommand: BuildSystemCommand(startedInfo, targetInfo: targetInfo))
705-
if let targetName = targetInfo?.targetName {
706-
serializedDiagnosticPathsByTargetName[targetName, default: []].append(contentsOf: startedInfo.serializedDiagnosticsPaths.compactMap {
707-
try? Basics.AbsolutePath(validating: $0.pathString)
708-
})
709-
}
710-
if self.buildParameters.outputParameters.enableTaskBacktraces {
711-
if let id = SWBBuildOperationBacktraceFrame.Identifier(taskSignatureData: Data(startedInfo.taskSignature.utf8)),
712-
let backtrace = SWBTaskBacktrace(from: id, collectedFrames: buildState.collectedBacktraceFrames) {
713-
let formattedBacktrace = backtrace.renderTextualRepresentation()
714-
if !formattedBacktrace.isEmpty {
715-
self.observabilityScope.emit(info: "Task backtrace:\n\(formattedBacktrace)")
716-
}
717-
}
718-
}
719-
case .targetStarted(let info):
720-
try buildState.started(target: info)
721-
case .backtraceFrame(let info):
722-
if self.buildParameters.outputParameters.enableTaskBacktraces {
723-
buildState.collectedBacktraceFrames.add(frame: info)
724-
}
725-
case .planningOperationStarted, .planningOperationCompleted, .reportBuildDescription, .reportPathMap, .preparedForIndex, .buildStarted, .preparationComplete, .targetUpToDate, .targetComplete, .taskUpToDate:
726-
break
727-
case .buildDiagnostic, .targetDiagnostic, .taskDiagnostic:
728-
break // deprecated
729-
case .buildOutput, .targetOutput, .taskOutput:
730-
break // deprecated
731-
@unknown default:
732-
break
733-
}
734-
}
735-
736600
let operation = try await session.createBuildOperation(
737601
request: request,
738602
delegate: SwiftBuildSystemPlanningOperationDelegate(),
739603
retainBuildDescription: true
740604
)
741605

742606
var buildDescriptionID: SWBBuildDescriptionID? = nil
743-
var buildState = BuildState()
744607
for try await event in try await operation.start() {
745608
if case .reportBuildDescription(let info) = event {
746609
if buildDescriptionID != nil {
747610
self.observabilityScope.emit(debug: "build unexpectedly reported multiple build description IDs")
748611
}
749612
buildDescriptionID = SWBBuildDescriptionID(info.buildDescriptionID)
750613
}
751-
try emitEvent(event, buildState: &buildState)
614+
if let delegateCallback = try buildMessageHandler.emitEvent(event) {
615+
delegateCallback(self)
616+
}
752617
}
753618

754619
await operation.waitForCompletion()
755620

756621
switch operation.state {
757622
case .succeeded:
758623
guard !self.logLevel.isQuiet else { return }
759-
progressAnimation.update(step: 100, total: 100, text: "")
760-
progressAnimation.complete(success: true)
624+
buildMessageHandler.progressAnimation.update(step: 100, total: 100, text: "")
625+
buildMessageHandler.progressAnimation.complete(success: true)
761626
let duration = ContinuousClock.Instant.now - buildStartTime
762627
let formattedDuration = duration.formatted(.units(allowed: [.seconds], fractionalPart: .show(length: 2, rounded: .up)))
763628
self.outputStream.send("Build complete! (\(formattedDuration))\n")
@@ -824,7 +689,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
824689
}
825690

826691
return BuildResult(
827-
serializedDiagnosticPathsByTargetName: .success(serializedDiagnosticPathsByTargetName),
692+
serializedDiagnosticPathsByTargetName: .success(buildMessageHandler.serializedDiagnosticPathsByTargetName),
828693
symbolGraph: SymbolGraphResult(
829694
outputLocationForTarget: { target, buildParameters in
830695
return ["\(buildParameters.triple.archName)", "\(target).symbolgraphs"]
@@ -1299,46 +1164,6 @@ extension String {
12991164
}
13001165
}
13011166

1302-
fileprivate extension SwiftBuild.SwiftBuildMessage.DiagnosticInfo.Location {
1303-
var userDescription: String? {
1304-
switch self {
1305-
case .path(let path, let fileLocation):
1306-
switch fileLocation {
1307-
case .textual(let line, let column):
1308-
var description = "\(path):\(line)"
1309-
if let column { description += ":\(column)" }
1310-
return description
1311-
case .object(let identifier):
1312-
return "\(path):\(identifier)"
1313-
case .none:
1314-
return path
1315-
}
1316-
1317-
case .buildSettings(let names):
1318-
return names.joined(separator: ", ")
1319-
1320-
case .buildFiles(let buildFiles, let targetGUID):
1321-
return "\(targetGUID): " + buildFiles.map { String(describing: $0) }.joined(separator: ", ")
1322-
1323-
case .unknown:
1324-
return nil
1325-
}
1326-
}
1327-
}
1328-
1329-
fileprivate extension BuildSystemCommand {
1330-
init(_ taskStartedInfo: SwiftBuildMessage.TaskStartedInfo, targetInfo: SwiftBuildMessage.TargetStartedInfo?) {
1331-
self = .init(
1332-
name: taskStartedInfo.executionDescription,
1333-
targetName: targetInfo?.targetName,
1334-
description: taskStartedInfo.commandLineDisplayString ?? "",
1335-
serializedDiagnosticPaths: taskStartedInfo.serializedDiagnosticsPaths.compactMap {
1336-
try? Basics.AbsolutePath(validating: $0.pathString)
1337-
}
1338-
)
1339-
}
1340-
}
1341-
13421167
fileprivate extension Triple {
13431168
var deploymentTargetSettingName: String? {
13441169
switch (self.os, self.environment) {

0 commit comments

Comments
 (0)