iris-0.1.0.0: src/Iris/Tool.hs
{- |
Module : Iris.Tool
Copyright : (c) 2022 Dmitrii Kovanikov
SPDX-License-Identifier : MPL-2.0
Maintainer : Dmitrii Kovanikov <kovanikov@gmail.com>
Stability : Experimental
Portability : Portable
Utilities to check required tools and their minimal version for a CLI app.
Sometimes, your CLI application
@since 0.0.0.0
-}
module Iris.Tool (
-- * Requiring an executable
need,
Tool (..),
ToolSelector (..),
defaultToolSelector,
-- * Tool requirements check
ToolCheckResult (..),
ToolCheckError (..),
ToolCheckException (..),
checkTool,
) where
import Control.Exception (Exception, throwIO)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Foldable (traverse_)
import Data.String (IsString (..))
import Data.Text (Text)
import System.Directory (findExecutable)
import System.Process (readProcess)
import qualified Data.Text as Text
{- |
@since 0.0.0.0
-}
data Tool = Tool
{ toolName :: Text
-- ^ @since 0.0.0.0
, toolSelector :: Maybe ToolSelector
-- ^ @since 0.0.0.0
}
{- |
@since 0.0.0.0
-}
instance IsString Tool where
fromString :: String -> Tool
fromString s =
Tool
{ toolName = fromString s
, toolSelector = Nothing
}
{- |
@since 0.0.0.0
-}
data ToolSelector = ToolSelector
{ toolSelectorFunction :: Text -> Bool
-- ^ @since 0.0.0.0
, toolSelectorVersionArg :: Maybe Text
-- ^ @since 0.0.0.0
}
{- |
@since 0.0.0.0
-}
defaultToolSelector :: ToolSelector
defaultToolSelector =
ToolSelector
{ toolSelectorFunction = const True
, toolSelectorVersionArg = Nothing
}
{- |
@since 0.0.0.0
-}
data ToolCheckResult
= -- |
--
-- @since 0.1.0.0
ToolCheckError ToolCheckError
| -- |
--
-- @since 0.0.0.0
ToolOk
deriving stock
( Show
-- ^ @since 0.0.0.0
, Eq
-- ^ @since 0.0.0.0
)
{- |
@since 0.1.0.0
-}
data ToolCheckError
= -- |
--
-- @since 0.1.0.0
ToolNotFound Text
| -- |
--
-- @since 0.1.0.0
ToolWrongVersion Text
deriving stock
( Show
-- ^ @since 0.1.0.0
, Eq
-- ^ @since 0.1.0.0
)
{- |
@since 0.0.0.0
-}
checkTool :: Tool -> IO ToolCheckResult
checkTool Tool{..} =
findExecutable (Text.unpack toolName) >>= \case
Nothing -> pure $ ToolCheckError $ ToolNotFound toolName
Just exe -> case toolSelector of
Nothing -> pure ToolOk
Just ToolSelector{..} -> case toolSelectorVersionArg of
Nothing -> pure ToolOk
Just versionArg -> do
toolVersionOutput <- readProcess exe [Text.unpack versionArg] ""
let version = Text.strip $ Text.pack toolVersionOutput
if toolSelectorFunction version
then pure ToolOk
else pure $ ToolCheckError $ ToolWrongVersion version
{- | An exception thrown by 'need' when there's an error requiring a tool.
@since 0.1.0.0
-}
newtype ToolCheckException = ToolCheckException ToolCheckError
deriving stock
( Show
-- ^ @since 0.1.0.0
)
deriving newtype
( Eq
-- ^ @since 0.1.0.0
)
deriving anyclass
( Exception
-- ^ @since 0.1.0.0
)
{- | Use this function to require specific CLI tools for your CLI application.
The function can be used in the beginning of each command in the following way:
@
app :: App ()
app = Iris.'Iris.Env.asksCliEnv' Iris.'Iris.Env.cliEnvCmd' >>= __\\case__
Download url -> do
Iris.'need' ["curl"]
runDownload url
Evaluate hs -> do
Iris.'need' ["ghc", "cabal"]
runEvaluate hs
@
__Throws:__ 'ToolCheckException' if can't find a tool or if it has wrong version.
@since 0.0.0.0
-}
need :: MonadIO m => [Tool] -> m ()
need = traverse_ $ \tool ->
liftIO $
checkTool tool >>= \case
ToolOk -> pure ()
ToolCheckError toolErr -> throwIO $ ToolCheckException toolErr