Skip to content

Commit 9b9b76a

Browse files
committed
Add diagnostics for unsupported plugin dependency
Resolves rdar://95117424
1 parent d98de63 commit 9b9b76a

File tree

3 files changed

+199
-2
lines changed

3 files changed

+199
-2
lines changed

Sources/PackageGraph/PackageGraph+Loading.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,8 +791,10 @@ private final class ResolvedTargetBuilder: ResolvedBuilder<ResolvedTarget> {
791791
let dependencies = try self.dependencies.map { dependency -> ResolvedTarget.Dependency in
792792
switch dependency {
793793
case .target(let targetBuilder, let conditions):
794+
try self.target.validateDependency(target: targetBuilder.target)
794795
return .target(try targetBuilder.construct(), conditions: conditions)
795796
case .product(let productBuilder, let conditions):
797+
try self.target.validateDependency(product: productBuilder.product, productPackage: productBuilder.packageBuilder.package.identity)
796798
let product = try productBuilder.construct()
797799
if !productBuilder.packageBuilder.isAllowedToVendUnsafeProducts {
798800
try self.diagnoseInvalidUseOfUnsafeFlags(product)
@@ -810,6 +812,19 @@ private final class ResolvedTargetBuilder: ResolvedBuilder<ResolvedTarget> {
810812
}
811813
}
812814

815+
extension Target {
816+
817+
func validateDependency(target: Target) throws {
818+
if self.type == .plugin && target.type == .library {
819+
throw PackageGraphError.unsupported(targetName: self.name, targetType: self.type.rawValue, dependencyName: target.name, dependencyType: target.type.rawValue, dependencyPackage: nil)
820+
}
821+
}
822+
func validateDependency(product: Product, productPackage: PackageIdentity) throws {
823+
if self.type == .plugin && product.type.isLibrary {
824+
throw PackageGraphError.unsupported(targetName: self.name, targetType: self.type.rawValue, dependencyName: product.name, dependencyType: product.type.description, dependencyPackage: productPackage.description)
825+
}
826+
}
827+
}
813828
/// Builder for resolved package.
814829
private final class ResolvedPackageBuilder: ResolvedBuilder<ResolvedPackage> {
815830

Sources/PackageGraph/PackageGraph.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ enum PackageGraphError: Swift.Error {
3535
targetName: String,
3636
packageIdentifier: String
3737
)
38-
38+
/// Dependency between a given target of its type and a dependent target/product of its type is unsupported
39+
case unsupported(targetName: String, targetType: String, dependencyName: String, dependencyType: String, dependencyPackage: String?)
3940
/// A product was found in multiple packages.
4041
case duplicateProduct(product: String, packages: [String])
4142

@@ -235,6 +236,12 @@ extension PackageGraphError: CustomStringConvertible {
235236
return "multiple aliases: ['\(aliases.joined(separator: "', '"))'] found for target '\(target)' in product '\(product)' from package '\(package)'"
236237
case .invalidSourcesForModuleAliasing(let target, let product, let package):
237238
return "module aliasing can only be used for Swift based targets; non-Swift sources found in target '\(target)' for product '\(product)' from package '\(package)'"
239+
case .unsupported(let targetName, let targetType, let dependencyName, let dependencyType, let dependencyPackage):
240+
var trailingMsg = ""
241+
if let depPkg = dependencyPackage {
242+
trailingMsg = " from package '\(depPkg)'"
243+
}
244+
return "target '\(targetName)' of type '\(targetType)' cannot depend on '\(dependencyName)' of type '\(dependencyType)'\(trailingMsg); this dependency is unsupported"
238245
}
239246
}
240247
}

Tests/SPMBuildCoreTests/PluginInvocationTests.swift

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import Basics
14-
import PackageGraph
14+
@testable import PackageGraph
1515
import PackageLoading
1616
import PackageModel
1717
@testable import SPMBuildCore
@@ -579,4 +579,179 @@ class PluginInvocationTests: XCTestCase {
579579
}
580580
}
581581
}
582+
583+
func testUnsupportedDependencyProduct() throws {
584+
try testWithTemporaryDirectory { tmpPath in
585+
// Create a sample package with a library target and a plugin.
586+
let packageDir = tmpPath.appending(components: "MyPackage")
587+
try localFileSystem.createDirectory(packageDir, recursive: true)
588+
try localFileSystem.writeFileContents(packageDir.appending(component: "Package.swift"), string: """
589+
// swift-tools-version: 5.7
590+
import PackageDescription
591+
let package = Package(
592+
name: "MyPackage",
593+
dependencies: [
594+
.package(path: "../FooPackage"),
595+
],
596+
targets: [
597+
.plugin(
598+
name: "MyPlugin",
599+
capability: .buildTool(),
600+
dependencies: [
601+
.product(name: "FooLib", package: "FooPackage"),
602+
]
603+
),
604+
]
605+
)
606+
""")
607+
608+
let myPluginTargetDir = packageDir.appending(components: "Plugins", "MyPlugin")
609+
try localFileSystem.createDirectory(myPluginTargetDir, recursive: true)
610+
try localFileSystem.writeFileContents(myPluginTargetDir.appending(component: "plugin.swift"), string: """
611+
import PackagePlugin
612+
import Foo
613+
@main struct MyBuildToolPlugin: BuildToolPlugin {
614+
func createBuildCommands(
615+
context: PluginContext,
616+
target: Target
617+
) throws -> [Command] { }
618+
}
619+
""")
620+
621+
let fooPkgDir = tmpPath.appending(components: "FooPackage")
622+
try localFileSystem.createDirectory(fooPkgDir, recursive: true)
623+
try localFileSystem.writeFileContents(fooPkgDir.appending(component: "Package.swift"), string: """
624+
// swift-tools-version: 5.7
625+
import PackageDescription
626+
let package = Package(
627+
name: "FooPackage",
628+
products: [
629+
.library(name: "FooLib",
630+
targets: ["Foo"]),
631+
],
632+
targets: [
633+
.target(
634+
name: "Foo",
635+
dependencies: []
636+
),
637+
]
638+
)
639+
""")
640+
let fooTargetDir = fooPkgDir.appending(components: "Sources", "Foo")
641+
try localFileSystem.createDirectory(fooTargetDir, recursive: true)
642+
try localFileSystem.writeFileContents(fooTargetDir.appending(component: "file.swift"), string: """
643+
public func foo() { }
644+
""")
645+
646+
// Load a workspace from the package.
647+
let observability = ObservabilitySystem.makeForTesting()
648+
let workspace = try Workspace(
649+
fileSystem: localFileSystem,
650+
forRootPackage: packageDir,
651+
customManifestLoader: ManifestLoader(toolchain: UserToolchain.default),
652+
delegate: MockWorkspaceDelegate()
653+
)
654+
655+
// Load the root manifest.
656+
let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: [])
657+
let rootManifests = try tsc_await {
658+
workspace.loadRootManifests(
659+
packages: rootInput.packages,
660+
observabilityScope: observability.topScope,
661+
completion: $0
662+
)
663+
}
664+
XCTAssert(rootManifests.count == 1, "\(rootManifests)")
665+
666+
// Load the package graph.
667+
XCTAssertThrowsError(try workspace.loadPackageGraph(rootInput: rootInput, observabilityScope: observability.topScope)) { error in
668+
var diagnosed = false
669+
if let realError = error as? PackageGraphError,
670+
realError.description == "target 'MyPlugin' of type 'plugin' cannot depend on 'FooLib' of type 'library' from package 'foopackage'; this dependency is unsupported" {
671+
diagnosed = true
672+
}
673+
XCTAssertTrue(diagnosed)
674+
}
675+
}
676+
}
677+
678+
func testUnsupportedDependencyTarget() throws {
679+
try testWithTemporaryDirectory { tmpPath in
680+
// Create a sample package with a library target and a plugin.
681+
let packageDir = tmpPath.appending(components: "MyPackage")
682+
try localFileSystem.createDirectory(packageDir, recursive: true)
683+
try localFileSystem.writeFileContents(packageDir.appending(component: "Package.swift"), string: """
684+
// swift-tools-version: 5.7
685+
import PackageDescription
686+
let package = Package(
687+
name: "MyPackage",
688+
dependencies: [
689+
.package(path: "../FooPackage"),
690+
],
691+
targets: [
692+
.target(
693+
name: "MyLibrary",
694+
dependencies: []
695+
),
696+
.plugin(
697+
name: "MyPlugin",
698+
capability: .buildTool(),
699+
dependencies: [
700+
"MyLibrary",
701+
.product(name: "FooLib", package: "FooPackage"),
702+
]
703+
),
704+
]
705+
)
706+
""")
707+
708+
let myLibraryTargetDir = packageDir.appending(components: "Sources", "MyLibrary")
709+
try localFileSystem.createDirectory(myLibraryTargetDir, recursive: true)
710+
try localFileSystem.writeFileContents(myLibraryTargetDir.appending(component: "library.swift"), string: """
711+
public func hello() { }
712+
""")
713+
let myPluginTargetDir = packageDir.appending(components: "Plugins", "MyPlugin")
714+
try localFileSystem.createDirectory(myPluginTargetDir, recursive: true)
715+
try localFileSystem.writeFileContents(myPluginTargetDir.appending(component: "plugin.swift"), string: """
716+
import PackagePlugin
717+
import MyLibrary
718+
@main struct MyBuildToolPlugin: BuildToolPlugin {
719+
func createBuildCommands(
720+
context: PluginContext,
721+
target: Target
722+
) throws -> [Command] { }
723+
}
724+
""")
725+
726+
// Load a workspace from the package.
727+
let observability = ObservabilitySystem.makeForTesting()
728+
let workspace = try Workspace(
729+
fileSystem: localFileSystem,
730+
forRootPackage: packageDir,
731+
customManifestLoader: ManifestLoader(toolchain: UserToolchain.default),
732+
delegate: MockWorkspaceDelegate()
733+
)
734+
735+
// Load the root manifest.
736+
let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: [])
737+
let rootManifests = try tsc_await {
738+
workspace.loadRootManifests(
739+
packages: rootInput.packages,
740+
observabilityScope: observability.topScope,
741+
completion: $0
742+
)
743+
}
744+
XCTAssert(rootManifests.count == 1, "\(rootManifests)")
745+
746+
// Load the package graph.
747+
XCTAssertThrowsError(try workspace.loadPackageGraph(rootInput: rootInput, observabilityScope: observability.topScope)) { error in
748+
var diagnosed = false
749+
if let realError = error as? PackageGraphError,
750+
realError.description == "target 'MyPlugin' of type 'plugin' cannot depend on 'MyLibrary' of type 'library'; this dependency is unsupported" {
751+
diagnosed = true
752+
}
753+
XCTAssertTrue(diagnosed)
754+
}
755+
}
756+
}
582757
}

0 commit comments

Comments
 (0)