src/Importify/Path.hs
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeOperators #-}
-- | This module contains common utilities for working with importify cache.
module Importify.Path
( -- * Predefined directories
importifyDir
, importifyPath
, extensionsFile
, extensionsPath
, modulesFile
, modulesPath
, symbolsDir
, symbolsPath
, testDataPath
-- * Utility functions to work with files and directories
, decodeFileOrMempty
, doInsideDir
, findCabalFile
, lookupToRoot
) where
import Universum
import Data.Aeson (FromJSON, eitherDecodeStrict)
import Fmt ((+|), (+||), (|+), (||+))
import Path (Abs, Dir, File, Path, Rel, dirname, fromAbsDir, fromAbsFile, fromRelDir, fromRelFile,
parent, reldir, relfile, toFilePath, (</>))
import Path.IO (doesFileExist, ensureDir, getCurrentDir, listDir)
import System.FilePath (takeExtension)
import Turtle (cd, pwd)
import Extended.System.Wlog (printNotice, printWarning)
import qualified Data.ByteString as BS (readFile)
importifyPath :: Path Rel Dir
importifyPath = [reldir|.importify/|]
-- | Path to file that stores mapping from module names to their packages.
modulesPath :: Path Rel File
modulesPath = [relfile|modules|]
symbolsPath :: Path Rel Dir
symbolsPath = [reldir|symbols/|]
-- | Path to golden tests.
testDataPath :: Path Rel Dir
testDataPath = [reldir|test/test-data/|]
-- | Path to JSON-encoded Map from target to its list of default extensions.
extensionsPath :: Path Rel File
extensionsPath = [relfile|extensions|]
importifyDir, extensionsFile, modulesFile, symbolsDir :: FilePath
importifyDir = fromRelDir importifyPath
extensionsFile = fromRelFile extensionsPath
modulesFile = fromRelFile modulesPath
symbolsDir = fromRelDir symbolsPath
-- | Returns relative path to cabal file under given directory.
findCabalFile :: MonadIO m => Path Abs Dir -> m $ Maybe $ Path Abs File
findCabalFile projectPath = do
(_, projectFiles) <- listDir projectPath
return $ find isCabalFile projectFiles
isCabalFile :: Path Abs File -> Bool
isCabalFile = (== ".cabal") . takeExtension . fromAbsFile
-- | Create given directory and perform given action inside it.
doInsideDir :: (MonadIO m, MonadMask m) => Path Abs Dir -> m a -> m a
doInsideDir dir action = do
thisDirectory <- pwd
bracket_ (do ensureDir dir
cd $ fromString $ fromAbsDir dir)
(cd thisDirectory)
action
-- | Walk up till root while unpure predicate is 'False'. Returns
-- absolute path to directory where predicate is 'True' and suffix of
-- current directory prepended to given file.
lookupToRoot :: (Path Abs Dir -> IO Bool)
-> Path Rel File
-> IO (Maybe (Path Abs Dir, Path Rel File))
lookupToRoot predicate relativeFile = do
currentDir <- getCurrentDir
pathLoop currentDir relativeFile
where
pathLoop :: Path Abs Dir -> Path Rel File -> IO (Maybe (Path Abs Dir, Path Rel File))
pathLoop directory file = do
predicateIsTrue <- predicate directory
if predicateIsTrue then
return $ Just (directory, file)
else if parent directory == directory then -- fixpoint reached
return Nothing
else do
let parentDir = parent directory
let thisDir = dirname directory
pathLoop parentDir (thisDir </> file)
-- | Tries to read file and then 'decode' it. If either of two phases
-- fails then 'mempty' returned and warning is printed to console.
decodeFileOrMempty :: forall t m f b .
(FromJSON t, Monoid m, MonadIO f)
=> Path b File -- ^ Path to json data
-> (t -> f m) -- ^ Action from decoded value
-> f m
decodeFileOrMempty file onDecodedContent = do
isFileExist <- doesFileExist file
if isFileExist then
eitherDecodeStrict <$> liftIO (BS.readFile $ toFilePath file) >>= \case
Right value -> onDecodedContent value
Left msg -> do
let warning = "File '"+||file||+"' decoded incorrectly because of: "+|msg|+""
mempty <$ printWarning warning
else do
let msg = "File '"+||file||+
"' doesn't exist: caching first time or previous caching failed"
mempty <$ printNotice msg