-
Notifications
You must be signed in to change notification settings - Fork 221
/
Xcode.swift
executable file
·322 lines (276 loc) · 11.7 KB
/
Xcode.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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
//
// Xcode.swift
// SourceKitten
//
// Created by JP Simard on 7/15/15.
// Copyright © 2015 SourceKitten. All rights reserved.
//
import Foundation
import Yams
internal enum XcodeBuild {
/**
Run `xcodebuild clean build` along with any passed in build arguments.
- parameter arguments: Arguments to pass to `xcodebuild`.
- parameter path: Path to run `xcodebuild` from.
- returns: `xcodebuild`'s STDERR+STDOUT output combined.
*/
internal static func cleanBuild(arguments: [String], inPath path: String) -> String? {
let arguments = arguments + ["clean",
"build",
"CODE_SIGN_IDENTITY=",
"CODE_SIGNING_REQUIRED=NO",
"CODE_SIGNING_ALLOWED=NO"]
fputs("Running xcodebuild\n", stderr)
return run(arguments: arguments, inPath: path)
}
/**
Run `xcodebuild` along with any passed in build arguments.
- parameter arguments: Arguments to pass to `xcodebuild`.
- parameter path: Path to run `xcodebuild` from.
- returns: `xcodebuild`'s STDERR+STDOUT output combined.
*/
internal static func run(arguments: [String], inPath path: String) -> String? {
return String(data: launch(arguments: arguments, inPath: path, pipingStandardError: true), encoding: .utf8)
}
/**
Launch `xcodebuild` along with any passed in build arguments.
- parameter arguments: Arguments to pass to `xcodebuild`.
- parameter path: Path to run `xcodebuild` from.
- parameter pipingStandardError: Whether to pipe the standard error output. The default value is `true`.
- returns: `xcodebuild`'s STDOUT output and, optionally, both STDERR+STDOUT output combined.
*/
internal static func launch(arguments: [String], inPath path: String, pipingStandardError: Bool = true) -> Data {
let task = Process()
let pathOfXcodebuild = "/usr/bin/xcodebuild"
task.arguments = arguments
let pipe = Pipe()
task.standardOutput = pipe
if pipingStandardError {
task.standardError = pipe
}
do {
#if canImport(Darwin)
if #available(macOS 10.13, *) {
task.executableURL = URL(fileURLWithPath: pathOfXcodebuild)
task.currentDirectoryURL = URL(fileURLWithPath: path)
try task.run()
} else {
task.launchPath = pathOfXcodebuild
task.currentDirectoryPath = path
task.launch()
}
#elseif compiler(>=5)
task.executableURL = URL(fileURLWithPath: pathOfXcodebuild)
task.currentDirectoryURL = URL(fileURLWithPath: path)
try task.run()
#else
task.launchPath = pathOfXcodebuild
task.currentDirectoryPath = path
task.launch()
#endif
} catch {
return Data()
}
let file = pipe.fileHandleForReading
defer { file.closeFile() }
return file.readDataToEndOfFile()
}
/**
Runs `xcodebuild -showBuildSettings` along with any passed in build arguments.
- parameter arguments: Arguments to pass to `xcodebuild`.
- parameter path: Path to run `xcodebuild` from.
- returns: An array of `XcodeBuildSetting`s.
*/
internal static func showBuildSettings(arguments xcodeBuildArguments: [String],
inPath: String) -> [XcodeBuildSetting]? {
let arguments = xcodeBuildArguments + ["-showBuildSettings", "-json"]
let outputData = XcodeBuild.launch(arguments: arguments, inPath: inPath, pipingStandardError: false)
return try? JSONDecoder().decode([XcodeBuildSetting].self, from: outputData)
}
}
/**
Parses likely module name from compiler or `xcodebuild` arguments.
Will use the following values, in this priority: module name, target name, scheme name.
- parameter arguments: Compiler or `xcodebuild` arguments to parse.
- returns: Module name if successful.
*/
internal func moduleName(fromArguments arguments: [String]) -> String? {
for flag in ["-module-name", "-target", "-scheme"] {
if let flagIndex = arguments.index(of: flag), flagIndex + 1 < arguments.count {
return arguments[flagIndex + 1]
}
}
return nil
}
/**
Partially filters compiler arguments from `xcodebuild` to something that SourceKit/Clang will accept.
- parameter args: Compiler arguments, as parsed from `xcodebuild`.
- returns: A tuple of partially filtered compiler arguments in `.0`, and whether or not there are
more flags to remove in `.1`.
*/
private func partiallyFilter(arguments args: [String]) -> ([String], Bool) {
guard let indexOfFlagToRemove = args.index(of: "-output-file-map") else {
return (args, false)
}
var args = args
args.remove(at: args.index(after: indexOfFlagToRemove))
args.remove(at: indexOfFlagToRemove)
return (args, true)
}
/**
Filters compiler arguments from `xcodebuild` to something that SourceKit/Clang will accept.
- parameter args: Compiler arguments, as parsed from `xcodebuild`.
- returns: Filtered compiler arguments.
*/
private func filter(arguments args: [String]) -> [String] {
var args = args
args.append(contentsOf: ["-D", "DEBUG"])
var shouldContinueToFilterArguments = true
while shouldContinueToFilterArguments {
(args, shouldContinueToFilterArguments) = partiallyFilter(arguments: args)
}
return args.filter {
![
"-parseable-output",
"-incremental",
"-serialize-diagnostics",
"-emit-dependencies"
].contains($0)
}.map {
if $0 == "-O" {
return "-Onone"
} else if $0 == "-DNDEBUG=1" {
return "-DDEBUG=1"
}
return $0
}
}
/**
Parses the compiler arguments needed to compile the `language` files.
- parameter xcodebuildOutput: Output of `xcodebuild` to be parsed for compiler arguments.
- parameter language: Language to parse for.
- parameter moduleName: Name of the Module for which to extract compiler arguments.
- returns: Compiler arguments, filtered for suitable use by SourceKit if `.Swift` or Clang if `.ObjC`.
*/
internal func parseCompilerArguments(xcodebuildOutput: String, language: Language, moduleName: String?) -> [String]? {
let pattern: String
if language == .objc {
pattern = "/usr/bin/clang.*"
} else if let moduleName = moduleName {
pattern = "/usr/bin/swiftc.*-module-name \(moduleName) .*"
} else {
pattern = "/usr/bin/swiftc.*"
}
let regex = try! NSRegularExpression(pattern: pattern, options: []) // Safe to force try
let range = NSRange(xcodebuildOutput.startIndex..<xcodebuildOutput.endIndex, in: xcodebuildOutput)
guard let regexMatch = regex.firstMatch(in: xcodebuildOutput, range: range),
let matchRange = Range(regexMatch.range, in: xcodebuildOutput) else {
return nil
}
let escapedSpacePlaceholder = "\u{0}"
let args = filter(arguments: String(xcodebuildOutput[matchRange])
.replacingOccurrences(of: "\\ ", with: escapedSpacePlaceholder)
.unescaped
.components(separatedBy: " "))
// Remove first argument (swiftc/clang) and re-add spaces in arguments
return (args[1..<args.count]).map {
$0.replacingOccurrences(of: escapedSpacePlaceholder, with: " ")
}
}
/**
Extracts Objective-C header files and `xcodebuild` arguments from an array of header files followed by `xcodebuild` arguments.
- parameter sourcekittenArguments: Array of Objective-C header files followed by `xcodebuild` arguments.
- returns: Tuple of header files and xcodebuild arguments.
*/
public func parseHeaderFilesAndXcodebuildArguments(sourcekittenArguments: [String]) -> (headerFiles: [String], xcodebuildArguments: [String]) {
var xcodebuildArguments = sourcekittenArguments
var headerFiles = [String]()
while let headerFile = xcodebuildArguments.first, headerFile.bridge().isObjectiveCHeaderFile() {
headerFiles.append(xcodebuildArguments.remove(at: 0).bridge().absolutePathRepresentation())
}
return (headerFiles, xcodebuildArguments)
}
public func sdkPath() -> String {
#if os(Linux)
// xcrun does not exist on Linux
return ""
#else
let task = Process()
let pathOfXcrun = "/usr/bin/xcrun"
task.arguments = ["--show-sdk-path", "--sdk", "macosx"]
let pipe = Pipe()
task.standardOutput = pipe
do {
#if canImport(Darwin)
if #available(macOS 10.13, *) {
task.executableURL = URL(fileURLWithPath: pathOfXcrun)
try task.run()
} else {
task.launchPath = pathOfXcrun
task.launch()
}
#elseif compiler(>=5)
task.executableURL = URL(fileURLWithPath: pathOfXcrun)
try task.run()
#else
task.launchPath = pathOfXcrun
task.launch()
#endif
} catch {
return ""
}
let file = pipe.fileHandleForReading
let sdkPath = String(data: file.readDataToEndOfFile(), encoding: .utf8)
file.closeFile()
return sdkPath?.replacingOccurrences(of: "\n", with: "") ?? ""
#endif
}
/**
Extracts compilerArguments by parsing `${PROJECT_TEMP_ROOT}/XCBuildData/ *-manifest.xcbuild`
as `llbuild` manifest file used by New Build System.
```
${PROJECT_TEMP_ROOT}
├── XCBuildData
│ ├── 0a834e06ba44b3930a452b71c1425ef7-desc.xcbuild
│ ├── 0a834e06ba44b3930a452b71c1425ef7-manifest.xcbuild
│ ├── 0f0e5da56188a83852ce7539aad77821-desc.xcbuild
│ ├── 0f0e5da56188a83852ce7539aad77821-manifest.xcbuild
```
- parameter projectTempRoot: Path.
- parameter moduleName: Name of the Module for which to extract compiler arguments.
- returns: Compiler arguments, filtered for suitable use by SourceKit.
*/
internal func checkNewBuildSystem(in projectTempRoot: String, moduleName: String? = nil) -> [String]? {
let xcbuildDataURL = URL(fileURLWithPath: projectTempRoot).appendingPathComponent("XCBuildData")
do {
// Find manifests in `PROJECT_TEMP_ROOT`
let fileURLs = try FileManager.default.contentsOfDirectory(at: xcbuildDataURL, includingPropertiesForKeys: [.fileSizeKey])
let manifestURLs = try fileURLs.filter { $0.path.hasSuffix("-manifest.xcbuild") }
.map { (url: $0, size: try $0.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0) }
.sorted { $0.size < $1.size }
.map { $0.url }
let result = manifestURLs.lazy.compactMap { manifestURL -> [String]? in
guard let contents = try? String(contentsOf: manifestURL),
let yaml = try? Yams.compose(yaml: contents),
let commands = yaml?["commands"]?.mapping?.values else {
return nil
}
for command in commands where command["description"]?.string?.hasSuffix("com.apple.xcode.tools.swift.compiler") ?? false {
if let args = command["args"]?.sequence,
let index = args.index(of: "-module-name"),
moduleName != nil ? args[args.index(after: index)].string == moduleName : true {
let fullArgs = args.compactMap { $0.string }
let swiftCIndex = fullArgs.index(of: "--").flatMap(fullArgs.index(after:)) ?? fullArgs.startIndex
return Array(fullArgs.suffix(from: fullArgs.index(after: swiftCIndex)))
}
}
return nil
}.first.map { filter(arguments: $0) }
if result != nil {
fputs("Assuming New Build System is used.\n", stderr)
}
return result
} catch {
return nil
}
}