ghc-mod/Language/Haskell/GhcMod/CabalHelper.hs

303 lines
11 KiB
Haskell
Raw Normal View History

-- 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:33:42 +00:00
{-# LANGUAGE CPP #-}
module Language.Haskell.GhcMod.CabalHelper
#ifndef SPEC
( getComponents
, getGhcMergedPkgOptions
, getCabalPackageDbStack
2015-08-17 05:41:46 +00:00
, getStackPackageDbStack
, getCustomPkgDbStack
, prepareCabalHelper
)
#endif
where
import Control.Applicative
import Control.Monad
import Control.Category ((.))
import Data.Maybe
import Data.Monoid
2015-06-07 01:36:50 +00:00
import Data.Serialize (Serialize)
import Data.Traversable
import Distribution.Helper hiding (Programs(..))
import qualified Distribution.Helper as CH
2015-03-15 19:48:55 +00:00
import qualified Language.Haskell.GhcMod.Types as T
import Language.Haskell.GhcMod.Types
import Language.Haskell.GhcMod.Monad.Types
import Language.Haskell.GhcMod.Utils
import Language.Haskell.GhcMod.PathsAndFiles
import Language.Haskell.GhcMod.Logging
2015-08-13 07:01:58 +00:00
import Language.Haskell.GhcMod.Output
import System.FilePath
2015-08-17 05:41:46 +00:00
import System.Directory (findExecutable)
import System.Process
import System.Exit
import Prelude hiding ((.))
2015-03-28 01:33:42 +00:00
import Paths_ghc_mod as GhcMod
-- | Only package related GHC options, sufficient for things that don't need to
-- access home modules
getGhcMergedPkgOptions :: (Applicative m, IOish m, GmEnv m, GmState m, GmLog m)
=> m [GHCOption]
2015-08-19 04:48:27 +00:00
getGhcMergedPkgOptions = chCached $ \distdir -> Cached {
cacheLens = Just (lGmcMergedPkgOptions . lGmCaches),
2015-08-19 04:48:27 +00:00
cacheFile = mergedPkgOptsCacheFile distdir,
cachedAction = \ _tcf (progs, rootdir, _) _ma -> do
2015-08-13 07:01:58 +00:00
readProc <- gmReadProcess
opts <- withCabal $ runQuery'' readProc progs rootdir distdir $
ghcMergedPkgOptions
2015-08-19 04:48:27 +00:00
return ([setupConfigPath distdir], opts)
}
getCabalPackageDbStack :: (IOish m, GmEnv m, GmState m, GmLog m) => m [GhcPkgDb]
2015-08-19 04:48:27 +00:00
getCabalPackageDbStack = chCached $ \distdir -> Cached {
cacheLens = Just (lGmcPackageDbStack . lGmCaches),
2015-08-19 04:48:27 +00:00
cacheFile = pkgDbStackCacheFile distdir,
cachedAction = \ _tcf (progs, rootdir, _) _ma -> do
crdl <- cradle
2015-08-13 07:01:58 +00:00
readProc <- gmReadProcess
dbs <- withCabal $ map chPkgToGhcPkg <$> runQuery'' readProc progs rootdir distdir packageDbStack
return ([setupConfigFile crdl, sandboxConfigFile crdl], dbs)
}
chPkgToGhcPkg :: ChPkgDb -> GhcPkgDb
chPkgToGhcPkg ChPkgGlobal = GlobalDb
chPkgToGhcPkg ChPkgUser = UserDb
chPkgToGhcPkg (ChPkgSpecific f) = PackageDb f
-- | Primary interface to cabal-helper and intended single entrypoint to
-- constructing 'GmComponent's
--
-- The Component\'s 'gmcHomeModuleGraph' will be empty and has to be resolved by
-- 'resolveGmComponents'.
getComponents :: (Applicative m, IOish m, GmEnv m, GmState m, GmLog m)
2015-08-03 01:09:56 +00:00
=> m [GmComponent 'GMCRaw ChEntrypoint]
2015-08-19 04:48:27 +00:00
getComponents = chCached$ \distdir -> Cached {
cacheLens = Just (lGmcComponents . lGmCaches),
2015-08-19 04:48:27 +00:00
cacheFile = cabalHelperCacheFile distdir,
cachedAction = \ _tcf (progs, rootdir, _vers) _ma -> do
2015-08-13 07:01:58 +00:00
readProc <- gmReadProcess
runQuery'' readProc progs rootdir distdir $ do
q <- join7
<$> ghcOptions
<*> ghcPkgOptions
<*> ghcSrcOptions
<*> ghcLangOptions
<*> entrypoints
<*> entrypoints
<*> sourceDirs
2015-05-05 12:44:42 +00:00
let cs = flip map q $ curry8 (GmComponent mempty)
2015-08-19 04:48:27 +00:00
return ([setupConfigPath distdir], cs)
}
2015-03-15 19:48:55 +00:00
where
2015-05-05 12:44:42 +00:00
curry8 fn (a, (b, (c, (d, (e, (f, (g, h))))))) = fn a b c d e f g h
join7 a b c d e f = join' a . join' b . join' c . join' d . join' e . join' f
2015-03-15 19:48:55 +00:00
join' :: Eq a => [(a,b)] -> [(a,c)] -> [(a,(b,c))]
join' lb lc = [ (a, (b, c))
| (a, b) <- lb
2015-05-05 12:44:42 +00:00
, (a', c) <- lc
, a == a'
]
2015-03-28 01:33:42 +00:00
prepareCabalHelper :: (IOish m, GmEnv m, GmLog m) => m ()
prepareCabalHelper = do
crdl <- cradle
let projdir = cradleRootDir crdl
2015-08-18 09:41:14 +00:00
distdir = projdir </> cradleDistDir crdl
readProc <- gmReadProcess
2015-08-18 12:55:45 +00:00
when (cradleProjectType crdl == CabalProject || cradleProjectType crdl == StackProject) $
withCabal $ liftIO $ prepare readProc projdir distdir
parseCustomPackageDb :: String -> [GhcPkgDb]
parseCustomPackageDb src = map parsePkgDb $ filter (not . null) $ lines src
where
parsePkgDb "global" = GlobalDb
parsePkgDb "user" = UserDb
parsePkgDb s = PackageDb s
getCustomPkgDbStack :: (IOish m, GmEnv m) => m (Maybe [GhcPkgDb])
getCustomPkgDbStack = do
mCusPkgDbFile <- liftIO . (traverse readFile <=< findCustomPackageDbFile) . cradleRootDir =<< cradle
return $ parseCustomPackageDb <$> mCusPkgDbFile
2015-08-17 05:41:46 +00:00
getStackPackageDbStack :: IOish m => m [GhcPkgDb]
getStackPackageDbStack = do
mstack <- liftIO $ findExecutable "stack"
case mstack of
Nothing -> return []
Just stack -> do
snapshotDb <- liftIO $ readProcess stack ["path", "--snapshot-pkg-db"] ""
localDb <- liftIO $ readProcess stack ["path", "--local-pkg-db"] ""
return $ map (PackageDb . takeWhile (/='\n')) [snapshotDb, localDb]
patchStackPrograms :: IOish m => OutputOpts -> Cradle -> Programs -> m Programs
patchStackPrograms _oopts crdl progs
| cradleProjectType crdl /= StackProject = return progs
patchStackPrograms oopts crdl progs = do
let projdir = cradleRootDir crdl
Just ghc <- liftIO $ getStackGhcPath oopts projdir
Just ghcPkg <- liftIO $ getStackGhcPkgPath oopts projdir
return $ progs {
ghcProgram = ghc
, ghcPkgProgram = ghcPkg
}
withCabal :: (IOish m, GmEnv m, GmLog m) => m a -> m a
withCabal action = do
crdl <- cradle
opts <- options
2015-08-13 07:01:58 +00:00
readProc <- gmReadProcess
2015-08-12 07:04:09 +00:00
let projdir = cradleRootDir crdl
2015-08-18 09:41:14 +00:00
distdir = projdir </> cradleDistDir crdl
2015-08-12 07:04:09 +00:00
mCabalFile <- liftIO $ timeFile `traverse` cradleCabalFile crdl
mCabalConfig <- liftIO $ timeMaybe (setupConfigFile crdl)
mCabalSandboxConfig <- liftIO $ timeMaybe (sandboxConfigFile crdl)
mCusPkgDbStack <- getCustomPkgDbStack
pkgDbStackOutOfSync <-
case mCusPkgDbStack of
Just cusPkgDbStack -> do
pkgDb <- runQuery'' readProc (helperProgs $ programs opts) projdir distdir $
map chPkgToGhcPkg <$> packageDbStack
return $ pkgDb /= cusPkgDbStack
Nothing -> return False
2015-08-19 07:04:25 +00:00
projType <- cradleProjectType <$> cradle
when (isSetupConfigOutOfDate mCabalFile mCabalConfig) $
gmLog GmDebug "" $ strDoc $ "setup configuration is out of date, reconfiguring Cabal project."
when (isSetupConfigOutOfDate mCabalSandboxConfig mCabalConfig) $
gmLog GmDebug "" $ strDoc $ "sandbox configuration is out of date, reconfiguring Cabal project."
when pkgDbStackOutOfSync $
gmLog GmDebug "" $ strDoc $ "package-db stack out of sync with ghc-mod.package-db-stack, reconfiguring Cabal project."
when ( isSetupConfigOutOfDate mCabalFile mCabalConfig
|| pkgDbStackOutOfSync
|| isSetupConfigOutOfDate mCabalSandboxConfig mCabalConfig) $
2015-08-19 07:04:25 +00:00
case projType of
CabalProject ->
cabalReconfigure readProc (programs opts) crdl projdir distdir
2015-08-19 07:04:25 +00:00
StackProject ->
stackReconfigure crdl (programs opts)
2015-08-19 07:04:25 +00:00
_ ->
error $ "withCabal: unsupported project type: " ++ show projType
action
2015-03-03 11:18:54 +00:00
2015-08-19 07:04:25 +00:00
where
cabalReconfigure readProc progs crdl projdir distdir = do
2015-08-19 07:04:25 +00:00
withDirectory_ (cradleRootDir crdl) $ do
cusPkgStack <- maybe [] ((PackageDb "clear"):) <$> getCustomPkgDbStack
let progOpts =
[ "--with-ghc=" ++ T.ghcProgram progs ]
2015-08-19 07:04:25 +00:00
-- Only pass ghc-pkg if it was actually set otherwise we
-- might break cabal's guessing logic
++ if T.ghcPkgProgram progs /= T.ghcPkgProgram (programs defaultOptions)
then [ "--with-ghc-pkg=" ++ T.ghcPkgProgram progs ]
2015-08-19 07:04:25 +00:00
else []
++ map pkgDbArg cusPkgStack
liftIO $ void $ readProc (T.cabalProgram progs) ("configure":progOpts) ""
2015-08-19 07:04:25 +00:00
gmLog GmDebug "" $ strDoc $ "writing Cabal autogen files"
liftIO $ writeAutogenFiles readProc projdir distdir
stackReconfigure crdl progs = do
withDirectory_ (cradleRootDir crdl) $ do
supported <- haveStackSupport
if supported
then do
spawn [T.stackProgram progs, "build", "--only-dependencies"]
spawn [T.stackProgram progs, "build", "--only-configure"]
else
gmLog GmWarning "" $ strDoc $ "Stack project configuration is out of date, please reconfigure manually using 'stack build' as your stack version is too old (need at least 1.4.0.0)"
spawn [] = return ()
spawn (exe:args) = do
readProc <- gmReadProcess
liftIO $ void $ readProc exe args ""
haveStackSupport = do
(rv, _, _) <-
liftIO $ readProcessWithExitCode "stack" ["--numeric-version"] ""
case rv of
ExitSuccess -> return True
ExitFailure _ -> return False
2015-08-19 07:04:25 +00:00
2015-03-03 11:18:54 +00:00
pkgDbArg :: GhcPkgDb -> String
pkgDbArg GlobalDb = "--package-db=global"
pkgDbArg UserDb = "--package-db=user"
pkgDbArg (PackageDb p) = "--package-db=" ++ p
-- * Neither file exists -> should return False:
-- @Nothing < Nothing = False@
-- (since we don't need to @cabal configure@ when no cabal file exists.)
--
-- * Cabal file doesn't exist (impossible since cabal-helper is only used with
-- cabal projects) -> should return False
-- @Just cc < Nothing = False@
--
-- * dist/setup-config doesn't exist yet -> should return True:
-- @Nothing < Just cf = True@
--
-- * Both files exist
-- @Just cc < Just cf = cc < cf = cc `olderThan` cf@
isSetupConfigOutOfDate :: Maybe TimedFile -> Maybe TimedFile -> Bool
isSetupConfigOutOfDate worldCabalFile worldCabalConfig = do
worldCabalConfig < worldCabalFile
helperProgs :: Programs -> CH.Programs
helperProgs progs = CH.Programs {
cabalProgram = T.cabalProgram progs,
ghcProgram = T.ghcProgram progs,
ghcPkgProgram = T.ghcPkgProgram progs
}
chCached :: (Applicative m, IOish m, GmEnv m, GmState m, GmLog m, Serialize a)
2015-08-18 09:41:14 +00:00
=> (FilePath -> Cached m GhcModState ChCacheData a) -> m a
chCached c = do
root <- cradleRootDir <$> cradle
2015-08-18 09:41:14 +00:00
dist <- cradleDistDir <$> cradle
2015-08-19 04:48:27 +00:00
d <- cacheInputData root
2015-08-18 09:41:14 +00:00
withCabal $ cached root (c dist) d
where
2015-08-19 04:48:27 +00:00
-- we don't need to include the disdir in the cache input because when it
-- changes the cache files will be gone anyways ;)
cacheInputData root = do
opts <- options
let oopts = outputOpts opts
progs = programs opts
crdl <- cradle
progs' <- patchStackPrograms oopts crdl progs
return $ ( helperProgs progs'
2015-08-10 07:07:41 +00:00
, root
, (gmVer, chVer)
)
gmVer = GhcMod.version
chVer = VERSION_cabal_helper