@@ -12,6 +12,8 @@ import Basic
12
12
import SPMUtility
13
13
import SPMLLBuild
14
14
import Dispatch
15
+ import Foundation
16
+ import POSIX
15
17
16
18
/// Diagnostic error when a llbuild command encounters an error.
17
19
struct LLBuildCommandErrorDiagnostic : DiagnosticData {
@@ -160,6 +162,21 @@ struct LLBuildCommandError: DiagnosticData {
160
162
let message : String
161
163
}
162
164
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
+
163
180
extension SPMLLBuild . Diagnostic : DiagnosticDataConvertible {
164
181
public var diagnosticData : DiagnosticData {
165
182
switch kind {
@@ -171,44 +188,20 @@ extension SPMLLBuild.Diagnostic: DiagnosticDataConvertible {
171
188
}
172
189
173
190
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 {
203
192
private let diagnostics : DiagnosticsEngine
204
193
public var outputStream : ThreadSafeOutputByteStream
205
194
public var progressAnimation : ProgressAnimationProtocol
206
- public var isVerbose : Bool = false
207
195
public var onCommmandFailure : ( ( ) -> Void ) ?
208
- private var commandCounter = CommandCounter ( )
196
+ public var isVerbose : Bool = false
209
197
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 ] = [ : ]
210
202
211
203
public init (
204
+ plan: BuildPlan ,
212
205
diagnostics: DiagnosticsEngine ,
213
206
outputStream: OutputByteStream ,
214
207
progressAnimation: ProgressAnimationProtocol
@@ -218,6 +211,12 @@ public final class BuildDelegate: BuildSystemDelegate {
218
211
// https://forums.swift.org/t/allow-self-x-in-class-convenience-initializers/15924
219
212
self . outputStream = outputStream as? ThreadSafeOutputByteStream ?? ThreadSafeOutputByteStream ( outputStream)
220
213
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
+ } ) )
221
220
}
222
221
223
222
public var fs : SPMLLBuild . FileSystem ? {
@@ -237,9 +236,6 @@ public final class BuildDelegate: BuildSystemDelegate {
237
236
}
238
237
239
238
public func commandStatusChanged( _ command: SPMLLBuild . Command , kind: CommandStatusKind ) {
240
- queue. sync {
241
- commandCounter. update ( command: command, kind: kind)
242
- }
243
239
}
244
240
245
241
public func commandPreparing( _ command: SPMLLBuild . Command ) {
@@ -249,16 +245,12 @@ public final class BuildDelegate: BuildSystemDelegate {
249
245
guard command. shouldShowStatus else { return }
250
246
251
247
queue. sync {
252
- commandCounter. startedCount += 1
253
-
254
248
if isVerbose {
255
249
outputStream <<< command. verboseDescription <<< " \n "
256
250
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 ( )
262
254
}
263
255
}
264
256
}
@@ -268,6 +260,14 @@ public final class BuildDelegate: BuildSystemDelegate {
268
260
}
269
261
270
262
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
+ }
271
271
}
272
272
273
273
public func commandHadError( _ command: SPMLLBuild . Command , message: String ) {
@@ -302,9 +302,15 @@ public final class BuildDelegate: BuildSystemDelegate {
302
302
}
303
303
304
304
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
+ }
308
314
}
309
315
310
316
public func commandProcessFinished(
@@ -321,4 +327,165 @@ public final class BuildDelegate: BuildSystemDelegate {
321
327
public func shouldResolveCycle( rules: [ BuildKey ] , candidate: BuildKey , action: CycleAction ) -> Bool {
322
328
return false
323
329
}
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
+ }
324
491
}
0 commit comments