{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
module Cabal.Project (
Project (..),
triverseProject,
emptyProject,
readProject,
parseProject,
resolveProject,
ResolveError (..),
renderResolveError,
readPackagesOfProject
) where
import Control.Exception (Exception (..), throwIO)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)
import Data.Bifoldable (Bifoldable (..))
import Data.Bifunctor (Bifunctor (..))
import Data.Bitraversable (Bitraversable (..), bifoldMapDefault, bimapDefault)
import Data.ByteString (ByteString)
import Data.Either (partitionEithers)
import Data.Foldable (toList)
import Data.Function ((&))
import Data.Functor (void)
import Data.List (foldl')
import Data.Traversable (for)
import Data.Void (Void)
import Distribution.Compat.Lens (LensLike', over)
import GHC.Generics (Generic)
import Network.URI (URI, parseURI)
import System.Directory (doesDirectoryExist, doesFileExist)
import System.FilePath (takeDirectory, takeExtension, (</>))
import Text.ParserCombinators.ReadP (readP_to_S)
import qualified Data.ByteString as BS
import qualified Data.Map.Strict as M
import qualified Distribution.CabalSpecVersion as C
import qualified Distribution.FieldGrammar as C
import qualified Distribution.Fields as C
import qualified Distribution.PackageDescription as C
import qualified Distribution.Parsec as C
import qualified Distribution.Parsec.Newtypes as C
import Cabal.Internal.Glob
import Cabal.Internal.Newtypes
import Cabal.Optimization
import Cabal.Package
import Cabal.Parse
import Cabal.SourceRepo
infixl 1 <&>
(<&>) :: Functor f => f a -> (a -> b) -> f b
(<&>) = flip fmap
data Project uri opt pkg = Project
{ prjPackages :: [pkg]
, prjOptPackages :: [opt]
, prjUriPackages :: [uri]
, prjConstraints :: [String]
, prjAllowNewer :: [String]
, prjReorderGoals :: Bool
, prjMaxBackjumps :: Maybe Int
, prjOptimization :: Optimization
, prjSourceRepos :: [SourceRepositoryPackage Maybe]
, prjOrigFields :: [C.PrettyField ()]
}
deriving (Functor, Foldable, Traversable, Generic)
instance (Eq uri, Eq opt, Eq pkg) => Eq (Project uri opt pkg) where
x == y = and
[ eqOn prjPackages
, eqOn prjOptPackages
, eqOn prjUriPackages
, eqOn prjConstraints
, eqOn prjAllowNewer
, eqOn prjReorderGoals
, eqOn prjMaxBackjumps
, eqOn prjOptimization
, eqOn prjSourceRepos
]
where
eqOn f = f x == f y
instance Bifunctor (Project c) where bimap = bimapDefault
instance Bifoldable (Project c) where bifoldMap = bifoldMapDefault
triverseProject
:: Applicative f
=> (uri -> f uri')
-> (opt -> f opt')
-> (pkg -> f pkg')
-> Project uri opt pkg -> f (Project uri' opt' pkg')
triverseProject f g h prj =
(\c b a -> prj { prjPackages = a, prjOptPackages = b, prjUriPackages = c })
<$> traverse f (prjUriPackages prj)
<*> traverse g (prjOptPackages prj)
<*> traverse h (prjPackages prj)
instance Bitraversable (Project uri) where
bitraverse = triverseProject pure
emptyProject :: Project c b a
emptyProject = Project [] [] [] [] [] False Nothing OptimizationOn [] []
readProject :: FilePath -> IO (Project URI Void (FilePath, C.GenericPackageDescription))
readProject fp = do
contents <- BS.readFile fp
prj0 <- either throwIO return (parseProject fp contents)
prj1 <- resolveProject fp prj0 >>= either throwIO return
readPackagesOfProject prj1 >>= either throwIO return
parseProject :: FilePath -> ByteString -> Either ParseError (Project Void String String)
parseProject = parseWith $ \fields0 -> do
let (fields1, sections) = C.partitionFields fields0
let fields2 = M.filterWithKey (\k _ -> k `elem` knownFields) fields1
parse fields0 fields2 sections
where
knownFields = C.fieldGrammarKnownFieldList $ grammar []
parse origFields fields sections = do
let prettyOrigFields = map void $ C.fromParsecFields $ filter notPackages origFields
prj <- C.parseFieldGrammar C.cabalSpecLatest fields $ grammar prettyOrigFields
foldl' (&) prj <$> traverse parseSec (concat sections)
parseSec :: C.Section C.Position -> C.ParseResult (Project Void String String -> Project Void String String)
parseSec (C.MkSection (C.Name _pos name) [] fields) | name == "source-repository-package" = do
let fields' = fst $ C.partitionFields fields
repos <- C.parseFieldGrammar C.cabalSpecLatest fields' sourceRepositoryPackageGrammar
return $ over prjSourceReposL (++ toList (srpFanOut repos))
parseSec _ = return id
notPackages :: C.Field ann -> Bool
notPackages (C.Field (C.Name _ "packages") _) = False
notPackages _ = True
grammar :: [C.PrettyField ()] -> C.ParsecFieldGrammar (Project Void String String) (Project Void String String)
grammar origFields = Project
<$> C.monoidalFieldAla "packages" (C.alaList' C.FSep PackageLocation) prjPackagesL
<*> C.monoidalFieldAla "optional-packages" (C.alaList' C.FSep PackageLocation) prjOptPackagesL
<*> pure []
<*> C.monoidalFieldAla "constraints" (C.alaList' C.CommaVCat NoCommas) prjConstraintsL
<*> C.monoidalFieldAla "allow-newer" (C.alaList' C.CommaVCat NoCommas) prjAllowNewerL
<*> C.booleanFieldDef "reorder-goals" prjReorderGoalsL False
<*> C.optionalFieldAla "max-backjumps" Int' prjMaxBackjumpsL
<*> C.optionalFieldDef "optimization" prjOptimizationL OptimizationOn
<*> pure []
<*> pure origFields
prjPackagesL :: Functor f => LensLike' f (Project uri opt pkg) [pkg]
prjPackagesL f prj = f (prjPackages prj) <&> \x -> prj { prjPackages = x }
prjOptPackagesL :: Functor f => LensLike' f (Project uri opt pkg) [opt]
prjOptPackagesL f prj = f (prjOptPackages prj) <&> \x -> prj { prjOptPackages = x }
prjConstraintsL :: Functor f => LensLike' f (Project uri opt pkg) [String]
prjConstraintsL f prj = f (prjConstraints prj) <&> \x -> prj { prjConstraints = x }
prjAllowNewerL :: Functor f => LensLike' f (Project uri opt pkg) [String]
prjAllowNewerL f prj = f (prjAllowNewer prj) <&> \x -> prj { prjAllowNewer = x }
prjReorderGoalsL :: Functor f => LensLike' f (Project uri opt pkg) Bool
prjReorderGoalsL f prj = f (prjReorderGoals prj) <&> \x -> prj { prjReorderGoals = x }
prjMaxBackjumpsL :: Functor f => LensLike' f (Project uri opt pkg) (Maybe Int)
prjMaxBackjumpsL f prj = f (prjMaxBackjumps prj) <&> \x -> prj { prjMaxBackjumps = x }
prjOptimizationL :: Functor f => LensLike' f (Project uri opt pkg) Optimization
prjOptimizationL f prj = f (prjOptimization prj) <&> \x -> prj { prjOptimization = x }
prjSourceReposL :: Functor f => LensLike' f (Project uri opt pkg) [SourceRepositoryPackage Maybe]
prjSourceReposL f prj = f (prjSourceRepos prj) <&> \x -> prj { prjSourceRepos = x }
newtype ResolveError = BadPackageLocation String
deriving Show
instance Exception ResolveError where
displayException = renderResolveError
renderResolveError :: ResolveError -> String
renderResolveError (BadPackageLocation s) = "Bad package location: " ++ show s
resolveProject
:: FilePath
-> Project Void String String
-> IO (Either ResolveError (Project URI Void FilePath))
resolveProject filePath prj = runExceptT $ do
prj' <- bitraverse findOptProjectPackage findProjectPackage prj
let (uris, pkgs) = partitionEithers $ concat $ prjPackages prj'
return prj'
{ prjPackages = pkgs ++ concat (prjOptPackages prj')
, prjOptPackages = []
, prjUriPackages = uris
}
where
rootdir = takeDirectory filePath
findProjectPackage :: String -> ExceptT ResolveError IO [Either URI FilePath]
findProjectPackage pkglocstr = do
mfp <- fmap3 Right (checkisFileGlobPackage pkglocstr) `mplusMaybeT`
fmap3 Right (checkIsSingleFilePackage pkglocstr) `mplusMaybeT`
fmap2 (\uri -> [Left uri]) (return $ parseURI pkglocstr)
maybe (throwE $ BadPackageLocation pkglocstr) return mfp
fmap2 f = fmap (fmap f)
fmap3 f = fmap (fmap (fmap f))
findOptProjectPackage pkglocstr = do
mfp <- checkisFileGlobPackage pkglocstr `mplusMaybeT`
checkIsSingleFilePackage pkglocstr
maybe (return []) return mfp
checkIsSingleFilePackage pkglocstr = do
let abspath = rootdir </> pkglocstr
isFile <- liftIO $ doesFileExist abspath
isDir <- liftIO $ doesDirectoryExist abspath
if | isFile && takeExtension pkglocstr == ".cabal" -> return (Just [abspath])
| isDir -> checkisFileGlobPackage (pkglocstr </> "*.cabal")
| otherwise -> return Nothing
checkisFileGlobPackage pkglocstr =
case filter (null . snd) $ readP_to_S parseFilePathGlobRel pkglocstr of
[(g, "")] -> do
files <- liftIO $ expandRelGlob rootdir g
let files' = filter ((== ".cabal") . takeExtension) files
if null files' then return Nothing else return (Just files')
_ -> return Nothing
mplusMaybeT :: Monad m => m (Maybe a) -> m (Maybe a) -> m (Maybe a)
mplusMaybeT ma mb = do
mx <- ma
case mx of
Nothing -> mb
Just x -> return (Just x)
readPackagesOfProject :: Project uri opt FilePath -> IO (Either ParseError (Project uri opt (FilePath, C.GenericPackageDescription)))
readPackagesOfProject prj = runExceptT $ for prj $ \fp -> do
contents <- liftIO $ BS.readFile fp
either throwE (\gpd -> return (fp, gpd)) (parsePackage fp contents)