packages feed

ghcup-0.2.6.2: lib/GHCup/Query/DB.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE PatternSynonyms #-}

module GHCup.Query.DB where

import GHCup.Errors
import GHCup.Hardcoded.Version
import GHCup.Input.SymlinkSpec
import GHCup.Legacy.Utils
import GHCup.Prelude
import GHCup.Prelude.MegaParsec
import GHCup.Query.GHCupDirs
import GHCup.Types
import GHCup.Types.Optics

import Control.DeepSeq                ( force )
import Control.Exception
    ( Exception (displayException), SomeException, evaluate )
import Control.Exception.Safe         ( try )
import Control.Monad                  ( forM )
import Control.Monad.Catch            ( MonadCatch )
import Control.Monad.IO.Class         ( MonadIO, liftIO )
import Control.Monad.Reader           ( MonadReader )
import Control.Monad.Trans            ( lift )
import Data.Either                    ( rights )
import Data.Functor                   ( (<&>) )
import Data.List                      ( nub, nubBy )
import Data.Set                       ( Set )
import Data.Text                      ( Text )
import Data.Variant.Excepts
    ( Excepts, liftE, pattern V, pattern VLeft, pattern VRight, runE, throwE )
import Data.Versions                  ( Version, version )
import Data.Yaml                      ( decodeEither' )
import Optics                         ( preview, (%) )
import System.FilePath                ( takeExtension, (</>) )
import System.IO.Error                ( doesNotExistErrorType )
import Text.PrettyPrint.HughesPJClass ( prettyShow )

import qualified Data.ByteString as B
import qualified Data.Map.Strict as Map
import qualified Data.Set        as Set
import qualified Data.Text       as T
import qualified Data.Text.IO    as T
import qualified Text.Megaparsec as MP



    --------------------
    --[ DownloadInfo ]--
    --------------------


getInstallMetadata ::
  ( MonadIO m
  , MonadCatch m
  , MonadReader env m
  , HasDirs env
  , HasLog env
  , MonadFail m
  )
  => Tool
  -> TargetVersion
  -> Excepts '[FileDoesNotExistError, ParseError] m InstallMetadata
getInstallMetadata tool tver = do
  f <- lift $ recordedInstallationSpecFile tool tver
  -- we have to trigger 'doesNotExistErrorType' explicitly, since libyaml swallows it, so
  -- we have to avoid 'decodeFileEither':
  --   https://github.com/snoyberg/yaml/blob/7380d7f560daa2f45ff265d425866f497ca07966/libyaml/src/Text/Libyaml.hs#L656-L657
  r <- liftIOException doesNotExistErrorType (FileDoesNotExistError f) $ liftIO $ do
    contents <- B.readFile f
    pure $ decodeEither' contents
  either (throwE . ParseError . displayException) pure r


getInstalledRevision ::
  ( MonadIO m
  , MonadCatch m
  , MonadReader env m
  , HasDirs env
  , HasLog env
  , MonadFail m
  )
  => Tool
  -> TargetVersion
  -> m Int
getInstalledRevision tool tver = do
  runE (getInstallMetadata tool tver) >>= \case
    VLeft _ -> pure 0
    VRight InstallMetadata{..} -> pure _imRevision


    --------------------
    --[ Symlink spec ]--
    --------------------


getSymlinkSpec ::
  ( MonadReader env m
  , HasDirs env
  , HasPlatformReq env
  , HasLog env
  , MonadIOish m
  )
  => Tool
  -> TargetVersion
  -> Excepts '[FileDoesNotExistError, ParseError, NoInstallInfo] m [SymlinkFileSpec]
getSymlinkSpec tool tver = do
  spec <- getSymlinkSpec' tool tver

  -- substitute
  pure $ spec <&> substituteSpec

getSymlinkSpec' ::
  ( MonadReader env m
  , HasDirs env
  , HasPlatformReq env
  , HasLog env
  , MonadIOish m
  )
  => Tool
  -> TargetVersion
  -> Excepts
       '[FileDoesNotExistError, ParseError, NoInstallInfo]
       m
       [SymlinkSpec [Either Char Version]]
getSymlinkSpec' tool tver = do
  dli <- liftE $ getInstallMetadata tool tver
  logDebug2 $ T.pack $ show dli
  spec <- preview (imResolvedInstallSpec % isExeSymLinked) dli ?? NoInstallInfo tool tver

  forM spec (liftE . parseSymlinkSpec (_tvVersion tver))

getSymlinkSpecPortable ::
  ( MonadReader env m
  , HasDirs env
  , HasPlatformReq env
  , HasLog env
  , MonadIOish m
  )
  => Tool
  -> TargetVersion
  -> m [SymlinkFileSpec]
getSymlinkSpecPortable tool tver = do
  runE (getSymlinkSpec tool tver) >>= \case
    VRight r -> pure r
    VLeft (V pe@(ParseError _)) -> fail $ prettyHFError pe
    VLeft _ -> do -- legacy
      if | tool == ghc   -> do
             pfreq <- getPlatformReq
             pure $ defaultGHCExeSymLinked pfreq tver (ghcBinaries pfreq tver)
         | tool == cabal -> pure []
         | tool == stack -> pure []
         | tool == hls   -> pure []
         | tool == ghcup -> pure [] -- Hm
         | otherwise     -> pure []

-- TODO: not neat the random combination of different SymlinkSpec types
getSymlinkSpecPortable' ::
  ( MonadReader env m
  , HasDirs env
  , HasPlatformReq env
  , HasLog env
  , MonadIOish m
  )
  => Tool
  -> TargetVersion
  -> Excepts '[ParseError] m [SymlinkSpec [Either Char Version]]
getSymlinkSpecPortable' tool tver = do
  runE (getSymlinkSpec' tool tver) >>= \case
    VRight r -> pure r
    VLeft (V pe@(ParseError _)) -> fail $ prettyHFError pe
    VLeft _ -> do -- legacy
      if | tool == ghc   -> do
             pfreq <- getPlatformReq
             liftE $ forM (defaultGHCExeSymLinked pfreq tver (ghcBinaries pfreq tver)) $ parseSymlinkSpec (_tvVersion tver)
         | tool == cabal -> pure []
         | tool == stack -> pure []
         | tool == hls   -> pure []
         | tool == ghcup -> pure [] -- Hm
         | otherwise     -> pure []



    -----------------
    --[ Installed ]--
    -----------------


-- | Returns 'Nothing' for legacy installs.
getInstalledFiles :: ( MonadIO m
                     , MonadCatch m
                     , MonadReader env m
                     , HasDirs env
                     , MonadFail m
                     )
                  => Tool
                  -> TargetVersion
                  -> m (Maybe [FilePath])
getInstalledFiles t v' = hideErrorDef [doesNotExistErrorType] Nothing $ do
  f <- recordedInstallationFile t v'
  (force -> !c) <- liftIO
    (readFile f >>= evaluate)
  pure (Just $ lines c)


isInstalled ::
  ( MonadReader env m
  , HasDirs env
  , HasLog env
  , MonadIOish m
  )
  => Tool
  -> TargetVersion
  -> m (Maybe Int)
isInstalled tool ver
  | tool == ghc =
      isInstalledNew $ ghcInstalled ver
  | tool == hls =
      isInstalledNew $ hlsInstalled (_tvVersion ver)
  | tool == cabal =
      isInstalledNew $ cabalInstalled (_tvVersion ver)
  | tool == stack =
      isInstalledNew $ stackInstalled (_tvVersion ver)
  | tool == ghcup =
      if ghcUpVer' == ver
      then pure $ Just 0
      else pure Nothing
  | otherwise = isInstalledNew (pure False)
 where
   -- for new installations we can just check the DB
  isInstalledNew fallback = do
    runE (getInstallMetadata tool ver) >>= \case
      VLeft _ -> do
        b <- fallback
        if b
        then pure (Just 0)
        else pure Nothing
      VRight InstallMetadata{..} -> pure (Just _imRevision)

getInstalledVersions ::
  ( MonadReader env m
  , HasDirs env
  , HasLog env
  , MonadIOish m
  )
  => Tool
  -> Maybe T.Text
  -> m [VersionRev]
getInstalledVersions tool mtarget = do
  r <- getInstalledVersions' tool
  pure $ extract' r
 where
  extract' = fmap (\TargetVersionRev{..} -> VersionRev (_tvVersion _tvrTargetVer) _tvrRev)
           . filter (\TargetVersionRev{..} -> _tvTarget _tvrTargetVer == mtarget)

getInstalledVersions' ::
  ( MonadReader env m
  , HasDirs env
  , HasLog env
  , MonadIOish m
  )
  => Tool
  -> m [TargetVersionRev]
getInstalledVersions' tool
  | tool == ghc = do
      new <- getInstalledNew
      legacy <- fmap rights getInstalledGHCs
      pure $ nubBy (\a b -> _tvrTargetVer a == _tvrTargetVer b) (new ++ (unsafeToTargetVersionRev <$> legacy))
  | tool == cabal = do
      new <- getInstalledNew
      legacy <- fmap (fmap mkTVer . rights) getInstalledCabals
      pure $ nubBy (\a b -> _tvrTargetVer a == _tvrTargetVer b) (new ++ (unsafeToTargetVersionRev <$> legacy))
  | tool == stack = do
      new <- getInstalledNew
      legacy <- fmap (fmap mkTVer . rights)  getInstalledStacks
      pure $ nubBy (\a b -> _tvrTargetVer a == _tvrTargetVer b) (new ++ (unsafeToTargetVersionRev <$> legacy))
  | tool == hls = do
      new <- getInstalledNew
      legacy <- fmap (fmap mkTVer . rights) getInstalledHLSs
      pure $ nubBy (\a b -> _tvrTargetVer a == _tvrTargetVer b) (new ++ (unsafeToTargetVersionRev <$> legacy))
  | tool == ghcup = do
      pure [unsafeToTargetVersionRev ghcUpVer']
  | otherwise = getInstalledNew
 where
  parseGHCVer = throwEither . MP.parse ghcTargetVerP "getInstalledVersions'" . T.pack
  getInstalledNew = do
    Dirs {..}  <- getDirs
    let dbPath = fromGHCupPath dbDir </> prettyShow tool
    -- we have to ignore '.spec' and the 'set' file
    contents <- fmap (filter (\f -> takeExtension f `notElem` [".spec", ".set"] && f /= "set"))
      $ liftIO $ handleIO' doesNotExistErrorType (\_ -> pure []) $ listDirectoryFiles dbPath
    forM contents $ \c -> do
      tv <- parseGHCVer c
      rev <- getInstalledRevision tool tv
      pure $ TargetVersionRev tv rev


-- | Might return more tools than requested.
getAllInstalledTools ::
  ( MonadReader env m
  , HasGHCupInfo env
  , HasDirs env
  , HasPlatformReq env
  , HasLog env
  , MonadIOish m
  )
  => Maybe [Tool]
  -> Excepts '[ParseError] m (Map.Map Tool (Map.Map (Maybe Text) ([VersionRev], Maybe VersionRev)))
getAllInstalledTools mtools = do
  Dirs{..} <- lift getDirs
  tools <- case mtools of
    Nothing -> do
      newTools <- fmap Tool <$> liftIO (listDirectoryDirs $ fromGHCupPath dbDir)
      pure (nub $ ghcup:ghc:cabal:hls:stack:newTools)
    Just tools'
      -- for GHC we also need to fetch HLS info, so we can display 'hls-powered'
      | ghc `elem` tools' -> pure (nub $ hls:tools')
      | otherwise -> pure (nub tools')
  fmap Map.fromList $ forM tools $ \newTool -> do
    vs <- lift $ getInstalledVersions' newTool
    -- add information on which is the 'set' version, if any
    nm <- Map.traverseWithKey (trav newTool) (groupByTarget' vs)

    pure (newTool, nm)
 where
  trav tool mtarget vers' = do
    mv <- getSetVersion tool mtarget
    pure (vers', mv)



    -----------
    --[ Set ]--
    -----------


getSetVersion' ::
  ( MonadReader env m
  , HasDirs env
  , HasLog env
  , MonadIOish m
  )
  => Tool
  -> Maybe T.Text
  -> Excepts '[ParseError] m (Maybe (VersionRev, Maybe FilePath))
getSetVersion' tool target = do
  setFile <- lift $ recordedSetVersionFile tool target
  ec <- liftIO $ try @_ @SomeException (T.readFile setFile)
  case ec of
    Left _
      | tool == ghc ->
          lift $ fmap ((,Nothing) . (`VersionRev` 0) . _tvVersion) <$> ghcSet target
      | tool == cabal ->
          lift $ fmap ((,Nothing) . (`VersionRev` 0)) <$> cabalSet
      | tool == hls ->
          lift $ fmap ((,Nothing) . (`VersionRev` 0)) <$> hlsSet
      | tool == stack ->
          lift $ fmap ((,Nothing) . (`VersionRev` 0)) <$> stackSet
      | tool == ghcup ->
          pure (Just ((`VersionRev` 0) $ _tvVersion ghcUpVer', Nothing))
      | otherwise -> pure Nothing
    Right c -> do
      ver <- either (throwE . ParseError . displayException) pure . version $ c
      rev <- getInstalledRevision tool (TargetVersion target ver)
      pure $ Just (VersionRev ver rev, Just setFile)

getSetVersion ::
  ( MonadReader env m
  , HasDirs env
  , HasLog env
  , MonadIOish m
  )
  => Tool
  -> Maybe T.Text
  -> Excepts '[ParseError] m (Maybe VersionRev)
getSetVersion tool = (fmap . fmap) fst . getSetVersion' tool

isSet ::
  ( MonadReader env m
  , HasDirs env
  , HasLog env
  , MonadIOish m
  )
  => Tool
  -> TargetVersion
  -> Excepts '[ParseError] m (Maybe Int)
isSet tool tver = do
  mv <- (fmap . fmap) fst . getSetVersion' tool $ _tvTarget tver
  case mv of
    Just v ->
      if _tvVersion tver == _vrVersion v
      then pure $ Just (_vrRev v)
      else pure Nothing
    _ -> pure Nothing




    -------------
    --[ Other ]--
    -------------


groupByTarget :: [(TargetVersionRev, VersionMetadata)] -> Map.Map (Maybe Text) [(VersionRev, VersionMetadata)]
groupByTarget = foldr (\(TargetVersionRev{..}, vm) -> Map.alter (f (VersionRev (_tvVersion _tvrTargetVer) _tvrRev) vm) (_tvTarget _tvrTargetVer)) mempty
 where
  f tvVersion' vm Nothing   = Just [(tvVersion', vm)]
  f tvVersion' vm (Just xs) = Just ((tvVersion', vm):xs)

groupByTarget' :: [TargetVersionRev] -> Map.Map (Maybe Text) [VersionRev]
groupByTarget' = foldr (\(TargetVersionRev{..}) -> Map.alter (f (VersionRev (_tvVersion _tvrTargetVer) _tvrRev)) (_tvTarget _tvrTargetVer)) mempty
 where
  f tvVersion' Nothing   = Just [tvVersion']
  f tvVersion' (Just xs) = Just (tvVersion':xs)

groupByTargetS :: [(TargetVersion, VersionMetadata)] -> Map.Map (Maybe Text) (Set (Version, VersionMetadata))
groupByTargetS = foldr (\(TargetVersion{..}, vm) -> Map.alter (f _tvVersion vm) _tvTarget) mempty
 where
  f tvVersion' vm Nothing   = Just $ Set.singleton (tvVersion', vm)
  f tvVersion' vm (Just xs) = Just $ Set.insert (tvVersion', vm) xs