{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE CPP #-} module Devel ( devel ) where import qualified Distribution.Simple.Utils as D import qualified Distribution.Verbosity as D import qualified Distribution.Package as D import qualified Distribution.PackageDescription.Parse as D import qualified Distribution.PackageDescription as D import Control.Concurrent (forkIO, threadDelay) import qualified Control.Exception as Ex import Control.Monad (forever) import qualified Data.List as L import qualified Data.Map as Map import Data.Maybe (listToMaybe) import qualified Data.Text as T import qualified Data.Text.IO as T import System.Directory (createDirectoryIfMissing, removeFile, doesFileExist, getDirectoryContents) import System.Exit (exitFailure, exitSuccess) import System.Posix.Types (EpochTime) import System.PosixCompat.Files (modificationTime, getFileStatus) import System.Process (runCommand, terminateProcess, waitForProcess, rawSystem) import Text.Shakespeare.Text (st) import Build (recompDeps, getDeps,findHaskellFiles) import Control.Monad (unless) #if __GLASGOW_HASKELL__ >= 700 #define ST st #else #define ST $st #endif lockFile :: FilePath lockFile = "dist/devel-terminate" writeLock :: IO () writeLock = do createDirectoryIfMissing True "dist" writeFile lockFile "" removeLock :: IO () removeLock = try_ (removeFile lockFile) devel :: Bool -> IO () devel isDevel = do writeLock putStrLn "Yesod devel server. Press ENTER to quit" _ <- forkIO $ do cabal <- D.findPackageDesc "." gpd <- D.readPackageDescription D.normal cabal let pid = (D.package . D.packageDescription) gpd checkCabalFile gpd _ <- if isDevel then rawSystem "cabal-dev" [ "configure" , "--cabal-install-arg=-fdevel" -- legacy , "--cabal-install-arg=-flibrary-only" , "--disable-library-profiling" ] else rawSystem "cabal" [ "configure" , "-fdevel" -- legacy , "-flibrary-only" , "--disable-library-profiling" ] exists <- doesFileExist "dist/devel.hs" unless exists $ T.writeFile "dist/devel.hs" (develFile pid) mainLoop isDevel _ <- getLine writeLock exitSuccess mainLoop :: Bool -> IO () mainLoop isDevel = forever $ do putStrLn "Rebuilding application..." recompDeps list <- getFileList _ <- if isDevel then rawSystem "cabal-dev" ["build"] else rawSystem "cabal" ["build"] removeLock putStrLn "Starting development server..." pkg <- pkgConfigs isDevel ph <- runCommand $ concat ["runghc ", pkg, " dist/devel.hs"] watchTid <- forkIO . try_ $ do watchForChanges list putStrLn "Stopping development server..." writeLock threadDelay 1000000 putStrLn "Terminating development server..." terminateProcess ph ec <- waitForProcess ph putStrLn $ "Exit code: " ++ show ec Ex.throwTo watchTid (userError "process finished") watchForChanges list try_ :: forall a. IO a -> IO () try_ x = (Ex.try x :: IO (Either Ex.SomeException a)) >> return () pkgConfigs :: Bool -> IO String pkgConfigs isDev | isDev = do devContents <- getDirectoryContents "cabal-dev" let confs = filter isConfig devContents return . unwords $ inplacePkg : map ("-package-confcabal-dev/"++) confs | otherwise = return inplacePkg where inplacePkg = "-package-confdist/package.conf.inplace" isConfig dir = "packages-" `L.isPrefixOf` dir && ".conf" `L.isSuffixOf` dir type FileList = Map.Map FilePath EpochTime getFileList :: IO FileList getFileList = do files <- findHaskellFiles "." deps <- getDeps let files' = files ++ map fst (Map.toList deps) fmap Map.fromList $ flip mapM files' $ \f -> do fs <- getFileStatus f return (f, modificationTime fs) watchForChanges :: FileList -> IO () watchForChanges list = do newList <- getFileList if list /= newList then return () else threadDelay 1000000 >> watchForChanges list showPkgName :: D.PackageId -> String showPkgName = (\(D.PackageName n) -> n) . D.pkgName develFile :: D.PackageId -> T.Text develFile pid = [ST| {-# LANGUAGE PackageImports #-} import "#{showPkgName pid}" Application (withDevelAppPort) import Data.Dynamic (fromDynamic) import Network.Wai.Handler.Warp (runSettings, defaultSettings, settingsPort, settingsHost) import Data.Maybe (fromJust) import Control.Concurrent (forkIO) import System.Directory (doesFileExist, removeFile) import System.Exit (exitSuccess) import Control.Concurrent (threadDelay) main :: IO () main = do putStrLn "Starting devel application" wdap <- (return . fromJust . fromDynamic) withDevelAppPort forkIO . wdap $ \(port, app) -> runSettings defaultSettings { settingsPort = port , settingsHost = "0.0.0.0" } app loop loop :: IO () loop = do threadDelay 100000 e <- doesFileExist "dist/devel-terminate" if e then terminateDevel else loop terminateDevel :: IO () terminateDevel = exitSuccess |] checkCabalFile :: D.GenericPackageDescription -> IO () checkCabalFile gpd = case D.condLibrary gpd of Nothing -> do putStrLn "Error: incorrect cabal file, no library" exitFailure Just ct -> case lookupDevelLib ct of Nothing -> do putStrLn "Error: no library configuration for -fdevel" exitFailure Just dLib -> case (D.hsSourceDirs . D.libBuildInfo) dLib of [] -> return () ["."] -> return () _ -> putStrLn $ "WARNING: yesod devel may not work correctly with " ++ "custom hs-source-dirs" lookupDevelLib :: D.CondTree D.ConfVar c a -> Maybe a lookupDevelLib ct = listToMaybe . map (\(_,x,_) -> D.condTreeData x) . filter isDevelLib . D.condTreeComponents $ ct where isDevelLib ((D.Var (D.Flag (D.FlagName f))), _, _) = f `elem` ["library-only", "devel"] isDevelLib _ = False