Commit e4294f31 authored by quasicomputational's avatar quasicomputational

Allow globs to match against a suffix of a file's extensions

This has the effect of allowing a glob `*.html` to match the file
`foo.en.html`. For compatibility, this is only allowed with
`cabal-version: 3.0` or later; for earlier spec versions, a warning
will be generated by `cabal check` if there are files affected by this
change in behaviour.

Fixes #5057. Fixes #784. Closes #5061.
parent d6e0edbe
......@@ -38,6 +38,15 @@
`cxx-options`, `cpp-options` are not deduplicated anymore
([#4449](https://github.com/haskell/cabal/issues/4449)).
* Deprecated `cabal hscolour` in favour of `cabal haddock --hyperlink-source` ([#5236](https://github.com/haskell/cabal/pull/5236/)).
* With `cabal-version: 3.0`, when matching a wildcard, the
requirement for the full extension to match exactly has been
loosened. Instead, if the wildcard's extension is a suffix of the
file's extension, the file will be selected. For example,
previously `foo.en.html` would not match `*.html`, and
`foo.solaris.tar.gz` would not match `*.tar.gz`, but now both
do. This may lead to files unexpectedly being included by `sdist`;
please audit your package descriptions if you rely on this
behaviour to keep sensitive data out of distributed packages.
----
......
......@@ -57,6 +57,7 @@ import Distribution.Types.CondTree
import Distribution.Types.ExeDependency
import Distribution.Types.UnqualComponentName
import Distribution.Utils.Generic (isAscii)
import Distribution.Verbosity
import Distribution.Version
import Language.Haskell.Extension
import System.FilePath
......@@ -111,7 +112,7 @@ data PackageCheck =
-- quite legitimately refuse to publicly distribute packages with these
-- problems.
| PackageDistInexcusable { explanation :: String }
deriving (Eq)
deriving (Eq, Ord)
instance Show PackageCheck where
show notice = explanation notice
......@@ -1840,7 +1841,13 @@ checkDevelopmentOnlyFlags pkg =
-- package and expects to find the package unpacked in at the given file path.
--
checkPackageFiles :: PackageDescription -> FilePath -> NoCallStackIO [PackageCheck]
checkPackageFiles pkg root = checkPackageContent checkFilesIO pkg
checkPackageFiles pkg root = do
contentChecks <- checkPackageContent checkFilesIO pkg
missingFileChecks <- checkPackageMissingFiles pkg root
-- Sort because different platforms will provide files from
-- `getDirectoryContents` in different orders, and we'd like to be
-- stable for test output.
return (sort contentChecks ++ sort missingFileChecks)
where
checkFilesIO = CheckPackageContentOps {
doesFileExist = System.doesFileExist . relative,
......@@ -2136,6 +2143,48 @@ checkTarPath path
++ "Files with an empty name cannot be stored in a tar archive or in "
++ "standard file systems."
-- ------------------------------------------------------------
-- * Checks for missing content
-- ------------------------------------------------------------
-- | Similar to 'checkPackageContent', 'checkPackageMissingFiles' inspects
-- the files included in the package, but is primarily looking for files in
-- the working tree that may have been missed.
--
-- Because Hackage necessarily checks the uploaded tarball, it is too late to
-- check these on the server; these checks only make sense in the development
-- and package-creation environment. Hence we can use IO, rather than needing
-- to pass a 'CheckPackageContentOps' dictionary around.
checkPackageMissingFiles :: PackageDescription -> FilePath -> NoCallStackIO [PackageCheck]
checkPackageMissingFiles = checkGlobMultiDot
-- | Before Cabal 3.0, the extensions of globs had to match the file
-- exactly. This has been relaxed in 3.0 to allow matching only the
-- suffix. This warning detects when pre-3.0 package descriptions are
-- omitting files purely because of the stricter check.
checkGlobMultiDot :: PackageDescription
-> FilePath
-> NoCallStackIO [PackageCheck]
checkGlobMultiDot pkg root =
fmap concat $ for allGlobs $ \(field, dir, glob) -> do
--TODO: baked-in verbosity
results <- matchDirFileGlob' normal (specVersion pkg) (root </> dir) glob
return
[ PackageDistSuspiciousWarn $
"In '" ++ field ++ "': the pattern '" ++ glob ++ "' does not"
++ " match the file '" ++ file ++ "' because the extensions do not"
++ " exactly match (e.g., foo.en.html does not exactly match *.html)."
++ " To enable looser suffix-only matching, set 'cabal-version: 3.0' or higher."
| GlobWarnMultiDot file <- results
]
where
adjustedDataDir = if null (dataDir pkg) then "." else dataDir pkg
allGlobs = concat
[ (,,) "extra-source-files" "." <$> extraSrcFiles pkg
, (,,) "extra-doc-files" "." <$> extraDocFiles pkg
, (,,) "data-files" adjustedDataDir <$> dataFiles pkg
]
-- ------------------------------------------------------------
-- * Utils
-- ------------------------------------------------------------
......
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}
......@@ -14,19 +15,23 @@
-- Simple file globbing.
module Distribution.Simple.Glob (
GlobSyntaxError(..),
GlobResult(..),
globMatches,
matchFileGlob,
matchDirFileGlob,
matchDirFileGlob',
fileGlobMatches,
parseFileGlob,
explainGlobSyntaxError,
GlobSyntaxError(..),
Glob,
) where
import Prelude ()
import Distribution.Compat.Prelude
import Control.Monad (guard)
import Distribution.Simple.Utils
import Distribution.Verbosity
import Distribution.Version
......@@ -39,6 +44,21 @@ import System.FilePath (joinPath, splitExtensions, splitDirectories, takeFileNam
-- slash and backslash as its path separators, if we left in the
-- separators from the glob we might not end up properly normalised.
data GlobResult a
= GlobMatch a
-- ^ The glob matched the value supplied.
| GlobWarnMultiDot a
-- ^ The glob did not match the value supplied because the
-- cabal-version is too low and the extensions on the file did
-- not precisely match the glob's extensions, but rather the
-- glob was a proper suffix of the file's extensions; i.e., if
-- not for the low cabal-version, it would have matched.
deriving (Show, Eq, Ord, Functor)
-- | Extract the matches from a list of 'GlobResult's.
globMatches :: [GlobResult a] -> [a]
globMatches input = [ a | GlobMatch a <- input ]
data GlobSyntaxError
= StarInDirectory
| StarInFileName
......@@ -86,35 +106,59 @@ explainGlobSyntaxError filepath VersionDoesNotSupportGlob =
data IsRecursive = Recursive | NonRecursive
data MultiDot = MultiDotDisabled | MultiDotEnabled
data Glob
= GlobStem String Glob
= GlobStem FilePath Glob
-- ^ A single subdirectory component + remainder.
| GlobFinal GlobFinal
data GlobFinal
= FinalMatch IsRecursive String
= FinalMatch IsRecursive MultiDot String
-- ^ First argument: Is this a @**/*.ext@ pattern?
-- Second argument: the extensions to accept.
-- Second argument: should we match against the exact extensions, or accept a suffix?
-- Third argument: the extensions to accept.
| FinalLit FilePath
-- ^ Literal file name.
fileGlobMatches :: Glob -> FilePath -> Bool
fileGlobMatches pat = fileGlobMatchesSegments pat . splitDirectories
-- | Returns 'Nothing' if the glob didn't match at all, or 'Just' the
-- result if the glob matched (or would have matched with a higher
-- cabal-version).
fileGlobMatches :: Glob -> FilePath -> Maybe (GlobResult FilePath)
fileGlobMatches pat candidate = do
match <- fileGlobMatchesSegments pat (splitDirectories candidate)
return (candidate <$ match)
fileGlobMatchesSegments :: Glob -> [FilePath] -> Bool
fileGlobMatchesSegments _ [] = False
fileGlobMatchesSegments :: Glob -> [FilePath] -> Maybe (GlobResult ())
fileGlobMatchesSegments _ [] = Nothing
fileGlobMatchesSegments pat (seg : segs) = case pat of
GlobStem dir pat' ->
dir == seg && fileGlobMatchesSegments pat' segs
GlobStem dir pat' -> do
guard (dir == seg)
fileGlobMatchesSegments pat' segs
GlobFinal final -> case final of
FinalMatch Recursive ext ->
FinalMatch Recursive multidot ext -> do
let (candidateBase, candidateExts) = splitExtensions (last $ seg:segs)
in ext == candidateExts && not (null candidateBase)
FinalMatch NonRecursive ext ->
guard (not (null candidateBase))
checkExt multidot ext candidateExts
FinalMatch NonRecursive multidot ext -> do
let (candidateBase, candidateExts) = splitExtensions seg
in null segs && ext == candidateExts && not (null candidateBase)
FinalLit filename ->
null segs && filename == seg
guard (null segs && not (null candidateBase))
checkExt multidot ext candidateExts
FinalLit filename -> do
guard (null segs && filename == seg)
return (GlobMatch ())
checkExt
:: MultiDot
-> String -- ^ The pattern's extension
-> String -- ^ The candidate file's extension
-> Maybe (GlobResult ())
checkExt multidot ext candidate
| ext == candidate = Just (GlobMatch ())
| ext `isSuffixOf` candidate = case multidot of
MultiDotDisabled -> Just (GlobWarnMultiDot ())
MultiDotEnabled -> Just (GlobMatch ())
| otherwise = Nothing
parseFileGlob :: Version -> FilePath -> Either GlobSyntaxError Glob
parseFileGlob version filepath = case reverse (splitDirectories filepath) of
......@@ -127,14 +171,14 @@ parseFileGlob version filepath = case reverse (splitDirectories filepath) of
| null ext -> Left NoExtensionOnStar
| otherwise -> Right ext
_ -> Left LiteralFileNameGlobStar
foldM addStem (GlobFinal $ FinalMatch Recursive ext) segments
foldM addStem (GlobFinal $ FinalMatch Recursive multidot ext) segments
| otherwise -> Left VersionDoesNotSupportGlobStar
(filename : segments) -> do
pat <- case splitExtensions filename of
("*", ext) | not allowGlob -> Left VersionDoesNotSupportGlob
| '*' `elem` ext -> Left StarInExtension
| null ext -> Left NoExtensionOnStar
| otherwise -> Right (FinalMatch NonRecursive ext)
| otherwise -> Right (FinalMatch NonRecursive multidot ext)
(_, ext) | '*' `elem` ext -> Left StarInExtension
| '*' `elem` filename -> Left StarInFileName
| otherwise -> Right (FinalLit filename)
......@@ -145,16 +189,19 @@ parseFileGlob version filepath = case reverse (splitDirectories filepath) of
addStem pat seg
| '*' `elem` seg = Left StarInDirectory
| otherwise = Right (GlobStem seg pat)
multidot
| version >= mkVersion [3,0] = MultiDotEnabled
| otherwise = MultiDotDisabled
matchFileGlob :: Verbosity -> Version -> FilePath -> IO [FilePath]
matchFileGlob :: Verbosity -> Version -> FilePath -> IO [GlobResult FilePath]
matchFileGlob verbosity version = matchDirFileGlob verbosity version "."
-- | Like 'matchDirFileGlob'', but will 'die'' when the glob matches
-- no files.
matchDirFileGlob :: Verbosity -> Version -> FilePath -> FilePath -> IO [FilePath]
matchDirFileGlob :: Verbosity -> Version -> FilePath -> FilePath -> IO [GlobResult FilePath]
matchDirFileGlob verbosity version dir filepath = do
matches <- matchDirFileGlob' verbosity version dir filepath
when (null matches) $ die' verbosity $
when (null $ globMatches matches) $ die' verbosity $
"filepath wildcard '" ++ filepath
++ "' does not match any files."
return matches
......@@ -162,7 +209,7 @@ matchDirFileGlob verbosity version dir filepath = do
-- | Match files against a glob, starting in a directory.
--
-- The returned values do not include the supplied @dir@ prefix.
matchDirFileGlob' :: Verbosity -> Version -> FilePath -> FilePath -> IO [FilePath]
matchDirFileGlob' :: Verbosity -> Version -> FilePath -> FilePath -> IO [GlobResult FilePath]
matchDirFileGlob' verbosity version rawDir filepath = case parseFileGlob version filepath of
Left err -> die' verbosity $ explainGlobSyntaxError filepath err
Right pat -> do
......@@ -185,20 +232,21 @@ matchDirFileGlob' verbosity version rawDir filepath = case parseFileGlob version
-- literal.
let (prefixSegments, final) = splitConstantPrefix pat
joinedPrefix = joinPath prefixSegments
files <- case final of
FinalMatch recursive exts -> do
case final of
FinalMatch recursive multidot exts -> do
let prefix = dir </> joinedPrefix
candidates <- case recursive of
Recursive -> getDirectoryContentsRecursive prefix
NonRecursive -> filterM (doesFileExist . (prefix </>)) =<< getDirectoryContents prefix
let checkName candidate =
let checkName candidate = do
let (candidateBase, candidateExts) = splitExtensions $ takeFileName candidate
in not (null candidateBase) && exts == candidateExts
return $ filter checkName candidates
guard (not (null candidateBase))
match <- checkExt multidot exts candidateExts
return (joinedPrefix </> candidate <$ match)
return $ mapMaybe checkName candidates
FinalLit fn -> do
exists <- doesFileExist (dir </> joinedPrefix </> fn)
return [ fn | exists ]
return $ fmap (joinedPrefix </>) files
return [ GlobMatch (joinedPrefix </> fn) | exists ]
unfoldr' :: (a -> Either r (b, a)) -> a -> ([b], r)
unfoldr' f a = case f a of
......
......@@ -306,7 +306,7 @@ haddock pkg_descr lbi suffixes flags' = do
CBench _ -> (when (flag haddockBenchmarks) $ smsg >> doExe component) >> return index
for_ (extraDocFiles pkg_descr) $ \ fpath -> do
files <- matchFileGlob verbosity (specVersion pkg_descr) fpath
files <- fmap globMatches $ matchFileGlob verbosity (specVersion pkg_descr) fpath
for_ files $ copyFileTo verbosity (unDir $ argOutputDir commonArgs)
-- ------------------------------------------------------------------------------
......
......@@ -33,7 +33,7 @@ import Distribution.Package
import Distribution.PackageDescription
import Distribution.Simple.LocalBuildInfo
import Distribution.Simple.BuildPaths (haddockName, haddockPref)
import Distribution.Simple.Glob (matchDirFileGlob)
import Distribution.Simple.Glob (matchDirFileGlob, globMatches)
import Distribution.Simple.Utils
( createDirectoryIfMissingVerbose
, installDirectoryContents, installOrdinaryFile, isInSearchPath
......@@ -238,7 +238,7 @@ installDataFiles verbosity pkg_descr destDataDir =
srcDataDir = if null srcDataDirRaw
then "."
else srcDataDirRaw
files <- matchDirFileGlob verbosity (specVersion pkg_descr) srcDataDir file
files <- globMatches <$> matchDirFileGlob verbosity (specVersion pkg_descr) srcDataDir file
let dir = takeDirectory file
createDirectoryIfMissingVerbose verbosity True (destDataDir </> dir)
sequence_ [ installOrdinaryFile verbosity (srcDataDir </> file')
......
......@@ -147,7 +147,9 @@ listPackageSources verbosity pkg_descr0 pps = do
listPackageSourcesMaybeExecutable :: Verbosity -> PackageDescription -> IO [FilePath]
listPackageSourcesMaybeExecutable verbosity pkg_descr =
-- Extra source files.
fmap concat . for (extraSrcFiles pkg_descr) $ \fpath -> matchFileGlob verbosity (specVersion pkg_descr) fpath
fmap concat . for (extraSrcFiles pkg_descr) $ \fpath ->
fmap globMatches $
matchFileGlob verbosity (specVersion pkg_descr) fpath
-- | List those source files that should be copied with ordinary permissions.
listPackageSourcesOrdinary :: Verbosity
......@@ -214,12 +216,14 @@ listPackageSourcesOrdinary verbosity pkg_descr pps =
then "."
else srcDataDirRaw
in fmap (fmap (srcDataDir </>)) $
matchDirFileGlob verbosity (specVersion pkg_descr) srcDataDir filename
fmap globMatches $
matchDirFileGlob verbosity (specVersion pkg_descr) srcDataDir filename
-- Extra doc files.
, fmap concat
. for (extraDocFiles pkg_descr) $ \ filename ->
matchFileGlob verbosity (specVersion pkg_descr) filename
fmap globMatches $
matchFileGlob verbosity (specVersion pkg_descr) filename
-- License file(s).
, return (licenseFiles pkg_descr)
......
......@@ -990,11 +990,15 @@ describe the package as a whole:
- ``*`` wildcards are only allowed in place of the file name, not
in the directory name or file extension. It must replace the
whole file name (e.g., ``*.html`` is allowed, but
``chapter-*.html`` is not). Furthermore, if a wildcard is used
it must be used with an extension, so ``data-files: data/*`` is
not allowed. When matching a wildcard plus extension, a file's
full extension must match exactly, so ``*.gz`` matches
``foo.gz`` but not ``foo.tar.gz``.
``chapter-*.html`` is not). If a wildcard is used, it must be
used with an extension, so ``data-files: data/*`` is not
allowed.
- Prior to Cabal 3.0, when matching a wildcard plus extension, a
file's full extension must match exactly, so ``*.gz`` matches
``foo.gz`` but not ``foo.tar.gz``. This restriction has been
lifted when ``cabal-version: 3.0`` or greater so that ``*.gz``
does match ``foo.tar.gz``
- ``*`` wildcards will not match if the file name is empty (e.g.,
``*.html`` will not match ``foo/.html``).
......
......@@ -6,6 +6,7 @@ import Control.Monad
import Data.Foldable (for_)
import Data.Function (on)
import Data.List (sort)
import Data.Maybe (mapMaybe)
import Distribution.Simple.Glob
import qualified Distribution.Verbosity as Verbosity
import Distribution.Version
......@@ -21,7 +22,6 @@ sampleFileNames =
, "a.html"
, "b.html"
, "b.html.gz"
, "c.en.html"
, "foo/.blah.html"
, "foo/.html"
, "foo/a"
......@@ -52,25 +52,23 @@ makeSampleFiles dir = for_ sampleFileNames $ \filename -> do
compatibilityTests :: Version -> [TestTree]
compatibilityTests version =
[ testCase "literal match" $
testMatches "foo/a" ["foo/a"]
testMatches "foo/a" [GlobMatch "foo/a"]
, testCase "literal no match on prefix" $
testMatches "foo/c.html" []
, testCase "literal no match on suffix" $
testMatches "foo/a.html" ["foo/a.html"]
testMatches "foo/a.html" [GlobMatch "foo/a.html"]
, testCase "literal no prefix" $
testMatches "a" ["a"]
testMatches "a" [GlobMatch "a"]
, testCase "literal multiple prefix" $
testMatches "foo/bar/a.html" ["foo/bar/a.html"]
testMatches "foo/bar/a.html" [GlobMatch "foo/bar/a.html"]
, testCase "glob" $
testMatches "*.html" ["a.html", "b.html"]
testMatches "*.html" [GlobMatch "a.html", GlobMatch "b.html"]
, testCase "glob in subdir" $
testMatches "foo/*.html" ["foo/a.html", "foo/b.html"]
testMatches "foo/*.html" [GlobMatch "foo/a.html", GlobMatch "foo/b.html"]
, testCase "glob multiple extensions" $
testMatches "foo/*.html.gz" ["foo/a.html.gz", "foo/b.html.gz"]
, testCase "glob single extension not matching multiple" $
testMatches "foo/*.gz" ["foo/x.gz"]
testMatches "foo/*.html.gz" [GlobMatch "foo/a.html.gz", GlobMatch "foo/b.html.gz"]
, testCase "glob in deep subdir" $
testMatches "foo/bar/*.tex" ["foo/bar/a.tex"]
testMatches "foo/bar/*.tex" [GlobMatch "foo/bar/a.tex"]
, testCase "star in directory" $
testFailParse "blah/*/foo" StarInDirectory
, testCase "star plus text in segment" $
......@@ -93,13 +91,13 @@ compatibilityTests version =
--
-- TODO: Work out how to construct the sample tree once for all tests,
-- rather than once for each test.
testMatchesVersion :: Version -> FilePath -> [FilePath] -> Assertion
testMatchesVersion :: Version -> FilePath -> [GlobResult FilePath] -> Assertion
testMatchesVersion version pat expected = do
-- Test the pure glob matcher.
case parseFileGlob version pat of
Left _ -> assertFailure "Couldn't compile the pattern."
Right globPat ->
let actual = filter (fileGlobMatches globPat) sampleFileNames
let actual = mapMaybe (fileGlobMatches globPat) sampleFileNames
in unless (sort expected == sort actual) $
assertFailure $ "Unexpected result (pure matcher): " ++ show actual
-- ...and the impure glob matcher.
......@@ -109,7 +107,7 @@ testMatchesVersion version pat expected = do
unless (isEqual actual expected) $
assertFailure $ "Unexpected result (impure matcher): " ++ show actual
where
isEqual = (==) `on` (sort . fmap normalise)
isEqual = (==) `on` (sort . fmap (fmap normalise))
testFailParseVersion :: Version -> FilePath -> GlobSyntaxError -> Assertion
testFailParseVersion version pat expected =
......@@ -129,14 +127,28 @@ globstarTests =
, testCase "fails with literal filename" $
testFailParse "**/a.html" LiteralFileNameGlobStar
, testCase "with glob filename" $
testMatches "**/*.html" ["a.html", "b.html", "foo/a.html", "foo/b.html", "foo/bar/a.html", "foo/bar/b.html", "xyz/foo/a.html"]
testMatches "**/*.html" [GlobMatch "a.html", GlobMatch "b.html", GlobMatch "foo/a.html", GlobMatch "foo/b.html", GlobMatch "foo/bar/a.html", GlobMatch "foo/bar/b.html", GlobMatch "xyz/foo/a.html"]
, testCase "glob with prefix" $
testMatches "foo/**/*.html" ["foo/a.html", "foo/b.html", "foo/bar/a.html", "foo/bar/b.html"]
testMatches "foo/**/*.html" [GlobMatch "foo/a.html", GlobMatch "foo/b.html", GlobMatch "foo/bar/a.html", GlobMatch "foo/bar/b.html"]
]
where
testFailParse = testFailParseVersion (mkVersion [3,0])
testMatches = testMatchesVersion (mkVersion [3,0])
multiDotTests :: [TestTree]
multiDotTests =
[ testCase "pre-3.0 single extension not matching multiple" $
testMatchesVersion (mkVersion [2,2]) "foo/*.gz" [GlobWarnMultiDot "foo/a.html.gz", GlobWarnMultiDot "foo/a.tex.gz", GlobWarnMultiDot "foo/b.html.gz", GlobMatch "foo/x.gz"]
, testCase "doesn't match literal" $
testMatches "foo/a.tex" [GlobMatch "foo/a.tex"]
, testCase "works" $
testMatches "foo/*.gz" [GlobMatch "foo/a.html.gz", GlobMatch "foo/a.tex.gz", GlobMatch "foo/b.html.gz", GlobMatch "foo/x.gz"]
, testCase "works with globstar" $
testMatches "foo/**/*.gz" [GlobMatch "foo/a.html.gz", GlobMatch "foo/a.tex.gz", GlobMatch "foo/b.html.gz", GlobMatch "foo/x.gz", GlobMatch "foo/bar/a.html.gz", GlobMatch "foo/bar/a.tex.gz", GlobMatch "foo/bar/b.html.gz"]
]
where
testMatches = testMatchesVersion (mkVersion [3,0])
tests :: [TestTree]
tests =
[ testGroup "pre-3.0 compatibility" $
......@@ -146,4 +158,5 @@ tests =
, testGroup "globstar" globstarTests
, testCase "pre-1.6 rejects globbing" $
testFailParseVersion (mkVersion [1,4]) "foo/*.bar" VersionDoesNotSupportGlob
, testGroup "multi-dot globbing" multiDotTests
]
# cabal check
Warning: These warnings may cause trouble when distributing the package:
Warning: In 'data-files': the pattern 'data/*.dat' does not match the file 'data/foo.bar.dat' because the extensions do not exactly match (e.g., foo.en.html does not exactly match *.html). To enable looser suffix-only matching, set 'cabal-version: 3.0' or higher.
Warning: In 'extra-doc-files': the pattern 'doc/*.html' does not match the file 'doc/foo.en.html' because the extensions do not exactly match (e.g., foo.en.html does not exactly match *.html). To enable looser suffix-only matching, set 'cabal-version: 3.0' or higher.
Warning: In 'extra-doc-files': the pattern 'doc/*.html' does not match the file 'doc/foo.fr.html' because the extensions do not exactly match (e.g., foo.en.html does not exactly match *.html). To enable looser suffix-only matching, set 'cabal-version: 3.0' or higher.
import Test.Cabal.Prelude
main = cabalTest $
cabal "check" []
cabal-version: 2.2
name: test
version: 0
license: BSD-3-Clause
synopsis: foo
description: foobar
category: example
maintainer: none@example.com
extra-doc-files:
doc/*.html
extra-source-files:
-- Include a glob that doesn't match anything, to make sure that the code in Check.hs won't call 'die' because of it.
src/*.js
data-files:
data/*.dat
executable foo
main-is: Main.hs
default-language: Haskell2010
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment