Skip to content

command plugins: Optionally print build logs during packageManager.build #7255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ import PackagePlugin
struct diagnostics_stub: CommandPlugin {
// This is a helper for testing plugin diagnostics. It sends different messages to SwiftPM depending on its arguments.
func performCommand(context: PluginContext, arguments: [String]) async throws {
// Build a target, possibly asking SwiftPM to echo the logs as they are produced.
if arguments.contains("build") {
// If echoLogs is true, SwiftPM will print build logs to stderr as they are produced.
// SwiftPM does not add a prefix to these logs.
let result = try packageManager.build(
.product("placeholder"),
parameters: .init(echoLogs: arguments.contains("echologs"))
)

// To verify that logs are also returned correctly to the plugin,
// print the accumulated log buffer lines with a prefix to
// distinguish them from echoed logs. These logs are normal output
// from the plugin and will be printed on stdout.
if arguments.contains("printlogs") {
for line in result.logText.components(separatedBy: "\n") {
print("command plugin: packageManager.build logtext: \(line)")
}
}
}

// Anything a plugin writes to standard output appears on standard output.
// Printing to stderr will also go to standard output because SwiftPM combines
// stdout and stderr before launching the plugin.
Expand Down
44 changes: 42 additions & 2 deletions Sources/Commands/Utilities/PluginDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Foundation
import PackageModel
import SPMBuildCore

import protocol TSCBasic.OutputByteStream
import class TSCBasic.BufferedOutputByteStream
import class TSCBasic.Process
import struct TSCBasic.ProcessResult
Expand Down Expand Up @@ -66,6 +67,40 @@ final class PluginDelegate: PluginInvocationDelegate {
}
}

class TeeOutputByteStream: OutputByteStream {
var downstreams: [OutputByteStream]

public init(_ downstreams: [OutputByteStream]) {
self.downstreams = downstreams
}

var position: Int {
return 0 // should be related to the downstreams somehow
Copy link
Contributor Author

@euanh euanh Jan 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should 'position' return here? The downstreams might all be at different positions. Should this be the number of bytes which have been passed to write, or should it be related to positions of the downstreams?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IDK, the doc comment for that protocol says it's "The current offset within the output stream", if that helps in any way.

}

public func write(_ byte: UInt8) {
for downstream in downstreams {
downstream.write(byte)
}
}

func write<C: Collection>(_ bytes: C) where C.Element == UInt8 {
for downstream in downstreams {
downstream.write(bytes)
}
}

public func flush() {
for downstream in downstreams {
downstream.flush()
}
}

public func addStream(_ stream: OutputByteStream) {
self.downstreams.append(stream)
}
}

private func performBuildForPlugin(
subset: PluginInvocationBuildSubset,
parameters: PluginInvocationBuildParameters
Expand Down Expand Up @@ -112,7 +147,12 @@ final class PluginDelegate: PluginInvocationDelegate {
}

// Create a build operation. We have to disable the cache in order to get a build plan created.
let outputStream = BufferedOutputByteStream()
let bufferedOutputStream = BufferedOutputByteStream()
let outputStream = TeeOutputByteStream([bufferedOutputStream])
if parameters.echoLogs {
outputStream.addStream(swiftTool.outputStream)
}

let buildSystem = try swiftTool.createBuildSystem(
explicitBuildSystem: .native,
explicitProduct: explicitProduct,
Expand Down Expand Up @@ -151,7 +191,7 @@ final class PluginDelegate: PluginInvocationDelegate {
}
return PluginInvocationBuildResult(
succeeded: success,
logText: outputStream.bytes.cString,
logText: bufferedOutputStream.bytes.cString,
builtArtifacts: builtArtifacts)
}

Expand Down
9 changes: 7 additions & 2 deletions Sources/PackagePlugin/PackageManagerProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ public struct PackageManager {

/// Controls the amount of detail in the log returned in the build result.
public var logging: BuildLogVerbosity


/// Whether to print build logs to the console
public var echoLogs: Bool

/// Additional flags to pass to all C compiler invocations.
public var otherCFlags: [String] = []

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

public init(configuration: BuildConfiguration = .debug, logging: BuildLogVerbosity = .concise) {
public init(configuration: BuildConfiguration = .debug, logging: BuildLogVerbosity = .concise, echoLogs: Bool = false) {
self.configuration = configuration
self.logging = logging
self.echoLogs = echoLogs
}
}

Expand Down Expand Up @@ -314,6 +318,7 @@ fileprivate extension PluginToHostMessage.BuildParameters {
init(_ parameters: PackageManager.BuildParameters) {
self.configuration = .init(parameters.configuration)
self.logging = .init(parameters.logging)
self.echoLogs = parameters.echoLogs
self.otherCFlags = parameters.otherCFlags
self.otherCxxFlags = parameters.otherCxxFlags
self.otherSwiftcFlags = parameters.otherSwiftcFlags
Expand Down
1 change: 1 addition & 0 deletions Sources/PackagePlugin/PluginMessages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ enum PluginToHostMessage: Codable {
enum LogVerbosity: String, Codable {
case concise, verbose, debug
}
var echoLogs: Bool
var otherCFlags: [String]
var otherCxxFlags: [String]
var otherSwiftcFlags: [String]
Expand Down
2 changes: 2 additions & 0 deletions Sources/SPMBuildCore/Plugins/PluginInvocation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,7 @@ public struct PluginInvocationBuildParameters {
public enum LogVerbosity: String {
case concise, verbose, debug
}
public var echoLogs: Bool
public var otherCFlags: [String]
public var otherCxxFlags: [String]
public var otherSwiftcFlags: [String]
Expand Down Expand Up @@ -979,6 +980,7 @@ fileprivate extension PluginInvocationBuildParameters {
init(_ parameters: PluginToHostMessage.BuildParameters) {
self.configuration = .init(parameters.configuration)
self.logging = .init(parameters.logging)
self.echoLogs = parameters.echoLogs
self.otherCFlags = parameters.otherCFlags
self.otherCxxFlags = parameters.otherCxxFlags
self.otherSwiftcFlags = parameters.otherSwiftcFlags
Expand Down
47 changes: 47 additions & 0 deletions Tests/CommandsTests/PackageToolTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2049,6 +2049,53 @@ final class PackageToolTests: CommandsTestCase {
}
}

// Test logging of builds initiated by a command plugin
func testCommandPluginBuildLogs() throws {
// Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require).
try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency")

// Match patterns for expected messages

let isEmpty = StringPattern.equal("")

// result.logText printed by the plugin has a prefix
let containsLogtext = StringPattern.contains("command plugin: packageManager.build logtext: Building for debugging...")

// Echoed logs have no prefix
let containsLogecho = StringPattern.regex("^Building for debugging...\n")

// These tests involve building a target, so each test must run with a fresh copy of the fixture
// otherwise the logs may be different in subsequent tests.

// Check than nothing is echoed when echoLogs is false
try fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in
let (stdout, stderr) = try SwiftPM.Package.execute(["print-diagnostics", "build"], packagePath: fixturePath)
XCTAssertMatch(stdout, isEmpty)
XCTAssertMatch(stderr, isEmpty)
}

// Check that logs are returned to the plugin when echoLogs is false
try fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in
let (stdout, stderr) = try SwiftPM.Package.execute(["print-diagnostics", "build", "printlogs"], packagePath: fixturePath)
XCTAssertMatch(stdout, containsLogtext)
XCTAssertMatch(stderr, isEmpty)
}

// Check that logs echoed to the console (on stderr) when echoLogs is true
try fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in
let (stdout, stderr) = try SwiftPM.Package.execute(["print-diagnostics", "build", "echologs"], packagePath: fixturePath)
XCTAssertMatch(stdout, isEmpty)
XCTAssertMatch(stderr, containsLogecho)
}

// Check that logs are returned to the plugin and echoed to the console (on stderr) when echoLogs is true
try fixture(name: "Miscellaneous/Plugins/CommandPluginTestStub") { fixturePath in
let (stdout, stderr) = try SwiftPM.Package.execute(["print-diagnostics", "build", "printlogs", "echologs"], packagePath: fixturePath)
XCTAssertMatch(stdout, containsLogtext)
XCTAssertMatch(stderr, containsLogecho)
}
}

func testCommandPluginNetworkingPermissions(permissionsManifestFragment: String, permissionError: String, reason: String, remedy: [String]) throws {
// Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require).
try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency")
Expand Down