Skip to content

Commit 9913527

Browse files
authored
Merge pull request #2050 from hartbit/build-delegate-swift-tasks
Update the build animation for each Swift compiler task
2 parents 1c05042 + fcc65db commit 9913527

File tree

8 files changed

+311
-101
lines changed

8 files changed

+311
-101
lines changed

Sources/Build/BuildDelegate.swift

Lines changed: 211 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import Basic
1212
import SPMUtility
1313
import SPMLLBuild
1414
import Dispatch
15+
import Foundation
16+
import POSIX
1517

1618
/// Diagnostic error when a llbuild command encounters an error.
1719
struct LLBuildCommandErrorDiagnostic: DiagnosticData {
@@ -160,6 +162,21 @@ struct LLBuildCommandError: DiagnosticData {
160162
let message: String
161163
}
162164

165+
/// Swift Compiler output parsing error
166+
struct SwiftCompilerOutputParsingError: DiagnosticData {
167+
static let id = DiagnosticID(
168+
type: SwiftCompilerOutputParsingError.self,
169+
name: "org.swift.diags.swift-compiler-output-parsing-error",
170+
defaultBehavior: .error,
171+
description: {
172+
$0 <<< "failed parsing the Swift compiler output: "
173+
$0 <<< { $0.message }
174+
}
175+
)
176+
177+
let message: String
178+
}
179+
163180
extension SPMLLBuild.Diagnostic: DiagnosticDataConvertible {
164181
public var diagnosticData: DiagnosticData {
165182
switch kind {
@@ -171,44 +188,20 @@ extension SPMLLBuild.Diagnostic: DiagnosticDataConvertible {
171188
}
172189

173190
private let newLineByte: UInt8 = 10
174-
public final class BuildDelegate: BuildSystemDelegate {
175-
// Track counts of commands based on their CommandStatusKind
176-
private struct CommandCounter {
177-
var scanningCount = 0
178-
var upToDateCount = 0
179-
var completedCount = 0
180-
var startedCount = 0
181-
182-
var estimatedMaximum: Int {
183-
return completedCount + scanningCount - upToDateCount
184-
}
185-
186-
mutating func update(command: SPMLLBuild.Command, kind: CommandStatusKind) {
187-
guard command.shouldShowStatus else { return }
188-
189-
switch kind {
190-
case .isScanning:
191-
scanningCount += 1
192-
case .isUpToDate:
193-
scanningCount -= 1
194-
upToDateCount += 1
195-
completedCount += 1
196-
case .isComplete:
197-
scanningCount -= 1
198-
completedCount += 1
199-
}
200-
}
201-
}
202-
191+
public final class BuildDelegate: BuildSystemDelegate, SwiftCompilerOutputParserDelegate {
203192
private let diagnostics: DiagnosticsEngine
204193
public var outputStream: ThreadSafeOutputByteStream
205194
public var progressAnimation: ProgressAnimationProtocol
206-
public var isVerbose: Bool = false
207195
public var onCommmandFailure: (() -> Void)?
208-
private var commandCounter = CommandCounter()
196+
public var isVerbose: Bool = false
209197
private let queue = DispatchQueue(label: "org.swift.swiftpm.build-delegate")
198+
private var taskTracker = CommandTaskTracker()
199+
200+
/// Swift parsers keyed by llbuild command name.
201+
private var swiftParsers: [String: SwiftCompilerOutputParser] = [:]
210202

211203
public init(
204+
plan: BuildPlan,
212205
diagnostics: DiagnosticsEngine,
213206
outputStream: OutputByteStream,
214207
progressAnimation: ProgressAnimationProtocol
@@ -218,6 +211,12 @@ public final class BuildDelegate: BuildSystemDelegate {
218211
// https://forums.swift.org/t/allow-self-x-in-class-convenience-initializers/15924
219212
self.outputStream = outputStream as? ThreadSafeOutputByteStream ?? ThreadSafeOutputByteStream(outputStream)
220213
self.progressAnimation = progressAnimation
214+
215+
let buildConfig = plan.buildParameters.configuration.dirname
216+
swiftParsers = Dictionary(uniqueKeysWithValues: plan.targetMap.compactMap({ (target, description) in
217+
guard case .swift = description else { return nil }
218+
return (target.getCommandName(config: buildConfig), SwiftCompilerOutputParser(delegate: self))
219+
}))
221220
}
222221

223222
public var fs: SPMLLBuild.FileSystem? {
@@ -237,9 +236,6 @@ public final class BuildDelegate: BuildSystemDelegate {
237236
}
238237

239238
public func commandStatusChanged(_ command: SPMLLBuild.Command, kind: CommandStatusKind) {
240-
queue.sync {
241-
commandCounter.update(command: command, kind: kind)
242-
}
243239
}
244240

245241
public func commandPreparing(_ command: SPMLLBuild.Command) {
@@ -249,16 +245,12 @@ public final class BuildDelegate: BuildSystemDelegate {
249245
guard command.shouldShowStatus else { return }
250246

251247
queue.sync {
252-
commandCounter.startedCount += 1
253-
254248
if isVerbose {
255249
outputStream <<< command.verboseDescription <<< "\n"
256250
outputStream.flush()
257-
} else {
258-
progressAnimation.update(
259-
step: commandCounter.startedCount,
260-
total: commandCounter.estimatedMaximum,
261-
text: command.description)
251+
} else if !swiftParsers.keys.contains(command.name) {
252+
taskTracker.commandStarted(command)
253+
updateProgress()
262254
}
263255
}
264256
}
@@ -268,6 +260,14 @@ public final class BuildDelegate: BuildSystemDelegate {
268260
}
269261

270262
public func commandFinished(_ command: SPMLLBuild.Command, result: CommandResult) {
263+
guard command.shouldShowStatus else { return }
264+
guard !swiftParsers.keys.contains(command.name) else { return }
265+
guard !isVerbose else { return }
266+
267+
queue.sync {
268+
taskTracker.commandFinished(command, result: result)
269+
updateProgress()
270+
}
271271
}
272272

273273
public func commandHadError(_ command: SPMLLBuild.Command, message: String) {
@@ -302,9 +302,15 @@ public final class BuildDelegate: BuildSystemDelegate {
302302
}
303303

304304
public func commandProcessHadOutput(_ command: SPMLLBuild.Command, process: ProcessHandle, data: [UInt8]) {
305-
progressAnimation.clear()
306-
outputStream <<< data
307-
outputStream.flush()
305+
guard command.shouldShowStatus else { return }
306+
307+
if let swiftParser = swiftParsers[command.name] {
308+
swiftParser.parse(bytes: data)
309+
} else {
310+
progressAnimation.clear()
311+
outputStream <<< data
312+
outputStream.flush()
313+
}
308314
}
309315

310316
public func commandProcessFinished(
@@ -321,4 +327,165 @@ public final class BuildDelegate: BuildSystemDelegate {
321327
public func shouldResolveCycle(rules: [BuildKey], candidate: BuildKey, action: CycleAction) -> Bool {
322328
return false
323329
}
330+
331+
func swiftCompilerDidOutputMessage(_ message: SwiftCompilerMessage) {
332+
queue.sync {
333+
if isVerbose {
334+
if let text = message.verboseProgressText {
335+
outputStream <<< text <<< "\n"
336+
outputStream.flush()
337+
}
338+
} else {
339+
taskTracker.swiftCompilerDidOuputMessage(message)
340+
updateProgress()
341+
}
342+
343+
if let output = message.standardOutput {
344+
if !isVerbose {
345+
progressAnimation.clear()
346+
}
347+
348+
outputStream <<< output
349+
outputStream.flush()
350+
}
351+
}
352+
}
353+
354+
func swiftCompilerOutputParserDidFail(withError error: Error) {
355+
let message = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
356+
diagnostics.emit(data: SwiftCompilerOutputParsingError(message: message))
357+
onCommmandFailure?()
358+
}
359+
360+
private func updateProgress() {
361+
if let progressText = taskTracker.latestRunningText {
362+
progressAnimation.update(
363+
step: taskTracker.finishedCount,
364+
total: taskTracker.totalCount,
365+
text: progressText)
366+
}
367+
}
368+
}
369+
370+
/// Tracks tasks based on command status and swift compiler output.
371+
fileprivate struct CommandTaskTracker {
372+
private struct Task {
373+
let identifier: String
374+
let text: String
375+
}
376+
377+
private var tasks: [Task] = []
378+
private(set) var finishedCount = 0
379+
private(set) var totalCount = 0
380+
381+
/// The last task text before the task list was emptied.
382+
private var lastText: String?
383+
384+
var latestRunningText: String? {
385+
return tasks.last?.text ?? lastText
386+
}
387+
388+
mutating func commandStarted(_ command: SPMLLBuild.Command) {
389+
addTask(identifier: command.name, text: command.description)
390+
totalCount += 1
391+
}
392+
393+
mutating func commandFinished(_ command: SPMLLBuild.Command, result: CommandResult) {
394+
removeTask(identifier: command.name)
395+
396+
switch result {
397+
case .succeeded:
398+
finishedCount += 1
399+
case .cancelled, .failed, .skipped:
400+
break
401+
}
402+
}
403+
404+
mutating func swiftCompilerDidOuputMessage(_ message: SwiftCompilerMessage) {
405+
switch message.kind {
406+
case .began(let info):
407+
if let text = message.progressText {
408+
addTask(identifier: info.pid.description, text: text)
409+
}
410+
411+
totalCount += 1
412+
case .finished(let info):
413+
removeTask(identifier: info.pid.description)
414+
finishedCount += 1
415+
case .signalled(let info):
416+
removeTask(identifier: info.pid.description)
417+
case .skipped:
418+
break
419+
}
420+
}
421+
422+
private mutating func addTask(identifier: String, text: String) {
423+
tasks.append(Task(identifier: identifier, text: text))
424+
}
425+
426+
private mutating func removeTask(identifier: String) {
427+
if let index = tasks.index(where: { $0.identifier == identifier }) {
428+
if tasks.count == 1 {
429+
lastText = tasks[0].text
430+
}
431+
432+
tasks.remove(at: index)
433+
}
434+
}
435+
}
436+
437+
extension SwiftCompilerMessage {
438+
fileprivate var progressText: String? {
439+
if case .began(let info) = kind {
440+
switch name {
441+
case "compile":
442+
if let sourceFile = info.inputs.first {
443+
return generateProgressText(prefix: "Compiling", file: sourceFile)
444+
}
445+
case "link":
446+
if let imageFile = info.outputs.first(where: { $0.type == "image" })?.path {
447+
return generateProgressText(prefix: "Linking", file: imageFile)
448+
}
449+
case "merge-module":
450+
if let moduleFile = info.outputs.first(where: { $0.type == "swiftmodule" })?.path {
451+
return generateProgressText(prefix: "Merging module", file: moduleFile)
452+
}
453+
case "generate-dsym":
454+
if let dSYMFile = info.outputs.first(where: { $0.type == "dSYM" })?.path {
455+
return generateProgressText(prefix: "Generating dSYM", file: dSYMFile)
456+
}
457+
case "generate-pch":
458+
if let pchFile = info.outputs.first(where: { $0.type == "pch" })?.path {
459+
return generateProgressText(prefix: "Generating PCH", file: pchFile)
460+
}
461+
default:
462+
break
463+
}
464+
}
465+
466+
return nil
467+
}
468+
469+
fileprivate var verboseProgressText: String? {
470+
if case .began(let info) = kind {
471+
return ([info.commandExecutable] + info.commandArguments).joined(separator: " ")
472+
} else {
473+
return nil
474+
}
475+
}
476+
477+
fileprivate var standardOutput: String? {
478+
switch kind {
479+
case .finished(let info),
480+
.signalled(let info):
481+
return info.output
482+
default:
483+
return nil
484+
}
485+
}
486+
487+
private func generateProgressText(prefix: String, file: String) -> String {
488+
let relativePath = AbsolutePath(file).relative(to: AbsolutePath(getcwd()))
489+
return "\(prefix) \(relativePath)"
490+
}
324491
}

Sources/Build/BuildPlan.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,7 @@ public final class SwiftTargetBuildDescription {
511511
args += additionalFlags
512512
args += moduleCacheArgs
513513
args += buildParameters.sanitizers.compileSwiftFlags()
514+
args += ["-parseable-output"]
514515

515516
// Add arguments needed for code coverage if it is enabled.
516517
if buildParameters.enableCodeCoverage {

0 commit comments

Comments
 (0)