1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
|
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2021-2022 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import Basics
import Foundation
import PackageModel
import PackageLoading
import PackageGraph
/// Implements the mechanics of running and communicating with a plugin (implemented as a set of Swift source files). In most environments this is done by compiling the code to an executable, invoking it as a sandboxed subprocess, and communicating with it using pipes. Specific implementations are free to implement things differently, however.
public protocol PluginScriptRunner {
/// Public protocol function that starts compiling the plugin script to an executable. The name is used as the basename for the executable and auxiliary files. The tools version controls the availability of APIs in PackagePlugin, and should be set to the tools version of the package that defines the plugin (not of the target to which it is being applied). This function returns immediately and then calls the completion handler on the callback queue when compilation ends.
@available(*, noasync, message: "Use the async alternative")
func compilePluginScript(
sourceFiles: [AbsolutePath],
pluginName: String,
toolsVersion: ToolsVersion,
observabilityScope: ObservabilityScope,
callbackQueue: DispatchQueue,
delegate: PluginScriptCompilerDelegate,
completion: @escaping (Result<PluginCompilationResult, Error>) -> Void
)
/// Implements the mechanics of running a plugin script implemented as a set of Swift source files, for use
/// by the package graph when it is evaluating package plugins.
///
/// The `sources` refer to the Swift source files and are accessible in the provided `fileSystem`. The input is
/// a PluginScriptRunnerInput structure.
///
/// The text output callback handler will receive free-form output from the script as it's running. Structured
/// diagnostics emitted by the plugin will be added to the observability scope.
///
/// Every concrete implementation should cache any intermediates as necessary to avoid redundant work.
func runPluginScript(
sourceFiles: [AbsolutePath],
pluginName: String,
initialMessage: Data,
toolsVersion: ToolsVersion,
workingDirectory: AbsolutePath,
writableDirectories: [AbsolutePath],
readOnlyDirectories: [AbsolutePath],
allowNetworkConnections: [SandboxNetworkPermission],
fileSystem: FileSystem,
observabilityScope: ObservabilityScope,
callbackQueue: DispatchQueue,
delegate: PluginScriptCompilerDelegate & PluginScriptRunnerDelegate,
completion: @escaping (Result<Int32, Error>) -> Void
)
/// Returns the Triple that represents the host for which plugin script tools should be built, or for which binary
/// tools should be selected.
var hostTriple: Triple { get throws }
}
public extension PluginScriptRunner {
func compilePluginScript(
sourceFiles: [AbsolutePath],
pluginName: String,
toolsVersion: ToolsVersion,
observabilityScope: ObservabilityScope,
callbackQueue: DispatchQueue,
delegate: PluginScriptCompilerDelegate
) async throws -> PluginCompilationResult {
try await safe_async {
self.compilePluginScript(
sourceFiles: sourceFiles,
pluginName: pluginName,
toolsVersion: toolsVersion,
observabilityScope: observabilityScope,
callbackQueue: callbackQueue,
delegate: delegate,
completion: $0
)
}
}
}
/// Protocol by which `PluginScriptRunner` communicates back to the caller as it compiles plugins.
public protocol PluginScriptCompilerDelegate {
/// Called immediately before compiling a plugin. Will not be called if the plugin didn't have to be compiled. This call is always followed by a `didCompilePlugin()` but is mutually exclusive with a `skippedCompilingPlugin()` call.
func willCompilePlugin(commandLine: [String], environment: [String: String])
/// Called immediately after compiling a plugin (regardless of whether it succeeded or failed). Will not be called if the plugin didn't have to be compiled. This call is always follows a `willCompilePlugin()` but is mutually exclusive with a `skippedCompilingPlugin()` call.
func didCompilePlugin(result: PluginCompilationResult)
/// Called if a plugin didn't need to be compiled because previous compilation results were still valid. In this case neither `willCompilePlugin()` nor `didCompilePlugin()` will be called.
func skippedCompilingPlugin(cachedResult: PluginCompilationResult)
}
/// Protocol by which `PluginScriptRunner` communicates back to the caller as it runs plugins.
public protocol PluginScriptRunnerDelegate {
/// Called for each piece of textual output data emitted by the plugin. Note that there is no guarantee that the data begins and ends on a UTF-8 byte sequence boundary (much less on a line boundary) so the delegate should buffer partial data as appropriate.
func handleOutput(data: Data)
/// Called for each length-delimited message received from the plugin. The `responder` is closure that can be used to send one or more messages in reply.
func handleMessage(data: Data, responder: @escaping (Data) -> Void) throws
}
/// The result of compiling a plugin. The executable path will only be present if the compilation succeeds, while the other properties are present in all cases.
public struct PluginCompilationResult: Equatable {
/// Whether compilation succeeded.
public var succeeded: Bool
/// Complete compiler command line.
public var commandLine: [String]
/// Path of the compiled executable.
public var executableFile: AbsolutePath
/// Path of the libClang diagnostics file emitted by the compiler.
public var diagnosticsFile: AbsolutePath
/// Any output emitted by the compiler (stdout and stderr combined).
public var rawCompilerOutput: String
/// Whether the compilation result came from the cache (false means that the compiler did run).
public var cached: Bool
public init(
succeeded: Bool,
commandLine: [String],
executableFile: AbsolutePath,
diagnosticsFile: AbsolutePath,
compilerOutput rawCompilerOutput: String,
cached: Bool
) {
self.succeeded = succeeded
self.commandLine = commandLine
self.executableFile = executableFile
self.diagnosticsFile = diagnosticsFile
self.rawCompilerOutput = rawCompilerOutput
self.cached = cached
}
}
extension PluginCompilationResult {
public var compilerOutput: String {
let output = self.rawCompilerOutput.spm_chomp()
return output + (output.isEmpty || output.hasSuffix("\n") ? "" : "\n")
}
}
extension PluginCompilationResult: CustomDebugStringConvertible {
public var debugDescription: String {
return """
<PluginCompilationResult(
succeeded: \(succeeded),
commandLine: \(commandLine.map{ $0.spm_shellEscaped() }.joined(separator: " ")),
executable: \(executableFile.prettyPath())
diagnostics: \(diagnosticsFile.prettyPath())
compilerOutput: \(compilerOutput.spm_shellEscaped())
)>
"""
}
}
|