packages feed

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

{- |
   Module      : Distribution.Gentoo.CmdLine.Types

   Functions and logic for parsing command line options and converting them
   into a valid internal representation of haskell-updater modes (see
   @Distribution.Gentoo.Types.Mode@).
 -}

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}

module Distribution.Gentoo.CmdLine
  ( parseArgs
  , mkHUMode
  , options
  , argString
  ) where

import           Control.Monad         ((>=>))
import qualified Data.List.NonEmpty as NE
import           Data.Monoid (Ap(..))
import           Data.Maybe (fromMaybe)
import           Data.Proxy
import           Data.Semigroup (Last(..), sconcat)
import           System.Console.GetOpt

import Distribution.Gentoo.CmdLine.Types
import Distribution.Gentoo.PkgManager
import Distribution.Gentoo.PkgManager.Types
import Distribution.Gentoo.Types
import qualified Distribution.Gentoo.Types.Mode as Mode
import Distribution.Gentoo.Util (These(..), singletonNE)
import Output

-- | Process arguments from the command line. Returns an error string if the
--   user provided incorrect or unknown options.
parseArgs :: PkgManager -> RawPMArgs -> Either String (CmdLineArgs, RawPMArgs)
parseArgs defPM args = case getOpt' Permute options args of
    (_, _, _, errs@(_:_)) -> Left $ unwords $ "Errors in arguments:" : errs
    (_, _, unk@(_:_), _) -> Left $ unwords $ "Unknown options:" : unk
    (fs, raw, _, _) ->
        (,raw) <$> foldr (>=>) pure fs (defCmdLineArgs defPM)

-- | Parse processed command line arguments into a 'Mode.HUMode'. Returns an
--   error string if the user supplied non-compatible option combinations.
mkHUMode :: CmdLineArgs -> RawPMArgs -> Either String Mode.HUMode
mkHUMode cmdLine raw
    | cmdLineHelp cmdLine = pure Mode.HelpMode
    | cmdLineVersion cmdLine = pure Mode.VersionMode
    | otherwise = do
        mPkgMgr <- mkPkgManager (cmdLinePkgManager cmdLine)
        pure $ Mode.RunMode runModifier mPkgMgr
  where
    mkPkgManager :: PkgManager -> Either String Mode.PkgManager
    mkPkgManager = \case
        Portage -> Mode.Portage <$> mkPortageMode (cmdLineMode cmdLine)
        PkgCore -> Mode.PkgCore <$> mkMode (cmdLineMode cmdLine)
        Paludis -> Mode.Paludis <$> mkMode (cmdLineMode cmdLine)
        CustomPM pm -> Mode.CustomPM pm <$> mkMode (cmdLineMode cmdLine)
        pm@(InvalidPM _) -> Left $
            "Invalid package manager in mkHUMode: " ++ show pm

    -- Logic for parsing modes for non-portage package managers
    mkMode :: RunMode -> Either String Mode.RunMode
    mkMode = \case
        BasicMode -> Mode.BasicMode <$> go (cmdLineTargets cmdLine)
        ListMode -> Mode.ListMode <$> go (cmdLineTargets cmdLine)
        ReinstallAtomsMode -> Left
            "reinstall-atoms mode is only supported by the portage package manager"
      where
        go = maybe (Right Mode.OnlyInvalid) (onlyLast mkTarget)

    -- Logic for parsing targets for non-portage package managers
    mkTarget :: Either CustomTarget BuildTarget -> Either String Mode.Target
    mkTarget = \case
        Right OnlyInvalid -> Right Mode.OnlyInvalid
        Right AllInstalled -> Right Mode.AllInstalled
        Right PreservedRebuild -> Left $
            "preserved-rebuild target is only supported by the portage \
            \package manager"
        Right WorldTarget -> Left
            "world target is only supported in reinstall-atoms mode"
        Left _ -> Left
            "custom targets are only supported in reinstall-atoms mode"

    -- Logic for parsing modes for portage
    mkPortageMode
        :: RunMode
        -> Either String Mode.PortageMode
    mkPortageMode = \case
        BasicMode -> Mode.PortageBasicMode
            <$> withDefTarget
                    (onlyLast mkPortageBasicTarget)
                    maybeTargs
        ListMode -> Mode.PortageListMode
            <$> withDefTarget
                    (onlyLast mkPortageTarget)
                    maybeTargs
        ReinstallAtomsMode -> Mode.ReinstallAtomsMode
            <$> withDefTarget mkPortageRATarget maybeTargs
      where
        maybeTargs = cmdLineTargets cmdLine

    -- Logic for parsing targets for portage's basic mode
    mkPortageBasicTarget
        :: Either CustomTarget BuildTarget
        -> Either String (Either Mode.PortageBasicTarget Mode.Target)
    mkPortageBasicTarget = \case
        Right PreservedRebuild -> Right $ Left Mode.PreservedRebuild
        targ -> Right <$> mkPortageTarget targ

    -- Logic for parsing targets for portage's reinstall-atoms mode
    mkPortageRATarget
        :: NE.NonEmpty (Either CustomTarget BuildTarget)
        -> Either String Mode.RATargets
    mkPortageRATarget = foldTargets $ \case
        Left ct -> Right $ Mode.RATargets $ That $ That $ singletonNE ct
        Right WorldTarget -> Right $ Mode.RATargets $ That $ This $
            if cmdLineWorldFull cmdLine
                then Mode.WorldFullTarget
                else Mode.WorldTarget
        targ -> Mode.RATargets . This <$> mkPortageTarget targ

    -- Logic for parsing targets for portage's list mode; also common logic
    -- for parsing targets, between portage's basic and reinstall-atoms modes
    mkPortageTarget
        :: Either CustomTarget BuildTarget
        -> Either String Mode.Target
    mkPortageTarget = \case
        Right OnlyInvalid -> Right Mode.OnlyInvalid
        Right AllInstalled -> Right Mode.AllInstalled
        Right PreservedRebuild -> Left
            "preserved-rebuild target is only supported in basic mode"
        Right WorldTarget -> Left
            "world target is only supported in reinstall-atoms mode"
        Left _ -> Left
            "custom targets are only supported in reinstall-atoms mode"

    runModifier :: RunModifier
    runModifier = RM
        { flags = (if cmdLinePretend cmdLine then (PretendBuild:) else id)
                    $ if cmdLineNoDeep cmdLine
                        then [UpdateAsNeeded]
                        else [UpdateDeep]
        , withCmd = cmdLineAction cmdLine
        , rawPMArgs = raw
        , verbosity = cmdLineVerbosity cmdLine
        }

    -- Uses the default target if none were specified on the command line
    withDefTarget
        :: (NE.NonEmpty (Either CustomTarget BuildTarget) -> a)
        -> Maybe (NE.NonEmpty (Either CustomTarget BuildTarget))
        -> a
    withDefTarget f = f . fromMaybe (NE.singleton defTarget)
      where
        defTarget = Right OnlyInvalid

    -- Uses 'Data.Semigroup.Last' to only grab the last target specified
    onlyLast
        :: Applicative f
        => (a -> f b)
        -> NE.NonEmpty a
        -> f b
    onlyLast f = fmap (getLast . sconcat . fmap Last) . traverse f

    -- Uses 'Data.Monoid.Ap' to combine RATargets inside an Applicative
    foldTargets
        :: (Applicative f, Semigroup b)
        => (a -> f b)
        -> NE.NonEmpty a
        -> f b
    foldTargets f = getAp . sconcat . fmap (Ap . f)

options :: [OptDescr (CmdLineArgs -> Either String CmdLineArgs)]
options =
    [ Option ['P'] ["package-manager"]
      (ReqArg mkPM "PM")
        $ "Use package manager PM, where PM can be one of:\n"
              ++ pmList ++ defPM
    , Option ['C'] ["custom-pm"]
      (ReqArg (\s c -> pure $ c { cmdLinePkgManager = CustomPM s }) "command")
        $ "Use custom command as package manager;\n"
          ++ "    ignores the --pretend and --no-deep flags."
    , Option ['p'] ["pretend"]
        (naUpdate $ \c -> c { cmdLinePretend = True } )
        "Only pretend to build packages."
    , Option []    ["no-deep"]
        (naUpdate $ \c -> c { cmdLineNoDeep = True } )
        "Don't pull deep dependencies (--deep with emerge)."
    , Option ['V'] ["version"]
        (naUpdate $ \c -> c { cmdLineVersion = True })
        "Version information."
    , Option []    ["action"]
        (ReqArg (fromCmdline (\a c -> c { cmdLineAction = a })) "action")
        (argHelp (Proxy @WithCmd))
    , Option []    ["target"]
        (ReqArg (fromCmdline (\a -> updateTarget (Right a))) "target")
        (argHelp (Proxy @BuildTarget))
    , Option ['c'] ["dep-check"]
        (naUpdate $ updateTarget (Right OnlyInvalid))
        $ "alias for --target=" ++ argString OnlyInvalid
      -- deprecated alias for 'dep-check'
    , Option ['u'] ["upgrade"]
        (naUpdate $ updateTarget (Right OnlyInvalid))
        $ "alias for --target=" ++ argString OnlyInvalid
    , Option ['a'] ["all"]
        (naUpdate $ updateTarget (Right AllInstalled))
        $ "alias for --target=" ++ argString AllInstalled
    , Option ['W']    ["world"]
        (naUpdate $ \c -> updateTarget (Right WorldTarget) c
            { cmdLinePkgManager = Portage
            , cmdLineMode = ReinstallAtomsMode
            }
        ) $      "alias for --package-manager=portage"
         ++ " \\\n          --target=" ++ argString WorldTarget
         ++ " \\\n          --mode=" ++ argString ReinstallAtomsMode
    , Option [] ["world-full"]
        (naUpdate $ \c -> updateTarget (Right WorldTarget) c
            { cmdLinePkgManager = Portage
            , cmdLineMode = ReinstallAtomsMode
            , cmdLineWorldFull = True
            }
        ) $ "alias for --world -- --newuse --with-bdeps=y"
    , Option [] ["preserved-rebuild"]
        (naUpdate $ updateTarget (Right PreservedRebuild))
        $ "alias for --target=" ++ argString PreservedRebuild
    , Option ['T'] ["custom-target"]
        (ReqArg
            (\s c -> pure $ updateTarget (Left s) c
                { cmdLinePkgManager = Portage
                , cmdLineMode = ReinstallAtomsMode
                }
            )
        "target")
        "Use a custom target. May be given multiple times.\n\
        \    Enables portage PM and reinstall-targets mode."
    , Option []    ["mode"]
        (ReqArg (fromCmdline (\a c -> c { cmdLineMode = a })) "mode")
        (argHelp (Proxy @RunMode))
    , Option ['l'] ["list-only"]
        (naUpdate $ \c -> c { cmdLineMode = ListMode })
        $ "alias for --mode=" ++ argString ListMode
    , Option ['R']    ["reinstall-atoms"]
        (naUpdate $ \c -> c { cmdLineMode = ReinstallAtomsMode })
        $ "alias for --mode=" ++ argString ReinstallAtomsMode
    , Option ['q']      ["quiet"]
        (naUpdate $ \c -> c { cmdLineVerbosity = Quiet })
        "Print only fatal errors (to stderr)."
    , Option ['v']      ["verbose"]
        (naUpdate $ \c -> c { cmdLineVerbosity = Verbose })
        "Be more elaborate (to stderr)."
    , Option ['h', '?'] ["help"]
        (naUpdate $ \c -> c { cmdLineHelp = True })
        "Print this help message."
    ]

  where
    naUpdate f = NoArg (pure . f)

    -- This touches some legacy code so we need a custom handler for it
    mkPM :: String -> CmdLineArgs -> Either String CmdLineArgs
    mkPM s c = case choosePM s of
        InvalidPM pm -> Left $ "Unknown package manager: " ++ pm
        Portage -> Right $ c { cmdLinePkgManager = Portage }
        Paludis -> Right $ c { cmdLinePkgManager = Paludis }
        PkgCore -> Right $ c { cmdLinePkgManager = PkgCore }
        CustomPM _ -> error "Undefined behavior in mkPM"

    pmList = unlines . map (" * " ++) $ definedPMs
    defPM = "The last valid value of PM specified is chosen.\n\
            \    The default package manager is: " ++ defaultPMName ++ ",\n\
            \    which can be overriden with the \"PACKAGE_MANAGER\"\n\
            \    environment variable."

    updateTarget :: Either CustomTarget BuildTarget -> CmdLineArgs -> CmdLineArgs
    updateTarget new old =
        let ne = NE.singleton new
            newT = maybe ne (<> ne) (cmdLineTargets old)
        in old { cmdLineTargets = Just newT }