Skip to content

Commit 1e72ef8

Browse files
authored
Don't link plugin dependencies to products (#6695)
This fixes two issues with plugins and their dependencies: - we were traversing plugin products and including their targets as part of any product - since we are including plugin targets as dependencies as well, we could traverse plugin targets in a similarly incorrect way
1 parent 127edb9 commit 1e72ef8

File tree

8 files changed

+88
-3
lines changed

8 files changed

+88
-3
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// swift-tools-version:5.9
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "111920845-sample",
6+
platforms: [
7+
.macOS(.v10_15), // example uses swift concurrency which is only available in 10.15 or newer
8+
],
9+
products: [
10+
.executable(name: "MyPluginExecutable", targets: ["MyPluginExecutable"]),
11+
.plugin(name: "MyPlugin", targets: ["MyPlugin"]),
12+
],
13+
targets: [
14+
.executableTarget(name: "MyPluginExecutable"),
15+
.plugin(name: "MyPlugin", capability: .buildTool, dependencies: ["MyPluginExecutable"]),
16+
17+
.target(name: "MyLibrary", plugins: ["MyPlugin"]),
18+
.executableTarget(name: "MyExecutable", dependencies: ["MyLibrary"]),
19+
.testTarget(name: "MyExecutableTests", dependencies: ["MyExecutable"]),
20+
]
21+
)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import PackagePlugin
2+
3+
@main
4+
struct MyPlugin: BuildToolPlugin {
5+
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
6+
return [.buildCommand(
7+
displayName: "Running MyPluginExecutable",
8+
executable: try context.tool(named: "MyPluginExecutable").path,
9+
arguments: [],
10+
environment: [:],
11+
inputFiles: [],
12+
outputFiles: []
13+
)]
14+
}
15+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import MyLibrary
2+
3+
@main
4+
struct MyExecutable {
5+
static func main() async throws {
6+
print("Hello from executable.")
7+
}
8+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@main
2+
struct MyExecutable {
3+
static func main() async throws {
4+
print("Hello from plugin executable.")
5+
}
6+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import XCTest
2+
@testable import MyExecutable
3+
4+
final class MyExecutableTests: XCTestCase {
5+
func test() throws {
6+
XCTAssertTrue(true)
7+
}
8+
}

Sources/Build/BuildPlan.swift

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -695,25 +695,42 @@ public class BuildPlan: SPMBuildCore.BuildPlan {
695695
libraryBinaryPaths: Set<AbsolutePath>,
696696
availableTools: [String: AbsolutePath]
697697
) {
698+
/* Prior to tools-version 5.9, we used to errorneously recursively traverse plugin dependencies and statically include their
699+
targets. For compatibility reasons, we preserve that behavior for older tools-versions. */
700+
let shouldExcludePlugins: Bool
701+
if let toolsVersion = self.graph.package(for: product)?.manifest.toolsVersion {
702+
shouldExcludePlugins = toolsVersion >= .v5_9
703+
} else {
704+
shouldExcludePlugins = false
705+
}
698706

699707
// Sort the product targets in topological order.
700708
let nodes: [ResolvedTarget.Dependency] = product.targets.map { .target($0, conditions: []) }
701709
let allTargets = try topologicalSort(nodes, successors: { dependency in
702710
switch dependency {
703711
// Include all the dependencies of a target.
704712
case .target(let target, _):
713+
if target.type == .macro {
714+
return []
715+
}
716+
if shouldExcludePlugins, target.type == .plugin {
717+
return []
718+
}
705719
return target.dependencies.filter { $0.satisfies(self.buildEnvironment) }
706720

707721
// For a product dependency, we only include its content only if we
708-
// need to statically link it or if it's a plugin.
722+
// need to statically link it.
709723
case .product(let product, _):
710724
guard dependency.satisfies(self.buildEnvironment) else {
711725
return []
712726
}
713727

728+
let productDependencies: [ResolvedTarget.Dependency] = product.targets.map { .target($0, conditions: []) }
714729
switch product.type {
715-
case .library(.automatic), .library(.static), .plugin:
716-
return product.targets.map { .target($0, conditions: []) }
730+
case .library(.automatic), .library(.static):
731+
return productDependencies
732+
case .plugin:
733+
return shouldExcludePlugins ? [] : productDependencies
717734
case .library(.dynamic), .test, .executable, .snippet, .macro:
718735
return []
719736
}

Tests/FunctionalTests/PluginTests.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -969,6 +969,15 @@ class PluginTests: XCTestCase {
969969
}
970970
}
971971

972+
func testIncorrectDependencies() throws {
973+
try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency")
974+
975+
try fixture(name: "Miscellaneous/Plugins") { path in
976+
let (stdout, stderr) = try executeSwiftBuild(path.appending("IncorrectDependencies"), extraArgs: ["--build-tests"])
977+
XCTAssert(stdout.contains("Build complete!"), "output:\n\(stderr)\n\(stdout)")
978+
}
979+
}
980+
972981
func testSandboxViolatingBuildToolPluginCommands() throws {
973982
#if !os(macOS)
974983
try XCTSkipIf(true, "sandboxing tests are only supported on macOS")

0 commit comments

Comments
 (0)