-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
Copy pathPluginDelegate.swift
492 lines (442 loc) · 21.5 KB
/
PluginDelegate.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
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2014-2022 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import Basics
import _Concurrency
import CoreCommands
import Foundation
import PackageModel
import SPMBuildCore
import PackageGraph
import protocol TSCBasic.OutputByteStream
import class TSCBasic.BufferedOutputByteStream
import class Basics.AsyncProcess
import struct Basics.AsyncProcessResult
final class PluginDelegate: PluginInvocationDelegate {
let swiftCommandState: SwiftCommandState
let plugin: PluginModule
var lineBufferedOutput: Data
init(swiftCommandState: SwiftCommandState, plugin: PluginModule) {
self.swiftCommandState = swiftCommandState
self.plugin = plugin
self.lineBufferedOutput = Data()
}
func pluginCompilationStarted(commandLine: [String], environment: [String: String]) {
}
func pluginCompilationEnded(result: PluginCompilationResult) {
}
func pluginCompilationWasSkipped(cachedResult: PluginCompilationResult) {
}
func pluginEmittedOutput(_ data: Data) {
lineBufferedOutput += data
while let newlineIdx = lineBufferedOutput.firstIndex(of: UInt8(ascii: "\n")) {
let lineData = lineBufferedOutput.prefix(upTo: newlineIdx)
print(String(decoding: lineData, as: UTF8.self))
lineBufferedOutput = lineBufferedOutput.suffix(from: newlineIdx.advanced(by: 1))
}
}
func pluginEmittedDiagnostic(_ diagnostic: Basics.Diagnostic) {
swiftCommandState.observabilityScope.emit(diagnostic)
}
func pluginEmittedProgress(_ message: String) {
swiftCommandState.outputStream.write("[\(plugin.name)] \(message)\n")
swiftCommandState.outputStream.flush()
}
func pluginRequestedBuildOperation(
subset: PluginInvocationBuildSubset,
parameters: PluginInvocationBuildParameters,
completion: @escaping (Result<PluginInvocationBuildResult, Error>) -> Void
) {
// Run the build in the background and call the completion handler when done.
Task {
do {
try await completion(.success(self.performBuildForPlugin(subset: subset, parameters: parameters)))
} catch {
completion(.failure(error))
}
}
}
class TeeOutputByteStream: OutputByteStream {
var downstreams: [OutputByteStream]
public init(_ downstreams: [OutputByteStream]) {
self.downstreams = downstreams
}
var position: Int {
return 0 // should be related to the downstreams somehow
}
public func write(_ byte: UInt8) {
for downstream in downstreams {
downstream.write(byte)
}
}
func write<C: Collection>(_ bytes: C) where C.Element == UInt8 {
for downstream in downstreams {
downstream.write(bytes)
}
}
public func flush() {
for downstream in downstreams {
downstream.flush()
}
}
public func addStream(_ stream: OutputByteStream) {
self.downstreams.append(stream)
}
}
private func performBuildForPlugin(
subset: PluginInvocationBuildSubset,
parameters: PluginInvocationBuildParameters
) async throws -> PluginInvocationBuildResult {
// Configure the build parameters.
var buildParameters = try self.swiftCommandState.productsBuildParameters
switch parameters.configuration {
case .debug:
buildParameters.configuration = .debug
case .release:
buildParameters.configuration = .release
case .inherit:
// The top level argument parser set buildParameters.configuration according to the
// --configuration command line parameter. We don't need to do anything to inherit it.
break
}
buildParameters.flags.cCompilerFlags.append(contentsOf: parameters.otherCFlags)
buildParameters.flags.cxxCompilerFlags.append(contentsOf: parameters.otherCxxFlags)
buildParameters.flags.swiftCompilerFlags.append(contentsOf: parameters.otherSwiftcFlags)
buildParameters.flags.linkerFlags.append(contentsOf: parameters.otherLinkerFlags)
// Configure the verbosity of the output.
let logLevel: Basics.Diagnostic.Severity
switch parameters.logging {
case .concise:
logLevel = .warning
case .verbose:
logLevel = .info
case .debug:
logLevel = .debug
}
// Determine the subset of products and targets to build.
var explicitProduct: String? = .none
let buildSubset: BuildSubset
switch subset {
case .all(let includingTests):
buildSubset = includingTests ? .allIncludingTests : .allExcludingTests
if includingTests {
// Enable testability if we're building tests explicitly.
buildParameters.testingParameters.explicitlyEnabledTestability = true
}
case .product(let name):
buildSubset = .product(name)
explicitProduct = name
case .target(let name):
buildSubset = .target(name)
}
// Create a build operation. We have to disable the cache in order to get a build plan created.
let bufferedOutputStream = BufferedOutputByteStream()
let outputStream = TeeOutputByteStream([bufferedOutputStream])
if parameters.echoLogs {
outputStream.addStream(swiftCommandState.outputStream)
}
let progressOutputStream = parameters.progressToConsole ? swiftCommandState.outputStream : outputStream
let buildSystem = try await swiftCommandState.createBuildSystem(
explicitBuildSystem: .native,
explicitProduct: explicitProduct,
traitConfiguration: .init(),
cacheBuildManifest: false,
productsBuildParameters: buildParameters,
outputStream: outputStream,
progressOutputStream: progressOutputStream,
logLevel: logLevel
)
// Run the build. This doesn't return until the build is complete.
let success = await buildSystem.buildIgnoringError(subset: buildSubset)
// Create and return the build result record based on what the delegate collected and what's in the build plan.
let builtProducts = try buildSystem.buildPlan.buildProducts.filter {
switch subset {
case .all(let includingTests):
return includingTests ? true : $0.product.type != .test
case .product(let name):
return $0.product.name == name
case .target(let name):
return $0.product.name == name
}
}
let builtArtifacts: [PluginInvocationBuildResult.BuiltArtifact] = try builtProducts.compactMap {
switch $0.product.type {
case .library(let kind):
return try .init(
path: $0.binaryPath.pathString,
kind: (kind == .dynamic) ? .dynamicLibrary : .staticLibrary
)
case .executable:
return try .init(path: $0.binaryPath.pathString, kind: .executable)
default:
return nil
}
}
return PluginInvocationBuildResult(
succeeded: success,
logText: bufferedOutputStream.bytes.cString,
builtArtifacts: builtArtifacts)
}
func pluginRequestedTestOperation(
subset: PluginInvocationTestSubset,
parameters: PluginInvocationTestParameters,
completion: @escaping (Result<PluginInvocationTestResult, Error>
) -> Void) {
// Run the test in the background and call the completion handler when done.
Task {
do {
try await completion(.success(self.performTestsForPlugin(subset: subset, parameters: parameters)))
} catch {
completion(.failure(error))
}
}
}
func performTestsForPlugin(
subset: PluginInvocationTestSubset,
parameters: PluginInvocationTestParameters
) async throws -> PluginInvocationTestResult {
// Build the tests. Ideally we should only build those that match the subset, but we don't have a way to know
// which ones they are until we've built them and can examine the binaries.
let toolchain = try swiftCommandState.getHostToolchain()
var toolsBuildParameters = try swiftCommandState.toolsBuildParameters
toolsBuildParameters.testingParameters.explicitlyEnabledTestability = true
toolsBuildParameters.testingParameters.enableCodeCoverage = parameters.enableCodeCoverage
let buildSystem = try await swiftCommandState.createBuildSystem(
traitConfiguration: .init(),
toolsBuildParameters: toolsBuildParameters
)
try await buildSystem.build(subset: .allIncludingTests)
// Clean out the code coverage directory that may contain stale `profraw` files from a previous run of
// the code coverage tool.
if parameters.enableCodeCoverage {
try swiftCommandState.fileSystem.removeFileTree(toolsBuildParameters.codeCovPath)
}
// Construct the environment we'll pass down to the tests.
let testEnvironment = try TestingSupport.constructTestEnvironment(
toolchain: toolchain,
destinationBuildParameters: toolsBuildParameters,
sanitizers: swiftCommandState.options.build.sanitizers,
library: .xctest // FIXME: support both libraries
)
// Iterate over the tests and run those that match the filter.
var testTargetResults: [PluginInvocationTestResult.TestTarget] = []
var numFailedTests = 0
for testProduct in await buildSystem.builtTestProducts {
// Get the test suites in the bundle. Each is just a container for test cases.
let testSuites = try TestingSupport.getTestSuites(
fromTestAt: testProduct.bundlePath,
swiftCommandState: swiftCommandState,
enableCodeCoverage: parameters.enableCodeCoverage,
shouldSkipBuilding: false,
experimentalTestOutput: false,
sanitizers: swiftCommandState.options.build.sanitizers
)
for testSuite in testSuites {
// Each test suite is just a container for test cases (confusingly called "tests",
// though they are test cases).
for testCase in testSuite.tests {
// Each test case corresponds to a combination of target and a XCTestCase, and is
// a collection of tests that can actually be run.
var testResults: [PluginInvocationTestResult.TestTarget.TestCase.Test] = []
for testName in testCase.tests {
// Check if we should filter out this test.
let testSpecifier = testCase.name + "/" + testName
if case .filtered(let regexes) = subset {
guard regexes.contains(
where: { testSpecifier.range(of: $0, options: .regularExpression) != nil }
) else {
continue
}
}
// Configure a test runner.
let additionalArguments = TestRunner.xctestArguments(forTestSpecifiers: CollectionOfOne(testSpecifier))
let testRunner = TestRunner(
bundlePaths: [testProduct.bundlePath],
additionalArguments: additionalArguments,
cancellator: swiftCommandState.cancellator,
toolchain: toolchain,
testEnv: testEnvironment,
observabilityScope: swiftCommandState.observabilityScope,
library: .xctest) // FIXME: support both libraries
// Run the test — for now we run the sequentially so we can capture accurate timing results.
let startTime = DispatchTime.now()
let result = testRunner.test(outputHandler: { _ in }) // this drops the tests output
let duration = Double(startTime.distance(to: .now()).milliseconds() ?? 0) / 1000.0
numFailedTests += (result != .failure) ? 0 : 1
testResults.append(
.init(name: testName, result: (result != .failure) ? .succeeded : .failed, duration: duration)
)
}
// Don't add any results if we didn't run any tests.
if testResults.isEmpty { continue }
// Otherwise we either create a new create a new target result or add to the previous one,
// depending on whether the target name is the same.
let testTargetName = testCase.name.prefix(while: { $0 != "." })
if let lastTestTargetName = testTargetResults.last?.name, testTargetName == lastTestTargetName {
// Same as last one, just extend its list of cases. We know we have a last one at this point.
testTargetResults[testTargetResults.count-1].testCases.append(
.init(name: testCase.name, tests: testResults)
)
}
else {
// Not the same, so start a new target result.
testTargetResults.append(
.init(
name: String(testTargetName),
testCases: [.init(name: testCase.name, tests: testResults)]
)
)
}
}
}
}
// Deal with code coverage, if enabled.
let codeCoverageDataFile: AbsolutePath?
if parameters.enableCodeCoverage {
// Use `llvm-prof` to merge all the `.profraw` files into a single `.profdata` file.
let mergedCovFile = toolsBuildParameters.codeCovDataFile
let codeCovFileNames = try swiftCommandState.fileSystem.getDirectoryContents(toolsBuildParameters.codeCovPath)
var llvmProfCommand = [try toolchain.getLLVMProf().pathString]
llvmProfCommand += ["merge", "-sparse"]
for fileName in codeCovFileNames where fileName.hasSuffix(".profraw") {
let filePath = toolsBuildParameters.codeCovPath.appending(component: fileName)
llvmProfCommand.append(filePath.pathString)
}
llvmProfCommand += ["-o", mergedCovFile.pathString]
try await AsyncProcess.checkNonZeroExit(arguments: llvmProfCommand)
// Use `llvm-cov` to export the merged `.profdata` file contents in JSON form.
var llvmCovCommand = [try toolchain.getLLVMCov().pathString]
llvmCovCommand += ["export", "-instr-profile=\(mergedCovFile.pathString)"]
for product in await buildSystem.builtTestProducts {
llvmCovCommand.append("-object")
llvmCovCommand.append(product.binaryPath.pathString)
}
// We get the output on stdout, and have to write it to a JSON ourselves.
let jsonOutput = try await AsyncProcess.checkNonZeroExit(arguments: llvmCovCommand)
let jsonCovFile = toolsBuildParameters.codeCovDataFile.parentDirectory.appending(
component: toolsBuildParameters.codeCovDataFile.basenameWithoutExt + ".json"
)
try swiftCommandState.fileSystem.writeFileContents(jsonCovFile, string: jsonOutput)
// Return the path of the exported code coverage data file.
codeCoverageDataFile = jsonCovFile
}
else {
codeCoverageDataFile = nil
}
// Return the results to the plugin. We only consider the test run a success if no test failed.
return PluginInvocationTestResult(
succeeded: (numFailedTests == 0),
testTargets: testTargetResults,
codeCoverageDataFile: codeCoverageDataFile?.pathString)
}
func pluginRequestedSymbolGraph(
forTarget targetName: String,
options: PluginInvocationSymbolGraphOptions,
completion: @escaping (Result<PluginInvocationSymbolGraphResult, Error>) -> Void
) {
// Extract the symbol graph in the background and call the completion handler when done.
Task {
do {
try await completion(.success(self.createSymbolGraphForPlugin(forTarget: targetName, options: options)))
} catch {
completion(.failure(error))
}
}
}
private func createSymbolGraphForPlugin(
forTarget targetName: String,
options: PluginInvocationSymbolGraphOptions
) async throws -> PluginInvocationSymbolGraphResult {
// Current implementation uses `SymbolGraphExtract()`, but in the future we should emit the symbol graph
// while building.
// Create a build system for building the target., skipping the the cache because we need the build plan.
let buildSystem = try await swiftCommandState.createBuildSystem(
explicitBuildSystem: .native,
traitConfiguration: TraitConfiguration(enableAllTraits: true),
cacheBuildManifest: false
)
func lookupDescription(
for moduleName: String,
destination: BuildParameters.Destination
) throws -> ModuleBuildDescription? {
try buildSystem.buildPlan.buildModules.first {
$0.module.name == moduleName && $0.buildParameters.destination == destination
}
}
// Build the target, if needed. This would also create a build plan.
try await buildSystem.build(subset: .target(targetName))
// FIXME: The name alone doesn't give us enough information to figure out what
// the destination is, this logic prefers "target" over "host" because that's
// historically how this was setup. Ideally we should be building for both "host"
// and "target" if module is configured for them but that would require changing
// `PluginInvocationSymbolGraphResult` to carry multiple directories.
let description = if let targetDescription = try lookupDescription(for: targetName, destination: .target) {
targetDescription
} else if let hostDescription = try lookupDescription(for: targetName, destination: .host) {
hostDescription
} else {
throw InternalError("could not find a target named: \(targetName)")
}
// Configure the symbol graph extractor.
var symbolGraphExtractor = try SymbolGraphExtract(
fileSystem: swiftCommandState.fileSystem,
tool: swiftCommandState.getTargetToolchain().getSymbolGraphExtract(),
observabilityScope: swiftCommandState.observabilityScope
)
symbolGraphExtractor.skipSynthesizedMembers = !options.includeSynthesized
switch options.minimumAccessLevel {
case .private:
symbolGraphExtractor.minimumAccessLevel = .private
case .fileprivate:
symbolGraphExtractor.minimumAccessLevel = .fileprivate
case .internal:
symbolGraphExtractor.minimumAccessLevel = .internal
case .package:
symbolGraphExtractor.minimumAccessLevel = .package
case .public:
symbolGraphExtractor.minimumAccessLevel = .public
case .open:
symbolGraphExtractor.minimumAccessLevel = .open
}
symbolGraphExtractor.skipInheritedDocs = true
symbolGraphExtractor.includeSPISymbols = options.includeSPI
symbolGraphExtractor.emitExtensionBlockSymbols = options.emitExtensionBlocks
// Determine the output directory, and remove any old version if it already exists.
let outputDir = description.buildParameters.dataPath.appending(
components: "extracted-symbols",
description.package.identity.description,
targetName
)
try swiftCommandState.fileSystem.removeFileTree(outputDir)
// Run the symbol graph extractor on the target.
let result = try symbolGraphExtractor.extractSymbolGraph(
for: description,
outputRedirection: .collect,
outputDirectory: outputDir,
verboseOutput: self.swiftCommandState.logLevel <= .info
)
guard result.exitStatus == .terminated(code: 0) else {
throw AsyncProcessResult.Error.nonZeroExit(result)
}
// Return the results to the plugin.
return PluginInvocationSymbolGraphResult(directoryPath: outputDir.pathString)
}
}
extension BuildSystem {
fileprivate func buildIgnoringError(subset: BuildSubset) async -> Bool {
do {
try await self.build(subset: subset)
return true
} catch {
return false
}
}
}