module HsDev.Project (
module HsDev.Project.Types,
infoSourceDirsDef, targetFiles, projectTargetFiles,
analyzeCabal,
readProject, loadProject,
withExtensions,
fileInTarget, fileTarget, fileTargets, findSourceDir, sourceDirs,
targetOpts,
showExtension, flagExtension, extensionFlag,
extensionsOpts
) where
import Control.Arrow
import Control.Lens hiding ((.=), (%=), (<.>), set')
import Control.Monad.Except
import Control.Monad.Loops
import Data.List
import Data.Maybe
import Data.Text (Text, pack, unpack)
import qualified Data.Text as T (intercalate)
import Data.Text.Lens (unpacked)
import Distribution.Compiler (CompilerFlavor(GHC))
import qualified Distribution.Package as P
import qualified Distribution.PackageDescription as PD
import qualified Distribution.ModuleName as PD (toFilePath)
import Distribution.PackageDescription.Parse
import Distribution.ModuleName (components)
import Distribution.Text (display)
import Language.Haskell.Extension
import System.FilePath
import System.Log.Simple hiding (Level(..))
import qualified System.Log.Simple as Log (Level(..))
import Text.Format
import System.Directory.Paths
import HsDev.Project.Compat
import HsDev.Project.Types
import HsDev.Error
import HsDev.Util
infoSourceDirsDef :: Lens' Info [Path]
infoSourceDirsDef = lens get' set' where
get' i = case _infoSourceDirs i of
[] -> ["."]
dirs -> dirs
set' i ["."] = i { _infoSourceDirs = [] }
set' i dirs = i { _infoSourceDirs = dirs }
targetFiles :: Target t => t -> [Path]
targetFiles target' = concat [
maybeToList (targetMain target'),
map toFile $ targetModules target',
map toFile $ target' ^.. buildInfo . infoOtherModules . each]
where
toFile ps = fromFilePath (joinPath (ps ^.. each . unpacked) <.> "hs")
projectTargetFiles :: (MonadLog m, Target t) => Project -> t -> m [Path]
projectTargetFiles proj t = do
liftM concat $ forM files $ \file' -> do
candidate <- liftIO $ firstM (fileExists . absolutise (proj ^. projectPath)) [subPath srcDir file' | srcDir <- srcDirs]
case candidate of
Nothing -> do
sendLog Log.Warning $ "Unable to locate source file: {} in source-dirs: {}" ~~ file' ~~ (T.intercalate ", " srcDirs)
return []
Just file'' -> return [normPath file'']
where
files = targetFiles t
srcDirs = t ^.. buildInfo . infoSourceDirsDef . each
analyzeCabal :: String -> Either String ProjectDescription
analyzeCabal source = case liftM flattenDescr $ parsePackageDesc source of
ParseOk _ r -> Right ProjectDescription {
_projectVersion = pack $ showVer $ P.pkgVersion $ PD.package r,
_projectLibrary = fmap toLibrary $ PD.library r,
_projectExecutables = fmap toExecutable $ PD.executables r,
_projectTests = fmap toTest $ PD.testSuites r }
ParseFailed e -> Left $ "Parse failed: " ++ show e
where
toLibrary lib = Library (map (map pack . components) $ PD.exposedModules lib) (toInfo $ PD.libBuildInfo lib)
toExecutable exe = Executable (componentName $ PD.exeName exe) (fromFilePath $ PD.modulePath exe) (toInfo $ PD.buildInfo exe)
toTest test = Test (componentName $ PD.testName test) (testSuiteEnabled test) (fmap fromFilePath mainFile) (toInfo $ PD.testBuildInfo test) where
mainFile = case PD.testInterface test of
PD.TestSuiteExeV10 _ fpath -> Just fpath
PD.TestSuiteLibV09 _ mname -> Just $ PD.toFilePath mname
_ -> Nothing
toInfo info = Info {
_infoDepends = map pkgName (PD.targetBuildDepends info),
_infoLanguage = PD.defaultLanguage info,
_infoExtensions = PD.defaultExtensions info ++ PD.otherExtensions info ++ PD.oldExtensions info,
_infoGHCOptions = maybe [] (map pack) $ lookup GHC (PD.options info),
_infoSourceDirs = map pack $ PD.hsSourceDirs info,
_infoOtherModules = map (map pack . components) (PD.otherModules info) }
pkgName :: P.Dependency -> Text
pkgName (P.Dependency dep _) = pack $ P.unPackageName dep
flattenDescr :: PD.GenericPackageDescription -> PD.PackageDescription
flattenDescr gpkg = pkg {
PD.library = flip fmap mlib $ flattenCondTree
(insertInfo PD.libBuildInfo (\i l -> l { PD.libBuildInfo = i })),
PD.executables = flip fmap mexes $
second (flattenCondTree (insertInfo PD.buildInfo (\i l -> l { PD.buildInfo = i }))) >>>
(\(n, e) -> e { PD.exeName = n }),
PD.testSuites = flip fmap mtests $
second (flattenCondTree (insertInfo PD.testBuildInfo (\i l -> l { PD.testBuildInfo = i }))) >>>
(\(n, t) -> t { PD.testName = n }) }
where
pkg = PD.packageDescription gpkg
mlib = PD.condLibrary gpkg
mexes = PD.condExecutables gpkg
mtests = PD.condTestSuites gpkg
insertInfo :: (a -> PD.BuildInfo) -> (PD.BuildInfo -> a -> a) -> [P.Dependency] -> a -> a
insertInfo f s deps' x = s ((f x) { PD.targetBuildDepends = deps' }) x
readProject :: FilePath -> IO Project
readProject file' = do
source <- readFile file'
length source `seq` either (hsdevError . InspectCabalError file') (return . mkProject) $ analyzeCabal source
where
mkProject desc = (project file') {
_projectDescription = Just desc }
loadProject :: Project -> IO Project
loadProject p
| isJust (_projectDescription p) = return p
| otherwise = readProject (_projectCabal p ^. path)
withExtensions :: a -> Info -> Extensions a
withExtensions x i = Extensions {
_extensions = _infoExtensions i,
_ghcOptions = _infoGHCOptions i,
_entity = x }
fileInTarget :: Path -> Info -> Bool
fileInTarget src info = any (`isParent` src) $ view infoSourceDirsDef info
fileTarget :: Project -> Path -> Maybe Info
fileTarget p f = listToMaybe $ fileTargets p f
fileTargets :: Project -> Path -> [Info]
fileTargets p f = case filter ((`isParent` f') . view executablePath) exes of
[] -> filter (f' `fileInTarget`) (p ^.. projectDescription . _Just . infos)
exes' -> map _executableBuildInfo exes'
where
f' = relPathTo (_projectPath p) f
exes = p ^. projectDescription . _Just . projectExecutables
findSourceDir :: Project -> Path -> Maybe (Extensions Path)
findSourceDir p f = do
info <- listToMaybe $ fileTargets p f
fmap (`withExtensions` info) $ listToMaybe $ filter (`isParent` f) $ map (_projectPath p `subPath`) (info ^. infoSourceDirsDef)
sourceDirs :: ProjectDescription -> [Extensions Path]
sourceDirs = ordNub . concatMap dirs . toListOf infos where
dirs i = map (`withExtensions` i) (i ^. infoSourceDirsDef)
targetOpts :: Info -> [String]
targetOpts info' = concat [
["-i" ++ unpack s | s <- _infoSourceDirs info'],
extensionsOpts $ withExtensions () info',
["-package " ++ unpack p | p <- _infoDepends info']]
showExtension :: Extension -> String
showExtension = display
flagExtension :: String -> Maybe String
flagExtension = stripPrefix "-X"
extensionFlag :: String -> String
extensionFlag = ("-X" ++)
extensionsOpts :: Extensions a -> [String]
extensionsOpts e = map (extensionFlag . showExtension) (_extensions e) ++ map unpack (_ghcOptions e)