diff --git a/Sources/SwiftDriver/Driver/Driver.swift b/Sources/SwiftDriver/Driver/Driver.swift
index 5705488c4..294d4c0cc 100644
--- a/Sources/SwiftDriver/Driver/Driver.swift
+++ b/Sources/SwiftDriver/Driver/Driver.swift
@@ -389,6 +389,12 @@ public struct Driver {
/// Path to the Objective-C generated header.
let objcGeneratedHeaderPath: VirtualPath.Handle?
+ /// Path to the SIL output file.
+ let silOutputPath: VirtualPath.Handle?
+
+ /// Path to the LLVM IR output file.
+ let llvmIROutputPath: VirtualPath.Handle?
+
/// Path to the loaded module trace file.
let loadedModuleTracePath: VirtualPath.Handle?
@@ -1296,6 +1302,11 @@ public struct Driver {
emitModuleSeparately: emitModuleSeparately,
outputFileMap: self.outputFileMap,
moduleName: moduleOutputInfo.name)
+ // SIL and IR outputs are handled through directory options and file maps
+ // rather than single output paths, so we set these to nil to enable
+ // the supplementary output path logic in FrontendJobHelpers
+ self.silOutputPath = nil
+ self.llvmIROutputPath = nil
if let loadedModuleTraceEnvVar = env["SWIFT_LOADED_MODULE_TRACE_FILE"] {
self.loadedModuleTracePath = try VirtualPath.intern(path: loadedModuleTraceEnvVar)
@@ -3871,6 +3882,19 @@ extension Driver {
return try VirtualPath.intern(path: moduleName.appendingFileTypeExtension(type))
}
+ /// Check if output file map has entries for SIL/IR to enable file map support
+ static func hasFileMapEntry(outputFileMap: OutputFileMap?, fileType: FileType) -> Bool {
+ guard let outputFileMap = outputFileMap else { return false }
+
+ // Check if any input has this file type in the output file map
+ for inputFile in outputFileMap.entries.keys {
+ if outputFileMap.entries[inputFile]?[fileType] != nil {
+ return true
+ }
+ }
+ return false
+ }
+
/// Determine if the build system has created a Project/ directory for auxiliary outputs.
static func computeProjectDirectoryPath(moduleOutputPath: VirtualPath.Handle?,
fileSystem: FileSystem) -> VirtualPath.Handle? {
diff --git a/Sources/SwiftDriver/Driver/OutputFileMap.swift b/Sources/SwiftDriver/Driver/OutputFileMap.swift
index 08e580df9..a6863d424 100644
--- a/Sources/SwiftDriver/Driver/OutputFileMap.swift
+++ b/Sources/SwiftDriver/Driver/OutputFileMap.swift
@@ -172,6 +172,11 @@ public struct OutputFileMap: Hashable, Codable {
}
return result
}
+
+ /// Check if the output file map has any entries for the given file type
+ public func hasEntries(for fileType: FileType) -> Bool {
+ return entries.values.contains { $0[fileType] != nil }
+ }
}
/// Struct for loading the JSON file from disk.
diff --git a/Sources/SwiftDriver/Jobs/CompileJob.swift b/Sources/SwiftDriver/Jobs/CompileJob.swift
index 3900e22d7..a2952864b 100644
--- a/Sources/SwiftDriver/Jobs/CompileJob.swift
+++ b/Sources/SwiftDriver/Jobs/CompileJob.swift
@@ -279,7 +279,8 @@ extension Driver {
moduleOutputInfo: self.moduleOutputInfo,
moduleOutputPaths: self.moduleOutputPaths,
includeModuleTracePath: emitModuleTrace,
- indexFilePaths: indexFilePaths)
+ indexFilePaths: indexFilePaths,
+ allInputs: inputs)
// Forward migrator flags.
try commandLine.appendLast(.apiDiffDataFile, from: &parsedOptions)
diff --git a/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift b/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift
index 2ac5cace7..def030093 100644
--- a/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift
+++ b/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift
@@ -667,9 +667,37 @@ extension Driver {
moduleOutputInfo: ModuleOutputInfo,
moduleOutputPaths: SupplementalModuleTargetOutputPaths,
includeModuleTracePath: Bool,
- indexFilePaths: [TypedVirtualPath]) throws -> [TypedVirtualPath] {
+ indexFilePaths: [TypedVirtualPath],
+ allInputs: [TypedVirtualPath] = []) throws -> [TypedVirtualPath] {
var flaggedInputOutputPairs: [(flag: String, input: TypedVirtualPath?, output: TypedVirtualPath)] = []
+ /// Generate directory-based output path for supplementary outputs
+ func generateSupplementaryOutputPath(for input: TypedVirtualPath, outputType: FileType, directory: String) throws -> TypedVirtualPath {
+ let inputBasename = input.file.basenameWithoutExt
+ let fileExtension = outputType == .sil ? "sil" : "ll"
+ let filename = "\(inputBasename).\(fileExtension)"
+ let individualPath = try VirtualPath(path: directory).appending(component: filename)
+ let outputPath = individualPath.intern()
+ return TypedVirtualPath(file: outputPath, type: outputType)
+ }
+
+ /// Process inputs for supplementary output generation (SIL/IR)
+ func processInputsForSupplementaryOutput(inputs: [TypedVirtualPath], outputType: FileType, flag: String, directory: String?) throws {
+ for inputFile in inputs {
+ // Check output file map first, then fall back to directory-based generation
+ if let outputFileMapPath = try outputFileMap?.existingOutput(inputFile: inputFile.fileHandle, outputType: outputType) {
+ flaggedInputOutputPairs.append((flag: flag, input: inputFile, output: TypedVirtualPath(file: outputFileMapPath, type: outputType)))
+ } else if let directory = directory {
+ let outputPath = try generateSupplementaryOutputPath(for: inputFile, outputType: outputType, directory: directory)
+ flaggedInputOutputPairs.append((flag: flag, input: inputFile, output: outputPath))
+ } else if parsedOptions.hasArgument(.saveTemps) {
+ // When using -save-temps without explicit directories, output to current directory
+ let outputPath = try generateSupplementaryOutputPath(for: inputFile, outputType: outputType, directory: ".")
+ flaggedInputOutputPairs.append((flag: flag, input: inputFile, output: outputPath))
+ }
+ }
+ }
+
/// Add output of a particular type, if needed.
func addOutputOfType(
outputType: FileType,
@@ -677,6 +705,25 @@ extension Driver {
input: TypedVirtualPath?,
flag: String
) throws {
+ // Handle directory-based options and file maps for SIL and LLVM IR when finalOutputPath is nil
+ if finalOutputPath == nil && (outputType == .sil || outputType == .llvmIR) {
+ let directoryOption: Option = outputType == .sil ? .silOutputDir : .irOutputDir
+ let directory = parsedOptions.getLastArgument(directoryOption)?.asSingle
+ let hasFileMapEntries = outputFileMap?.hasEntries(for: outputType) ?? false
+
+ if directory != nil || hasFileMapEntries || (parsedOptions.hasArgument(.saveTemps) && !hasFileMapEntries) {
+ let inputsToProcess: [TypedVirtualPath]
+ if compilerMode.usesPrimaryFileInputs {
+ inputsToProcess = input.map { [$0] } ?? []
+ } else {
+ inputsToProcess = allInputs
+ }
+
+ try processInputsForSupplementaryOutput(inputs: inputsToProcess, outputType: outputType, flag: flag, directory: directory)
+ return
+ }
+ }
+
// If there is no final output, there's nothing to do.
guard let finalOutputPath = finalOutputPath else { return }
@@ -763,6 +810,30 @@ extension Driver {
finalOutputPath: serializedDiagnosticsFilePath,
input: input,
flag: "-serialize-diagnostics-path")
+
+ // Add SIL and IR outputs when explicitly requested via directory options, file maps, or -save-temps
+ let saveTempsWithoutFileMap = parsedOptions.hasArgument(.saveTemps) && outputFileMap == nil
+ let hasSilFileMapEntries = outputFileMap?.hasEntries(for: .sil) ?? false
+ let hasIrFileMapEntries = outputFileMap?.hasEntries(for: .llvmIR) ?? false
+
+ let shouldAddSilOutput = parsedOptions.hasArgument(.silOutputDir) || saveTempsWithoutFileMap || hasSilFileMapEntries
+ let shouldAddIrOutput = parsedOptions.hasArgument(.irOutputDir) || saveTempsWithoutFileMap || hasIrFileMapEntries
+
+ if shouldAddSilOutput {
+ try addOutputOfType(
+ outputType: .sil,
+ finalOutputPath: silOutputPath,
+ input: input,
+ flag: "-sil-output-path")
+ }
+
+ if shouldAddIrOutput {
+ try addOutputOfType(
+ outputType: .llvmIR,
+ finalOutputPath: llvmIROutputPath,
+ input: input,
+ flag: "-ir-output-path")
+ }
}
if compilerMode.usesPrimaryFileInputs {
diff --git a/Sources/SwiftOptions/Options.swift b/Sources/SwiftOptions/Options.swift
index 68a3442fc..3c8ed1c7f 100644
--- a/Sources/SwiftOptions/Options.swift
+++ b/Sources/SwiftOptions/Options.swift
@@ -655,6 +655,7 @@ extension Option {
public static let Isystem: Option = Option("-Isystem", .separate, attributes: [.frontend, .synthesizeInterface, .argumentIsPath], helpText: "Add directory to the system import search path")
public static let I: Option = Option("-I", .joinedOrSeparate, attributes: [.frontend, .synthesizeInterface, .argumentIsPath], helpText: "Add directory to the import search path")
public static let i: Option = Option("-i", .flag, group: .modes)
+ public static let irOutputDir: Option = Option("-ir-output-dir", .separate, attributes: [.frontend, .argumentIsPath, .supplementaryOutput, .cacheInvariant], metaVar: "
", helpText: "Output LLVM IR files to directory as additional output during compilation")
public static let json: Option = Option("-json", .flag, attributes: [.noDriver], helpText: "Print output in JSON format.")
public static let json_: Option = Option("--json", .flag, alias: Option.json, attributes: [.noDriver], helpText: "Print output in JSON format.")
public static let j: Option = Option("-j", .joinedOrSeparate, attributes: [.doesNotAffectIncrementalBuild], metaVar: "", helpText: "Number of commands to execute in parallel")
@@ -864,6 +865,7 @@ extension Option {
public static let silDebugSerialization: Option = Option("-sil-debug-serialization", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Do not eliminate functions in Mandatory Inlining/SILCombine dead functions. (for debugging only)")
public static let silInlineCallerBenefitReductionFactor: Option = Option("-sil-inline-caller-benefit-reduction-factor", .separate, attributes: [.helpHidden, .frontend, .noDriver], metaVar: "<2>", helpText: "Controls the aggressiveness of performance inlining in -Osize mode by reducing the base benefits of a caller (lower value permits more inlining!)")
public static let silInlineThreshold: Option = Option("-sil-inline-threshold", .separate, attributes: [.helpHidden, .frontend, .noDriver], metaVar: "<50>", helpText: "Controls the aggressiveness of performance inlining")
+ public static let silOutputDir: Option = Option("-sil-output-dir", .separate, attributes: [.frontend, .argumentIsPath, .supplementaryOutput, .cacheInvariant], metaVar: "", helpText: "Output SIL files to directory as additional output during compilation")
public static let silOwnershipVerifyAll: Option = Option("-sil-ownership-verify-all", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Verify ownership after each transform")
public static let silStopOptznsBeforeLoweringOwnership: Option = Option("-sil-stop-optzns-before-lowering-ownership", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Stop optimizing at SIL time before we lower ownership from SIL. Intended only for SIL ossa tests")
public static let silUnrollThreshold: Option = Option("-sil-unroll-threshold", .separate, attributes: [.helpHidden, .frontend, .noDriver], metaVar: "<250>", helpText: "Controls the aggressiveness of loop unrolling")
@@ -1610,6 +1612,7 @@ extension Option {
Option.importPch,
Option.importPrescan,
Option.importUnderlyingModule,
+ Option.irOutputDir,
Option.inPlace,
Option.inProcessPluginServerPath,
Option.includeSpiSymbols,
@@ -1849,6 +1852,7 @@ extension Option {
Option.silDebugSerialization,
Option.silInlineCallerBenefitReductionFactor,
Option.silInlineThreshold,
+ Option.silOutputDir,
Option.silOwnershipVerifyAll,
Option.silStopOptznsBeforeLoweringOwnership,
Option.silUnrollThreshold,
diff --git a/Tests/SwiftDriverTests/JobExecutorTests.swift b/Tests/SwiftDriverTests/JobExecutorTests.swift
index 22daf594b..436af9a46 100644
--- a/Tests/SwiftDriverTests/JobExecutorTests.swift
+++ b/Tests/SwiftDriverTests/JobExecutorTests.swift
@@ -536,8 +536,12 @@ final class JobExecutorTests: XCTestCase {
executor: executor)
let jobs = try driver.planBuild()
XCTAssertEqual(jobs.removingAutolinkExtractJobs().map(\.kind), [.compile, .link])
- XCTAssertEqual(jobs[0].outputs.count, 1)
- let compileOutput = jobs[0].outputs[0].file
+ // With -save-temps, we now have additional SIL and IR outputs, so expect more outputs
+ XCTAssertTrue(jobs[0].outputs.count >= 1, "Should have at least the object file output")
+ // Find the main object file output
+ let objectOutput = jobs[0].outputs.first { $0.type == .object }
+ XCTAssertNotNil(objectOutput, "Should have object file output")
+ let compileOutput = objectOutput!.file
guard matchTemporary(compileOutput, "main.o") else {
XCTFail("unexpected output")
return
@@ -585,5 +589,38 @@ final class JobExecutorTests: XCTestCase {
)
}
}
+
+ // Test that -save-temps also saves SIL and IR intermediate files
+ do {
+ try withTemporaryDirectory { path in
+ let main = path.appending(component: "main.swift")
+ try localFileSystem.writeFileContents(main, bytes: "print(\"hello, world!\")")
+ let diags = DiagnosticsEngine()
+ let executor = try SwiftDriverExecutor(diagnosticsEngine: diags,
+ processSet: ProcessSet(),
+ fileSystem: localFileSystem,
+ env: ProcessEnv.block)
+ let outputPath = path.appending(component: "finalOutput")
+ var driver = try Driver(args: ["swiftc", main.pathString,
+ "-save-temps",
+ "-o", outputPath.pathString] + getHostToolchainSdkArg(executor),
+ envBlock: ProcessEnv.block,
+ diagnosticsOutput: .engine(diags),
+ fileSystem: localFileSystem,
+ executor: executor)
+ let jobs = try driver.planBuild()
+ let compileJobs = jobs.removingAutolinkExtractJobs()
+ XCTAssertEqual(compileJobs.map(\.kind), [.compile, .link])
+
+ let compileJob = compileJobs[0]
+
+ XCTAssertTrue(compileJob.commandLine.contains(.flag("-sil-output-path")))
+ XCTAssertTrue(compileJob.commandLine.contains(.flag("-ir-output-path")))
+
+ // Verify the compile job has additional outputs for SIL and IR
+ let hasMultipleOutputs = compileJob.outputs.count > 1
+ XCTAssertTrue(hasMultipleOutputs, "Should have additional SIL/IR outputs when using -save-temps")
+ }
+ }
}
}
diff --git a/Tests/SwiftDriverTests/SwiftDriverTests.swift b/Tests/SwiftDriverTests/SwiftDriverTests.swift
index 835649acc..3c5a6786f 100644
--- a/Tests/SwiftDriverTests/SwiftDriverTests.swift
+++ b/Tests/SwiftDriverTests/SwiftDriverTests.swift
@@ -556,6 +556,220 @@ final class SwiftDriverTests: XCTestCase {
}
}
+ func testSupplementarySilAndIrOutputDirectories() throws {
+ // Test SIL output directory with multiple files (single-file compilation mode)
+ do {
+ var driver = try Driver(args: ["swiftc", "foo.swift", "bar.swift", "-emit-object", "-sil-output-dir", "/tmp/sil-output"])
+ let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs()
+ XCTAssertEqual(plannedJobs.count, 2)
+
+ // First job should generate foo.sil in the output directory
+ XCTAssertEqual(plannedJobs[0].kind, .compile)
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/sil-output/foo.sil"))))
+
+ // Second job should generate bar.sil in the output directory
+ XCTAssertEqual(plannedJobs[1].kind, .compile)
+ try XCTAssertJobInvocationMatches(plannedJobs[1], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/sil-output/bar.sil"))))
+ }
+
+ // Test IR output directory with multiple files (single-file compilation mode)
+ do {
+ var driver = try Driver(args: ["swiftc", "foo.swift", "bar.swift", "-emit-object", "-ir-output-dir", "/tmp/ir-output"])
+ let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs()
+ XCTAssertEqual(plannedJobs.count, 2)
+
+ // First job should generate foo.ll in the output directory
+ XCTAssertEqual(plannedJobs[0].kind, .compile)
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/ir-output/foo.ll"))))
+
+ // Second job should generate bar.ll in the output directory
+ XCTAssertEqual(plannedJobs[1].kind, .compile)
+ try XCTAssertJobInvocationMatches(plannedJobs[1], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/ir-output/bar.ll"))))
+ }
+
+ // Test both SIL and IR output directories together (single-file compilation mode)
+ do {
+ var driver = try Driver(args: ["swiftc", "foo.swift", "bar.swift", "-emit-object", "-sil-output-dir", "/tmp/sil-output", "-ir-output-dir", "/tmp/ir-output"])
+ let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs()
+ XCTAssertEqual(plannedJobs.count, 2)
+
+ // First job should generate both foo.sil and foo.ll
+ XCTAssertEqual(plannedJobs[0].kind, .compile)
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/sil-output/foo.sil"))))
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/ir-output/foo.ll"))))
+
+ // Second job should generate both bar.sil and bar.ll
+ XCTAssertEqual(plannedJobs[1].kind, .compile)
+ try XCTAssertJobInvocationMatches(plannedJobs[1], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/sil-output/bar.sil"))))
+ try XCTAssertJobInvocationMatches(plannedJobs[1], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/ir-output/bar.ll"))))
+ }
+
+ // Test directory options with single-file compilation
+ do {
+ var driver = try Driver(args: ["swiftc", "foo.swift", "-emit-object", "-sil-output-dir", "/tmp/sil-output", "-o", "foo.o"])
+ let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs()
+ XCTAssertEqual(plannedJobs.count, 1)
+ XCTAssertEqual(plannedJobs[0].kind, .compile)
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/sil-output/foo.sil"))))
+ }
+
+ // Test directory options with WMO (whole module optimization)
+ do {
+ var driver = try Driver(args: ["swiftc", "foo.swift", "bar.swift", "-emit-object", "-wmo", "-sil-output-dir", "/tmp/sil-output", "-ir-output-dir", "/tmp/ir-output"])
+ let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs()
+ XCTAssertEqual(plannedJobs.count, 1)
+ XCTAssertEqual(plannedJobs[0].kind, .compile)
+
+ // In WMO mode, should generate output for all input files
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/sil-output/foo.sil"))))
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/sil-output/bar.sil"))))
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/ir-output/foo.ll"))))
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/ir-output/bar.ll"))))
+ }
+ }
+
+ func testSupplementarySilAndIrOutputFileMaps() throws {
+ // Test SIL and IR output file maps in WMO mode
+ try withTemporaryFile { fileMapFile in
+ let outputMapContents: ByteString = """
+ {
+ "foo.swift": {
+ "sil": "/tmp/build/foo_custom.sil",
+ "llvm-ir": "/tmp/build/foo_custom.ll"
+ },
+ "bar.swift": {
+ "sil": "/tmp/build/bar_custom.sil",
+ "llvm-ir": "/tmp/build/bar_custom.ll"
+ }
+ }
+ """
+ try localFileSystem.writeFileContents(fileMapFile.path, bytes: outputMapContents)
+
+ var driver = try Driver(args: ["swiftc", "foo.swift", "bar.swift", "-emit-object", "-wmo",
+ "-output-file-map", fileMapFile.path.pathString])
+ let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs()
+ XCTAssertEqual(plannedJobs.count, 1)
+ XCTAssertEqual(plannedJobs[0].kind, .compile)
+
+ // In WMO mode with file maps, should generate SIL/IR files at specified paths
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/build/foo_custom.sil"))))
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/build/bar_custom.sil"))))
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/build/foo_custom.ll"))))
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/build/bar_custom.ll"))))
+ }
+
+ // Test single-file compilation with file maps
+ try withTemporaryFile { fileMapFile in
+ let outputMapContents: ByteString = """
+ {
+ "foo.swift": {
+ "sil": "/tmp/build/foo_single.sil",
+ "llvm-ir": "/tmp/build/foo_single.ll"
+ },
+ "bar.swift": {
+ "sil": "/tmp/build/bar_single.sil",
+ "llvm-ir": "/tmp/build/bar_single.ll"
+ }
+ }
+ """
+ try localFileSystem.writeFileContents(fileMapFile.path, bytes: outputMapContents)
+
+ var driver = try Driver(args: ["swiftc", "foo.swift", "bar.swift", "-emit-object",
+ "-output-file-map", fileMapFile.path.pathString])
+ let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs()
+ XCTAssertEqual(plannedJobs.count, 2)
+
+ // First job for foo.swift
+ XCTAssertEqual(plannedJobs[0].kind, .compile)
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/build/foo_single.sil"))))
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/build/foo_single.ll"))))
+
+ // Second job for bar.swift
+ XCTAssertEqual(plannedJobs[1].kind, .compile)
+ try XCTAssertJobInvocationMatches(plannedJobs[1], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/build/bar_single.sil"))))
+ try XCTAssertJobInvocationMatches(plannedJobs[1], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/build/bar_single.ll"))))
+ }
+
+ // Test partial file map (only some files have SIL/IR entries)
+ try withTemporaryFile { fileMapFile in
+ let outputMapContents: ByteString = """
+ {
+ "foo.swift": {
+ "sil": "/tmp/build/foo_partial.sil"
+ },
+ "bar.swift": {
+ "llvm-ir": "/tmp/build/bar_partial.ll"
+ }
+ }
+ """
+ try localFileSystem.writeFileContents(fileMapFile.path, bytes: outputMapContents)
+
+ var driver = try Driver(args: ["swiftc", "foo.swift", "bar.swift", "-emit-object", "-wmo",
+ "-output-file-map", fileMapFile.path.pathString])
+ let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs()
+ XCTAssertEqual(plannedJobs.count, 1)
+ XCTAssertEqual(plannedJobs[0].kind, .compile)
+
+ // Should only generate files for entries that exist in the file map
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/build/foo_partial.sil"))))
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/build/bar_partial.ll"))))
+ }
+
+ // Test that file maps work alongside directory options (file maps should take precedence)
+ try withTemporaryFile { fileMapFile in
+ let outputMapContents: ByteString = """
+ {
+ "foo.swift": {
+ "sil": "/tmp/build/foo_precedence.sil",
+ "llvm-ir": "/tmp/build/foo_precedence.ll"
+ }
+ }
+ """
+ try localFileSystem.writeFileContents(fileMapFile.path, bytes: outputMapContents)
+
+ var driver = try Driver(args: ["swiftc", "foo.swift", "bar.swift", "-emit-object", "-wmo",
+ "-sil-output-dir", "/tmp/dir-output",
+ "-ir-output-dir", "/tmp/dir-output",
+ "-output-file-map", fileMapFile.path.pathString])
+ let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs()
+ XCTAssertEqual(plannedJobs.count, 1)
+ XCTAssertEqual(plannedJobs[0].kind, .compile)
+
+ // foo.swift should use file map paths (precedence over directory options)
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/build/foo_precedence.sil"))))
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/build/foo_precedence.ll"))))
+
+ // bar.swift should fall back to directory options
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/dir-output/bar.sil"))))
+ try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/dir-output/bar.ll"))))
+ }
+
+ // Ensure file maps without SIL/IR entries don't generate spurious flags
+ try withTemporaryFile { fileMapFile in
+ let outputMapContents: ByteString = """
+ {
+ "foo.swift": {
+ "object": "/tmp/build/foo.o"
+ },
+ "bar.swift": {
+ "object": "/tmp/build/bar.o"
+ }
+ }
+ """
+ try localFileSystem.writeFileContents(fileMapFile.path, bytes: outputMapContents)
+
+ var driver = try Driver(args: ["swiftc", "foo.swift", "bar.swift", "-emit-object", "-wmo",
+ "-output-file-map", fileMapFile.path.pathString])
+ let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs()
+ XCTAssertEqual(plannedJobs.count, 1)
+ XCTAssertEqual(plannedJobs[0].kind, .compile)
+
+ // Should not generate any SIL/IR flags when file map has no SIL/IR entries
+ XCTAssertFalse(plannedJobs[0].commandLine.contains(Job.ArgTemplate.flag("-sil-output-path")))
+ XCTAssertFalse(plannedJobs[0].commandLine.contains(Job.ArgTemplate.flag("-ir-output-path")))
+ }
+ }
+
func testMultithreading() throws {
XCTAssertNil(try Driver(args: ["swiftc"]).numParallelJobs)