@@ -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,36 @@ 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
+ // Only if we fail because there's an existing lock we need to acquire again as blocking.
895
+ try workspaceLock. lock ( type: . exclusive, blocking: true )
896
+ }
897
+ }
898
+
899
+ self . workspaceLock = workspaceLock
900
+ }
901
+
902
+ fileprivate func releaseLockIfNeeded( ) {
903
+ workspaceLock? . unlock ( )
904
+ }
869
905
}
870
906
871
907
/// Returns path of the nearest directory containing the manifest file w.r.t
0 commit comments