@@ -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
@@ -118,6 +120,7 @@ extension SwiftCommand {
118
120
} catch {
119
121
toolError = error
120
122
}
123
+ try swiftTool. releaseLockIfNeeded ( )
121
124
122
125
// wait for all observability items to process
123
126
swiftTool. waitForObservabilityEvents ( timeout: . now( ) + 5 )
@@ -396,6 +399,9 @@ public final class SwiftTool {
396
399
return workspace
397
400
}
398
401
402
+ // Before creating the workspace, we need to acquire a lock on the build directory.
403
+ try self . acquireLockIfNeeded ( )
404
+
399
405
if options. resolver. skipDependencyUpdate {
400
406
self . observabilityScope. emit ( warning: " '--skip-update' option is deprecated and will be removed in a future release " )
401
407
}
@@ -866,6 +872,34 @@ public final class SwiftTool {
866
872
case success
867
873
case failure
868
874
}
875
+
876
+ // MARK: - Locking
877
+
878
+ private var workspaceLock : FileLock ?
879
+
880
+ fileprivate func acquireLockIfNeeded( ) throws {
881
+ guard workspaceLock == nil else {
882
+ throw InternalError ( " acquireLockIfNeeded() called multiple times " )
883
+ }
884
+ let workspaceLock = try FileLock . prepareLock ( fileToLock: self . scratchDirectory)
885
+
886
+ // Try a non-blocking lock first so that we can inform the user about an already running SwiftPM.
887
+ do {
888
+ try workspaceLock. lock ( type: . exclusive, blocking: false )
889
+ } catch let ProcessLockError . unableToAquireLock( errno) {
890
+ if errno == EWOULDBLOCK {
891
+ self . outputStream. write ( " Another instance of SwiftPM is already running using ' \( self . scratchDirectory) ', waiting until that process has finished execution... " . utf8)
892
+ self . outputStream. flush ( )
893
+ }
894
+ }
895
+
896
+ try workspaceLock. lock ( type: . exclusive, blocking: true )
897
+ self . workspaceLock = workspaceLock
898
+ }
899
+
900
+ fileprivate func releaseLockIfNeeded( ) {
901
+ workspaceLock? . unlock ( )
902
+ }
869
903
}
870
904
871
905
/// Returns path of the nearest directory containing the manifest file w.r.t
0 commit comments