@@ -26,6 +26,16 @@ typealias ProcessID = HANDLE
2626typealias ProcessID = Never
2727#endif
2828
29+ /// A platform-specific wrapper type for various types used by `posix_spawn()`.
30+ ///
31+ /// Darwin and Linux differ in their optionality for the `posix_spawn()` types
32+ /// we use, so we use this typealias to paper over the differences.
33+ #if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD)
34+ fileprivate typealias P < T> = T ?
35+ #elseif os(Linux)
36+ fileprivate typealias P < T> = T
37+ #endif
38+
2939#if os(Linux) && !SWT_NO_DYNAMIC_LINKING
3040/// Close file descriptors above a given value when spawing a new process.
3141///
@@ -38,13 +48,29 @@ private let _posix_spawn_file_actions_addclosefrom_np = symbol(named: "posix_spa
3848}
3949#endif
4050
41- /// Spawn a process and wait for it to terminate.
51+ #if !SWT_NO_DYNAMIC_LINKING
52+ /// Change the current directory in the new process.
53+ ///
54+ /// This symbol is provided because `posix_spawn_file_actions_addchdir()` and
55+ /// its non-portable equivalent `posix_spawn_file_actions_addchdir_np()` are not
56+ /// consistently available across all platforms that implement the
57+ /// `posix_spawn()` API.
58+ private let _posix_spawn_file_actions_addchdir = symbol ( named: " posix_spawn_file_actions_addchdir " ) . map {
59+ castCFunction ( at: $0, to: ( @convention( c) ( UnsafeMutablePointer < P < posix_spawn_file_actions_t > > , UnsafePointer < CChar > ) - > CInt) . self)
60+ } ?? symbol ( named: " posix_spawn_file_actions_addchdir_np " ) . map {
61+ castCFunction ( at: $0, to: ( @convention( c) ( UnsafeMutablePointer < P < posix_spawn_file_actions_t > > , UnsafePointer < CChar > ) - > CInt) . self)
62+ }
63+ #endif
64+
65+ /// Spawn a child process.
4266///
4367/// - Parameters:
4468/// - executablePath: The path to the executable to spawn.
4569/// - arguments: The arguments to pass to the executable, not including the
4670/// executable path.
4771/// - environment: The environment block to pass to the executable.
72+ /// - currentDirectoryPath: The path to use as the executable's initial
73+ /// working directory.
4874/// - standardInput: If not `nil`, a file handle the child process should
4975/// inherit as its standard input stream. This file handle must be backed
5076/// by a file descriptor and be open for reading.
@@ -61,39 +87,33 @@ private let _posix_spawn_file_actions_addclosefrom_np = symbol(named: "posix_spa
6187/// eventually pass this value to ``wait(for:)`` to avoid leaking system
6288/// resources.
6389///
64- /// - Throws: Any error that prevented the process from spawning or its exit
65- /// condition from being read.
90+ /// - Throws: Any error that prevented the process from spawning.
6691func spawnExecutable(
6792 atPath executablePath: String ,
6893 arguments: [ String ] ,
6994 environment: [ String : String ] ,
95+ currentDirectoryPath: String ? = nil ,
7096 standardInput: borrowing FileHandle ? = nil ,
7197 standardOutput: borrowing FileHandle ? = nil ,
7298 standardError: borrowing FileHandle ? = nil ,
7399 additionalFileHandles: [ UnsafePointer < FileHandle > ] = [ ]
74100) throws -> ProcessID {
75- // Darwin and Linux differ in their optionality for the posix_spawn types we
76- // use, so use this typealias to paper over the differences.
77- #if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD)
78- typealias P < T> = T ?
79- #elseif os(Linux)
80- typealias P < T> = T
81- #endif
82-
83101#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD)
84102 return try withUnsafeTemporaryAllocation ( of: P< posix_spawn_file_actions_t> . self , capacity: 1 ) { fileActions in
85103 let fileActions = fileActions. baseAddress!
86- guard 0 == posix_spawn_file_actions_init ( fileActions) else {
87- throw CError ( rawValue: swt_errno ( ) )
104+ let fileActionsInitialized = posix_spawn_file_actions_init ( fileActions)
105+ guard 0 == fileActionsInitialized else {
106+ throw CError ( rawValue: fileActionsInitialized)
88107 }
89108 defer {
90109 _ = posix_spawn_file_actions_destroy ( fileActions)
91110 }
92111
93112 return try withUnsafeTemporaryAllocation ( of: P< posix_spawnattr_t> . self , capacity: 1 ) { attrs in
94113 let attrs = attrs. baseAddress!
95- guard 0 == posix_spawnattr_init ( attrs) else {
96- throw CError ( rawValue: swt_errno ( ) )
114+ let attrsInitialized = posix_spawnattr_init ( attrs)
115+ guard 0 == attrsInitialized else {
116+ throw CError ( rawValue: attrsInitialized)
97117 }
98118 defer {
99119 _ = posix_spawnattr_destroy ( attrs)
@@ -116,6 +136,25 @@ func spawnExecutable(
116136 flags |= CShort ( POSIX_SPAWN_SETSIGDEF)
117137 }
118138
139+ // Set the current working directory (do this as early as possible, so
140+ // before inheriting or opening any file descriptors.)
141+ if let currentDirectoryPath {
142+ if let _posix_spawn_file_actions_addchdir {
143+ let directoryChanged = _posix_spawn_file_actions_addchdir ( fileActions, currentDirectoryPath)
144+ guard 0 == directoryChanged else {
145+ throw CError ( rawValue: directoryChanged)
146+ }
147+ } else {
148+ // This platform does not support setting the current directory via
149+ // posix_spawn(), so set it here in the parent process and hope
150+ // another thread does not stomp on the change (as Foundation does.)
151+ // Platforms known to take this path: Amazon Linux 2, OpenBSD, QNX
152+ guard 0 == chdir ( currentDirectoryPath) else {
153+ throw CError ( rawValue: swt_errno ( ) )
154+ }
155+ }
156+ }
157+
119158 // Forward standard I/O streams and any explicitly added file handles.
120159 var highestFD = max ( STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO)
121160 func inherit( _ fileHandle: borrowing FileHandle , as standardFD: CInt ? = nil ) throws {
@@ -262,6 +301,7 @@ func spawnExecutable(
262301
263302 let commandLine = _escapeCommandLine ( CollectionOfOne ( executablePath) + arguments)
264303 let environ = environment. map { " \( $0. key) = \( $0. value) " } . joined ( separator: " \0 " ) + " \0 \0 "
304+ let currentDirectoryPath = currentDirectoryPath. map { Array ( $0. utf16) }
265305
266306 var flags = DWORD ( CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT)
267307#if DEBUG
@@ -281,7 +321,7 @@ func spawnExecutable(
281321 true , // bInheritHandles
282322 flags,
283323 . init( mutating: environ) ,
284- nil ,
324+ currentDirectoryPath ,
285325 startupInfo. pointer ( to: \. StartupInfo) !,
286326 & processInfo
287327 ) else {
@@ -396,4 +436,32 @@ private func _escapeCommandLine(_ arguments: [String]) -> String {
396436 } . joined ( separator: " " )
397437}
398438#endif
439+
440+ /// Spawn a child process and wait for it to terminate.
441+ ///
442+ /// - Parameters:
443+ /// - executablePath: The path to the executable to spawn.
444+ /// - arguments: The arguments to pass to the executable, not including the
445+ /// executable path.
446+ /// - environment: The environment block to pass to the executable.
447+ /// - currentDirectoryPath: The path to use as the executable's initial
448+ /// working directory.
449+ ///
450+ /// - Returns: The exit status of the spawned process.
451+ ///
452+ /// - Throws: Any error that prevented the process from spawning or its exit
453+ /// condition from being read.
454+ ///
455+ /// This function is a convenience that spawns the given process and waits for
456+ /// it to terminate. It is primarily for use by other targets in this package
457+ /// such as its cross-import overlays.
458+ package func spawnExecutableAtPathAndWait(
459+ atPath executablePath: String ,
460+ arguments: [ String ] = [ ] ,
461+ environment: [ String : String ] = [ : ] ,
462+ currentDirectoryPath: String ? = nil
463+ ) async throws -> ExitStatus {
464+ let processID = try spawnExecutable ( atPath: executablePath, arguments: arguments, environment: environment)
465+ return try await wait ( for: processID)
466+ }
399467#endif
0 commit comments