Skip to content

Commit bdd7963

Browse files
committed
Add AsyncBuildOperation alternative to BuildOperation
For now this is mostly a copy of `BuildOperation`, but with `async` entrypoints. This can be expanded to use more structured concurrency features in the future as we migrate more code. It is disabled by default, for testing purposes I'm exposing it as `swift build --experimental-async-build-system`, which creates and uses `AsyncBuildOperation` instead of blocking synchronous `BuildOperation`.
1 parent 95ebc1f commit bdd7963

14 files changed

+1502
-121
lines changed

Sources/Build/AsyncBuildOperation.swift

Lines changed: 837 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2018-2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Basics
14+
import LLBuildManifest
15+
import enum PackageModel.BuildConfiguration
16+
import TSCBasic
17+
import TSCUtility
18+
19+
@_spi(SwiftPMInternal)
20+
import SPMBuildCore
21+
22+
import SPMLLBuild
23+
24+
import enum Dispatch.DispatchTimeInterval
25+
import protocol Foundation.LocalizedError
26+
27+
/// Async-friendly llbuild delegate implementation
28+
final class AsyncLLBuildDelegate: LLBuildBuildSystemDelegate, SwiftCompilerOutputParserDelegate {
29+
private let outputStream: ThreadSafeOutputByteStream
30+
private let progressAnimation: ProgressAnimationProtocol
31+
var commandFailureHandler: (() -> Void)?
32+
private let logLevel: Basics.Diagnostic.Severity
33+
private let eventsContinuation: AsyncStream<BuildSystemEvent>.Continuation
34+
private let buildSystem: AsyncBuildOperation
35+
private var taskTracker = CommandTaskTracker()
36+
private var errorMessagesByTarget: [String: [String]] = [:]
37+
private let observabilityScope: ObservabilityScope
38+
private var cancelled: Bool = false
39+
40+
/// Swift parsers keyed by llbuild command name.
41+
private var swiftParsers: [String: SwiftCompilerOutputParser] = [:]
42+
43+
/// Buffer to accumulate non-swift output until command is finished
44+
private var nonSwiftMessageBuffers: [String: [UInt8]] = [:]
45+
46+
/// The build execution context.
47+
private let buildExecutionContext: BuildExecutionContext
48+
49+
init(
50+
buildSystem: AsyncBuildOperation,
51+
buildExecutionContext: BuildExecutionContext,
52+
eventsContinuation: AsyncStream<BuildSystemEvent>.Continuation,
53+
outputStream: OutputByteStream,
54+
progressAnimation: ProgressAnimationProtocol,
55+
logLevel: Basics.Diagnostic.Severity,
56+
observabilityScope: ObservabilityScope
57+
) {
58+
self.buildSystem = buildSystem
59+
self.buildExecutionContext = buildExecutionContext
60+
// FIXME: Implement a class convenience initializer that does this once they are supported
61+
// https://forums.swift.org/t/allow-self-x-in-class-convenience-initializers/15924
62+
self.outputStream = outputStream as? ThreadSafeOutputByteStream ?? ThreadSafeOutputByteStream(outputStream)
63+
self.progressAnimation = progressAnimation
64+
self.logLevel = logLevel
65+
self.observabilityScope = observabilityScope
66+
self.eventsContinuation = eventsContinuation
67+
68+
let swiftParsers = buildExecutionContext.buildDescription?.swiftCommands.mapValues { tool in
69+
SwiftCompilerOutputParser(targetName: tool.moduleName, delegate: self)
70+
} ?? [:]
71+
self.swiftParsers = swiftParsers
72+
73+
self.taskTracker.onTaskProgressUpdateText = { progressText, _ in
74+
self.eventsContinuation.yield(.didUpdateTaskProgress(text: progressText))
75+
}
76+
}
77+
78+
// MARK: llbuildSwift.BuildSystemDelegate
79+
80+
var fs: SPMLLBuild.FileSystem? {
81+
nil
82+
}
83+
84+
func lookupTool(_ name: String) -> Tool? {
85+
switch name {
86+
case TestDiscoveryTool.name:
87+
return InProcessTool(buildExecutionContext, type: TestDiscoveryCommand.self)
88+
case TestEntryPointTool.name:
89+
return InProcessTool(buildExecutionContext, type: TestEntryPointCommand.self)
90+
case PackageStructureTool.name:
91+
return InProcessTool(buildExecutionContext, type: PackageStructureCommand.self)
92+
case CopyTool.name:
93+
return InProcessTool(buildExecutionContext, type: CopyCommand.self)
94+
case WriteAuxiliaryFile.name:
95+
return InProcessTool(buildExecutionContext, type: WriteAuxiliaryFileCommand.self)
96+
default:
97+
return nil
98+
}
99+
}
100+
101+
func hadCommandFailure() {
102+
self.commandFailureHandler?()
103+
}
104+
105+
func handleDiagnostic(_ diagnostic: SPMLLBuild.Diagnostic) {
106+
switch diagnostic.kind {
107+
case .note:
108+
self.observabilityScope.emit(info: diagnostic.message)
109+
case .warning:
110+
self.observabilityScope.emit(warning: diagnostic.message)
111+
case .error:
112+
self.observabilityScope.emit(error: diagnostic.message)
113+
@unknown default:
114+
self.observabilityScope.emit(info: diagnostic.message)
115+
}
116+
}
117+
118+
func commandStatusChanged(_ command: SPMLLBuild.Command, kind: CommandStatusKind) {
119+
guard !self.logLevel.isVerbose else { return }
120+
guard command.shouldShowStatus else { return }
121+
guard !swiftParsers.keys.contains(command.name) else { return }
122+
123+
self.taskTracker.commandStatusChanged(command, kind: kind)
124+
self.updateProgress()
125+
}
126+
127+
func commandPreparing(_ command: SPMLLBuild.Command) {
128+
self.eventsContinuation.yield(.willStart(command: .init(command)))
129+
}
130+
131+
func commandStarted(_ command: SPMLLBuild.Command) {
132+
guard command.shouldShowStatus else { return }
133+
134+
self.eventsContinuation.yield(.didStart(command: .init(command)))
135+
if self.logLevel.isVerbose {
136+
self.outputStream.send("\(command.verboseDescription)\n")
137+
self.outputStream.flush()
138+
}
139+
}
140+
141+
func shouldCommandStart(_: SPMLLBuild.Command) -> Bool {
142+
true
143+
}
144+
145+
func commandFinished(_ command: SPMLLBuild.Command, result: CommandResult) {
146+
guard command.shouldShowStatus else { return }
147+
guard !swiftParsers.keys.contains(command.name) else { return }
148+
149+
if result == .cancelled {
150+
self.cancelled = true
151+
self.eventsContinuation.yield(.didCancel)
152+
}
153+
154+
self.eventsContinuation.yield(.didFinish(command: .init(command)))
155+
156+
if !self.logLevel.isVerbose {
157+
let targetName = self.swiftParsers[command.name]?.targetName
158+
self.taskTracker.commandFinished(command, result: result, targetName: targetName)
159+
self.updateProgress()
160+
}
161+
}
162+
163+
func commandHadError(_ command: SPMLLBuild.Command, message: String) {
164+
self.observabilityScope.emit(error: message)
165+
}
166+
167+
func commandHadNote(_ command: SPMLLBuild.Command, message: String) {
168+
self.observabilityScope.emit(info: message)
169+
}
170+
171+
func commandHadWarning(_ command: SPMLLBuild.Command, message: String) {
172+
self.observabilityScope.emit(warning: message)
173+
}
174+
175+
func commandCannotBuildOutputDueToMissingInputs(
176+
_ command: SPMLLBuild.Command,
177+
output: BuildKey,
178+
inputs: [BuildKey]
179+
) {
180+
self.observabilityScope.emit(.missingInputs(output: output, inputs: inputs))
181+
}
182+
183+
func cannotBuildNodeDueToMultipleProducers(output: BuildKey, commands: [SPMLLBuild.Command]) {
184+
self.observabilityScope.emit(.multipleProducers(output: output, commands: commands))
185+
}
186+
187+
func commandProcessStarted(_ command: SPMLLBuild.Command, process: ProcessHandle) {}
188+
189+
func commandProcessHadError(_ command: SPMLLBuild.Command, process: ProcessHandle, message: String) {
190+
self.observabilityScope.emit(.commandError(command: command, message: message))
191+
}
192+
193+
func commandProcessHadOutput(_ command: SPMLLBuild.Command, process: ProcessHandle, data: [UInt8]) {
194+
guard command.shouldShowStatus else { return }
195+
196+
if let swiftParser = swiftParsers[command.name] {
197+
swiftParser.parse(bytes: data)
198+
} else {
199+
self.nonSwiftMessageBuffers[command.name, default: []] += data
200+
}
201+
}
202+
203+
func commandProcessFinished(
204+
_ command: SPMLLBuild.Command,
205+
process: ProcessHandle,
206+
result: CommandExtendedResult
207+
) {
208+
// FIXME: This should really happen at the command-level and is just a stopgap measure.
209+
let shouldFilterOutput = !self.logLevel.isVerbose && command.verboseDescription.hasPrefix("codesign ") && result.result != .failed
210+
if let buffer = self.nonSwiftMessageBuffers[command.name], !shouldFilterOutput {
211+
self.progressAnimation.clear()
212+
self.outputStream.send(buffer)
213+
self.outputStream.flush()
214+
self.nonSwiftMessageBuffers[command.name] = nil
215+
}
216+
217+
switch result.result {
218+
case .cancelled:
219+
self.cancelled = true
220+
self.eventsContinuation.yield(.didCancel)
221+
case .failed:
222+
// The command failed, so we queue up an asynchronous task to see if we have any error messages from the
223+
// target to provide advice about.
224+
guard let target = self.swiftParsers[command.name]?.targetName else { return }
225+
guard let errorMessages = self.errorMessagesByTarget[target] else { return }
226+
for errorMessage in errorMessages {
227+
// Emit any advice that's provided for each error message.
228+
if let adviceMessage = self.buildExecutionContext.buildErrorAdviceProvider?.provideBuildErrorAdvice(
229+
for: target,
230+
command: command.name,
231+
message: errorMessage
232+
) {
233+
self.outputStream.send("note: \(adviceMessage)\n")
234+
self.outputStream.flush()
235+
}
236+
}
237+
case .succeeded, .skipped:
238+
break
239+
@unknown default:
240+
break
241+
}
242+
}
243+
244+
func cycleDetected(rules: [BuildKey]) {
245+
self.observabilityScope.emit(.cycleError(rules: rules))
246+
247+
self.eventsContinuation.yield(.didDetectCycleInRules)
248+
}
249+
250+
func shouldResolveCycle(rules: [BuildKey], candidate: BuildKey, action: CycleAction) -> Bool {
251+
false
252+
}
253+
254+
/// Invoked right before running an action taken before building.
255+
func preparationStepStarted(_ name: String) {
256+
self.taskTracker.buildPreparationStepStarted(name)
257+
self.updateProgress()
258+
}
259+
260+
/// Invoked when an action taken before building emits output.
261+
/// when verboseOnly is set to true, the output will only be printed in verbose logging mode
262+
func preparationStepHadOutput(_ name: String, output: String, verboseOnly: Bool) {
263+
self.progressAnimation.clear()
264+
if !verboseOnly || self.logLevel.isVerbose {
265+
self.outputStream.send("\(output.spm_chomp())\n")
266+
self.outputStream.flush()
267+
}
268+
}
269+
270+
/// Invoked right after running an action taken before building. The result
271+
/// indicates whether the action succeeded, failed, or was cancelled.
272+
func preparationStepFinished(_ name: String, result: CommandResult) {
273+
self.taskTracker.buildPreparationStepFinished(name)
274+
self.updateProgress()
275+
}
276+
277+
// MARK: SwiftCompilerOutputParserDelegate
278+
279+
func swiftCompilerOutputParser(_ parser: SwiftCompilerOutputParser, didParse message: SwiftCompilerMessage) {
280+
if self.logLevel.isVerbose {
281+
if let text = message.verboseProgressText {
282+
self.outputStream.send("\(text)\n")
283+
self.outputStream.flush()
284+
}
285+
} else {
286+
self.taskTracker.swiftCompilerDidOutputMessage(message, targetName: parser.targetName)
287+
self.updateProgress()
288+
}
289+
290+
if let output = message.standardOutput {
291+
// first we want to print the output so users have it handy
292+
if !self.logLevel.isVerbose {
293+
self.progressAnimation.clear()
294+
}
295+
296+
self.outputStream.send(output)
297+
self.outputStream.flush()
298+
299+
// next we want to try and scoop out any errors from the output (if reasonable size, otherwise this
300+
// will be very slow), so they can later be passed to the advice provider in case of failure.
301+
if output.utf8.count < 1024 * 10 {
302+
let regex = try! RegEx(pattern: #".*(error:[^\n]*)\n.*"#, options: .dotMatchesLineSeparators)
303+
for match in regex.matchGroups(in: output) {
304+
self.errorMessagesByTarget[parser.targetName] = (
305+
self.errorMessagesByTarget[parser.targetName] ?? []
306+
) + [match[0]]
307+
}
308+
}
309+
}
310+
}
311+
312+
func swiftCompilerOutputParser(_ parser: SwiftCompilerOutputParser, didFailWith error: Error) {
313+
let message = (error as? LocalizedError)?.errorDescription ?? error.localizedDescription
314+
self.observabilityScope.emit(.swiftCompilerOutputParsingError(message))
315+
self.commandFailureHandler?()
316+
}
317+
318+
func buildStart(configuration: BuildConfiguration) {
319+
self.progressAnimation.clear()
320+
self.outputStream.send("Building for \(configuration == .debug ? "debugging" : "production")...\n")
321+
self.outputStream.flush()
322+
}
323+
324+
func buildComplete(success: Bool, duration: DispatchTimeInterval, subsetDescriptor: String? = nil) {
325+
let subsetString: String
326+
if let subsetDescriptor {
327+
subsetString = "of \(subsetDescriptor) "
328+
} else {
329+
subsetString = ""
330+
}
331+
332+
self.progressAnimation.complete(success: success)
333+
if success {
334+
let message = cancelled ? "Build \(subsetString)cancelled!" : "Build \(subsetString)complete!"
335+
self.progressAnimation.clear()
336+
self.outputStream.send("\(message) (\(duration.descriptionInSeconds))\n")
337+
self.outputStream.flush()
338+
}
339+
}
340+
341+
// MARK: Private
342+
343+
private func updateProgress() {
344+
if let progressText = taskTracker.latestFinishedText {
345+
self.progressAnimation.update(
346+
step: taskTracker.finishedCount,
347+
total: taskTracker.totalCount,
348+
text: progressText
349+
)
350+
}
351+
}
352+
}

0 commit comments

Comments
 (0)