jbi-0.1.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.Tagged
import Control.Applicative (liftA2)
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), withFile)
import System.Process (CreateProcess(..),
StdStream(Inherit, UseHandle), proc,
readProcessWithExitCode, waitForProcess,
withCreateProcess)
import Text.ParserCombinators.ReadP (eof, readP_to_S)
--------------------------------------------------------------------------------
class Tool t where
commandName :: Tagged t CommandName
commandVersion :: 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) => IO (Maybe (Installed t))
commandInformation = commandPath >>= mapM getVersion
where
getVersion :: (Tool t') => Tagged t' CommandPath -> IO (Installed t')
getVersion tcp = Installed tcp <$> commandVersion 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 :: 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) -> FilePath -> IO (Maybe Version)
tryFindVersionBy findVersion cmd =
fmap (>>= parseVer) (tryRunOutput 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 :: FilePath -> Args -> IO (Maybe String)
tryRunOutput cmd args = do
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 :: FilePath -> Args -> IO (Maybe String)
tryRunLine cmd = fmap (>>= listToMaybe . lines) . tryRunOutput cmd
-- | Returns success of call.
tryRun :: Tagged t CommandPath -> Args -> IO ExitCode
tryRun cmd args = withCreateProcess cp $ \_ _ _ ph ->
waitForProcess ph
where
cmd' = stripTag cmd
cp = (proc cmd' args) { std_in = Inherit
, std_out = Inherit
, std_err = Inherit
}
tryRunToFile :: FilePath -> Tagged t CommandPath -> Args -> IO ExitCode
tryRunToFile file 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
}
-- | Equivalent to chaining all the calls with @&&@ in bash, etc.
--
-- Argument order to make it easier to feed it into a 'Tagged'-based
-- pipeline.
tryRunAll :: [Args] -> Tagged t CommandPath -> IO ExitCode
tryRunAll argss cmd = allSuccess $ map (tryRun cmd) argss
(.&&.) :: (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)