{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE ScopedTypeVariables #-} -- | Setting the package database to use when compiling modules. The daemon must have one single -- package database that cannot be changed after a package is loaded using that package database. -- Available package databases are the cabal global, the cabal sandbox, the stack or one that had -- been explicitely set by a file path. module Language.Haskell.Tools.Daemon.PackageDB (PackageDB(..), decidePkgDB, packageDBLoc, detectAutogen) where import Control.Applicative (Alternative(..)) import Control.Exception (SomeException, try) import Control.Monad import Data.Aeson (FromJSON(..)) import Data.Char (isSpace) import Data.List import Data.Maybe import GHC.Generics (Generic(..)) import System.Directory import System.Exit (ExitCode(..)) import System.FilePath (FilePath, ()) import System.Process (shell, readCreateProcessWithExitCode) -- | Possible package database configurations. data PackageDB = DefaultDB -- ^ Use the global cabal package database (like when using ghc). | CabalSandboxDB -- ^ Use the sandboxed cabal package database. | StackDB -- ^ Use the stack package databases (local and snapshot). | ExplicitDB { packageDBPath :: [FilePath] } -- ^ Set the package database explicitely. deriving (Eq, Show, Generic) instance FromJSON PackageDB -- | Decide which type of project we are dealing with based on the package folders. -- Should only be invoked if the user did not select the project-type. decidePkgDB :: [FilePath] -> IO (Maybe PackageDB) decidePkgDB [] = return Nothing decidePkgDB (firstRoot:packageRoots) = do fstRes <- decidePkgDB' firstRoot res <- mapM decidePkgDB' packageRoots if any (fstRes /=) res || (fstRes == CabalSandboxDB && not (null res)) then return Nothing else return (Just fstRes) decidePkgDB' :: FilePath -> IO PackageDB decidePkgDB' root = do isSandbox <- checkSandbox if isSandbox then return CabalSandboxDB else do isStack <- checkStack if isStack then return StackDB else return DefaultDB where checkStack = withCurrentDirectory root $ (fmap $ either (\(_ :: SomeException) -> False) id) $ try $ do projRoot <- runCommandExpectOK "stack path --allow-different-user --project-root" absPath <- canonicalizePath root -- we only accept stack projects where the packages are (direct or indirect) subdirectories of the project root return $ maybe False (`isPrefixOf` absPath) projRoot checkSandbox = do hasConfigFile <- doesFileExist (root "cabal.config") hasSandboxFile <- doesFileExist (root "cabal.sandbox.config") return $ hasConfigFile || hasSandboxFile -- | Finds the location of the package database based on the configuration. packageDBLoc :: PackageDB -> FilePath -> IO [FilePath] packageDBLoc DefaultDB _ = do dbs <- runCommandExpectOK "ghc-pkg list base" return $ maybe [] (filter (\l -> not (null l) && not (" " `isPrefixOf` l)) . lines) dbs packageDBLoc CabalSandboxDB path = do hasConfigFile <- doesFileExist (path "cabal.config") config <- if hasConfigFile then readFile (path "cabal.config") else readFile (path "cabal.sandbox.config") return $ map (drop (length "package-db: ")) $ filter ("package-db: " `isPrefixOf`) $ lines config packageDBLoc StackDB path = withCurrentDirectory path $ do -- TODO: group the 3 calls into one for speed, split the output globalDB <- runCommandExpectOK "stack path --allow-different-user --global-pkg-db" snapshotDB <- runCommandExpectOK "stack path --allow-different-user --snapshot-pkg-db" localDB <- runCommandExpectOK "stack path --allow-different-user --local-pkg-db" return $ maybeToList localDB ++ maybeToList snapshotDB ++ maybeToList globalDB packageDBLoc (ExplicitDB dirs) _ = return dirs -- | Gets the (probable) location of autogen folder depending on which type of -- build we are using. detectAutogen :: FilePath -> PackageDB -> IO (Maybe FilePath) detectAutogen root DefaultDB = ifExists (root "dist" "build" "autogen") detectAutogen root (ExplicitDB _) = ifExists (root "dist" "build" "autogen") detectAutogen root CabalSandboxDB = ifExists (root "dist" "build" "autogen") detectAutogen root StackDB = (fmap $ either (\(_ :: SomeException) -> Nothing) id) $ try $ do dir <- withCurrentDirectory root $ do distDir <- runCommandExpectOK "stack path --allow-different-user --dist-dir" return $ trim (fromMaybe "" distDir) genExists <- doesDirectoryExist (root dir "build" "autogen") buildExists <- doesDirectoryExist (root dir "build") if | genExists -> return $ Just (root dir "build" "autogen") | buildExists -> do -- for some packages, the autogen folder is inside a folder named after the package cont <- filterM doesDirectoryExist . map ((root dir "build") ) =<< listDirectory (root dir "build") existing <- mapM ifExists (map ( "autogen") cont) return $ choose existing | otherwise -> return Nothing -- | Run a command and return its result if successful display an error message otherwise. runCommandExpectOK :: String -> IO (Maybe String) runCommandExpectOK cmd = do (exitCode, res, errs) <- readCreateProcessWithExitCode (shell cmd) "" case exitCode of ExitSuccess -> return (Just $ trim res) ExitFailure code -> do putStrLn ("The command '" ++ cmd ++ "' exited with " ++ show code ++ ":\n" ++ errs) return Nothing trim :: String -> String trim = f . f where f = reverse . dropWhile isSpace -- take the first nonempty choose :: (Eq (f a), Alternative f) => [f a] -> f a choose = fromMaybe empty . find (/= empty) ifExists :: FilePath -> IO (Maybe FilePath) ifExists fp = do exists <- doesDirectoryExist fp if exists then return (Just fp) else return Nothing