Skip to content

Commit 061073b

Browse files
Ensure cabal check doesn't error out early on invalid globs
This has been a problem since #5372 began expanding globs in `cabal check`. Now the logic of running a glob is separated from the parsing, giving the caller the opportunity to handle parsing failures flexibly.
1 parent 536348d commit 061073b

File tree

6 files changed

+126
-87
lines changed

6 files changed

+126
-87
lines changed

Cabal/Distribution/PackageDescription/Check.hs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2167,17 +2167,19 @@ checkGlobFiles :: PackageDescription
21672167
-> FilePath
21682168
-> NoCallStackIO [PackageCheck]
21692169
checkGlobFiles pkg root =
2170-
fmap concat $ for allGlobs $ \(field, dir, glob) -> do
2171-
--TODO: baked-in verbosity
2172-
results <- matchDirFileGlob' normal (specVersion pkg) (root </> dir) glob
2173-
let individualWarnings = results >>= getWarning field glob
2174-
noMatchesWarning =
2175-
[ PackageDistSuspiciousWarn $
2176-
"In '" ++ field ++ "': the pattern '" ++ glob ++ "' does not"
2177-
++ " match any files."
2178-
| all (not . suppressesNoMatchesWarning) results
2179-
]
2180-
return (noMatchesWarning ++ individualWarnings)
2170+
fmap concat $ for allGlobs $ \(field, dir, glob) ->
2171+
-- Note: we just skip over parse errors here; they're reported elsewhere.
2172+
fmap concat $ for (parseFileGlob (specVersion pkg) glob) $ \ parsedGlob -> do
2173+
--TODO: baked-in verbosity
2174+
results <- runDirFileGlob normal (root </> dir) parsedGlob
2175+
let individualWarnings = results >>= getWarning field glob
2176+
noMatchesWarning =
2177+
[ PackageDistSuspiciousWarn $
2178+
"In '" ++ field ++ "': the pattern '" ++ glob ++ "' does not"
2179+
++ " match any files."
2180+
| all (not . suppressesNoMatchesWarning) results
2181+
]
2182+
return (noMatchesWarning ++ individualWarnings)
21812183
where
21822184
adjustedDataDir = if null (dataDir pkg) then "." else dataDir pkg
21832185
allGlobs = concat

Cabal/Distribution/Simple/Glob.hs

Lines changed: 78 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ module Distribution.Simple.Glob (
1818
GlobSyntaxError(..),
1919
GlobResult(..),
2020
matchDirFileGlob,
21-
matchDirFileGlob',
21+
runDirFileGlob,
2222
fileGlobMatches,
2323
parseFileGlob,
2424
explainGlobSyntaxError,
@@ -35,7 +35,7 @@ import Distribution.Verbosity
3535
import Distribution.Version
3636

3737
import System.Directory (getDirectoryContents, doesDirectoryExist, doesFileExist)
38-
import System.FilePath (joinPath, splitExtensions, splitDirectories, takeFileName, (</>))
38+
import System.FilePath (joinPath, splitExtensions, splitDirectories, takeFileName, (</>), (<.>))
3939

4040
-- Note throughout that we use splitDirectories, not splitPath. On
4141
-- Posix, this makes no difference, but, because Windows accepts both
@@ -127,6 +127,14 @@ data GlobFinal
127127
| FinalLit FilePath
128128
-- ^ Literal file name.
129129

130+
reconstructGlob :: Glob -> FilePath
131+
reconstructGlob (GlobStem dir glob) =
132+
dir </> reconstructGlob glob
133+
reconstructGlob (GlobFinal final) = case final of
134+
FinalMatch Recursive _ exts -> "**" </> "*" <.> exts
135+
FinalMatch NonRecursive _ exts -> "*" <.> exts
136+
FinalLit path -> path
137+
130138
-- | Returns 'Nothing' if the glob didn't match at all, or 'Just' the
131139
-- result if the glob matched (or would have matched with a higher
132140
-- cabal-version).
@@ -199,71 +207,77 @@ parseFileGlob version filepath = case reverse (splitDirectories filepath) of
199207
| version >= mkVersion [2,4] = MultiDotEnabled
200208
| otherwise = MultiDotDisabled
201209

202-
-- | Like 'matchDirFileGlob'', but will 'die'' when the glob matches
203-
-- no files, or if the glob refers to a missing directory.
210+
-- | This will 'die'' when the glob matches no files, or if the glob
211+
-- refers to a missing directory, or if the glob fails to parse.
212+
--
213+
-- The returned values do not include the supplied @dir@ prefix, which
214+
-- must itself be a valid directory (hence, it can't be the empty
215+
-- string).
204216
matchDirFileGlob :: Verbosity -> Version -> FilePath -> FilePath -> IO [FilePath]
205-
matchDirFileGlob verbosity version dir filepath = do
206-
results <- matchDirFileGlob' verbosity version dir filepath
207-
let missingDirectories =
208-
[ missingDir | GlobMissingDirectory missingDir <- results ]
209-
matches = globMatches results
210-
-- Check for missing directories first, since we'll obviously have
211-
-- no matches in that case.
212-
for_ missingDirectories $ \ missingDir ->
213-
die' verbosity $
214-
"filepath wildcard '" ++ filepath ++ "' refers to the directory"
215-
++ " '" ++ missingDir ++ "', which does not exist or is not a directory."
216-
when (null matches) $ die' verbosity $
217-
"filepath wildcard '" ++ filepath
218-
++ "' does not match any files."
219-
return matches
217+
matchDirFileGlob verbosity version dir filepath = case parseFileGlob version filepath of
218+
Left err -> die' verbosity $ explainGlobSyntaxError filepath err
219+
Right glob -> do
220+
results <- runDirFileGlob verbosity dir glob
221+
let missingDirectories =
222+
[ missingDir | GlobMissingDirectory missingDir <- results ]
223+
matches = globMatches results
224+
-- Check for missing directories first, since we'll obviously have
225+
-- no matches in that case.
226+
for_ missingDirectories $ \ missingDir ->
227+
die' verbosity $
228+
"filepath wildcard '" ++ filepath ++ "' refers to the directory"
229+
++ " '" ++ missingDir ++ "', which does not exist or is not a directory."
230+
when (null matches) $ die' verbosity $
231+
"filepath wildcard '" ++ filepath
232+
++ "' does not match any files."
233+
return matches
220234

221-
-- | Match files against a glob, starting in a directory.
235+
-- | Match files against a pre-parsed glob, starting in a directory.
222236
--
223-
-- The returned values do not include the supplied @dir@ prefix.
224-
matchDirFileGlob' :: Verbosity -> Version -> FilePath -> FilePath -> IO [GlobResult FilePath]
225-
matchDirFileGlob' verbosity version rawDir filepath = case parseFileGlob version filepath of
226-
Left err -> die' verbosity $ explainGlobSyntaxError filepath err
227-
Right pat -> do
228-
-- The default data-dir is null. Our callers -should- be
229-
-- converting that to '.' themselves, but it's a certainty that
230-
-- some future call-site will forget and trigger a really
231-
-- hard-to-debug failure if we don't check for that here.
232-
when (null rawDir) $
233-
warn verbosity $
234-
"Null dir passed to matchDirFileGlob; interpreting it "
235-
++ "as '.'. This is probably an internal error."
236-
let dir = if null rawDir then "." else rawDir
237-
debug verbosity $ "Expanding glob '" ++ filepath ++ "' in directory '" ++ dir ++ "'."
238-
-- This function might be called from the project root with dir as
239-
-- ".". Walking the tree starting there involves going into .git/
240-
-- and dist-newstyle/, which is a lot of work for no reward, so
241-
-- extract the constant prefix from the pattern and start walking
242-
-- there, and only walk as much as we need to: recursively if **,
243-
-- the whole directory if *, and just the specific file if it's a
244-
-- literal.
245-
let (prefixSegments, final) = splitConstantPrefix pat
246-
joinedPrefix = joinPath prefixSegments
247-
case final of
248-
FinalMatch recursive multidot exts -> do
249-
let prefix = dir </> joinedPrefix
250-
directoryExists <- doesDirectoryExist prefix
251-
if directoryExists
252-
then do
253-
candidates <- case recursive of
254-
Recursive -> getDirectoryContentsRecursive prefix
255-
NonRecursive -> filterM (doesFileExist . (prefix </>)) =<< getDirectoryContents prefix
256-
let checkName candidate = do
257-
let (candidateBase, candidateExts) = splitExtensions $ takeFileName candidate
258-
guard (not (null candidateBase))
259-
match <- checkExt multidot exts candidateExts
260-
return (joinedPrefix </> candidate <$ match)
261-
return $ mapMaybe checkName candidates
262-
else
263-
return [ GlobMissingDirectory joinedPrefix ]
264-
FinalLit fn -> do
265-
exists <- doesFileExist (dir </> joinedPrefix </> fn)
266-
return [ GlobMatch (joinedPrefix </> fn) | exists ]
237+
-- The returned values do not include the supplied @dir@ prefix, which
238+
-- must itself be a valid directory (hence, it can't be the empty
239+
-- string).
240+
runDirFileGlob :: Verbosity -> FilePath -> Glob -> IO [GlobResult FilePath]
241+
runDirFileGlob verbosity rawDir pat = do
242+
-- The default data-dir is null. Our callers -should- be
243+
-- converting that to '.' themselves, but it's a certainty that
244+
-- some future call-site will forget and trigger a really
245+
-- hard-to-debug failure if we don't check for that here.
246+
when (null rawDir) $
247+
warn verbosity $
248+
"Null dir passed to runDirFileGlob; interpreting it "
249+
++ "as '.'. This is probably an internal error."
250+
let dir = if null rawDir then "." else rawDir
251+
debug verbosity $ "Expanding glob '" ++ reconstructGlob pat ++ "' in directory '" ++ dir ++ "'."
252+
-- This function might be called from the project root with dir as
253+
-- ".". Walking the tree starting there involves going into .git/
254+
-- and dist-newstyle/, which is a lot of work for no reward, so
255+
-- extract the constant prefix from the pattern and start walking
256+
-- there, and only walk as much as we need to: recursively if **,
257+
-- the whole directory if *, and just the specific file if it's a
258+
-- literal.
259+
let (prefixSegments, final) = splitConstantPrefix pat
260+
joinedPrefix = joinPath prefixSegments
261+
case final of
262+
FinalMatch recursive multidot exts -> do
263+
let prefix = dir </> joinedPrefix
264+
directoryExists <- doesDirectoryExist prefix
265+
if directoryExists
266+
then do
267+
candidates <- case recursive of
268+
Recursive -> getDirectoryContentsRecursive prefix
269+
NonRecursive -> filterM (doesFileExist . (prefix </>)) =<< getDirectoryContents prefix
270+
let checkName candidate = do
271+
let (candidateBase, candidateExts) = splitExtensions $ takeFileName candidate
272+
guard (not (null candidateBase))
273+
match <- checkExt multidot exts candidateExts
274+
return (joinedPrefix </> candidate <$ match)
275+
return $ mapMaybe checkName candidates
276+
else
277+
return [ GlobMissingDirectory joinedPrefix ]
278+
FinalLit fn -> do
279+
exists <- doesFileExist (dir </> joinedPrefix </> fn)
280+
return [ GlobMatch (joinedPrefix </> fn) | exists ]
267281

268282
unfoldr' :: (a -> Either r (b, a)) -> a -> ([b], r)
269283
unfoldr' f a = case f a of

Cabal/tests/UnitTests/Distribution/Simple/Glob.hs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,21 +93,23 @@ compatibilityTests version =
9393
-- rather than once for each test.
9494
testMatchesVersion :: Version -> FilePath -> [GlobResult FilePath] -> Assertion
9595
testMatchesVersion version pat expected = do
96-
-- Test the pure glob matcher.
97-
case parseFileGlob version pat of
96+
globPat <- case parseFileGlob version pat of
9897
Left _ -> assertFailure "Couldn't compile the pattern."
99-
Right globPat ->
100-
let actual = mapMaybe (fileGlobMatches globPat) sampleFileNames
101-
in unless (sort expected == sort actual) $
102-
assertFailure $ "Unexpected result (pure matcher): " ++ show actual
103-
-- ...and the impure glob matcher.
104-
withSystemTempDirectory "globstar-sample" $ \tmpdir -> do
105-
makeSampleFiles tmpdir
106-
actual <- matchDirFileGlob' Verbosity.normal version tmpdir pat
107-
unless (isEqual actual expected) $
108-
assertFailure $ "Unexpected result (impure matcher): " ++ show actual
98+
Right globPat -> return globPat
99+
checkPure globPat
100+
checkIO globPat
109101
where
110102
isEqual = (==) `on` (sort . fmap (fmap normalise))
103+
checkPure globPat = do
104+
let actual = mapMaybe (fileGlobMatches globPat) sampleFileNames
105+
unless (sort expected == sort actual) $
106+
assertFailure $ "Unexpected result (pure matcher): " ++ show actual
107+
checkIO globPat =
108+
withSystemTempDirectory "globstar-sample" $ \tmpdir -> do
109+
makeSampleFiles tmpdir
110+
actual <- runDirFileGlob Verbosity.normal tmpdir globPat
111+
unless (isEqual actual expected) $
112+
assertFailure $ "Unexpected result (impure matcher): " ++ show actual
111113

112114
testFailParseVersion :: Version -> FilePath -> GlobSyntaxError -> Assertion
113115
testFailParseVersion version pat expected =
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# cabal check
2+
Warning: The following errors will cause portability problems on other environments:
3+
Warning: No 'synopsis' or 'description' field.
4+
Warning: In the 'extra-doc-files' field: invalid file glob '***.html'. Wildcards '*' may only totally replace the file's base name, not only parts of it.
5+
Warning: Hackage would reject this package.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import Test.Cabal.Prelude
2+
main = cabalTest $
3+
fails $ cabal "check" []
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
cabal-version: 2.2
2+
name: pkg
3+
version: 0
4+
extra-doc-files:
5+
***.html
6+
category: example
7+
maintainer: [email protected]
8+
license: BSD-3-Clause
9+
10+
library
11+
exposed-modules: Foo
12+
default-language: Haskell2010
13+

0 commit comments

Comments
 (0)