Skip to content

Commit cfd8902

Browse files
committed
Implement 'getSingleFrontendInvocationFromDriverArguments' utility API
This function currently exists in the legacy C++-based Swift Driver. This is a re-implementation of the same functionality which will eventually replace it. Part of rdar://75851402
1 parent 5cf131f commit cfd8902

File tree

7 files changed

+289
-3
lines changed

7 files changed

+289
-3
lines changed

Sources/SwiftDriver/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ add_library(SwiftDriver
9797
Toolchains/WebAssemblyToolchain.swift
9898
Toolchains/WindowsToolchain.swift
9999

100+
ToolingInterface/SimpleExecutor.swift
101+
ToolingInterface/ToolingUtil.swift
102+
100103
Utilities/DOTJobGraphSerializer.swift
101104
Utilities/DOTModuleDependencyGraphSerializer.swift
102105
Utilities/DateAdditions.swift

Sources/SwiftDriver/Driver/Driver.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1247,7 +1247,7 @@ extension Driver {
12471247
}
12481248

12491249
/// Expand response files in the input arguments and return a new argument list.
1250-
@_spi(Testing) public static func expandResponseFiles(
1250+
public static func expandResponseFiles(
12511251
_ args: [String],
12521252
fileSystem: FileSystem,
12531253
diagnosticsEngine: DiagnosticsEngine
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//===------- SimpleExecutor.swift - Swift Driver Source Version-----------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import protocol TSCBasic.FileSystem
14+
import struct TSCBasic.ProcessResult
15+
import class TSCBasic.Process
16+
17+
/// A simple executor sufficient for managing processes required during
18+
/// build planning: e.g. querying frontend target info.
19+
///
20+
/// TODO: It would be nice if build planning did not involve an executor.
21+
/// We must hunt down all uses of an executor during planning and move
22+
/// relevant compiler functionality into libSwiftScan.
23+
internal class SimpleExecutor: DriverExecutor {
24+
let resolver: ArgsResolver
25+
let fileSystem: FileSystem
26+
let env: [String: String]
27+
28+
init(resolver: ArgsResolver, fileSystem: FileSystem, env: [String: String]) {
29+
self.resolver = resolver
30+
self.fileSystem = fileSystem
31+
self.env = env
32+
}
33+
34+
func execute(job: Job,
35+
forceResponseFiles: Bool,
36+
recordedInputModificationDates: [TypedVirtualPath : TimePoint]) throws -> ProcessResult {
37+
let arguments: [String] = try resolver.resolveArgumentList(for: job,
38+
useResponseFiles: .heuristic)
39+
var childEnv = env
40+
childEnv.merge(job.extraEnvironment, uniquingKeysWith: { (_, new) in new })
41+
let process = try Process.launchProcess(arguments: arguments, env: childEnv)
42+
return try process.waitUntilExit()
43+
}
44+
45+
func execute(workload: DriverExecutorWorkload, delegate: JobExecutionDelegate,
46+
numParallelJobs: Int, forceResponseFiles: Bool,
47+
recordedInputModificationDates: [TypedVirtualPath : TimePoint]) throws {
48+
fatalError("Unsupported operation on current executor")
49+
}
50+
51+
func checkNonZeroExit(args: String..., environment: [String : String]) throws -> String {
52+
try Process.checkNonZeroExit(arguments: args, environment: environment)
53+
}
54+
55+
func description(of job: Job, forceResponseFiles: Bool) throws -> String {
56+
let useResponseFiles : ResponseFileHandling = forceResponseFiles ? .forced : .heuristic
57+
let (args, usedResponseFile) = try resolver.resolveArgumentList(for: job, useResponseFiles: useResponseFiles)
58+
var result = args.map { $0.spm_shellEscaped() }.joined(separator: " ")
59+
if usedResponseFile {
60+
result += " # \(job.commandLine.joinedUnresolvedArguments)"
61+
}
62+
return result
63+
}
64+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//===------- ToolingUtil.swift - Swift Driver Source Version--------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import class TSCBasic.DiagnosticsEngine
14+
import struct TSCBasic.Diagnostic
15+
import class TSCBasic.ProcessSet
16+
import enum TSCBasic.ProcessEnv
17+
import var TSCBasic.localFileSystem
18+
import SwiftOptions
19+
20+
/// Generates the list of arguments that would be passed to the compiler
21+
/// frontend from the given driver arguments.
22+
///
23+
/// \param ArgList The driver arguments (i.e. normal arguments for \c swiftc).
24+
/// \param ForceNoOutputs If true, override the output mode to "-typecheck" and
25+
/// produce no outputs. For example, this disables "-emit-module" and "-c" and
26+
/// prevents the creation of temporary files.
27+
/// \param outputFrontendArgs Contains the resulting frontend invocation command
28+
/// \param emittedDiagnostics Contains the diagnostics emitted by the driver
29+
///
30+
/// \returns true on error
31+
///
32+
/// \note This function is not intended to create invocations which are
33+
/// suitable for use in REPL or immediate modes.
34+
public func getSingleFrontendInvocationFromDriverArguments(argList: [String],
35+
outputFrontendArgs: inout [String],
36+
emittedDiagnostics: inout [Diagnostic],
37+
forceNoOutputs: Bool = false) -> Bool {
38+
var args: [String] = []
39+
args.append(contentsOf: argList)
40+
41+
// When creating a CompilerInvocation, ensure that the driver creates a single
42+
// frontend command.
43+
args.append("-whole-module-optimization")
44+
45+
// Explicitly disable batch mode to avoid a spurious warning when combining
46+
// -enable-batch-mode with -whole-module-optimization. This is an
47+
// implementation detail.
48+
args.append("-disable-batch-mode");
49+
50+
// Prevent having a separate job for emit-module, we would like
51+
// to just have one job
52+
args.append("-no-emit-module-separately-wmo")
53+
54+
// Avoid using filelists
55+
args.append("-driver-filelist-threshold");
56+
args.append(String(Int.max));
57+
58+
let diagnosticsEngine = DiagnosticsEngine()
59+
defer { emittedDiagnostics = diagnosticsEngine.diagnostics }
60+
61+
do {
62+
args = try ["swiftc"] + Driver.expandResponseFiles(args,
63+
fileSystem: localFileSystem,
64+
diagnosticsEngine: diagnosticsEngine)
65+
66+
let optionTable = OptionTable()
67+
var parsedOptions = try optionTable.parse(Array(args), for: .batch, delayThrows: true)
68+
if forceNoOutputs {
69+
// Clear existing output modes and supplementary outputs.
70+
parsedOptions.eraseAllArguments(in: .modes)
71+
parsedOptions.eraseSupplementaryOutputs()
72+
parsedOptions.addOption(.typecheck, argument: .none)
73+
}
74+
75+
// Instantiate the driver, setting up the toolchain in the process, etc.
76+
let resolver = try ArgsResolver(fileSystem: localFileSystem)
77+
let executor = SimpleExecutor(resolver: resolver,
78+
fileSystem: localFileSystem,
79+
env: ProcessEnv.vars)
80+
var driver = try Driver(args: parsedOptions.commandLine,
81+
diagnosticsEngine: diagnosticsEngine,
82+
executor: executor)
83+
if diagnosticsEngine.hasErrors {
84+
return true
85+
}
86+
87+
88+
let buildPlan = try driver.planBuild()
89+
if diagnosticsEngine.hasErrors {
90+
return true
91+
}
92+
let compileJobs = buildPlan.filter({ $0.kind == .compile })
93+
guard let compileJob = compileJobs.spm_only else {
94+
diagnosticsEngine.emit(.error_expected_one_frontend_job())
95+
return true
96+
}
97+
if !compileJob.commandLine.starts(with: [.flag("-frontend")]) {
98+
diagnosticsEngine.emit(.error_expected_frontend_command())
99+
return true
100+
}
101+
outputFrontendArgs = try executor.description(of: compileJob,
102+
forceResponseFiles: false).components(separatedBy: " ")
103+
} catch {
104+
print("Unexpected error: \(error).")
105+
return true
106+
}
107+
108+
return false
109+
}

Sources/SwiftDriver/Utilities/Diagnostics.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,4 +146,12 @@ extension Diagnostic.Message {
146146
static func warning_option_overrides_another(overridingOption: Option, overridenOption: Option) -> Diagnostic.Message {
147147
.warning("ignoring '\(overridenOption.spelling)' because '\(overridingOption.spelling)' was also specified")
148148
}
149+
150+
static func error_expected_one_frontend_job() -> Diagnostic.Message {
151+
.error("unable to handle compilation, expected exactly one frontend job")
152+
}
153+
154+
static func error_expected_frontend_command() -> Diagnostic.Message {
155+
.error("expected a swift frontend command")
156+
}
149157
}

Sources/SwiftOptions/ParsedOptions.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,8 @@ extension ParsedOptions {
134134
}
135135
}
136136

137-
extension ParsedOptions {
138-
mutating func buildIndex() {
137+
public extension ParsedOptions {
138+
internal mutating func buildIndex() {
139139
optionIndex.removeAll()
140140
for parsed in parsedOptions {
141141
optionIndex[parsed.option.canonical.spelling, default: []].append(parsed)
@@ -344,6 +344,24 @@ extension ParsedOptions {
344344
groupIndex[group]?.removeAll { $0.option == option }
345345
}
346346
}
347+
348+
/// Remove all arguments of a given group from parsed options.
349+
public mutating func eraseAllArguments(in group: Option.Group) {
350+
for parsedOption in parsedOptions {
351+
if parsedOption.option.group == group {
352+
eraseArgument(parsedOption.option)
353+
}
354+
}
355+
}
356+
357+
/// Remove all arguments with a .supplementaryOutput attribute
358+
public mutating func eraseSupplementaryOutputs() {
359+
for parsedOption in parsedOptions {
360+
if parsedOption.option.attributes.contains(.supplementaryOutput) {
361+
eraseArgument(parsedOption.option)
362+
}
363+
}
364+
}
347365

348366
public var unconsumedOptions: [ParsedOption] {
349367
zip(parsedOptions, consumed).filter { !$0.1 }.map(\.0)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//===---- SwiftDriverToolingInterfaceTests.swift - Swift Driver Tests ----===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
import SwiftDriver
13+
import SwiftOptions
14+
import TSCBasic
15+
import XCTest
16+
17+
final class SwiftDriverToolingInterfaceTests: XCTestCase {
18+
func testCreateCompilerInvocation() throws {
19+
try withTemporaryDirectory { path in
20+
let inputFile = path.appending(components: "test.swift")
21+
try localFileSystem.writeFileContents(inputFile) { $0 <<< "public func foo()" }
22+
23+
// Expected success scenarios:
24+
do {
25+
let testCommand = inputFile.description
26+
var resultingFrontendArgs: [String] = []
27+
var emittedDiagnostics: [Diagnostic] = []
28+
XCTAssertFalse(getSingleFrontendInvocationFromDriverArguments(argList: testCommand.components(separatedBy: " "),
29+
outputFrontendArgs: &resultingFrontendArgs,
30+
emittedDiagnostics: &emittedDiagnostics))
31+
}
32+
do {
33+
let testCommand = "-emit-executable " + inputFile.description + " main.swift lib.swift -module-name createCompilerInvocation -emit-module -emit-objc-header -o t.out"
34+
var resultingFrontendArgs: [String] = []
35+
var emittedDiagnostics: [Diagnostic] = []
36+
XCTAssertFalse(getSingleFrontendInvocationFromDriverArguments(argList: testCommand.components(separatedBy: " "),
37+
outputFrontendArgs: &resultingFrontendArgs,
38+
emittedDiagnostics: &emittedDiagnostics))
39+
}
40+
do {
41+
let testCommand = "-c " + inputFile.description + " main.swift lib.swift -module-name createCompilerInvocation -emit-module -emit-objc-header"
42+
var resultingFrontendArgs: [String] = []
43+
var emittedDiagnostics: [Diagnostic] = []
44+
XCTAssertFalse(getSingleFrontendInvocationFromDriverArguments(argList: testCommand.components(separatedBy: " "),
45+
outputFrontendArgs: &resultingFrontendArgs,
46+
emittedDiagnostics: &emittedDiagnostics))
47+
}
48+
do {
49+
let testCommand = inputFile.description + " -enable-batch-mode"
50+
var resultingFrontendArgs: [String] = []
51+
var emittedDiagnostics: [Diagnostic] = []
52+
XCTAssertFalse(getSingleFrontendInvocationFromDriverArguments(argList: testCommand.components(separatedBy: " "),
53+
outputFrontendArgs: &resultingFrontendArgs,
54+
emittedDiagnostics: &emittedDiagnostics))
55+
}
56+
do { // Force no outputs
57+
let testCommand = "-module-name foo -emit-module -emit-module-path /tmp/foo.swiftmodule -emit-objc-header -emit-objc-header-path /tmp/foo.h -enable-library-evolution -emit-module-interface -emit-module-interface-path /tmp/foo.swiftinterface -emit-library -emit-tbd -emit-tbd-path /tmp/foo.tbd -emit-dependencies -serialize-diagnostics " + inputFile.description
58+
var resultingFrontendArgs: [String] = []
59+
var emittedDiagnostics: [Diagnostic] = []
60+
XCTAssertFalse(getSingleFrontendInvocationFromDriverArguments(argList: testCommand.components(separatedBy: " "),
61+
outputFrontendArgs: &resultingFrontendArgs,
62+
emittedDiagnostics: &emittedDiagnostics,
63+
forceNoOutputs: true))
64+
XCTAssertFalse(resultingFrontendArgs.contains("-emit-module-interface-path"))
65+
XCTAssertFalse(resultingFrontendArgs.contains("-emit-objc-header"))
66+
XCTAssertFalse(resultingFrontendArgs.contains("-emit-objc-header-path"))
67+
XCTAssertFalse(resultingFrontendArgs.contains("-emit-module-path"))
68+
XCTAssertFalse(resultingFrontendArgs.contains("-emit-tbd-path"))
69+
}
70+
71+
// Expected failure scenarios:
72+
do {
73+
let testCommand = "-v" // No inputs
74+
var resultingFrontendArgs: [String] = []
75+
var emittedDiagnostics: [Diagnostic] = []
76+
XCTAssertTrue(getSingleFrontendInvocationFromDriverArguments(argList: testCommand.components(separatedBy: " "),
77+
outputFrontendArgs: &resultingFrontendArgs,
78+
emittedDiagnostics: &emittedDiagnostics))
79+
let errorMessage = try XCTUnwrap(emittedDiagnostics.first?.message.text)
80+
XCTAssertEqual(errorMessage, "unable to handle compilation, expected exactly one frontend job")
81+
}
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)