1111//===----------------------------------------------------------------------===//
1212
1313public import Foundation
14- import SWBLibc
14+ public import SWBLibc
15+ import Synchronization
1516
16- #if os(Windows )
17- public typealias pid_t = Int32
17+ #if canImport(Subprocess) && (!canImport(Darwin) || os(macOS) )
18+ import Subprocess
1819#endif
1920
20- #if !canImport(Darwin)
21- extension ProcessInfo {
22- public var isMacCatalystApp : Bool {
23- false
24- }
25- }
21+ #if canImport(System)
22+ public import System
23+ #else
24+ public import SystemPackage
25+ #endif
26+
27+ #if os(Windows)
28+ public typealias pid_t = Int32
2629#endif
2730
2831#if (!canImport(Foundation.NSTask) || targetEnvironment(macCatalyst)) && canImport(Darwin)
@@ -64,7 +67,7 @@ public typealias Process = Foundation.Process
6467#endif
6568
6669extension Process {
67- public static var hasUnsafeWorkingDirectorySupport : Bool {
70+ fileprivate static var hasUnsafeWorkingDirectorySupport : Bool {
6871 get throws {
6972 switch try ProcessInfo . processInfo. hostOperatingSystem ( ) {
7073 case . linux:
@@ -81,6 +84,30 @@ extension Process {
8184
8285extension Process {
8386 public static func getOutput( url: URL , arguments: [ String ] , currentDirectoryURL: URL ? = nil , environment: Environment ? = nil , interruptible: Bool = true ) async throws -> Processes . ExecutionResult {
87+ #if canImport(Subprocess)
88+ #if !canImport(Darwin) || os(macOS)
89+ var platformOptions = PlatformOptions ( )
90+ if interruptible {
91+ platformOptions. teardownSequence = [ . gracefulShutDown( allowedDurationToNextStep: . seconds( 5 ) ) ]
92+ }
93+ let configuration = try Subprocess . Configuration (
94+ . path( FilePath ( url. filePath. str) ) ,
95+ arguments: . init( arguments) ,
96+ environment: environment. map { . custom( . init( $0) ) } ?? . inherit,
97+ workingDirectory: ( currentDirectoryURL? . filePath. str) . map { FilePath ( $0) } ?? nil ,
98+ platformOptions: platformOptions
99+ )
100+ let result = try await Subprocess . run ( configuration, body: { execution, inputWriter, outputReader, errorReader in
101+ async let stdoutBytes = outputReader. collect ( ) . flatMap { $0. withUnsafeBytes ( Array . init) }
102+ async let stderrBytes = errorReader. collect ( ) . flatMap { $0. withUnsafeBytes ( Array . init) }
103+ try await inputWriter. finish ( )
104+ return try await ( stdoutBytes, stderrBytes)
105+ } )
106+ return Processes . ExecutionResult ( exitStatus: . init( result. terminationStatus) , stdout: Data ( result. value. 0 ) , stderr: Data ( result. value. 1 ) )
107+ #else
108+ throw StubError . error ( " Process spawning is unavailable " )
109+ #endif
110+ #else
84111 if #available( macOS 15 , iOS 18 , tvOS 18 , watchOS 11 , visionOS 2 , * ) {
85112 let stdoutPipe = Pipe ( )
86113 let stderrPipe = Pipe ( )
@@ -118,9 +145,40 @@ extension Process {
118145 }
119146 return Processes . ExecutionResult ( exitStatus: exitStatus, stdout: Data ( output. stdoutData) , stderr: Data ( output. stderrData) )
120147 }
148+ #endif
121149 }
122150
123151 public static func getMergedOutput( url: URL , arguments: [ String ] , currentDirectoryURL: URL ? = nil , environment: Environment ? = nil , interruptible: Bool = true ) async throws -> ( exitStatus: Processes . ExitStatus , output: Data ) {
152+ #if canImport(Subprocess)
153+ #if !canImport(Darwin) || os(macOS)
154+ let ( readEnd, writeEnd) = try FileDescriptor . pipe ( )
155+ return try await readEnd. closeAfter {
156+ // Direct both stdout and stderr to the same fd. Only set `closeAfterSpawningProcess` on one of the outputs so it isn't double-closed (similarly avoid using closeAfter for the same reason).
157+ var platformOptions = PlatformOptions ( )
158+ if interruptible {
159+ platformOptions. teardownSequence = [ . gracefulShutDown( allowedDurationToNextStep: . seconds( 5 ) ) ]
160+ }
161+ let configuration = try Subprocess . Configuration (
162+ . path( FilePath ( url. filePath. str) ) ,
163+ arguments: . init( arguments) ,
164+ environment: environment. map { . custom( . init( $0) ) } ?? . inherit,
165+ workingDirectory: ( currentDirectoryURL? . filePath. str) . map { FilePath ( $0) } ?? nil ,
166+ platformOptions: platformOptions
167+ )
168+ // FIXME: Use new API from https://github.com/swiftlang/swift-subprocess/pull/180
169+ let result = try await Subprocess . run ( configuration, output: . fileDescriptor( writeEnd, closeAfterSpawningProcess: true ) , error: . fileDescriptor( writeEnd, closeAfterSpawningProcess: false ) , body: { execution in
170+ if #available( macOS 15 , iOS 18 , tvOS 18 , watchOS 11 , visionOS 2 , * ) {
171+ try await Array ( Data ( DispatchFD ( fileDescriptor: readEnd) . dataStream ( ) . collect ( ) ) )
172+ } else {
173+ try await Array ( Data ( DispatchFD ( fileDescriptor: readEnd) . _dataStream ( ) . collect ( ) ) )
174+ }
175+ } )
176+ return ( . init( result. terminationStatus) , Data ( result. value) )
177+ }
178+ #else
179+ throw StubError . error ( " Process spawning is unavailable " )
180+ #endif
181+ #else
124182 if #available( macOS 15 , iOS 18 , tvOS 18 , watchOS 11 , visionOS 2 , * ) {
125183 let pipe = Pipe ( )
126184
@@ -150,6 +208,7 @@ extension Process {
150208 }
151209 return ( exitStatus: exitStatus, output: Data ( output) )
152210 }
211+ #endif
153212 }
154213
155214 private static func _getOutput< T, U> ( url: URL , arguments: [ String ] , currentDirectoryURL: URL ? , environment: Environment ? , interruptible: Bool , setup: ( Process ) -> T , collect: @Sendable ( T) async throws -> U ) async throws -> ( exitStatus: Processes . ExitStatus , output: U ) {
@@ -221,9 +280,8 @@ public enum Processes: Sendable {
221280 case exit( _ code: Int32 )
222281 case uncaughtSignal( _ signal: Int32 )
223282
224- public init ? ( rawValue: Int32 ) {
225- #if os(Windows)
226- let dwExitCode = DWORD ( bitPattern: rawValue)
283+ #if os(Windows)
284+ public init ( dwExitCode: DWORD ) {
227285 // Do the same thing as swift-corelibs-foundation (the input value is the GetExitCodeProcess return value)
228286 if ( dwExitCode & 0xF0000000 ) == 0x80000000 // HRESULT
229287 || ( dwExitCode & 0xF0000000 ) == 0xC0000000 // NTSTATUS
@@ -233,6 +291,12 @@ public enum Processes: Sendable {
233291 } else {
234292 self = . exit( Int32 ( bitPattern: UInt32 ( dwExitCode) ) )
235293 }
294+ }
295+ #endif
296+
297+ public init ? ( rawValue: Int32 ) {
298+ #if os(Windows)
299+ self = . init( dwExitCode: DWORD ( bitPattern: rawValue) )
236300 #else
237301 func WSTOPSIG( _ status: Int32 ) -> Int32 {
238302 return status >> 8
@@ -312,6 +376,37 @@ public enum Processes: Sendable {
312376 }
313377}
314378
379+ #if canImport(Subprocess) && (!canImport(Darwin) || os(macOS))
380+ extension Processes . ExitStatus {
381+ init ( _ terminationStatus: TerminationStatus ) {
382+ switch terminationStatus {
383+ case let . exited( code) :
384+ self = . exit( numericCast ( code) )
385+ case let . unhandledException( code) :
386+ #if os(Windows)
387+ // Currently swift-subprocess returns the original raw GetExitCodeProcess value as uncaughtSignal for all values other than zero.
388+ // See also: https://github.com/swiftlang/swift-subprocess/issues/114
389+ self = . init( dwExitCode: code)
390+ #else
391+ self = . uncaughtSignal( code)
392+ #endif
393+ }
394+ }
395+ }
396+
397+ extension [ Subprocess . Environment . Key : String ] {
398+ internal init ( _ environment: Environment ) {
399+ self . init ( )
400+ let sorted = environment. sorted { $0. key < $1. key }
401+ for (key, value) in sorted {
402+ if let typedKey = Subprocess . Environment. Key ( rawValue: key. rawValue) {
403+ self [ typedKey] = value
404+ }
405+ }
406+ }
407+ }
408+ #endif
409+
315410extension Processes . ExitStatus {
316411 public init ( _ process: Process ) throws {
317412 assert ( !process. isRunning)
0 commit comments