-
Notifications
You must be signed in to change notification settings - Fork 10.5k
/
Copy pathSwiftcInvocation.swift
201 lines (177 loc) · 6.85 KB
/
SwiftcInvocation.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
//===------- SwiftcInvocation.swift - Utilities for invoking swiftc -------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
// This file provides the logic for invoking swiftc to parse Swift files.
//===----------------------------------------------------------------------===//
import Foundation
#if os(macOS)
import Darwin
#elseif os(Linux)
import Glibc
#endif
/// The result of process execution, containing the exit status code,
/// stdout, and stderr
struct ProcessResult {
/// The process exit code. A non-zero exit code usually indicates failure.
let exitCode: Int
/// The contents of the process's stdout as Data.
let stdoutData: Data
/// The contents of the process's stderr as Data.
let stderrData: Data
/// The contents of the process's stdout, assuming the data was UTF-8 encoded.
var stdout: String {
return String(data: stdoutData, encoding: .utf8)!
}
/// The contents of the process's stderr, assuming the data was UTF-8 encoded.
var stderr: String {
return String(data: stderrData, encoding: .utf8)!
}
/// Whether or not this process had a non-zero exit code.
var wasSuccessful: Bool {
return exitCode == 0
}
}
/// Runs the provided executable with the provided arguments and returns the
/// contents of stdout and stderr as Data.
/// - Parameters:
/// - executable: The full file URL to the executable you're running.
/// - arguments: A list of strings to pass to the process as arguments.
/// - Returns: A ProcessResult containing stdout, stderr, and the exit code.
func run(_ executable: URL, arguments: [String] = []) -> ProcessResult {
// Use an autoreleasepool to prevent memory- and file-descriptor leaks.
return autoreleasepool {
() -> ProcessResult in
let stdoutPipe = Pipe()
var stdoutData = Data()
stdoutPipe.fileHandleForReading.readabilityHandler = { file in
stdoutData.append(file.availableData)
}
let stderrPipe = Pipe()
var stderrData = Data()
stderrPipe.fileHandleForReading.readabilityHandler = { file in
stderrData.append(file.availableData)
}
let process = Process()
process.terminationHandler = { process in
stdoutPipe.fileHandleForReading.readabilityHandler = nil
stderrPipe.fileHandleForReading.readabilityHandler = nil
}
process.launchPath = executable.path
process.arguments = arguments
process.standardOutput = stdoutPipe
process.standardError = stderrPipe
process.launch()
process.waitUntilExit()
return ProcessResult(exitCode: Int(process.terminationStatus),
stdoutData: stdoutData,
stderrData: stderrData)
}
}
/// Finds the dylib or executable which the provided address falls in.
/// - Parameter dsohandle: A pointer to a symbol in the object file you're
/// looking for. If not provided, defaults to the
/// caller's `#dsohandle`, which will give you the
/// object file the caller resides in.
/// - Returns: A File URL pointing to the object where the provided address
/// resides. This may be a dylib, shared object, static library,
/// or executable. If unable to find the appropriate object, returns
/// `nil`.
func findFirstObjectFile(for dsohandle: UnsafeRawPointer = #dsohandle) -> URL? {
var info = dl_info()
if dladdr(dsohandle, &info) == 0 {
return nil
}
let path = String(cString: info.dli_fname)
return URL(fileURLWithPath: path)
}
enum InvocationError: Error, CustomStringConvertible {
case couldNotFindSwiftc
case couldNotFindSDK
var description: String {
switch self {
case .couldNotFindSwiftc:
return "could not locate swift compiler binary"
case .couldNotFindSDK:
return "could not locate macOS SDK"
}
}
}
struct SwiftcRunner {
/// Gets the `swiftc` binary packaged alongside this library.
/// - Returns: The path to `swiftc` relative to the path of this library
/// file, or `nil` if it could not be found.
/// - Note: This makes assumptions about your Swift installation directory
/// structure. Importantly, it assumes that the directory tree is
/// shaped like this:
/// ```
/// install_root/
/// - bin/
/// - swiftc
/// - lib/
/// - swift/
/// - ${target}/
/// - libswiftSwiftSyntax.[dylib|so]
/// ```
static func locateSwiftc() -> URL? {
guard let libraryPath = findFirstObjectFile() else { return nil }
let swiftcURL = libraryPath.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.appendingPathComponent("bin")
.appendingPathComponent("swiftc")
guard FileManager.default.fileExists(atPath: swiftcURL.path) else {
return nil
}
return swiftcURL
}
#if os(macOS)
/// The location of the macOS SDK, or `nil` if it could not be found.
static let macOSSDK: String? = {
let url = URL(fileURLWithPath: "/usr/bin/env")
let result = run(url, arguments: ["xcrun", "--show-sdk-path"])
guard result.wasSuccessful else { return nil }
let toolPath = result.stdout.trimmingCharacters(in: .whitespacesAndNewlines)
if toolPath.isEmpty { return nil }
return toolPath
}()
#endif
/// Internal static cache of the Swiftc path.
static let _swiftcURL: URL? = SwiftcRunner.locateSwiftc()
/// The URL where the `swiftc` binary lies.
let swiftcURL: URL
/// The source file being parsed.
let sourceFile: URL
/// Creates a SwiftcRunner that will parse and emit the syntax
/// tree for a provided source file.
/// - Parameter sourceFile: The URL to the source file you're trying
/// to parse.
init(sourceFile: URL) throws {
guard let url = SwiftcRunner._swiftcURL else {
throw InvocationError.couldNotFindSwiftc
}
self.swiftcURL = url
self.sourceFile = sourceFile
}
/// Invokes swiftc with the provided arguments.
func invoke() throws -> ProcessResult {
var arguments = ["-frontend", "-emit-syntax"]
arguments.append(sourceFile.path)
#if os(macOS)
guard let sdk = SwiftcRunner.macOSSDK else {
throw InvocationError.couldNotFindSDK
}
arguments.append("-sdk")
arguments.append(sdk)
#endif
return run(swiftcURL, arguments: arguments)
}
}