Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
255 changes: 211 additions & 44 deletions Sources/Build/BuildDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import Basic
import SPMUtility
import SPMLLBuild
import Dispatch
import Foundation
import POSIX

/// Diagnostic error when a llbuild command encounters an error.
struct LLBuildCommandErrorDiagnostic: DiagnosticData {
Expand Down Expand Up @@ -160,6 +162,21 @@ struct LLBuildCommandError: DiagnosticData {
let message: String
}

/// Swift Compiler output parsing error
struct SwiftCompilerOutputParsingError: DiagnosticData {
static let id = DiagnosticID(
type: SwiftCompilerOutputParsingError.self,
name: "org.swift.diags.swift-compiler-output-parsing-error",
defaultBehavior: .error,
description: {
$0 <<< "failed parsing the Swift compiler output: "
$0 <<< { $0.message }
}
)

let message: String
}

extension SPMLLBuild.Diagnostic: DiagnosticDataConvertible {
public var diagnosticData: DiagnosticData {
switch kind {
Expand All @@ -171,44 +188,20 @@ extension SPMLLBuild.Diagnostic: DiagnosticDataConvertible {
}

private let newLineByte: UInt8 = 10
public final class BuildDelegate: BuildSystemDelegate {
// Track counts of commands based on their CommandStatusKind
private struct CommandCounter {
var scanningCount = 0
var upToDateCount = 0
var completedCount = 0
var startedCount = 0

var estimatedMaximum: Int {
return completedCount + scanningCount - upToDateCount
}

mutating func update(command: SPMLLBuild.Command, kind: CommandStatusKind) {
guard command.shouldShowStatus else { return }

switch kind {
case .isScanning:
scanningCount += 1
case .isUpToDate:
scanningCount -= 1
upToDateCount += 1
completedCount += 1
case .isComplete:
scanningCount -= 1
completedCount += 1
}
}
}

public final class BuildDelegate: BuildSystemDelegate, SwiftCompilerOutputParserDelegate {
private let diagnostics: DiagnosticsEngine
public var outputStream: ThreadSafeOutputByteStream
public var progressAnimation: ProgressAnimationProtocol
public var isVerbose: Bool = false
public var onCommmandFailure: (() -> Void)?
private var commandCounter = CommandCounter()
public var isVerbose: Bool = false
private let queue = DispatchQueue(label: "org.swift.swiftpm.build-delegate")
private var taskTracker = CommandTaskTracker()

/// Swift parsers keyed by llbuild command name.
private var swiftParsers: [String: SwiftCompilerOutputParser] = [:]

public init(
plan: BuildPlan,
diagnostics: DiagnosticsEngine,
outputStream: OutputByteStream,
progressAnimation: ProgressAnimationProtocol
Expand All @@ -218,6 +211,12 @@ public final class BuildDelegate: BuildSystemDelegate {
// https://forums.swift.org/t/allow-self-x-in-class-convenience-initializers/15924
self.outputStream = outputStream as? ThreadSafeOutputByteStream ?? ThreadSafeOutputByteStream(outputStream)
self.progressAnimation = progressAnimation

let buildConfig = plan.buildParameters.configuration.dirname
swiftParsers = Dictionary(uniqueKeysWithValues: plan.targetMap.compactMap({ (target, description) in
guard case .swift = description else { return nil }
return (target.getCommandName(config: buildConfig), SwiftCompilerOutputParser(delegate: self))
}))
}

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

public func commandStatusChanged(_ command: SPMLLBuild.Command, kind: CommandStatusKind) {
queue.sync {
commandCounter.update(command: command, kind: kind)
}
}

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

queue.sync {
commandCounter.startedCount += 1

if isVerbose {
outputStream <<< command.verboseDescription <<< "\n"
outputStream.flush()
} else {
progressAnimation.update(
step: commandCounter.startedCount,
total: commandCounter.estimatedMaximum,
text: command.description)
} else if !swiftParsers.keys.contains(command.name) {
taskTracker.commandStarted(command)
updateProgress()
}
}
}
Expand All @@ -268,6 +260,14 @@ public final class BuildDelegate: BuildSystemDelegate {
}

public func commandFinished(_ command: SPMLLBuild.Command, result: CommandResult) {
guard command.shouldShowStatus else { return }
guard !swiftParsers.keys.contains(command.name) else { return }
guard !isVerbose else { return }

queue.sync {
taskTracker.commandFinished(command, result: result)
updateProgress()
}
}

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

public func commandProcessHadOutput(_ command: SPMLLBuild.Command, process: ProcessHandle, data: [UInt8]) {
progressAnimation.clear()
outputStream <<< data
outputStream.flush()
guard command.shouldShowStatus else { return }

if let swiftParser = swiftParsers[command.name] {
swiftParser.parse(bytes: data)
} else {
progressAnimation.clear()
outputStream <<< data
outputStream.flush()
}
}

public func commandProcessFinished(
Expand All @@ -321,4 +327,165 @@ public final class BuildDelegate: BuildSystemDelegate {
public func shouldResolveCycle(rules: [BuildKey], candidate: BuildKey, action: CycleAction) -> Bool {
return false
}

func swiftCompilerDidOutputMessage(_ message: SwiftCompilerMessage) {
queue.sync {
if isVerbose {
if let text = message.verboseProgressText {
outputStream <<< text <<< "\n"
outputStream.flush()
}
} else {
taskTracker.swiftCompilerDidOuputMessage(message)
updateProgress()
}

if let output = message.standardOutput {
if !isVerbose {
progressAnimation.clear()
}

outputStream <<< output
outputStream.flush()
}
}
}

func swiftCompilerOutputParserDidFail(withError error: Error) {
let message = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
diagnostics.emit(data: SwiftCompilerOutputParsingError(message: message))
onCommmandFailure?()
}

private func updateProgress() {
if let progressText = taskTracker.latestRunningText {
progressAnimation.update(
step: taskTracker.finishedCount,
total: taskTracker.totalCount,
text: progressText)
}
}
}

/// Tracks tasks based on command status and swift compiler output.
fileprivate struct CommandTaskTracker {
private struct Task {
let identifier: String
let text: String
}

private var tasks: [Task] = []
private(set) var finishedCount = 0
private(set) var totalCount = 0

/// The last task text before the task list was emptied.
private var lastText: String?

var latestRunningText: String? {
return tasks.last?.text ?? lastText
}

mutating func commandStarted(_ command: SPMLLBuild.Command) {
addTask(identifier: command.name, text: command.description)
totalCount += 1
}

mutating func commandFinished(_ command: SPMLLBuild.Command, result: CommandResult) {
removeTask(identifier: command.name)

switch result {
case .succeeded:
finishedCount += 1
case .cancelled, .failed, .skipped:
break
}
}

mutating func swiftCompilerDidOuputMessage(_ message: SwiftCompilerMessage) {
switch message.kind {
case .began(let info):
if let text = message.progressText {
addTask(identifier: info.pid.description, text: text)
}

totalCount += 1
case .finished(let info):
removeTask(identifier: info.pid.description)
finishedCount += 1
case .signalled(let info):
removeTask(identifier: info.pid.description)
case .skipped:
break
}
}

private mutating func addTask(identifier: String, text: String) {
tasks.append(Task(identifier: identifier, text: text))
}

private mutating func removeTask(identifier: String) {
if let index = tasks.index(where: { $0.identifier == identifier }) {
if tasks.count == 1 {
lastText = tasks[0].text
}

tasks.remove(at: index)
}
}
}

extension SwiftCompilerMessage {
fileprivate var progressText: String? {
if case .began(let info) = kind {
switch name {
case "compile":
if let sourceFile = info.inputs.first {
return generateProgressText(prefix: "Compiling", file: sourceFile)
}
case "link":
if let imageFile = info.outputs.first(where: { $0.type == "image" })?.path {
return generateProgressText(prefix: "Linking", file: imageFile)
}
case "merge-module":
if let moduleFile = info.outputs.first(where: { $0.type == "swiftmodule" })?.path {
return generateProgressText(prefix: "Merging module", file: moduleFile)
}
case "generate-dsym":
if let dSYMFile = info.outputs.first(where: { $0.type == "dSYM" })?.path {
return generateProgressText(prefix: "Generating dSYM", file: dSYMFile)
}
case "generate-pch":
if let pchFile = info.outputs.first(where: { $0.type == "pch" })?.path {
return generateProgressText(prefix: "Generating PCH", file: pchFile)
}
default:
break
}
}

return nil
}

fileprivate var verboseProgressText: String? {
if case .began(let info) = kind {
return ([info.commandExecutable] + info.commandArguments).joined(separator: " ")
} else {
return nil
}
}

fileprivate var standardOutput: String? {
switch kind {
case .finished(let info),
.signalled(let info):
return info.output
default:
return nil
}
}

private func generateProgressText(prefix: String, file: String) -> String {
let relativePath = AbsolutePath(file).relative(to: AbsolutePath(getcwd()))
return "\(prefix) \(relativePath)"
}
}
1 change: 1 addition & 0 deletions Sources/Build/BuildPlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ public final class SwiftTargetBuildDescription {
args += additionalFlags
args += moduleCacheArgs
args += buildParameters.sanitizers.compileSwiftFlags()
args += ["-parseable-output"]

// Add arguments needed for code coverage if it is enabled.
if buildParameters.enableCodeCoverage {
Expand Down
Loading