From eb0a203ad6b3757e0ce0378c0bf088531085e716 Mon Sep 17 00:00:00 2001 From: Euan Harris Date: Thu, 11 Jan 2024 13:54:15 +0000 Subject: [PATCH] command plugins: Add a 'progress' diagnostic message at default verbosity This change adds a 'progress' message to the diagnostics which a plugin can send back to SwiftPM. This message is printed to standard error at the default verbosity level. ### Motivation: Currently, a plugin can write output to standard output or send diagnostics which SwiftPM writes to standard error. If the plugin spawns long-running operations we might want to print progress messages to let the user know that it is still running. The existing options all have compromises: * Anything the plugin prints to its standard output or standard error is echoed to SwiftPM's standard output, but we may want to keep progress information separate from other plugin outputs - the plugin might be called as part of a shell pipeline where a downstream process will consume its output, for example. * The existing diagnostic messages - remark (info), warning and error - are all suppressed at the default verbosity level. * Increasing the level with `-v` causes SwiftPM to print lots of extra logging information from the build, which swamps the plugin info messages. This commit adds a 'progress' message which SwiftPM will print to standard error at the default verbosity level, allowing progress to be shown without polluting standard output or pulling in additional build logging which might not be relevant to the user. ### Modifications: * `Diagnostics.progress` takes a message as a string and passes it to SwiftPM, which prints it to standard error. From the plugin programmer's point of view `Diagnostics.progress` is logically a diagnostics message, however it is a new type in the SwiftPM <-> plugin protocol because extending the existing diagnostics enum would require extensive changes to add a the new message and in many existing cases it would not be relevant. ### Result: A plugin can show the user that it is making progress by printing messages which SwiftPM will echoed to stderr at the default verbosity level. --- .../diagnostics-stub/diagnostics_stub.swift | 4 ++ .../Commands/Utilities/PluginDelegate.swift | 5 ++ Sources/PackagePlugin/Diagnostics.swift | 5 ++ Sources/PackagePlugin/PluginMessages.swift | 5 +- .../Plugins/PluginInvocation.swift | 12 ++++- Tests/CommandsTests/PackageToolTests.swift | 46 ++++++++++++++----- Tests/FunctionalTests/PluginTests.swift | 4 ++ 7 files changed, 66 insertions(+), 15 deletions(-) diff --git a/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/diagnostics-stub/diagnostics_stub.swift b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/diagnostics-stub/diagnostics_stub.swift index 2affe7a0e87..3e545707c26 100644 --- a/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/diagnostics-stub/diagnostics_stub.swift +++ b/Fixtures/Miscellaneous/Plugins/CommandPluginTestStub/Plugins/diagnostics-stub/diagnostics_stub.swift @@ -13,6 +13,10 @@ struct diagnostics_stub: CommandPlugin { } // Diagnostics are collected by SwiftPM and printed to standard error, depending on the current log verbosity level. + if arguments.contains("progress") { + Diagnostics.progress("command plugin: Diagnostics.progress") // prefixed with [plugin_name] + } + if arguments.contains("remark") { Diagnostics.remark("command plugin: Diagnostics.remark") // prefixed with 'info:' when printed } diff --git a/Sources/Commands/Utilities/PluginDelegate.swift b/Sources/Commands/Utilities/PluginDelegate.swift index e98cff936ca..92a506c4f88 100644 --- a/Sources/Commands/Utilities/PluginDelegate.swift +++ b/Sources/Commands/Utilities/PluginDelegate.swift @@ -53,6 +53,11 @@ final class PluginDelegate: PluginInvocationDelegate { swiftTool.observabilityScope.emit(diagnostic) } + func pluginEmittedProgress(_ message: String) { + swiftTool.outputStream.write("[\(plugin.name)] \(message)\n") + swiftTool.outputStream.flush() + } + func pluginRequestedBuildOperation( subset: PluginInvocationBuildSubset, parameters: PluginInvocationBuildParameters, diff --git a/Sources/PackagePlugin/Diagnostics.swift b/Sources/PackagePlugin/Diagnostics.swift index 4892ebba44a..0d05eff2f92 100644 --- a/Sources/PackagePlugin/Diagnostics.swift +++ b/Sources/PackagePlugin/Diagnostics.swift @@ -49,4 +49,9 @@ public struct Diagnostics { public static func remark(_ message: String, file: String? = #file, line: Int? = #line) { self.emit(.remark, message, file: file, line: line) } + + /// Emits a progress message + public static func progress(_ message: String) { + try? pluginHostConnection.sendMessage(.emitProgress(message: message)) + } } diff --git a/Sources/PackagePlugin/PluginMessages.swift b/Sources/PackagePlugin/PluginMessages.swift index be17ed0bd19..e374f1a9823 100644 --- a/Sources/PackagePlugin/PluginMessages.swift +++ b/Sources/PackagePlugin/PluginMessages.swift @@ -269,7 +269,10 @@ enum PluginToHostMessage: Codable { enum DiagnosticSeverity: String, Codable { case error, warning, remark } - + + /// The plugin emits a progress message. + case emitProgress(message: String) + /// The plugin defines a build command. case defineBuildCommand(configuration: CommandConfiguration, inputFiles: [URL], outputFiles: [URL]) diff --git a/Sources/SPMBuildCore/Plugins/PluginInvocation.swift b/Sources/SPMBuildCore/Plugins/PluginInvocation.swift index f6c92fc834d..fe873e80a9c 100644 --- a/Sources/SPMBuildCore/Plugins/PluginInvocation.swift +++ b/Sources/SPMBuildCore/Plugins/PluginInvocation.swift @@ -240,7 +240,10 @@ extension PluginTarget { diagnostic = .info(message, metadata: metadata) } self.invocationDelegate.pluginEmittedDiagnostic(diagnostic) - + + case .emitProgress(let message): + self.invocationDelegate.pluginEmittedProgress(message) + case .defineBuildCommand(let config, let inputFiles, let outputFiles): if config.version != 2 { throw PluginEvaluationError.pluginUsesIncompatibleVersion(expected: 2, actual: config.version) @@ -485,7 +488,9 @@ extension PackageGraph { dispatchPrecondition(condition: .onQueue(delegateQueue)) outputData.append(contentsOf: data) } - + + func pluginEmittedProgress(_ message: String) {} + func pluginEmittedDiagnostic(_ diagnostic: Basics.Diagnostic) { dispatchPrecondition(condition: .onQueue(delegateQueue)) diagnostics.append(diagnostic) @@ -819,6 +824,9 @@ public protocol PluginInvocationDelegate { /// Called when a plugin emits a diagnostic through the PackagePlugin APIs. func pluginEmittedDiagnostic(_: Basics.Diagnostic) + /// Called when a plugin emits a progress message through the PackagePlugin APIs. + func pluginEmittedProgress(_: String) + /// Called when a plugin defines a build command through the PackagePlugin APIs. func pluginDefinedBuildCommand(displayName: String?, executable: AbsolutePath, arguments: [String], environment: [String: String], workingDirectory: AbsolutePath?, inputFiles: [AbsolutePath], outputFiles: [AbsolutePath]) diff --git a/Tests/CommandsTests/PackageToolTests.swift b/Tests/CommandsTests/PackageToolTests.swift index 4e770167975..0076ff30ffa 100644 --- a/Tests/CommandsTests/PackageToolTests.swift +++ b/Tests/CommandsTests/PackageToolTests.swift @@ -1886,6 +1886,7 @@ final class PackageToolTests: CommandsTestCase { // Match patterns for expected messages let isEmpty = StringPattern.equal("") let isOnlyPrint = StringPattern.equal("command plugin: print\n") + let containsProgress = StringPattern.contains("[diagnostics-stub] command plugin: Diagnostics.progress") let containsRemark = StringPattern.contains("command plugin: Diagnostics.remark") let containsWarning = StringPattern.contains("command plugin: Diagnostics.warning") let containsError = StringPattern.contains("command plugin: Diagnostics.error") @@ -1915,18 +1916,25 @@ final class PackageToolTests: CommandsTestCase { XCTAssertMatch(stderr, isEmpty) } - try runPlugin(flags: [], diagnostics: ["print", "remark"]) { stdout, stderr in + try runPlugin(flags: [], diagnostics: ["print", "progress"]) { stdout, stderr in + XCTAssertMatch(stdout, isOnlyPrint) + XCTAssertMatch(stderr, containsProgress) + } + + try runPlugin(flags: [], diagnostics: ["print", "progress", "remark"]) { stdout, stderr in XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, isEmpty) + XCTAssertMatch(stderr, containsProgress) } - try runPlugin(flags: [], diagnostics: ["print", "remark", "warning"]) { stdout, stderr in + try runPlugin(flags: [], diagnostics: ["print", "progress", "remark", "warning"]) { stdout, stderr in XCTAssertMatch(stdout, isOnlyPrint) + XCTAssertMatch(stderr, containsProgress) XCTAssertMatch(stderr, containsWarning) } - try runPluginWithError(flags: [], diagnostics: ["print", "remark", "warning", "error"]) { stdout, stderr in + try runPluginWithError(flags: [], diagnostics: ["print", "progress", "remark", "warning", "error"]) { stdout, stderr in XCTAssertMatch(stdout, isOnlyPrint) + XCTAssertMatch(stderr, containsProgress) XCTAssertMatch(stderr, containsWarning) XCTAssertMatch(stderr, containsError) } @@ -1940,18 +1948,24 @@ final class PackageToolTests: CommandsTestCase { XCTAssertMatch(stderr, isEmpty) } - try runPlugin(flags: ["-q"], diagnostics: ["print", "remark"]) { stdout, stderr in + try runPlugin(flags: ["-q"], diagnostics: ["print", "progress"]) { stdout, stderr in XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, isEmpty) + XCTAssertMatch(stderr, containsProgress) } - try runPlugin(flags: ["-q"], diagnostics: ["print", "remark", "warning"]) { stdout, stderr in + try runPlugin(flags: ["-q"], diagnostics: ["print", "progress", "remark"]) { stdout, stderr in XCTAssertMatch(stdout, isOnlyPrint) - XCTAssertMatch(stderr, isEmpty) + XCTAssertMatch(stderr, containsProgress) } - try runPluginWithError(flags: ["-q"], diagnostics: ["print", "remark", "warning", "error"]) { stdout, stderr in + try runPlugin(flags: ["-q"], diagnostics: ["print", "progress", "remark", "warning"]) { stdout, stderr in XCTAssertMatch(stdout, isOnlyPrint) + XCTAssertMatch(stderr, containsProgress) + } + + try runPluginWithError(flags: ["-q"], diagnostics: ["print", "progress", "remark", "warning", "error"]) { stdout, stderr in + XCTAssertMatch(stdout, isOnlyPrint) + XCTAssertMatch(stderr, containsProgress) XCTAssertNoMatch(stderr, containsRemark) XCTAssertNoMatch(stderr, containsWarning) XCTAssertMatch(stderr, containsError) @@ -1967,19 +1981,27 @@ final class PackageToolTests: CommandsTestCase { // At this level stderr contains extra compiler output even if the plugin does not print diagnostics } - try runPlugin(flags: ["-v"], diagnostics: ["print", "remark"]) { stdout, stderr in + try runPlugin(flags: ["-v"], diagnostics: ["print", "progress"]) { stdout, stderr in + XCTAssertMatch(stdout, isOnlyPrint) + XCTAssertMatch(stderr, containsProgress) + } + + try runPlugin(flags: ["-v"], diagnostics: ["print", "progress", "remark"]) { stdout, stderr in XCTAssertMatch(stdout, isOnlyPrint) + XCTAssertMatch(stderr, containsProgress) XCTAssertMatch(stderr, containsRemark) } - try runPlugin(flags: ["-v"], diagnostics: ["print", "remark", "warning"]) { stdout, stderr in + try runPlugin(flags: ["-v"], diagnostics: ["print", "progress", "remark", "warning"]) { stdout, stderr in XCTAssertMatch(stdout, isOnlyPrint) + XCTAssertMatch(stderr, containsProgress) XCTAssertMatch(stderr, containsRemark) XCTAssertMatch(stderr, containsWarning) } - try runPluginWithError(flags: ["-v"], diagnostics: ["print", "remark", "warning", "error"]) { stdout, stderr in + try runPluginWithError(flags: ["-v"], diagnostics: ["print", "progress", "remark", "warning", "error"]) { stdout, stderr in XCTAssertMatch(stdout, isOnlyPrint) + XCTAssertMatch(stderr, containsProgress) XCTAssertMatch(stderr, containsRemark) XCTAssertMatch(stderr, containsWarning) XCTAssertMatch(stderr, containsError) diff --git a/Tests/FunctionalTests/PluginTests.swift b/Tests/FunctionalTests/PluginTests.swift index 20eb444400e..e3a54c5ae9c 100644 --- a/Tests/FunctionalTests/PluginTests.swift +++ b/Tests/FunctionalTests/PluginTests.swift @@ -478,6 +478,8 @@ class PluginTests: XCTestCase { print("[DIAG] \(diagnostic)") diagnostics.append(diagnostic) } + + func pluginEmittedProgress(_ message: String) {} } // Helper function to invoke a plugin with given input and to check its outputs. @@ -767,6 +769,8 @@ class PluginTests: XCTestCase { dispatchPrecondition(condition: .onQueue(delegateQueue)) diagnostics.append(diagnostic) } + + func pluginEmittedProgress(_ message: String) {} } // Find the relevant plugin.