cabal-ghci-0.3: Distribution/Dev/Interactive.hs
-- | Retrieve ghci options for your cabal project
module Distribution.Dev.Interactive (
-- * Installing into your .ghci
-- $install
-- * Arguments
-- $args
-- * Exported functions
cabalSet, packageOpts, loadCabal, lookForCabalFile, withOpts, LoadCabalRet(..)
) where
import Distribution.Text (display)
import Distribution.Compiler (buildCompilerFlavor, CompilerId(..))
import Distribution.Verbosity (normal)
import Distribution.System (buildPlatform)
import Distribution.Package (PackageName(..), Dependency(..))
import Distribution.PackageDescription (
FlagName(..), FlagAssignment, BuildInfo(..), hcOptions, allExtensions,
PackageDescription(..), Executable(..), allBuildInfo)
import Distribution.PackageDescription.Parse (readPackageDescription)
import Distribution.PackageDescription.Configuration (
finalizePackageDescription)
import Distribution.Simple.PackageIndex (lookupDependency)
import Distribution.Simple.LocalBuildInfo (LocalBuildInfo, installedPkgs)
import Data.Version (Version, showVersion)
import Distribution.Simple.BuildPaths (autogenModulesDir, cppHeaderName)
import System.FilePath (takeDirectory, (</>), takeExtension)
import System.Directory (
getDirectoryContents, getCurrentDirectory, canonicalizePath)
import System.Info (compilerVersion)
import Data.Maybe (listToMaybe, fromMaybe)
import Control.Exception (try, SomeException)
type Deps = [(PackageName, Version)]
-- | Return value for 'loadCabal'
data LoadCabalRet =
NoCabalFile | -- ^ No cabal file found
MissingDeps [Dependency] | -- ^ Missing dependencies
Pkg FilePath PackageDescription (Maybe LocalBuildInfo) -- ^ Successful loading and parsing of cabal file
deriving Show
compiler ∷ CompilerId
compiler = CompilerId buildCompilerFlavor compilerVersion
-- | Build a list of ghci options needed to load files from a cabal project
packageOpts
∷ FilePath -- ^ path to the .cabal file
→ PackageDescription -- ^ parsed package description
→ Maybe LocalBuildInfo -- ^ parsed build config
→ String -- ^ name of executable
→ Maybe [String]
packageOpts path pkg mlbi executable =
maybe Nothing (\bi → Just $ ghcOpts path bi mlbi (listDeps bi =<< mlbi)) $
listToMaybe $
if executable == ""
then allBuildInfo pkg
else
fmap buildInfo .
filter (\x → exeName x == executable) .
executables $ pkg
listDeps ∷ BuildInfo → LocalBuildInfo → Maybe Deps
listDeps bi lbi = sequence $ map find reqs
where
reqs = targetBuildDepends bi
db = installedPkgs lbi
find dep@(Dependency pkg _) = do
(fst . head → ver) ← return $ lookupDependency db dep
return (pkg, ver)
-- | GHC options for a 'BuildInfo'
ghcOpts ∷ FilePath → BuildInfo → Maybe LocalBuildInfo → Maybe Deps → [String]
ghcOpts path bi mlbi deps = filter validGHCiFlag $ concat $
maybe [] ((noPkgs:) . add "-package=" . map addDep) deps :
map ($ bi) [
hcOptions buildCompilerFlavor,
addf "-X" display . allExtensions,
addf "-i" (dir </>) . (autogendir:) . hsSourceDirs,
add "-optP" . cppOptions,
add "-optc" . ccOptions,
add "-optl" . ldOptions,
const ["-optP-include", "-optP" ++ (autogendir </> cppHeaderName)]
-- Other cabal settings currently ignored by cabal-ghci:
-- frameworks cSources otherModules extraLibs extraLibsDirs includes
]
where
autogendir
| Just lbi ← mlbi = autogenModulesDir lbi
| otherwise = "dist/build/autogen"
dir
| s@(_:_) ← takeDirectory path = s
| otherwise = "."
add s = map (s++)
addf ∷ String → (a → String) → [a] → [String]
addf s f = map ((s++) . f)
noPkgs = "-hide-all-packages"
addDep (PackageName pkg, showVersion → ver) = pkg ++ "-" ++ ver
-- flags sensible for GHCi
validGHCiFlag "-O" = False
validGHCiFlag ['-','O',n] | n `elem` ['0'..'9'] = False
validGHCiFlag "-debug" = False
validGHCiFlag "-rtsopts" = False
validGHCiFlag "-threaded" = False
validGHCiFlag "-ticky" = False
validGHCiFlag _ = True
-- | Load the current cabal project file and parse it
loadCabal
∷ FilePath -- ^ usually the current directory
→ FlagAssignment -- ^ list of cabal flag assignments
→ IO LoadCabalRet
loadCabal path flags = do
mCabalFile ← lookForCabalFile =<< canonicalizePath path
flip (maybe (return NoCabalFile))
mCabalFile $ \cabalFile → do
gdescr ← readPackageDescription normal cabalFile
mlbi ← loadCabalConfig (takeDirectory cabalFile </> "dist" </> "setup-config")
case finalizePackageDescription flags (const True)
buildPlatform compiler [] gdescr of
Left deps → return $ MissingDeps deps
Right (descr, _) → return $ Pkg cabalFile descr mlbi
maybeNth ∷ Int → [a] → Maybe a
maybeNth 0 (x:_) = Just x
maybeNth n (_:xs) = maybeNth (n-1) xs
maybeNth _ _ = Nothing
maybeRead ∷ Read a ⇒ String → Maybe a
maybeRead s = case reads s of [(a, "")] → Just a; _ → Nothing
-- | Load the 'LocalBuildInfo' from dist/setup-config
loadCabalConfig ∷ FilePath → IO (Maybe LocalBuildInfo)
loadCabalConfig path =
-- TODO: don't ignore all exceptions
fmap (either ignore id) $ try $
fmap ((maybeRead =<<) . maybeNth 1 . lines) $ readFile path
where
ignore ∷ SomeException → Maybe a
ignore _ = Nothing
-- | Find a .cabal file in the path or any of it's parent directories
lookForCabalFile
∷ FilePath -- ^ canonicalised path
→ IO (Maybe FilePath)
lookForCabalFile "/" = return Nothing
lookForCabalFile path = do
files ← getDirectoryContents path
let cabals = filter (\f →
takeExtension f == ".cabal"
&& f /= ".cabal") files
case cabals of
[] → lookForCabalFile (takeDirectory path)
[a] → return $ Just $ path </> a
_ → return Nothing
-- | 'cabalSet' returns a list of ghci commands (seperated by newlines) that :set the 'packageOpts' of the current cabal project
cabalSet
∷ String -- ^ arguments seperated by spaces
→ IO String
cabalSet args =
withOpts (words args)
(\x → putStrLn x >> return "")
((\x → putStrLn x >> return x) .
unlines . (map (":set "++)) . map show )
-- | Generalised version of 'cabalSet'
withOpts
∷ [String] -- ^ List of cabal flag arguments and executable name
→ (String → IO a) -- ^ Error continuation. Recieves an error message.
→ ([String] → IO a) -- ^ Success continuation. Recieves a list of ghci arguments.
→ IO a
withOpts args err go = do
let (flags, executable) = parseArgs args
here ← getCurrentDirectory
ret ← loadCabal here flags
case ret of
NoCabalFile → err "Current directory is not a cabal project"
MissingDeps deps → err $ "Missing dependencies: " ++ unwords (map show deps)
Pkg path descr mlbi → do
let mopts = packageOpts path descr mlbi executable
case mopts of
Nothing → err (
if executable /= ""
then "No such executable in cabal file"
else "No library defined in cabal file")
Just opts → go opts
parseArgs ∷ [String] → (FlagAssignment, String)
parseArgs args =
(map (makeFlag . drop 2) . filter flag $ args,
fromMaybe "" . listToMaybe . filter (not . flag) $ args)
where
flag x = take 2 x == "-f"
makeFlag ∷ String → (FlagName, Bool)
makeFlag ('-':f) = (FlagName f, False)
makeFlag f = (FlagName f, True)
-- $install
-- @
-- $ head -n 4 >> ~/.ghci
-- :m + Distribution.Dev.Interactive
-- :def cabalset cabalSet
-- :cabalset -fcabal-ghci
-- :m - Distribution.Dev.Interactive
-- @
--
-- After you've added those lines into your .ghci file, use the @:cabalset@
-- command to reload your .cabal file.
-- $args
-- [@-fflag@] enable flag
--
-- [@-f-flag@] disable flag
--
-- [@exec@] load options for the exec executable