Skip to content

Commit a6ac80f

Browse files
authored
SwiftPM does not build all dependency executables before invoking plugin build command (#5636)
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 5cfba06 commit a6ac80f

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)