Skip to content

Commit cdc2e95

Browse files
Fix a bug where an executable target used by a plugin might not get linked before the plugin needs it (#3858)
* Implemented fix by synthesizing a product for any executable that a plug‐in target depends on (funnelling it into the same code that does the synthesis for executable targets in the root package). Without this, the plugin target would depend only on the executable target (compiling the code) but not on the product (linking it into an executable). * Added tests.
1 parent 71f881c commit cdc2e95

File tree

15 files changed

+238
-30
lines changed

15 files changed

+238
-30
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// swift-tools-version: 5.5
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "ClientOfPluginWithInternalExecutable",
6+
dependencies: [
7+
.package(path: "../PluginWithInternalExecutable")
8+
],
9+
targets: [
10+
.executableTarget(
11+
name: "RootTarget",
12+
plugins: [
13+
.plugin(name: "PluginScriptProduct", package: "PluginWithInternalExecutable")
14+
]
15+
),
16+
]
17+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
I am Foo!
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("Generated string Foo: '\(foo)'")
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// swift-tools-version: 5.5
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "InvalidUseOfInternalPluginExecutable",
6+
dependencies: [
7+
.package(path: "../PluginWithInternalExecutable")
8+
],
9+
targets: [
10+
.executableTarget(
11+
name: "RootTarget",
12+
dependencies: [
13+
.product(name: "PluginExecutable", package: "PluginWithInternalExecutable")
14+
],
15+
plugins: [
16+
.plugin(name: "PluginScriptProduct", package: "PluginWithInternalExecutable")
17+
]
18+
),
19+
]
20+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
I am Foo!
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("Generated string Foo: '\(foo)'")
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// swift-tools-version: 5.5
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "PluginWithInternalExecutable",
6+
products: [
7+
.plugin(
8+
name: "PluginScriptProduct",
9+
targets: ["PluginScriptTarget"]
10+
),
11+
],
12+
targets: [
13+
.plugin(
14+
name: "PluginScriptTarget",
15+
capability: .buildTool(),
16+
dependencies: [
17+
"PluginExecutable",
18+
]
19+
),
20+
.executableTarget(
21+
name: "PluginExecutable"
22+
),
23+
]
24+
)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import PackagePlugin
2+
3+
@main
4+
struct PluginScript: BuildToolPlugin {
5+
6+
func createBuildCommands(context: TargetBuildContext) throws -> [Command] {
7+
print("Hello from the plugin script!")
8+
9+
return try context.inputFiles.map{ $0.path }.compactMap {
10+
guard $0.extension == "dat" else { return .none }
11+
let outputName = $0.stem + ".swift"
12+
let outputPath = context.pluginWorkDirectory.appending(outputName)
13+
return .buildCommand(
14+
displayName:
15+
"Generating \(outputName) from \($0.lastComponent)",
16+
executable:
17+
try context.tool(named: "PluginExecutable").path,
18+
arguments: [
19+
"\($0)",
20+
"\(outputPath)"
21+
],
22+
inputFiles: [
23+
$0,
24+
],
25+
outputFiles: [
26+
outputPath
27+
]
28+
)
29+
}
30+
}
31+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Foundation
2+
3+
extension String {
4+
5+
var quotedForSourceCode: String {
6+
return "\"" + self
7+
.replacingOccurrences(of: "\\", with: "\\\\")
8+
.replacingOccurrences(of: "\"", with: "\\\"")
9+
+ "\""
10+
}
11+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Foundation
2+
3+
// Sample source generator tool that emits a Swift variable declaration of a string containing the hex representation of the contents of a file as a quoted string. The variable name is the base name of the input file. The input file is the first argument and the output file is the second.
4+
if ProcessInfo.processInfo.arguments.count != 3 {
5+
print("usage: MySourceGenBuildTool <input> <output>")
6+
exit(1)
7+
}
8+
let inputFile = ProcessInfo.processInfo.arguments[1]
9+
let outputFile = ProcessInfo.processInfo.arguments[2]
10+
11+
let variableName = URL(fileURLWithPath: inputFile).deletingPathExtension().lastPathComponent
12+
13+
let inputData = FileManager.default.contents(atPath: inputFile) ?? Data()
14+
let dataAsHex = inputData.map { String(format: "%02hhx", $0) }.joined()
15+
let outputString = "public var \(variableName) = \(dataAsHex.quotedForSourceCode)\n"
16+
let outputData = outputString.data(using: .utf8)
17+
FileManager.default.createFile(atPath: outputFile, contents: outputData)

Sources/PackageGraph/PackageGraph+Loading.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,8 +397,11 @@ private func createResolvedPackages(
397397

398398
// Get all the products from dependencies of this package.
399399
let productDependencies = packageBuilder.dependencies
400-
.flatMap({ $0.products })
401-
.filter({ $0.product.type != .test })
400+
.flatMap({ (dependency: ResolvedPackageBuilder) -> [ResolvedProductBuilder] in
401+
// Filter out synthesized products such as tests and implicit executables.
402+
let explicit = Set(dependency.package.manifest.products.lazy.map({ $0.name }))
403+
return dependency.products.filter({ explicit.contains($0.product.name) })
404+
})
402405
let productDependencyMap = productDependencies.spm_createDictionary({ ($0.product.name, $0) })
403406

404407
// Establish dependencies in each target.

Sources/PackageLoading/PackageBuilder.swift

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,37 +1255,46 @@ public final class PackageBuilder {
12551255
append(Product(name: product.name, type: product.type, targets: targets))
12561256
}
12571257

1258-
// Add implicit executables - for root packages only.
1258+
// Add implicit executables - for root packages and for dependency plugins.
12591259

1260-
if self.manifest.packageKind.isRoot {
1261-
// Compute the list of targets which are being used in an
1262-
// executable product so we don't create implicit executables
1263-
// for them.
1264-
let explicitProductsTargets = Set(self.manifest.products.flatMap{ product -> [String] in
1265-
switch product.type {
1266-
case .library, .plugin, .test:
1267-
return []
1268-
case .executable, .snippet:
1269-
return product.targets
1270-
}
1271-
})
1272-
1273-
let productMap = products.reduce(into: [String: Product]()) { partial, iterator in
1274-
partial[iterator.key] = iterator.item
1260+
// Compute the list of targets which are being used in an
1261+
// executable product so we don't create implicit executables
1262+
// for them.
1263+
let explicitProductsTargets = Set(self.manifest.products.flatMap{ product -> [String] in
1264+
switch product.type {
1265+
case .library, .plugin, .test:
1266+
return []
1267+
case .executable, .snippet:
1268+
return product.targets
12751269
}
1270+
})
12761271

1277-
for target in targets where target.type == .executable {
1278-
if explicitProductsTargets.contains(target.name) {
1279-
// If there is already an executable target with this name, skip generating a product for it
1280-
continue
1281-
} else if let product = productMap[target.name] {
1282-
// If there is already a product with this name skip generating a product for it,
1283-
// but warn if that product is not executable
1284-
if product.type != .executable {
1285-
self.observabilityScope.emit(warning: "The target named '\(target.name)' was identified as an executable target but a non-executable product with this name already exists.")
1286-
}
1287-
continue
1288-
} else {
1272+
let productMap = products.reduce(into: [String: Product]()) { partial, iterator in
1273+
partial[iterator.key] = iterator.item
1274+
}
1275+
1276+
let implicitPlugInExecutables = Set(
1277+
targets.lazy
1278+
.filter({ $0.type == .plugin })
1279+
.flatMap({ $0.dependencies })
1280+
.map({ $0.name })
1281+
)
1282+
1283+
for target in targets where target.type == .executable {
1284+
if self.manifest.packageKind.isRoot && explicitProductsTargets.contains(target.name) {
1285+
// If there is already an executable target with this name, skip generating a product for it
1286+
// (This shortcut only works for the root manifest, because for dependencies,
1287+
// products that correspond to plug‐ins may have been culled during resolution.)
1288+
continue
1289+
} else if let product = productMap[target.name] {
1290+
// If there is already a product with this name skip generating a product for it,
1291+
// but warn if that product is not executable
1292+
if product.type != .executable {
1293+
self.observabilityScope.emit(warning: "The target named '\(target.name)' was identified as an executable target but a non-executable product with this name already exists.")
1294+
}
1295+
continue
1296+
} else {
1297+
if self.manifest.packageKind.isRoot || implicitPlugInExecutables.contains(target.name) {
12891298
// Generate an implicit product for the executable target
12901299
let product = Product(name: target.name, type: .executable, targets: [target])
12911300
append(product)

Sources/PackageModel/Manifest.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,9 @@ public final class Manifest {
256256
for targetDependency in target.dependencies {
257257
register(targetDependency: targetDependency, registry: &registry, availablePackages: availablePackages)
258258
}
259+
for requiredPlugIn in target.pluginUsages ?? [] {
260+
register(requiredPlugIn: requiredPlugIn, registry: &registry, availablePackages: availablePackages)
261+
}
259262
}
260263

261264
// Products whose package could not be determined are marked as needed on every dependency.
@@ -362,6 +365,35 @@ public final class Manifest {
362365
}
363366
}
364367

368+
/// Registers a required plug‐in with a particular dependency if possible, or registers it as unknown.
369+
///
370+
/// - Parameters:
371+
/// - requiredPlugIn: The plug‐in to register.
372+
/// - registry: The registry in which to record the assocation.
373+
/// - availablePackages: The set of available packages.
374+
private func register(
375+
requiredPlugIn: TargetDescription.PluginUsage,
376+
registry: inout (known: [PackageIdentity: ProductFilter], unknown: Set<String>),
377+
availablePackages: Set<PackageIdentity>
378+
) {
379+
switch requiredPlugIn {
380+
case .plugin(let name, let package):
381+
if let package = package {
382+
if !register(
383+
product: name,
384+
inPackage: .plain(package),
385+
registry: &registry.known,
386+
availablePackages: availablePackages) {
387+
// Invalid, diagnosed later; see the dependency version of this method.
388+
registry.unknown.insert(name)
389+
}
390+
} else {
391+
// The plug‐in is in the same package.
392+
break
393+
}
394+
}
395+
}
396+
365397
/// Registers a required product with a particular dependency if possible.
366398
///
367399
/// - Parameters:

Tests/FunctionalTests/PluginTests.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,45 @@ class PluginTests: XCTestCase {
7777
}
7878
}
7979

80+
func testUseOfPluginWithInternalExecutable() {
81+
// Check if the host compiler supports the '-entry-point-function-name' flag. It's not needed for this test but is needed to build any executable from a package that uses tools version 5.5.
82+
#if swift(<5.5)
83+
try XCTSkipIf(true, "skipping because host compiler doesn't support '-entry-point-function-name'")
84+
#endif
85+
86+
fixture(name: "Miscellaneous/Plugins") { path in
87+
let (stdout, _) = try executeSwiftBuild(path.appending(component: "ClientOfPluginWithInternalExecutable"))
88+
XCTAssert(stdout.contains("Compiling PluginExecutable main.swift"), "stdout:\n\(stdout)")
89+
XCTAssert(stdout.contains("Linking PluginExecutable"), "stdout:\n\(stdout)")
90+
XCTAssert(stdout.contains("Generating foo.swift from foo.dat"), "stdout:\n\(stdout)")
91+
XCTAssert(stdout.contains("Compiling RootTarget foo.swift"), "stdout:\n\(stdout)")
92+
XCTAssert(stdout.contains("Linking RootTarget"), "stdout:\n\(stdout)")
93+
XCTAssert(stdout.contains("Build complete!"), "stdout:\n\(stdout)")
94+
}
95+
}
96+
97+
func testInternalExecutableAvailableOnlyToPlugin() {
98+
// Check if the host compiler supports the '-entry-point-function-name' flag. It's not needed for this test but is needed to build any executable from a package that uses tools version 5.5.
99+
#if swift(<5.5)
100+
try XCTSkipIf(true, "skipping because host compiler doesn't support '-entry-point-function-name'")
101+
#endif
102+
103+
fixture(name: "Miscellaneous/Plugins") { path in
104+
do {
105+
let (stdout, _) = try executeSwiftBuild(path.appending(component: "InvalidUseOfInternalPluginExecutable"))
106+
XCTFail("Illegally used internal executable.\nstdout:\n\(stdout)")
107+
}
108+
catch SwiftPMProductError.executionFailure(_, _, let stderr) {
109+
XCTAssert(
110+
stderr.contains(
111+
"product 'PluginExecutable' required by package 'invaliduseofinternalpluginexecutable' target 'RootTarget' not found in package 'PluginWithInternalExecutable'."
112+
),
113+
"stderr:\n\(stderr)"
114+
)
115+
}
116+
}
117+
}
118+
80119
func testContrivedTestCases() throws {
81120
// Check if the host compiler supports the '-entry-point-function-name' flag. It's not needed for this test but is needed to build any executable from a package that uses tools version 5.5.
82121
#if swift(<5.5)

Tests/PackageModelTests/ManifestTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class ManifestTests: XCTestCase {
5959
"Bar",
6060
"Baz",
6161
"Foo",
62+
"MyPlugin",
6263
])
6364
#endif
6465
}

0 commit comments

Comments
 (0)