Skip to content

Commit 894f7a9

Browse files
committed
Recursive globs in file monitoring
1 parent 5ac41c2 commit 894f7a9

14 files changed

Lines changed: 580 additions & 36 deletions

File tree

Cabal/src/Distribution/Simple/Glob/Internal.hs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ data Glob
3131
GlobDir !GlobPieces !Glob
3232
| -- | @**/<glob>@, where @**@ denotes recursively traversing
3333
-- all directories and matching filenames on <glob>.
34+
--
35+
-- Note that the @<glob>@ portion can only match on filenames, not paths,
36+
-- so for example @**/foo/*.txt@ is not supported.
3437
GlobDirRecursive !GlobPieces
3538
| -- | A file glob.
3639
GlobFile !GlobPieces
@@ -74,13 +77,6 @@ instance Pretty Glob where
7477
instance Parsec Glob where
7578
parsec = parsecPath
7679
where
77-
parsecPath :: CabalParsing m => m Glob
78-
parsecPath = do
79-
glob <- parsecGlob
80-
dirSep *> (GlobDir glob <$> parsecPath <|> pure (GlobDir glob GlobDirTrailing)) <|> pure (GlobFile glob)
81-
-- We could support parsing recursive directory search syntax
82-
-- @**@ here too, rather than just in 'parseFileGlob'
83-
8480
dirSep :: CabalParsing m => m ()
8581
dirSep =
8682
() <$ P.char '/'
@@ -91,6 +87,17 @@ instance Parsec Glob where
9187
P.notFollowedBy (P.satisfy isGlobEscapedChar)
9288
)
9389

90+
parsecPath :: CabalParsing m => m Glob
91+
parsecPath =
92+
P.choice
93+
[ do
94+
P.try (P.string "**" *> dirSep)
95+
GlobDirRecursive <$> parsecGlob
96+
, do
97+
glob <- parsecGlob
98+
dirSep *> (GlobDir glob <$> parsecPath <|> pure (GlobDir glob GlobDirTrailing)) <|> pure (GlobFile glob)
99+
]
100+
94101
parsecGlob :: CabalParsing m => m GlobPieces
95102
parsecGlob = some parsecPiece
96103
where

cabal-install/src/Distribution/Client/FileMonitor.hs

Lines changed: 231 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -127,17 +127,47 @@ data MonitorStateGlob
127127
!MonitorStateGlobRel
128128
deriving (Show, Generic)
129129

130+
-- | Monitoring state for a 'Glob'. Constructors mirror those of Glob
130131
data MonitorStateGlobRel
131-
= MonitorStateGlobDirs
132+
= -- | Monitoring state for 'GlobDir'
133+
MonitorStateGlobDirs
132134
!GlobPieces
135+
-- ^ Glob matching on subdirectory in current directory
133136
!Glob
137+
-- ^ Glob tail matching on anything below subdirectory
134138
!ModTime
135-
![(FilePath, MonitorStateGlobRel)] -- invariant: sorted
136-
| MonitorStateGlobFiles
139+
-- ^ Cached directory modification time
140+
![(FilePath, MonitorStateGlobRel)]
141+
-- ^ Per-file monitoring state.
142+
-- Invariant: sorted
143+
| -- | Monitoring state for 'GlobFile'
144+
MonitorStateGlobFiles
137145
!GlobPieces
146+
-- ^ Glob matching on file in current directory
138147
!ModTime
148+
-- ^ Cached directory modification time
139149
![(FilePath, MonitorStateFileStatus)] -- invariant: sorted
140-
| MonitorStateGlobDirTrailing
150+
151+
-- ^ Per-file monitoring state.
152+
-- Invariant: sorted
153+
| -- | Monitoring state for 'GlobDirRecursive'
154+
MonitorStateGlobRecursive
155+
!GlobPieces
156+
-- ^ Glob matching on file in current directory subtree (current
157+
-- directory and all of its descendants).
158+
!ModTime
159+
-- ^ Cached directory modification time
160+
![(FilePath, MonitorStateFileStatus)]
161+
-- ^ Per-file monitoring state for files immediately below the current
162+
-- directory.
163+
-- Invariant: sorted
164+
![(FilePath, MonitorStateGlobRel)]
165+
-- ^ Monitoring state for immediate subdirectories. Transient
166+
-- subdirectories are represented recursively within these.
167+
-- Invariant: sorted
168+
| -- | Monitoring state for 'GlobDirTrailing'
169+
-- (Trivial, because there is no data in 'GlobDirTrailing')
170+
MonitorStateGlobDirTrailing
141171
deriving (Show, Generic)
142172

143173
instance Binary MonitorStateGlob
@@ -161,10 +191,18 @@ reconstructMonitorFilePaths (MonitorStateFileSet singlePaths globPaths) =
161191
getGlobPath (MonitorStateGlob kindfile kinddir root gstate) =
162192
MonitorFileGlob kindfile kinddir $
163193
RootedGlob root $
164-
case gstate of
165-
MonitorStateGlobDirs glob globs _ _ -> GlobDir glob globs
166-
MonitorStateGlobFiles glob _ _ -> GlobFile glob
167-
MonitorStateGlobDirTrailing -> GlobDirTrailing
194+
monitorStateGlobRelGlob gstate
195+
196+
-- | Reconstruct a 'Glob' from a 'MonitorStateGlobRel'. This simply erases the
197+
-- additional information in 'MonitorStateGlobRel' added via
198+
-- 'buildMonitorStateGlobRel'.
199+
monitorStateGlobRelGlob :: MonitorStateGlobRel -> Glob
200+
monitorStateGlobRelGlob gstate =
201+
case gstate of
202+
MonitorStateGlobDirs glob globs _ _ -> GlobDir glob globs
203+
MonitorStateGlobFiles glob _ _ -> GlobFile glob
204+
MonitorStateGlobRecursive glob _ _ _ -> GlobDirRecursive glob
205+
MonitorStateGlobDirTrailing -> GlobDirTrailing
168206

169207
------------------------------------------------------------------------------
170208
-- Checking the status of monitored files
@@ -522,22 +560,84 @@ probeMonitorStateGlob
522560
MonitorStateGlob kindfile kinddir globroot
523561
<$> probeMonitorStateGlobRel kindfile kinddir root "" glob
524562

525-
probeMonitorStateGlobRel
563+
probeMonitorStateFiles
564+
:: FilePath
565+
-- ^ root path
566+
-> FilePath
567+
-- ^ path of the directory we are
568+
-- looking in relative to @root@
569+
-> GlobPieces
570+
-- ^ file glob to filter monitored files
571+
-> ModTime
572+
-- ^ cached directory modification time
573+
-> [(FilePath, MonitorStateFileStatus)]
574+
-> ChangedM (ModTime, [(FilePath, MonitorStateFileStatus)])
575+
probeMonitorStateFiles
576+
root
577+
dirName
578+
glob
579+
mtime
580+
children = do
581+
change <- liftIO $ checkDirectoryModificationTime (root </> dirName) mtime
582+
mtime' <- case change of
583+
Nothing -> return mtime
584+
Just mtime' -> do
585+
-- directory modification time changed:
586+
-- a matching file may have been added or deleted
587+
matches <-
588+
return . filter (matchGlobPieces glob)
589+
=<< liftIO (listDirectory (root </> dirName))
590+
591+
traverse_ probeMergeResult $
592+
mergeBy
593+
(\(path1, _) path2 -> compare path1 path2)
594+
children
595+
(sort matches)
596+
return mtime'
597+
598+
-- Check that none of the children have changed
599+
for_ children $ \(file, status) ->
600+
probeMonitorStateFileStatus root (dirName </> file) status
601+
602+
return (mtime', children)
603+
where
604+
-- Again, we don't force a cache rewrite with 'cacheChanged', but we do use
605+
-- the new mtime' if any.
606+
607+
probeMergeResult
608+
:: MergeResult (FilePath, MonitorStateFileStatus) FilePath
609+
-> ChangedM ()
610+
probeMergeResult mr = case mr of
611+
InBoth _ _ -> return ()
612+
-- this is just to be able to accurately report which file changed:
613+
OnlyInLeft (path, _) -> somethingChanged (dirName </> path)
614+
OnlyInRight path -> somethingChanged (dirName </> path)
615+
616+
probeMonitorStateDirs
526617
:: MonitorKindFile
527618
-> MonitorKindDir
528619
-> FilePath
529620
-- ^ root path
530621
-> FilePath
531622
-- ^ path of the directory we are
532623
-- looking in relative to @root@
533-
-> MonitorStateGlobRel
534-
-> ChangedM MonitorStateGlobRel
535-
probeMonitorStateGlobRel
624+
-> Maybe GlobPieces
625+
-- ^ optional glob to filter filenames by
626+
-> Glob
627+
-- ^ glob to filter subdirectories by
628+
-> ModTime
629+
-- ^ cached directory modification time
630+
-> [(FilePath, MonitorStateGlobRel)]
631+
-> ChangedM (ModTime, [(FilePath, MonitorStateGlobRel)])
632+
probeMonitorStateDirs
536633
kindfile
537634
kinddir
538635
root
539636
dirName
540-
(MonitorStateGlobDirs glob globPath mtime children) = do
637+
globMaybe
638+
globPath
639+
mtime
640+
children = do
541641
change <- liftIO $ checkDirectoryModificationTime (root </> dirName) mtime
542642
case change of
543643
Nothing -> do
@@ -554,7 +654,7 @@ probeMonitorStateGlobRel
554654
return (fname, fstate')
555655
| (fname, fstate) <- children
556656
]
557-
return $! MonitorStateGlobDirs glob globPath mtime children'
657+
return $! (mtime, children')
558658
Just mtime' -> do
559659
-- directory modification time changed:
560660
-- a matching subdir may have been added or deleted
@@ -564,7 +664,7 @@ probeMonitorStateGlobRel
564664
let subdir = root </> dirName </> entry
565665
in liftIO $ doesDirectoryExist subdir
566666
)
567-
. filter (matchGlobPieces glob)
667+
. maybe id (filter . matchGlobPieces) globMaybe
568668
=<< liftIO (listDirectory (root </> dirName))
569669

570670
children' <-
@@ -573,7 +673,7 @@ probeMonitorStateGlobRel
573673
(\(path1, _) path2 -> compare path1 path2)
574674
children
575675
(sort matches)
576-
return $! MonitorStateGlobDirs glob globPath mtime' children'
676+
return $! (mtime', children')
577677
where
578678
-- Note that just because the directory has changed, we don't force
579679
-- a cache rewrite with 'cacheChanged' since that has some cost, and
@@ -626,17 +726,56 @@ probeMonitorStateGlobRel
626726
fstate
627727
return (path, fstate')
628728

629-
-- \| Does a 'MonitorStateGlob' have any relevant files within it?
630-
allMatchingFiles :: FilePath -> MonitorStateGlobRel -> [FilePath]
631-
allMatchingFiles dir (MonitorStateGlobFiles _ _ entries) =
729+
allMatchingFilesFromGlobFiles :: FilePath -> [(FilePath, a)] -> [FilePath]
730+
allMatchingFilesFromGlobFiles dir entries =
632731
[dir </> fname | (fname, _) <- entries]
633-
allMatchingFiles dir (MonitorStateGlobDirs _ _ _ entries) =
732+
733+
allMatchingFilesFromGlobDirs :: FilePath -> [(FilePath, MonitorStateGlobRel)] -> [FilePath]
734+
allMatchingFilesFromGlobDirs dir entries =
634735
[ res
635736
| (subdir, fstate) <- entries
636737
, res <- allMatchingFiles (dir </> subdir) fstate
637738
]
739+
740+
-- \| Does a 'MonitorStateGlob' have any relevant files within it?
741+
allMatchingFiles :: FilePath -> MonitorStateGlobRel -> [FilePath]
742+
allMatchingFiles dir (MonitorStateGlobFiles _ _ entries) =
743+
allMatchingFilesFromGlobFiles dir entries
744+
allMatchingFiles dir (MonitorStateGlobDirs _ _ _ entries) =
745+
allMatchingFilesFromGlobDirs dir entries
746+
allMatchingFiles dir (MonitorStateGlobRecursive _ _ fileEntries dirEntries) =
747+
allMatchingFilesFromGlobFiles dir fileEntries
748+
++ allMatchingFilesFromGlobDirs dir dirEntries
638749
allMatchingFiles dir MonitorStateGlobDirTrailing =
639750
[dir]
751+
752+
probeMonitorStateGlobRel
753+
:: MonitorKindFile
754+
-> MonitorKindDir
755+
-> FilePath
756+
-- ^ root path
757+
-> FilePath
758+
-- ^ path of the directory we are
759+
-- looking in relative to @root@
760+
-> MonitorStateGlobRel
761+
-> ChangedM MonitorStateGlobRel
762+
probeMonitorStateGlobRel
763+
kindfile
764+
kinddir
765+
root
766+
dirName
767+
(MonitorStateGlobDirs glob globPath mtime children) = do
768+
(mtime', children') <-
769+
probeMonitorStateDirs
770+
kindfile
771+
kinddir
772+
root
773+
dirName
774+
(Just glob)
775+
globPath
776+
mtime
777+
children
778+
return $! MonitorStateGlobDirs glob globPath mtime' children'
640779
probeMonitorStateGlobRel
641780
_
642781
_
@@ -677,6 +816,33 @@ probeMonitorStateGlobRel
677816
-- this is just to be able to accurately report which file changed:
678817
OnlyInLeft (path, _) -> somethingChanged (dirName </> path)
679818
OnlyInRight path -> somethingChanged (dirName </> path)
819+
probeMonitorStateGlobRel
820+
kindfile
821+
kinddir
822+
root
823+
dirName
824+
(MonitorStateGlobRecursive glob mtime fileChildren dirChildren) = do
825+
-- For recursive globs, we check the file children first, then recurse
826+
-- into subdirectories, applying the same logic as 'MonitorStateGlobFiles'
827+
-- and 'MonitorStateGlobDirs', respectively.
828+
(_, fileChildren') <-
829+
probeMonitorStateFiles
830+
root
831+
dirName
832+
glob
833+
mtime
834+
fileChildren
835+
(mtime', dirChildren') <-
836+
probeMonitorStateDirs
837+
kindfile
838+
kinddir
839+
root
840+
dirName
841+
Nothing
842+
(GlobDirRecursive glob)
843+
mtime
844+
dirChildren
845+
return $! MonitorStateGlobRecursive glob mtime' fileChildren' dirChildren'
680846
probeMonitorStateGlobRel _ _ _ _ MonitorStateGlobDirTrailing =
681847
return MonitorStateGlobDirTrailing
682848

@@ -916,7 +1082,37 @@ buildMonitorStateGlobRel
9161082
dirEntries <- listDirectory absdir
9171083
dirMTime <- getModTime absdir
9181084
case globPath of
919-
GlobDirRecursive{} -> error "Monitoring directory-recursive globs (i.e. ../**/...) is currently unsupported"
1085+
GlobDirRecursive glob -> do
1086+
-- evaluate globPath' over the current directory
1087+
let files = filter (matchGlobPieces glob) dirEntries
1088+
filesStates <-
1089+
for (sort files) $ \file -> do
1090+
fstate <-
1091+
buildMonitorStateFile
1092+
mstartTime
1093+
hashcache
1094+
kindfile
1095+
kinddir
1096+
root
1097+
(dir </> file)
1098+
return (file, fstate)
1099+
-- evaluate globPath' over every subdirectory
1100+
subdirs <-
1101+
filterM (\subdir -> doesDirectoryExist (absdir </> subdir)) dirEntries
1102+
subdirStates <-
1103+
for (sort subdirs) $ \subdir -> do
1104+
fstate <-
1105+
buildMonitorStateGlobRel
1106+
mstartTime
1107+
hashcache
1108+
kindfile
1109+
kinddir
1110+
root
1111+
(dir </> subdir)
1112+
globPath
1113+
return (subdir, fstate)
1114+
1115+
return $! MonitorStateGlobRecursive glob dirMTime filesStates subdirStates
9201116
GlobDir glob globPath' -> do
9211117
subdirs <-
9221118
filterM (\subdir -> doesDirectoryExist (absdir </> subdir)) $
@@ -1015,16 +1211,27 @@ readCacheFileHashes monitor =
10151211
, (fpath, (mtime, hash)) <- collectGlobHashes "" gstate
10161212
]
10171213

1018-
collectGlobHashes :: FilePath -> MonitorStateGlobRel -> [(FilePath, (ModTime, HashValue))]
1019-
collectGlobHashes dir (MonitorStateGlobDirs _ _ _ entries) =
1214+
collectDirHashes :: FilePath -> [(FilePath, MonitorStateGlobRel)] -> [(FilePath, (ModTime, HashValue))]
1215+
collectDirHashes dir entries =
10201216
[ res
10211217
| (subdir, fstate) <- entries
10221218
, res <- collectGlobHashes (dir </> subdir) fstate
10231219
]
1024-
collectGlobHashes dir (MonitorStateGlobFiles _ _ entries) =
1220+
1221+
collectFileHashes :: FilePath -> [(FilePath, MonitorStateFileStatus)] -> [(FilePath, (ModTime, HashValue))]
1222+
collectFileHashes dir entries =
10251223
[ (dir </> fname, (mtime, hash))
10261224
| (fname, MonitorStateFileHashed mtime hash) <- entries
10271225
]
1226+
1227+
collectGlobHashes :: FilePath -> MonitorStateGlobRel -> [(FilePath, (ModTime, HashValue))]
1228+
collectGlobHashes dir (MonitorStateGlobDirs _ _ _ entries) =
1229+
collectDirHashes dir entries
1230+
collectGlobHashes dir (MonitorStateGlobFiles _ _ entries) =
1231+
collectFileHashes dir entries
1232+
collectGlobHashes dir (MonitorStateGlobRecursive _ _ fileEntries dirEntries) =
1233+
collectFileHashes dir fileEntries
1234+
++ collectDirHashes dir dirEntries
10281235
collectGlobHashes _dir MonitorStateGlobDirTrailing =
10291236
[]
10301237

0 commit comments

Comments
 (0)