{-| Module : Devel.Compile Description : Attempts to compile the WAI application. Copyright : (c) 2015 Njagi Mwaniki License : MIT Maintainer : njagi@urbanslug.com Stability : experimental Portability : POSIX Compile compiles the app to give: Either a list of source errors or an ide-backend session. -} {-# LANGUAGE OverloadedStrings #-} module Devel.Compile ( initCompile , compile , finishCompile ) where -- The backbone library of ide-backend. -- Almost everything is dependent on ide-backend. import IdeSession -- From Cabal-ide-backend -- for parsing the cabal file and extracting lang extensions used. import Distribution.PackageDescription import Distribution.ModuleName import Distribution.PackageDescription.Parse import Distribution.PackageDescription.Configuration import Language.Haskell.Extension -- Used internally for showing errors. import Data.Text (unpack) -- Utility functions import Data.Monoid ((<>)) -- Local imports import Devel.Paths import Devel.Types import System.FilePath.Posix (takeExtension, pathSeparator) import System.Directory (doesFileExist) import Data.List (union, delete, isInfixOf, nub, (\\)) import Data.Maybe (fromMaybe) import Control.Monad (filterM) import Data.IORef -- |Initialize the compilation process. initCompile :: [String] -> SessionConfig -> Maybe IdeSession -> IO (IdeSession, [GhcExtension], [FilePath], [FilePath]) initCompile watchDirectories sessionConfig mSession = do -- Initialize the session session <- case mSession of Just session -> return session Nothing -> initSession defaultSessionInitParams sessionConfig -- This is "rebuilding" the cabal file. (extensionList, srcDir, cabalSrcList) <- getExtensions sourceList <- getSourceList srcDir cabalSrcList additionalWatchList <- fmap concat (mapM getRecursiveContents watchDirectories) return (session, extensionList, sourceList, additionalWatchList) getSourceList :: [FilePath] -> [FilePath] -> IO [FilePath] getSourceList srcDir cabalSrcList = do fileList' <- mapM getRecursiveContents srcDir let -- Remove duplicate values. fileList = foldr union [] fileList' -- Remove files in test dir. fileListNoTests = filter (not.(\x -> isInfixOf "test/" x || isInfixOf "cabal-sandbox/" x || isInfixOf "stack-work/" x)) fileList -- Add both lists together. fileListCombined = fileListNoTests ++ cabalSrcList -- Remove all non hs and non .lhs files. -- also remove "app/devel.hs" because it has an extra main function sourceList' = filter (\f -> let ext = takeExtension f in ext == ".lhs" || ext == ".hs") fileListCombined -- nub to remove duplicates yet again. sourceList = nub $ delete "app/DevelMain.hs" $ delete "app/devel.hs" sourceList' -- Drop leading "./" in filepaths with `map (\x -> x \\ "./") sourceList` return $ map (\x -> x \\ "./") sourceList -- sourceList -- | Determining the target files and creating the session update. compile :: IdeSession -> FilePath -> [GhcExtension] -> [FilePath] -> IO (IdeSession, IdeSessionUpdate) compile session buildFile extensionList sourceList = do -- Description of session updates. let targetList = TargetsInclude (if buildFile `elem` sourceList then sourceList else buildFile : sourceList) :: Targets update = updateTargets targetList <> updateCodeGeneration True <> updateGhcOpts (["-Wall", "-ddump-hi", "-ddump-to-file"] ++ extensionList) return (session, update) -- |Finalising compilation process. finishCompile :: (IdeSession, IdeSessionUpdate) -> IORef [String] -> IO (Either [SourceError'] IdeSession) finishCompile (session, update) iStrLst = do let printer upLst = do lstt <- readIORef iStrLst let str = show upLst newLst = lstt ++ [str] _ <- putStrLn str writeIORef iStrLst newLst _ <- updateSession session update printer -- Customizing error showing. errorList' <- getSourceErrors session let errorList = case filterErrors errorList' of [] -> [] _ -> prettyPrintErrors errorList' -- We still want to see errors and warnings on the terminal. let prettyErrors = prettyPrintErrors errorList' errorsToBrowser = do lstt <- readIORef iStrLst writeIORef iStrLst $ lstt ++ prettyErrors _ <- errorsToBrowser mapM_ putStrLn $ prettyErrors return $ case errorList of [] -> Right session _ -> Left errorList -- ----------------------------------------------------------- -- Utility functions. -- ----------------------------------------------------------- -- | Parse the cabal file to get the ghc extensions in use. getExtensions :: IO ([GhcExtension], [FilePath], [FilePath]) getExtensions = do cabalFilePath <- getCabalFile cabalFile <- readFile cabalFilePath let unsafePackageDescription = parsePackageDescription cabalFile genericPackageDescription = case unsafePackageDescription of ParseOk _ a -> a _ -> error "failed package description." packDescription = flattenPackageDescription genericPackageDescription rawExt = usedExtensions $ head $ allBuildInfo packDescription lib = fromMaybe emptyLibrary $ library packDescription -- I think it would be wise to avoid src files under executable to avoid conflict. srcDir = hsSourceDirs $ libBuildInfo lib srcList = extraSrcFiles packDescription parseExtension :: Extension -> String parseExtension (EnableExtension extension) = "-X" ++ show extension parseExtension (DisableExtension extension) = "-XNo" ++ show extension parseExtension (UnknownExtension extension) = "-X" ++ show extension extensions = map parseExtension rawExt -- Now we handle source files from executable section here. execList = executables packDescription paths <- mapM getPathList execList return (extensions, srcDir, (srcList ++ (concat paths))) where getPathList :: Executable -> IO [FilePath] getPathList exec = let mainModule' = modulePath exec bInfo = buildInfo exec execModuleList = otherModules bInfo srcDirsList = hsSourceDirs bInfo execSrcFileList = map toFilePath execModuleList nonPaths = [dir ++ (pathSeparator : fp) | fp <- execSrcFileList, dir <- srcDirsList] paths' = map (++ (pathSeparator : mainModule') ) srcDirsList -- For the main module ++ [x++y | x <- nonPaths, y <- [".hs", ".lhs"]] in filterM doesFileExist paths' -- | Remove the warnings from [SourceError] if any. -- Return an empty list if there are no errors and only warnings -- Return non empty list if there are errors. filterErrors :: [SourceError] -> [SourceError] filterErrors [] = [] filterErrors (x:xs) = case errorKind x of KindWarning -> filterErrors xs _ -> x : filterErrors xs prettyPrintErrors :: [SourceError] -> [SourceError'] prettyPrintErrors [] = [] prettyPrintErrors (x: xs) = case errorKind x of KindWarning -> ("Warning: " ++ show (errorSpan x) ++ " " ++ unpack (errorMsg x)) : prettyPrintErrors xs KindError -> ("Error: " ++ show (errorSpan x) ++ " " ++ unpack (errorMsg x)) : prettyPrintErrors xs KindServerDied -> show (errorKind x) : prettyPrintErrors xs