Skip to content

Commit c3d36dc

Browse files
Add swift crash producer support
When swift compiler crashed with swift caching enabled, create a crash reproducer automatically.
1 parent 145dc76 commit c3d36dc

File tree

8 files changed

+86
-3
lines changed

8 files changed

+86
-3
lines changed

Sources/SwiftDriver/Driver/Driver.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import struct TSCBasic.AbsolutePath
2323
import struct TSCBasic.ByteString
2424
import struct TSCBasic.Diagnostic
2525
import struct TSCBasic.FileInfo
26+
import struct TSCBasic.ProcessResult
2627
import struct TSCBasic.RelativePath
2728
import struct TSCBasic.SHA256
2829
import var TSCBasic.localFileSystem
@@ -71,6 +72,7 @@ public struct Driver {
7172
case conditionalCompilationFlagIsNotValidIdentifier(String)
7273
case baselineGenerationRequiresTopLevelModule(String)
7374
case optionRequiresAnother(String, String)
75+
case unableToCreateReproducer
7476
// Explicit Module Build Failures
7577
case malformedModuleDependency(String, String)
7678
case missingModuleDependency(String)
@@ -142,6 +144,8 @@ public struct Driver {
142144
return "generating a baseline with '\(arg)' is only supported with '-emit-module' or '-emit-module-path'"
143145
case .optionRequiresAnother(let first, let second):
144146
return "'\(first)' cannot be specified if '\(second)' is not present"
147+
case .unableToCreateReproducer:
148+
return "failed to create reproducer"
145149
}
146150
}
147151
}
@@ -962,7 +966,7 @@ public struct Driver {
962966
negative: .disableIncrementalFileHashing,
963967
default: false)
964968
self.recordedInputMetadata = .init(uniqueKeysWithValues:
965-
Set(inputFiles).compactMap { inputFile -> (TypedVirtualPath, FileMetadata)? in
969+
Set(inputFiles).compactMap { inputFile -> (TypedVirtualPath, FileMetadata)? in
966970
guard let modTime = try? fileSystem.lastModificationTime(for: inputFile.file) else { return nil }
967971
if incrementalFileHashes {
968972
guard let data = try? fileSystem.readFileContents(inputFile.file) else { return nil }
@@ -1957,7 +1961,8 @@ extension Driver {
19571961
buildRecordInfo: buildRecordInfo,
19581962
showJobLifecycle: showJobLifecycle,
19591963
argsResolver: executor.resolver,
1960-
diagnosticEngine: diagnosticEngine)
1964+
diagnosticEngine: diagnosticEngine,
1965+
reproducerCallback: supportsReproducer ? Driver.generateReproducer : nil)
19611966
}
19621967

19631968
private mutating func performTheBuild(

Sources/SwiftDriver/Driver/ToolExecutionDelegate.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import struct TSCBasic.Diagnostic
3030
import struct TSCBasic.ProcessResult
3131
import var TSCBasic.stderrStream
3232
import var TSCBasic.stdoutStream
33+
import class TSCBasic.Process
3334

3435
/// Delegate for printing execution information on the command-line.
3536
@_spi(Testing) public final class ToolExecutionDelegate: JobExecutionDelegate {
@@ -49,6 +50,8 @@ import var TSCBasic.stdoutStream
4950
case silent
5051
}
5152

53+
public typealias ReproducerCallback = (Job, VirtualPath) -> Job
54+
5255
public let mode: Mode
5356
public let buildRecordInfo: BuildRecordInfo?
5457
public let showJobLifecycle: Bool
@@ -58,18 +61,21 @@ import var TSCBasic.stdoutStream
5861
private var nextBatchQuasiPID: Int
5962
private let argsResolver: ArgsResolver
6063
private var batchJobInputQuasiPIDMap = TwoLevelMap<Job, TypedVirtualPath, Int>()
64+
private let reproducerCallback: ReproducerCallback?
6165

6266
@_spi(Testing) public init(mode: ToolExecutionDelegate.Mode,
6367
buildRecordInfo: BuildRecordInfo?,
6468
showJobLifecycle: Bool,
6569
argsResolver: ArgsResolver,
66-
diagnosticEngine: DiagnosticsEngine) {
70+
diagnosticEngine: DiagnosticsEngine,
71+
reproducerCallback: ReproducerCallback? = nil) {
6772
self.mode = mode
6873
self.buildRecordInfo = buildRecordInfo
6974
self.showJobLifecycle = showJobLifecycle
7075
self.diagnosticEngine = diagnosticEngine
7176
self.argsResolver = argsResolver
7277
self.nextBatchQuasiPID = ToolExecutionDelegate.QUASI_PID_START
78+
self.reproducerCallback = reproducerCallback
7379
}
7480

7581
public func jobStarted(job: Job, arguments: [String], pid: Int) {
@@ -170,6 +176,13 @@ import var TSCBasic.stdoutStream
170176
}
171177
}
172178

179+
public func getReproducerJob(job: Job, output: VirtualPath) -> Job? {
180+
guard let reproducerCallback = reproducerCallback else {
181+
return nil
182+
}
183+
return reproducerCallback(job, output)
184+
}
185+
173186
private func emit(_ message: ParsableMessage) {
174187
// FIXME: Do we need to do error handling here? Can this even fail?
175188
guard let json = try? message.toJSON() else { return }

Sources/SwiftDriver/Execution/DriverExecutor.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,9 @@ public protocol JobExecutionDelegate {
206206

207207
/// Called when a job is skipped.
208208
func jobSkipped(job: Job)
209+
210+
/// Create a new job that constructs a reproducer for the providing job.
211+
func getReproducerJob(job: Job, output: VirtualPath) -> Job?
209212
}
210213

211214
@_spi(Testing) public extension ProcessEnvironmentBlock {

Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,6 +1071,23 @@ extension Driver {
10711071
}
10721072
}
10731073

1074+
// Generate reproducer.
1075+
extension Driver {
1076+
var supportsReproducer: Bool {
1077+
isFrontendArgSupported(.genReproducer) && enableCaching
1078+
}
1079+
1080+
static func generateReproducer(_ job: Job, _ output: VirtualPath) -> Job {
1081+
var reproJob = job
1082+
reproJob.commandLine.appendFlag(.genReproducer)
1083+
reproJob.commandLine.appendFlag(.genReproducerDir)
1084+
reproJob.commandLine.appendPath(output)
1085+
reproJob.outputs.removeAll()
1086+
reproJob.outputCacheKeys.removeAll()
1087+
return reproJob
1088+
}
1089+
}
1090+
10741091
extension ParsedOptions {
10751092
/// Checks whether experimental embedded mode is enabled.
10761093
var isEmbeddedEnabled: Bool {

Sources/SwiftDriver/Jobs/PrebuiltModulesJob.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,13 +270,19 @@ fileprivate class ModuleCompileDelegate: JobExecutionDelegate {
270270
static func canHandle(job: Job) -> Bool {
271271
return job.kind == .compile
272272
}
273+
func getReproducerJob(job: Job, output: VirtualPath) -> Job? {
274+
nil
275+
}
273276
}
274277

275278
fileprivate class ABICheckingDelegate: JobExecutionDelegate {
276279
let verbose: Bool
277280
let logPath: AbsolutePath?
278281

279282
func jobSkipped(job: Job) {}
283+
func getReproducerJob(job: Job, output: VirtualPath) -> Job? {
284+
nil
285+
}
280286

281287
public init(_ verbose: Bool, _ logPath: AbsolutePath?) {
282288
self.verbose = verbose
@@ -339,6 +345,9 @@ public class PrebuiltModuleGenerationDelegate: JobExecutionDelegate {
339345
public func jobSkipped(job: Job) {
340346
selectDelegate(job: job).jobSkipped(job: job)
341347
}
348+
public func getReproducerJob(job: Job, output: VirtualPath) -> Job? {
349+
selectDelegate(job: job).getReproducerJob(job: job, output: output)
350+
}
342351
public var shouldRunDanglingJobs: Bool {
343352
return compileDelegate.shouldRunDanglingJobs
344353
}

Sources/SwiftDriverExecution/MultiJobExecutor.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import protocol TSCBasic.DiagnosticData
2525
import protocol TSCBasic.FileSystem
2626
import struct TSCBasic.Diagnostic
2727
import struct TSCBasic.ProcessResult
28+
import func TSCBasic.withTemporaryDirectory
2829
import typealias TSCBasic.ProcessEnvironmentBlock
2930
import enum TSCUtility.Diagnostics
3031

@@ -632,12 +633,14 @@ class ExecuteJobRule: LLBuildRule {
632633
#if os(Windows)
633634
case let .abnormal(exception):
634635
context.diagnosticsEngine.emit(.error_command_exception(kind: job.kind, exception: exception))
636+
try handleSignalledJob(for: job)
635637
#else
636638
case let .signalled(signal):
637639
// An interrupt of an individual compiler job means it was deliberately cancelled,
638640
// most likely by the driver itself. This does not constitute an error.
639641
if signal != SIGINT {
640642
context.diagnosticsEngine.emit(.error_command_signalled(kind: job.kind, signal: signal))
643+
try handleSignalledJob(for: job)
641644
}
642645
#endif
643646
}
@@ -673,6 +676,23 @@ class ExecuteJobRule: LLBuildRule {
673676

674677
engine.taskIsComplete(value)
675678
}
679+
680+
private func handleSignalledJob(for job: Job) throws {
681+
try withTemporaryDirectory(dir: fileSystem.tempDirectory, prefix: "swift-reproducer", removeTreeOnDeinit: false) { tempDir in
682+
guard let reproJob = context.executorDelegate.getReproducerJob(job: job, output: VirtualPath.absolute(tempDir)) else {
683+
return
684+
}
685+
let arguments: [String] = try context.argsResolver.resolveArgumentList(for: reproJob,
686+
useResponseFiles: .heuristic)
687+
let process = try context.processType.launchProcess(arguments: arguments, env: context.env)
688+
let reproResult = try process.waitUntilExit()
689+
if case .terminated(let code) = reproResult.exitStatus, code == 0 {
690+
context.diagnosticsEngine.emit(.note_reproducer_created(tempDir.pathString))
691+
} else {
692+
context.diagnosticsEngine.emit(.error_failed_to_create_reproducer)
693+
}
694+
}
695+
}
676696
}
677697

678698
fileprivate extension Job {
@@ -700,4 +720,12 @@ private extension TSCBasic.Diagnostic.Message {
700720
static func error_command_exception(kind: Job.Kind, exception: UInt32) -> TSCBasic.Diagnostic.Message {
701721
.error("\(kind.rawValue) command failed due to exception \(exception) (use -v to see invocation)")
702722
}
723+
724+
static var error_failed_to_create_reproducer: Diagnostic.Message {
725+
.error("failed to create crash reproducer")
726+
}
727+
728+
static func note_reproducer_created(_ path: String) -> Diagnostic.Message {
729+
.note("crash reproducer is created at: \(path)")
730+
}
703731
}

Sources/SwiftOptions/Options.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,8 @@ extension Option {
586586
public static let F: Option = Option("-F", .joinedOrSeparate, attributes: [.frontend, .synthesizeInterface, .argumentIsPath], helpText: "Add directory to framework search path")
587587
public static let gccToolchain: Option = Option("-gcc-toolchain", .separate, attributes: [.helpHidden, .argumentIsPath], metaVar: "<path>", helpText: "Specify a directory where the clang importer and clang linker can find headers and libraries")
588588
public static let gdwarfTypes: Option = Option("-gdwarf-types", .flag, attributes: [.frontend], helpText: "Emit full DWARF type info.", group: .g)
589+
public static let genReproducerDir: Option = Option("-gen-reproducer-dir", .separate, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Path to directory where reproducers write to.")
590+
public static let genReproducer: Option = Option("-gen-reproducer", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Generate a reproducer for current compilation.")
589591
public static let generateEmptyBaseline: Option = Option("-generate-empty-baseline", .flag, attributes: [.noDriver], helpText: "Generate an empty baseline")
590592
public static let generateEmptyBaseline_: Option = Option("--generate-empty-baseline", .flag, alias: Option.generateEmptyBaseline, attributes: [.noDriver], helpText: "Generate an empty baseline")
591593
public static let generateMigrationScript: Option = Option("-generate-migration-script", .flag, attributes: [.noDriver], helpText: "Compare SDK content in JSON file and generate migration script")
@@ -1553,6 +1555,8 @@ extension Option {
15531555
Option.F,
15541556
Option.gccToolchain,
15551557
Option.gdwarfTypes,
1558+
Option.genReproducerDir,
1559+
Option.genReproducer,
15561560
Option.generateEmptyBaseline,
15571561
Option.generateEmptyBaseline_,
15581562
Option.generateMigrationScript,

Tests/SwiftDriverTests/JobExecutorTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ class JobCollectingDelegate: JobExecutionDelegate {
5858
}
5959

6060
func jobSkipped(job: Job) {}
61+
62+
func getReproducerJob(job: Job, output: VirtualPath) -> Job? {
63+
nil
64+
}
6165
}
6266

6367
extension DarwinToolchain {

0 commit comments

Comments
 (0)