jbi-0.2.0.0: lib/System/JBI/Commands/Tool.hs
{-# LANGUAGE DeriveAnyClass, DeriveGeneric, OverloadedStrings #-}
{- |
Module : System.JBI.Commands.Tool
Description : Common tooling commands
Copyright : (c) Ivan Lazar Miljenovic
License : MIT
Maintainer : Ivan.Miljenovic@gmail.com
-}
module System.JBI.Commands.Tool where
import System.JBI.Config
import System.JBI.Tagged
import Control.Applicative (liftA2)
import Control.Monad (when)
import Data.Aeson (ToJSON(toJSON))
import Data.Char (isDigit)
import Data.Maybe (listToMaybe)
import Data.String (IsString(..))
import Data.Version (Version, parseVersion)
import GHC.Generics (Generic)
import System.Directory (findExecutable)
import System.Exit (ExitCode(ExitSuccess))
import System.IO (IOMode(WriteMode), hPutStrLn, stderr,
withFile)
import System.Process (CreateProcess(..),
StdStream(Inherit, UseHandle), proc,
readProcessWithExitCode,
showCommandForUser, waitForProcess,
withCreateProcess)
import Text.ParserCombinators.ReadP (eof, readP_to_S)
--------------------------------------------------------------------------------
class Tool t where
commandName :: Tagged t CommandName
commandVersion :: Config -> Tagged t CommandPath -> IO (Maybe (Tagged t Version))
commandVersion = withTaggedF . tryFindVersion
commandPath :: (Tool t) => IO (Maybe (Tagged t CommandPath))
commandPath = withTaggedF findExecutable commandName
commandInformation :: (Tool t) => Config -> IO (Maybe (Installed t))
commandInformation cfg = commandPath >>= mapM getVersion
where
getVersion :: (Tool t') => Tagged t' CommandPath -> IO (Installed t')
getVersion tcp = Installed tcp <$> commandVersion cfg tcp
data GHC
instance Tool GHC where
commandName = "ghc"
--------------------------------------------------------------------------------
newtype CommandName = CommandName { nameOfCommand :: String }
deriving (Eq, Ord, Show, Read)
instance IsString CommandName where
fromString = CommandName
newtype CommandPath = CommandPath { pathToCommand :: FilePath }
deriving (Eq, Ord, Show, Read)
instance ToJSON CommandPath where
toJSON = toJSON . pathToCommand
instance IsString CommandPath where
fromString = CommandPath
data Installed t = Installed
{ path :: !(Tagged t CommandPath)
, version :: !(Maybe (Tagged t Version))
-- ^ Try and determine the version. Only a factor in
-- case any features are version-specific.
} deriving (Eq, Ord, Show, Read, Generic, ToJSON)
--------------------------------------------------------------------------------
-- | Attempt to find the version of the provided command, by assuming
-- it's contained in the first line of the output of @command
-- --version@.
tryFindVersion :: Config -> FilePath -> IO (Maybe Version)
tryFindVersion = tryFindVersionBy findVersion
where
findVersion str = takeVersion (dropWhile (not . isDigit) str)
-- | If we're at the start of a Version, take all of it.
takeVersion :: String -> String
takeVersion = takeWhile (liftA2 (||) isDigit (=='.'))
tryFindVersionBy :: (String -> String) -> Config -> FilePath -> IO (Maybe Version)
tryFindVersionBy findVersion cfg cmd =
fmap (>>= parseVer) (tryRunOutput cfg cmd ["--version"])
where
parseVer ver = case readP_to_S (parseVersion <* eof) (findVersion ver) of
[(v,"")] -> Just v
_ -> Nothing
type Args = [String]
-- | Only return the stdout if the process was successful and had no stderr.
tryRunOutput :: Config -> FilePath -> Args -> IO (Maybe String)
tryRunOutput cfg cmd args = do
printDebug cfg cmd args
res <- readProcessWithExitCode cmd args ""
return $ case res of
(ExitSuccess, out, "" ) -> Just out
-- Some tools (e.g. Stack) put output to stderr
(ExitSuccess, "", err) -> Just err
_ -> Nothing
-- | As with 'tryRunOutput' but only return the first line (if any).
tryRunLine :: Config -> FilePath -> Args -> IO (Maybe String)
tryRunLine cfg cmd = fmap (>>= listToMaybe . lines) . tryRunOutput cfg cmd
-- | Returns success of call.
tryRun :: Config -> Tagged t CommandPath -> Args -> IO ExitCode
tryRun cfg cmd args = do
printDebug cfg cmd' args
withCreateProcess cp $ \_ _ _ ph ->
waitForProcess ph
where
cmd' = stripTag cmd
cp = (proc cmd' args) { std_in = Inherit
, std_out = Inherit
, std_err = Inherit
}
-- | Print the error message if it isn't successful.
tryRunErr :: String -> IO ExitCode -> IO ExitCode
tryRunErr msg act = do
res <- act
if res == ExitSuccess
then return res
else res <$ hPutStrLn stderr msg
tryRunToFile :: Config -> FilePath -> Tagged t CommandPath -> Args -> IO ExitCode
tryRunToFile cfg file cmd args = do
printDebug cfg cmd' args
withFile file WriteMode $ \h ->
withCreateProcess (cp h) $ \_ _ _ ph ->
waitForProcess ph
where
cmd' = stripTag cmd
cp h = (proc cmd' args) { std_in = Inherit
, std_out = UseHandle h
, std_err = Inherit
}
printDebug :: Config -> FilePath -> Args -> IO ()
printDebug cfg cmd args =
when (debugMode cfg) (hPutStrLn stderr (makeBox ("Running: " ++ cmdStr)))
where
cmdStr = showCommandForUser cmd args
(.&&.) :: (Monad m) => m ExitCode -> m ExitCode -> m ExitCode
m1 .&&. m2 = do ec1 <- m1
case ec1 of
ExitSuccess -> m2
_ -> return ec1
infixr 3 .&&.
(.||.) :: (Monad m) => m ExitCode -> m ExitCode -> m ExitCode
m1 .||. m2 = do ec1 <- m1
case ec1 of
ExitSuccess -> return ec1
_ -> m2
infixr 2 .||.
tryCommand :: String -> IO ExitCode -> IO ExitCode -> IO ExitCode
tryCommand msg tryWith run = run .||. tryAgain
where
tryAgain = do
putStrLn (makeBox msg)
tryWith .&&. run
makeBox :: String -> String
makeBox msg = unlines [ border
, "* " ++ msg ++ " *"
, border
]
where
msgLen = length msg
boxLen = msgLen + 4 -- asterisk + space on either side
border = replicate boxLen '*'
allSuccess :: (Monad m, Foldable t) => t (m ExitCode) -> m ExitCode
allSuccess = foldr (.&&.) (return ExitSuccess)
-- | Monad version of 'all', aborts the computation at the first @False@ value
allM :: Monad m => (a -> m Bool) -> [a] -> m Bool
allM _ [] = return True
allM f (b:bs) = f b >>= (\bv -> if bv then allM f bs else return False)