Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
235dd3e
Forray into capturing swift compiler output logs
bripeticca Nov 5, 2025
f9dfdf3
Revert usage of JSON parser; selectively emit DiagnosticInfo
bripeticca Nov 11, 2025
b1ff231
Implement per-task-buffer of Data output
bripeticca Nov 13, 2025
ac6b81b
Fallback to locationContext if locationContext2 properties are nil
bripeticca Nov 13, 2025
1ae0f38
Merge branch 'main' into swb/diagnosticcodesnippet
bripeticca Nov 13, 2025
b81bfca
Cleanup; add descriptions related to redundant task output
bripeticca Nov 13, 2025
f3aaabf
attempt to parse decoded string into individual diagnostics
bripeticca Nov 20, 2025
c48e606
cleanup
bripeticca Nov 20, 2025
f14600c
Revert diagnostic parsing and emit directly to outputStream
bripeticca Nov 21, 2025
359331a
Address PR comments
bripeticca Nov 21, 2025
56f0a45
implement generic print method for observability scope
bripeticca Nov 24, 2025
dd1505a
Introduce model to store data buffer per task type
bripeticca Nov 25, 2025
08306e2
minor changes to TaskDataBuffer + cleanup
bripeticca Nov 25, 2025
424492f
Modify emission of command line display strings
bripeticca Nov 27, 2025
4074c59
cleanup; stronger assertions for redundant task output
bripeticca Nov 27, 2025
05ee043
Fix protocol adherence errors
bripeticca Dec 2, 2025
252286b
Create test suite for SwiftBuildSystemMessageHandler
bripeticca Dec 2, 2025
a658f58
Modify test mocks using exposed memberwise inits
bripeticca Dec 4, 2025
7a00f35
Address PR comments
bripeticca Dec 4, 2025
e88ab96
Fix check on global task for LocationContext
bripeticca Dec 5, 2025
5f0911d
Track serialized diagnostic path by targetID
bripeticca Dec 5, 2025
8f713a0
Merge branch 'main' into swb/diagnosticcodesnippet
bripeticca Dec 5, 2025
fd21a42
Add FIXME + richer model to track emitted tasks
bripeticca Dec 8, 2025
7a91843
Merge branch 'main' into swb/diagnosticcodesnippet
bripeticca Dec 8, 2025
4e29267
Add documentation
bripeticca Dec 9, 2025
45dc48c
Merge branch 'main' into swb/diagnosticcodesnippet
bripeticca Dec 9, 2025
f5dd65c
remove old print method
bripeticca Dec 9, 2025
11d3945
Merge branch 'swb/diagnosticcodesnippet' into swb/messagehandlertests
bripeticca Dec 9, 2025
30fc79a
Create new testing observability system that can be supplied an outpu…
bripeticca Dec 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Sources/Basics/Observability.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ public class ObservabilitySystem {
func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic) {
self.underlying(scope, diagnostic)
}

func print(_ output: String, verbose: Bool) {
self.diagnosticsHandler.print(output, verbose: verbose)
}
}

public static var NOOP: ObservabilityScope {
Expand Down Expand Up @@ -128,6 +132,10 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol, Sendable, Cus
return parent?.errorsReportedInAnyScope ?? false
}

public func print(_ output: String, verbose: Bool) {
self.diagnosticsHandler.print(output, verbose: verbose)
}

// DiagnosticsEmitterProtocol
public func emit(_ diagnostic: Diagnostic) {
var diagnostic = diagnostic
Expand All @@ -150,6 +158,10 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol, Sendable, Cus
self.underlying.handleDiagnostic(scope: scope, diagnostic: diagnostic)
}

public func print(_ output: String, verbose: Bool) {
self.underlying.print(output, verbose: verbose)
}

var errorsReported: Bool {
self._errorsReported.get() ?? false
}
Expand All @@ -160,6 +172,8 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol, Sendable, Cus

public protocol DiagnosticsHandler: Sendable {
func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic)

func print(_ output: String, verbose: Bool)
}

/// Helper protocol to share default behavior.
Expand Down
6 changes: 6 additions & 0 deletions Sources/CoreCommands/SwiftCommandObservabilityHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ public struct SwiftCommandObservabilityHandler: ObservabilityHandlerProvider {
}
}

func printToOutput(message: String) {
self.queue.async(group: self.sync) {
self.write(message)
}
}

// for raw output reporting
func print(_ output: String, verbose: Bool) {
self.queue.async(group: self.sync) {
Expand Down
3 changes: 2 additions & 1 deletion Sources/SwiftBuildSupport/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ add_library(SwiftBuildSupport STATIC
PIF.swift
PIFBuilder.swift
PluginConfiguration.swift
SwiftBuildSystem.swift)
SwiftBuildSystem.swift
SwiftBuildSystemMessageHandler.swift)
target_link_libraries(SwiftBuildSupport PUBLIC
Build
DriverSupport
Expand Down
203 changes: 12 additions & 191 deletions Sources/SwiftBuildSupport/SwiftBuildSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {

public var hasIntegratedAPIDigesterSupport: Bool { true }

public var enableTaskBacktraces: Bool {
self.buildParameters.outputParameters.enableTaskBacktraces
}

public init(
buildParameters: BuildParameters,
packageGraphLoader: @escaping () async throws -> ModulesGraph,
Expand Down Expand Up @@ -537,12 +541,12 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
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(
observabilityScope: self.observabilityScope,
outputStream: self.outputStream,
logLevel: self.logLevel
)

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")
Expand Down Expand Up @@ -582,173 +586,30 @@ 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] = [:]
var collectedBacktraceFrames = SWBBuildOperationCollectedBacktraceFrames()

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):
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 ?? "<no command line>")")
}
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)
})
}
if self.buildParameters.outputParameters.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):
if self.buildParameters.outputParameters.enableTaskBacktraces {
buildState.collectedBacktraceFrames.add(frame: info)
}
case .planningOperationStarted, .planningOperationCompleted, .reportBuildDescription, .reportPathMap, .preparedForIndex, .buildStarted, .preparationComplete, .targetUpToDate, .targetComplete, .taskUpToDate:
break
case .buildDiagnostic, .targetDiagnostic, .taskDiagnostic:
break // deprecated
case .buildOutput, .targetOutput, .taskOutput:
break // deprecated
@unknown default:
break
}
}

let operation = try await session.createBuildOperation(
request: request,
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 {
self.observabilityScope.emit(debug: "build unexpectedly reported multiple build description IDs")
}
buildDescriptionID = SWBBuildDescriptionID(info.buildDescriptionID)
}
try emitEvent(event, buildState: &buildState)
try buildMessageHandler.emitEvent(event, self)
}

await operation.waitForCompletion()

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")
Expand Down Expand Up @@ -815,7 +676,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"]
Expand Down Expand Up @@ -1247,46 +1108,6 @@ extension String {
}
}

fileprivate extension SwiftBuild.SwiftBuildMessage.DiagnosticInfo.Location {
var userDescription: String? {
switch self {
case .path(let path, let fileLocation):
switch fileLocation {
case .textual(let line, let column):
var description = "\(path):\(line)"
if let column { description += ":\(column)" }
return description
case .object(let identifier):
return "\(path):\(identifier)"
case .none:
return path
}

case .buildSettings(let names):
return names.joined(separator: ", ")

case .buildFiles(let buildFiles, let targetGUID):
return "\(targetGUID): " + buildFiles.map { String(describing: $0) }.joined(separator: ", ")

case .unknown:
return nil
}
}
}

fileprivate extension BuildSystemCommand {
init(_ taskStartedInfo: SwiftBuildMessage.TaskStartedInfo, targetInfo: SwiftBuildMessage.TargetStartedInfo?) {
self = .init(
name: taskStartedInfo.executionDescription,
targetName: targetInfo?.targetName,
description: taskStartedInfo.commandLineDisplayString ?? "",
serializedDiagnosticPaths: taskStartedInfo.serializedDiagnosticsPaths.compactMap {
try? Basics.AbsolutePath(validating: $0.pathString)
}
)
}
}

fileprivate extension Triple {
var deploymentTargetSettingName: String? {
switch (self.os, self.environment) {
Expand Down
Loading
Loading