{- | Module : Iris.Tool Copyright : (c) 2022 Dmitrii Kovanikov SPDX-License-Identifier : MPL-2.0 Maintainer : Dmitrii Kovanikov 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