1414
1515import Dispatch
1616import PackagePlugin
17-
18- #if canImport(FoundationEssentials)
19- import FoundationEssentials
20- #else
21- import struct Foundation. URL
22- import struct Foundation. Data
23- import struct Foundation. CharacterSet
24- import class Foundation. Process
25- import class Foundation. Pipe
26- #endif
17+ import Synchronization
18+ import Foundation
2719
2820struct Utils {
2921 @discardableResult
3022 static func execute(
3123 executable: URL ,
3224 arguments: [ String ] ,
33- customWorkingDirectory: URL ? = nil ,
25+ customWorkingDirectory: URL ? = . none ,
3426 logLevel: ProcessLogLevel
3527 ) throws -> String {
3628 if logLevel >= . debug {
3729 print ( " \( executable. absoluteString) \( arguments. joined ( separator: " " ) ) " )
3830 }
3931
40- // this shared global variable is safe because we're mutating it in a dispatch group
41- // https://developer.apple.com/documentation/foundation/process/1408746-terminationhandler
42- nonisolated ( unsafe) var output = " "
32+ let fd = dup ( 1 )
33+ let stdout = fdopen ( fd, " rw " )
34+ defer { fclose ( stdout) }
35+
36+ // We need to use an unsafe transfer here to get the fd into our Sendable closure.
37+ // This transfer is fine, because we guarantee that the code in the outputHandler
38+ // is run before we continue the functions execution, where the fd is used again.
39+ // See `process.waitUntilExit()` and the following `outputSync.wait()`
40+ struct UnsafeTransfer < Value> : @unchecked Sendable {
41+ let value : Value
42+ }
43+
44+ let outputMutex = Mutex ( " " )
4345 let outputSync = DispatchGroup ( )
44- let outputQueue = DispatchQueue ( label: " AWSLambdaPlugin.output " )
46+ let outputQueue = DispatchQueue ( label: " AWSLambdaPackager.output " )
47+ let unsafeTransfer = UnsafeTransfer ( value: stdout)
4548 let outputHandler = { @Sendable ( data: Data? ) in
4649 dispatchPrecondition ( condition: . onQueue( outputQueue) )
4750
4851 outputSync. enter ( )
4952 defer { outputSync. leave ( ) }
5053
51- guard let _output = data. flatMap ( { String ( decoding: $0, as: UTF8 . self) . trimmingCharacters ( in: CharacterSet ( [ " \n " ] ) ) } ) , !_output. isEmpty else {
54+ guard
55+ let _output = data. flatMap ( {
56+ String ( data: $0, encoding: . utf8) ? . trimmingCharacters ( in: CharacterSet ( [ " \n " ] ) )
57+ } ) , !_output. isEmpty
58+ else {
5259 return
5360 }
5461
55- output += _output + " \n "
62+ outputMutex. withLock { output in
63+ output += _output + " \n "
64+ }
5665
5766 switch logLevel {
5867 case . silent:
5968 break
6069 case . debug( let outputIndent) , . output( let outputIndent) :
6170 print ( String ( repeating: " " , count: outputIndent) , terminator: " " )
6271 print ( _output)
63- fflush ( stdout )
72+ fflush ( unsafeTransfer . value )
6473 }
6574 }
6675
6776 let pipe = Pipe ( )
6877 pipe. fileHandleForReading. readabilityHandler = { fileHandle in
69- outputQueue. async {
70- outputHandler ( fileHandle. availableData)
71- }
78+ outputQueue. async { outputHandler ( fileHandle. availableData) }
7279 }
7380
7481 let process = Process ( )
7582 process. standardOutput = pipe
7683 process. standardError = pipe
77- process. executableURL = executable
84+ process. executableURL = URL ( fileURLWithPath : executable. description )
7885 process. arguments = arguments
79- if let customWorkingDirectory {
80- process. currentDirectoryURL = customWorkingDirectory
86+ if let workingDirectory = customWorkingDirectory {
87+ process. currentDirectoryURL = URL ( fileURLWithPath : workingDirectory . path ( ) )
8188 }
8289 process. terminationHandler = { _ in
8390 outputQueue. async {
@@ -91,24 +98,26 @@ struct Utils {
9198 // wait for output to be full processed
9299 outputSync. wait ( )
93100
101+ let output = outputMutex. withLock { $0 }
102+
94103 if process. terminationStatus != 0 {
95104 // print output on failure and if not already printed
96105 if logLevel < . output {
97106 print ( output)
98107 fflush ( stdout)
99108 }
100- throw ProcessError . processFailed ( [ executable. absoluteString ] + arguments, process. terminationStatus, output )
109+ throw ProcessError . processFailed ( [ executable. path ( ) ] + arguments, process. terminationStatus)
101110 }
102111
103112 return output
104113 }
105114
106115 enum ProcessError : Error , CustomStringConvertible {
107- case processFailed( [ String ] , Int32 , String )
116+ case processFailed( [ String ] , Int32 )
108117
109118 var description : String {
110119 switch self {
111- case . processFailed( let arguments, let code, _ ) :
120+ case . processFailed( let arguments, let code) :
112121 return " \( arguments. joined ( separator: " " ) ) failed with code \( code) "
113122 }
114123 }
0 commit comments