Skip to content

Commit 4cd3d67

Browse files
committed
command plugin: Optionally print build logs during packageManager.build
When a plugin asks for a target to be built by calling packageManager.build, SwiftPM accumulates the build log in a buffer and returns the whole log when the build completes. No feedback is printed on the console while the build is running, so if the target is large the user can't tell whether the build is making progress or is stuck. This commit adds the option to echo logs to the console as they are produced, in addition to accumulating them in a buffer. Console echoing is turned off by default.
1 parent 95397a9 commit 4cd3d67

File tree

6 files changed

+120
-4
lines changed

6 files changed

+120
-4
lines changed

Fixtures/Miscellaneous/Plugins/CommandPluginDiagnosticsStub/Plugins/diagnostics_stub.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@ import PackagePlugin
55
struct diagnostics_stub: CommandPlugin {
66
// This is a helper for testing plugin diagnostics. It sends different messages to SwiftPM depending on its arguments.
77
func performCommand(context: PluginContext, arguments: [String]) async throws {
8+
// Build a target, possibly asking SwiftPM to echo the logs as they are produced.
9+
if arguments.contains("build") {
10+
// If echoLogs is true, SwiftPM will print build logs to stderr as they are produced.
11+
// SwiftPM does not add a prefix to these logs.
12+
let result = try packageManager.build(
13+
.product("placeholder"),
14+
parameters: .init(echoLogs: arguments.contains("echologs"))
15+
)
16+
17+
// To verify that logs are also returned correctly to the plugin,
18+
// print the accumulated log buffer lines with a prefix to
19+
// distinguish them from echoed logs. These logs are normal output
20+
// from the plugin and will be printed on stdout.
21+
if arguments.contains("printlogs") {
22+
for line in result.logText.components(separatedBy: "\n") {
23+
print("command plugin: packageManager.build logtext: \(line)")
24+
}
25+
}
26+
}
27+
828
// Anything a plugin writes to standard output appears on standard output.
929
// Printing to stderr will also go to standard output because SwiftPM combines
1030
// stdout and stderr before launching the plugin.

Sources/Commands/Utilities/PluginDelegate.swift

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import Foundation
1616
import PackageModel
1717
import SPMBuildCore
1818

19+
import protocol TSCBasic.OutputByteStream
1920
import class TSCBasic.BufferedOutputByteStream
2021
import class TSCBasic.Process
2122
import struct TSCBasic.ProcessResult
@@ -66,6 +67,40 @@ final class PluginDelegate: PluginInvocationDelegate {
6667
}
6768
}
6869

70+
class TeeOutputByteStream: OutputByteStream {
71+
var downstreams: [OutputByteStream]
72+
73+
public init(_ downstreams: [OutputByteStream]) {
74+
self.downstreams = downstreams
75+
}
76+
77+
var position: Int {
78+
return 0 // should be related to the downstreams somehow
79+
}
80+
81+
public func write(_ byte: UInt8) {
82+
for downstream in downstreams {
83+
downstream.write(byte)
84+
}
85+
}
86+
87+
func write<C: Collection>(_ bytes: C) where C.Element == UInt8 {
88+
for downstream in downstreams {
89+
downstream.write(bytes)
90+
}
91+
}
92+
93+
public func flush() {
94+
for downstream in downstreams {
95+
downstream.flush()
96+
}
97+
}
98+
99+
public func addStream(_ stream: OutputByteStream) {
100+
self.downstreams.append(stream)
101+
}
102+
}
103+
69104
private func performBuildForPlugin(
70105
subset: PluginInvocationBuildSubset,
71106
parameters: PluginInvocationBuildParameters
@@ -108,7 +143,12 @@ final class PluginDelegate: PluginInvocationDelegate {
108143
}
109144

110145
// Create a build operation. We have to disable the cache in order to get a build plan created.
111-
let outputStream = BufferedOutputByteStream()
146+
let bufferedOutputStream = BufferedOutputByteStream()
147+
let outputStream = TeeOutputByteStream([bufferedOutputStream])
148+
if parameters.echoLogs {
149+
outputStream.addStream(swiftTool.outputStream)
150+
}
151+
112152
let buildSystem = try swiftTool.createBuildSystem(
113153
explicitBuildSystem: .native,
114154
explicitProduct: explicitProduct,
@@ -147,7 +187,7 @@ final class PluginDelegate: PluginInvocationDelegate {
147187
}
148188
return PluginInvocationBuildResult(
149189
succeeded: success,
150-
logText: outputStream.bytes.cString,
190+
logText: bufferedOutputStream.bytes.cString,
151191
builtArtifacts: builtArtifacts)
152192
}
153193

Sources/PackagePlugin/PackageManagerProxy.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ public struct PackageManager {
5959

6060
/// Controls the amount of detail in the log returned in the build result.
6161
public var logging: BuildLogVerbosity
62-
62+
63+
/// Whether to print build logs to the console
64+
public var echoLogs: Bool
65+
6366
/// Additional flags to pass to all C compiler invocations.
6467
public var otherCFlags: [String] = []
6568

@@ -72,9 +75,10 @@ public struct PackageManager {
7275
/// Additional flags to pass to all linker invocations.
7376
public var otherLinkerFlags: [String] = []
7477

75-
public init(configuration: BuildConfiguration = .debug, logging: BuildLogVerbosity = .concise) {
78+
public init(configuration: BuildConfiguration = .debug, logging: BuildLogVerbosity = .concise, echoLogs: Bool = false) {
7679
self.configuration = configuration
7780
self.logging = logging
81+
self.echoLogs = echoLogs
7882
}
7983
}
8084

@@ -314,6 +318,7 @@ fileprivate extension PluginToHostMessage.BuildParameters {
314318
init(_ parameters: PackageManager.BuildParameters) {
315319
self.configuration = .init(parameters.configuration)
316320
self.logging = .init(parameters.logging)
321+
self.echoLogs = parameters.echoLogs
317322
self.otherCFlags = parameters.otherCFlags
318323
self.otherCxxFlags = parameters.otherCxxFlags
319324
self.otherSwiftcFlags = parameters.otherSwiftcFlags

Sources/PackagePlugin/PluginMessages.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ enum PluginToHostMessage: Codable {
303303
enum LogVerbosity: String, Codable {
304304
case concise, verbose, debug
305305
}
306+
var echoLogs: Bool
306307
var otherCFlags: [String]
307308
var otherCxxFlags: [String]
308309
var otherSwiftcFlags: [String]

Sources/SPMBuildCore/Plugins/PluginInvocation.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,7 @@ public struct PluginInvocationBuildParameters {
867867
public enum LogVerbosity: String {
868868
case concise, verbose, debug
869869
}
870+
public var echoLogs: Bool
870871
public var otherCFlags: [String]
871872
public var otherCxxFlags: [String]
872873
public var otherSwiftcFlags: [String]
@@ -979,6 +980,7 @@ fileprivate extension PluginInvocationBuildParameters {
979980
init(_ parameters: PluginToHostMessage.BuildParameters) {
980981
self.configuration = .init(parameters.configuration)
981982
self.logging = .init(parameters.logging)
983+
self.echoLogs = parameters.echoLogs
982984
self.otherCFlags = parameters.otherCFlags
983985
self.otherCxxFlags = parameters.otherCxxFlags
984986
self.otherSwiftcFlags = parameters.otherSwiftcFlags

Tests/CommandsTests/PackageToolTests.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1983,6 +1983,54 @@ final class PackageToolTests: CommandsTestCase {
19831983
}
19841984
}
19851985

1986+
// Test logging of builds initiated by a command plugin
1987+
func testCommandPluginBuildLogs() throws {
1988+
// Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require).
1989+
try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency")
1990+
1991+
// Match patterns for expected messages
1992+
1993+
let isEmpty = StringPattern.equal("")
1994+
1995+
// result.logText printed by the plugin has a prefix
1996+
let containsLogtext = StringPattern.contains("command plugin: packageManager.build logtext: Building for debugging...")
1997+
1998+
// Echoed logs have no prefix
1999+
let containsLogecho = StringPattern.regex("^Building for debugging...\n")
2000+
2001+
// These tests involve building a target, so each test must run with a fresh copy of the fixture
2002+
// otherwise the logs may be different in subsequent tests.
2003+
2004+
// Check than nothing is echoed when echoLogs is false
2005+
try fixture(name: "Miscellaneous/Plugins/CommandPluginDiagnosticsStub") { fixturePath in
2006+
let (stdout, stderr) = try SwiftPM.Package.execute(["print-diagnostics", "build"], packagePath: fixturePath)
2007+
XCTAssertMatch(stdout, isEmpty)
2008+
XCTAssertMatch(stderr, isEmpty)
2009+
}
2010+
2011+
// Check that logs are returned to the plugin when echoLogs is false
2012+
try fixture(name: "Miscellaneous/Plugins/CommandPluginDiagnosticsStub") { fixturePath in
2013+
let (stdout, stderr) = try SwiftPM.Package.execute(["print-diagnostics", "build", "printlogs"], packagePath: fixturePath)
2014+
XCTAssertMatch(stdout, containsLogtext)
2015+
XCTAssertMatch(stderr, isEmpty)
2016+
}
2017+
2018+
// Check that logs echoed to the console (on stderr) when echoLogs is true
2019+
try fixture(name: "Miscellaneous/Plugins/CommandPluginDiagnosticsStub") { fixturePath in
2020+
let (stdout, stderr) = try SwiftPM.Package.execute(["print-diagnostics", "build", "echologs"], packagePath: fixturePath)
2021+
XCTAssertMatch(stdout, isEmpty)
2022+
XCTAssertMatch(stderr, containsLogecho)
2023+
}
2024+
2025+
// Check that logs are returned to the plugin and echoed to the console (on stderr) when echoLogs is true
2026+
try fixture(name: "Miscellaneous/Plugins/CommandPluginDiagnosticsStub") { fixturePath in
2027+
let (stdout, stderr) = try SwiftPM.Package.execute(["print-diagnostics", "build", "printlogs", "echologs"], packagePath: fixturePath)
2028+
XCTAssertMatch(stdout, containsLogtext)
2029+
XCTAssertMatch(stderr, containsLogecho)
2030+
}
2031+
}
2032+
2033+
19862034
func testCommandPluginNetworkingPermissions(permissionsManifestFragment: String, permissionError: String, reason: String, remedy: [String]) throws {
19872035
// Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require).
19882036
try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency")

0 commit comments

Comments
 (0)