packages feed

hackager-1.3.0.0: src/HackageMonad.hs

{-# LANGUAGE CPP #-}
-- | Monad for Hackage Test. Just a simple state passing monad with appropriate
-- getter and setters.
module HackageMonad (
        -- state holder types
        PkgName, Hkg, HkgState, startState,

        -- configure build paths and tools
        setRunPath, getRunPath, getCabal, setCabal, getGhc, setGhc, getGhcPkg,
        setGhcPkg, getCabalFlags, setCabalFlags, getDepFlags, setDepFlags,
        getRegex, setRegex, getPkgFlags, setPkgFlags,

        -- setup and retrieve list of packages to test
        addPkg, getPkgs,

        -- build scratch / isolated environment
        getTempPackageConf, getScratchDir, rmScratchDir, rmAllScratch,

        -- control build parallelism
        setThreads, getThreads,

        -- stats (what packages do we think we can and can't try to build?)
        addInstallablePackage, addNotInstallablePackage, addErrorPackage,
        addRevDepCounts,
        
        -- list of packages we've determined we can attempt to build
        getInstallablePackages,

        -- build outcomes
        buildSucceeded, buildFailed, buildDepsFailed,

        -- save results to disk
        dumpStats, dumpResults,

        -- logging utils
        info, warn, die
    ) where

import Control.Concurrent (MVar, newMVar)
import qualified Control.Concurrent as C
import Control.Monad.State
import Data.Function
#if __GLASGOW_HASKELL__ < 710
import Data.Functor
#endif
import Data.List
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Set (Set)
import qualified Data.Set as Set
import System.Directory
import System.FilePath
import System.Exit (exitWith, ExitCode(..))
import System.IO

import Utils

type PkgName = String

type Hkg = StateT HkgState IO

-- | The state of Hackager
data HkgState = HkgState {
        -- These are set based on the command line flags
        st_dir      :: FilePath,
        st_cabal    :: FilePath,
        st_ghc      :: FilePath,
        st_ghcPkg   :: FilePath,
        st_cabFlags :: [String],
        st_depFlags :: [String],
        st_pkgFlags :: [String],
        st_threads  :: Int,
        st_regex    :: String,
        st_pkgs     :: Set PkgName,

        -- These are set by the stats-collection pass:
        st_installablePackages    :: MVar (Set PkgName),
        st_notInstallablePackages :: MVar (Set PkgName),
        st_errorPackages          :: MVar (Set PkgName),
        st_installCounts          :: MVar (Map PkgName Int),

        -- These are set by the installation pass:
        st_buildablePackages       :: MVar (Set PkgName),
        st_buildFailurePackages    :: MVar (Set PkgName),
        st_buildDepFailurePackages :: MVar (Set PkgName),

        -- internal locks for making stdout thread safe
        st_iolock :: MVar ()
    }

startState :: IO HkgState
startState = do
    apkgs  <- newMVar Set.empty
    npkgs  <- newMVar Set.empty
    fpkgs  <- newMVar Set.empty
    count  <- newMVar Map.empty
    bbpkgs <- newMVar Set.empty
    bfpkgs <- newMVar Set.empty
    bdpkgs <- newMVar Set.empty
    iolock <- newMVar ()
    return HkgState {
        st_dir                     = "",
        st_cabal                   = "",
        st_ghc                     = "",
        st_ghcPkg                  = "",
        st_cabFlags                = [],
        st_depFlags                = [],
        st_pkgFlags                = [],
        st_threads                 = 1,
        st_regex                   = "",
        st_pkgs                    = Set.empty,
        st_installablePackages     = apkgs,
        st_notInstallablePackages  = npkgs,
        st_errorPackages           = fpkgs,
        st_installCounts           = count,
        st_buildablePackages       = bbpkgs,
        st_buildFailurePackages    = bfpkgs,
        st_buildDepFailurePackages = bdpkgs,
        st_iolock                  = iolock
    }

------------------------------------------------
-- Helpers

setRunPath :: FilePath -> Hkg ()
setRunPath name = do
    dir <- liftIO getCurrentDirectory
    modify $ \st -> st { st_dir = dir </> name }

getRunPath :: Hkg FilePath
getRunPath = gets st_dir

getTempPackageConf :: PkgName -> Hkg FilePath
getTempPackageConf p = (<.> "package.conf") <$> getScratchDir p

getScratchDir :: PkgName -> Hkg FilePath
getScratchDir p = (</> "scratch" </> p) <$> getRunPath

rmScratchDir :: PkgName -> Hkg ()
rmScratchDir p = do
    dir <- getRunPath
    liftIO . ignoreException $
        removeDirectoryRecursive (dir </> "scratch" </> p)

rmAllScratch :: Hkg ()
rmAllScratch = do
    dir <- getRunPath
    liftIO . ignoreException $ removeDirectoryRecursive (dir </> "scratch")

getCabal :: Hkg FilePath
getCabal = gets st_cabal

setCabal :: FilePath -> Hkg ()
setCabal ci = modify $ \st -> st { st_cabal = ci }

getGhc :: Hkg FilePath
getGhc = gets st_ghc

setGhc :: FilePath -> Hkg ()
setGhc ghc = modify $ \st -> st { st_ghc = ghc }

getGhcPkg :: Hkg FilePath
getGhcPkg = gets st_ghcPkg

setGhcPkg :: FilePath -> Hkg ()
setGhcPkg ghcPkg = modify $ \st -> st { st_ghcPkg = ghcPkg }

getCabalFlags :: Hkg [String]
getCabalFlags = gets st_cabFlags

setCabalFlags :: String -> Hkg ()
setCabalFlags cf = modify $ \st -> st { st_cabFlags = parseFlags cf }

getDepFlags :: Hkg [String]
getDepFlags = gets st_depFlags

setDepFlags :: String -> Hkg ()
setDepFlags depFlags = modify $ \st -> st { st_depFlags = parseFlags depFlags }

getPkgFlags :: Hkg [String]
getPkgFlags = gets st_pkgFlags

setPkgFlags :: String -> Hkg ()
setPkgFlags pkgFlags = modify $ \st -> st { st_pkgFlags = parseFlags pkgFlags }

getPkgs :: Hkg [String]
getPkgs = gets $ Set.toList . st_pkgs

addPkg :: String -> Hkg ()
addPkg p = modify $ \st -> st { st_pkgs = Set.insert p (st_pkgs st) }

getThreads :: Hkg Int
getThreads = gets st_threads

setThreads :: Int -> Hkg ()
setThreads n = modify $ \st -> st { st_threads = n }

getRegex :: Hkg String
getRegex = gets st_regex

setRegex :: String -> Hkg ()
setRegex s = modify $ \st -> st { st_regex = s }


parseFlags :: String -> [String]
parseFlags str =
    case reads str of
        [(flags, "")] -> flags
        _             -> words str

addRevDepCounts :: PkgName -> Hkg ()
addRevDepCounts pn = do
    st <- get
    ics <- takeMVar $ st_installCounts st
    let ics' = Map.insertWith (+) pn 1 ics
    putMVar (st_installCounts st) ics'

addInstallablePackage :: PkgName -> Hkg ()
addInstallablePackage pkg = do
    st <- get
    s <- takeMVar $ st_installablePackages st
    putMVar (st_installablePackages st) $ Set.insert pkg s

getInstallablePackages :: Hkg [PkgName]
getInstallablePackages = do
    st <- get
    s <- takeMVar $ st_installablePackages st
    return $ Set.toList s

addNotInstallablePackage :: PkgName -> Hkg ()
addNotInstallablePackage pkg = do
    st <- get
    s <- takeMVar $ st_notInstallablePackages st
    putMVar (st_notInstallablePackages st) $ Set.insert pkg s 

addErrorPackage :: PkgName -> Hkg ()
addErrorPackage pkg = do
    st <- get
    s <- takeMVar $ st_errorPackages st
    putMVar (st_errorPackages st) $ Set.insert pkg s

buildSucceeded :: PkgName -> Hkg ()
buildSucceeded pkg = do
    st <- get
    s <- takeMVar $ st_buildablePackages st
    putMVar (st_buildablePackages st) $ Set.insert pkg s

buildFailed :: PkgName -> Hkg ()
buildFailed pkg = do
    st <- get
    s <- takeMVar $ st_buildFailurePackages st
    putMVar (st_buildFailurePackages st) $ Set.insert pkg s

buildDepsFailed :: PkgName -> Hkg ()
buildDepsFailed pkg = do
    st <- get
    s <- takeMVar $ st_buildDepFailurePackages st
    putMVar (st_buildDepFailurePackages st) $ Set.insert pkg s

dumpStats :: Int -> Hkg ()
dumpStats n = do
    st <- get
    apkgs <- readMVar $ st_installablePackages st
    npkgs <- readMVar $ st_notInstallablePackages st
    fpkgs <- readMVar $ st_errorPackages st
    count <- readMVar $ st_installCounts st

    let fullHistogram = sortBy (flip compare) (map swap $ Map.assocs count)
        total = Set.size apkgs + sum (map fst fullHistogram)
        summaryTable = [ ["Num packages" , show n]              
                       , ["Installable"  , show $ Set.size apkgs]
                       , ["Uninstallable", show $ Set.size npkgs]
                       , ["Errored"      , show $ Set.size fpkgs]
                       , ["Installations", show total]
                       ]

    rpath <- getRunPath
    liftIO $ do
        writeFile (rpath </> "stats.summary")
                  (unlines $ showTable [rpad, rpad] summaryTable)
        writeFile (rpath </> "stats.dependency-count")
                  (unlines $ showCompleteHistogram fullHistogram)
        writeFile (rpath </> "stats.dependency-histogram")
                  (unlines $ showSummaryHistogram fullHistogram)
        writeFile (rpath </> "packages.installable")
                  (unlines $ Set.toList apkgs)
        writeFile (rpath </> "packages.uninstallable")
                  (unlines $ Set.toList npkgs)
        writeFile (rpath </> "packages.error")
                  (unlines $ Set.toList fpkgs)

  where
    showCompleteHistogram hist = showTable [rpad, rpad]
                                           [ [show count, pkg]
                                           | (count, pkg) <- hist ]
    showSummaryHistogram hist =
        let hist' = groupBy (on (==) fst) hist
            hist'' = [ [show $ fst $ head histogramRow,
                        show $ length histogramRow]
                     | histogramRow <- hist' ]
        in showTable [rpad, rpad]
                     (["Reverse Dependencies",
                       "Number of Packages"] :
                      hist'')

dumpResults :: Hkg ()
dumpResults = do
    st <- get
    bpkgs <- readMVar $ st_buildablePackages st
    fpkgs <- readMVar $ st_buildFailurePackages st
    dpkgs <- readMVar $ st_buildDepFailurePackages st
    rpath <- getRunPath
    
    let total = Set.size bpkgs + Set.size fpkgs + Set.size dpkgs
        summaryTable = [ ["Attempted"  , show total]              
                       , ["Succeeded"  , show $ Set.size bpkgs]
                       , ["Failed"     , show $ Set.size fpkgs]
                       , ["Deps Failed", show $ Set.size dpkgs]
                       ]

    liftIO $ do
      writeFile (rpath </> "build.summary")
                (unlines $ showTable [rpad, rpad] summaryTable)
      writeFile (rpath </> "build.success") (unlines $ Set.toList bpkgs)
      writeFile (rpath </> "build.fail")    (unlines $ Set.toList fpkgs)
      writeFile (rpath </> "build.depfail") (unlines $ Set.toList dpkgs)

takeMVar :: MVar a -> Hkg a
takeMVar m = liftIO $ C.takeMVar m

putMVar :: MVar a -> a -> Hkg ()
putMVar m v = liftIO $ C.putMVar m v

readMVar :: MVar a -> Hkg a
readMVar m = liftIO $ C.readMVar m

-- | Print message to stdout.
info :: String -> Hkg ()
info msg = do
    l <- st_iolock <$> get
    void $ takeMVar l
    liftIO (putStrLn msg)
    putMVar l ()

-- | Print message to stderr.
warn :: String -> Hkg ()
warn msg = do
    l <- st_iolock <$> get
    void $ takeMVar l
    liftIO $ hPutStrLn stderr msg
    putMVar l ()

-- | Exit with error message.
die :: String -> Hkg a
die err = do
    l <- st_iolock <$> get
    void $ takeMVar l
    liftIO $ hPutStrLn stderr err
    putMVar l ()
    liftIO $ exitWith (ExitFailure 1)