-- | -- Module: Staversion.Internal.Command -- Description: Command from the user. -- Maintainer: Toshio Ito -- -- __This is an internal module. End-users should not use it.__ module Staversion.Internal.Command ( Command(..), parseCommandArgs, defFormatConfig, _parseCommandStrings ) where import Control.Applicative ((<$>), (<*>), optional, some, (<|>), many) import Data.Function (on) import Data.Monoid (mconcat, (<>)) import Data.Text (pack) import qualified Options.Applicative as Opt import qualified Paths_staversion as MyInfo import System.Directory (getHomeDirectory) import System.FilePath (()) import qualified Text.PrettyPrint.ANSI.Leijen as Pretty import Staversion.Internal.Aggregate (Aggregator) import qualified Staversion.Internal.Aggregate as Agg import Staversion.Internal.Format (FormatConfig(..), FormatVersion) import qualified Staversion.Internal.Format as Format import Staversion.Internal.Log ( LogLevel(..), Logger(loggerThreshold), defaultLogger ) import Staversion.Internal.Query ( Resolver, PackageName, Query(..), parseQuery, PackageSource(..) ) import Staversion.Internal.Version (showBaseVersion) -- | Command from the user. data Command = Command { commBuildPlanDir :: FilePath, -- ^ path to the directory where build plan files are stored. commStackCommand :: String, -- ^ shell command to invoke @stack@ tool. commLogger :: Logger, -- ^ the logger commSources :: [PackageSource], -- ^ package sources to search commQueries :: [Query], -- ^ package queries commAllowNetwork :: Bool, -- ^ if 'True', it accesses the Internet to query build plans etc. commAggregator :: Maybe Aggregator, -- ^ if 'Just', do aggregation over the results. commFormatConfig :: FormatConfig -- ^ config for the formatter } -- | Default values for 'Command'. data DefCommand = DefCommand { defBuildPlanDir :: FilePath } deriving (Show,Eq,Ord) defCommand :: IO DefCommand defCommand = DefCommand <$> def_build_plan_dir where def_build_plan_dir = do home <- getHomeDirectory return $ home ".stack" "build-plan" commandParser :: DefCommand -> Opt.Parser Command commandParser def_comm = Command <$> build_plan_dir <*> stack_command <*> logger <*> sources <*> queries <*> network <*> aggregate <*> format_config where logger = makeLogger <$> is_verbose makeLogger True = defaultLogger { loggerThreshold = Just LogDebug } makeLogger False = defaultLogger is_verbose = Opt.switch $ mconcat [ Opt.long "verbose", Opt.short 'v', Opt.help "Verbose messages." ] build_plan_dir = Opt.strOption $ mconcat [ Opt.long "build-plan-dir", Opt.help "Directory where build plan YAML files are stored.", Opt.metavar "DIR", Opt.value (defBuildPlanDir def_comm), Opt.showDefault ] withDefault :: Functor m => [a] -> m [a] -> m [a] withDefault def_vals = fmap applyDef where applyDef [] = def_vals applyDef vs = vs sources = withDefault [SourceStackDefault] $ many $ resolver <|> hackage <|> stack_explicit <|> stack_default resolver = fmap SourceStackage $ Opt.strOption $ mconcat [ Opt.long "resolver", Opt.short 'r', Opt.help "Stackage resolver to search. e.g. \"lts-6.15\"", Opt.metavar "RESOLVER_NAME" ] hackage = Opt.flag' SourceHackage $ mconcat [ Opt.long "hackage", Opt.short 'H', Opt.help "Search hackage.org for the latest version." ] stack_explicit = fmap SourceStackYaml $ Opt.strOption $ mconcat [ Opt.long "stack", Opt.help ( "Path to stack.yaml file." ++ " It searches for package versions of the resolver of the specified stack.yaml file." ), Opt.metavar "FILE" ] stack_default = Opt.flag' SourceStackDefault $ mconcat [ Opt.long "stack-default", Opt.short 'S', Opt.help ( "Search the resolver that 'stack' command would use by default." ++ " This option is implied if there is no options about package source (e.g. -r and -H)." ) ] queries = withDefault [QueryStackYamlDefault] $ many $ parseQuery <$> (query_package <|> query_cabal <|> query_stack_yaml) query_package = Opt.strArgument $ mconcat [ Opt.help "Name of package whose version you want to check.", Opt.metavar "PACKAGE_NAME" ] query_cabal = Opt.strArgument $ mconcat [ Opt.help ".cabal file name. It checks versions of packages in build-deps lists.", Opt.metavar "CABAL_FILEPATH" ] query_stack_yaml = Opt.strArgument $ mconcat [ Opt.help ( "Path to stack.yaml file." ++ " It checks versions of packages in build-deps of all cabal projects listed in the stack.yaml." ++ " If you just type 'stack.yaml'," ++ " it means the default configuration that 'stack' command would use by default." ++ " 'stack.yaml' is implied if there is no query argument." ), Opt.metavar "STACK_YAML_FILEPATH" ] network = not <$> no_network no_network = Opt.switch $ mconcat [ Opt.long "no-network", Opt.help "Forbid network access." ] aggregate = optional $ Opt.option (maybeReader "AGGREGATOR" parseAggregator) $ mconcat [ Opt.long "aggregate", Opt.short 'a', Opt.metavar "AGGREGATOR", Opt.helpDoc $ Just $ docAggregators "AGGREGATOR" ] format_config = FormatConfig <$> format_version format_version = Opt.option (maybeReader "FORMAT" $ parseSelect formatVersions) $ mconcat [ Opt.long "format-version", Opt.metavar "FORMAT", Opt.helpDoc $ Just $ docFormatVersions "FORMAT", Opt.value $ fconfFormatVersion defFormatConfig ] stack_command = Opt.strOption $ mconcat [ Opt.long "stack-command", Opt.help "Shell command for stack tool.", Opt.metavar "COMMAND", Opt.value "stack", Opt.showDefault ] maybeReader :: String -> (String -> Maybe a) -> Opt.ReadM a maybeReader metavar mfunc = do got <- Opt.str case mfunc got of Nothing -> Opt.readerError ("Unknown " ++ metavar ++ ": " ++ got) Just v -> return v data SelectSpec a = SelectSpec { selectResult :: a, selectSymbol :: String, selectDesc :: String } type AggregatorSpec = SelectSpec Aggregator aggregators :: [AggregatorSpec] aggregators = [ SelectSpec Agg.aggOr "or" "concatenate versions with (||).", SelectSpec Agg.aggPvpMajor "pvp-major" ( "aggregate versions to a range that is supposed to be " ++ "compatible with the given versions " ++ "in terms of PVP (Package Versioning Policy.) " ++ "Major versions are used for upper bounds." ), SelectSpec Agg.aggPvpMajor "pvp" "alias for 'pvp-major'", SelectSpec Agg.aggPvpMinor "pvp-minor" ( "aggregate versions to a range that is supposed to be " ++ "compatible with the given versions " ++ "in terms of PVP. " ++ "Minor versions are used for upper bounds, i.e. this is stricter than 'pvp-major'." ) ] parseSelect :: [SelectSpec a] -> String -> Maybe a parseSelect specs symbol = toMaybe $ filter (\spec -> selectSymbol spec == symbol) specs where toMaybe [] = Nothing toMaybe (spec : _) = Just $ selectResult spec parseAggregator :: String -> Maybe Aggregator parseAggregator = parseSelect aggregators wrapped :: String -> Pretty.Doc wrapped = Pretty.fillSep . map Pretty.text . words docSelect :: [SelectSpec a] -> String -> String -> Pretty.Doc docSelect specs foreword_str metavar = Pretty.vsep $ (foreword :) $ map docSpec specs where foreword = wrapped ( foreword_str ++ " Possible " ++ metavar ++ " is:" ) docSpec SelectSpec {selectSymbol = symbol, selectDesc = desc} = Pretty.hang 2 $ wrapped (symbol <> ": " <> desc) docSelectWithDefault :: [SelectSpec a] -> String -> String -> Pretty.Doc docSelectWithDefault [] foreword metavar = docSelect [] foreword metavar where docSelectWithDefault (def_spec : rest) foreword metavar = docSelect (def_spec' : rest) foreword metavar where def_spec' = def_spec { selectSymbol = selectSymbol def_spec <> " [DEFAULT]" } docAggregators :: String -> Pretty.Doc docAggregators = docSelect aggregators "Aggregate version results over different resolvers." defFormatConfig :: FormatConfig defFormatConfig = FormatConfig { fconfFormatVersion = selectResult $ head formatVersions } formatVersions :: [SelectSpec FormatVersion] formatVersions = [ SelectSpec Format.formatVersionCabal "cabal" ( "Let Cabal format VersionRanges" ), SelectSpec Format.formatVersionCabalCaret "cabal-caret" ( "Similar to 'cabal', but it uses the caret operator (^>=) if possible" ) ] docFormatVersions :: String -> Pretty.Doc docFormatVersions = docSelectWithDefault formatVersions "Format for package version ranges." programDescription :: Opt.Parser a -> Opt.ParserInfo a programDescription parser = Opt.info (Opt.helper <*> parser) $ mconcat [ Opt.fullDesc, Opt.progDesc ( "Look for version numbers for Haskell packages in specific stackage resolvers" ++ " (or possibly other package sources)" ), Opt.footer ("Version: " ++ (showBaseVersion MyInfo.version)) ] parseCommandArgs :: IO Command parseCommandArgs = Opt.execParser . programDescription . commandParser =<< defCommand -- | Just for testing. _parseCommandStrings :: [String] -> IO (Maybe Command) _parseCommandStrings args = fmap (doParse . programDescription . commandParser) defCommand where doParse pinfo = Opt.getParseResult $ Opt.execParserPure prefs pinfo args prefs = Opt.prefs mempty