diff options
Diffstat (limited to 'lib/Distribution/Helper.hs')
-rw-r--r-- | lib/Distribution/Helper.hs | 527 |
1 files changed, 527 insertions, 0 deletions
diff --git a/lib/Distribution/Helper.hs b/lib/Distribution/Helper.hs new file mode 100644 index 0000000..73ad668 --- /dev/null +++ b/lib/Distribution/Helper.hs @@ -0,0 +1,527 @@ +-- ghc-mod: Making Haskell development *more* fun +-- Copyright (C) 2015,2017 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/>. + +{-# LANGUAGE CPP, RecordWildCards, FlexibleContexts, ConstraintKinds, + GeneralizedNewtypeDeriving, DeriveDataTypeable, DeriveGeneric, DeriveFunctor + #-} + +{-| +Module : Distribution.Helper +License : AGPL-3 +Maintainer : dxld@darkboxed.org +Portability : POSIX +-} + +module Distribution.Helper ( + -- * Running Queries + Query + , runQuery + + -- * Queries against Cabal\'s on disk state + + -- ** Package queries + , packageId + , packageDbStack + , packageFlags + , packageLicenses + , compilerVersion + + , ghcMergedPkgOptions + + -- ** cabal-install queries + , configFlags + , nonDefaultConfigFlags + + + -- ** Component queries + , ComponentQuery + , components + + , ghcSrcOptions + , ghcPkgOptions + , ghcLangOptions + , ghcOptions + , sourceDirs + , entrypoints + + -- * Query environment + , QueryEnv + , mkQueryEnv + , qeReadProcess + , qePrograms + , qeProjectDir + , qeDistDir + , qeCabalPkgDb + , qeCabalVer + + , Programs(..) + , defaultPrograms + + + -- * Result types + , ChModuleName(..) + , ChComponentName(..) + , ChPkgDb(..) + , ChEntrypoint(..) + + -- * General information + , buildPlatform + + -- * Stuff that cabal-install really should export + , Distribution.Helper.getSandboxPkgDb + + -- * Managing @dist/@ + , prepare + , reconfigure + , writeAutogenFiles + + -- * $libexec related error handling + , LibexecNotFoundError(..) + , libexecNotFoundError + + -- * Reexports + , module Data.Functor.Apply + ) where + +import Control.Applicative +import Control.Monad +import Control.Monad.IO.Class +import Control.Monad.State.Strict +import Control.Monad.Reader +import Control.Exception as E +import Data.Char +import Data.List +import Data.Maybe +import Data.Version +import Data.Typeable +import Data.Functor.Apply +import Distribution.Simple.BuildPaths (exeExtension) +import System.Environment +import System.FilePath hiding ((<.>)) +import qualified System.FilePath as FP +import System.Directory +import System.Process +import System.IO.Unsafe +import Text.Printf +import GHC.Generics +import Prelude + +import Paths_cabal_helper (getLibexecDir) +import CabalHelper.Shared.InterfaceTypes +import CabalHelper.Shared.Sandbox + +-- | Paths or names of various programs we need. +data Programs = Programs { + -- | The path to the @cabal@ program. + cabalProgram :: FilePath, + + -- | The path to the @ghc@ program. + ghcProgram :: FilePath, + + -- | The path to the @ghc-pkg@ program. If + -- not changed it will be derived from the path to 'ghcProgram'. + ghcPkgProgram :: FilePath + } deriving (Eq, Ord, Show, Read, Generic, Typeable) + +-- | Default all programs to their unqualified names, i.e. they will be searched +-- for on @PATH@. +defaultPrograms :: Programs +defaultPrograms = Programs "cabal" "ghc" "ghc-pkg" + +-- | Environment for running a 'Query'. The real constructor is not exposed, +-- the field accessors are however. See below. Use the 'mkQueryEnv' smart +-- constructor to construct one. +data QueryEnv = QueryEnv { + -- | Field accessor for 'QueryEnv'. Defines how to start the cabal-helper + -- process. Useful if you need to capture stderr output from the helper. + qeReadProcess :: FilePath -> [String] -> String -> IO String, + + -- | Field accessor for 'QueryEnv'. + qePrograms :: Programs, + + -- | Field accessor for 'QueryEnv'. Defines path to the project directory, + -- i.e. a directory containing a @project.cabal@ file + qeProjectDir :: FilePath, + + + -- | Field accessor for 'QueryEnv'. Defines path to the @dist/@ directory, + -- /builddir/ in Cabal terminology. + qeDistDir :: FilePath, + + -- | Field accessor for 'QueryEnv'. Defines where to look for the Cabal + -- library when linking the helper. + qeCabalPkgDb :: Maybe FilePath, + + -- | Field accessor for 'QueryEnv'. If @dist/setup-config@ wasn\'t written + -- by this version of Cabal an error is thrown when running the query. + qeCabalVer :: Maybe Version + } + +-- | @mkQueryEnv projdir distdir@. Smart constructor for 'QueryEnv'. +-- Sets fields 'qeProjectDir' and 'qeDistDir' to @projdir@ and @distdir@ +-- respectively and provides sensible defaults for the other fields. +mkQueryEnv :: FilePath + -- ^ Path to the project directory, i.e. the directory containing a + -- @project.cabal@ file + -> FilePath + -- ^ Path to the @dist/@ directory, called /builddir/ in Cabal + -- terminology. + -> QueryEnv +mkQueryEnv projdir distdir = QueryEnv { + qeReadProcess = readProcess + , qePrograms = defaultPrograms + , qeProjectDir = projdir + , qeDistDir = distdir + , qeCabalPkgDb = Nothing + , qeCabalVer = Nothing + } + +data SomeLocalBuildInfo = SomeLocalBuildInfo { + slbiPackageDbStack :: [ChPkgDb], + slbiPackageFlags :: [(String, Bool)], + slbiPkgLicenses :: [(String, [(String, Version)])], + slbiCompilerVersion :: (String, Version), + + slbiGhcMergedPkgOptions :: [String], + + slbiConfigFlags :: [(String, Bool)], + slbiNonDefaultConfigFlags :: [(String, Bool)], + + slbiGhcSrcOptions :: [(ChComponentName, [String])], + slbiGhcPkgOptions :: [(ChComponentName, [String])], + slbiGhcLangOptions :: [(ChComponentName, [String])], + slbiGhcOptions :: [(ChComponentName, [String])], + + slbiSourceDirs :: [(ChComponentName, [String])], + slbiEntrypoints :: [(ChComponentName, ChEntrypoint)] + } deriving (Eq, Ord, Read, Show) + +-- | A lazy, cached, query against a package's Cabal configuration. Use +-- 'runQuery' to execute it. +newtype Query m a = Query { unQuery :: StateT (Maybe SomeLocalBuildInfo) + (ReaderT QueryEnv m) a } + deriving (Functor, Applicative, Monad, MonadIO) + +instance MonadTrans Query where + lift = Query . lift . lift + +type MonadQuery m = ( MonadIO m + , MonadState (Maybe SomeLocalBuildInfo) m + , MonadReader QueryEnv m) + +-- | A 'Query' to run on all components of a package. Use 'components' to get a +-- regular 'Query'. +newtype ComponentQuery m a = ComponentQuery (Query m [(ChComponentName, a)]) + deriving (Functor) + +instance Monad m => Apply (ComponentQuery m) where + ComponentQuery flab <.> ComponentQuery fla = + ComponentQuery $ liftM2 go flab fla + where + go :: [(ChComponentName, a -> b)] + -> [(ChComponentName, a)] + -> [(ChComponentName, b)] + go lab la = + [ (cn, ab a) + | (cn, ab) <- lab + , (cn', a) <- la + , cn == cn' + ] + +run :: Monad m => QueryEnv -> Maybe SomeLocalBuildInfo -> Query m a -> m a +run e s action = flip runReaderT e (flip evalStateT s (unQuery action)) + +-- | @runQuery env query@. Run a 'Query' under a given 'QueryEnv'. +runQuery :: Monad m + => QueryEnv + -> Query m a + -> m a +runQuery qe action = run qe Nothing action + +getSlbi :: MonadQuery m => m SomeLocalBuildInfo +getSlbi = do + s <- get + case s of + Nothing -> do + slbi <- getSomeConfigState + put (Just slbi) + return slbi + Just slbi -> return slbi + +-- | List of package databases to use. +packageDbStack :: MonadIO m => Query m [ChPkgDb] + +-- | Like @ghcPkgOptions@ but for the whole package not just one component +ghcMergedPkgOptions :: MonadIO m => Query m [String] + +-- | Get the licenses of the packages the current project is linking against. +packageLicenses :: MonadIO m => Query m [(String, [(String, Version)])] + +-- | Flag definitions from cabal file +packageFlags :: MonadIO m => Query m [(String, Bool)] + +-- | Flag assignments from setup-config +configFlags :: MonadIO m => Query m [(String, Bool)] + +-- | Flag assignments from setup-config which differ from the default +-- setting. This can also include flags which cabal decided to modify, +-- i.e. don't rely on these being the flags set by the user directly. +nonDefaultConfigFlags :: MonadIO m => Query m [(String, Bool)] + +-- | The version of GHC the project is configured to use +compilerVersion :: MonadIO m => Query m (String, Version) + +-- | Package identifier, i.e. package name and version +packageId :: MonadIO m => Query m (String, Version) + +-- | Run a ComponentQuery on all components of the package. +components :: Monad m => ComponentQuery m (ChComponentName -> b) -> Query m [b] +components (ComponentQuery sc) = map (\(cn, f) -> f cn) <$> sc + +-- | Modules or files Cabal would have the compiler build directly. Can be used +-- to compute the home module closure for a component. +entrypoints :: MonadIO m => ComponentQuery m ChEntrypoint + +-- | A component's @source-dirs@ field, beware since if this is empty implicit +-- behaviour in GHC kicks in. +sourceDirs :: MonadIO m => ComponentQuery m [FilePath] + +-- | All options Cabal would pass to GHC. +ghcOptions :: MonadIO m => ComponentQuery m [String] + +-- | Only search path related GHC options. +ghcSrcOptions :: MonadIO m => ComponentQuery m [String] + +-- | Only package related GHC options, sufficient for things don't need to +-- access any home modules. +ghcPkgOptions :: MonadIO m => ComponentQuery m [String] + +-- | Only language related options, i.e. @-XSomeExtension@ +ghcLangOptions :: MonadIO m => ComponentQuery m [String] + +packageId = Query $ getPackageId +packageDbStack = Query $ slbiPackageDbStack `liftM` getSlbi +packageFlags = Query $ slbiPackageFlags `liftM` getSlbi +packageLicenses = Query $ slbiPkgLicenses `liftM` getSlbi +compilerVersion = Query $ slbiCompilerVersion `liftM` getSlbi +ghcMergedPkgOptions = Query $ slbiGhcMergedPkgOptions `liftM` getSlbi +configFlags = Query $ slbiConfigFlags `liftM` getSlbi +nonDefaultConfigFlags = Query $ slbiNonDefaultConfigFlags `liftM` getSlbi + +ghcSrcOptions = ComponentQuery $ Query $ slbiGhcSrcOptions `liftM` getSlbi +ghcPkgOptions = ComponentQuery $ Query $ slbiGhcPkgOptions `liftM` getSlbi +ghcOptions = ComponentQuery $ Query $ slbiGhcOptions `liftM` getSlbi +ghcLangOptions = ComponentQuery $ Query $ slbiGhcLangOptions `liftM` getSlbi +sourceDirs = ComponentQuery $ Query $ slbiSourceDirs `liftM` getSlbi +entrypoints = ComponentQuery $ Query $ slbiEntrypoints `liftM` getSlbi + +-- | Run @cabal configure@ +reconfigure :: MonadIO m + => (FilePath -> [String] -> String -> IO String) + -> Programs -- ^ Program paths + -> [String] -- ^ Command line arguments to be passed to @cabal@ + -> m () +reconfigure readProc progs cabalOpts = do + let progOpts = + [ "--with-ghc=" ++ ghcProgram progs ] + -- Only pass ghc-pkg if it was actually set otherwise we + -- might break cabal's guessing logic + ++ if ghcPkgProgram progs /= "ghc-pkg" + then [ "--with-ghc-pkg=" ++ ghcPkgProgram progs ] + else [] + ++ cabalOpts + _ <- liftIO $ readProc (cabalProgram progs) ("configure":progOpts) "" + return () + +readHelper :: (MonadIO m, MonadQuery m) => [String] -> m [Maybe ChResponse] +readHelper args = ask >>= \qe -> liftIO $ do + out <- either error id <$> invokeHelper qe args + let res = read out + liftIO $ evaluate res `E.catch` \se@(SomeException _) -> do + md <- lookupEnv' "CABAL_HELPER_DEBUG" + let msg = "readHelper: exception: '" ++ show se ++ "'" + error $ msg ++ case md of + Nothing -> ", for more information set the environment variable CABAL_HELPER_DEBUG" + Just _ -> ", output: '"++ out ++"'" + +invokeHelper :: QueryEnv -> [String] -> IO (Either String String) +invokeHelper QueryEnv {..} args = do + let progArgs = [ "--with-ghc=" ++ ghcProgram qePrograms + , "--with-ghc-pkg=" ++ ghcPkgProgram qePrograms + , "--with-cabal=" ++ cabalProgram qePrograms + ] + exe <- findLibexecExe + let args' = progArgs ++ qeProjectDir:qeDistDir:args + out <- qeReadProcess exe args' "" + (Right <$> evaluate out) `E.catch` \(SomeException _) -> + return $ Left $ concat + ["invokeHelper", ": ", exe, " " + , intercalate " " (map show args') + , " failed" + ] + +getPackageId :: MonadQuery m => m (String, Version) +getPackageId = ask >>= \QueryEnv {..} -> do + [ Just (ChResponseVersion pkgName pkgVer) ] <- readHelper [ "package-id" ] + return (pkgName, pkgVer) + +getSomeConfigState :: MonadQuery m => m SomeLocalBuildInfo +getSomeConfigState = ask >>= \QueryEnv {..} -> do + res <- readHelper + [ "package-db-stack" + , "flags" + , "licenses" + , "compiler-version" + + , "ghc-merged-pkg-options" + + , "config-flags" + , "non-default-config-flags" + + , "ghc-src-options" + , "ghc-pkg-options" + , "ghc-lang-options" + , "ghc-options" + + , "source-dirs" + , "entrypoints" + ] + let [ Just (ChResponsePkgDbs slbiPackageDbStack), + Just (ChResponseFlags slbiPackageFlags), + Just (ChResponseLicenses slbiPkgLicenses), + Just (ChResponseVersion comp compVer), + + Just (ChResponseList slbiGhcMergedPkgOptions), + + Just (ChResponseFlags slbiConfigFlags), + Just (ChResponseFlags slbiNonDefaultConfigFlags), + + Just (ChResponseCompList slbiGhcSrcOptions), + Just (ChResponseCompList slbiGhcPkgOptions), + Just (ChResponseCompList slbiGhcLangOptions), + Just (ChResponseCompList slbiGhcOptions), + + Just (ChResponseCompList slbiSourceDirs), + Just (ChResponseEntrypoints slbiEntrypoints) + ] = res + slbiCompilerVersion = (comp, compVer) + return $ SomeLocalBuildInfo {..} + + +-- | Make sure the appropriate helper executable for the given project is +-- installed and ready to run queries. +prepare :: MonadIO m => QueryEnv -> m () +prepare qe = + liftIO $ void $ invokeHelper qe [] + +-- | Create @cabal_macros.h@ and @Paths_\<pkg\>@ possibly other generated files +-- in the usual place. +writeAutogenFiles :: MonadIO m => QueryEnv -> m () +writeAutogenFiles qe = + liftIO $ void $ invokeHelper qe ["write-autogen-files"] + +-- | Get the path to the sandbox package-db in a project +getSandboxPkgDb :: (FilePath -> [String] -> String -> IO String) + -> FilePath + -- ^ Cabal build platform, i.e. @buildPlatform@ + -> Version + -- ^ GHC version (@cProjectVersion@ is your friend) + -> IO (Maybe FilePath) +getSandboxPkgDb readProc = + CabalHelper.Shared.Sandbox.getSandboxPkgDb $ unsafePerformIO $ buildPlatform readProc + +buildPlatform :: (FilePath -> [String] -> String -> IO String) -> IO String +buildPlatform readProc = do + exe <- findLibexecExe + CabalHelper.Shared.Sandbox.dropWhileEnd isSpace <$> readProc exe ["print-build-platform"] "" + +-- | This exception is thrown by all 'runQuery' functions if the internal +-- wrapper executable cannot be found. You may catch this and present the user +-- an appropriate error message however the default is to print +-- 'libexecNotFoundError'. +data LibexecNotFoundError = LibexecNotFoundError String FilePath + deriving (Typeable) +instance Exception LibexecNotFoundError +instance Show LibexecNotFoundError where + show (LibexecNotFoundError exe dir) = + libexecNotFoundError exe dir "https://github.com/DanielG/cabal-helper/issues" + +findLibexecExe :: IO FilePath +findLibexecExe = do + libexecdir <- getLibexecDir + let exeName = "cabal-helper-wrapper" + exe = libexecdir </> exeName FP.<.> exeExtension' + + exists <- doesFileExist exe + + if exists + then return exe + else do + mdir <- tryFindCabalHelperTreeLibexecDir + case mdir of + Nothing -> + error $ throw $ LibexecNotFoundError exeName libexecdir + Just dir -> + return $ dir </> "dist" </> "build" </> exeName </> exeName + +tryFindCabalHelperTreeLibexecDir :: IO (Maybe FilePath) +tryFindCabalHelperTreeLibexecDir = do + exe <- getExecutablePath' + dir <- case takeFileName exe of + "ghc" -> do -- we're probably in ghci; try CWD + getCurrentDirectory + _ -> + return $ (!!4) $ iterate takeDirectory exe + exists <- doesFileExist $ dir </> "cabal-helper.cabal" + return $ if exists + then Just dir + else Nothing + +libexecNotFoundError :: String -- ^ Name of the executable we were trying to + -- find + -> FilePath -- ^ Path to @$libexecdir@ + -> String -- ^ URL the user will be directed towards to + -- report a bug. + -> String +libexecNotFoundError exe dir reportBug = printf + ( "Could not find $libexecdir/%s\n" + ++"\n" + ++"If you are a cabal-helper developer you can set the environment variable\n" + ++"`cabal_helper_libexecdir' to override $libexecdir[1]. The following will\n" + ++"work in the cabal-helper source tree:\n" + ++"\n" + ++" $ export cabal_helper_libexecdir=$PWD/dist/build/%s\n" + ++"\n" + ++"[1]: %s\n" + ++"\n" + ++"If you don't know what I'm talking about something went wrong with your\n" + ++"installation. Please report this problem here:\n" + ++"\n" + ++" %s") exe exe dir reportBug + +getExecutablePath' :: IO FilePath +getExecutablePath' = +#if MIN_VERSION_base(4,6,0) + getExecutablePath +#else + getProgName +#endif + +lookupEnv' :: String -> IO (Maybe String) +lookupEnv' k = lookup k <$> getEnvironment + +exeExtension' :: FilePath +exeExtension' = Distribution.Simple.BuildPaths.exeExtension |