@@ -39,9 +39,11 @@ import Musl
39
39
#endif
40
40
41
41
import func TSCBasic. exec
42
+ import class TSCBasic. FileLock
42
43
import protocol TSCBasic. OutputByteStream
43
44
import class TSCBasic. Process
44
45
import enum TSCBasic. ProcessEnv
46
+ import enum TSCBasic. ProcessLockError
45
47
import var TSCBasic. stderrStream
46
48
import class TSCBasic. TerminalController
47
49
import class TSCBasic. ThreadSafeOutputByteStream
@@ -80,6 +82,7 @@ public typealias WorkspaceLoaderProvider = (_ fileSystem: FileSystem, _ observab
80
82
-> WorkspaceLoader
81
83
82
84
public protocol _SwiftCommand {
85
+ var globalOptions : GlobalOptions { get }
83
86
var toolWorkspaceConfiguration : ToolWorkspaceConfiguration { get }
84
87
var workspaceDelegateProvider : WorkspaceDelegateProvider { get }
85
88
var workspaceLoaderProvider : WorkspaceLoaderProvider { get }
@@ -93,8 +96,6 @@ extension _SwiftCommand {
93
96
}
94
97
95
98
public protocol SwiftCommand : ParsableCommand , _SwiftCommand {
96
- var globalOptions : GlobalOptions { get }
97
-
98
99
func run( _ swiftTool: SwiftTool ) throws
99
100
}
100
101
@@ -108,6 +109,10 @@ extension SwiftCommand {
108
109
workspaceDelegateProvider: self . workspaceDelegateProvider,
109
110
workspaceLoaderProvider: self . workspaceLoaderProvider
110
111
)
112
+
113
+ // We use this to attempt to catch misuse of the locking APIs since we only release the lock from here.
114
+ swiftTool. setNeedsLocking ( )
115
+
111
116
swiftTool. buildSystemProvider = try buildSystemProvider ( swiftTool)
112
117
var toolError : Error ? = . none
113
118
do {
@@ -119,6 +124,8 @@ extension SwiftCommand {
119
124
toolError = error
120
125
}
121
126
127
+ swiftTool. releaseLockIfNeeded ( )
128
+
122
129
// wait for all observability items to process
123
130
swiftTool. waitForObservabilityEvents ( timeout: . now( ) + 5 )
124
131
@@ -129,21 +136,24 @@ extension SwiftCommand {
129
136
}
130
137
131
138
public protocol AsyncSwiftCommand : AsyncParsableCommand , _SwiftCommand {
132
- var globalOptions : GlobalOptions { get }
133
-
134
139
func run( _ swiftTool: SwiftTool ) async throws
135
140
}
136
141
137
142
extension AsyncSwiftCommand {
138
143
public static var _errorLabel : String { " error " }
139
144
145
+ // FIXME: It doesn't seem great to have this be duplicated with `SwiftCommand`.
140
146
public func run( ) async throws {
141
147
let swiftTool = try SwiftTool (
142
148
options: globalOptions,
143
149
toolWorkspaceConfiguration: self . toolWorkspaceConfiguration,
144
150
workspaceDelegateProvider: self . workspaceDelegateProvider,
145
151
workspaceLoaderProvider: self . workspaceLoaderProvider
146
152
)
153
+
154
+ // We use this to attempt to catch misuse of the locking APIs since we only release the lock from here.
155
+ swiftTool. setNeedsLocking ( )
156
+
147
157
swiftTool. buildSystemProvider = try buildSystemProvider ( swiftTool)
148
158
var toolError : Error ? = . none
149
159
do {
@@ -155,6 +165,8 @@ extension AsyncSwiftCommand {
155
165
toolError = error
156
166
}
157
167
168
+ swiftTool. releaseLockIfNeeded ( )
169
+
158
170
// wait for all observability items to process
159
171
swiftTool. waitForObservabilityEvents ( timeout: . now( ) + 5 )
160
172
@@ -396,6 +408,9 @@ public final class SwiftTool {
396
408
return workspace
397
409
}
398
410
411
+ // Before creating the workspace, we need to acquire a lock on the build directory.
412
+ try self . acquireLockIfNeeded ( )
413
+
399
414
if options. resolver. skipDependencyUpdate {
400
415
self . observabilityScope. emit ( warning: " '--skip-update' option is deprecated and will be removed in a future release " )
401
416
}
@@ -866,6 +881,57 @@ public final class SwiftTool {
866
881
case success
867
882
case failure
868
883
}
884
+
885
+ // MARK: - Locking
886
+
887
+ // This is used to attempt to prevent accidental misuse of the locking APIs.
888
+ private enum WorkspaceLockState {
889
+ case unspecified
890
+ case needsLocking
891
+ case locked
892
+ case unlocked
893
+ }
894
+
895
+ private var workspaceLockState : WorkspaceLockState = . unspecified
896
+ private var workspaceLock : FileLock ?
897
+
898
+ fileprivate func setNeedsLocking( ) {
899
+ assert ( workspaceLockState == . unspecified, " attempting to `setNeedsLocking()` from unexpected state: \( workspaceLockState) " )
900
+ workspaceLockState = . needsLocking
901
+ }
902
+
903
+ fileprivate func acquireLockIfNeeded( ) throws {
904
+ assert ( workspaceLockState == . needsLocking, " attempting to `acquireLockIfNeeded()` from unexpected state: \( workspaceLockState) " )
905
+ guard workspaceLock == nil else {
906
+ throw InternalError ( " acquireLockIfNeeded() called multiple times " )
907
+ }
908
+ workspaceLockState = . locked
909
+
910
+ let workspaceLock = try FileLock . prepareLock ( fileToLock: self . scratchDirectory)
911
+
912
+ // Try a non-blocking lock first so that we can inform the user about an already running SwiftPM.
913
+ do {
914
+ try workspaceLock. lock ( type: . exclusive, blocking: false )
915
+ } catch let ProcessLockError . unableToAquireLock( errno) {
916
+ if errno == EWOULDBLOCK {
917
+ self . outputStream. write ( " Another instance of SwiftPM is already running using ' \( self . scratchDirectory) ', waiting until that process has finished execution... " . utf8)
918
+ self . outputStream. flush ( )
919
+
920
+ // Only if we fail because there's an existing lock we need to acquire again as blocking.
921
+ try workspaceLock. lock ( type: . exclusive, blocking: true )
922
+ }
923
+ }
924
+
925
+ self . workspaceLock = workspaceLock
926
+ }
927
+
928
+ fileprivate func releaseLockIfNeeded( ) {
929
+ // Never having acquired the lock is not an error case.
930
+ assert ( workspaceLockState == . locked || workspaceLockState == . needsLocking, " attempting to `releaseLockIfNeeded()` from unexpected state: \( workspaceLockState) " )
931
+ workspaceLockState = . unlocked
932
+
933
+ workspaceLock? . unlock ( )
934
+ }
869
935
}
870
936
871
937
/// Returns path of the nearest directory containing the manifest file w.r.t
0 commit comments