Skip to content

Commit 921daeb

Browse files
committed
Fix crash in PIF generation
Fix a crash when unwrapping an optional 'mainModule' of a module. In the case where a plugin has a dependency on a 'binaryTarget' there is no main module. This only presents itself when you have defined the binaryTarget as both a product and target. * Update method that determines plugin dependencies to handle binaryTarget types explicitly. * Add a PIF generation test that captures this behavior.
1 parent 3cdae8e commit 921daeb

File tree

8 files changed

+159
-0
lines changed

8 files changed

+159
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"schemaVersion": "1.0",
3+
"artifacts": {
4+
"mytool": {
5+
"type": "executable",
6+
"version": "1.2.3",
7+
"variants": [
8+
{
9+
"path": "mytool-macos/mytool",
10+
"supportedTriples": ["x86_64-apple-macosx", "arm64-apple-macosx"]
11+
},
12+
{
13+
"path": "mytool-linux/mytool",
14+
"supportedTriples": ["x86_64-unknown-linux-gnu"]
15+
}
16+
]
17+
}
18+
}
19+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/bin/bash
2+
3+
print_usage() {
4+
echo "usage: ${0##*/} [--verbose] <in> <out>"
5+
}
6+
7+
# Parse arguments until we find '--' or an argument that isn't an option.
8+
until [ $# -eq 0 ]
9+
do
10+
case "$1" in
11+
--verbose) verbose=1; shift;;
12+
--) shift; break;;
13+
-*) echo "unknown option: ${1}"; print_usage; exit 1; shift;;
14+
*) break;;
15+
esac
16+
done
17+
18+
# Print usage and leave if we don't have exactly two arguments.
19+
if [ $# -ne 2 ]; then
20+
print_usage
21+
exit 1
22+
fi
23+
24+
# For our sample tool we just copy from one to the other.
25+
if [ $verbose != 0 ]; then
26+
echo "[${0##*/}-linux] '$1' '$2'"
27+
fi
28+
29+
cp "$1" "$2"
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/bin/bash
2+
3+
print_usage() {
4+
echo "usage: ${0##*/} [--verbose] <in> <out>"
5+
}
6+
7+
# Parse arguments until we find '--' or an argument that isn't an option.
8+
until [ $# -eq 0 ]
9+
do
10+
case "$1" in
11+
--verbose) verbose=1; shift;;
12+
--) shift; break;;
13+
-*) echo "unknown option: ${1}"; print_usage; exit 1; shift;;
14+
*) break;;
15+
esac
16+
done
17+
18+
# Print usage and leave if we don't have exactly two arguments.
19+
if [ $# -ne 2 ]; then
20+
print_usage
21+
exit 1
22+
fi
23+
24+
# For our sample tool we just copy from one to the other.
25+
if [ $verbose != 0 ]; then
26+
echo "[${0##*/}-macosx] '$1' '$2'"
27+
fi
28+
29+
cp "$1" "$2"
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// swift-tools-version: 5.6
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "MyBinaryTargetExePlugin",
6+
7+
products: [
8+
.executable(
9+
name: "MyPluginExe",
10+
targets: ["MyPluginExe"]
11+
),
12+
.plugin(
13+
name: "MyPlugin",
14+
targets: ["MyPlugin"]
15+
),
16+
.executable(
17+
name: "MyBinaryTargetExe",
18+
targets: ["MyBinaryTargetExeArtifactBundle"]
19+
),
20+
],
21+
targets: [
22+
.executableTarget(
23+
name: "MyPluginExe",
24+
dependencies: [],
25+
exclude: [],
26+
),
27+
28+
.plugin(
29+
name: "MyPlugin",
30+
capability: .buildTool(),
31+
dependencies: ["MyPluginExe", "MyBinaryTargetExeArtifactBundle"]
32+
),
33+
.binaryTarget(
34+
name: "MyBinaryTargetExeArtifactBundle",
35+
path: "Dependency/MyBinaryTargetExeArtifactBundle.artifactbundle"
36+
),
37+
]
38+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import PackagePlugin
2+
3+
@main
4+
struct MyPlugin: BuildToolPlugin {
5+
6+
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
7+
print("Hello from MyPlugin!")
8+
}
9+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("It's Me MyPluginExe\n")

Sources/SwiftBuildSupport/PackagePIFBuilder+Helpers.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,12 @@ extension PackageGraph.ResolvedModule {
513513

514514
func productRepresentingDependencyOfBuildPlugin(in mainModuleProducts: [ResolvedProduct]) -> ResolvedProduct? {
515515
mainModuleProducts.only { (mainModuleProduct: ResolvedProduct) -> Bool in
516+
// Handle binary-only executable products that don't have a main module, i.e. binaryTarget
517+
guard let mainModule = mainModuleProduct.mainModule else {
518+
return mainModuleProduct.type == .executable &&
519+
mainModuleProduct.modules.only?.type == .binary &&
520+
mainModuleProduct.modules.only?.name == self.name
521+
}
516522
// NOTE: We can't use the 'id' here as we need to explicitly ignore the build triple because our build
517523
// triple will be '.tools' while the target we want to depend on will have a build triple of '.destination'.
518524
// See for more details:

Tests/SwiftBuildSupportTests/PIFBuilderTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,32 @@ struct PIFBuilderTests {
135135
#expect(releaseConfig.settings.platformSpecificSettings[.windows]?[.SWIFT_ACTIVE_COMPILATION_CONDITIONS] == ["$(inherited)"])
136136
}
137137
}
138+
139+
@Test func pluginWithBinaryTargetDependency() async throws {
140+
try await withGeneratedPIF(fromFixture: "Miscellaneous/Plugins/BinaryTargetExePlugin") { pif, observabilitySystem in
141+
// Verify that PIF generation succeeds for a package with a plugin that depends on a binary target
142+
#expect(pif.workspace.projects.count > 0)
143+
144+
let project = try pif.workspace.project(named: "MyBinaryTargetExePlugin")
145+
146+
// Verify the plugin target exists
147+
let pluginTarget = try project.target(named: "MyPlugin")
148+
#expect(pluginTarget.common.name == "MyPlugin")
149+
150+
// Verify the executable target that uses the plugin exists
151+
let executableTarget = try project.target(named: "MyPluginExe")
152+
#expect(executableTarget.common.name == "MyPluginExe")
153+
154+
// Verify no errors were emitted during PIF generation
155+
let errors = observabilitySystem.diagnostics.filter { $0.severity == .error }
156+
#expect(errors.isEmpty, "Expected no errors during PIF generation, but got: \(errors)")
157+
158+
// Verify that the plugin target has a dependency (binary targets are handled differently in PIF)
159+
// The key test is that PIF generation succeeds without errors when a plugin depends on a binary target
160+
let binaryArtifactMessages = observabilitySystem.diagnostics.filter {
161+
$0.message.contains("found binary artifact")
162+
}
163+
#expect(binaryArtifactMessages.count > 0, "Expected to find binary artifact processing messages")
164+
}
165+
}
138166
}

0 commit comments

Comments
 (0)