Skip to content

Commit 63d19f3

Browse files
authored
Package command plugins should have the same initial working directory as the invocation (#4014)
Motivation: This allows relative paths passed as parameters to work. For build tool plugins we pass in the package directory so that relative paths in configuration files work there as well. This also matches the behavior of many build systems. The proposal didn't specify the initial working directory and usually all paths are absolute, but having predictable initial working directories follows the principle of least surprise. rdar://86831942
1 parent d5079ad commit 63d19f3

File tree

6 files changed

+35
-5
lines changed

6 files changed

+35
-5
lines changed

Sources/Commands/SwiftPackageTool.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,7 @@ extension SwiftPackageTool {
10501050
package: package,
10511051
buildEnvironment: buildEnvironment,
10521052
scriptRunner: pluginScriptRunner,
1053+
workingDirectory: swiftTool.originalWorkingDirectory,
10531054
outputDirectory: outputDir,
10541055
toolSearchDirectories: toolSearchDirs,
10551056
toolNamesToPaths: toolNamesToPaths,

Sources/SPMBuildCore/PluginInvocation.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ extension PluginTarget {
4040
/// - action: The plugin action (i.e. entry point) to invoke, possibly containing parameters.
4141
/// - package: The root of the package graph to pass down to the plugin.
4242
/// - scriptRunner: Entity responsible for actually running the code of the plugin.
43+
/// - workingDirectory: The initial working directory of the invoked plugin.
4344
/// - outputDirectory: A directory under which the plugin can write anything it wants to.
4445
/// - toolNamesToPaths: A mapping from name of tools available to the plugin to the corresponding absolute paths.
4546
/// - fileSystem: The file system to which all of the paths refers.
@@ -50,6 +51,7 @@ extension PluginTarget {
5051
package: ResolvedPackage,
5152
buildEnvironment: BuildEnvironment,
5253
scriptRunner: PluginScriptRunner,
54+
workingDirectory: AbsolutePath,
5355
outputDirectory: AbsolutePath,
5456
toolSearchDirectories: [AbsolutePath],
5557
toolNamesToPaths: [String: AbsolutePath],
@@ -90,6 +92,7 @@ extension PluginTarget {
9092
sources: sources,
9193
input: inputStruct,
9294
toolsVersion: self.apiVersion,
95+
workingDirectory: workingDirectory,
9396
writableDirectories: writableDirectories,
9497
readOnlyDirectories: readOnlyDirectories,
9598
fileSystem: fileSystem,
@@ -242,6 +245,7 @@ extension PackageGraph {
242245
package: package,
243246
buildEnvironment: buildEnvironment,
244247
scriptRunner: pluginScriptRunner,
248+
workingDirectory: package.path,
245249
outputDirectory: pluginOutputDir,
246250
toolSearchDirectories: toolSearchDirectories,
247251
toolNamesToPaths: toolNamesToPaths,
@@ -390,6 +394,7 @@ public protocol PluginScriptRunner {
390394
sources: Sources,
391395
input: PluginScriptRunnerInput,
392396
toolsVersion: ToolsVersion,
397+
workingDirectory: AbsolutePath,
393398
writableDirectories: [AbsolutePath],
394399
readOnlyDirectories: [AbsolutePath],
395400
fileSystem: FileSystem,

Sources/Workspace/DefaultPluginScriptRunner.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner {
7070
sources: Sources,
7171
input: PluginScriptRunnerInput,
7272
toolsVersion: ToolsVersion,
73+
workingDirectory: AbsolutePath,
7374
writableDirectories: [AbsolutePath],
7475
readOnlyDirectories: [AbsolutePath],
7576
fileSystem: FileSystem,
@@ -93,6 +94,7 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner {
9394
// Compilation succeeded, so run the executable. We are already running on an asynchronous queue.
9495
self.invoke(
9596
compiledExec: result.compiledExecutable,
97+
workingDirectory: workingDirectory,
9698
writableDirectories: writableDirectories,
9799
readOnlyDirectories: readOnlyDirectories,
98100
input: input,
@@ -343,6 +345,7 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner {
343345
/// Private function that invokes a compiled plugin executable and communicates with it until it finishes.
344346
fileprivate func invoke(
345347
compiledExec: AbsolutePath,
348+
workingDirectory: AbsolutePath,
346349
writableDirectories: [AbsolutePath],
347350
readOnlyDirectories: [AbsolutePath],
348351
input: PluginScriptRunnerInput,
@@ -369,7 +372,7 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner {
369372
process.executableURL = Foundation.URL(fileURLWithPath: command[0])
370373
process.arguments = Array(command.dropFirst())
371374
process.environment = ProcessInfo.processInfo.environment
372-
process.currentDirectoryURL = self.cacheDir.asURL
375+
process.currentDirectoryURL = workingDirectory.asURL
373376

374377
// Set up a pipe for sending structured messages to the plugin on its stdin.
375378
let stdinPipe = Pipe()

Tests/CommandsTests/PackageToolTests.swift

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,17 +1083,23 @@ final class PackageToolTests: CommandsTestCase {
10831083
try localFileSystem.writeFileContents(packageDir.appending(components: "Plugins", "MyPlugin", "plugin.swift")) {
10841084
$0 <<< """
10851085
import PackagePlugin
1086-
1086+
import Foundation
10871087
@main
10881088
struct MyBuildToolPlugin: BuildToolPlugin {
10891089
func createBuildCommands(
10901090
context: PluginContext,
10911091
target: Target
10921092
) throws -> [Command] {
1093+
// Expect the initial working directory for build tool plugins is the package directory.
1094+
guard FileManager.default.currentDirectoryPath == context.package.directory.string else {
1095+
throw "expected initial working directory ‘\\(FileManager.default.currentDirectoryPath)’"
1096+
}
1097+
10931098
// Check that the package display name is what we expect.
10941099
guard context.package.displayName == "MyPackage" else {
10951100
throw "expected display name to be ‘MyPackage’ but found ‘\\(context.package.displayName)’"
10961101
}
1102+
10971103
// Create and return a build command that uses all the `.foo` files in the target as inputs, so they get counted as having been handled.
10981104
let fooFiles = (target as? SourceModuleTarget)?.sourceFiles.compactMap{ $0.path.extension == "foo" ? $0.path : nil } ?? []
10991105
return [ .buildCommand(displayName: "A command", executable: "/bin/echo", arguments: ["Hello"], inputFiles: fooFiles) ]
@@ -1106,8 +1112,9 @@ final class PackageToolTests: CommandsTestCase {
11061112

11071113
// Invoke it, and check the results.
11081114
let result = try SwiftPMProduct.SwiftBuild.executeProcess([], packagePath: packageDir)
1109-
XCTAssertEqual(result.exitStatus, .terminated(code: 0))
1110-
XCTAssert(try result.utf8Output().contains("Build complete!"))
1115+
let output = try result.utf8Output() + result.utf8stderrOutput()
1116+
XCTAssertEqual(result.exitStatus, .terminated(code: 0), "output: \(output)")
1117+
XCTAssert(output.contains("Build complete!"))
11111118

11121119
// We expect a warning about `library.bar` but not about `library.foo`.
11131120
let stderrOutput = try result.utf8stderrOutput()
@@ -1270,7 +1277,7 @@ final class PackageToolTests: CommandsTestCase {
12701277
try localFileSystem.writeFileContents(packageDir.appending(components: "Plugins", "MyPlugin", "plugin.swift")) {
12711278
$0 <<< """
12721279
import PackagePlugin
1273-
1280+
import Foundation
12741281
@main
12751282
struct MyCommandPlugin: CommandPlugin {
12761283
func performCommand(
@@ -1280,6 +1287,9 @@ final class PackageToolTests: CommandsTestCase {
12801287
) throws {
12811288
print("This is MyCommandPlugin.")
12821289
1290+
// Print out the initial working directory so we can check it in the test.
1291+
print("Initial working directory: \\(FileManager.default.currentDirectoryPath)")
1292+
12831293
// Check that we can find a binary-provided tool in the same package.
12841294
print("Looking for LocalBinaryTool...")
12851295
let localBinaryTool = try context.tool(named: "LocalBinaryTool")
@@ -1395,6 +1405,15 @@ final class PackageToolTests: CommandsTestCase {
13951405
XCTAssertMatch(output, .contains("Sources/MyLibrary/library.swift: source"))
13961406
XCTAssertMatch(output, .contains("Sources/MyLibrary/test.docc: unknown"))
13971407
}
1408+
1409+
// Check that the initial working directory is what we expected.
1410+
do {
1411+
let workingDirectory = FileManager.default.currentDirectoryPath
1412+
let result = try SwiftPMProduct.SwiftPackage.executeProcess(["mycmd"], packagePath: packageDir)
1413+
let output = try result.utf8Output() + result.utf8stderrOutput()
1414+
XCTAssertEqual(result.exitStatus, .terminated(code: 0), "output: \(output)")
1415+
XCTAssertMatch(output, .contains("Initial working directory: \(workingDirectory)"))
1416+
}
13981417
}
13991418
}
14001419

Tests/FunctionalTests/PluginTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ class PluginTests: XCTestCase {
425425
package: package,
426426
buildEnvironment: BuildEnvironment(platform: .macOS, configuration: .debug),
427427
scriptRunner: scriptRunner,
428+
workingDirectory: package.path,
428429
outputDirectory: pluginDir.appending(component: "output"),
429430
toolSearchDirectories: [UserToolchain.default.swiftCompilerPath.parentDirectory],
430431
toolNamesToPaths: [:],

Tests/SPMBuildCoreTests/PluginInvocationTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ class PluginInvocationTests: XCTestCase {
9292
sources: Sources,
9393
input: PluginScriptRunnerInput,
9494
toolsVersion: ToolsVersion,
95+
workingDirectory: AbsolutePath,
9596
writableDirectories: [AbsolutePath],
9697
readOnlyDirectories: [AbsolutePath],
9798
fileSystem: FileSystem,

0 commit comments

Comments
 (0)