2015-03-03 19:28:34 +00:00
|
|
|
-- ghc-mod: Making Haskell development *more* fun
|
|
|
|
-- Copyright (C) 2015 Daniel Gröber <dxld ÄT darkboxed DOT org>
|
|
|
|
--
|
|
|
|
-- This program is free software: you can redistribute it and/or modify
|
|
|
|
-- it under the terms of the GNU Affero General Public License as published by
|
|
|
|
-- the Free Software Foundation, either version 3 of the License, or
|
|
|
|
-- (at your option) any later version.
|
|
|
|
--
|
|
|
|
-- This program is distributed in the hope that it will be useful,
|
|
|
|
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
-- GNU Affero General Public License for more details.
|
|
|
|
--
|
|
|
|
-- You should have received a copy of the GNU Affero General Public License
|
|
|
|
-- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
2015-03-28 01:30:51 +00:00
|
|
|
module Language.Haskell.GhcMod.PathsAndFiles (
|
|
|
|
module Language.Haskell.GhcMod.PathsAndFiles
|
|
|
|
, module Language.Haskell.GhcMod.Caching
|
|
|
|
) where
|
2014-11-01 21:02:47 +00:00
|
|
|
|
2015-02-07 22:55:57 +00:00
|
|
|
import Config (cProjectVersion)
|
2014-11-01 21:02:47 +00:00
|
|
|
import Control.Applicative
|
2015-08-31 06:01:20 +00:00
|
|
|
import Control.Exception as E
|
2014-11-01 21:02:47 +00:00
|
|
|
import Control.Monad
|
2015-08-19 04:48:27 +00:00
|
|
|
import Control.Monad.Trans.Maybe
|
2015-09-01 08:27:12 +00:00
|
|
|
import Control.Monad.Trans.Class
|
2014-11-01 21:02:47 +00:00
|
|
|
import Data.List
|
|
|
|
import Data.Char
|
|
|
|
import Data.Maybe
|
2015-08-03 05:51:23 +00:00
|
|
|
import Data.Traversable hiding (mapM)
|
2015-04-12 00:39:55 +00:00
|
|
|
import Distribution.Helper (buildPlatform)
|
2014-11-01 21:02:47 +00:00
|
|
|
import System.Directory
|
|
|
|
import System.FilePath
|
2015-08-12 07:04:35 +00:00
|
|
|
import System.Process
|
2015-08-28 07:44:20 +00:00
|
|
|
import System.Info.Extra
|
2014-11-01 21:02:47 +00:00
|
|
|
|
2015-03-03 20:12:43 +00:00
|
|
|
import Language.Haskell.GhcMod.Types
|
2015-09-01 08:27:12 +00:00
|
|
|
import Language.Haskell.GhcMod.Monad.Types
|
2015-03-28 01:30:51 +00:00
|
|
|
import Language.Haskell.GhcMod.Caching
|
2015-08-31 05:33:36 +00:00
|
|
|
import Language.Haskell.GhcMod.Output
|
2014-11-01 21:02:47 +00:00
|
|
|
import qualified Language.Haskell.GhcMod.Utils as U
|
2015-05-19 12:04:15 +00:00
|
|
|
import Utils (mightExist)
|
2015-08-03 01:09:56 +00:00
|
|
|
import Prelude
|
2014-11-01 21:02:47 +00:00
|
|
|
|
|
|
|
-- | Guaranteed to be a path to a directory with no trailing slash.
|
|
|
|
type DirPath = FilePath
|
|
|
|
|
|
|
|
-- | Guaranteed to be the name of a file only (no slashes).
|
|
|
|
type FileName = String
|
|
|
|
|
2015-03-03 20:12:43 +00:00
|
|
|
newtype UnString = UnString { unString :: String }
|
|
|
|
|
|
|
|
instance Show UnString where
|
|
|
|
show = unString
|
|
|
|
|
|
|
|
instance Read UnString where
|
|
|
|
readsPrec _ = \str -> [(UnString str, "")]
|
|
|
|
|
2014-11-01 21:02:47 +00:00
|
|
|
-- | @findCabalFiles dir@. Searches for a @.cabal@ files in @dir@'s parent
|
|
|
|
-- directories. The first parent directory containing more than one cabal file
|
|
|
|
-- is assumed to be the project directory. If only one cabal file exists in this
|
|
|
|
-- directory it is returned otherwise @findCabalFiles@ throws 'GMENoCabalFile'
|
|
|
|
-- or 'GMETooManyCabalFiles'
|
2014-11-02 23:45:27 +00:00
|
|
|
findCabalFile :: FilePath -> IO (Maybe FilePath)
|
2015-02-07 15:40:22 +00:00
|
|
|
findCabalFile dir = do
|
2015-03-03 20:12:43 +00:00
|
|
|
-- List of directories and all cabal file candidates
|
|
|
|
dcs <- findFileInParentsP isCabalFile dir :: IO ([(DirPath, [FileName])])
|
|
|
|
let css = uncurry appendDir `map` dcs :: [[FilePath]]
|
|
|
|
case find (not . null) css of
|
|
|
|
Nothing -> return Nothing
|
2014-11-01 21:02:47 +00:00
|
|
|
Just cfs@(_:_:_) -> throw $ GMETooManyCabalFiles cfs
|
2015-03-03 20:12:43 +00:00
|
|
|
Just (a:_) -> return (Just a)
|
|
|
|
Just [] -> error "findCabalFile"
|
|
|
|
where
|
|
|
|
appendDir :: DirPath -> [FileName] -> [FilePath]
|
|
|
|
appendDir d fs = (d </>) `map` fs
|
2015-02-07 15:40:22 +00:00
|
|
|
|
2015-08-17 05:41:46 +00:00
|
|
|
findStackConfigFile :: FilePath -> IO (Maybe FilePath)
|
|
|
|
findStackConfigFile dir = mightExist (dir </> "stack.yaml")
|
|
|
|
|
2015-09-01 08:27:12 +00:00
|
|
|
getStackDistDir :: (IOish m, GmOut m) => FilePath -> m (Maybe FilePath)
|
|
|
|
getStackDistDir projdir = U.withDirectory_ projdir $ runMaybeT $ do
|
|
|
|
takeWhile (/='\n') <$> readStack ["path", "--dist-dir"]
|
2015-08-28 07:44:20 +00:00
|
|
|
|
2015-09-01 08:27:12 +00:00
|
|
|
getStackGhcPath :: (IOish m, GmOut m) => FilePath -> m (Maybe FilePath)
|
|
|
|
getStackGhcPath = findExecutablesInStackBinPath "ghc"
|
2015-08-28 07:44:20 +00:00
|
|
|
|
2015-09-01 08:27:12 +00:00
|
|
|
getStackGhcPkgPath :: (IOish m, GmOut m) => FilePath -> m (Maybe FilePath)
|
|
|
|
getStackGhcPkgPath = findExecutablesInStackBinPath "ghc-pkg"
|
2015-08-28 07:44:20 +00:00
|
|
|
|
2015-09-01 08:27:12 +00:00
|
|
|
findExecutablesInStackBinPath :: (IOish m, GmOut m) => String -> FilePath -> m (Maybe FilePath)
|
|
|
|
findExecutablesInStackBinPath exe projdir =
|
2015-08-28 07:44:20 +00:00
|
|
|
U.withDirectory_ projdir $ runMaybeT $ do
|
|
|
|
path <- splitSearchPath . takeWhile (/='\n')
|
2015-09-01 08:27:12 +00:00
|
|
|
<$> readStack ["path", "--bin-path"]
|
|
|
|
MaybeT $ liftIO $ listToMaybe <$> findExecutablesInDirectories' path exe
|
2015-08-28 07:44:20 +00:00
|
|
|
|
|
|
|
findExecutablesInDirectories' :: [FilePath] -> String -> IO [FilePath]
|
|
|
|
findExecutablesInDirectories' path binary =
|
|
|
|
U.findFilesWith' isExecutable path (binary <.> exeExtension)
|
|
|
|
where isExecutable file = do
|
|
|
|
perms <- getPermissions file
|
|
|
|
return $ executable perms
|
|
|
|
|
|
|
|
exeExtension = if isWindows then "exe" else ""
|
|
|
|
|
2015-09-01 08:27:12 +00:00
|
|
|
readStack :: (IOish m, GmOut m) => [String] -> MaybeT m String
|
|
|
|
readStack args = do
|
|
|
|
stack <- MaybeT $ liftIO $ findExecutable "stack"
|
|
|
|
readProc <- lift gmReadProcess
|
2015-08-31 06:01:20 +00:00
|
|
|
liftIO $ flip E.catch (\(e :: IOError) -> throw $ GMEStackBootrap $ show e) $ do
|
2015-09-01 08:27:12 +00:00
|
|
|
evaluate =<< readProc stack args ""
|
2015-08-18 09:41:14 +00:00
|
|
|
|
2015-08-07 04:47:34 +00:00
|
|
|
-- | Get path to sandbox config file
|
2015-08-19 06:46:56 +00:00
|
|
|
getSandboxDb :: Cradle -> IO (Maybe GhcPkgDb)
|
|
|
|
getSandboxDb crdl = do
|
|
|
|
mConf <-traverse readFile =<< mightExist (sandboxConfigFile crdl)
|
2015-08-12 07:04:35 +00:00
|
|
|
bp <- buildPlatform readProcess
|
|
|
|
return $ PackageDb . fixPkgDbVer bp <$> (extractSandboxDbDir =<< mConf)
|
2015-08-07 04:47:34 +00:00
|
|
|
|
|
|
|
where
|
2015-08-12 07:04:35 +00:00
|
|
|
fixPkgDbVer bp dir =
|
|
|
|
case takeFileName dir == ghcSandboxPkgDbDir bp of
|
2015-08-07 04:47:34 +00:00
|
|
|
True -> dir
|
2015-08-12 07:04:35 +00:00
|
|
|
False -> takeDirectory dir </> ghcSandboxPkgDbDir bp
|
2015-08-07 04:47:34 +00:00
|
|
|
|
|
|
|
-- | Extract the sandbox package db directory from the cabal.sandbox.config
|
|
|
|
-- file. Exception is thrown if the sandbox config file is broken.
|
|
|
|
extractSandboxDbDir :: String -> Maybe FilePath
|
|
|
|
extractSandboxDbDir conf = extractValue <$> parse conf
|
|
|
|
where
|
|
|
|
key = "package-db:"
|
|
|
|
keyLen = length key
|
|
|
|
|
|
|
|
parse = listToMaybe . filter (key `isPrefixOf`) . lines
|
|
|
|
extractValue = U.dropWhileEnd isSpace . dropWhile isSpace . drop keyLen
|
|
|
|
|
|
|
|
|
2015-02-07 15:40:22 +00:00
|
|
|
-- |
|
|
|
|
-- >>> isCabalFile "/home/user/.cabal"
|
|
|
|
-- False
|
|
|
|
isCabalFile :: FilePath -> Bool
|
|
|
|
isCabalFile f = takeExtension' f == ".cabal"
|
|
|
|
|
|
|
|
-- |
|
|
|
|
-- >>> takeExtension' "/some/dir/bla.cabal"
|
|
|
|
-- ".cabal"
|
|
|
|
--
|
|
|
|
-- >>> takeExtension' "some/reldir/bla.cabal"
|
|
|
|
-- ".cabal"
|
|
|
|
--
|
|
|
|
-- >>> takeExtension' "bla.cabal"
|
|
|
|
-- ".cabal"
|
|
|
|
--
|
|
|
|
-- >>> takeExtension' ".cabal"
|
|
|
|
-- ""
|
|
|
|
takeExtension' :: FilePath -> String
|
|
|
|
takeExtension' p =
|
|
|
|
if takeFileName p == takeExtension p
|
|
|
|
then "" -- just ".cabal" is not a valid cabal file
|
|
|
|
else takeExtension p
|
|
|
|
|
|
|
|
-- | @findFileInParentsP p dir@ Look for files satisfying @p@ in @dir@ and all
|
|
|
|
-- it's parent directories.
|
|
|
|
findFileInParentsP :: (FilePath -> Bool) -> FilePath
|
|
|
|
-> IO [(DirPath, [FileName])]
|
|
|
|
findFileInParentsP p dir =
|
|
|
|
getFilesP p `zipMapM` parents dir
|
|
|
|
|
|
|
|
-- | @getFilesP p dir@. Find all __files__ satisfying @p@ in @.cabal@ in @dir@.
|
|
|
|
getFilesP :: (FilePath -> Bool) -> DirPath -> IO [FileName]
|
|
|
|
getFilesP p dir = filterM p' =<< getDirectoryContents dir
|
2014-11-02 23:45:27 +00:00
|
|
|
where
|
2015-02-07 15:40:22 +00:00
|
|
|
p' fn = do
|
|
|
|
(p fn && ) <$> doesFileExist (dir </> fn)
|
|
|
|
|
|
|
|
findCabalSandboxDir :: FilePath -> IO (Maybe FilePath)
|
|
|
|
findCabalSandboxDir dir = do
|
|
|
|
dss <- findFileInParentsP isSandboxConfig dir
|
|
|
|
return $ case find (not . null . snd) $ dss of
|
|
|
|
Just (sbDir, _:_) -> Just sbDir
|
|
|
|
_ -> Nothing
|
2014-11-01 21:02:47 +00:00
|
|
|
|
2014-11-02 23:04:15 +00:00
|
|
|
where
|
2015-08-19 06:46:56 +00:00
|
|
|
isSandboxConfig = (==sandboxConfigFileName)
|
2015-02-07 15:40:22 +00:00
|
|
|
|
2014-11-01 21:02:47 +00:00
|
|
|
zipMapM :: Monad m => (a -> m c) -> [a] -> m [(a,c)]
|
2015-03-03 20:12:43 +00:00
|
|
|
zipMapM f as = mapM (\a -> liftM ((,) a) $ f a) as
|
2014-11-01 21:02:47 +00:00
|
|
|
|
|
|
|
-- | @parents dir@. Returns all parent directories of @dir@ including @dir@.
|
|
|
|
--
|
|
|
|
-- Examples
|
|
|
|
--
|
|
|
|
-- >>> parents "foo"
|
|
|
|
-- ["foo"]
|
|
|
|
--
|
|
|
|
-- >>> parents "/foo"
|
|
|
|
-- ["/foo","/"]
|
|
|
|
--
|
|
|
|
-- >>> parents "/foo/bar"
|
|
|
|
-- ["/foo/bar","/foo","/"]
|
|
|
|
--
|
|
|
|
-- >>> parents "foo/bar"
|
|
|
|
-- ["foo/bar","foo"]
|
|
|
|
parents :: FilePath -> [FilePath]
|
|
|
|
parents "" = []
|
|
|
|
parents dir' =
|
|
|
|
let (drive, dir) = splitDrive $ normalise $ dropTrailingPathSeparator dir'
|
|
|
|
in map (joinDrive drive) $ parents' $ filter (/=".") $ splitDirectories dir
|
|
|
|
where
|
|
|
|
parents' :: [String] -> [FilePath]
|
|
|
|
parents' [] | isAbsolute dir' = "":[]
|
|
|
|
parents' [] = []
|
|
|
|
parents' dir = [joinPath dir] ++ parents' (init dir)
|
|
|
|
|
|
|
|
----------------------------------------------------------------
|
|
|
|
|
|
|
|
setupConfigFile :: Cradle -> FilePath
|
2015-08-19 04:48:27 +00:00
|
|
|
setupConfigFile crdl =
|
|
|
|
cradleRootDir crdl </> setupConfigPath (cradleDistDir crdl)
|
2014-11-01 21:02:47 +00:00
|
|
|
|
2015-08-19 06:46:56 +00:00
|
|
|
sandboxConfigFile :: Cradle -> FilePath
|
|
|
|
sandboxConfigFile crdl = cradleRootDir crdl </> sandboxConfigFileName
|
|
|
|
|
|
|
|
sandboxConfigFileName :: String
|
|
|
|
sandboxConfigFileName = "cabal.sandbox.config"
|
2015-08-07 04:47:34 +00:00
|
|
|
|
2014-11-01 21:02:47 +00:00
|
|
|
-- | Path to 'LocalBuildInfo' file, usually @dist/setup-config@
|
2015-08-19 04:48:27 +00:00
|
|
|
setupConfigPath :: FilePath -> FilePath
|
|
|
|
setupConfigPath dist = dist </> "setup-config"
|
|
|
|
-- localBuildInfoFile defaultDistPref
|
2014-11-01 21:02:47 +00:00
|
|
|
|
2015-04-12 00:36:17 +00:00
|
|
|
macrosHeaderPath :: FilePath
|
2015-08-18 09:41:14 +00:00
|
|
|
macrosHeaderPath = "build/autogen/cabal_macros.h"
|
2015-04-12 00:36:17 +00:00
|
|
|
|
2015-08-12 07:04:35 +00:00
|
|
|
ghcSandboxPkgDbDir :: String -> String
|
|
|
|
ghcSandboxPkgDbDir buildPlatf = do
|
|
|
|
buildPlatf ++ "-ghc-" ++ cProjectVersion ++ "-packages.conf.d"
|
2015-02-07 14:23:00 +00:00
|
|
|
|
2014-11-01 21:02:47 +00:00
|
|
|
packageCache :: String
|
|
|
|
packageCache = "package.cache"
|
2015-02-07 14:23:00 +00:00
|
|
|
|
2015-02-07 22:52:26 +00:00
|
|
|
-- | Filename of the symbol table cache file.
|
|
|
|
symbolCache :: Cradle -> FilePath
|
|
|
|
symbolCache crdl = cradleTempDir crdl </> symbolCacheFile
|
|
|
|
|
|
|
|
symbolCacheFile :: String
|
2015-03-03 20:12:43 +00:00
|
|
|
symbolCacheFile = "ghc-mod.symbol-cache"
|
2015-03-28 01:30:51 +00:00
|
|
|
|
2015-08-19 04:48:27 +00:00
|
|
|
resolvedComponentsCacheFile :: FilePath -> FilePath
|
|
|
|
resolvedComponentsCacheFile dist =
|
|
|
|
setupConfigPath dist <.> "ghc-mod.resolved-components"
|
2015-03-28 01:30:51 +00:00
|
|
|
|
2015-08-19 04:48:27 +00:00
|
|
|
cabalHelperCacheFile :: FilePath -> FilePath
|
|
|
|
cabalHelperCacheFile dist =
|
|
|
|
setupConfigPath dist <.> "ghc-mod.cabal-components"
|
2015-06-05 20:42:46 +00:00
|
|
|
|
2015-08-19 04:48:27 +00:00
|
|
|
mergedPkgOptsCacheFile :: FilePath -> FilePath
|
|
|
|
mergedPkgOptsCacheFile dist =
|
|
|
|
setupConfigPath dist <.> "ghc-mod.package-options"
|
2015-03-03 11:18:54 +00:00
|
|
|
|
2015-08-19 04:48:27 +00:00
|
|
|
pkgDbStackCacheFile :: FilePath -> FilePath
|
|
|
|
pkgDbStackCacheFile dist =
|
|
|
|
setupConfigPath dist <.> "ghc-mod.package-db-stack"
|
2015-08-07 04:47:34 +00:00
|
|
|
|
|
|
|
-- | @findCustomPackageDbFile dir@. Searches for a @.ghc-mod.cradle@ file in @dir@.
|
2015-03-03 11:18:54 +00:00
|
|
|
-- If it exists in the given directory it is returned otherwise @findCradleFile@ returns @Nothing@
|
2015-08-07 04:47:34 +00:00
|
|
|
findCustomPackageDbFile :: FilePath -> IO (Maybe FilePath)
|
|
|
|
findCustomPackageDbFile directory = do
|
|
|
|
let path = directory </> "ghc-mod.package-db-stack"
|
2015-05-19 12:04:15 +00:00
|
|
|
mightExist path
|