ghcup-0.2.6.2: lib/GHCup/Command/Rm.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
module GHCup.Command.Rm where
import GHCup.Errors
import GHCup.Input.SymlinkSpec
import GHCup.Legacy.Cabal
import GHCup.Legacy.HLS
import GHCup.Legacy.Stack
import GHCup.Legacy.Utils
import GHCup.Prelude
import GHCup.Query.DB
import GHCup.Query.GHCupDirs
import GHCup.Query.Symlink
import GHCup.System.Directory
import GHCup.Types
import GHCup.Types.Optics
import Control.Applicative
import Control.Exception.Safe
import Control.Monad
#if !MIN_VERSION_base(4,13,0)
import Control.Monad.Fail ( MonadFail )
#endif
import Control.Monad.Reader
import Control.Monad.Trans.Resource hiding ( throwM )
import Data.List
import Data.Maybe
import Data.Variant.Excepts
import GHC.IO.Exception
import Prelude hiding ( abs, writeFile )
import System.Environment
import System.FilePath
import System.IO.Error
import System.IO.Temp
import qualified Data.Text as T
import GHCup.Command.Install.LowLevel
-- | Remove a tool completely.
--
-- This has two phases:
--
-- 1. the files installed from the bindist
-- 2. the files symlinked into the GHCup bin directory
--
-- Both are stored on the DB, but in different files/formats.
rmToolVersion ::
( MonadReader env m
, HasDirs env
, HasLog env
, HasPlatformReq env
, MonadIOish m
)
=> Tool
-> TargetVersion
-> Excepts '[NotInstalled, UninstallFailed, ParseError, MalformedInstallInfo] m ()
rmToolVersion tool tver = do
logInfo $ "Removing " <> prettyText tool <> " version " <> prettyText tver
let target = _tvTarget tver
ver' = _tvVersion tver
unlessM (lift $ fmap isJust $ isInstalled tool tver) $ throwE (NotInstalled tool tver)
mset <- liftE $ getSetVersion' tool target
vspec <- lift $ runE $ getSymlinkSpec' tool tver
case vspec of
VLeft _ -> do
-- legacy, no spec files
pure ()
VRight specRaw -> do
let spec = substituteSpec <$> specRaw
lift $ logInfo "Removing executable symlinks"
toolDir <- fromGHCupPath <$> lift (toolInstallDestination tool tver)
case mset of
Just (vr@VersionRev{..}, mSetFile) -> do
lift $ logDebug $ "Set version: " <> prettyVerRev vr
when (_vrVersion == ver') $ lift $ do
unqualSyml <- getUnqualifiedSymlinks spec toolDir
forM_ unqualSyml (rmLink . snd)
forM_ mSetFile recycleFile
rmGHCShareDir
Nothing -> pure ()
pvpSyml <- lift $ getPVPSymlinks spec toolDir
forM_ pvpSyml (lift . rmLink . snd)
pvpMajorSyml <- liftE $ getPVPMajorSymlinks specRaw ver' toolDir
forM_ pvpMajorSyml (rmLink . snd)
f <- recordedInstallationSpecFile tool tver
hideError NoSuchThing $ recycleFile f
dir' <- lift $ toolInstallDestination tool tver
let dir = fromGHCupPath dir'
lift (getInstalledFiles tool tver) >>= \case
Just files -> do
-- @.spec@ file was added after the DB record files, so it's possible
-- we may need to delete symlinks manually.
-- We need to do this before the actual removal. The legacy methods
-- rely on inspecting the internal GHC directories.
case vspec of
-- nothing to do, was already removed earlier
VRight _ -> pure ()
-- handle legacy symlink removal
VLeft _
| tool == ghc -> do
liftE $ rmMinorGHCSymlinks tver
liftE $ rmMajorGHCSymlinks tver
set <- lift $ ghcSet target
when (Just tver == set) $ do
liftE $ rmPlainGHC target
lift rmGHCShareDir
| tool == hls -> do
liftE $ rmMinorHLSSymlinks ver'
set <- lift hlsSet
when (Just ver' == set) $ liftE rmPlainHLS
-- cabal and stack don't need special handling
| otherwise -> pure ()
lift $ logInfo $ "Removing files safely from: " <> T.pack dir
forM_ files (lift . hideError NoSuchThing . recycleFile . (\f -> dir </> dropDrive f))
hideError NoSuchThing $ hideError UnsatisfiedConstraints $ removeEmptyDirsRecursive dir
survivors <- liftIO $ hideErrorDef [doesNotExistErrorType] [] $ listDirectory dir
f <- recordedInstallationFile tool tver
lift $ logDebug $ T.pack (show f)
lift $ recycleFile f
when (not (null survivors)) $ throwE $ UninstallFailed dir survivors
Nothing
| tool == ghc -> legacyGHCRm
| tool == cabal -> liftE $ rmCabalVer ver'
| tool == hls -> liftE $ rmHLSVer ver'
| tool == stack -> liftE $ rmStackVer ver'
| tool == ghcup -> lift rmGhcup
| otherwise -> fail "Could not find installed files... your DB seems corrupted"
-- now restore the X.Y symlink
majorMinor <- getMajorMinorV (_tvVersion tver)
liftE $ fixMajorMinorSymlink tool (_tvTarget tver) majorMinor
where
rmGHCShareDir = do
Dirs {..} <- getDirs
hideError doesNotExistErrorType $ rmDirectoryLink (fromGHCupPath baseDir </> "share")
legacyGHCRm = do
liftE $ rmMinorGHCSymlinks tver
liftE $ rmMajorGHCSymlinks tver
set <- lift $ ghcSet (_tvTarget tver)
when (Just tver == set) $ do
liftE $ rmPlainGHC (_tvTarget tver)
lift rmGHCShareDir
ghcdir <- ghcupGHCDir tver
let dir = fromGHCupPath ghcdir
isDir <- liftIO $ doesDirectoryExist dir
isSyml <- liftIO $ handleIO (\_ -> pure False) $ pathIsSymbolicLink dir
when (isDir && not isSyml) $ do
lift $ logInfo $ "Removing legacy directory recursively: " <> T.pack dir
recyclePathForcibly ghcdir
-- assuming the current scheme of having just 1 ghcup bin, no version info is required.
rmGhcup :: ( MonadReader env m
, HasDirs env
, MonadIO m
, MonadCatch m
, HasLog env
, MonadMask m
, MonadUnliftIO m
)
=> m ()
rmGhcup = do
Dirs { .. } <- getDirs
let ghcupFilename = "ghcup" <> exeExt
let ghcupFilepath = binDir </> ghcupFilename
currentRunningExecPath <- liftIO getExecutablePath
-- if paths do no exist, warn user, and continue to compare them, as is,
-- which should eventually fail and result in a non-standard install warning
p1 <- handleIO' doesNotExistErrorType
(handlePathNotPresent currentRunningExecPath)
(liftIO $ canonicalizePath currentRunningExecPath)
p2 <- handleIO' doesNotExistErrorType
(handlePathNotPresent ghcupFilepath)
(liftIO $ canonicalizePath ghcupFilepath)
let areEqualPaths = equalFilePath p1 p2
unless areEqualPaths $ logWarn $ nonStandardInstallLocationMsg currentRunningExecPath
if isWindows
then do
-- since it doesn't seem possible to delete a running exe on windows
-- we move it to system temp dir, to be deleted at next reboot
tmp <- liftIO $ getCanonicalTemporaryDirectory >>= \t -> createTempDirectory t "ghcup"
hideError UnsupportedOperation $
liftIO $ hideError NoSuchThing $
moveFile ghcupFilepath (tmp </> "ghcup")
else
-- delete it.
hideError doesNotExistErrorType $ rmFile ghcupFilepath
where
handlePathNotPresent fp _err = do
logDebug $ "Error: The path does not exist, " <> T.pack fp
pure fp
nonStandardInstallLocationMsg path = T.pack $
"current ghcup is invoked from a non-standard location: \n"
<> path <>
"\n you may have to uninstall it manually."