14
14
15
15
import Dispatch
16
16
import 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
27
19
28
20
struct Utils {
29
21
@discardableResult
30
22
static func execute(
31
23
executable: URL ,
32
24
arguments: [ String ] ,
33
- customWorkingDirectory: URL ? = nil ,
25
+ customWorkingDirectory: URL ? = . none ,
34
26
logLevel: ProcessLogLevel
35
27
) throws -> String {
36
28
if logLevel >= . debug {
37
29
print ( " \( executable. absoluteString) \( arguments. joined ( separator: " " ) ) " )
38
30
}
39
31
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 ( " " )
43
45
let outputSync = DispatchGroup ( )
44
- let outputQueue = DispatchQueue ( label: " AWSLambdaPlugin.output " )
46
+ let outputQueue = DispatchQueue ( label: " AWSLambdaPackager.output " )
47
+ let unsafeTransfer = UnsafeTransfer ( value: stdout)
45
48
let outputHandler = { @Sendable ( data: Data? ) in
46
49
dispatchPrecondition ( condition: . onQueue( outputQueue) )
47
50
48
51
outputSync. enter ( )
49
52
defer { outputSync. leave ( ) }
50
53
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 {
52
59
return
53
60
}
54
61
55
- output += _output + " \n "
62
+ outputMutex. withLock { output in
63
+ output += _output + " \n "
64
+ }
56
65
57
66
switch logLevel {
58
67
case . silent:
59
68
break
60
69
case . debug( let outputIndent) , . output( let outputIndent) :
61
70
print ( String ( repeating: " " , count: outputIndent) , terminator: " " )
62
71
print ( _output)
63
- fflush ( stdout )
72
+ fflush ( unsafeTransfer . value )
64
73
}
65
74
}
66
75
67
76
let pipe = Pipe ( )
68
77
pipe. fileHandleForReading. readabilityHandler = { fileHandle in
69
- outputQueue. async {
70
- outputHandler ( fileHandle. availableData)
71
- }
78
+ outputQueue. async { outputHandler ( fileHandle. availableData) }
72
79
}
73
80
74
81
let process = Process ( )
75
82
process. standardOutput = pipe
76
83
process. standardError = pipe
77
- process. executableURL = executable
84
+ process. executableURL = URL ( fileURLWithPath : executable. description )
78
85
process. arguments = arguments
79
- if let customWorkingDirectory {
80
- process. currentDirectoryURL = customWorkingDirectory
86
+ if let workingDirectory = customWorkingDirectory {
87
+ process. currentDirectoryURL = URL ( fileURLWithPath : workingDirectory . path ( ) )
81
88
}
82
89
process. terminationHandler = { _ in
83
90
outputQueue. async {
@@ -91,24 +98,26 @@ struct Utils {
91
98
// wait for output to be full processed
92
99
outputSync. wait ( )
93
100
101
+ let output = outputMutex. withLock { $0 }
102
+
94
103
if process. terminationStatus != 0 {
95
104
// print output on failure and if not already printed
96
105
if logLevel < . output {
97
106
print ( output)
98
107
fflush ( stdout)
99
108
}
100
- throw ProcessError . processFailed ( [ executable. absoluteString ] + arguments, process. terminationStatus, output )
109
+ throw ProcessError . processFailed ( [ executable. path ( ) ] + arguments, process. terminationStatus)
101
110
}
102
111
103
112
return output
104
113
}
105
114
106
115
enum ProcessError : Error , CustomStringConvertible {
107
- case processFailed( [ String ] , Int32 , String )
116
+ case processFailed( [ String ] , Int32 )
108
117
109
118
var description : String {
110
119
switch self {
111
- case . processFailed( let arguments, let code, _ ) :
120
+ case . processFailed( let arguments, let code) :
112
121
return " \( arguments. joined ( separator: " " ) ) failed with code \( code) "
113
122
}
114
123
}
0 commit comments