Skip to content

Commit 4bc7241

Browse files
authored
Compile all of the plugins at the start of a build with swiftbuild (#9190)
When building with the swiftbuild build system there can be a compilation problem with the plugin.swift with one of the plugins. However, you won't discover that unless you run the command plugin specifically. This is not the behaviour of the existing native build system that does a plugin pass at the start of a build. Implement a similar plugin pass with the swiftbuild system to catch problems with the plugin code when building the packages. Fixes #8977
1 parent ed227e8 commit 4bc7241

File tree

2 files changed

+182
-61
lines changed

2 files changed

+182
-61
lines changed

Sources/SwiftBuildSupport/SwiftBuildSystem.swift

Lines changed: 158 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import func TSCBasic.withTemporaryFile
3131

3232
import enum TSCUtility.Diagnostics
3333

34+
import var TSCBasic.stdoutStream
35+
3436
import Foundation
3537
import SWBBuildService
3638
import 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

Tests/CommandsTests/PackageCommandTests.swift

Lines changed: 24 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6831,10 +6831,6 @@ struct PackageCommandTests {
68316831
}
68326832

68336833
@Test(
6834-
.issue(
6835-
"https://github.com/swiftlang/swift-package-manager/issues/8977",
6836-
relationship: .defect
6837-
),
68386834
.requiresSwiftConcurrencySupport,
68396835
.IssueWindowsLongPath,
68406836
.tags(
@@ -6961,28 +6957,37 @@ struct PackageCommandTests {
69616957
ProcessInfo.hostOperatingSystem == .windows && data.buildSystem == .swiftbuild
69626958
}
69636959

6964-
try await withKnownIssue {
6965-
// Check that building just one of them just compiles that plugin and doesn't build anything else.
6966-
do {
6967-
let (stdout, stderr) = try await executeSwiftBuild(
6968-
packageDir,
6969-
configuration: data.config,
6970-
extraArgs: ["--target", "MyCommandPlugin"],
6971-
buildSystem: data.buildSystem,
6972-
)
6973-
if data.buildSystem == .native {
6960+
// Check that building just one of them just compiles that plugin and doesn't build anything else.
6961+
do {
6962+
let (stdout, stderr) = try await executeSwiftBuild(
6963+
packageDir,
6964+
configuration: data.config,
6965+
extraArgs: ["--target", "MyCommandPlugin"],
6966+
buildSystem: data.buildSystem,
6967+
)
6968+
switch data.buildSystem {
6969+
case .native:
69746970
#expect(!stdout.contains("Compiling plugin MyBuildToolPlugin"), "stderr: \(stderr)")
69756971
#expect(stdout.contains("Compiling plugin MyCommandPlugin"), "stderr: \(stderr)")
6976-
}
6977-
#expect(!stdout.contains("Building for \(data.config.buildFor)..."), "stderr: \(stderr)")
6972+
case .swiftbuild:
6973+
// nothing specific
6974+
break
6975+
case .xcode:
6976+
Issue.record("Test expected have not been considered")
69786977
}
6979-
} when: {
6980-
data.buildSystem == .swiftbuild
6978+
#expect(!stdout.contains("Building for \(data.config.buildFor)..."), "stderr: \(stderr)")
69816979
}
69826980
}
69836981
}
69846982

6985-
private static func commandPluginCompilationErrorImplementation(
6983+
@Test(
6984+
.requiresSwiftConcurrencySupport,
6985+
.tags(
6986+
.Feature.Command.Package.CommandPlugin,
6987+
),
6988+
arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms),
6989+
)
6990+
func commandPluginCompilationErrorImplementation(
69866991
data: BuildData,
69876992
) async throws {
69886993
try await fixture(name: "Miscellaneous/Plugins/CommandPluginCompilationError") { packageDir in
@@ -7017,44 +7022,6 @@ struct PackageCommandTests {
70177022
}
70187023
}
70197024

7020-
@Test(
7021-
.requiresSwiftConcurrencySupport,
7022-
.tags(
7023-
.Feature.Command.Package.CommandPlugin,
7024-
),
7025-
// arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms),
7026-
arguments: getBuildData(for: [.native]),
7027-
)
7028-
func commandPluginCompilationError(
7029-
data: BuildData,
7030-
) async throws {
7031-
try await Self.commandPluginCompilationErrorImplementation(data: data)
7032-
}
7033-
7034-
@Test(
7035-
.disabled("the swift-build process currently has a fatal error"),
7036-
.issue(
7037-
"https://github.com/swiftlang/swift-package-manager/issues/8977",
7038-
relationship: .defect
7039-
),
7040-
.SWBINTTODO("Building sample package causes a backtrace on linux"),
7041-
.requireSwift6_2,
7042-
.requiresSwiftConcurrencySupport,
7043-
.tags(
7044-
.Feature.Command.Package.CommandPlugin,
7045-
),
7046-
// arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms),
7047-
arguments: getBuildData(for: [.swiftbuild]),
7048-
)
7049-
func commandPluginCompilationErrorSwiftBuild(
7050-
data: BuildData,
7051-
) async throws {
7052-
// Once this is fix, merge data iunto commandPluginCompilationError
7053-
await withKnownIssue {
7054-
try await Self.commandPluginCompilationErrorImplementation(data: data)
7055-
}
7056-
}
7057-
70587025
@Test(
70597026
.requiresSwiftConcurrencySupport,
70607027
.tags(

0 commit comments

Comments
 (0)