diff --git a/Sources/SwiftDriver/Driver/Driver.swift b/Sources/SwiftDriver/Driver/Driver.swift index 6f7c74ba0..a89dac9dd 100644 --- a/Sources/SwiftDriver/Driver/Driver.swift +++ b/Sources/SwiftDriver/Driver/Driver.swift @@ -23,6 +23,7 @@ import struct TSCBasic.AbsolutePath import struct TSCBasic.ByteString import struct TSCBasic.Diagnostic import struct TSCBasic.FileInfo +import struct TSCBasic.ProcessResult import struct TSCBasic.RelativePath import struct TSCBasic.SHA256 import var TSCBasic.localFileSystem @@ -69,6 +70,7 @@ public struct Driver { case conditionalCompilationFlagIsNotValidIdentifier(String) case baselineGenerationRequiresTopLevelModule(String) case optionRequiresAnother(String, String) + case unableToCreateReproducer // Explicit Module Build Failures case malformedModuleDependency(String, String) case missingModuleDependency(String) @@ -140,6 +142,8 @@ public struct Driver { return "generating a baseline with '\(arg)' is only supported with '-emit-module' or '-emit-module-path'" case .optionRequiresAnother(let first, let second): return "'\(first)' cannot be specified if '\(second)' is not present" + case .unableToCreateReproducer: + return "failed to create reproducer" } } } @@ -955,7 +959,7 @@ public struct Driver { negative: .disableIncrementalFileHashing, default: false) self.recordedInputMetadata = .init(uniqueKeysWithValues: - Set(inputFiles).compactMap { inputFile -> (TypedVirtualPath, FileMetadata)? in + Set(inputFiles).compactMap { inputFile -> (TypedVirtualPath, FileMetadata)? in guard let modTime = try? fileSystem.lastModificationTime(for: inputFile.file) else { return nil } if incrementalFileHashes { guard let data = try? fileSystem.readFileContents(inputFile.file) else { return nil } @@ -1950,7 +1954,8 @@ extension Driver { buildRecordInfo: buildRecordInfo, showJobLifecycle: showJobLifecycle, argsResolver: executor.resolver, - diagnosticEngine: diagnosticEngine) + diagnosticEngine: diagnosticEngine, + reproducerCallback: self.generateReproducer) } private mutating func performTheBuild( @@ -4012,3 +4017,36 @@ extension Driver { return mapping } } + +// Generate reproducer. +extension Driver { + func generateReproducer(_ job: Job, _ status: ProcessResult.ExitStatus) throws { + // If process is not terminated with certain error code, no need to create reproducer. +#if os(Windows) + guard case .abnormal = status else { + return; + } +#else + guard case .signalled = status else { + return; + } +#endif + guard isFrontendArgSupported(.genReproducer) else { + return + } + // TODO: check flag is supported. + try withTemporaryDirectory(dir: fileSystem.tempDirectory, prefix: "swift-reproducer", removeTreeOnDeinit: false) { tempDir in + var reproJob = job + reproJob.commandLine.appendFlag(.genReproducer) + reproJob.commandLine.appendFlag(.genReproducerDir) + reproJob.commandLine.appendPath(tempDir) + reproJob.outputs.removeAll() + reproJob.outputCacheKeys.removeAll() + let result = try executor.execute(job: reproJob, forceResponseFiles: false, recordedInputModificationDates: [:]) + guard case .terminated(let code) = result.exitStatus, code == 0 else { + throw Error.unableToCreateReproducer + } + diagnosticEngine.emit(.note_reproducer_created(tempDir.pathString)) + } + } +} diff --git a/Sources/SwiftDriver/Driver/ToolExecutionDelegate.swift b/Sources/SwiftDriver/Driver/ToolExecutionDelegate.swift index 4b409aca6..13484a35c 100644 --- a/Sources/SwiftDriver/Driver/ToolExecutionDelegate.swift +++ b/Sources/SwiftDriver/Driver/ToolExecutionDelegate.swift @@ -30,6 +30,7 @@ import struct TSCBasic.Diagnostic import struct TSCBasic.ProcessResult import var TSCBasic.stderrStream import var TSCBasic.stdoutStream +import class TSCBasic.Process /// Delegate for printing execution information on the command-line. @_spi(Testing) public final class ToolExecutionDelegate: JobExecutionDelegate { @@ -49,6 +50,8 @@ import var TSCBasic.stdoutStream case silent } + public typealias ReproducerCallback = (Job, ProcessResult.ExitStatus) throws -> Void + public let mode: Mode public let buildRecordInfo: BuildRecordInfo? public let showJobLifecycle: Bool @@ -58,18 +61,21 @@ import var TSCBasic.stdoutStream private var nextBatchQuasiPID: Int private let argsResolver: ArgsResolver private var batchJobInputQuasiPIDMap = TwoLevelMap() + private let reproducerCallback: ReproducerCallback? @_spi(Testing) public init(mode: ToolExecutionDelegate.Mode, buildRecordInfo: BuildRecordInfo?, showJobLifecycle: Bool, argsResolver: ArgsResolver, - diagnosticEngine: DiagnosticsEngine) { + diagnosticEngine: DiagnosticsEngine, + reproducerCallback: ReproducerCallback? = nil) { self.mode = mode self.buildRecordInfo = buildRecordInfo self.showJobLifecycle = showJobLifecycle self.diagnosticEngine = diagnosticEngine self.argsResolver = argsResolver self.nextBatchQuasiPID = ToolExecutionDelegate.QUASI_PID_START + self.reproducerCallback = reproducerCallback } public func jobStarted(job: Job, arguments: [String], pid: Int) { @@ -107,6 +113,14 @@ import var TSCBasic.stdoutStream } #endif + if let reproducerCallback = reproducerCallback { + do { + try reproducerCallback(job, result.exitStatus) + } catch { + diagnosticEngine.emit(.error_failed_to_create_reproducer) + } + } + switch mode { case .silent: break diff --git a/Sources/SwiftDriver/Utilities/Diagnostics.swift b/Sources/SwiftDriver/Utilities/Diagnostics.swift index 8b41e2d05..6bf53eed0 100644 --- a/Sources/SwiftDriver/Utilities/Diagnostics.swift +++ b/Sources/SwiftDriver/Utilities/Diagnostics.swift @@ -182,4 +182,12 @@ extension Diagnostic.Message { static var error_no_objc_interop_embedded: Diagnostic.Message { .error("Objective-C interop cannot be enabled with embedded Swift.") } + + static var error_failed_to_create_reproducer: Diagnostic.Message { + .error("failed to create crash reproducer") + } + + static func note_reproducer_created(_ path: String) -> Diagnostic.Message { + .note("crash reproducer is created at: \(path)") + } } diff --git a/Sources/SwiftOptions/Options.swift b/Sources/SwiftOptions/Options.swift index bd4fea1ae..9d57244d9 100644 --- a/Sources/SwiftOptions/Options.swift +++ b/Sources/SwiftOptions/Options.swift @@ -585,6 +585,8 @@ extension Option { public static let F: Option = Option("-F", .joinedOrSeparate, attributes: [.frontend, .synthesizeInterface, .argumentIsPath], helpText: "Add directory to framework search path") public static let gccToolchain: Option = Option("-gcc-toolchain", .separate, attributes: [.helpHidden, .argumentIsPath], metaVar: "", helpText: "Specify a directory where the clang importer and clang linker can find headers and libraries") public static let gdwarfTypes: Option = Option("-gdwarf-types", .flag, attributes: [.frontend], helpText: "Emit full DWARF type info.", group: .g) + public static let genReproducerDir: Option = Option("-gen-reproducer-dir", .separate, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Path to directory where reproducers write to.") + public static let genReproducer: Option = Option("-gen-reproducer", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Generate a reproducer for current compilation.") public static let generateEmptyBaseline: Option = Option("-generate-empty-baseline", .flag, attributes: [.noDriver], helpText: "Generate an empty baseline") public static let generateEmptyBaseline_: Option = Option("--generate-empty-baseline", .flag, alias: Option.generateEmptyBaseline, attributes: [.noDriver], helpText: "Generate an empty baseline") public static let generateMigrationScript: Option = Option("-generate-migration-script", .flag, attributes: [.noDriver], helpText: "Compare SDK content in JSON file and generate migration script") @@ -1551,6 +1553,8 @@ extension Option { Option.F, Option.gccToolchain, Option.gdwarfTypes, + Option.genReproducerDir, + Option.genReproducer, Option.generateEmptyBaseline, Option.generateEmptyBaseline_, Option.generateMigrationScript,