packages feed

ghcup-0.2.1.0: lib-opt/GHCup/OptParse/Config.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-}

module GHCup.OptParse.Config where


import GHCup.Errors
import GHCup.Hardcoded.URLs
import GHCup.Input.Parsers     ( parseNewUrlSource )
import GHCup.OptParse.Common
import GHCup.OptParse.Reset    ( resetUserConfig, toUserSettingsKey )
import GHCup.Prelude
import GHCup.Prelude.String.QQ
import GHCup.Query.GHCupDirs
import GHCup.Types

#if !MIN_VERSION_base(4,13,0)
import Control.Monad.Fail ( MonadFail )
#endif
import Control.Exception               ( displayException )
import Control.Exception.Safe          ( MonadMask )
import Control.Monad                   ( when )
import Control.Monad.Reader
import Control.Monad.Trans.Resource
import Data.Foldable                   ( foldl' )
import Data.Functor
import Data.Maybe
import Data.Variant.Excepts
import Options.Applicative             hiding ( ParseError, style )
import Options.Applicative.Pretty.Shim ( text )
import Prelude                         hiding ( appendFile )
import System.Exit

import qualified Data.ByteString.UTF8 as UTF8
import qualified Data.Text            as T
import qualified Data.Yaml.Aeson      as Y




    ----------------
    --[ Commands ]--
    ----------------


data ConfigCommand
  = ShowConfig
  | SetConfig String (Maybe String)
  | ResetConfig ResetCommand
  | InitConfig
  | AddReleaseChannel Bool NewURLSource
  deriving (Eq, Show)

data ResetCommand
  = ResetKeys [String]
  | ResetAll
  deriving (Eq, Show)


    ---------------
    --[ Parsers ]--
    ---------------


configP :: Parser ConfigCommand
configP = subparser
      (  command "init" initP
      <> command "set"  setP -- [set] KEY VALUE at help lhs
      <> command "reset" resetP
      <> command "show" showP
      <> command "add-release-channel" addP
      )
    <|> pure ShowConfig
 where
  initP = info (pure InitConfig) (progDesc "Write default config to ~/.ghcup/config.yaml")
  showP = info (pure ShowConfig) (progDesc "Show current config (default)")
  setP  = info setArgsP (progDesc "Set config KEY to VALUE (or specify as single json value)" <> footerDoc (Just $ text configSetFooter))
  setArgsP = SetConfig <$> argument str (metavar "<JSON_VALUE | YAML_KEY>") <*> optional (argument str (metavar "YAML_VALUE"))
  resetP = info resetArgsP
    (progDesc "Reset the whole config or just specific keys" <> footerDoc (Just $ text configResetFooter))
  resetArgsP = ResetConfig <$> subparser
       ( command "all"
         (info (pure ResetAll) (progDesc "Reset the whole config"))
      <> command "keys"
         (info resetKeysP (progDesc "Reset specific keys of the config"))
       )
  resetKeysP = ResetKeys <$> some (strArgument
    (  metavar "YAML_KEY"
    <> help "Specify key(s)" ))
  addP  = info (AddReleaseChannel <$> switch (long "force" <> help "Delete existing entry (if any) and append instead of failing")
                <*> argument (eitherReader parseNewUrlSource) (metavar "<URL_SOURCE|cross|prereleases|vanilla>" <> completer urlSourceCompleter))
    (progDesc "Add a release channel, e.g. from a URI or using alias")


    --------------
    --[ Footer ]--
    --------------


configFooter :: String
configFooter = [s|Examples:

  # show current config
  ghcup config

  # initialize config
  ghcup config init

  # set <key> <value> configuration pair
  ghcup config set <key> <value>

  # reset config key(s)
  ghcup config reset keys <key> <key> ...

  # add a release channel
  ghcup config add-release-channel prereleases|]

configSetFooter :: String
configSetFooter = [s|Examples:
  # disable caching
  ghcup config set cache false

  # switch downloader to wget
  ghcup config set downloader Wget

  # set vanilla channel as default
  ghcup config set url-source vanilla

  # use the default GHCup channel
  ghcup config set url-source GHCupURL

  # set mirror for ghcup metadata
  ghcup config set '{url-source: { OwnSource: "<url>"}}'|]

configResetFooter :: String
configResetFooter = [s|Examples:
  # reset the whole config
  ghcup config reset all

  # reset one key (cache)
  ghcup config reset keys cache

  # reset some keys (cache, url-source and downloader)
  ghcup config reset keys cache url-source downloader|]


    -----------------
    --[ Utilities ]--
    -----------------


formatConfig :: UserSettings -> String
formatConfig = UTF8.toString . Y.encode


updateSettings :: UserSettings -> UserSettings -> UserSettings
updateSettings usl usr =
   let cache'      = uCache usl      <|> uCache usr
       metaCache'  = uMetaCache usl  <|> uMetaCache usr
       metaMode'   = uMetaMode usl   <|> uMetaMode usr
       noVerify'   = uNoVerify usl   <|> uNoVerify usr
       verbose'    = uVerbose usl    <|> uVerbose usr
       keepDirs'   = uKeepDirs usl   <|> uKeepDirs usr
       downloader' = uDownloader usl <|> uDownloader usr
       urlSource'  = uUrlSource usl  <|> uUrlSource usr
       noNetwork'  = uNoNetwork usl  <|> uNoNetwork usr
       gpgSetting' = uGPGSetting usl <|> uGPGSetting usr
       platformOverride' = uPlatformOverride usl <|> uPlatformOverride usr
       mirrors' = uMirrors usl <|> uMirrors usr
       defGHCconfOptions' = uDefGHCConfOptions usl <|> uDefGHCConfOptions usr
       pagerConfig' = uPager usl <|> uPager usr
       guessVersion' = uGuessVersion usl <|> uGuessVersion usr
       buildWrapper' = uBuildWrapper usl <|> uBuildWrapper usr
   in UserSettings cache' metaCache' metaMode' noVerify' verbose' keepDirs' downloader' (updateKeyBindings (uKeyBindings usl) (uKeyBindings usr)) urlSource' noNetwork' gpgSetting' platformOverride' mirrors' defGHCconfOptions' pagerConfig' guessVersion' buildWrapper'
 where
  updateKeyBindings :: Maybe UserKeyBindings -> Maybe UserKeyBindings -> Maybe UserKeyBindings
  updateKeyBindings Nothing Nothing = Nothing
  updateKeyBindings (Just kbl) Nothing = Just kbl
  updateKeyBindings Nothing (Just kbr) = Just kbr
  updateKeyBindings (Just kbl) (Just kbr) =
       Just $ UserKeyBindings {
           kUp = kUp kbl <|> kUp kbr
         , kDown = kDown kbl <|> kDown kbr
         , kQuit = kQuit kbl <|> kQuit kbr
         , kLeft = kLeft kbl <|> kLeft kbr
         , kRight = kRight kbl <|> kRight kbr
         , kTab = kTab kbl <|> kTab kbr
         , kInstall = kInstall kbl <|> kInstall kbr
         , kUninstall = kUninstall kbl <|> kUninstall kbr
         , kSet = kSet kbl <|> kSet kbr
         , kChangelog = kChangelog kbl <|> kChangelog kbr
         , kShowAll = kShowAll kbl <|> kShowAll kbr
         }



    ------------------
    --[ Entrypoint ]--
    ------------------

data Duplicate
  = Duplicate -- ^ there is a duplicate somewhere in the middle
  | NoDuplicate -- ^ there is no duplicate
  | DuplicateLast


config :: forall m. ( Monad m
          , MonadMask m
          , MonadUnliftIO m
          , MonadFail m
          )
     => ConfigCommand
     -> Settings
     -> UserSettings
     -> KeyBindings
     -> (IO (AppState, IO ()), LeanAppState)
     -> m ExitCode
config configCommand settings userConf keybindings (_getAppState', leanAppstate) = case configCommand of
  InitConfig -> do
    path <- getConfigFilePath
    liftIO $ writeFile path $ formatConfig $ fromSettings settings (Just keybindings)
    runLogger $ logDebug $ "config.yaml initialized at " <> T.pack path
    pure ExitSuccess

  ShowConfig -> do
    liftIO $ putStrLn $ formatConfig $ fromSettings settings (Just keybindings)
    pure ExitSuccess

  (SetConfig k mv) -> do
    r <- runE @'[JSONError, ParseError] $ do
      case mv of
        Just "" ->
          throwE $ ParseError "Empty values are not allowed"
        Nothing -> do
          usersettings <- decodeSettings k
          when (usersettings == defaultUserSettings)
            $ throwE $ ParseError ("Failed to parse setting (maybe typo?): " <> k)
          lift $ doConfig usersettings
          pure ()
        Just v -> do
          usersettings <- decodeSettings (k <> ": " <> v <> "\n")
          when (usersettings == defaultUserSettings)
            $ throwE $ ParseError ("Failed to parse key '" <> k <> "' with value '" <> v <> "' as user setting. Maybe typo?")
          lift $ doConfig usersettings
          pure ()
    case r of
      VRight _ -> pure ExitSuccess
      VLeft (V (JSONDecodeError e)) -> do
        runLogger $ logError $ "Error decoding config: " <> T.pack e
        pure $ ExitFailure 65
      VLeft e -> do
        runLogger (logError $ T.pack $ prettyHFError e)
        pure $ ExitFailure 65
  (ResetConfig resetCommand) -> do
    r <- runE @'[ParseError] $ do
      case resetCommand of
        ResetAll -> do
          lift $ doReset defaultUserSettings
          pure ()
        ResetKeys stringKeys -> do
          lift $ runLogger $ logDebug $ "Raw keys: " <> T.pack (show stringKeys)
          let eKeys = traverse toUserSettingsKey stringKeys
          lift $ runLogger $ logDebug $ "Handled keys: " <> T.pack (show eKeys)
          case eKeys of
            Left invalidString -> do
              throwE $ ParseError $ "Key <<" <> invalidString <> ">> is invalid"
            Right keys -> do
              lift $ runLogger $ logDebug $ "userConf: " <> T.pack (show userConf)
              let newUserConf = foldl' (\conf key -> resetUserConfig conf key ) userConf keys
              lift $ doReset newUserConf
              pure ()
    case r of
      VRight _ -> pure ExitSuccess
      VLeft e -> do
        runLogger (logError $ T.pack $ prettyHFError e)
        pure $ ExitFailure 65

  AddReleaseChannel force new -> do
    r <- runE @'[DuplicateReleaseChannel] $ do
      let oldSources = urlSource settings
      let merged = oldSources ++ [new]
      case checkDuplicate (aliasToURI <$> oldSources) (aliasToURI new) of
        Duplicate
          | not force -> throwE (DuplicateReleaseChannel new)
        DuplicateLast -> pure ()
        _ -> lift $ doConfig (defaultUserSettings { uUrlSource = Just $ SimpleList merged })
    case r of
      VRight _ -> do
        pure ExitSuccess
      VLeft e -> do
        runLogger $ logError $ T.pack $ prettyHFError e
        pure $ ExitFailure 15

 where
  runLogger = flip runReaderT leanAppstate

  checkDuplicate :: Eq a => [a] -> a -> Duplicate
  checkDuplicate xs a
    | last xs == a = DuplicateLast
    | a `elem` xs  = Duplicate
    | otherwise    = NoDuplicate

  aliasToURI :: NewURLSource -> NewURLSource
  aliasToURI (NewChannelAlias a) = NewURI (channelURL a)
  aliasToURI v                   = v

  doConfig :: MonadIO m => UserSettings -> m ()
  doConfig usersettings = do
    let settings' = updateSettings usersettings userConf
    path <- liftIO getConfigFilePath
    liftIO $ writeFile path $ formatConfig settings'
    runLogger $ logDebug $ T.pack $ show settings'
    pure ()

  doReset :: MonadIO m => UserSettings -> m ()
  doReset resetUserSettings = do
    path <- liftIO getConfigFilePath
    liftIO $ writeFile path $ formatConfig resetUserSettings
    runLogger $ logDebug $ "reset to config: " <> T.pack (show resetUserSettings)
    pure ()

  decodeSettings = lE' (JSONDecodeError . displayException) . Y.decodeEither' . UTF8.fromString