@@ -31,6 +31,8 @@ import func TSCBasic.withTemporaryFile
31
31
32
32
import enum TSCUtility. Diagnostics
33
33
34
+ import var TSCBasic. stdoutStream
35
+
34
36
import Foundation
35
37
import SWBBuildService
36
38
import SwiftBuild
@@ -342,11 +344,22 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
342
344
}
343
345
344
346
public func build( subset: BuildSubset , buildOutputs: [ BuildOutput ] ) async throws -> BuildResult {
347
+ // If any plugins are part of the build set, compile them now to surface
348
+ // any errors up-front. Returns true if we should proceed with the build
349
+ // or false if not. It will already have thrown any appropriate error.
350
+ var result = BuildResult (
351
+ serializedDiagnosticPathsByTargetName: . failure( StringError ( " Building was skipped " ) ) ,
352
+ replArguments: nil ,
353
+ )
354
+
345
355
guard !buildParameters. shouldSkipBuilding else {
346
- return BuildResult (
347
- serializedDiagnosticPathsByTargetName: . failure( StringError ( " Building was skipped " ) ) ,
348
- replArguments: nil ,
349
- )
356
+ result. serializedDiagnosticPathsByTargetName = . failure( StringError ( " Building was skipped " ) )
357
+ return result
358
+ }
359
+
360
+ guard try await self . compilePlugins ( in: subset) else {
361
+ result. serializedDiagnosticPathsByTargetName = . failure( StringError ( " Plugin compilation failed " ) )
362
+ return result
350
363
}
351
364
352
365
try await writePIF ( buildParameters: buildParameters)
@@ -357,6 +370,146 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
357
370
)
358
371
}
359
372
373
+ /// Compiles any plugins specified or implied by the build subset, returning
374
+ /// true if the build should proceed. Throws an error in case of failure. A
375
+ /// reason why the build might not proceed even on success is if only plugins
376
+ /// should be compiled.
377
+ func compilePlugins( in subset: BuildSubset ) async throws -> Bool {
378
+ // Figure out what, if any, plugin descriptions to compile, and whether
379
+ // to continue building after that based on the subset.
380
+ let graph = try await getPackageGraph ( )
381
+
382
+ /// Description for a plugin module. This is treated a bit differently from the
383
+ /// regular kinds of modules, and is not included in the LLBuild description.
384
+ /// But because the modules graph and build plan are not loaded for incremental
385
+ /// builds, this information is included in the BuildDescription, and the plugin
386
+ /// modules are compiled directly.
387
+ struct PluginBuildDescription : Codable {
388
+ /// The identity of the package in which the plugin is defined.
389
+ public let package : PackageIdentity
390
+
391
+ /// The name of the plugin module in that package (this is also the name of
392
+ /// the plugin).
393
+ public let moduleName : String
394
+
395
+ /// The language-level module name.
396
+ public let moduleC99Name : String
397
+
398
+ /// The names of any plugin products in that package that vend the plugin
399
+ /// to other packages.
400
+ public let productNames : [ String ]
401
+
402
+ /// The tools version of the package that declared the module. This affects
403
+ /// the API that is available in the PackagePlugin module.
404
+ public let toolsVersion : ToolsVersion
405
+
406
+ /// Swift source files that comprise the plugin.
407
+ public let sources : Sources
408
+
409
+ /// Initialize a new plugin module description. The module is expected to be
410
+ /// a `PluginTarget`.
411
+ init (
412
+ module: ResolvedModule ,
413
+ products: [ ResolvedProduct ] ,
414
+ package : ResolvedPackage ,
415
+ toolsVersion: ToolsVersion ,
416
+ testDiscoveryTarget: Bool = false ,
417
+ fileSystem: FileSystem
418
+ ) throws {
419
+ guard module. underlying is PluginModule else {
420
+ throw InternalError ( " underlying target type mismatch \( module) " )
421
+ }
422
+
423
+ self . package = package . identity
424
+ self . moduleName = module. name
425
+ self . moduleC99Name = module. c99name
426
+ self . productNames = products. map ( \. name)
427
+ self . toolsVersion = toolsVersion
428
+ self . sources = module. sources
429
+ }
430
+ }
431
+
432
+ var allPlugins : [ PluginBuildDescription ] = [ ]
433
+
434
+ for pluginModule in graph. allModules. filter ( { ( $0. underlying as? PluginModule ) != nil } ) {
435
+ guard let package = graph. package ( for: pluginModule) else {
436
+ throw InternalError ( " Package not found for module: \( pluginModule. name) " )
437
+ }
438
+
439
+ let toolsVersion = package . manifest. toolsVersion
440
+
441
+ let pluginProducts = package . products. filter { $0. modules. contains ( id: pluginModule. id) }
442
+
443
+ allPlugins. append ( try PluginBuildDescription (
444
+ module: pluginModule,
445
+ products: pluginProducts,
446
+ package : package ,
447
+ toolsVersion: toolsVersion,
448
+ fileSystem: fileSystem
449
+ ) )
450
+ }
451
+
452
+ let pluginsToCompile : [ PluginBuildDescription ]
453
+ let continueBuilding : Bool
454
+ switch subset {
455
+ case . allExcludingTests, . allIncludingTests:
456
+ pluginsToCompile = allPlugins
457
+ continueBuilding = true
458
+ case . product( let productName, _) :
459
+ pluginsToCompile = allPlugins. filter { $0. productNames. contains ( productName) }
460
+ continueBuilding = pluginsToCompile. isEmpty
461
+ case . target( let targetName, _) :
462
+ pluginsToCompile = allPlugins. filter { $0. moduleName == targetName }
463
+ continueBuilding = pluginsToCompile. isEmpty
464
+ }
465
+
466
+ final class Delegate : PluginScriptCompilerDelegate {
467
+ var failed : Bool = false
468
+ var observabilityScope : ObservabilityScope
469
+
470
+ public init ( observabilityScope: ObservabilityScope ) {
471
+ self . observabilityScope = observabilityScope
472
+ }
473
+
474
+ func willCompilePlugin( commandLine: [ String ] , environment: [ String : String ] ) { }
475
+
476
+ func didCompilePlugin( result: PluginCompilationResult ) {
477
+ if !result. compilerOutput. isEmpty && !result. succeeded {
478
+ print ( result. compilerOutput, to: & stdoutStream)
479
+ } else if !result. compilerOutput. isEmpty {
480
+ observabilityScope. emit ( info: result. compilerOutput)
481
+ }
482
+
483
+ failed = !result. succeeded
484
+ }
485
+
486
+ func skippedCompilingPlugin( cachedResult: PluginCompilationResult ) { }
487
+ }
488
+
489
+ // Compile any plugins we ended up with. If any of them fails, it will
490
+ // throw.
491
+ for plugin in pluginsToCompile {
492
+ let delegate = Delegate ( observabilityScope: observabilityScope)
493
+
494
+ _ = try await self . pluginConfiguration. scriptRunner. compilePluginScript (
495
+ sourceFiles: plugin. sources. paths,
496
+ pluginName: plugin. moduleName,
497
+ toolsVersion: plugin. toolsVersion,
498
+ observabilityScope: observabilityScope,
499
+ callbackQueue: DispatchQueue . sharedConcurrent,
500
+ delegate: delegate
501
+ )
502
+
503
+ if delegate. failed {
504
+ throw Diagnostics . fatalError
505
+ }
506
+ }
507
+
508
+ // If we get this far they all succeeded. Return whether to continue the
509
+ // build, based on the subset.
510
+ return continueBuilding
511
+ }
512
+
360
513
private func startSWBuildOperation(
361
514
pifTargetName: String ,
362
515
buildOutputs: [ BuildOutput ]
@@ -371,6 +524,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
371
524
continue
372
525
}
373
526
}
527
+
374
528
var replArguments : CLIArguments ?
375
529
var artifacts : [ ( String , PluginInvocationBuildResult . BuiltArtifact ) ] ?
376
530
return try await withService ( connectionMode: . inProcessStatic( swiftbuildServiceEntryPoint) ) { service in
0 commit comments