packages feed

ghcup-0.2.1.0: lib/GHCup/Command/Install.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}

module GHCup.Command.Install where

import GHCup.Builder
import GHCup.Command.Install.LowLevel
import GHCup.Command.Rm
import GHCup.Download
import GHCup.Errors
import GHCup.Input.SymlinkSpec
import GHCup.Prelude
import GHCup.Query.DB
import GHCup.Query.GHCupDirs
import GHCup.Query.Metadata
import GHCup.System.Cmd
import GHCup.Types
import GHCup.Types.Optics
import GHCup.Unpack

import Control.Applicative
import Control.Monad
#if !MIN_VERSION_base(4,13,0)
import Control.Monad.Fail ( MonadFail )
#endif
import Conduit                        ( runConduitRes, (.|) )
import Control.Monad.Reader
import Control.Monad.Trans.Resource   hiding ( throwM )
import Data.Maybe
import Data.Variant.Excepts
import Optics
import Prelude                        hiding ( abs )
import System.FilePath                ( dropDrive )
import System.IO.Error                ( doesNotExistErrorType )
import Text.PrettyPrint.HughesPJClass ( prettyShow )

import qualified Data.Conduit.Combinators as C
import qualified Data.Text                as T



    -------------------------
    --[ Tool installation ]--
    -------------------------

-- | Installs cabal into @~\/.ghcup\/bin/cabal-\<ver\>@ and
-- creates a default @cabal -> cabal-x.y.z.q@ symlink for
-- the latest installed version.
installTool ::
  ( MonadReader env m
  , HasPlatformReq env
  , HasGHCupInfo env  -- this is the main difference to the other functions
  , HasDirs env
  , HasSettings env
  , HasLog env
  , MonadResource m
  , MonadIOish m
  )
  => Tool
  -> TargetVersionReq
  -> InstallDir
  -> Bool
  -> [String]
  -> Maybe [String]
  -> Excepts
       '[ AlreadyInstalled
        , CopyError
        , DigestError
        , ContentLengthError
        , GPGError
        , DownloadFailed
        , NoDownload
        , NotInstalled
        , UnknownArchive
        , TarDirDoesNotExist
        , ArchiveResult
        , FileAlreadyExistsError
        , URIParseError
        , NoInstallInfo
        , MergeFileTreeError
        , ProcessError
        , ParseError
        , DirNotEmpty
        , UninstallFailed
        , MalformedInstallInfo
        ]
       m
       (InstallationSpecResolved, FilePath)
installTool tool treq installDir forceInstall extraArgs installTargets = do
  GHCupInfo { _ghcupDownloads = dls } <- getGHCupInfo
  (rev, dlinfo) <- liftE $ getDownloadInfoE' tool treq
  installBindist
    tool
    (preview (_GHCupDownloads % ix tool % toolDetails % _Just) dls)
    dlinfo
    (TargetVersionRev (treq ^. tvqTargetVer) rev)
    installDir
    forceInstall
    extraArgs
    installTargets

-- | Like 'installTool', except takes the 'DownloadInfo' as
-- argument instead of looking it up from 'GHCupDownloads'.
installBindist ::
  ( MonadReader env m
  , HasPlatformReq env
  , HasDirs env
  , HasSettings env
  , HasLog env
  , MonadResource m
  , MonadIOish m
  )
  => Tool
  -> Maybe ToolDescription
  -> DownloadInfo
  -> TargetVersionRev
  -> InstallDir
  -> Bool           -- ^ Force install
  -> [String]
  -> Maybe [String]
  -> Excepts
       '[ AlreadyInstalled
        , CopyError
        , DigestError
        , ContentLengthError
        , GPGError
        , DownloadFailed
        , NoDownload
        , NotInstalled
        , UnknownArchive
        , TarDirDoesNotExist
        , ArchiveResult
        , FileAlreadyExistsError
        , URIParseError
        , NoInstallInfo
        , MergeFileTreeError
        , ProcessError
        , ParseError
        , DirNotEmpty
        , UninstallFailed
        , MalformedInstallInfo
        ]
       m
       (InstallationSpecResolved, FilePath)
installBindist tool toolDesc dlinfo trev@(TargetVersionRev tver rev) installDir forceInstall extraArgs installTargets = do
  lift $ logDebug $ "Requested to install "
                     <> T.pack (prettyShow tool)
                     <> " version " <> T.pack (prettyShow tver)

  installed <- lift $ isInstalled tool tver
  if
    | not forceInstall
    , Just irev <- installed
    , GHCupInternal <- installDir -> do
        if rev == irev
        then throwE $ AlreadyInstalled tool tver
        else do
          lift $ logInfo "Removing the currently installed revision first!"
          liftE $ rmToolVersion tool tver

    | forceInstall
    , Just _ <- installed
    , GHCupInternal <- installDir -> do
        lift $ logInfo "Removing the currently installed version first!"
        liftE $ rmToolVersion tool tver

    | otherwise -> pure ()


  -- download (or use cached version)
  dl <- liftE $ downloadCached dlinfo Nothing

  case installDir of
    IsolateDir isoDir -> do             -- isolated install
      lift $ logInfo $ "isolated installing Cabal to " <> T.pack isoDir
      installSpec <- liftE $ installPackedBindist tool toolDesc dl dlinfo (IsolateDirResolved isoDir) trev forceInstall extraArgs installTargets
      pure (installSpec, isoDir)

    GHCupInternal -> do                 -- regular install
      instDir <- lift $ toolInstallDestination tool tver
      installSpec <- liftE $ installPackedBindist tool toolDesc dl dlinfo (GHCupDir instDir) trev forceInstall extraArgs installTargets
      pure (installSpec, fromGHCupPath instDir)

installPackedBindist ::
  ( MonadReader env m
  , HasDirs env
  , HasPlatformReq env
  , HasSettings env
  , HasLog env
  , MonadResource m
  , MonadIOish m
  )
  => Tool
  -> Maybe ToolDescription
  -> FilePath             -- ^ Path to the tarball
  -> DownloadInfo
  -> InstallDirResolved   -- ^ Path to install to
  -> TargetVersionRev
  -> Bool
  -> [String]
  -> Maybe [String]
  -> Excepts '[ CopyError
              , FileAlreadyExistsError
              , MergeFileTreeError
              , ProcessError
              , ParseError
              , UnknownArchive
              , TarDirDoesNotExist
              , DirNotEmpty
              , ArchiveResult
              , NoInstallInfo
              , MalformedInstallInfo
              ] m InstallationSpecResolved
installPackedBindist tool toolDesc dl dlInfo inst trev forceInstall extraArgs installTargets = do
  PlatformRequest {..} <- lift getPlatformReq

  unless forceInstall
    (liftE $ installDestSanityCheck inst)

  tmpUnpack <- lift withGHCupTmpDir
  liftE $ cleanUpOnError tmpUnpack (unpackToDir (fromGHCupPath tmpUnpack) dl)
  liftE $ catchWarn $ lEM @_ @'[ProcessError] $ darwinNotarization _rPlatform
    (fromGHCupPath tmpUnpack)

  -- the subdir of the archive where we do the work
  workdir <- fromGHCupPath <$> maybe (pure tmpUnpack)
    (liftE . intoSubdir tmpUnpack) (view dlSubdir dlInfo)

  tmpInstallDest <- lift withGHCupTmpDir

  liftE $ runBuildAction tmpUnpack $ installUnpackedBindist tool toolDesc workdir
        inst tmpInstallDest dlInfo trev forceInstall extraArgs installTargets

-- | Install an unpacked distribution.
installUnpackedBindist :: forall m env .
  ( HasLog env
  , HasDirs env
  , HasSettings env
  , HasPlatformReq env
  , MonadReader env m
  , MonadIOish m
  )
  => Tool
  -> Maybe ToolDescription
  -> FilePath             -- ^ Path to the unpacked cabal bindist (where the executable resides)
  -> InstallDirResolved   -- ^ Path to install to
  -> GHCupPath            -- ^ DESTDIR
  -> DownloadInfo
  -> TargetVersionRev
  -> Bool
  -> [String]
  -> Maybe [String]
  -> Excepts '[ CopyError
              , FileAlreadyExistsError
              , MergeFileTreeError
              , ProcessError
              , ParseError
              , NoInstallInfo
              , MalformedInstallInfo
              ] m InstallationSpecResolved
installUnpackedBindist tool toolDesc workdir installDest tmpInstallDest dlInfo trev@(TargetVersionRev tver _rev) forceInstall extraArgs installTargets = do
  instSpec <- liftE $ installationSpecFromMetadata' dlInfo tool tver
  lift $ logInfo $ "Installing " <> T.pack (prettyShow tool)
  liftE $ installTheSpec instSpec workdir installDest tmpInstallDest extraArgs installTargets forceInstall
  let preserveMtimes = _isPreserveMtimes instSpec
  resolvedSymlinkSpecs <- liftE $ forM (view isExeSymLinked instSpec) (resolveSymlinkSpec (tmpInstallDest `appendGHCupPath` dropDrive (fromInstallDir installDest)))
  let resolvedInstSpec = resolveInstallationSpec (mconcat resolvedSymlinkSpecs) instSpec

  -- then merge to the live filesystem
  liftE $ mergeToFileSystem tool tver installDest tmpInstallDest preserveMtimes forceInstall False

  -- TODO: move to optparse install
  -- symlinking
  Dirs {..} <- lift getDirs
  parsedSymlinkSpec <- forM (_isExeSymLinked resolvedInstSpec) (liftE . parseSymlinkSpec (_tvVersion tver))
  liftE $ symlinkBinaries installDest parsedSymlinkSpec (GHCupBinDir binDir) tool tver

  -- write InstallationInfo to the disk
  lift $ recordInstallationInfo installDest tool toolDesc trev dlInfo resolvedInstSpec
  pure resolvedInstSpec


-- | Does basic checks for isolated installs
-- Isolated Directory:
--   1. if it doesn't exist -> proceed
--   2. if it exists and is empty -> proceed
--   3. if it exists and is non-empty -> panic and leave the house
installDestSanityCheck :: ( MonadIOish m
                          ) =>
                          InstallDirResolved ->
                          Excepts '[DirNotEmpty] m ()
installDestSanityCheck (IsolateDirResolved isoDir) = do
  hideErrorDef [doesNotExistErrorType] () $ do
    empty' <- lift $ runConduitRes $ getDirectoryContentsRecursiveUnsafe isoDir .| C.null
    when (not empty') (throwE $ DirNotEmpty isoDir)
installDestSanityCheck _ = pure ()