Skip to content

SwiftPM does not build all dependency executables before invoking plugin build command #5636

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// swift-tools-version: 5.7
import PackageDescription

let package = Package(
name: "MyBuildToolPluginDependencies",
targets: [
// A local tool that uses a build tool plugin.
.executableTarget(
name: "MyLocalTool",
plugins: [
"MySourceGenBuildToolPlugin",
]
),
// The plugin that generates build tool commands to invoke MySourceGenBuildTool.
.plugin(
name: "MySourceGenBuildToolPlugin",
capability: .buildTool(),
dependencies: [
"MySourceGenBuildTool",
]
),
// A command line tool that generates source files.
.executableTarget(
name: "MySourceGenBuildTool",
dependencies: [
"MySourceGenBuildToolLib",
]
),
// A library used by MySourceGenBuildTool (not the client).
.target(
name: "MySourceGenBuildToolLib"
),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import PackagePlugin

@main
struct MyPlugin: BuildToolPlugin {

// Create build commands that don't invoke the MySourceGenBuildTool source generator
// tool directly, but instead invoke a system tool that invokes it indirectly. We
// want to test that we still end up with a dependency on not only that tool but also
// on the library it depends on, even without including an explicit dependency on it.
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
print("Hello from the Build Tool Plugin!")
guard let target = target as? SourceModuleTarget else { return [] }
let inputFiles = target.sourceFiles.filter({ $0.path.extension == "dat" })
return try inputFiles.map {
let inputFile = $0
let inputPath = inputFile.path
let outputName = inputPath.stem + ".swift"
let outputPath = context.pluginWorkDirectory.appending(outputName)
return .buildCommand(
displayName:
"Generating \(outputName) from \(inputPath.lastComponent)",
executable:
Path("/usr/bin/env"),
arguments: [
try context.tool(named: "MySourceGenBuildTool").path,
"\(inputPath)",
"\(outputPath)"
],
inputFiles: [
inputPath,
],
outputFiles: [
outputPath
]
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let foo = "I am Foo!"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
print("Generated string Foo: '\(foo)'")
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Foundation
import MySourceGenBuildToolLib

// 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.
if ProcessInfo.processInfo.arguments.count != 3 {
print("usage: MySourceGenBuildTool <input> <output>")
exit(1)
}
let inputFile = ProcessInfo.processInfo.arguments[1]
let outputFile = ProcessInfo.processInfo.arguments[2]

let variableName = URL(fileURLWithPath: inputFile).deletingPathExtension().lastPathComponent

let inputData = FileManager.default.contents(atPath: inputFile) ?? Data()
let dataAsHex = inputData.map { String(format: "%02hhx", $0) }.joined()
let outputString = "public var \(variableName) = \(dataAsHex.quotedForSourceCode)\n"
let outputData = outputString.data(using: .utf8)
FileManager.default.createFile(atPath: outputFile, contents: outputData)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

extension String {

public var quotedForSourceCode: String {
return "\"" + self
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "\"", with: "\\\"")
+ "\""
}
}
2 changes: 1 addition & 1 deletion Sources/Build/LLBuildManifestBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ extension LLBuildManifestBuilder {
manifest.addShellCmd(
name: displayName + "-" + ByteString(encodingAsUTF8: uniquedName).sha256Checksum,
description: displayName,
inputs: [.file(execPath)] + command.inputFiles.map{ .file($0) },
inputs: command.inputFiles.map{ .file($0) },
outputs: command.outputFiles.map{ .file($0) },
arguments: commandLine,
environment: command.configuration.environment,
Expand Down
11 changes: 8 additions & 3 deletions Sources/SPMBuildCore/PluginInvocation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,9 @@ extension PackageGraph {
dict[name] = path
}
})

// Determine additional input dependencies for any plugin commands, based on any executables the plugin target depends on.
let toolPaths = toolNamesToPaths.values.sorted()

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

init(delegateQueue: DispatchQueue) {
init(delegateQueue: DispatchQueue, toolPaths: [AbsolutePath]) {
self.delegateQueue = delegateQueue
self.toolPaths = toolPaths
}

func pluginEmittedOutput(_ data: Data) {
Expand All @@ -395,7 +400,7 @@ extension PackageGraph {
arguments: arguments,
environment: environment,
workingDirectory: workingDirectory),
inputFiles: inputFiles,
inputFiles: toolPaths + inputFiles,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the key change. Nothing particularly risky or anything that requires extra scrutiny, I think, but it seemed worth calling out amid all the more mundane changes.

outputFiles: outputFiles))
}

Expand All @@ -411,7 +416,7 @@ extension PackageGraph {
outputFilesDirectory: outputFilesDirectory))
}
}
let delegate = PluginDelegate(delegateQueue: delegateQueue)
let delegate = PluginDelegate(delegateQueue: delegateQueue, toolPaths: toolPaths)

// Invoke the build tool plugin with the input parameters and the delegate that will collect outputs.
let startTime = DispatchTime.now()
Expand Down
14 changes: 14 additions & 0 deletions Tests/FunctionalTests/PluginTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ class PluginTests: XCTestCase {
XCTAssert(stdout.contains("Build complete!"), "stdout:\n\(stdout)")
}
}

func testBuildToolPluginDependencies() throws {
// Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require).
try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency")

try fixture(name: "Miscellaneous/Plugins") { fixturePath in
let (stdout, _) = try executeSwiftBuild(fixturePath.appending(component: "MyBuildToolPluginDependencies"))
XCTAssert(stdout.contains("Compiling MySourceGenBuildTool main.swift"), "stdout:\n\(stdout)")
XCTAssert(stdout.contains("Linking MySourceGenBuildTool"), "stdout:\n\(stdout)")
XCTAssert(stdout.contains("Generating foo.swift from foo.dat"), "stdout:\n\(stdout)")
XCTAssert(stdout.contains("Compiling MyLocalTool foo.swift"), "stdout:\n\(stdout)")
XCTAssert(stdout.contains("Build complete!"), "stdout:\n\(stdout)")
}
}

func testContrivedTestCases() throws {
// Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require).
Expand Down
2 changes: 1 addition & 1 deletion Tests/SPMBuildCoreTests/PluginInvocationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ class PluginInvocationTests: XCTestCase {
XCTAssertEqual(evalFirstCommand.configuration.arguments, ["-c", "/Foo/Sources/Foo/SomeFile.abc"])
XCTAssertEqual(evalFirstCommand.configuration.environment, ["X": "Y"])
XCTAssertEqual(evalFirstCommand.configuration.workingDirectory, AbsolutePath("/Foo/Sources/Foo"))
XCTAssertEqual(evalFirstCommand.inputFiles, [])
XCTAssertEqual(evalFirstCommand.inputFiles, [builtToolsDir.appending(component: "FooTool")])
XCTAssertEqual(evalFirstCommand.outputFiles, [])

XCTAssertEqual(evalFirstResult.diagnostics.count, 1)
Expand Down