@@ -31,6 +31,8 @@ import func TSCBasic.withTemporaryFile
3131
3232import enum TSCUtility. Diagnostics
3333
34+ import var TSCBasic. stdoutStream
35+
3436import Foundation
3537import SWBBuildService
3638import SwiftBuild
@@ -342,11 +344,22 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
342344 }
343345
344346 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+
345355 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
350363 }
351364
352365 try await writePIF ( buildParameters: buildParameters)
@@ -357,6 +370,146 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
357370 )
358371 }
359372
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+
360513 private func startSWBuildOperation(
361514 pifTargetName: String ,
362515 buildOutputs: [ BuildOutput ]
@@ -371,6 +524,7 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem {
371524 continue
372525 }
373526 }
527+
374528 var replArguments : CLIArguments ?
375529 var artifacts : [ ( String , PluginInvocationBuildResult . BuiltArtifact ) ] ?
376530 return try await withService ( connectionMode: . inProcessStatic( swiftbuildServiceEntryPoint) ) { service in
0 commit comments