Skip to content

Commit 2f2a3f8

Browse files
authored
Merge pull request #572 from artemcm/ParsableThroneOfLies
[Parsable Output] Break down batch compile jobs into constituent messages
2 parents 12a7e77 + 3fe87f7 commit 2f2a3f8

File tree

7 files changed

+540
-96
lines changed

7 files changed

+540
-96
lines changed

Sources/SwiftDriver/Driver/Driver.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,6 +1018,7 @@ extension Driver {
10181018
buildRecordInfo: buildRecordInfo,
10191019
incrementalCompilationState: incrementalCompilationState,
10201020
showJobLifecycle: showJobLifecycle,
1021+
argsResolver: executor.resolver,
10211022
diagnosticEngine: diagnosticEngine)
10221023
}
10231024

Sources/SwiftDriver/Driver/ToolExecutionDelegate.swift

Lines changed: 178 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,16 @@ import Glibc
2323
#endif
2424

2525
/// Delegate for printing execution information on the command-line.
26-
final class ToolExecutionDelegate: JobExecutionDelegate {
26+
@_spi(Testing) public final class ToolExecutionDelegate: JobExecutionDelegate {
27+
/// Quasi-PIDs are _negative_ PID-like unique keys used to
28+
/// masquerade batch job constituents as (quasi)processes, when writing
29+
/// parseable output to consumers that don't understand the idea of a batch
30+
/// job. They are negative in order to avoid possibly colliding with real
31+
/// PIDs (which are always positive). We start at -1000 here as a crude but
32+
/// harmless hedge against colliding with an errno value that might slip
33+
/// into the stream of real PIDs.
34+
static let QUASI_PID_START = -1000
35+
2736
public enum Mode {
2837
case verbose
2938
case parsableOutput
@@ -35,19 +44,25 @@ final class ToolExecutionDelegate: JobExecutionDelegate {
3544
public let incrementalCompilationState: IncrementalCompilationState?
3645
public let showJobLifecycle: Bool
3746
public let diagnosticEngine: DiagnosticsEngine
38-
3947
public var anyJobHadAbnormalExit: Bool = false
4048

41-
init(mode: ToolExecutionDelegate.Mode,
42-
buildRecordInfo: BuildRecordInfo?,
43-
incrementalCompilationState: IncrementalCompilationState?,
44-
showJobLifecycle: Bool,
45-
diagnosticEngine: DiagnosticsEngine) {
49+
private var nextBatchQuasiPID: Int
50+
private let argsResolver: ArgsResolver
51+
private var batchJobInputQuasiPIDMap = DictionaryOfDictionaries<Job, TypedVirtualPath, Int>()
52+
53+
@_spi(Testing) public init(mode: ToolExecutionDelegate.Mode,
54+
buildRecordInfo: BuildRecordInfo?,
55+
incrementalCompilationState: IncrementalCompilationState?,
56+
showJobLifecycle: Bool,
57+
argsResolver: ArgsResolver,
58+
diagnosticEngine: DiagnosticsEngine) {
4659
self.mode = mode
4760
self.buildRecordInfo = buildRecordInfo
4861
self.incrementalCompilationState = incrementalCompilationState
4962
self.showJobLifecycle = showJobLifecycle
5063
self.diagnosticEngine = diagnosticEngine
64+
self.argsResolver = argsResolver
65+
self.nextBatchQuasiPID = ToolExecutionDelegate.QUASI_PID_START
5166
}
5267

5368
public func jobStarted(job: Job, arguments: [String], pid: Int) {
@@ -61,22 +76,10 @@ final class ToolExecutionDelegate: JobExecutionDelegate {
6176
stdoutStream <<< arguments.map { $0.spm_shellEscaped() }.joined(separator: " ") <<< "\n"
6277
stdoutStream.flush()
6378
case .parsableOutput:
64-
65-
// Compute the outputs for the message.
66-
let outputs: [BeganMessage.Output] = job.outputs.map {
67-
.init(path: $0.file.name, type: $0.type.description)
79+
let messages = constructJobBeganMessages(job: job, arguments: arguments, pid: pid)
80+
for beganMessage in messages {
81+
emit(ParsableMessage(name: job.kind.rawValue, kind: .began(beganMessage)))
6882
}
69-
70-
let beganMessage = BeganMessage(
71-
pid: pid,
72-
inputs: job.displayInputs.map{ $0.file.name },
73-
outputs: outputs,
74-
commandExecutable: arguments[0],
75-
commandArguments: arguments[1...].map { String($0) }
76-
)
77-
78-
let message = ParsableMessage(name: job.kind.rawValue, kind: .began(beganMessage))
79-
emit(message)
8083
}
8184
}
8285

@@ -108,21 +111,26 @@ final class ToolExecutionDelegate: JobExecutionDelegate {
108111

109112
case .parsableOutput:
110113
let output = (try? result.utf8Output() + result.utf8stderrOutput()).flatMap { $0.isEmpty ? nil : $0 }
111-
let message: ParsableMessage
114+
let messages: [ParsableMessage]
112115

113116
switch result.exitStatus {
114117
case .terminated(let code):
115-
let finishedMessage = FinishedMessage(exitStatus: Int(code), pid: pid, output: output)
116-
message = ParsableMessage(name: job.kind.rawValue, kind: .finished(finishedMessage))
117-
118+
messages = constructJobFinishedMessages(job: job, exitCode: code, output: output,
119+
pid: pid).map {
120+
ParsableMessage(name: job.kind.rawValue, kind: .finished($0))
121+
}
118122
#if !os(Windows)
119123
case .signalled(let signal):
120124
let errorMessage = strsignal(signal).map { String(cString: $0) } ?? ""
121-
let signalledMessage = SignalledMessage(pid: pid, output: output, errorMessage: errorMessage, signal: Int(signal))
122-
message = ParsableMessage(name: job.kind.rawValue, kind: .signalled(signalledMessage))
125+
messages = constructJobSignalledMessages(job: job, error: errorMessage, output: output,
126+
signal: signal, pid: pid).map {
127+
ParsableMessage(name: job.kind.rawValue, kind: .signalled($0))
128+
}
123129
#endif
124130
}
125-
emit(message)
131+
for message in messages {
132+
emit(message)
133+
}
126134
}
127135
}
128136

@@ -151,6 +159,147 @@ final class ToolExecutionDelegate: JobExecutionDelegate {
151159
}
152160
}
153161

162+
// MARK: - Message Construction
163+
/// Generation of messages from jobs, including breaking down batch compile jobs into constituent messages.
164+
private extension ToolExecutionDelegate {
165+
166+
// MARK: - Job Began
167+
func constructJobBeganMessages(job: Job, arguments: [String], pid: Int) -> [BeganMessage] {
168+
let result : [BeganMessage]
169+
if job.kind == .compile,
170+
job.primaryInputs.count > 1 {
171+
// Batched compile jobs need to be broken up into multiple messages, one per constituent.
172+
result = constructBatchCompileBeginMessages(job: job, arguments: arguments, pid: pid,
173+
quasiPIDBase: nextBatchQuasiPID)
174+
// Today, parseable-output messages are constructed and emitted synchronously
175+
// on `MultiJobExecutor`'s `delegateQueue`. This is why the below operation is safe.
176+
nextBatchQuasiPID -= result.count
177+
} else {
178+
result = [constructSingleBeganMessage(inputs: job.displayInputs,
179+
outputs: job.outputs,
180+
arguments: arguments,
181+
pid: pid,
182+
realPid: pid)]
183+
}
184+
185+
return result
186+
}
187+
188+
func constructBatchCompileBeginMessages(job: Job, arguments: [String], pid: Int,
189+
quasiPIDBase: Int) -> [BeganMessage] {
190+
precondition(job.kind == .compile && job.primaryInputs.count > 1)
191+
var quasiPID = quasiPIDBase
192+
var result : [BeganMessage] = []
193+
for input in job.primaryInputs {
194+
let outputs = job.getCompileInputOutputs(for: input) ?? []
195+
let outputPaths = outputs.map {
196+
TypedVirtualPath(file: try! VirtualPath.intern(path: argsResolver.resolve(.path($0.file))),
197+
type: $0.type)
198+
}
199+
result.append(
200+
constructSingleBeganMessage(inputs: [input],
201+
outputs: outputPaths,
202+
arguments: arguments,
203+
pid: quasiPID,
204+
realPid: pid))
205+
// Save the quasiPID of this job/input combination in order to generate the correct
206+
// `finished` message
207+
batchJobInputQuasiPIDMap[(job, input)] = quasiPID
208+
quasiPID -= 1
209+
}
210+
return result
211+
}
212+
213+
func constructSingleBeganMessage(inputs: [TypedVirtualPath], outputs: [TypedVirtualPath],
214+
arguments: [String], pid: Int, realPid: Int) -> BeganMessage {
215+
let outputs: [BeganMessage.Output] = outputs.map {
216+
.init(path: $0.file.name, type: $0.type.description)
217+
}
218+
219+
return BeganMessage(
220+
pid: pid,
221+
realPid: realPid,
222+
inputs: inputs.map{ $0.file.name },
223+
outputs: outputs,
224+
commandExecutable: arguments[0],
225+
commandArguments: arguments[1...].map { String($0) }
226+
)
227+
}
228+
229+
// MARK: - Job Finished
230+
func constructJobFinishedMessages(job: Job, exitCode: Int32, output: String?, pid: Int)
231+
-> [FinishedMessage] {
232+
let result : [FinishedMessage]
233+
if job.kind == .compile,
234+
job.primaryInputs.count > 1 {
235+
result = constructBatchCompileFinishedMessages(job: job, exitCode: exitCode,
236+
output: output, pid: pid)
237+
} else {
238+
result = [constructSingleFinishedMessage(exitCode: exitCode, output: output,
239+
pid: pid, realPid: pid)]
240+
}
241+
return result
242+
}
243+
244+
func constructBatchCompileFinishedMessages(job: Job, exitCode: Int32, output: String?, pid: Int)
245+
-> [FinishedMessage] {
246+
precondition(job.kind == .compile && job.primaryInputs.count > 1)
247+
var result : [FinishedMessage] = []
248+
for input in job.primaryInputs {
249+
guard let quasiPid = batchJobInputQuasiPIDMap[(job, input)] else {
250+
fatalError("Parsable-Output batch sub-job finished with no matching started message: \(job.description) : \(input.file.description)")
251+
}
252+
result.append(
253+
constructSingleFinishedMessage(exitCode: exitCode, output: output,
254+
pid: quasiPid, realPid: pid))
255+
}
256+
return result
257+
}
258+
259+
func constructSingleFinishedMessage(exitCode: Int32, output: String?, pid: Int, realPid: Int)
260+
-> FinishedMessage {
261+
return FinishedMessage(exitStatus: Int(exitCode), output: output, pid: pid, realPid: realPid)
262+
}
263+
264+
// MARK: - Job Signalled
265+
func constructJobSignalledMessages(job: Job, error: String, output: String?,
266+
signal: Int32, pid: Int) -> [SignalledMessage] {
267+
let result : [SignalledMessage]
268+
if job.kind == .compile,
269+
job.primaryInputs.count > 1 {
270+
result = constructBatchCompileSignalledMessages(job: job, error: error, output: output,
271+
signal: signal, pid: pid)
272+
} else {
273+
result = [constructSingleSignalledMessage(error: error, output: output, signal: signal,
274+
pid: pid, realPid: pid)]
275+
}
276+
return result
277+
}
278+
279+
func constructBatchCompileSignalledMessages(job: Job, error: String, output: String?,
280+
signal: Int32, pid: Int)
281+
-> [SignalledMessage] {
282+
precondition(job.kind == .compile && job.primaryInputs.count > 1)
283+
var result : [SignalledMessage] = []
284+
for input in job.primaryInputs {
285+
guard let quasiPid = batchJobInputQuasiPIDMap[(job, input)] else {
286+
fatalError("Parsable-Output batch sub-job signalled with no matching started message: \(job.description) : \(input.file.description)")
287+
}
288+
result.append(
289+
constructSingleSignalledMessage(error: error, output: output, signal: signal,
290+
pid: quasiPid, realPid: pid))
291+
}
292+
return result
293+
}
294+
295+
func constructSingleSignalledMessage(error: String, output: String?, signal: Int32,
296+
pid: Int, realPid: Int)
297+
-> SignalledMessage {
298+
return SignalledMessage(pid: pid, realPid: realPid, output: output,
299+
errorMessage: error, signal: Int(signal))
300+
}
301+
}
302+
154303
fileprivate extension Diagnostic.Message {
155304
static func remark_job_lifecycle(_ what: String, _ job: Job
156305
) -> Diagnostic.Message {

Sources/SwiftDriver/Execution/ParsableOutput.swift

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ import Foundation
3939
}
4040
}
4141

42+
@_spi(Testing) public struct ActualProcess: Encodable {
43+
public let realPid: Int
44+
45+
public init(realPid: Int) {
46+
self.realPid = realPid
47+
}
48+
49+
private enum CodingKeys: String, CodingKey {
50+
case realPid = "real_pid"
51+
}
52+
}
53+
4254
@_spi(Testing) public struct BeganMessage: Encodable {
4355
public struct Output: Encodable {
4456
public let type: String
@@ -50,6 +62,7 @@ import Foundation
5062
}
5163
}
5264

65+
public let process: ActualProcess
5366
public let pid: Int
5467
public let inputs: [String]
5568
public let outputs: [Output]
@@ -58,12 +71,14 @@ import Foundation
5871

5972
public init(
6073
pid: Int,
74+
realPid: Int,
6175
inputs: [String],
6276
outputs: [Output],
6377
commandExecutable: String,
6478
commandArguments: [String]
6579
) {
6680
self.pid = pid
81+
self.process = ActualProcess(realPid: realPid)
6782
self.inputs = inputs
6883
self.outputs = outputs
6984
self.commandExecutable = commandExecutable
@@ -72,6 +87,7 @@ import Foundation
7287

7388
private enum CodingKeys: String, CodingKey {
7489
case pid
90+
case process
7591
case inputs
7692
case outputs
7793
case commandExecutable = "command_executable"
@@ -94,42 +110,47 @@ import Foundation
94110
@_spi(Testing) public struct FinishedMessage: Encodable {
95111
let exitStatus: Int
96112
let pid: Int
113+
let process: ActualProcess
97114
let output: String?
98115

99-
// proc-info
100-
101116
public init(
102117
exitStatus: Int,
118+
output: String?,
103119
pid: Int,
104-
output: String?
120+
realPid: Int
105121
) {
106122
self.exitStatus = exitStatus
107123
self.pid = pid
124+
self.process = ActualProcess(realPid: realPid)
108125
self.output = output
109126
}
110127

111128
private enum CodingKeys: String, CodingKey {
112129
case pid
130+
case process
113131
case output
114132
case exitStatus = "exit-status"
115133
}
116134
}
117135

118136
@_spi(Testing) public struct SignalledMessage: Encodable {
119137
let pid: Int
138+
let process: ActualProcess
120139
let output: String?
121140
let errorMessage: String
122141
let signal: Int
123142

124-
public init(pid: Int, output: String?, errorMessage: String, signal: Int) {
143+
public init(pid: Int, realPid: Int, output: String?, errorMessage: String, signal: Int) {
125144
self.pid = pid
145+
self.process = ActualProcess(realPid: realPid)
126146
self.output = output
127147
self.errorMessage = errorMessage
128148
self.signal = signal
129149
}
130150

131151
private enum CodingKeys: String, CodingKey {
132152
case pid
153+
case process
133154
case output
134155
case errorMessage = "error-message"
135156
case signal

0 commit comments

Comments
 (0)