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.