packages feed

ghcup-0.2.6.2: lib/GHCup/Command/List.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}

{-|
Module      : GHCup.Command.List
Description : Listing versions and tools
Copyright   : (c) Julian Ospald, 2020
License     : LGPL-3.0
Maintainer  : hasufell@hasufell.de
Stability   : experimental
Portability : portable
-}
module GHCup.Command.List where

import GHCup.Errors
import GHCup.Query.DB
import GHCup.Query.DB.HLS   ( getHLSGHCs )
import GHCup.Query.Metadata
import GHCup.Types
import GHCup.Types.JSON
    ()
import GHCup.Types.Optics

import Control.Applicative
import Control.DeepSeq              ( NFData )
import Control.Monad
#if !MIN_VERSION_base(4,13,0)
import Control.Monad.Fail ( MonadFail )
#endif
import Control.Monad.Reader
import Control.Monad.ST
import Data.Bifunctor
import Data.List
import Data.Maybe
import Data.STRef
import Data.Set             ( Set )
import Data.Text            ( Text )
import Data.Time.Calendar   ( Day )
import Data.Traversable.WithIndex ( iforM )
import Data.Variant.Excepts
import Data.Versions        hiding ( patch )
import Optics
import Prelude              hiding ( abs, writeFile )

import qualified Data.Map.Strict as M
import qualified Data.Map.Strict as Map
import qualified GHC.Generics    as GHC






    ------------------
    --[ List tools ]--
    ------------------


-- | Filter data type for 'listVersions'.
data ListCriteria
  = ListInstalled Bool
  | ListSet Bool
  | ListAvailable Bool
  | ListTarget Bool (Maybe Text)
  | ListTag Bool Tag
  deriving (Eq, Show)


type ToolListResult = M.Map Tool (Maybe ToolDescription, [ListResult])
type ProcessedListResult = M.Map Tool (Maybe ToolDescription, M.Map (Maybe Text) (M.Map Version (M.Map Int ListResult)))

data RevTag = RevUpdate
            | RevOutdated
            | RevNormal
  deriving (Eq, Ord, Show, GHC.Generic)

instance NFData RevTag

-- | A list result describes a single tool version
-- and various of its properties.
data ListResult = ListResult
  { lVer :: Version
  , lCross :: Maybe Text
    -- ^ currently only for GHC
  , lRev :: (Int, RevTag)  -- the Bool indicates whether this is an update from an older rev
  , lTag :: [Tag]
  , lInstalled :: Bool
  , lSet :: Bool
    -- ^ currently active version
  , lStray :: Bool
    -- ^ not in download info
  , lNoBindist :: Bool
    -- ^ whether the version is available for this platform/arch
  , hlsPowered :: Bool
  , lReleaseDay :: Maybe Day
  }
  deriving (Eq, Ord, Show, GHC.Generic)

instance NFData ListResult

makeLensesWith (lensRules & lensField .~ mappingNamer (\n -> [n <> "L"])) ''ListResult

-- | Extract all available tool versions and their tags.
availableToolVersions :: GHCupDownloads -> Tool -> Map.Map TargetVersion VersionMetadata
availableToolVersions av tool = view
  (_GHCupDownloads % at tool % to ((unToolVersionSpec . _toolVersions) <$>) % non Map.empty)
  av


-- | List all versions from the download info, as well as stray
-- versions.
listVersions ::
  forall m env .
  ( HasLog env
  , MonadReader env m
  , HasDirs env
  , HasPlatformReq env
  , HasGHCupInfo env
  , MonadIOish m
  )
  => Maybe [Tool]
  -> [ListCriteria]
  -> ShowRevisions
  -> Bool
  -> ShowNightly
  -> (Maybe Day, Maybe Day)
  -> Excepts '[ParseError] m ToolListResult
listVersions lt' criteria showRevisions hideOld showNightly days = do
  GHCupInfo { _ghcupDownloads = dls } <- getGHCupInfo
  pfreq <- lift getPlatformReq
  instTools <- getAllInstalledTools lt'
  hlsGHCs <- let hlsSet = do
                   hlsMap <- instTools M.!? hls
                   (_, mSet) <- hlsMap M.!? Nothing
                   mSet
             in case hlsSet of
               Just VersionRev{..} -> do
                 getHLSGHCs _vrVersion
               Nothing -> pure []
  pure $ listVersions' dls pfreq (filterTools lt' instTools) hlsGHCs lt' criteria showRevisions hideOld showNightly days

filterTools ::
     Maybe [Tool]
  -> Map.Map Tool (Map.Map (Maybe Text) ([VersionRev], Maybe VersionRev))
  -> Map.Map Tool (Map.Map (Maybe Text) ([VersionRev], Maybe VersionRev))
filterTools Nothing    tools = tools
filterTools (Just lt') tools = M.filterWithKey (\k _ -> k `elem` lt') tools

listVersions' ::
     GHCupDownloads
  -> PlatformRequest
  -> Map.Map Tool (Map.Map (Maybe Text) ([VersionRev], Maybe VersionRev))
  -> [Version]
  -> Maybe [Tool]
  -> [ListCriteria]
  -> ShowRevisions
  -> Bool
  -> ShowNightly
  -> (Maybe Day, Maybe Day)
  -> ToolListResult
listVersions' dls pfreq instTools hlsGHCs lt' criteria showRevisions hideOld showNightly days =
  -- available tools from the metadata
  let allAvailableTools' :: [(Tool, [(TargetVersion, VersionMetadata)])]
        = maybe (allAvailableTools dls) (\ts -> filter (\(t, _) -> t `elem` ts) $ allAvailableTools dls) lt'
      avTools :: M.Map Tool (M.Map (Maybe Text) (Set (Version, VersionMetadata)))
        = M.fromList $ (fmap . fmap) groupByTargetS allAvailableTools'

      avToolsProcessed = runST $ do
        stRefAvToolsProcessed <- newSTRef mempty

        -- process installed tools first
        void $ iforM instTools $ \tool targetMap -> do
          iforM targetMap $ \target (vers', mset) ->
            forM_ vers' $ \vr@VersionRev{..} -> do
              let tver = TargetVersion target _vrVersion
                  mvm = getVersionMetadata tver tool dls
                  tags = maybe [] _vmTags mvm
                  lReleaseDay = mvm >>= _vmReleaseDay
                  lRev = (_vrRev, RevNormal)
                  tDesc = preview (_GHCupDownloads % ix tool % toolDetails % _Just) dls

                  hlsPowered = getHlsPowered tool target _vrVersion

              -- lNoBindist and lStray are updated when we traverse the metadata
              -- bindist tags (as opposed to tool tags) will also be added later
              let lr = ListResult { lVer = _vrVersion
                                  , lCross = target
                                  , lTag = tags
                                  , lSet = mset == Just vr
                                  , lInstalled = True
                                  , lNoBindist = False
                                  , lStray = True
                                  , ..
                                  }

              insertListResult stRefAvToolsProcessed tool target tDesc _vrVersion _vrRev lr

        installedProcessed <- readSTRef stRefAvToolsProcessed

        -- then process tools in the metadata, that are not installed
        -- and add the installed ones in the process
        void $ iforM avTools $ \tool targetMap -> do
          let tDesc = preview (ix tool % _1 % _Just) installedProcessed <|> preview (_GHCupDownloads % ix tool % toolDetails % _Just) dls
          iforM targetMap $ \target vers' ->
            forM_ vers' $ \(ver', VersionMetadata{..}) -> do
              -- versions from the metadata that's already installed
              let avInstVers :: M.Map Version (M.Map Int ListResult) =
                    fromMaybe mempty $ do
                      (_, m) <- installedProcessed M.!? tool
                      m M.!? target

              let lReleaseDay = _vmReleaseDay

              let (Rev latestMetaRev) = maximum $ M.keys (unRevisionSpec _vmRevisionSpec)


              -- add all revision
              forM_ (M.toList (unRevisionSpec _vmRevisionSpec)) $ \(Rev metaRev, vi) -> do
                let mdli = getDownloadInfo' pfreq vi
                let bTags = fromMaybe [] $ mdli >>= _dlTag

                -- this has to consider the installed rev as well,
                -- which may or may not be in the metadata

                case iheadOf (ix ver' % itraversed) avInstVers of
                  -- it's installed
                  Just (installedRev, installedLr) -> do
                    if installedRev == metaRev
                    then do -- and even the same revision
                      -- add information from the metadata to the installed rev
                      modifyListResult stRefAvToolsProcessed tool target ver' installedRev
                        [ lNoBindistL .~ (isNothing mdli && not (lStray installedLr))
                        , lTagL       %~ (<> bTags)
                        , lStrayL     .~ False
                        ]
                    else do -- installed, but not this revision
                      when (showRevisions /= ShowNone) $ do

                        -- is it the latest revision?
                        let isLatestRev = metaRev == latestMetaRev && (metaRev > installedRev)
                        -- it could be an update
                        let isUpdate = isLatestRev

                        -- tag shenanigans
                        let dropOld = if isUpdate then filter (/= Old) else id
                            filterNotLatestRec = filter (`notElem` [Latest, Recommended])
                            dropLatestRec = if isLatestRev then id else filterNotLatestRec

                        when isUpdate $
                          -- remove the latest/recommended tag from the installed revision
                          modifyListResult stRefAvToolsProcessed tool target ver' installedRev
                            [ lRevL % _2 .~ RevOutdated
                            , lTagL      %~ filterNotLatestRec
                            ]

                        -- Insert the installed revision
                        -- updates are always inserted, but non-updates are only relevant if the user wants to see all revisions
                        let lrNew = ListResult
                                      { lVer = ver'
                                      , lCross = target
                                      , lTag = dropLatestRec $ dropOld (_vmTags <> bTags)
                                      , lSet = False
                                      , lInstalled = False
                                      , hlsPowered = False
                                      , lNoBindist = isNothing mdli
                                      , lStray = False
                                      , lRev = (metaRev, if isUpdate then RevUpdate else RevNormal)
                                      , ..
                                      }
                        when (showRevisions == ShowAll || isUpdate) $
                          insertListResult stRefAvToolsProcessed tool target tDesc ver' metaRev lrNew
                  -- not installed, just insert
                  Nothing -> do
                    let isLatestRev = metaRev == latestMetaRev
                    let lr = ListResult
                               { lVer = ver'
                               , lCross = target
                               , lTag = _vmTags <> bTags
                               , lSet = False
                               , lInstalled = False
                               , hlsPowered = False
                               , lNoBindist = isNothing mdli
                               , lStray = False
                               , lRev = (metaRev, RevNormal)
                               , ..
                               }
                    when (showRevisions == ShowAll || isLatestRev) $
                      insertListResult stRefAvToolsProcessed tool target tDesc ver' metaRev lr
        readSTRef stRefAvToolsProcessed

  in toToolListResult avToolsProcessed

 where
  toToolListResult :: ProcessedListResult -> ToolListResult
  toToolListResult = M.map (second (toListOf (traversed % traversed % traversed)))

  modifyListResult ::
       STRef s ProcessedListResult
    -> Tool
    -> Maybe Text
    -> Version
    -> Int
    -> [ListResult -> ListResult]
    -> ST s ()
  modifyListResult ref tool target ver' rev setters =
    modifySTRef' ref
      (ix tool % _2 % ix target % ix ver' % at rev %~ (>>= (\lr' -> filter' (foldl (&) lr' setters))))

  insertListResult ::
       STRef s ProcessedListResult
    -> Tool
    -> Maybe Text
    -> Maybe ToolDescription
    -> Version
    -> Int
    -> ListResult
    -> ST s ()
  insertListResult ref tool target tDesc ver' rev lr' =
    modifySTRef' ref
      (at tool % non (tDesc, mempty) % _2 % at target % non mempty % at ver' % non mempty % at rev .~ filter' lr')

  getHlsPowered tool target ver' =
    (tool == ghc && isNothing target) && ver' `elem` hlsGHCs

  filter' :: ListResult -> Maybe ListResult
  filter' lr = do
    lrN <- filterNightly lr
    lrOld <- filterOld lrN
    lrCrit <- if foldr (\a b -> fromCriteria a lrOld && b) True criteria
              then Just lrOld
              else Nothing
    filterDays lrCrit

  filterDays :: ListResult -> Maybe ListResult
  filterDays lr@ListResult{..} =
    case days of
      (Nothing, Nothing)    -> Just lr
      (Just from, Just to')
        | Just True <- (\d -> d >= from && d <= to') <$> lReleaseDay
        -> Just lr
        | otherwise -> Nothing
      (Nothing, Just to')
        | Just True <- (<= to') <$> lReleaseDay
        -> Just lr
        | otherwise -> Nothing
      (Just from, Nothing)
        | Just True <- (>= from) <$> lReleaseDay
        -> Just lr
        | otherwise -> Nothing

  fromCriteria :: ListCriteria -> ListResult -> Bool
  fromCriteria lc ListResult{..} = case lc of
    ListInstalled  b -> f b lInstalled
    ListSet        b -> f b lSet
    ListAvailable  b -> f b $ not lNoBindist
    ListTarget     b t -> f b (lCross == t)
    ListTag        b t -> f b (t `elem` lTag)
   where
    f b
      | b         = id
      | otherwise = not

  filterOld :: ListResult -> Maybe ListResult
  filterOld lr@ListResult {..}
    | hideOld   = if lInstalled || Old `notElem` lTag then Just lr else Nothing
    | otherwise = Just lr

  filterNightly :: ListResult -> Maybe ListResult
  filterNightly lr@ListResult {..} = case showNightly of
    NShowAll -> Just lr
    NShowLatest -> if | lInstalled -> Just lr
                      | LatestNightly `elem` lTag -> Just lr
                      | Nightly `elem` lTag -> Nothing
                      | otherwise -> Just lr
    NShowNone -> if | lInstalled -> Just lr
                    | LatestNightly `elem` lTag -> Nothing
                    | Nightly `elem` lTag -> Nothing
                    | otherwise -> Just lr