Skip to content

Commit 6cb8f61

Browse files
authored
Improve handling of missing plugins (#5662)
- if a plugin is referenced by its product name in the same package, automatically use the target instead - if a plugin referenced in the same package doesn't exist, emit an error fixes #5650 rdar://96664671 (cherry picked from commit 92dcfa0)
1 parent 94deafb commit 6cb8f61

File tree

10 files changed

+123
-5
lines changed

10 files changed

+123
-5
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// swift-tools-version: 5.7
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "MissingPlugin",
7+
targets: [
8+
.target(name: "MissingPlugin", plugins: ["NonExistingPlugin"]),
9+
]
10+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
public struct MissingPlugin {
2+
public private(set) var text = "Hello, World!"
3+
4+
public init() {
5+
}
6+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// swift-tools-version: 5.7
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "PluginCanBeReferencedByProductName",
7+
products: [
8+
.plugin(name: "MyPluginProduct", targets: ["MyPlugin"]),
9+
],
10+
targets: [
11+
.target(name: "PluginCanBeReferencedByProductName", plugins: ["MyPluginProduct"]),
12+
.executableTarget(name: "Exec"),
13+
.plugin(name: "MyPlugin", capability: .buildTool(), dependencies: ["Exec"]),
14+
]
15+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import PackagePlugin
2+
3+
@main
4+
struct MyPlugin: BuildToolPlugin {
5+
func createBuildCommands(context: PackagePlugin.PluginContext, target: PackagePlugin.Target) async throws -> [PackagePlugin.Command] {
6+
let output = context.pluginWorkDirectory.appending("gen.swift")
7+
return [
8+
.buildCommand(displayName: "Generating code",
9+
executable: try context.tool(named: "Exec").path,
10+
arguments: [output.string],
11+
outputFiles: [output])
12+
]
13+
}
14+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import Foundation
2+
3+
let output = ProcessInfo.processInfo.arguments[1]
4+
try "let stringConstant = \"Hello, World!\"".write(to: URL(fileURLWithPath: output), atomically: true, encoding: .utf8)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
public struct PluginCanBeReferencedByProductName {
2+
public private(set) var text = stringConstant
3+
4+
public init() {
5+
}
6+
}

Sources/PackageLoading/Diagnostics.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ extension Basics.Diagnostic {
8282
.error("executable product '\(product)' should not have more than one executable target")
8383
}
8484

85+
static func pluginNotFound(name: String) -> Self {
86+
.error("no plugin named '\(name)' found")
87+
}
88+
8589
static func pluginProductWithNoTargets(product: String) -> Self {
8690
.error("plugin product '\(product)' should have at least one plugin target")
8791
}

Sources/PackageLoading/PackageBuilder.swift

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,17 @@ public final class PackageBuilder {
557557
throw ModuleError.moduleNotFound(missingModuleName, type)
558558
}
559559

560+
let products = Dictionary(manifest.products.map({ ($0.name, $0) }), uniquingKeysWith: { $1 })
561+
562+
// If there happens to be a plugin product with the right name in the same package, we want to use that automatically.
563+
func pluginTargetName(for productName: String) -> String? {
564+
if let product = products[productName], product.type == .plugin {
565+
return product.targets.first
566+
} else {
567+
return nil
568+
}
569+
}
570+
560571
let potentialModuleMap = Dictionary(potentialModules.map({ ($0.name, $0) }), uniquingKeysWith: { $1 })
561572
let successors: (PotentialModule) -> [PotentialModule] = {
562573
// No reference of this target in manifest, i.e. it has no dependencies.
@@ -580,8 +591,16 @@ public final class PackageBuilder {
580591
if let pluginUsages = target.pluginUsages {
581592
successors += pluginUsages.compactMap({
582593
switch $0 {
583-
case .plugin(let name, let package):
584-
return (package == nil) ? potentialModuleMap[name] : nil
594+
case .plugin(_, .some(_)):
595+
return nil
596+
case .plugin(let name, nil):
597+
if let potentialModule = potentialModuleMap[name] {
598+
return potentialModule
599+
} else if let targetName = pluginTargetName(for: name), let potentialModule = potentialModuleMap[targetName] {
600+
return potentialModule
601+
} else {
602+
return nil
603+
}
585604
}
586605
})
587606
}
@@ -649,8 +668,15 @@ public final class PackageBuilder {
649668
return .product(Target.ProductReference(name: name, package: package), conditions: [])
650669
}
651670
else {
652-
guard let target = targets[name] else { return nil }
653-
return .target(target, conditions: [])
671+
if let target = targets[name] {
672+
return .target(target, conditions: [])
673+
} else if let targetName = pluginTargetName(for: name), let target = targets[targetName] {
674+
return .target(target, conditions: [])
675+
} else {
676+
self.observabilityScope.emit(.pluginNotFound(name: name))
677+
return nil
678+
}
679+
654680
}
655681
}
656682
}

Sources/PackageModel/Manifest.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ public final class Manifest {
214214

215215
/// Returns the targets required for building the provided products.
216216
public func targetsRequired(for products: [ProductDescription]) -> [TargetDescription] {
217+
let productsByName = Dictionary(products.map({ ($0.name, $0) }), uniquingKeysWith: { $1 })
217218
let targetsByName = Dictionary(targets.map({ ($0.name, $0) }), uniquingKeysWith: { $1 })
218219
let productTargetNames = products.flatMap({ $0.targets })
219220

@@ -233,7 +234,13 @@ public final class Manifest {
233234
let plugins: [String] = target.pluginUsages?.compactMap { pluginUsage in
234235
switch pluginUsage {
235236
case .plugin(name: let name, package: nil):
236-
return targetsByName.keys.contains(name) ? name : nil
237+
if targetsByName.keys.contains(name) {
238+
return name
239+
} else if let targetName = productsByName[name]?.targets.first {
240+
return targetName
241+
} else {
242+
return nil
243+
}
237244
default:
238245
return nil
239246
}

Tests/FunctionalTests/PluginTests.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,4 +911,30 @@ class PluginTests: XCTestCase {
911911
XCTAssert(stdout.contains("Build complete!"), "stdout:\n\(stdout)")
912912
}
913913
}
914+
915+
func testMissingPlugin() throws {
916+
// Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require).
917+
try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency")
918+
919+
try fixture(name: "Miscellaneous/Plugins") { fixturePath in
920+
do {
921+
try executeSwiftBuild(fixturePath.appending(component: "MissingPlugin"))
922+
} catch SwiftPMProductError.executionFailure(_, _, let stderr) {
923+
XCTAssert(stderr.contains("error: 'missingplugin': no plugin named 'NonExistingPlugin' found"), "stderr:\n\(stderr)")
924+
}
925+
}
926+
}
927+
928+
func testPluginCanBeReferencedByProductName() throws {
929+
// Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require).
930+
try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency")
931+
932+
try fixture(name: "Miscellaneous/Plugins") { fixturePath in
933+
let (stdout, _) = try executeSwiftBuild(fixturePath.appending(component: "PluginCanBeReferencedByProductName"))
934+
XCTAssert(stdout.contains("Compiling plugin MyPlugin..."), "stdout:\n\(stdout)")
935+
XCTAssert(stdout.contains("Compiling PluginCanBeReferencedByProductName gen.swift"), "stdout:\n\(stdout)")
936+
XCTAssert(stdout.contains("Compiling PluginCanBeReferencedByProductName PluginCanBeReferencedByProductName.swift"), "stdout:\n\(stdout)")
937+
XCTAssert(stdout.contains("Build complete!"), "stdout:\n\(stdout)")
938+
}
939+
}
914940
}

0 commit comments

Comments
 (0)