Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
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
31 changes: 23 additions & 8 deletions Sources/Basics/ProgressAnimation/NinjaProgressAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ extension ProgressAnimation {
@_spi(SwiftPMInternal)
public static func ninja(
stream: WritableByteStream,
verbose: Bool
verbose: Bool,
normalizeStep: Bool = true
) -> any ProgressAnimationProtocol {
Self.dynamic(
stream: stream,
verbose: verbose,
ttyTerminalAnimationFactory: { RedrawingNinjaProgressAnimation(terminal: $0) },
ttyTerminalAnimationFactory: { RedrawingNinjaProgressAnimation(terminal: $0, normalizeStep: normalizeStep) },
dumbTerminalAnimationFactory: { SingleLinePercentProgressAnimation(stream: stream, header: nil) },
defaultAnimationFactory: { MultiLineNinjaProgressAnimation(stream: stream) }
defaultAnimationFactory: { MultiLineNinjaProgressAnimation(stream: stream, normalizeStep: normalizeStep) }
)
}
}
Expand All @@ -34,17 +35,24 @@ extension ProgressAnimation {
final class RedrawingNinjaProgressAnimation: ProgressAnimationProtocol {
private let terminal: TerminalController
private var hasDisplayedProgress = false
private let normalizeStep: Bool

init(terminal: TerminalController) {
init(terminal: TerminalController, normalizeStep: Bool) {
self.terminal = terminal
self.normalizeStep = normalizeStep
}

func update(step: Int, total: Int, text: String) {
assert(step <= total)

terminal.clearLine()

let progressText = "[\(step)/\(total)] \(text)"
var progressText = ""
if step < 0 && normalizeStep || step >= 0 {
let normalizedStep = max(0, step)
progressText = "[\(normalizedStep)/\(total)] \(text)"
} else {
progressText = "\(text)"
}
let width = terminal.width
if progressText.utf8.count > width {
let suffix = "…"
Expand Down Expand Up @@ -78,17 +86,24 @@ final class MultiLineNinjaProgressAnimation: ProgressAnimationProtocol {

private let stream: WritableByteStream
private var lastDisplayedText: String? = nil
private let normalizeStep: Bool

init(stream: WritableByteStream) {
init(stream: WritableByteStream, normalizeStep: Bool) {
self.stream = stream
self.normalizeStep = normalizeStep
}

func update(step: Int, total: Int, text: String) {
assert(step <= total)

guard text != lastDisplayedText else { return }

stream.send("[\(step)/\(total)] ").send(text)
if (step < 0 && normalizeStep) || step >= 0 {
let normalizedStep = max(0, step)
stream.send("[\(normalizedStep)/\(total)] ")
}

stream.send(text)
stream.send("\n")
stream.flush()
lastDisplayedText = text
Expand Down
2 changes: 1 addition & 1 deletion Sources/Commands/CommandWorkspaceDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ final class CommandWorkspaceDelegate: WorkspaceDelegate {
}

func didComputeVersion(package: PackageIdentity, location: String, version: String, duration: DispatchTimeInterval) {
self.outputHandler("Computed \(location) at \(version) (\(duration.descriptionInSeconds))", false)
self.outputHandler("Computed \(location) at \(version) (\(duration.descriptionInSeconds))", true)
}

func willDownloadBinaryArtifact(from url: String, fromCache: Bool) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public struct SwiftCommandObservabilityHandler: ObservabilityHandlerProvider {
// for raw output reporting
func print(_ output: String, verbose: Bool) {
self.queue.async(group: self.sync) {
guard !verbose || self.logLevel.isVerbose else {
guard verbose || self.logLevel.isVerbose else {
return
}
self.write(output)
Expand Down
107 changes: 74 additions & 33 deletions Sources/SwiftBuildSupport/SwiftBuildSystemMessageHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ public final class SwiftBuildSystemMessageHandler {
self.logLevel = logLevel
self.progressAnimation = ProgressAnimation.ninja(
stream: outputStream,
verbose: self.logLevel.isVerbose
verbose: self.logLevel.isVerbose,
normalizeStep: false
)
self.enableBacktraces = enableBacktraces
self.buildDelegate = buildDelegate
Expand Down Expand Up @@ -94,7 +95,7 @@ public final class SwiftBuildSystemMessageHandler {
}
}

private func emitDiagnosticCompilerOutput(_ info: SwiftBuildMessage.TaskStartedInfo) {
private func emitDiagnosticCompilerOutput(_ info: SwiftBuildMessage.TaskStartedInfo, verbose: Bool) {
// Don't redundantly emit task output.
guard !self.tasksEmitted.contains(info.taskSignature) else {
return
Expand All @@ -108,7 +109,7 @@ public final class SwiftBuildSystemMessageHandler {
let decodedOutput = String(decoding: buffer, as: UTF8.self)

// Emit message.
observabilityScope.print(decodedOutput, verbose: self.logLevel.isVerbose)
observabilityScope.print(decodedOutput, verbose: verbose)

// Record that we've emitted the output for a given task.
self.tasksEmitted.insert(info)
Expand All @@ -121,12 +122,11 @@ public final class SwiftBuildSystemMessageHandler {
) throws {
// Begin by emitting the text received by the task started event.
if let started = self.buildState.startedInfo(for: startedInfo) {
// Determine where to emit depending on the verbosity level.
if self.logLevel.isVerbose {
self.outputStream.send(started.description + "\n")
} else {
observabilityScope.emit(info: started)
// Emit depending on verbosity level.
if logLevel.isVerbose {
self.outputStream.send(started)
}
self.observabilityScope.print(started, verbose: self.logLevel.isVerbose)
}

guard info.result == .success else {
Expand All @@ -135,13 +135,15 @@ public final class SwiftBuildSystemMessageHandler {
}

// Handle diagnostics, if applicable.
// This handles diagnostics for successful tasks, which could be notes or warnings.
let diagnostics = self.buildState.diagnostics(for: info)
if !diagnostics.isEmpty {
// Emit diagnostics using the `DiagnosticInfo` model.
diagnostics.forEach({ emitInfoAsDiagnostic(info: $0) })
} else {
// Emit diagnostics through textual compiler output.
emitDiagnosticCompilerOutput(startedInfo)
let isDiagnosticOutput = self.buildState.diagnosticDataBufferExists(for: info)
emitDiagnosticCompilerOutput(startedInfo, verbose: isDiagnosticOutput || self.logLevel.isVerbose)
}

// Handle task backtraces, if applicable.
Expand Down Expand Up @@ -181,7 +183,7 @@ public final class SwiftBuildSystemMessageHandler {
if !diagnosticsBuffer.isEmpty {
diagnosticsBuffer.forEach({ emitInfoAsDiagnostic(info: $0) })
} else {
emitDiagnosticCompilerOutput(startedInfo)
emitDiagnosticCompilerOutput(startedInfo, verbose: true)
}

let message = "\(startedInfo.ruleInfo) failed with a nonzero exit code."
Expand Down Expand Up @@ -216,14 +218,18 @@ public final class SwiftBuildSystemMessageHandler {
}
}
case .didUpdateProgress(let progressInfo):
var step = Int(progressInfo.percentComplete)
if step < 0 { step = 0 }
let step = Int(progressInfo.percentComplete)
let message = if let targetName = progressInfo.targetName {
"\(targetName) \(progressInfo.message)"
} else {
"\(progressInfo.message)"
}
progressAnimation.update(step: step, total: 100, text: message)

// Skip if message doesn't contain anything useful to display.
if message.contains(where: \.isLetter) {
progressAnimation.update(step: step, total: 100, text: message)
}

callback = { [weak self] buildSystem in
self?.buildDelegate?.buildSystem(buildSystem, didUpdateTaskProgress: message)
}
Expand All @@ -235,12 +241,15 @@ public final class SwiftBuildSystemMessageHandler {
emitInfoAsDiagnostic(info: info)
} else if info.appendToOutputStream {
buildState.appendDiagnostic(info)
} else {
// Track task IDs for diagnostics to later emit them via compiler output.
buildState.appendDiagnosticID(info)
}
case .output(let info):
// Append to buffer-per-task storage
buildState.appendToBuffer(info)
case .taskStarted(let info):
try buildState.started(task: info)
try buildState.started(task: info, self.logLevel)

let targetInfo = try buildState.target(for: info)
callback = { [weak self] buildSystem in
Expand All @@ -250,8 +259,8 @@ public final class SwiftBuildSystemMessageHandler {
case .taskComplete(let info):
let startedInfo = try buildState.completed(task: info)

// Handler for failed tasks, if applicable.
try handleTaskOutput(info, startedInfo, self.enableBacktraces)
// Handler for task output, handling failures if applicable.
try self.handleTaskOutput(info, startedInfo, self.enableBacktraces)

let targetInfo = try buildState.target(for: startedInfo)
callback = { [weak self] buildSystem in
Expand All @@ -270,7 +279,23 @@ public final class SwiftBuildSystemMessageHandler {
}
case .targetComplete(let info):
_ = try buildState.completed(target: info)
case .planningOperationStarted, .planningOperationCompleted, .reportBuildDescription, .reportPathMap, .preparedForIndex, .buildStarted, .preparationComplete, .targetUpToDate, .taskUpToDate:
case .planningOperationStarted(_):
// Emitting under higher-level verbosity so as not to overwhelm output.
// This is the same behaviour as the native system.
if self.logLevel.isVerbose {
self.outputStream.send("Planning build" + "\n")
}
case .planningOperationCompleted(_):
// Emitting under higher-level verbosity so as not to overwhelm output.
if self.logLevel.isVerbose {
self.outputStream.send("Planning complete" + "\n")
}
case .targetUpToDate(let info):
// Received when a target is entirely up to date and did not need to be built.
if self.logLevel.isVerbose {
self.outputStream.send("Target \(info.guid) up to date." + "\n")
}
case .reportBuildDescription, .reportPathMap, .preparedForIndex, .buildStarted, .preparationComplete, .taskUpToDate:
break
case .buildDiagnostic, .targetDiagnostic, .taskDiagnostic:
break // deprecated
Expand Down Expand Up @@ -303,27 +328,28 @@ extension SwiftBuildSystemMessageHandler {
// Per-task buffers
private var taskDataBuffer: TaskDataBuffer = .init()
private var diagnosticsBuffer: TaskDiagnosticBuffer = .init()
private var diagnosticTaskIDs: Set<Int> = []

// Backtrace frames
internal var collectedBacktraceFrames = SWBBuildOperationCollectedBacktraceFrames()

/// Registers the start of a build task, validating that the task hasn't already been started.
/// - Parameter task: The task start information containing task ID and signature
/// - Throws: Fatal error if the task is already active
mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) throws {
mutating func started(task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo, _ logLevel: Basics.Diagnostic.Severity) throws {
if activeTasks[task.taskID] != nil {
throw Diagnostics.fatalError
}
activeTasks[task.taskID] = task
taskIDToSignature[task.taskID] = task.taskSignature

// Track relevant task info to emit to user.
let output = if let cmdLineDisplayStr = task.commandLineDisplayString {
let output = if let cmdLineDisplayStr = task.commandLineDisplayString, logLevel.isVerbose {
"\(task.executionDescription)\n\(cmdLineDisplayStr)"
} else {
task.executionDescription
}
taskDataBuffer[task] = output
taskDataBuffer.setTaskStartedInfo(task, output)
}

/// Marks a task as completed and removes it from active tracking.
Expand Down Expand Up @@ -486,17 +512,16 @@ extension SwiftBuildSystemMessageHandler.BuildState {
}
}

subscript(task: SwiftBuildMessage.TaskStartedInfo) -> String? {
get {
guard let result = taskStartedNotifications[task.taskID] else {
return nil
}

return result
}
set {
self.taskStartedNotifications[task.taskID] = newValue
func taskStartedInfo(_ task: SwiftBuildMessage.TaskStartedInfo) -> String? {
guard let result = taskStartedNotifications[task.taskID] else {
return nil
}

return result
}

mutating func setTaskStartedInfo(_ task: SwiftBuildMessage.TaskStartedInfo, _ text: String) {
self.taskStartedNotifications[task.taskID] = text
}
}

Expand Down Expand Up @@ -536,7 +561,7 @@ extension SwiftBuildSystemMessageHandler.BuildState {
}

func startedInfo(for task: SwiftBuild.SwiftBuildMessage.TaskStartedInfo) -> String? {
return self.taskDataBuffer[task]
return self.taskDataBuffer.taskStartedInfo(task)
}
}

Expand Down Expand Up @@ -619,11 +644,28 @@ extension SwiftBuildSystemMessageHandler.BuildState {
diagnosticsBuffer[taskID].append(info)
}

/// Appends a diagnostic task ID to the appropriate diagnostic buffer.
/// - Parameter info: The diagnostic information to store, containing location context for identification
mutating func appendDiagnosticID(_ info: SwiftBuildMessage.DiagnosticInfo) {
guard let taskID = info.locationContext.taskID else {
return
}

self.diagnosticTaskIDs.insert(taskID)
}

/// Retrieves all diagnostic information for a completed task.
/// - Parameter task: The task completion information containing the task ID
/// - Returns: Array of diagnostic info associated with the task
func diagnostics(for task: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo) -> [SwiftBuildMessage.DiagnosticInfo] {
return diagnosticsBuffer[task.taskID]
return self.diagnosticsBuffer[task.taskID]
}

/// Determines whether there is a data buffer for the given diagnostic task ID.
/// - Parameter task: The task completion information containing the task ID
/// - Returns: A Bool that indicates whether a data buffer entry exists for the given task ID.
func diagnosticDataBufferExists(for task: SwiftBuild.SwiftBuildMessage.TaskCompleteInfo) -> Bool {
return self.diagnosticTaskIDs.contains(task.taskID)
}
}

Expand Down Expand Up @@ -770,7 +812,6 @@ extension SwiftBuildMessage.LocationContext {
}
}


fileprivate extension SwiftBuild.SwiftBuildMessage.DiagnosticInfo.Location {
var userDescription: String? {
switch self {
Expand Down
4 changes: 1 addition & 3 deletions Tests/BuildTests/BuildSystemDelegateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ struct BuildSystemDelegateTests {
let (stdout, stderr) = try await executeSwiftBuild(
fixturePath,
configuration: data.config,
// extraArgs: ["--verbose"],
buildSystem: data.buildSystem,
)
switch data.buildSystem {
Expand All @@ -45,13 +44,12 @@ struct BuildSystemDelegateTests {
"log didn't contain expected linker diagnostics. stderr: '\(stderr)')",
)
case .swiftbuild:
let searchPathRegex = try Regex("warning:(.*)Search path 'foobar' not found")
#expect(
stderr.contains("ld: warning: search path 'foobar' not found"),
"log didn't contain expected linker diagnostics. stderr: '\(stderr)",
)
#expect(
!stdout.contains(searchPathRegex),
!stdout.contains("ld: warning: search path 'foobar' not found"),
"log didn't contain expected linker diagnostics. stderr: '\(stderr)')",
)
case .xcode:
Expand Down
Loading
Loading