stackage/Stackage2/BuildPlan.hs
2014-12-07 19:10:35 +02:00

298 lines
12 KiB
Haskell

{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE TypeFamilies #-}
-- | Representation of a concrete build plan, and how to generate a new one
-- based on constraints.
module Stackage2.BuildPlan
( BuildPlan (..)
, PackageBuild (..)
, newBuildPlan
) where
import Distribution.Package (Dependency (..))
import Distribution.PackageDescription
import Distribution.Version (withinRange, anyVersion, simplifyVersionRange)
import Stackage2.PackageConstraints
import Stackage2.PackageIndex
import Stackage2.Prelude
import Stackage2.GithubPings
import Control.Monad.State.Strict (execState, get, put)
import qualified Data.Map as Map
import qualified Data.Set as Set
import Data.Aeson
import Stackage2.PackageDescription
import qualified Distribution.System
import qualified Distribution.Compiler
data BuildPlan desc = BuildPlan
{ bpCore :: Map PackageName Version
, bpCoreExecutables :: Set ExeName
, bpGhcVersion :: Version
, bpOS :: Distribution.System.OS
, bpArch :: Distribution.System.Arch
, bpTools :: Vector (PackageName, Version)
, bpExtra :: Map PackageName (PackageBuild desc)
}
deriving (Functor, Foldable, Traversable, Show, Eq)
type instance Element (BuildPlan desc) = desc
instance MonoFunctor (BuildPlan desc)
instance MonoFoldable (BuildPlan desc)
instance MonoTraversable (BuildPlan desc)
instance ToJSON (BuildPlan desc) where
toJSON BuildPlan {..} = object
[ "core" .= asMap (mapFromList $ map toCore $ mapToList bpCore)
, "core-exes" .= bpCoreExecutables
, "ghc-version" .= asText (display bpGhcVersion)
, "os" .= asText (display bpOS)
, "arch" .= asText (display bpArch)
, "tools" .= map goTool bpTools
, "extra" .= Map.mapKeysWith const (unPackageName) bpExtra
]
where
toCore (x, y) = (asText $ display x, asText $ display y)
goTool (name, version) = object
[ "name" .= asText (display name)
, "version" .= asText (display version)
]
instance desc ~ () => FromJSON (BuildPlan desc) where
parseJSON = withObject "BuildPlan" $ \o -> do
bpCore <- (o .: "core") >>= goCore
bpCoreExecutables <- o .: "core-exes"
bpGhcVersion <- (o .: "ghc-version") >>= either (fail . show) return . simpleParse . asText
bpOS <- o .: "os" >>= either (fail . show) return . simpleParse . asText
bpArch <- (o .: "arch") >>= either (fail . show) return . simpleParse . asText
bpTools <- (o .: "tools") >>= mapM goTool
bpExtra <- goExtra <$> (o .: "extra")
return BuildPlan {..}
where
goCore =
fmap mapFromList . mapM goCore' . mapToList . asHashMap
where
goCore' (k, v) = do
k' <- either (fail . show) return $ simpleParse $ asText k
v' <- either (fail . show) return $ simpleParse $ asText v
return (k', v')
goTool = withObject "Tool" $ \o -> (,)
<$> ((o .: "name") >>=
either (fail . show) return . simpleParse . asText)
<*> ((o .: "version") >>=
either (fail . show) return . simpleParse . asText)
goExtra = Map.mapKeysWith const PackageName
data PackageBuild desc = PackageBuild
{ pbVersion :: Version
, pbVersionRange :: VersionRange
-- ^ This is vital for ensuring old constraints are kept in place when bumping
, pbMaintainer :: Maybe Maintainer
, pbGithubPings :: Set Text
, pbUsers :: Set PackageName
, pbFlags :: Map FlagName Bool
, pbTestState :: TestState
, pbHaddockState :: TestState
, pbTryBuildBenchmark :: Bool
, pbDesc :: desc
}
deriving (Functor, Foldable, Traversable, Show, Eq)
type instance Element (PackageBuild desc) = desc
instance MonoFunctor (PackageBuild desc)
instance MonoFoldable (PackageBuild desc)
instance MonoTraversable (PackageBuild desc)
-- | There seems to be a bug in Cabal where serializing and deserializing
-- version ranges winds up with different representations. So we have a
-- super-simplifier to deal with that.
superSimplifyVersionRange :: VersionRange -> VersionRange
superSimplifyVersionRange vr =
fromMaybe (assert False vr') $ simpleParse $ asList $ display vr'
where
vr' = simplifyVersionRange vr
instance ToJSON (PackageBuild desc) where
toJSON PackageBuild {..} = object $ concat
[ maybe [] (\m -> ["maintainer" .= m]) pbMaintainer
,
[ "version" .= asText (display pbVersion)
, "version-range" .= asText (display $ superSimplifyVersionRange pbVersionRange)
, "github-pings" .= pbGithubPings
, "users" .= map unPackageName (unpack pbUsers)
, "flags" .= Map.mapKeysWith const (\(FlagName f) -> asText $ pack f) pbFlags
, "test-state" .= pbTestState
, "haddock-state" .= pbHaddockState
, "build-benchmark" .= pbTryBuildBenchmark
]
]
instance desc ~ () => FromJSON (PackageBuild desc) where
parseJSON = withObject "PackageBuild" $ \o -> PackageBuild
<$> (o .: "version" >>= efail . simpleParse . asText)
<*> (o .: "version-range" >>= fmap superSimplifyVersionRange . efail . simpleParse . asText)
<*> o .:? "maintainer"
<*> o .:? "github-pings" .!= mempty
<*> (Set.map PackageName <$> (o .:? "users" .!= mempty))
<*> (toFlags <$> (o .:? "flags" .!= mempty))
<*> o .: "test-state"
<*> o .: "haddock-state"
<*> o .: "build-benchmark"
<*> pure ()
where
toFlags = Map.mapKeysWith const (FlagName . unpack . asText)
efail = either (fail . show) return
newBuildPlan :: MonadIO m => PackageConstraints -> m (BuildPlan FlatComponent)
newBuildPlan pc = liftIO $ do
extraOrig <- getLatestDescriptions (isAllowed pc) (mkPackageBuild pc)
let toolMap = makeToolMap extraOrig
extra = populateUsers $ removeUnincluded pc toolMap extraOrig
toolNames :: [ExeName]
toolNames = concatMap (Map.keys . seTools . fcExtra . pbDesc) extra
tools <- topologicalSortTools toolMap $ mapFromList $ do
exeName <- toolNames
guard $ exeName `notMember` pcCoreExecutables pc
packageName <- maybe mempty setToList $ lookup exeName toolMap
packageBuild <- maybeToList $ lookup packageName extraOrig
return (packageName, packageBuild)
-- FIXME topologically sort packages? maybe just leave that to the build phase
return BuildPlan
{ bpCore = pcCorePackages pc
, bpCoreExecutables = pcCoreExecutables pc
, bpGhcVersion = pcGhcVersion pc
, bpOS = pcOS pc
, bpArch = pcArch pc
, bpTools = tools
, bpExtra = extra
}
makeToolMap :: Map PackageName (PackageBuild FlatComponent)
-> Map ExeName (Set PackageName)
makeToolMap =
unionsWith (++) . map go . mapToList
where
go (packageName, pb) =
foldMap go' $ seProvidedExes $ fcExtra $ pbDesc pb
where
go' exeName = singletonMap exeName (singletonSet packageName)
topologicalSortTools :: MonadThrow m
=> Map ExeName (Set PackageName)
-> Map PackageName (PackageBuild FlatComponent)
-> m (Vector (PackageName, Version))
topologicalSortTools toolMap = topologicalSort
pbVersion
(concatMap (fromMaybe mempty . flip lookup toolMap) . Map.keys . seTools . fcExtra . pbDesc)
topologicalSort :: (Ord key, Show key, MonadThrow m, Typeable key)
=> (value -> finalValue)
-> (value -> Set key) -- ^ deps
-> Map key value
-> m (Vector (key, finalValue))
topologicalSort toFinal toDeps =
loop id . mapWithKey removeSelfDeps . fmap (toDeps &&& toFinal)
where
removeSelfDeps k (deps, final) = (deleteSet k deps, final)
loop front toProcess | null toProcess = return $ pack $ front []
loop front toProcess
| null noDeps = throwM $ NoEmptyDeps (map fst toProcess')
| otherwise = loop (front . noDeps') (mapFromList hasDeps)
where
toProcess' = fmap (first removeUnavailable) toProcess
allKeys = Map.keysSet toProcess
removeUnavailable = asSet . setFromList . filter (`member` allKeys) . setToList
(noDeps, hasDeps) = partition (null . fst . snd) $ mapToList toProcess'
noDeps' = (map (second snd) noDeps ++)
data TopologicalSortException key = NoEmptyDeps (Map key (Set key))
deriving (Show, Typeable)
instance (Show key, Typeable key) => Exception (TopologicalSortException key)
removeUnincluded :: PackageConstraints
-> Map ExeName (Set PackageName)
-> Map PackageName (PackageBuild FlatComponent)
-> Map PackageName (PackageBuild FlatComponent)
removeUnincluded pc toolMap orig =
mapFromList $ filter (\(x, _) -> x `member` included) $ mapToList orig
where
coreExes = pcCoreExecutables pc
included :: Set PackageName
included = flip execState mempty $
mapM_ (add . fst) $ mapToList $ pcPackages pc
add name = do
inc <- get
when (name `notMember` inc) $ do
put $ insertSet name inc
case lookup name orig of
Nothing -> return ()
Just pb -> do
mapM_ (add . fst) $ mapToList $ fcDeps $ pbDesc pb
forM_ (map fst $ mapToList $ seTools $ fcExtra $ pbDesc pb) $
\exeName -> when (exeName `notMember` coreExes)
$ mapM_ add $ fromMaybe mempty $ lookup exeName toolMap
populateUsers :: Map PackageName (PackageBuild FlatComponent)
-> Map PackageName (PackageBuild FlatComponent)
populateUsers orig =
mapWithKey go orig
where
go name pb = pb { pbUsers = foldMap (go2 name) (mapToList orig) }
go2 dep (user, pb)
| dep `member` fcDeps (pbDesc pb) = singletonSet user
| otherwise = mempty
isAllowed :: PackageConstraints
-> PackageName -> Version -> Bool
isAllowed pc = \name version ->
case lookup name $ pcCorePackages pc of
Just _ -> False -- never reinstall a core package
Nothing ->
case lookup name $ pcPackages pc of
Nothing -> True -- no constraints
Just (range, _) -> withinRange version range
mkPackageBuild :: MonadThrow m
=> PackageConstraints
-> GenericPackageDescription
-> m (PackageBuild FlatComponent)
mkPackageBuild pc gpd = do
let overrides = pcFlagOverrides pc name
getFlag MkFlag {..} =
(flagName, fromMaybe flagDefault $ lookup flagName overrides)
flags = mapFromList $ map getFlag $ genPackageFlags gpd
desc <- getFlattenedComponent
CheckCond
{ ccPackageName = name
, ccOS = pcOS pc
, ccArch = pcArch pc
, ccCompilerFlavor = Distribution.Compiler.GHC
, ccCompilerVersion = pcGhcVersion pc
, ccFlags = flags
}
(pcTests pc name /= Don'tBuild)
(pcBuildBenchmark pc name)
gpd
return PackageBuild
{ pbVersion = version
, pbVersionRange = superSimplifyVersionRange
$ maybe anyVersion fst $ lookup name $ pcPackages pc
, pbMaintainer = lookup name (pcPackages pc) >>= snd
, pbGithubPings = getGithubPings gpd
, pbUsers = mempty -- must be filled in later
, pbFlags = flags
, pbTestState = pcTests pc name
, pbHaddockState = pcHaddocks pc name
, pbTryBuildBenchmark = pcBuildBenchmark pc name
, pbDesc = desc
}
where
PackageIdentifier name version = package $ packageDescription gpd