ghcup-hs/lib/GHCup/Utils/Dirs.hs

370 lines
11 KiB
Haskell
Raw Normal View History

2021-05-14 21:09:45 +00:00
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
2020-03-21 21:19:37 +00:00
{-# LANGUAGE OverloadedStrings #-}
2020-07-31 18:10:40 +00:00
{-# LANGUAGE FlexibleContexts #-}
2020-04-25 10:06:41 +00:00
{-# LANGUAGE ViewPatterns #-}
2020-01-11 20:15:05 +00:00
2020-07-21 23:08:58 +00:00
{-|
Module : GHCup.Utils.Dirs
Description : Definition of GHCup directories
Copyright : (c) Julian Ospald, 2020
2020-07-30 18:04:02 +00:00
License : LGPL-3.0
2020-07-21 23:08:58 +00:00
Maintainer : hasufell@hasufell.de
Stability : experimental
2021-05-14 21:09:45 +00:00
Portability : portable
2020-07-21 23:08:58 +00:00
-}
2020-07-31 18:10:40 +00:00
module GHCup.Utils.Dirs
( getAllDirs
2021-05-14 21:09:45 +00:00
, ghcupBaseDir
, ghcupConfigFile
, ghcupCacheDir
2020-07-31 18:10:40 +00:00
, ghcupGHCBaseDir
, ghcupGHCDir
2022-02-05 00:53:04 +00:00
, ghcupHLSBaseDir
, ghcupHLSDir
2020-07-31 18:10:40 +00:00
, mkGhcupTmpDir
, parseGHCupGHCDir
2022-02-05 00:53:04 +00:00
, parseGHCupHLSDir
2020-07-31 18:10:40 +00:00
, relativeSymlink
, withGHCupTmpDir
, getConfigFilePath
, useXDG
2021-07-22 13:45:08 +00:00
, cleanupTrash
2020-07-31 18:10:40 +00:00
)
where
2020-01-11 20:15:05 +00:00
import GHCup.Errors
2020-04-25 10:06:41 +00:00
import GHCup.Types
2020-01-11 20:15:05 +00:00
import GHCup.Types.JSON ( )
import GHCup.Types.Optics
2020-04-25 10:06:41 +00:00
import GHCup.Utils.MegaParsec
2021-09-23 10:53:01 +00:00
import GHCup.Utils.Logger
2020-01-11 20:15:05 +00:00
import GHCup.Utils.Prelude
import Control.Exception.Safe
import Control.Monad
import Control.Monad.IO.Unlift
2020-01-11 20:15:05 +00:00
import Control.Monad.Reader
import Control.Monad.Trans.Resource hiding (throwM)
import Data.Bifunctor
2020-01-11 20:15:05 +00:00
import Data.Maybe
2022-02-05 00:53:04 +00:00
import Data.Versions
import GHC.IO.Exception ( IOErrorType(NoSuchThing) )
import Haskus.Utils.Variant.Excepts
2020-01-11 20:15:05 +00:00
import Optics
2021-05-14 21:09:45 +00:00
import System.Directory
import System.DiskSpace
2021-05-14 21:09:45 +00:00
import System.Environment
import System.FilePath
import System.IO.Temp
import qualified Data.ByteString as BS
import qualified Data.Text as T
2021-10-21 21:17:26 +00:00
import qualified Data.Yaml.Aeson as Y
2020-04-25 10:06:41 +00:00
import qualified Text.Megaparsec as MP
import Control.Concurrent (threadDelay)
2020-01-11 20:15:05 +00:00
2020-07-31 18:10:40 +00:00
------------------------------
--[ GHCup base directories ]--
------------------------------
2020-01-11 20:15:05 +00:00
2020-04-25 10:06:41 +00:00
-- | ~/.ghcup by default
2020-07-28 23:43:00 +00:00
--
-- If 'GHCUP_USE_XDG_DIRS' is set (to anything),
-- then uses 'XDG_DATA_HOME/ghcup' as per xdg spec.
2021-05-14 21:09:45 +00:00
ghcupBaseDir :: IO FilePath
2021-10-17 18:39:49 +00:00
ghcupBaseDir
| isWindows = do
bdir <- fromMaybe "C:\\" <$> lookupEnv "GHCUP_INSTALL_BASE_PREFIX"
2021-05-14 21:09:45 +00:00
pure (bdir </> "ghcup")
2021-10-17 18:39:49 +00:00
| otherwise = do
xdg <- useXDG
if xdg
then do
bdir <- lookupEnv "XDG_DATA_HOME" >>= \case
Just r -> pure r
Nothing -> do
home <- liftIO getHomeDirectory
pure (home </> ".local" </> "share")
pure (bdir </> "ghcup")
else do
bdir <- lookupEnv "GHCUP_INSTALL_BASE_PREFIX" >>= \case
Just r -> pure r
Nothing -> liftIO getHomeDirectory
pure (bdir </> ".ghcup")
2020-01-11 20:15:05 +00:00
2020-04-25 10:06:41 +00:00
-- | ~/.ghcup by default
--
-- If 'GHCUP_USE_XDG_DIRS' is set (to anything),
-- then uses 'XDG_CONFIG_HOME/ghcup' as per xdg spec.
2021-05-14 21:09:45 +00:00
ghcupConfigDir :: IO FilePath
2021-10-17 18:39:49 +00:00
ghcupConfigDir
| isWindows = ghcupBaseDir
| otherwise = do
xdg <- useXDG
if xdg
then do
bdir <- lookupEnv "XDG_CONFIG_HOME" >>= \case
Just r -> pure r
Nothing -> do
home <- liftIO getHomeDirectory
pure (home </> ".config")
pure (bdir </> "ghcup")
else do
bdir <- lookupEnv "GHCUP_INSTALL_BASE_PREFIX" >>= \case
Just r -> pure r
Nothing -> liftIO getHomeDirectory
pure (bdir </> ".ghcup")
2020-07-28 23:43:00 +00:00
-- | If 'GHCUP_USE_XDG_DIRS' is set (to anything),
-- then uses 'XDG_BIN_HOME' env var or defaults to '~/.local/bin'
-- (which, sadly is not strictly xdg spec).
2021-05-14 21:09:45 +00:00
ghcupBinDir :: IO FilePath
2021-10-17 18:39:49 +00:00
ghcupBinDir
| isWindows = ghcupBaseDir <&> (</> "bin")
| otherwise = do
xdg <- useXDG
if xdg
then do
lookupEnv "XDG_BIN_HOME" >>= \case
Just r -> pure r
Nothing -> do
home <- liftIO getHomeDirectory
pure (home </> ".local" </> "bin")
else ghcupBaseDir <&> (</> "bin")
2020-07-28 23:43:00 +00:00
2020-07-31 18:10:40 +00:00
2020-07-28 23:43:00 +00:00
-- | Defaults to '~/.ghcup/cache'.
--
-- If 'GHCUP_USE_XDG_DIRS' is set (to anything),
-- then uses 'XDG_CACHE_HOME/ghcup' as per xdg spec.
2021-05-14 21:09:45 +00:00
ghcupCacheDir :: IO FilePath
2021-10-17 18:39:49 +00:00
ghcupCacheDir
| isWindows = ghcupBaseDir <&> (</> "cache")
| otherwise = do
xdg <- useXDG
if xdg
then do
bdir <- lookupEnv "XDG_CACHE_HOME" >>= \case
Just r -> pure r
Nothing -> do
home <- liftIO getHomeDirectory
pure (home </> ".cache")
pure (bdir </> "ghcup")
else ghcupBaseDir <&> (</> "cache")
2020-07-28 23:43:00 +00:00
2020-07-31 18:10:40 +00:00
2020-07-28 23:43:00 +00:00
-- | Defaults to '~/.ghcup/logs'.
--
-- If 'GHCUP_USE_XDG_DIRS' is set (to anything),
-- then uses 'XDG_CACHE_HOME/ghcup/logs' as per xdg spec.
2021-05-14 21:09:45 +00:00
ghcupLogsDir :: IO FilePath
2021-10-17 18:39:49 +00:00
ghcupLogsDir
| isWindows = ghcupBaseDir <&> (</> "logs")
| otherwise = do
xdg <- useXDG
if xdg
then do
bdir <- lookupEnv "XDG_CACHE_HOME" >>= \case
Just r -> pure r
Nothing -> do
home <- liftIO getHomeDirectory
pure (home </> ".cache")
pure (bdir </> "ghcup" </> "logs")
else ghcupBaseDir <&> (</> "logs")
2020-01-11 20:15:05 +00:00
2021-07-22 13:45:08 +00:00
-- | '~/.ghcup/trash'.
-- Mainly used on windows to improve file removal operations
ghcupRecycleDir :: IO FilePath
ghcupRecycleDir = ghcupBaseDir <&> (</> "trash")
getAllDirs :: IO Dirs
getAllDirs = do
2021-07-22 13:45:08 +00:00
baseDir <- ghcupBaseDir
binDir <- ghcupBinDir
cacheDir <- ghcupCacheDir
logsDir <- ghcupLogsDir
confDir <- ghcupConfigDir
recycleDir <- ghcupRecycleDir
2020-07-31 18:10:40 +00:00
pure Dirs { .. }
-------------------
--[ GHCup files ]--
-------------------
getConfigFilePath :: (MonadIO m) => m FilePath
getConfigFilePath = do
confDir <- liftIO ghcupConfigDir
pure $ confDir </> "config.yaml"
ghcupConfigFile :: (MonadIO m)
=> Excepts '[JSONError] m UserSettings
ghcupConfigFile = do
filepath <- getConfigFilePath
contents <- liftIO $ handleIO' NoSuchThing (\_ -> pure Nothing) $ Just <$> BS.readFile filepath
2021-05-14 21:09:45 +00:00
case contents of
Nothing -> pure defaultUserSettings
2021-10-21 21:17:26 +00:00
Just contents' -> lE' JSONDecodeError . first displayException . Y.decodeEither' $ contents'
2020-07-31 18:10:40 +00:00
-------------------------
--[ GHCup directories ]--
-------------------------
-- | ~/.ghcup/ghc by default.
ghcupGHCBaseDir :: (MonadReader env m, HasDirs env) => m FilePath
2020-07-31 18:10:40 +00:00
ghcupGHCBaseDir = do
Dirs {..} <- getDirs
2021-05-14 21:09:45 +00:00
pure (baseDir </> "ghc")
2020-07-31 18:10:40 +00:00
-- | Gets '~/.ghcup/ghc/<ghcupGHCDir>'.
-- The dir may be of the form
-- * armv7-unknown-linux-gnueabihf-8.8.3
-- * 8.8.4
ghcupGHCDir :: (MonadReader env m, HasDirs env, MonadThrow m)
2020-07-31 18:10:40 +00:00
=> GHCTargetVersion
2021-05-14 21:09:45 +00:00
-> m FilePath
2020-07-31 18:10:40 +00:00
ghcupGHCDir ver = do
2021-05-14 21:09:45 +00:00
ghcbasedir <- ghcupGHCBaseDir
let verdir = T.unpack $ tVerToText ver
2020-07-31 18:10:40 +00:00
pure (ghcbasedir </> verdir)
-- | See 'ghcupToolParser'.
2021-05-14 21:09:45 +00:00
parseGHCupGHCDir :: MonadThrow m => FilePath -> m GHCTargetVersion
parseGHCupGHCDir (T.pack -> fp) =
2020-07-31 18:10:40 +00:00
throwEither $ MP.parse ghcTargetVerP "" fp
2022-02-05 00:53:04 +00:00
parseGHCupHLSDir :: MonadThrow m => FilePath -> m Version
parseGHCupHLSDir (T.pack -> fp) =
throwEither $ MP.parse version' "" fp
-- | ~/.ghcup/hls by default, for new-style installs.
ghcupHLSBaseDir :: (MonadReader env m, HasDirs env) => m FilePath
ghcupHLSBaseDir = do
Dirs {..} <- getDirs
pure (baseDir </> "hls")
-- | Gets '~/.ghcup/hls/<hls-ver>' for new-style installs.
ghcupHLSDir :: (MonadReader env m, HasDirs env, MonadThrow m)
=> Version
-> m FilePath
ghcupHLSDir ver = do
basedir <- ghcupHLSBaseDir
let verdir = T.unpack $ prettyVer ver
pure (basedir </> verdir)
2020-07-31 18:10:40 +00:00
mkGhcupTmpDir :: ( MonadReader env m
, HasDirs env
, MonadUnliftIO m
2021-08-30 20:41:58 +00:00
, HasLog env
, MonadCatch m
, MonadThrow m
, MonadMask m
, MonadIO m)
=> m FilePath
2020-01-11 20:15:05 +00:00
mkGhcupTmpDir = do
2021-05-14 21:09:45 +00:00
tmpdir <- liftIO getCanonicalTemporaryDirectory
2021-04-25 19:32:58 +00:00
let minSpace = 5000 -- a rough guess, aight?
2021-05-14 21:09:45 +00:00
space <- handleIO (\_ -> pure Nothing) $ fmap Just $ liftIO $ getAvailSpace tmpdir
when (maybe False (toBytes minSpace >) space) $ do
2021-08-30 20:41:58 +00:00
logWarn ("Possibly insufficient disk space on "
2021-08-25 16:54:58 +00:00
<> T.pack tmpdir
<> ". At least "
<> T.pack (show minSpace)
<> " MB are recommended, but only "
<> toMB (fromJust space)
<> " are free. Consider freeing up disk space or setting TMPDIR env variable.")
2021-08-30 20:41:58 +00:00
logWarn
"...waiting for 10 seconds before continuing anyway, you can still abort..."
liftIO $ threadDelay 10000000 -- give the user a sec to intervene
2021-05-14 21:09:45 +00:00
liftIO $ createTempDirectory tmpdir "ghcup"
where
toBytes mb = mb * 1024 * 1024
2021-08-25 16:54:58 +00:00
toMB b = T.pack $ show (truncate' (fromIntegral b / (1024 * 1024) :: Double) 2)
truncate' :: Double -> Int -> Double
2021-04-25 16:00:32 +00:00
truncate' x n = fromIntegral (floor (x * t) :: Integer) / t
where t = 10^n
2020-01-11 20:15:05 +00:00
withGHCupTmpDir :: ( MonadReader env m
, HasDirs env
2021-08-30 20:41:58 +00:00
, HasLog env
, HasSettings env
, MonadUnliftIO m
, MonadCatch m
, MonadResource m
, MonadThrow m
, MonadMask m
, MonadIO m)
=> m FilePath
2021-07-22 13:45:08 +00:00
withGHCupTmpDir = snd <$> withRunInIO (\run ->
run
$ allocate
(run mkGhcupTmpDir)
(\fp ->
handleIO (\e -> run
2021-08-30 20:41:58 +00:00
$ logDebug ("Resource cleanup failed for " <> T.pack fp <> ", error was: " <> T.pack (displayException e)))
2021-07-22 13:45:08 +00:00
. rmPathForcibly
$ fp))
2020-01-11 20:15:05 +00:00
2020-07-31 18:10:40 +00:00
2020-01-11 20:15:05 +00:00
--------------
--[ Others ]--
--------------
2020-07-28 23:43:00 +00:00
useXDG :: IO Bool
2021-05-14 21:09:45 +00:00
useXDG = isJust <$> lookupEnv "GHCUP_USE_XDG_DIRS"
2020-07-28 23:43:00 +00:00
-- | Like 'relpath'. Assumes the inputs are resolved in case of symlinks.
2021-05-14 21:09:45 +00:00
relativeSymlink :: FilePath -- ^ the path in which to create the symlink
-> FilePath -- ^ the symlink destination
-> FilePath
relativeSymlink p1 p2 =
2020-07-31 18:10:40 +00:00
let d1 = splitDirectories p1
d2 = splitDirectories p2
common = takeWhile (\(x, y) -> x == y) $ zip d1 d2
cPrefix = drop (length common) d1
2020-07-28 23:43:00 +00:00
in joinPath (replicate (length cPrefix) "..")
2021-05-14 21:09:45 +00:00
<> joinPath ([pathSeparator] : drop (length common) d2)
2020-07-28 23:43:00 +00:00
2021-07-22 13:45:08 +00:00
cleanupTrash :: ( MonadIO m
, MonadMask m
, MonadReader env m
2021-08-30 20:41:58 +00:00
, HasLog env
2021-07-22 13:45:08 +00:00
, HasDirs env
2021-08-30 20:41:58 +00:00
, HasSettings env
2021-07-22 13:45:08 +00:00
)
=> m ()
cleanupTrash = do
Dirs { recycleDir } <- getDirs
contents <- liftIO $ listDirectory recycleDir
if null contents
then pure ()
else do
2021-08-30 20:41:58 +00:00
logWarn ("Removing leftover files in " <> T.pack recycleDir)
2021-07-22 13:45:08 +00:00
forM_ contents (\fp -> handleIO (\e ->
2021-08-30 20:41:58 +00:00
logDebug ("Resource cleanup failed for " <> T.pack fp <> ", error was: " <> T.pack (displayException e))
2021-07-22 13:45:08 +00:00
) $ liftIO $ removePathForcibly (recycleDir </> fp))