Skip to content

Commit 7708df4

Browse files
committed
Fix a bug that caused a plugin dependency on a local executable to not work properly
Motivation: This bug affects how packages that vend plugins that rely on built tools can be structured (remote tool dependencies work but local dependencies do not, without this fix). Changes: - pass `.product` instead of `.target` to make sure that the executable is linked and not just compiled - add a unit test that checks remote and local tools, with both implicitly and explicitly defined products for the local case rdar://89267060
1 parent 147a591 commit 7708df4

File tree

13 files changed

+153
-3
lines changed

13 files changed

+153
-3
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// swift-tools-version: 5.6
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "MyLibrary",
6+
dependencies: [
7+
.package(path: "../MyPlugin")
8+
],
9+
targets: [
10+
.target(
11+
name: "MyLibrary"
12+
),
13+
.testTarget(
14+
name: "MyLibraryTests",
15+
dependencies: ["MyLibrary"]
16+
)
17+
]
18+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
public func Foo() -> String {
2+
return "Foo"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import XCTest
2+
import MyLibrary
3+
4+
final class MyLibraryTests: XCTestCase {
5+
6+
func testLibrary() throws {
7+
XCTAssertEqual(Foo(), "Foo")
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
public func LocalToolHelperFunction() -> String {
2+
return "local"
3+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// swift-tools-version: 5.6
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "MyPlugin",
6+
products: [
7+
.plugin(
8+
name: "MyPlugin",
9+
targets: ["MyPlugin"]
10+
),
11+
],
12+
dependencies: [
13+
.package(path: "../RemoteTool"),
14+
],
15+
targets: [
16+
.plugin(
17+
name: "MyPlugin",
18+
capability: .command(
19+
intent: .custom(
20+
verb: "my-plugin",
21+
description: "Tester plugin"
22+
)
23+
),
24+
dependencies: [
25+
.product(name: "RemoteTool", package: "RemoteTool"),
26+
"LocalTool",
27+
"ImpliedLocalTool",
28+
]
29+
),
30+
.executableTarget(
31+
name: "LocalTool",
32+
dependencies: ["LocalToolHelperLibrary"],
33+
path: "Tools/LocalTool"
34+
),
35+
.executableTarget(
36+
name: "ImpliedLocalTool",
37+
dependencies: ["LocalToolHelperLibrary"],
38+
path: "Tools/ImpliedLocalTool"
39+
),
40+
.target(
41+
name: "LocalToolHelperLibrary",
42+
path: "Libraries/LocalToolHelperLibrary"
43+
),
44+
]
45+
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import PackagePlugin
2+
import Foundation
3+
4+
@main
5+
struct MyPlugin: CommandPlugin {
6+
func performCommand(context: PluginContext, arguments: [String]) async throws {
7+
for name in ["RemoteTool", "LocalTool", "ImpliedLocalTool"] {
8+
let tool = try context.tool(named: name)
9+
print("tool path is \(tool.path)")
10+
11+
do {
12+
let process = Process()
13+
process.executableURL = URL(fileURLWithPath: tool.path.string)
14+
try process.run()
15+
}
16+
catch {
17+
print("error: \(error)")
18+
}
19+
}
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import LocalToolHelperLibrary
2+
3+
print("A message from the implied \(LocalToolHelperFunction()) tool.")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import LocalToolHelperLibrary
2+
3+
print("A message from the \(LocalToolHelperFunction()) tool.")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
public func RemoteToolHelperLibraryFunction() -> String {
2+
return "remote"
3+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// swift-tools-version: 5.6
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "RemoteTool",
6+
products: [
7+
.executable(
8+
name: "RemoteTool",
9+
targets: ["RemoteTool"]
10+
),
11+
],
12+
targets: [
13+
.executableTarget(
14+
name: "RemoteTool",
15+
dependencies: ["RemoteToolHelperLibrary"],
16+
path: "Tools/RemoteTool"
17+
),
18+
.target(
19+
name: "RemoteToolHelperLibrary",
20+
path: "Libraries/RemoteToolHelperLibrary"
21+
)
22+
]
23+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import RemoteToolHelperLibrary
2+
3+
print("A message from the \(RemoteToolHelperLibraryFunction()) tool.")

Sources/Commands/SwiftPackageTool.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,9 +1044,9 @@ extension SwiftPackageTool {
10441044
toolNamesToPaths[exec.name] = exec.executablePath
10451045
}
10461046
}
1047-
else {
1048-
// Build the target referenced by the tool, and add the executable to the tool map.
1049-
try buildOperation.build(subset: .target(target.name))
1047+
else {
1048+
// Build the product referenced by the tool, and add the executable to the tool map. Product dependencies are not supported within a package, so we instead find the executable that corresponds to the product. There is always one, because of autogeneration of implicit executables with the same name as the target if there isn't an explicit one.
1049+
try buildOperation.build(subset: .product(target.name))
10501050
if let builtTool = buildOperation.buildPlan?.buildProducts.first(where: { $0.product.name == target.name}) {
10511051
toolNamesToPaths[target.name] = builtTool.binary
10521052
}

Tests/FunctionalTests/PluginTests.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,22 @@ class PluginTests: XCTestCase {
471471
}
472472
}
473473

474+
func testLocalAndRemoteToolDependencies() throws {
475+
// Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require).
476+
try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency")
477+
478+
try fixture(name: "Miscellaneous/Plugins/PluginUsingLocalAndRemoteTool") { path in
479+
let (stdout, stderr) = try executeSwiftPackage(path.appending(component: "MyLibrary"), configuration: .Debug, extraArgs: ["plugin", "my-plugin"])
480+
XCTAssert(stderr.contains("Linking RemoteTool"), "stdout:\n\(stderr)\n\(stdout)")
481+
XCTAssert(stderr.contains("Linking LocalTool"), "stdout:\n\(stderr)\n\(stdout)")
482+
XCTAssert(stderr.contains("Linking ImpliedLocalTool"), "stdout:\n\(stderr)\n\(stdout)")
483+
XCTAssert(stderr.contains("Build complete!"), "stdout:\n\(stderr)\n\(stdout)")
484+
XCTAssert(stdout.contains("A message from the remote tool."), "stdout:\n\(stderr)\n\(stdout)")
485+
XCTAssert(stdout.contains("A message from the local tool."), "stdout:\n\(stderr)\n\(stdout)")
486+
XCTAssert(stdout.contains("A message from the implied local tool."), "stdout:\n\(stderr)\n\(stdout)")
487+
}
488+
}
489+
474490
func testCommandPluginCancellation() throws {
475491
// Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require).
476492
try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency")

0 commit comments

Comments
 (0)