packages feed

module-management-0.20.2: testdata/debian/Debian/Repo/MonadOS.hs

{-# LANGUAGE DeriveDataTypeable, FlexibleContexts, FlexibleInstances, OverloadedStrings, PackageImports, ScopedTypeVariables, StandaloneDeriving, TemplateHaskell, TypeSynonymInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Debian.Repo.MonadOS
    ( MonadOS(getOS, putOS, modifyOS)
    , evalMonadOS
    , updateLists
    , withProc
    , withTmp
    , aptGetInstall
    , syncLocalPool
    , osFlushPackageCache
    , buildEssential
    , Debian.Repo.MonadOS.syncOS
    ) where

import Control.Applicative ((<$>))
import Control.DeepSeq (force)
import Control.Exception (evaluate, SomeException, catch)
import Control.Monad.Catch (bracket, MonadCatch, MonadMask, throwM, try)
import Control.Monad.State (MonadState(get), StateT, evalStateT, get)
import Control.Monad.Trans (liftIO, MonadIO, lift)
import Data.ByteString.Lazy as L (empty)
import Data.Time (NominalDiffTime)
import Debian.Pretty (ppDisplay)
import Debian.Relation (PkgName, Relations)
import Debian.Repo.EnvPath (EnvPath(EnvPath, envPath, envRoot), EnvRoot(rootPath))
import Debian.Repo.Internal.Repos (MonadRepos, osFromRoot, putOSImage, syncOS)
import Debian.Repo.LocalRepository (copyLocalRepo)
import Debian.Repo.OSImage as OS (OSImage(osRoot, osLocalMaster, osLocalCopy, osSourcePackageCache, osBinaryPackageCache))
import qualified Debian.Repo.OSImage as OS (buildEssential)
import Debian.Repo.Prelude.Verbosity (quieter, timeTask, readProcFailing, ePutStrLn)
import Debian.Repo.Top (MonadTop)
import Debian.Version (DebianVersion, prettyDebianVersion)
import System.Directory (createDirectoryIfMissing)
import System.Exit (ExitCode(ExitFailure))
import System.FilePath ((</>))
import System.Process (proc)
import System.Process.Chunks (collectProcessTriple)
import System.Unix.Chroot (useEnv)

-- | The problem with having an OSImage in the state of MonadOS is
-- that then we are modifying a copy of the OSImage in MonadRepos, we
-- want to go into MonadRepos and modify the map element there.  So
-- instead put an EnvRoot to look up the OSImage.
class (Monad m, Functor m) => MonadOS m where
    getOS :: m OSImage
    putOS :: OSImage -> m ()
    modifyOS :: (OSImage -> OSImage) -> m ()

instance MonadRepos m => MonadOS (StateT EnvRoot m) where
    getOS = get >>= \ root -> maybe (error "getOS") id <$> (lift $ osFromRoot root)
    putOS = lift . putOSImage
    modifyOS f = getOS >>= putOS . f

-- | Run MonadOS and update the osImageMap with the modified value
evalMonadOS :: MonadRepos m => StateT EnvRoot m a -> EnvRoot -> m a
evalMonadOS task root = do
  a <- evalStateT task root
  return a

-- | Run @apt-get update@ and @apt-get dist-upgrade@.  If @update@
-- fails, run @dpkg --configure -a@ before running @dist-upgrade@.
updateLists :: (MonadOS m, MonadIO m, MonadCatch m, MonadMask m) => m NominalDiffTime
updateLists = quieter 1 $
    do root <-rootPath . osRoot <$> getOS
       withProc $ liftIO $ do
         (code, _, _) <- useEnv root forceList (readProcFailing update "") >>= return . collectProcessTriple
         _ <- case code of
                ExitFailure _ ->
                    do _ <- useEnv root forceList (readProcFailing configure "")
                       useEnv root forceList (readProcFailing update "")
                _ -> return []
         (_, elapsed) <- timeTask (useEnv root forceList (readProcFailing upgrade ""))
         return elapsed
    where
       update = proc "apt-get" ["update"]
       configure = proc "dpkg" ["--configure", "-a"]
       upgrade = proc "apt-get" ["-y", "--force-yes", "dist-upgrade"]

-- | Do an IO task in the build environment with /proc mounted.
withProc :: forall m c. (MonadOS m, MonadIO m, MonadCatch m, MonadMask m) => m c -> m c
withProc task =
    do root <- rootPath . osRoot <$> getOS
       let proc' = root </> "proc"
           sys = root </> "sys"
           pre :: m ()
           mountProc = proc "mount" ["--bind", "/proc", proc']
           mountSys = proc "mount" ["--bind", "/sys", sys]
           umountProc = proc "umount" [proc']
           umountSys = proc "umount" [sys]
           umountProcLazy = proc "umount" ["-l", proc']
           umountSysLazy = proc "umount" ["-l", sys]

           pre = liftIO (do createDirectoryIfMissing True proc'
                            readProcFailing mountProc L.empty
                            createDirectoryIfMissing True sys
                            readProcFailing mountSys L.empty
                            return ())
           post :: () -> m ()
           post _ = liftIO $ do readProcFailing umountProc L.empty
                                  `catch` (\ (e :: IOError) ->
                                               ePutStrLn ("Exception unmounting proc, trying lazy: " ++ show e) >>
                                               readProcFailing umountProcLazy L.empty)
                                readProcFailing umountSys L.empty
                                  `catch` (\ (e :: IOError) ->
                                               ePutStrLn ("Exception unmounting sys, trying lazy: " ++ show e) >>
                                               readProcFailing umountSysLazy L.empty)
                                return ()
           task' :: () -> m c
           task' _ = task
       bracket pre post task'

-- | Do an IO task in the build environment with /proc mounted.
withTmp :: forall m c. (MonadOS m, MonadIO m, MonadCatch m, MonadMask m) => m c -> m c
withTmp task =
    do root <- rootPath . osRoot <$> getOS
       let dir = root </> "tmp"
           mountTmp = proc "mount" ["--bind", "/tmp", dir]
           umountTmp = proc "umount" [dir]
           pre :: m ()
           pre = liftIO $ do createDirectoryIfMissing True dir
                             readProcFailing mountTmp L.empty
                             return ()
           post :: () -> m ()
           post _ = liftIO $ readProcFailing umountTmp L.empty >> return ()
           task' :: () -> m c
           task' _ = try task >>= either (\ (e :: SomeException) -> throwM e) return
       bracket pre post task'

-- | Run an apt-get command in a particular directory with a
-- particular list of packages.  Note that apt-get source works for
-- binary or source package names.
aptGetInstall :: (MonadOS m, MonadIO m, PkgName n) => [(n, Maybe DebianVersion)] -> m ()
aptGetInstall packages =
    do root <- rootPath . osRoot <$> getOS
       liftIO $ useEnv root (return . force) $ do
         readProcFailing p L.empty
         return ()
    where
      p = proc "apt-get" args'
      args' = ["-y", "--force-yes", "install"] ++ map formatPackage packages
      formatPackage (name, Nothing) = ppDisplay name
      formatPackage (name, Just version) = ppDisplay name ++ "=" ++ show (prettyDebianVersion version)

-- | This is a deepseq thing
forceList :: [a] -> IO [a]
forceList output = evaluate (length output) >> return output

-- | Use rsync to synchronize the pool of locally built packages from
-- outside the build environment to the location inside the
-- environment where apt can see and install the packages.  On the
-- assumption that we are doing this because the pool changed, we also
-- flush the cached package lists.
syncLocalPool :: (MonadIO m, MonadOS m, MonadCatch m, MonadMask m) => m ()
syncLocalPool =
    do os <- getOS
       repo' <- copyLocalRepo (EnvPath {envRoot = osRoot os, envPath = "/work/localpool"}) (osLocalMaster os)
       putOS (os {osLocalCopy = repo'})
       updateLists
       -- Presumably we are doing this because the pool changed, and
       -- that invalidates the OS package lists.
       osFlushPackageCache

osFlushPackageCache :: MonadOS m => m ()
osFlushPackageCache = modifyOS (\ os -> os {osSourcePackageCache = Nothing, osBinaryPackageCache = Nothing})

-- | Get the version of the newest ghc available in a build environment.
-- ghcNewestAvailableVersion :: (MonadIO m, Functor m, MonadState OSImage m) => m (Maybe DebianVersion)
-- ghcNewestAvailableVersion = do
--   root <- rootPath . osRoot <$> get
--   liftIO $ GHC.ghcNewestAvailableVersion root

buildEssential :: (MonadOS m, MonadIO m) => m Relations
buildEssential = getOS >>= liftIO . OS.buildEssential

syncOS :: (MonadOS m, MonadTop m, MonadRepos m) => EnvRoot -> m ()
syncOS dstRoot =
    do srcOS <- getOS
       dstOS <- Debian.Repo.Internal.Repos.syncOS srcOS dstRoot
       putOS dstOS