ghc-mod/cabal-helper/Main.hs

346 lines
12 KiB
Haskell

-- 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/>.
{-# LANGUAGE CPP, BangPatterns, RecordWildCards #-}
{-# OPTIONS_GHC -fno-warn-deprecations #-}
import Distribution.Simple.Utils (cabalVersion)
import Distribution.Simple.Configure
import Distribution.Package (PackageIdentifier, InstalledPackageId, PackageId)
import Distribution.PackageDescription (PackageDescription,
FlagAssignment,
Executable(..),
Library(..),
TestSuite(..),
Benchmark(..),
BuildInfo(..),
TestSuiteInterface(..),
BenchmarkInterface(..),
withLib)
import Distribution.PackageDescription.Parse (readPackageDescription)
import Distribution.PackageDescription.Configuration (flattenPackageDescription)
import Distribution.Simple.Configure (getPersistBuildConfig)
import Distribution.Simple.LocalBuildInfo (LocalBuildInfo(..),
Component(..),
ComponentName(..),
ComponentLocalBuildInfo(..),
componentBuildInfo,
externalPackageDeps,
withComponentsLBI,
inplacePackageId)
import Distribution.Simple.GHC (componentGhcOptions)
import Distribution.Simple.Program.GHC (GhcOptions(..), renderGhcOptions)
import Distribution.Simple.Setup (ConfigFlags(..),Flag(..))
import Distribution.Simple.Build (initialBuildSteps)
import Distribution.Simple.BuildPaths (autogenModuleName, cppHeaderName, exeExtension)
import Distribution.Simple.Compiler (PackageDB(..))
import Distribution.ModuleName (components)
import qualified Distribution.ModuleName as C (ModuleName)
import Distribution.Text (display)
import Distribution.Verbosity (Verbosity, silent, deafening)
import Control.Applicative ((<$>))
import Control.Monad
import Control.Exception (catch, PatternMatchFail(..))
import Data.List
import Data.Maybe
import Data.Monoid
import Data.IORef
import System.Environment
import System.Directory
import System.FilePath
import System.Exit
import System.IO
import System.IO.Unsafe (unsafeInterleaveIO, unsafePerformIO)
import Text.Printf
import Common
--- \ / These types MUST be in sync with the copies in lib:ghc-mod
data GmComponentName = GmSetupHsName
| GmLibName
| GmExeName String
| GmTestName String
| GmBenchName String
deriving (Eq, Ord, Read, Show)
data GmCabalHelperResponse
= GmCabalHelperStrings [(GmComponentName, [String])]
| GmCabalHelperEntrypoints [(GmComponentName, Either FilePath [ModuleName])]
| GmCabalHelperLbi String
deriving (Read, Show)
--- ^ These types MUST be in sync with the copies in ../Types.hs
-- MUST be compatible to the one in GHC
newtype ModuleName = ModuleName String
deriving (Eq, Ord, Read, Show)
usage = do
prog <- getProgName
hPutStr stderr $ align "(" "|" ("Usage: " ++ prog ++ " " ++ usageMsg)
where
usageMsg = ""
++"DIST_DIR ( version\n"
++" | print-lbi\n"
++" | write-autogen-files\n"
++" | ghc-options [--with-inplace]\n"
++" | ghc-src-options [--with-inplace]\n"
++" | ghc-pkg-options [--with-inplace]\n"
++" | entrypoints\n"
++" | source-dirs\n"
++" ) ...\n"
commands :: [String]
commands = [ "print-bli"
, "write-autogen-files"
, "component-from-file"
, "ghc-options"
, "ghc-src-options"
, "ghc-pkg-options"
, "entrypoints"
, "source-dirs"]
main :: IO ()
main = do
args <- getArgs
distdir:args' <- case args of
[] -> usage >> exitFailure
_ -> return args
ddexists <- doesDirectoryExist distdir
when (not ddexists) $ do
errMsg $ "distdir '"++distdir++"' does not exist"
exitFailure
v <- maybe silent (const deafening) <$> lookupEnv "GHC_MOD_DEBUG"
lbi <- unsafeInterleaveIO $ getPersistBuildConfig distdir
let pd = localPkgDescr lbi
let
-- a =<< b $$ c == (a =<< b) $$ c
-- a <$$> b $$ c == a <$$> (b $$ c)
infixr 2 $$
($$) = ($)
infixr 1 <$$>
(<$$>) = (<$>)
collectCmdOptions :: [String] -> [[String]]
collectCmdOptions =
reverse . map reverse . foldl f [] . dropWhile isOpt
where
isOpt = ("--" `isPrefixOf`)
f [] x = [[x]]
f (a:as) x
| isOpt x = (x:a):as
| otherwise = [x]:(a:as)
let cmds = collectCmdOptions args'
if any (["version"] `isPrefixOf`) cmds
then do
putStrLn $
printf "using version %s of the Cabal library" (display cabalVersion)
exitSuccess
else return ()
print =<< flip mapM cmds $$ \cmd -> do
case cmd of
"write-autogen-files":[] -> do
let pd = localPkgDescr lbi
-- calls writeAutogenFiles
initialBuildSteps distdir pd lbi v
return Nothing
"ghc-options":flags ->
Just . GmCabalHelperStrings <$$> componentsMap lbi v distdir $$
\c clbi bi -> let
outdir = componentOutDir lbi c
(clbi', adopts) = case flags of
["--with-inplace"] -> (clbi, mempty)
[] -> removeInplaceDeps pd clbi
opts = componentGhcOptions v lbi bi clbi' outdir
in
renderGhcOptions (compiler lbi) $ opts `mappend` adopts
"ghc-src-options":flags ->
Just . GmCabalHelperStrings <$$> componentsMap lbi v distdir $$
\c clbi bi -> let
outdir = componentOutDir lbi c
(clbi', adopts) = case flags of
["--with-inplace"] -> (clbi, mempty)
[] -> removeInplaceDeps pd clbi
opts = componentGhcOptions v lbi bi clbi' outdir
comp = compiler lbi
opts' = mempty {
-- Not really needed but "unexpected package db stack: []"
ghcOptPackageDBs = [GlobalPackageDB],
ghcOptCppOptions = ghcOptCppOptions opts,
ghcOptCppIncludePath = ghcOptCppIncludePath opts,
ghcOptCppIncludes = ghcOptCppIncludes opts,
ghcOptFfiIncludes = ghcOptFfiIncludes opts,
ghcOptSourcePathClear = ghcOptSourcePathClear opts,
ghcOptSourcePath = ghcOptSourcePath opts
}
in
renderGhcOptions comp $ opts `mappend` adopts
"ghc-pkg-options":flags ->
Just . GmCabalHelperStrings <$$> componentsMap lbi v distdir $$
\c clbi bi -> let
comp = compiler lbi
outdir = componentOutDir lbi c
(clbi', adopts) = case flags of
["--with-inplace"] -> (clbi, mempty)
[] -> removeInplaceDeps pd clbi
opts = componentGhcOptions v lbi bi clbi' outdir
opts' = mempty {
ghcOptPackageDBs = ghcOptPackageDBs opts,
ghcOptPackages = ghcOptPackages opts,
ghcOptHideAllPackages = ghcOptHideAllPackages opts
}
in
renderGhcOptions (compiler lbi) $ opts' `mappend` adopts
"entrypoints":[] -> do
eps <- componentsMap lbi v distdir $ \c clbi bi ->
componentEntrypoints c
-- MUST append Setup component at the end otherwise CabalHelper gets
-- confused
let eps' = eps ++ [(GmSetupHsName, Right [ModuleName "Setup"])]
return $ Just $ GmCabalHelperEntrypoints eps'
"source-dirs":[] ->
Just . GmCabalHelperStrings <$$> componentsMap lbi v distdir $$
\c clbi bi -> hsSourceDirs bi
"print-lbi":[] ->
return $ Just $ GmCabalHelperLbi $ show lbi
cmd:_ | not (cmd `elem` commands) ->
errMsg ("Unknown command: " ++ cmd) >> usage >> exitFailure
_ ->
errMsg "Invalid usage!" >> usage >> exitFailure
getLibrary :: PackageDescription -> Library
getLibrary pd = unsafePerformIO $ do
lr <- newIORef (error "libraryMap: empty IORef")
withLib pd (writeIORef lr)
readIORef lr
componentsMap :: LocalBuildInfo
-> Verbosity
-> FilePath
-> ( Component
-> ComponentLocalBuildInfo
-> BuildInfo
-> a)
-> IO [(GmComponentName, a)]
componentsMap lbi v distdir f = do
let pd = localPkgDescr lbi
lr <- newIORef []
withComponentsLBI pd lbi $ \c clbi -> do
let bi = componentBuildInfo c
name = componentNameFromComponent c
l' <- readIORef lr
writeIORef lr $ (componentNameToGm name, f c clbi bi):l'
reverse <$> readIORef lr
componentNameToGm CLibName = GmLibName
componentNameToGm (CExeName n) = GmExeName n
componentNameToGm (CTestName n) = GmTestName n
componentNameToGm (CBenchName n) = GmBenchName n
componentNameFromComponent (CLib Library {}) = CLibName
componentNameFromComponent (CExe Executable {..}) = CExeName exeName
componentNameFromComponent (CTest TestSuite {..}) = CTestName testName
componentNameFromComponent (CBench Benchmark {..}) = CBenchName benchmarkName
componentOutDir lbi (CLib Library {..})= buildDir lbi
componentOutDir lbi (CExe Executable {..})= exeOutDir lbi exeName
componentOutDir lbi (CTest TestSuite { testInterface = TestSuiteExeV10 _ _, ..}) =
exeOutDir lbi testName
componentOutDir lbi (CTest TestSuite { testInterface = TestSuiteLibV09 _ _, ..}) =
exeOutDir lbi (testName ++ "Stub")
componentOutDir lbi (CBench Benchmark { benchmarkInterface = BenchmarkExeV10 _ _, ..})=
exeOutDir lbi benchmarkName
gmModuleName :: C.ModuleName -> ModuleName
gmModuleName = ModuleName . intercalate "." . components
componentEntrypoints :: Component -> Either FilePath [ModuleName]
componentEntrypoints (CLib Library {..})
= Right $ map gmModuleName exposedModules
componentEntrypoints (CExe Executable {..})
= Left modulePath
componentEntrypoints (CTest TestSuite { testInterface = TestSuiteExeV10 _ fp })
= Left fp
componentEntrypoints (CTest TestSuite { testInterface = TestSuiteLibV09 _ mn })
= Right [gmModuleName mn]
componentEntrypoints (CTest TestSuite {})
= Right []
componentEntrypoints (CBench Benchmark { benchmarkInterface = BenchmarkExeV10 _ fp})
= Left fp
componentEntrypoints (CBench Benchmark {})
= Left []
exeOutDir :: LocalBuildInfo -> String -> FilePath
exeOutDir lbi exeName =
----- Copied from Distribution/Simple/GHC.hs:buildOrReplExe
-- exeNameReal, the name that GHC really uses (with .exe on Windows)
let exeNameReal = exeName <.>
(if takeExtension exeName /= ('.':exeExtension)
then exeExtension
else "")
targetDir = (buildDir lbi) </> exeName
in targetDir
removeInplaceDeps :: PackageDescription
-> ComponentLocalBuildInfo
-> (ComponentLocalBuildInfo, GhcOptions)
removeInplaceDeps pd clbi = let
(ideps, deps) = partition isInplaceDep (componentPackageDeps clbi)
hasIdeps = not $ null ideps
clbi' = clbi { componentPackageDeps = deps }
lib = getLibrary pd
src_dirs = hsSourceDirs (libBuildInfo lib)
adopts = mempty {
ghcOptSourcePath =
#if CABAL_MAJOR == 1 && CABAL_MINOR >= 22
toNubListR src_dirs
#else
src_dirs
#endif
}
in (clbi', if hasIdeps then adopts else mempty)
where
isInplaceDep :: (InstalledPackageId, PackageId) -> Bool
isInplaceDep (ipid, pid) = inplacePackageId pid == ipid