packages feed

haskell-updater-1.5.0.0: src/Distribution/Gentoo/PkgManager.hs

{- |
   Module      : Distribution.Gentoo.PkgManager
   Description : Using package managers in Gentoo.
   Copyright   : (c) Ivan Lazar Miljenovic, Emil Karlson 2010
   License     : GPL-2 or later

   This module defines ways to use different Gentoo package managers. Much of
   the module is historical in nature.
 -}

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

module Distribution.Gentoo.PkgManager
       ( -- * Basic functions
         definedPMs
       , choosePM
       , stringToCustomPM
       , isValidPM
       , defaultPM
       , defaultPMName
       , nameOfPM
       , toPkgManager
         -- * BuildPkgs
       , BuildPkgs(..)
       , buildPkgsTargets
       , buildPkgsPending
         -- * MonadWritePkgState
       , MonadWritePkgState(..)
       ) where

import Distribution.Gentoo.Env
import Distribution.Gentoo.Packages
import Distribution.Gentoo.PkgManager.Types
import Distribution.Gentoo.Types
import qualified Distribution.Gentoo.Types.Mode as Mode

import Control.Monad.Reader
import Control.Monad.State.Strict
import Data.Char(toLower)
import Data.Maybe(mapMaybe, fromMaybe)
import qualified Data.Map as M
import Data.Map(Map)
import qualified Data.Set as Set
import System.Environment(getEnvironment)
import System.Exit (ExitCode (..), exitSuccess)
import System.Process (rawSystem)

-- -----------------------------------------------------------------------------

-- | The default package manager.  If the environment variable
--   @PACKAGE_MANAGER@ exists, use that; otherwise default to
--   "portage".  Note that even if that environment variable is
--   defined, if it is unknown then it won't be used.
defaultPM :: IO PkgManager
defaultPM = do eDPM <- lookup "PACKAGE_MANAGER" `fmap` getEnvironment
               let dPM = fromMaybe defaultPMName eDPM
                   mPM = dPM `M.lookup` pmNameMap
               return $ fromMaybe knownDef mPM
  where
    knownDef = pmNameMap M.! defaultPMName

-- | The default package manager (currently @"portage"@)
defaultPMName :: String
defaultPMName = "portage"

-- | The names of known package managers.
definedPMs :: [String]
definedPMs = M.keys pmNameMap

-- | Returns @Left pmname@ if the input is 'InvalidPM', or @Right pm@ otherwise
isValidPM                    :: PkgManager -> Either String PkgManager
isValidPM (InvalidPM pmname) = Left pmname
isValidPM pm                 = Right pm

-- | Map from string representations of package managers to their 'PkgManager'
--   representation, e.g. @"portage"@ to @v'Portage'@.
pmNameMap :: Map String PkgManager
pmNameMap = M.fromList [ ("portage", Portage)
                       , ("pkgcore", PkgCore)
                       , ("paludis", Paludis)
                       ]

-- | Inverted version of 'pmNameMap'
pmNameMap' :: Map PkgManager String
pmNameMap' = M.fromList . map (\(nm,pm) -> (pm,nm)) $ M.toList pmNameMap

-- | Human-readable name or description of a package manager
nameOfPM                    :: PkgManager -> String
nameOfPM (CustomPM pmname)  = "custom package manager command: " ++ pmname
nameOfPM (InvalidPM pmname) = "invalid package manager: " ++ pmname
nameOfPM pm                 = pmNameMap' M.! pm

-- | Choose the appropriate PM from the textual representation; throws
--   an error if that PM isn't known.
choosePM    :: String -> PkgManager
choosePM pm = fromMaybe (InvalidPM pm) $ pm' `M.lookup` pmNameMap
    where
      pm' = map toLower pm

-- | Create a v'CustomPM' from a 'String'
stringToCustomPM :: String -> PkgManager
stringToCustomPM = CustomPM

-- | Command-line compatible name for a 'PkgManager'
pmCommand                :: PkgManager -> String
pmCommand Portage        = "emerge"
pmCommand PkgCore        = "pmerge"
pmCommand Paludis        = "cave"
pmCommand (CustomPM cmd) = cmd
pmCommand (InvalidPM _)  = undefined

-- | Default command line flags for the given package manager
defaultPMFlags               :: PkgManager -> [String]
defaultPMFlags Portage       = [ "--oneshot"
                               , "--keep-going"
                               , "--complete-graph"
                               ]
defaultPMFlags PkgCore       = [ "--deep"
                               , "--oneshot"
                               , "--ignore-failures"
                               ]
defaultPMFlags Paludis       = [ "resolve"
                               , "--execute"
                               , "--preserve-world"
                               , "--continue-on-failure", "if-independent"
                               ]
defaultPMFlags CustomPM{}    = []
defaultPMFlags (InvalidPM _) = undefined

-- | Convert from a "Distributions.Gentoo.Types.Mode" 'Mode.PkgManager' to a
--   'PkgManager' as defined in "Distribution.Gentoo.PkgManager.Types".
toPkgManager :: Mode.PkgManager -> PkgManager
toPkgManager (Mode.Portage _) = Portage
toPkgManager (Mode.PkgCore _) = PkgCore
toPkgManager (Mode.Paludis _) = Paludis
toPkgManager (Mode.CustomPM s _) = CustomPM s

-- | A data type containing the information needed to pass to the package
--   manager, such as targets. The constructor determines which function
--   'buildPkgs' will run.
data BuildPkgs
    -- | Default mode
    = BuildNormal
        -- | the package manager that will be used
        Mode.PkgManager
        -- | Packages that will be rebuilt, passed to the PM as targets
        PendingPackages
        -- | Extra targets
        (Set.Set Target)
    -- | @--mode=reinstall-atoms@
    | BuildRAMode
        -- | Packages that will be marked for rebuild via --reinstall-atoms
        PendingPackages
        -- | atoms/sets that the PM will be targeting
        (Set.Set Target)
        -- | All installed Haskell packages (for use with @--usepkg-exclude@)
        AllPkgs

-- | The set of targets that will be passed to the package manager. This mostly
--   matters for 'BuildNormal', since there are two sets that must be merged
--   for the final target set.
buildPkgsTargets :: BuildPkgs -> Set.Set Target
buildPkgsTargets = \case
    BuildNormal _ pps extraTargs ->
        let pts = Set.singleton $ case pps of
                InvalidPending ps -> TargetInvalid ps
                AllPending as -> TargetAll as
        in pts <> extraTargs
    BuildRAMode _ targs _ -> targs

-- | Get the 'PendingPackages' from a 'BuildPkgs' constructor.
--
--   This is examined by the different looping strategies in order to monitor
--   progress and make choices about when to continue looping.
buildPkgsPending :: BuildPkgs -> PendingPackages
buildPkgsPending = \case
    BuildNormal _ pps _ -> pps
    BuildRAMode pps _ _ -> pps

-- | Write to the global package state. This is generally used with its
--   'IO' instance (using 'buildCmd'/'buildRACmd' to modify the global state),
--   but it is left open as a class for testing purposes.
class MonadExit m => MonadWritePkgState m where
    buildPkgs
        :: BuildPkgs
        -> m (ExitArg m)

instance (ExitArg m ~ ExitCode, MonadExit m, MonadIO m)
    => MonadWritePkgState (EnvT m) where
    buildPkgs bp = do
        rm <- askRunModifier
        rawArgs <- Mode.getExtraRawArgs <$> askPkgManager

        let (cmd, args) = case bp of
                BuildNormal pkgMgr _ _ ->
                    let targs = buildPkgsTargets bp
                    in buildCmd pkgMgr (flags rm) rawArgs (rawPMArgs rm) targs
                BuildRAMode pps targs allPkgs ->
                    buildRACmd (flags rm) rawArgs (rawPMArgs rm) pps targs allPkgs

        liftIO $ putStrLn ""
        liftIO $ runCmd (withCmd rm) cmd args

instance MonadWritePkgState m => MonadWritePkgState (StateT s m) where
    buildPkgs = lift . buildPkgs

instance MonadWritePkgState m => MonadWritePkgState (ReaderT r m) where
    buildPkgs = lift . buildPkgs

-- | Depending on the 'WithCmd' passed, print or execute (or both) a given
--   command. @stdout@ and @stderr@ are discarded.
runCmd :: WithCmd -> String -> [String] -> IO ExitCode
runCmd m cmd args = case m of
    RunOnly     ->                      rawSystem cmd args
    PrintOnly   -> putStrLn cmd_line >> exitSuccess
    PrintAndRun -> putStrLn cmd_line >> rawSystem cmd args
  where
    cmd_line = unwords (cmd : (showArg <$> args))
    showArg s
        | words s == [s] = s
        | otherwise = show s -- Put quotes around args with spaces in them

-- | Create a command for invoking the given 'Mode.PkgManager'.
buildCmd
    :: Mode.PkgManager
    -> [PMFlag] -- ^ Basic flags
    -> ExtraRawArgs -- ^ hard-coded extra flags
    -> RawPMArgs -- ^ User-supplied flags
    -> Set.Set Target -- ^ Packages to be rebuilt, and extra targets
    -> (String, [String])
buildCmd mpm fs (ExtraRawArgs rawArgs) userArgs targs =
    (  pmCommand pm
    ,  defaultPMFlags pm
    ++ mapMaybe (flagRep pm) fs
    ++ rawArgs
    ++ userArgs
    ++ printTargets targs
    )
  where
    pm = toPkgManager mpm

-- | Alternative version of 'buildCmd' which uses experimental @emerge@
--   invocation (using @--reinstall-atoms@). This is only to be used with the
--   'Portage' package manager.
--
--   The rationale is that by marking broken packages by using
--   @--reinstall-atoms@, portage will pretend that they are not yet
--   installed, thus forcing their reinstallation. @--update@ is
--   used and all installed Haskell packages are targeted so that the entire
--   Haskell environment is examined. This has a side-effect of skipping
--   packages that are masked or otherwise unavailable while still rebuilding
--   needed dependencies that have been broken.
buildRACmd
    :: [PMFlag] -- ^ Basic flags
    -> ExtraRawArgs -- ^ hard-coded extra flags
    -> RawPMArgs -- ^ User-supplied flags
    -> PendingPackages -- ^ Packages to be rebuilt
    -> Set.Set Target -- ^ emerge targets
    -> AllPkgs -- ^ for use with 'usepkgExclude'
    -> (String, [String])
buildRACmd fs (ExtraRawArgs rawArgs) userArgs pending targets allPs =
    (  pmCommand Portage
    ,  defaultPMFlags Portage
    ++ mapMaybe (flagRep Portage) fs
    ++ ["--update"]
    ++ rawArgs
    ++ userArgs
    ++ usepkgExclude allPs
    ++ reinst
    ++ printTargets targets
    )
  where
    reinst =
        let raArgs ps
              | Set.null ps = []
              | otherwise = ["--reinstall-atoms", unwords (printPkg <$> Set.toList ps)]
        in raArgs (getPkgs pending)

-- | Print a set of targets, suitable for passing to a package manager
printTargets :: Set.Set Target -> [String]
printTargets targets = Set.toList $ foldr go Set.empty targets
  where
    go targ set = case targ of
        TargetInvalid (InvalidPkgs p) -> foldr (Set.insert . printPkg) set p
        TargetAll (AllPkgs p) -> foldr (Set.insert . printPkg) set p
        CustomTarget t -> Set.insert t set

-- | Generate strings using portage's @--usepkg-exclude@ flag. This filters out
--   dev-haskell/* packages which can be specified using a wildcard, in order
--   to reduce the length of the emerge command a bit.
usepkgExclude :: PackageSet t => t -> [String]
usepkgExclude pkgs0
    | Set.null pkgs = []
    | otherwise = ["--usepkg-exclude", unwords ("dev-haskell/*" : filteredPkgs)]
  where
    filteredPkgs = mapMaybe
        ( \case
              Package "dev-haskell" _ _ -> Nothing
              p -> Just $ printPkg p
        )
        (Set.toList pkgs)
    pkgs = getPkgs pkgs0

-- -----------------------------------------------------------------------------

flagRep               :: PkgManager -> PMFlag -> Maybe String
flagRep Portage       = portagePMFlag
flagRep PkgCore       = pkgcorePMFlag
flagRep Paludis       = cavePMFlag
flagRep CustomPM{}    = const Nothing -- Can't tell how flags would work.
flagRep (InvalidPM _) = undefined

portagePMFlag                :: PMFlag -> Maybe String
portagePMFlag PretendBuild   = Just "--pretend"
portagePMFlag UpdateDeep     = Just "--deep"
portagePMFlag UpdateAsNeeded = Nothing

pkgcorePMFlag :: PMFlag -> Maybe String
pkgcorePMFlag = portagePMFlag -- The options are the same for the 3
                              -- current flags.

cavePMFlag                :: PMFlag -> Maybe String
cavePMFlag PretendBuild   = Just "--no-execute"
cavePMFlag UpdateDeep     = Just "--complete"
cavePMFlag UpdateAsNeeded = Just "--lazy"