Skip to content

Commit cceb982

Browse files
committed
Add Process support for workingDirectory
1 parent 16da354 commit cceb982

File tree

2 files changed

+95
-21
lines changed

2 files changed

+95
-21
lines changed

Sources/TSCBasic/Process.swift

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/*
22
This source file is part of the Swift.org open source project
3-
4-
Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
3+
4+
Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
6-
6+
77
See http://swift.org/LICENSE.txt for license information
88
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
@@ -92,7 +92,7 @@ public struct ProcessResult: CustomStringConvertible {
9292
self.stderrOutput = stderrOutput
9393
self.exitStatus = exitStatus
9494
}
95-
95+
9696
/// Converts stdout output bytes to string, assuming they're UTF8.
9797
public func utf8Output() throws -> String {
9898
return String(decoding: try output.get(), as: Unicode.UTF8.self)
@@ -122,15 +122,15 @@ public final class Process: ObjectIdentifierProtocol {
122122
/// The program requested to be executed cannot be found on the existing search paths, or is not executable.
123123
case missingExecutableProgram(program: String)
124124
}
125-
125+
126126
public enum OutputRedirection {
127127
/// Do not redirect the output
128128
case none
129129
/// Collect stdout and stderr output and provide it back via ProcessResult object
130130
case collect
131131
/// Stream stdout and stderr via the corresponding closures
132132
case stream(stdout: OutputClosure, stderr: OutputClosure)
133-
133+
134134
public var redirectsOutput: Bool {
135135
switch self {
136136
case .none:
@@ -139,7 +139,7 @@ public final class Process: ObjectIdentifierProtocol {
139139
return true
140140
}
141141
}
142-
142+
143143
public var outputClosures: (stdoutClosure: OutputClosure, stderrClosure: OutputClosure)? {
144144
switch self {
145145
case .stream(let stdoutClosure, let stderrClosure):
@@ -176,6 +176,9 @@ public final class Process: ObjectIdentifierProtocol {
176176
/// The environment with which the process was executed.
177177
public let environment: [String: String]
178178

179+
/// The path to the directory under which to run the process.
180+
public let workingDirectory: AbsolutePath?
181+
179182
/// The process id of the spawned process, available after the process is launched.
180183
#if os(Windows)
181184
private var _process: Foundation.Process?
@@ -213,7 +216,7 @@ public final class Process: ObjectIdentifierProtocol {
213216
/// Queue to protect reading/writing on map of validated executables.
214217
private static let executablesQueue = DispatchQueue(
215218
label: "org.swift.swiftpm.process.findExecutable")
216-
219+
217220
/// Indicates if a new progress group is created for the child process.
218221
private let startNewProcessGroup: Bool
219222

@@ -223,6 +226,36 @@ public final class Process: ObjectIdentifierProtocol {
223226
/// Value: Path to the executable, if found.
224227
static private var validatedExecutablesMap = [String: AbsolutePath?]()
225228

229+
#if os(macOS)
230+
/// Create a new process instance.
231+
///
232+
/// - Parameters:
233+
/// - arguments: The arguments for the subprocess.
234+
/// - environment: The environment to pass to subprocess. By default the current process environment
235+
/// will be inherited.
236+
/// - workingDirectory: The path to the directory under which to run the process.
237+
/// - outputRedirection: How process redirects its output. Default value is .collect.
238+
/// - verbose: If true, launch() will print the arguments of the subprocess before launching it.
239+
/// - startNewProcessGroup: If true, a new progress group is created for the child making it
240+
/// continue running even if the parent is killed or interrupted. Default value is true.
241+
@available(macOS 10.15, *)
242+
public init(
243+
arguments: [String],
244+
environment: [String: String] = ProcessEnv.vars,
245+
workingDirectory: AbsolutePath,
246+
outputRedirection: OutputRedirection = .collect,
247+
verbose: Bool = Process.verbose,
248+
startNewProcessGroup: Bool = true
249+
) {
250+
self.arguments = arguments
251+
self.environment = environment
252+
self.workingDirectory = workingDirectory
253+
self.outputRedirection = outputRedirection
254+
self.verbose = verbose
255+
self.startNewProcessGroup = startNewProcessGroup
256+
}
257+
#endif
258+
226259
/// Create a new process instance.
227260
///
228261
/// - Parameters:
@@ -242,6 +275,7 @@ public final class Process: ObjectIdentifierProtocol {
242275
) {
243276
self.arguments = arguments
244277
self.environment = environment
278+
self.workingDirectory = nil
245279
self.outputRedirection = outputRedirection
246280
self.verbose = verbose
247281
self.startNewProcessGroup = startNewProcessGroup
@@ -370,6 +404,17 @@ public final class Process: ObjectIdentifierProtocol {
370404
posix_spawn_file_actions_init(&fileActions)
371405
defer { posix_spawn_file_actions_destroy(&fileActions) }
372406

407+
#if os(macOS)
408+
if let workingDirectory = workingDirectory?.pathString {
409+
// The only way to set a workingDirectory is using an availability-gated initializer, so we don't need
410+
// to handle the case where the posix_spawn_file_actions_addchdir_np method is unavailable. This check only
411+
// exists here to make the compiler happy.
412+
if #available(macOS 10.15, *) {
413+
posix_spawn_file_actions_addchdir_np(&fileActions, workingDirectory)
414+
}
415+
}
416+
#endif
417+
373418
// Workaround for https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=89e435f3559c53084498e9baad22172b64429362
374419
// Change allowing for newer version of glibc
375420
guard let devNull = strdup("/dev/null") else {
@@ -408,7 +453,7 @@ public final class Process: ObjectIdentifierProtocol {
408453

409454
if outputRedirection.redirectsOutput {
410455
let outputClosures = outputRedirection.outputClosures
411-
456+
412457
// Close the write end of the output pipe.
413458
try close(fd: &outputPipe[1])
414459

@@ -682,7 +727,7 @@ extension ProcessResult.Error: CustomStringConvertible {
682727
stream <<< "\n"
683728
}
684729
}
685-
730+
686731
return stream.bytes.description
687732
}
688733
}

Tests/TSCBasicTests/ProcessTests.swift

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/*
22
This source file is part of the Swift.org open source project
3-
4-
Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
3+
4+
Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
6-
6+
77
See http://swift.org/LICENSE.txt for license information
88
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
@@ -82,7 +82,7 @@ class ProcessTests: XCTestCase {
8282
try localFileSystem.writeFileContents(tempExecutable, bytes: """
8383
#!/bin/sh
8484
exit
85-
85+
8686
""")
8787

8888
try withCustomEnv(["PATH": path.pathString]) {
@@ -98,7 +98,7 @@ class ProcessTests: XCTestCase {
9898
try localFileSystem.writeFileContents(tempExecutable, bytes: """
9999
#!/bin/sh
100100
exit
101-
101+
102102
""")
103103

104104
try withCustomEnv(["PATH": path.pathString]) {
@@ -161,27 +161,27 @@ class ProcessTests: XCTestCase {
161161
XCTAssertFalse(try Process.running(ProcessID(child), orDefunct: true))
162162
}
163163
}
164-
164+
165165
func testThreadSafetyOnWaitUntilExit() throws {
166166
let process = Process(args: "echo", "hello")
167167
try process.launch()
168-
168+
169169
var result1: String = ""
170170
var result2: String = ""
171-
171+
172172
let t1 = Thread {
173173
result1 = try! process.waitUntilExit().utf8Output()
174174
}
175-
175+
176176
let t2 = Thread {
177177
result2 = try! process.waitUntilExit().utf8Output()
178178
}
179-
179+
180180
t1.start()
181181
t2.start()
182182
t1.join()
183183
t2.join()
184-
184+
185185
XCTAssertEqual(result1, "hello\n")
186186
XCTAssertEqual(result2, "hello\n")
187187
}
@@ -210,4 +210,33 @@ class ProcessTests: XCTestCase {
210210
XCTAssertEqual(try result.utf8stderrOutput(), String(repeating: "2", count: count))
211211
}
212212
}
213+
214+
#if os(macOS)
215+
func testWorkingDirectory() throws {
216+
if #available(macOS 10.15, *) {
217+
try! withTemporaryDirectory(removeTreeOnDeinit: true) { tempDirPath in
218+
let parentPath = tempDirPath.appending(component: "file")
219+
let childPath = tempDirPath.appending(component: "subdir").appending(component: "file")
220+
221+
try localFileSystem.writeFileContents(parentPath, bytes: ByteString("parent"))
222+
try localFileSystem.createDirectory(childPath.parentDirectory, recursive: true)
223+
try localFileSystem.writeFileContents(childPath, bytes: ByteString("child"))
224+
225+
do {
226+
let process = Process(arguments: ["cat", "file"], workingDirectory: tempDirPath)
227+
try process.launch()
228+
let result = try process.waitUntilExit()
229+
XCTAssertEqual(try result.utf8Output(), "parent")
230+
}
231+
232+
do {
233+
let process = Process(arguments: ["cat", "file"], workingDirectory: childPath.parentDirectory)
234+
try process.launch()
235+
let result = try process.waitUntilExit()
236+
XCTAssertEqual(try result.utf8Output(), "child")
237+
}
238+
}
239+
}
240+
}
241+
#endif
213242
}

0 commit comments

Comments
 (0)