Skip to content

Commit f4bbde8

Browse files
authored
Merge pull request #2171 from aciidb0mb3r/watch-resolved-file
[Workspace] Add ability to notify when Package.resolved file is changed
2 parents 1e93297 + b6ecad4 commit f4bbde8

File tree

3 files changed

+86
-22
lines changed

3 files changed

+86
-22
lines changed

Sources/SPMUtility/FSWatch.swift

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,35 +13,23 @@ import Dispatch
1313
import Foundation
1414
import SPMLibc
1515

16-
public protocol FSWatchDelegate {
17-
// FIXME: We need to provide richer information about the events.
18-
func pathsDidReceiveEvent(_ paths: [AbsolutePath])
19-
}
20-
2116
/// FSWatch is a cross-platform filesystem watching utility.
2217
public class FSWatch {
2318

24-
/// Delegate for handling events from the underling watcher.
25-
fileprivate class _WatcherDelegate {
26-
27-
/// Back reference to the fswatch instance.
28-
unowned let fsWatch: FSWatch
19+
public typealias EventReceivedBlock = (_ paths: [AbsolutePath]) -> Void
2920

30-
init(_ fsWatch: FSWatch) {
31-
self.fsWatch = fsWatch
32-
}
21+
/// Delegate for handling events from the underling watcher.
22+
fileprivate struct _WatcherDelegate {
23+
let block: EventReceivedBlock
3324

3425
func pathsDidReceiveEvent(_ paths: [AbsolutePath]) {
35-
fsWatch.delegate.pathsDidReceiveEvent(paths)
26+
block(paths)
3627
}
3728
}
3829

3930
/// The paths being watched.
4031
public let paths: [AbsolutePath]
4132

42-
/// The delegate for reporting received events.
43-
let delegate: FSWatchDelegate
44-
4533
/// The underlying file watching utility.
4634
///
4735
/// This is FSEventStream on macOS and inotify on linux.
@@ -54,10 +42,9 @@ public class FSWatch {
5442
/// Create an instance with given paths.
5543
///
5644
/// Paths can be files or directories. Directories are watched recursively.
57-
public init(paths: [AbsolutePath], latency: Double = 1, delegate: FSWatchDelegate) {
45+
public init(paths: [AbsolutePath], latency: Double = 1, block: @escaping EventReceivedBlock) {
5846
precondition(!paths.isEmpty)
5947
self.paths = paths
60-
self.delegate = delegate
6148
self.latency = latency
6249

6350
#if canImport(Glibc)
@@ -75,9 +62,9 @@ public class FSWatch {
7562
}
7663
}
7764

78-
self._watcher = Inotify(paths: ipaths, latency: latency, delegate: _WatcherDelegate(self))
65+
self._watcher = Inotify(paths: ipaths, latency: latency, delegate: _WatcherDelegate(block: block))
7966
#elseif os(macOS)
80-
self._watcher = FSEventStream(paths: paths, latency: latency, delegate: _WatcherDelegate(self))
67+
self._watcher = FSEventStream(paths: paths, latency: latency, delegate: _WatcherDelegate(block: block))
8168
#else
8269
fatalError("Unsupported platform")
8370
#endif
@@ -603,7 +590,10 @@ public final class FSEventStream {
603590

604591
/// Stop watching the events.
605592
public func stop() {
606-
CFRunLoopStop(runLoop!)
593+
// FIXME: This is probably not thread safe?
594+
if let runLoop = self.runLoop {
595+
CFRunLoopStop(runLoop)
596+
}
607597
}
608598
}
609599
#endif

Sources/Workspace/PinsStore.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,48 @@ extension PinsStore.Pin: JSONMappable, JSONSerializable, Equatable {
182182
])
183183
}
184184
}
185+
186+
/// A file watcher utility for the Package.resolved file.
187+
///
188+
/// This is not intended to be used directly by clients.
189+
final class ResolvedFileWatcher {
190+
private var fswatch: FSWatch!
191+
private var existingValue: ByteString?
192+
private let valueLock: Lock = Lock()
193+
private let resolvedFile: AbsolutePath
194+
195+
public func updateValue() {
196+
valueLock.withLock {
197+
self.existingValue = try? localFileSystem.readFileContents(resolvedFile)
198+
}
199+
}
200+
201+
init(resolvedFile: AbsolutePath, onChange: @escaping () -> ()) throws {
202+
self.resolvedFile = resolvedFile
203+
204+
let block = { [weak self] (paths: [AbsolutePath]) in
205+
guard let self = self else { return }
206+
207+
// Check if resolved file is part of the received paths.
208+
let hasResolvedFile = paths.contains{ $0.appending(component: resolvedFile.basename) == resolvedFile }
209+
guard hasResolvedFile else { return }
210+
211+
self.valueLock.withLock {
212+
// Compute the contents of the resolved file and fire the onChange block
213+
// if its value is different than existing value.
214+
let newValue = try? localFileSystem.readFileContents(resolvedFile)
215+
if self.existingValue != newValue {
216+
self.existingValue = newValue
217+
onChange()
218+
}
219+
}
220+
}
221+
222+
fswatch = FSWatch(paths: [resolvedFile.parentDirectory], latency: 1, block: block)
223+
try fswatch.start()
224+
}
225+
226+
deinit {
227+
fswatch.stop()
228+
}
229+
}

Sources/Workspace/Workspace.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ public protocol WorkspaceDelegate: class {
4545

4646
/// Called when the resolver is about to be run.
4747
func willResolveDependencies()
48+
49+
/// Called when the Package.resolved file is changed *outside* of libSwiftPM operations.
50+
///
51+
/// This is only fired when activated using Workspace's watchResolvedFile() method.
52+
func resolvedFileChanged()
4853
}
4954

5055
public extension WorkspaceDelegate {
@@ -53,6 +58,7 @@ public extension WorkspaceDelegate {
5358
func repositoryDidUpdate(_ repository: String) {}
5459
func willResolveDependencies() {}
5560
func dependenciesUpToDate() {}
61+
func resolvedFileChanged() {}
5662
}
5763

5864
private class WorkspaceResolverDelegate: DependencyResolverDelegate {
@@ -252,6 +258,9 @@ public class Workspace {
252258
/// The Pins store. The pins file will be created when first pin is added to pins store.
253259
public let pinsStore: LoadableResult<PinsStore>
254260

261+
/// The path to the Package.resolved file for this workspace.
262+
public let resolvedFile: AbsolutePath
263+
255264
/// The path for working repository clones (checkouts).
256265
public let checkoutsPath: AbsolutePath
257266

@@ -291,6 +300,8 @@ public class Workspace {
291300
/// Write dependency resolver trace to a file.
292301
fileprivate let enableResolverTrace: Bool
293302

303+
fileprivate var resolvedFileWatcher: ResolvedFileWatcher?
304+
294305
/// Typealias for dependency resolver we use in the workspace.
295306
fileprivate typealias PackageDependencyResolver = DependencyResolver
296307
fileprivate typealias PubgrubResolver = PubgrubDependencyResolver
@@ -336,6 +347,7 @@ public class Workspace {
336347
self.enablePubgrubResolver = enablePubgrubResolver
337348
self.skipUpdate = skipUpdate
338349
self.enableResolverTrace = enableResolverTrace
350+
self.resolvedFile = pinsFile
339351

340352
let repositoriesPath = self.dataPath.appending(component: "repositories")
341353
self.repositoryManager = RepositoryManager(
@@ -904,13 +916,30 @@ extension Workspace {
904916
}
905917
}
906918
diagnostics.wrap({ try pinsStore.saveState() })
919+
920+
// Ask resolved file watcher to update its value so we don't fire
921+
// an extra event if the file was modified by us.
922+
self.resolvedFileWatcher?.updateValue()
907923
}
908924
}
909925

910926
// MARK: - Utility Functions
911927

912928
extension Workspace {
913929

930+
/// Watch the Package.resolved for changes.
931+
///
932+
/// This is useful if clients want to be notified when the Package.resolved
933+
/// file is changed *outside* of libSwiftPM operations. For example, as part
934+
/// of a git operation.
935+
public func watchResolvedFile() throws {
936+
// Return if we're already watching it.
937+
guard self.resolvedFileWatcher == nil else { return }
938+
self.resolvedFileWatcher = try ResolvedFileWatcher(resolvedFile: self.resolvedFile) { [weak self] in
939+
self?.delegate?.resolvedFileChanged()
940+
}
941+
}
942+
914943
/// Create the cache directories.
915944
fileprivate func createCacheDirectories(with diagnostics: DiagnosticsEngine) {
916945
do {

0 commit comments

Comments
 (0)