File: PluginScriptRunner.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (165 lines) | stat: -rw-r--r-- 7,935 bytes parent folder | download
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())
            )>
            """
    }
}