module Stackage.CheckBuildPlan
( checkBuildPlan
, BadBuildPlan
) where
import Control.Monad.Writer.Strict (Writer, execWriter, tell)
import qualified Data.Map.Strict as M
import Data.Semigroup (Option (..), Max (..))
import qualified Data.Text as T
import Stackage.BuildConstraints
import Stackage.BuildPlan
import Stackage.PackageDescription
import Stackage.Prelude
checkBuildPlan :: (MonadThrow m)
=> Bool
-> BuildPlan
-> m ()
checkBuildPlan failMissingCabal BuildPlan {..}
| null errs1 && null errs2 = return ()
| otherwise = throwM errs
where
allPackages = map (,mempty) (siCorePackages bpSystemInfo) ++
map (ppVersion &&& M.keys . M.filter libAndExe . sdPackages . ppDesc) bpPackages
errs@(BadBuildPlan errs1 errs2) = execWriter $ do
mapM_ (checkDeps getMaint allPackages) $ mapToList bpPackages
let cabalName = mkPackageName "Cabal"
case lookup cabalName bpPackages of
Nothing
| failMissingCabal -> tell
$ BadBuildPlan mempty
$ singletonMap cabalName
$ singleton "Cabal not found in build plan"
| otherwise -> return ()
Just (ppVersion -> cabalVersion) ->
let cabalVersion' =
case bpCabalFormatVersion of
Nothing -> cabalVersion
Just formatVersion -> min formatVersion cabalVersion
in mapM_ (checkCabalVersion cabalVersion') (mapToList bpPackages)
libAndExe (DepInfo cs _) = any (flip elem [CompLibrary,CompExecutable]) cs
getMaint :: PackageName -> Maybe Maintainer
getMaint pn = do
pp <- lookup pn bpPackages
pcMaintainer $ ppConstraints pp
checkDeps :: (PackageName -> Maybe Maintainer)
-> Map PackageName (Version,[PackageName])
-> (PackageName, PackagePlan)
-> Writer BadBuildPlan ()
checkDeps getMaint allPackages (user, pb) =
mapM_ go $ mapToList $ sdPackages $ ppDesc pb
where
go (dep, depInfo@(DepInfo _ range)) =
case lookup dep allPackages of
Nothing -> tell $ BadBuildPlan (singletonMap (dep, getMaint dep, Nothing) errMap) mempty
Just (version,deps)
| version `withinRange` range ->
occursCheck allPackages
(\d v ->
tell $ BadBuildPlan (singletonMap
(d, getMaint dep, v)
errMap) mempty)
dep
deps
[]
| otherwise -> tell $ BadBuildPlan (singletonMap
(dep, getMaint dep, Just version)
errMap) mempty
where
errMap = singletonMap pu depInfo
pu = PkgUser
{ puName = user
, puVersion = ppVersion pb
, puMaintainer = pcMaintainer $ ppConstraints pb
, puGithubPings = ppGithubPings pb
}
checkCabalVersion :: Version -> (PackageName, PackagePlan) -> Writer BadBuildPlan ()
checkCabalVersion cabalVersion (name, plan)
| Option (Just (Max neededVersion)) <- sdCabalVersion (ppDesc plan) =
unless (cabalVersion >= neededVersion) $ tell $ BadBuildPlan
mempty $ singletonMap name $ singleton $ concat
[ "Cabal version "
, display cabalVersion
, " not sufficient for "
, display neededVersion
]
| otherwise = return ()
occursCheck
:: Monad m
=> Map PackageName (Version,[PackageName])
-> (PackageName -> Maybe Version -> m ())
-> PackageName
-> [PackageName]
-> [PackageName]
-> m ()
occursCheck allPackages reportError =
go
where
go pkg deps seen =
case find (flip elem seen) deps of
Just cyclic ->
reportError cyclic $
fmap fst (lookup cyclic allPackages)
Nothing ->
forM_ deps $
\pkg' ->
case lookup pkg' allPackages of
Just (_v,deps')
| pkg' /= pkg -> go pkg' deps' seen'
_ -> return ()
where seen' = pkg : seen
data PkgUser = PkgUser
{ puName :: PackageName
, puVersion :: Version
, puMaintainer :: Maybe Maintainer
, puGithubPings :: Set Text
}
deriving (Eq, Ord)
pkgUserShow1 :: PkgUser -> Text
pkgUserShow1 PkgUser {..} = concat
[ display puName
, "-"
, display puVersion
]
pkgUserShow2 :: PkgUser -> Text
pkgUserShow2 PkgUser {..} = unwords
$ (maybe "No maintainer" unMaintainer puMaintainer ++ ".")
: map (cons '@') (setToList puGithubPings)
data BadBuildPlan = BadBuildPlan
(Map (PackageName, Maybe Maintainer, Maybe Version) (Map PkgUser DepInfo))
(Map PackageName (Vector Text))
deriving Typeable
instance Exception BadBuildPlan
instance Show BadBuildPlan where
show (BadBuildPlan errs1 errs2) =
unpack $ concatMap go1 (mapToList errs1) ++ concatMap go2 (mapToList errs2)
where
go1 ((dep, mmaint, mdepVer), users) = unlines
$ ""
: showDepVer dep mmaint mdepVer
: map showUser (mapToList users)
showDepVer :: PackageName
-> Maybe Maintainer
-> Maybe Version
-> Text
showDepVer dep mmaint Nothing = T.concat
[ display dep
, displayMaint mmaint
, " (not present) depended on by:"
]
showDepVer dep mmaint (Just version) = concat
[ display dep
, "-"
, display version
, displayMaint mmaint
, " is out of bounds for:"
]
displayMaint Nothing = ""
displayMaint (Just (Maintainer t)) = T.concat
[ " ("
, t
, ")"
]
showUser :: (PkgUser, DepInfo) -> Text
showUser (pu, (DepInfo comps range)) = concat
[ "- [ ] "
, pkgUserShow1 pu
, " ("
, T.replace "<" "< " $ display range
, "). "
, pkgUserShow2 pu
, ". Used by: "
, intercalate ", " $ map compToText $ setToList comps
]
go2 :: (PackageName, Vector Text) -> Text
go2 (name, errs) = unlines
$ display name
: map (\err -> " " ++ err) (toList errs)
instance Monoid BadBuildPlan where
mempty = BadBuildPlan mempty mempty
mappend (BadBuildPlan a x) (BadBuildPlan b y) = BadBuildPlan
(unionWith mappend a b)
(unionWith mappend x y)