Skip to content

Pass through shell script build phase environment and working directory for build tool commands #3333

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,27 @@
// swift-tools-version: 999.0
import PackageDescription

let package = Package(
name: "ContrivedTestPlugin",
targets: [
// A local tool that uses a build tool plugin.
.executableTarget(
name: "MyLocalTool",
dependencies: [
"MySourceGenBuildToolPlugin",
]
),
// The plugin that generates build tool commands to invoke MySourceGenBuildTool.
.plugin(
name: "MySourceGenBuildToolPlugin",
capability: .buildTool(),
dependencies: [
"MySourceGenBuildTool",
]
),
// The command line tool that generates source files.
.executableTarget(
name: "MySourceGenBuildTool"
),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
I am Foo!
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
print("Generated string Foo: '\(PREFIX_foo)'")
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Foundation

// 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, with the value from an environment value prepended. 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 variablePrefix = ProcessInfo.processInfo.environment["VARIABLE_NAME_PREFIX"] ?? ""
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 \(variablePrefix)\(variableName) = \(dataAsHex.quotedForSourceCode)\n"
let outputData = outputString.data(using: .utf8)
FileManager.default.createFile(atPath: outputFile, contents: outputData)

extension String {

public var quotedForSourceCode: String {
return "\"" + self
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "\"", with: "\\\"")
+ "\""
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import PackagePlugin

print("Hello from the Build Tool Plugin!")

for inputPath in targetBuildContext.otherFiles {
guard inputPath.suffix == ".dat" else { continue }
let outputName = inputPath.basename + ".swift"
let outputPath = targetBuildContext.outputDir.appending(outputName)
commandConstructor.createCommand(
displayName:
"Generating \(outputName) from \(inputPath.filename)",
executable:
try targetBuildContext.lookupTool(named: "MySourceGenBuildTool"),
arguments: [
"\(inputPath)",
"\(outputPath)"
],
environment: [
"VARIABLE_NAME_PREFIX": "PREFIX_"
],
inputPaths: [
inputPath,
],
outputPaths: [
outputPath
]
)
commandConstructor.addGeneratedOutputFile(path: outputPath)
}
7 changes: 4 additions & 3 deletions Sources/Build/ManifestBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -584,16 +584,17 @@ extension LLBuildManifestBuilder {

// Add any build tool commands created by plugins for the target (prebuild and postbuild commands are handled outside the build).
for command in target.pluginInvocationResults.reduce([], { $0 + $1.commands }) {
if case .buildToolCommand(let displayName, let executable, let arguments, _, _, let inputPaths, let outputPaths) = command {
if case .buildToolCommand(let displayName, let executable, let arguments, let environment, let workingDirectory, let inputPaths, let outputPaths) = command {
// Create a shell command to invoke the executable. We include the path of the executable as a dependency.
// FIXME: We will need to extend the addShellCmd() function to also take working directory and environment.
let execPath = AbsolutePath(executable, relativeTo: buildParameters.buildPath)
manifest.addShellCmd(
name: displayName,
description: displayName,
inputs: [.file(execPath)] + inputPaths.map{ .file($0) },
outputs: outputPaths.map{ .file($0) },
args: [execPath.pathString] + arguments)
args: [execPath.pathString] + arguments,
environ: environment,
workingDir: workingDirectory?.pathString)
}
}

Expand Down
14 changes: 13 additions & 1 deletion Sources/PackagePlugin/PublicAPI/CommandConstructor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ public final class CommandConstructor {
/// Note that input and output dependencies are ignored for prebuild and
/// postbuild actions, since they always run before and after the build
/// respectively.
public func createCommand(
displayName: String?,
executable: Path,
arguments: [String],
environment: [String: String]? = nil,
inputPaths: [Path] = [],
outputPaths: [Path] = []
) {
output.commands.append(Command(displayName: displayName, executable: executable, arguments: arguments, environment: environment, workingDirectory: nil, inputPaths: inputPaths, outputPaths: outputPaths))
}

@available(*, unavailable, message: "specifying the initial working directory for a command is not yet supported")
public func createCommand(
displayName: String?,
executable: Path,
Expand All @@ -32,7 +44,7 @@ public final class CommandConstructor {
inputPaths: [Path] = [],
outputPaths: [Path] = []
) {
output.commands.append(Command(displayName: displayName, executable: executable, arguments: arguments, environment: environment, workingDirectory: workingDirectory, inputPaths: inputPaths, outputPaths: outputPaths))
self.createCommand(displayName: displayName, executable: executable, arguments: arguments, environment: environment, inputPaths: inputPaths, outputPaths: outputPaths)
}

/// Registers the path of an output file generated by a `buildTool` plugin
Expand Down
20 changes: 20 additions & 0 deletions Tests/FunctionalTests/PluginTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,24 @@ class PluginTests: XCTestCase {
}
}
}

func testContrivedTestCases() throws {
// 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 999.0.
try XCTSkipUnless(doesHostSwiftCompilerSupportRenamingMainSymbol(), "skipping because host compiler doesn't support '-entry-point-function-name'")

fixture(name: "Miscellaneous/Plugins") { path in
do {
let (stdout, _) = try executeSwiftBuild(path.appending(component: "ContrivedTestPlugin"), configuration: .Debug, extraArgs: ["--product", "MyLocalTool"], env: ["SWIFTPM_ENABLE_PLUGINS": "1"])
XCTAssert(stdout.contains("Linking MySourceGenBuildTool"), "stdout:\n\(stdout)")
XCTAssert(stdout.contains("Generating foo.swift from foo.dat"), "stdout:\n\(stdout)")
XCTAssert(stdout.contains("Linking MyLocalTool"), "stdout:\n\(stdout)")
XCTAssert(stdout.contains("Build complete!"), "stdout:\n\(stdout)")
}
catch {
print(error)
throw error
}
}
}

}