Skip to content

Commit e26c128

Browse files
committed
SwiftPM does not build all dependency executables before invoking plugin build command
Make the list of input dependencies of an llbuild command that runs a plugin-provided build tool also have dependencies on the paths of all the tools on which the plugin has declared a dependency, not just the one that appears as an executable in the build command. Also adds a mildly elaborate unit test to make sure this works. rdar://93679015
1 parent d9ce602 commit e26c128

File tree

10 files changed

+127
-5
lines changed

10 files changed

+127
-5
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// swift-tools-version: 5.7
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "MyBuildToolPluginDependencies",
6+
targets: [
7+
// A local tool that uses a build tool plugin.
8+
.executableTarget(
9+
name: "MyLocalTool",
10+
plugins: [
11+
"MySourceGenBuildToolPlugin",
12+
]
13+
),
14+
// The plugin that generates build tool commands to invoke MySourceGenBuildTool.
15+
.plugin(
16+
name: "MySourceGenBuildToolPlugin",
17+
capability: .buildTool(),
18+
dependencies: [
19+
"MySourceGenBuildTool",
20+
]
21+
),
22+
// A command line tool that generates source files.
23+
.executableTarget(
24+
name: "MySourceGenBuildTool",
25+
dependencies: [
26+
"MySourceGenBuildToolLib",
27+
]
28+
),
29+
// A library used by MySourceGenBuildTool (not the client).
30+
.target(
31+
name: "MySourceGenBuildToolLib"
32+
),
33+
]
34+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import PackagePlugin
2+
3+
@main
4+
struct MyPlugin: BuildToolPlugin {
5+
6+
// Create build commands that don't invoke the MySourceGenBuildTool source generator
7+
// tool directly, but instead invoke a system tool that invokes it indirectly. We
8+
// want to test that we still end up with a dependency on not only that tool but also
9+
// on the library it depends on, even without including an explicit dependency on it.
10+
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
11+
print("Hello from the Build Tool Plugin!")
12+
guard let target = target as? SourceModuleTarget else { return [] }
13+
let inputFiles = target.sourceFiles.filter({ $0.path.extension == "dat" })
14+
return try inputFiles.map {
15+
let inputFile = $0
16+
let inputPath = inputFile.path
17+
let outputName = inputPath.stem + ".swift"
18+
let outputPath = context.pluginWorkDirectory.appending(outputName)
19+
return .buildCommand(
20+
displayName:
21+
"Generating \(outputName) from \(inputPath.lastComponent)",
22+
executable:
23+
Path("/usr/bin/env"),
24+
arguments: [
25+
try context.tool(named: "MySourceGenBuildTool").path,
26+
"\(inputPath)",
27+
"\(outputPath)"
28+
],
29+
inputFiles: [
30+
inputPath,
31+
],
32+
outputFiles: [
33+
outputPath
34+
]
35+
)
36+
}
37+
}
38+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
let foo = "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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Foundation
2+
import MySourceGenBuildToolLib
3+
4+
// 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.
5+
if ProcessInfo.processInfo.arguments.count != 3 {
6+
print("usage: MySourceGenBuildTool <input> <output>")
7+
exit(1)
8+
}
9+
let inputFile = ProcessInfo.processInfo.arguments[1]
10+
let outputFile = ProcessInfo.processInfo.arguments[2]
11+
12+
let variableName = URL(fileURLWithPath: inputFile).deletingPathExtension().lastPathComponent
13+
14+
let inputData = FileManager.default.contents(atPath: inputFile) ?? Data()
15+
let dataAsHex = inputData.map { String(format: "%02hhx", $0) }.joined()
16+
let outputString = "public var \(variableName) = \(dataAsHex.quotedForSourceCode)\n"
17+
let outputData = outputString.data(using: .utf8)
18+
FileManager.default.createFile(atPath: outputFile, contents: outputData)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Foundation
2+
3+
extension String {
4+
5+
public var quotedForSourceCode: String {
6+
return "\"" + self
7+
.replacingOccurrences(of: "\\", with: "\\\\")
8+
.replacingOccurrences(of: "\"", with: "\\\"")
9+
+ "\""
10+
}
11+
}

Sources/Build/LLBuildManifestBuilder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,7 @@ extension LLBuildManifestBuilder {
625625
manifest.addShellCmd(
626626
name: displayName + "-" + ByteString(encodingAsUTF8: uniquedName).sha256Checksum,
627627
description: displayName,
628-
inputs: [.file(execPath)] + command.inputFiles.map{ .file($0) },
628+
inputs: command.inputFiles.map{ .file($0) },
629629
outputs: command.outputFiles.map{ .file($0) },
630630
arguments: commandLine,
631631
environment: command.configuration.environment,

Sources/SPMBuildCore/PluginInvocation.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,9 @@ extension PackageGraph {
353353
dict[name] = path
354354
}
355355
})
356+
357+
// Determine additional input dependencies for any plugin commands, based on any executables the plugin target depends on.
358+
let toolPaths = toolNamesToPaths.values.sorted()
356359

357360
// Assign a plugin working directory based on the package, target, and plugin.
358361
let pluginOutputDir = outputDir.appending(components: package.identity.description, target.name, pluginTarget.name)
@@ -367,13 +370,15 @@ extension PackageGraph {
367370
let delegateQueue = DispatchQueue(label: "plugin-invocation")
368371
class PluginDelegate: PluginInvocationDelegate {
369372
let delegateQueue: DispatchQueue
373+
let toolPaths: [AbsolutePath]
370374
var outputData = Data()
371375
var diagnostics = [Basics.Diagnostic]()
372376
var buildCommands = [BuildToolPluginInvocationResult.BuildCommand]()
373377
var prebuildCommands = [BuildToolPluginInvocationResult.PrebuildCommand]()
374378

375-
init(delegateQueue: DispatchQueue) {
379+
init(delegateQueue: DispatchQueue, toolPaths: [AbsolutePath]) {
376380
self.delegateQueue = delegateQueue
381+
self.toolPaths = toolPaths
377382
}
378383

379384
func pluginEmittedOutput(_ data: Data) {
@@ -395,7 +400,7 @@ extension PackageGraph {
395400
arguments: arguments,
396401
environment: environment,
397402
workingDirectory: workingDirectory),
398-
inputFiles: inputFiles,
403+
inputFiles: toolPaths + inputFiles,
399404
outputFiles: outputFiles))
400405
}
401406

@@ -411,7 +416,7 @@ extension PackageGraph {
411416
outputFilesDirectory: outputFilesDirectory))
412417
}
413418
}
414-
let delegate = PluginDelegate(delegateQueue: delegateQueue)
419+
let delegate = PluginDelegate(delegateQueue: delegateQueue, toolPaths: toolPaths)
415420

416421
// Invoke the build tool plugin with the input parameters and the delegate that will collect outputs.
417422
let startTime = DispatchTime.now()

Tests/FunctionalTests/PluginTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,20 @@ class PluginTests: XCTestCase {
102102
XCTAssert(stdout.contains("Build complete!"), "stdout:\n\(stdout)")
103103
}
104104
}
105+
106+
func testBuildToolPluginDependencies() throws {
107+
// Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require).
108+
try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency")
109+
110+
try fixture(name: "Miscellaneous/Plugins") { fixturePath in
111+
let (stdout, _) = try executeSwiftBuild(fixturePath.appending(component: "MyBuildToolPluginDependencies"))
112+
XCTAssert(stdout.contains("Compiling MySourceGenBuildTool main.swift"), "stdout:\n\(stdout)")
113+
XCTAssert(stdout.contains("Linking MySourceGenBuildTool"), "stdout:\n\(stdout)")
114+
XCTAssert(stdout.contains("Generating foo.swift from foo.dat"), "stdout:\n\(stdout)")
115+
XCTAssert(stdout.contains("Compiling MyLocalTool foo.swift"), "stdout:\n\(stdout)")
116+
XCTAssert(stdout.contains("Build complete!"), "stdout:\n\(stdout)")
117+
}
118+
}
105119

106120
func testContrivedTestCases() throws {
107121
// Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require).

Tests/SPMBuildCoreTests/PluginInvocationTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ class PluginInvocationTests: XCTestCase {
212212
XCTAssertEqual(evalFirstCommand.configuration.arguments, ["-c", "/Foo/Sources/Foo/SomeFile.abc"])
213213
XCTAssertEqual(evalFirstCommand.configuration.environment, ["X": "Y"])
214214
XCTAssertEqual(evalFirstCommand.configuration.workingDirectory, AbsolutePath("/Foo/Sources/Foo"))
215-
XCTAssertEqual(evalFirstCommand.inputFiles, [])
215+
XCTAssertEqual(evalFirstCommand.inputFiles, [builtToolsDir.appending(component: "FooTool")])
216216
XCTAssertEqual(evalFirstCommand.outputFiles, [])
217217

218218
XCTAssertEqual(evalFirstResult.diagnostics.count, 1)

0 commit comments

Comments
 (0)