File: SWBBuildService.swift

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (312 lines) | stat: -rw-r--r-- 14,770 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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 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
//
//===----------------------------------------------------------------------===//

public import Foundation

import SWBProtocol
import SWBUtil


/// A generic error from Swift Build.
//
// FIXME: We should refine this.
public enum SwiftBuildError: Hashable, CustomStringConvertible, LocalizedError {
    /// An unexpected protocol response was received.
    case protocolError(description: String)

    /// A generic request failure error.
    case requestError(description: String)

    public var description: String {
        switch self {
        case .protocolError(description: let description):
            return description
        case .requestError(description: let description):
            return description
        }
    }

    public var errorDescription: String? {
        description
    }
}

/// Proxy object for communicating with the Swift Build build service.
public final class SWBBuildService: Sendable {
    private let connectionMode: SWBBuildServiceConnectionMode
    private let variant: SWBBuildServiceVariant
    private let serviceBundleURL: URL?
    private var connection: SWBBuildServiceConnection

    /// An opaque identifier representing the lifetime of the service's specific connection instance.
    var connectionUUID: Foundation.UUID {
        return connection.uuid
    }

    /// Whether or not the underlying Swift Build service subprocess has terminated.
    ///
    /// A terminated service subprocess can be restarted by calling ``restart()``.
    public var terminated: Bool {
        return connection.hasTerminated
    }

    /// The PID of the active service subprocess, for debugging purposes. This may change at any time, so this property should not be used for basing any actual functionality on.
    ///
    /// Returns `nil` if there is no underlying subprocess, which is the case if the build service is being run in-process.
    var subprocessPID: pid_t? {
        return connection.subprocessPID
    }

    public init(connectionMode: SWBBuildServiceConnectionMode = .default, variant: SWBBuildServiceVariant = .default, serviceBundleURL: URL? = nil) async throws {
        self.connectionMode = connectionMode
        self.variant = variant
        self.serviceBundleURL = serviceBundleURL
        let connection = try await SWBBuildServiceConnection(connectionMode: connectionMode, variant: variant, serviceBundleURL: serviceBundleURL)
        self.connection = connection
        connection.resume()
    }

    /// Restarts the connection to the build service executable.
    ///
    /// This can be called to restart communications if the connection has terminated
    /// due to the build service executable exiting or crashing. The service can only
    /// be restarted if it is being run as an external process (not if it is being run
    /// in-process).
    public func restart() async throws {
        if case .inProcess = connectionMode {
            throw StubError.error("Can't restart the service process connection because the service is running in-process.")
        }

        let connection = try await SWBBuildServiceConnection(connectionMode: connectionMode, variant: variant, serviceBundleURL: serviceBundleURL)
        await self.connection.close()
        self.connection = connection
        connection.resume()
    }

    public func close() async {
        await connection.close()
    }

    @_spi(Testing) public func terminate() async {
        await connection.terminate()
    }

    /// Sends a message returns its response.
    /// - Returns: The reply message.
    internal func send(_ message: any Message) async -> any Message {
        let serializer = MsgPackSerializer()
        IPCMessage(message).serialize(to: serializer)
        let contents = serializer.byteString.bytes.withUnsafeBytes(SWBDispatchData.init(bytes:))
        let data = await self.connection.send(contents)

        // Deserialize the message.
        //
        // FIXME: Shouldn't need to copy here.
        let deserializer = MsgPackDeserializer(ByteString(Array(data)))
        do {
            return try (deserializer.deserialize() as IPCMessage).message
        } catch {
            return ErrorResponse("Error decoding response to \(type(of: message)) message: \(error)")
        }
    }

    internal func send(_ message: any Message, onChannel channel: UInt64) {
        let serializer = MsgPackSerializer()
        IPCMessage(message).serialize(to: serializer)
        let contents = serializer.byteString

        contents.bytes.withUnsafeBytes{ buffer in
            self.connection.send(SWBDispatchData(bytes: buffer), onChannel: channel)
        }
    }

    internal func openChannel() -> SWBChannel {
        SWBChannel(service: self)
    }

    internal func openChannel(handler block: @Sendable @escaping (UInt64, any Message) -> Void) -> UInt64 {
        return self.connection.openChannel{ channel, data in
            // Deserialize the message.
            //
            // FIXME: Shouldn't need to copy here.
            let deserializer = MsgPackDeserializer(ByteString(Array(data)))
            do {
                let wrapper: IPCMessage = try deserializer.deserialize()
                block(channel, wrapper.message)
            } catch {
                block(channel, ErrorResponse("Error decoding response: \(error)"))
            }
        }
    }

    internal func openChannel(handler block: @Sendable @escaping (any Message) -> Void) -> UInt64 {
        openChannel(handler: { (_, message) in block(message) })
    }

    internal func closeChannel(_ channel: UInt64) {
        return self.connection.close(channel: channel)
    }

    /// Sends a message returns its response.
    /// - Throws: If the reply message is not of the expected `ResponseMessage` type.
    /// - Returns: The reply message.
    internal func send<R: RequestMessage>(request message: R) async throws -> R.ResponseMessage {
        switch await send(message) {
        case let message as R.ResponseMessage:
            return message
        case let message as ErrorResponse:
            throw SwiftBuildError.requestError(description: message.description)
        default:
            throw SwiftBuildError.protocolError(description: "unexpected response")
        }
    }

    public func checkAlive() async throws {
        _ = try await send(request: PingRequest())
    }

    /// Request the service to clear as many caches as possible.
    public func clearAllCaches() async throws {
        _ = try await send(request: ClearAllCachesRequest())
    }

    /// Set a service configurable value.
    public func setConfig(key: String, value: String) async throws {
        _ = try await send(request: SetConfigItemRequest(key: key, value: value))
    }

    /// Get a dump of the Swift Build statistics.
    public func getStatisticsDump() async throws -> String {
        try await send(request: GetStatisticsRequest()).value
    }

    @available(*, deprecated, message: "Switch to createSession(name, developerPath, cachePath, inferiorProductsPath , environment)")
    public func createSession(name: String, developerPath: String? = nil, cachePath: String?, inferiorProductsPath: String?) async -> (Result<SWBBuildServiceSession, any Error>, [SwiftBuildMessage.DiagnosticInfo]) {
        await createSession(name: name, developerPath: developerPath, cachePath: cachePath, inferiorProductsPath: inferiorProductsPath, environment: nil)
    }

    // ABI compatibility
    public func createSession(name: String, developerPath: String? = nil, cachePath: String?, inferiorProductsPath: String?, environment: [String:String]?) async -> (Result<SWBBuildServiceSession, any Error>, [SwiftBuildMessage.DiagnosticInfo]) {
        await createSession(name: name, developerPath: developerPath, resourceSearchPaths: [], cachePath: cachePath, inferiorProductsPath: inferiorProductsPath, environment: environment)
    }

    /// Create a new service session.
    ///
    /// - Parameters:
    ///   - name: The name of the workspace.
    ///   - cachePath: If provided, the path to where Swift Build should cache per-workspace data.
    ///   - inferiorProductsPath: If provided, the path to where inferior Xcode build data is located.
    ///   - environment: If provided, a set of environment variables that are relevant to the build session's context
    /// - returns: The new session.
    public func createSession(name: String, developerPath: String? = nil, resourceSearchPaths: [String], cachePath: String?, inferiorProductsPath: String?, environment: [String:String]?) async -> (Result<SWBBuildServiceSession, any Error>, [SwiftBuildMessage.DiagnosticInfo]) {
        do {
            let response = try await send(request: CreateSessionRequest(name: name, developerPath: developerPath.map(Path.init), resourceSearchPaths: resourceSearchPaths.map(Path.init), cachePath: cachePath.map(Path.init), inferiorProductsPath: inferiorProductsPath.map(Path.init), environment: environment))
            let diagnostics = response.diagnostics.map { SwiftBuildMessage.DiagnosticInfo(.init($0, .global)) }
            if let sessionID = response.sessionID {
                return (.success(SWBBuildServiceSession(name: name, uid: sessionID, service: self)), diagnostics)
            } else {
                return (.failure(SwiftBuildError.requestError(description: "Could not initialize build system")), diagnostics)
            }
        } catch {
            return (.failure(error), [])
        }
    }

    public func createSession(name: String, swiftToolchainPath: String, resourceSearchPaths: [String], cachePath: String?, inferiorProductsPath: String?, environment: [String:String]?) async -> (Result<SWBBuildServiceSession, any Error>, [SwiftBuildMessage.DiagnosticInfo]) {
        do {
            let response = try await send(request: CreateSessionRequest(name: name, developerPath: .swiftToolchain(Path(swiftToolchainPath)), resourceSearchPaths: resourceSearchPaths.map(Path.init), cachePath: cachePath.map(Path.init), inferiorProductsPath: inferiorProductsPath.map(Path.init), environment: environment))
            let diagnostics = response.diagnostics.map { SwiftBuildMessage.DiagnosticInfo(.init($0, .global)) }
            if let sessionID = response.sessionID {
                return (.success(SWBBuildServiceSession(name: name, uid: sessionID, service: self)), diagnostics)
            } else {
                return (.failure(SwiftBuildError.requestError(description: "Could not initialize build system")), diagnostics)
            }
        } catch {
            return (.failure(error), [])
        }
    }

    /// List the active session UIDs and info.
    ///
    /// - returns: A map of session UIDs to info.
    func listSessions() async throws -> ListSessionsResponse {
        try await send(request: ListSessionsRequest())
    }

    /// List the active session UIDs and names.
    ///
    /// - returns: A map of session UIDs to names.
    public func listSessions() async throws -> [String: String] {
        try await listSessions().sessions.mapValues(\.name)
    }

    @_spi(Testing) public func waitForQuiescence(sessionHandle: String) async throws {
        _ = try await send(request: WaitForQuiescenceRequest(sessionHandle: sessionHandle))
    }

    /// Delete the session with the specified handle.
    public func deleteSession(sessionHandle: String) async throws {
        _ = try await send(request: DeleteSessionRequest(sessionHandle: sessionHandle))
    }

    /// Execute an `swbuild` "command line tool".
    ///
    /// These are tools which are implemented internally,
    /// primarily for debugging and testing purposes.
    func executeCommandLineTool(_ args: [String], workingDirectory: Path, developerPath: String? = nil, stdoutHandler: @Sendable @escaping (Data) -> Void, stderrHandler: @Sendable @escaping (Data) -> Void) async -> Bool {
        let (channel, result): (UInt64, Bool) = await withCheckedContinuation { continuation in
            Task {
                // Allocate a channel.
                let channel = self.openChannel { channel, message in
                    // FIXME: This is rather awkward, transforming the responses into
                    // the meanings in a way decoupled from the IPC definition.
                    switch message {
                    case let asString as StringResponse:
                        stdoutHandler(asString.value.data(using: String.Encoding.utf8)!)
                    case let asError as ErrorResponse:
                        stderrHandler(asError.description.data(using: String.Encoding.utf8)!)
                    case let response as BoolResponse:
                        continuation.resume(returning: (channel, response.value))
                    default:
                        stderrHandler("error: unknown response: \(String(describing: message))".data(using: String.Encoding.utf8)!)
                    }
                }

                // Start the tool.
                _ = await send(ExecuteCommandLineToolRequest(commandLine: args, workingDirectory: workingDirectory, developerPath: developerPath.map(Path.init), replyChannel: channel))
            }
        }

        // Close the channel.
        self.connection.close(channel: channel)

        return result
    }

    /// Creates an XCFramework from `Swift Build`.
    public func createXCFramework(_ args: [String], currentWorkingDirectory: String, developerPath: String?) async -> (Bool, String) {
        do {
            return try await (true, send(request: CreateXCFrameworkRequest(developerPath: developerPath.map(Path.init), commandLine: args, currentWorkingDirectory: Path(currentWorkingDirectory))).value)
        } catch {
            return (false, "\(error)")
        }
    }

    @available(*, deprecated, message: "Do not use.")
    public func appleSystemFrameworkNames(developerPath: String?) async throws -> Set<String> {
        try await Set(send(request: AppleSystemFrameworkNamesRequest(developerPath: developerPath.map(Path.init))).value)
    }

    public func productTypeSupportsMacCatalyst(developerPath: String?, productTypeIdentifier: String) async throws -> Bool {
        try await send(request: ProductTypeSupportsMacCatalystRequest(developerPath: developerPath.map(Path.init), productTypeIdentifier: productTypeIdentifier)).value
    }
}