├── .ghci ├── .gitignore ├── .travis.yml ├── ChangeLog.md ├── LICENSE ├── README.md ├── Setup.hs ├── main └── Main.hs ├── src ├── Plugins.hs ├── Plugins │ └── Commands.hs ├── SimpleOptions.hs └── Stackage │ └── CLI.hs └── stackage-cli.cabal /.ghci: -------------------------------------------------------------------------------- 1 | :set -package system-fileio -package http-client -package bytestring 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist*/ 2 | .hsenv/ 3 | *.o 4 | *.hi 5 | cabal.config 6 | cabal.sandbox.config 7 | .cabal-sandbox 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Travis script based on https://github.com/hvr/multi-ghc-travis 2 | 3 | env: 4 | - CABALVER=1.18 GHCVER=7.8.4 5 | - CABALVER=1.22 GHCVER=7.10.1 6 | 7 | before_install: 8 | - travis_retry sudo add-apt-repository -y ppa:hvr/ghc 9 | - travis_retry sudo apt-get update 10 | - travis_retry sudo apt-get install cabal-install-$CABALVER ghc-$GHCVER 11 | - export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:$PATH 12 | 13 | install: 14 | - cabal --version 15 | - echo "$(ghc --version) [$(ghc --print-project-git-commit-id 2> /dev/null || echo '?')]" 16 | - travis_retry cabal update 17 | - cabal install --only-dependencies --enable-tests --enable-benchmarks 18 | 19 | script: 20 | - cabal configure -flib-Werror -v2 --enable-tests --enable-benchmarks 21 | - cabal build 22 | - cabal check 23 | - cabal sdist 24 | # check that the generated source-distribution can be built & installed 25 | - export SRC_TGZ=$(cabal info . | awk '{print $2 ".tar.gz";exit}') ; 26 | cd dist/; 27 | if [ -f "$SRC_TGZ" ]; then 28 | cabal install --force-reinstalls "$SRC_TGZ"; 29 | else 30 | echo "expected '$SRC_TGZ' not found"; 31 | exit 1; 32 | fi 33 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 2 | 3 | * Split off stackage-cabal and stackage-sandbox 4 | 5 | ## 0.0.0.4 6 | 7 | * stackage-init now uses https [#34](https://github.com/fpco/stackage-cli/pull/34) 8 | 9 | ## 0.0.0.3 10 | 11 | * Works with older versions of parsec [#28](https://github.com/fpco/stackage-cli/issues/28) 12 | 13 | ## 0.0.0.1 14 | 15 | * Compilation fix for GHC 7.10 [#29](https://github.com/fpco/stackage-cli/issues/29) 16 | 17 | ## 0.0.0 18 | 19 | * Initial release 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 FP Complete, http://www.fpcomplete.com/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | stackage-cli 2 | ============ 3 | 4 | [![Build Status](https://travis-ci.org/fpco/stackage-cli.svg)](https://travis-ci.org/fpco/stackage-cli) 5 | 6 | A command-line interface for leveraging stackage. 7 | 8 | You must have `ghc`, `ghc-pkg`, and `cabal` on your $PATH. This program will make various calls to those executables on your behalf. 9 | 10 | This package provides two executables: the `stackage` executable, and its alias, `stk`. The `stackage` command-line program will inspect your path for stackage plugins, and will dispatch to them. Anything on your $PATH prefixed `stackage-` that responds to the `--summary` flag is considered a stackage plugin. 11 | 12 | (This package also provides a library, `Stackage.CLI`, which is intended to make the process of writing stackage plugins easier.) 13 | 14 | This package no longer provides any plugins (which makes it rather useless on its own). 15 | You can find the `init`, `purge`, and `upgrade` plugins in the `stackage-cabal` package. 16 | You can find the `sandbox` plugin in the `stackage-sandbox` package. 17 | 18 | ## Further reading 19 | 20 | See also: [this example on the stackage-cli wiki](https://github.com/fpco/stackage-cli/wiki/Example). 21 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | 3 | main = defaultMainWithHooks simpleUserHooks 4 | { postInst = \_ _ _ _ -> putStrLn notice 5 | } 6 | 7 | notice = 8 | " \n\ 9 | \- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\ 10 | \ \n\ 11 | \ Notice: some stackage plugins have moved! \n\ 12 | \ \n\ 13 | \ cabal install stackage-cabal \n\ 14 | \ # will get you the plugins: \n\ 15 | \ # stackage-init \n\ 16 | \ # stackage-purge \n\ 17 | \ # stackage-upgrade \n\ 18 | \ \n\ 19 | \ cabal install stackage-sandbox \n\ 20 | \ # will get you the stackage-sandbox plugin \n\ 21 | \ \n\ 22 | \- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n" 23 | -------------------------------------------------------------------------------- /main/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE ViewPatterns #-} 3 | {-# LANGUAGE TemplateHaskell #-} 4 | 5 | module Main where 6 | 7 | import Control.Applicative 8 | import Control.Exception (catch) 9 | import Control.Monad 10 | import Data.Monoid ((<>)) 11 | import Data.Maybe (isJust) 12 | import Data.List as List 13 | import Data.Text (Text) 14 | import qualified Data.Text as T 15 | import qualified Data.Text.IO as T 16 | import Stackage.CLI 17 | import System.Environment 18 | import System.IO (stderr) 19 | import System.Exit 20 | import qualified Paths_stackage_cli as CabalInfo 21 | 22 | onPluginErr :: PluginException -> IO () 23 | onPluginErr (PluginNotFound _ name) = do 24 | T.hPutStr stderr $ "Stackage plugin unavailable: " <> name 25 | exitFailure 26 | onPluginErr (PluginExitFailure _ i) = do 27 | exitWith (ExitFailure i) 28 | 29 | version :: String 30 | version = $(simpleVersion CabalInfo.version) 31 | 32 | main :: IO () 33 | main = do 34 | stackage <- findPlugins "stackage" 35 | args <- getArgs 36 | case dropWhile (List.isPrefixOf "-") args of 37 | ((T.pack -> name):args') 38 | | isJust (lookupPlugin stackage name) -> 39 | callPlugin stackage name args' `catch` onPluginErr 40 | _ -> do 41 | simpleOptions 42 | version 43 | "Run stackage commands" 44 | "Run stackage commands" 45 | (pure ()) 46 | (commandsFromPlugins stackage) 47 | return () 48 | -------------------------------------------------------------------------------- /src/Plugins.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE RankNTypes #-} 3 | {-# LANGUAGE DeriveDataTypeable #-} 4 | 5 | -- | Dynamically look up available executables. 6 | module Plugins 7 | ( Plugin 8 | , pluginPrefix 9 | , pluginName 10 | , pluginSummary 11 | , pluginProc 12 | 13 | , Plugins 14 | , findPlugins 15 | , listPlugins 16 | , lookupPlugin 17 | , callPlugin 18 | 19 | , PluginException (..) 20 | ) where 21 | 22 | import Control.Applicative 23 | import Control.Exception (Exception) 24 | import Control.Monad 25 | import Control.Monad.Catch (MonadThrow, throwM) 26 | import Control.Monad.IO.Class (MonadIO, liftIO) 27 | import Control.Monad.Trans.Class (lift) 28 | import Control.Monad.Trans.State.Strict (StateT, get, put) 29 | import Data.Conduit 30 | import Data.Hashable (Hashable) 31 | import Data.HashSet (HashSet) 32 | import Data.HashMap.Strict (HashMap) 33 | import qualified Data.HashSet as HashSet 34 | import qualified Data.HashMap.Strict as HashMap 35 | import qualified Data.Conduit.List as CL 36 | import Data.Conduit.Lift (evalStateC) 37 | import qualified Data.List as L 38 | import Data.List.Split (splitOn) 39 | import Data.Text (Text, pack, unpack) 40 | import qualified Data.Text as T 41 | import Data.Typeable (Typeable) 42 | import Data.Monoid 43 | import System.Directory 44 | import System.Process (CreateProcess, proc, readProcess, readProcessWithExitCode, createProcess, waitForProcess) 45 | import System.FilePath ((), getSearchPath, splitExtension) 46 | import System.Environment (getEnv) 47 | import System.Exit (ExitCode (..)) 48 | 49 | -- | Represents a runnable plugin. 50 | -- Plugins must be discovered via `findPlugins`. 51 | data Plugin = Plugin 52 | { _pluginPrefix :: !Text 53 | , _pluginName :: !Text 54 | , _pluginSummary :: !Text 55 | } 56 | deriving (Show) 57 | 58 | -- | The program being plugged into. 59 | pluginPrefix :: Plugin -> Text 60 | pluginPrefix = _pluginPrefix 61 | 62 | -- | The name of this plugin (without the prefix). 63 | pluginName :: Plugin -> Text 64 | pluginName = _pluginName 65 | 66 | -- | A summary of what this plugin does 67 | pluginSummary :: Plugin -> Text 68 | pluginSummary = _pluginSummary 69 | 70 | -- | Describes how to create a process out of a plugin and arguments. 71 | -- You may use Data.Process and Data.Conduit.Process 72 | -- to manage the process's stdin, stdout, and stderr in various ways. 73 | pluginProc :: Plugin -> [String] -> CreateProcess 74 | pluginProc = proc . pluginProcessName 75 | 76 | -- Not exported 77 | pluginProcessName :: Plugin -> String 78 | pluginProcessName p = unpack $ pluginPrefix p <> "-" <> pluginName p 79 | 80 | 81 | -- | Represents the plugins available to a given program. 82 | -- See: `findPlugins`. 83 | data Plugins = Plugins 84 | { _pluginsPrefix :: !Text 85 | , _pluginsMap :: !(HashMap Text Plugin) 86 | } 87 | deriving (Show) 88 | 89 | 90 | -- | Find the plugins for a given program by inspecting everything on the PATH. 91 | -- Any program that is prefixed with the given name and responds 92 | -- to the `--summary` flag by writing one line to stdout 93 | -- is considered a plugin. 94 | findPlugins :: Text -> IO Plugins 95 | findPlugins t = fmap (Plugins t) 96 | $ discoverPlugins t 97 | $$ awaitForever (toPlugin t) 98 | =$ CL.fold insertPlugin HashMap.empty 99 | where 100 | insertPlugin m p = HashMap.insert (pluginName p) p m 101 | 102 | toPlugin :: (MonadIO m) => Text -> Text -> Producer m Plugin 103 | toPlugin prefix name = do 104 | let proc = unpack $ prefix <> "-" <> name 105 | (exit, out, _err) <- liftIO $ readProcessWithExitCode proc ["--summary"] "" 106 | case exit of 107 | ExitSuccess -> case T.lines (pack out) of 108 | [summary] -> yield $ Plugin 109 | { _pluginPrefix = prefix 110 | , _pluginName = name 111 | , _pluginSummary = summary 112 | } 113 | _ -> return () 114 | _ -> return () 115 | 116 | 117 | -- | Things that can go wrong when using `callPlugin`. 118 | data PluginException 119 | = PluginNotFound !Plugins !Text 120 | | PluginExitFailure !Plugin !Int 121 | deriving (Show, Typeable) 122 | instance Exception PluginException 123 | 124 | -- | Look up a particular plugin by name. 125 | lookupPlugin :: Plugins -> Text -> Maybe Plugin 126 | lookupPlugin ps t = HashMap.lookup t $ _pluginsMap ps 127 | 128 | -- | List the available plugins. 129 | listPlugins :: Plugins -> [Plugin] 130 | listPlugins = HashMap.elems . _pluginsMap 131 | 132 | -- | A convenience wrapper around lookupPlugin and pluginProc. 133 | -- Handles stdin, stdout, and stderr are all inherited by the plugin. 134 | -- Throws PluginException. 135 | callPlugin :: (MonadIO m, MonadThrow m) 136 | => Plugins -> Text -> [String] -> m () 137 | callPlugin ps name args = case lookupPlugin ps name of 138 | Nothing -> throwM $ PluginNotFound ps name 139 | Just plugin -> do 140 | exit <- liftIO $ do 141 | (_, _, _, process) <- createProcess $ pluginProc plugin args 142 | waitForProcess process 143 | case exit of 144 | ExitFailure i -> throwM $ PluginExitFailure plugin i 145 | ExitSuccess -> return () 146 | 147 | 148 | discoverPlugins :: MonadIO m => Text -> Producer m Text 149 | discoverPlugins t 150 | = getPathDirs 151 | $= clNub -- unique dirs on path 152 | $= awaitForever (executablesPrefixed $ unpack $ t <> "-") 153 | $= CL.map pack 154 | $= clNub -- unique executables 155 | 156 | executablesPrefixed :: (MonadIO m) => FilePath -> FilePath -> Producer m FilePath 157 | executablesPrefixed prefix dir 158 | = pathToContents dir 159 | $= CL.filter (L.isPrefixOf prefix) 160 | $= clFilterM (fileExistsIn dir) 161 | $= clFilterM (isExecutableIn dir) 162 | $= CL.mapMaybe (L.stripPrefix prefix . dropExeExt) 163 | 164 | -- | Drop the .exe extension if present 165 | dropExeExt :: FilePath -> FilePath 166 | dropExeExt fp 167 | | y == ".exe" = x 168 | | otherwise = fp 169 | where 170 | (x, y) = splitExtension fp 171 | 172 | getPathDirs :: (MonadIO m) => Producer m FilePath 173 | getPathDirs = liftIO getSearchPath >>= mapM_ yield 174 | 175 | pathToContents :: (MonadIO m) => FilePath -> Producer m FilePath 176 | pathToContents dir = do 177 | exists <- liftIO $ doesDirectoryExist dir 178 | when exists $ do 179 | contents <- liftIO $ getDirectoryContents dir 180 | CL.sourceList contents 181 | 182 | fileExistsIn :: (MonadIO m) => FilePath -> FilePath -> m Bool 183 | fileExistsIn dir file = liftIO $ doesFileExist $ dir file 184 | 185 | isExecutableIn :: (MonadIO m) => FilePath -> FilePath -> m Bool 186 | isExecutableIn dir file = liftIO $ do 187 | perms <- getPermissions $ dir file 188 | return (executable perms) 189 | 190 | clFilterM :: Monad m => (a -> m Bool) -> Conduit a m a 191 | clFilterM pred = awaitForever $ \a -> do 192 | predPassed <- lift $ pred a 193 | when predPassed $ yield a 194 | 195 | clNub :: (Monad m, Eq a, Hashable a) 196 | => Conduit a m a 197 | clNub = evalStateC HashSet.empty clNubState 198 | 199 | clNubState :: (Monad m, Eq a, Hashable a) 200 | => Conduit a (StateT (HashSet a) m) a 201 | clNubState = awaitForever $ \a -> do 202 | seen <- lift get 203 | unless (HashSet.member a seen) $ do 204 | lift $ put $ HashSet.insert a seen 205 | yield a 206 | -------------------------------------------------------------------------------- /src/Plugins/Commands.hs: -------------------------------------------------------------------------------- 1 | -- | Using Plugins with SimpleOptions 2 | module Plugins.Commands 3 | ( commandsFromPlugins 4 | , toCommand 5 | ) where 6 | 7 | import Control.Monad.Trans.Either (EitherT) 8 | import Control.Monad.Trans.Writer (Writer) 9 | import Data.Text (Text, unpack) 10 | import Data.Foldable (foldMap) 11 | import Plugins 12 | import Options.Applicative.Simple 13 | 14 | -- | Generate the "commands" argument to simpleOptions 15 | -- based on available plugins. 16 | commandsFromPlugins :: Plugins -> EitherT Text (Writer (Mod CommandFields Text)) () 17 | commandsFromPlugins plugins = mapM_ toCommand (listPlugins plugins) 18 | 19 | -- | Convert a single plugin into a command. 20 | toCommand :: Plugin -> EitherT Text (Writer (Mod CommandFields Text)) () 21 | toCommand plugin = addCommand 22 | (unpack $ pluginName plugin) 23 | (unpack $ pluginSummary plugin) 24 | id 25 | (pure $ pluginName plugin) 26 | -------------------------------------------------------------------------------- /src/SimpleOptions.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE RankNTypes #-} 2 | 3 | -- | A convenience wrapper around Options.Applicative. 4 | module SimpleOptions 5 | ( simpleOptions 6 | , Options.addCommand 7 | , Options.simpleVersion 8 | ) where 9 | 10 | import Control.Applicative 11 | import Control.Monad.Trans.Either (EitherT) 12 | import Control.Monad.Trans.Writer (Writer) 13 | import Data.Monoid 14 | import qualified Options.Applicative.Simple as Options 15 | 16 | -- | This is a drop-in replacement for simpleOptions from 17 | -- Options.Applicative.Simple, with the added feature of a `--summary` flag 18 | -- that prints out the header. (Should be one line) 19 | simpleOptions 20 | :: String 21 | -- ^ version string 22 | -> String 23 | -- ^ header 24 | -> String 25 | -- ^ program description 26 | -> Options.Parser a 27 | -- ^ global settings 28 | -> EitherT b (Writer (Options.Mod Options.CommandFields b)) () 29 | -- ^ commands (use 'addCommand') 30 | -> IO (a,b) 31 | simpleOptions versionString h pd globalParser mcommands = 32 | Options.simpleOptions 33 | versionString h pd globalParser' mcommands 34 | where 35 | globalParser' = summaryOption <*> globalParser 36 | summaryOption = Options.infoOption h 37 | $ Options.long "summary" 38 | <> Options.help "Show program summary" 39 | -------------------------------------------------------------------------------- /src/Stackage/CLI.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | -- | Functions for creating and calling Stackage plugins. 4 | module Stackage.CLI 5 | ( -- * Discovering and calling plugins 6 | runStackagePlugin 7 | , Plugins 8 | , findPlugins 9 | , callPlugin 10 | , PluginException (..) 11 | 12 | -- * Creating your own plugin 13 | , commandsFromPlugins 14 | , simpleOptions 15 | , addCommand 16 | , simpleVersion 17 | 18 | -- * Finer-grained inspection of plugins 19 | , listPlugins 20 | , lookupPlugin 21 | , Plugin 22 | , pluginPrefix 23 | , pluginName 24 | , pluginSummary 25 | , pluginProc 26 | ) where 27 | 28 | import Data.Text (Text) 29 | import Plugins 30 | import Plugins.Commands 31 | import SimpleOptions 32 | 33 | -- | Runs a stackage plugin. Handy for dynamic one-off runs, 34 | -- but if you'll be running multiple plugins, it is recommended 35 | -- that you use @findPlugins "stackage"@ so that the plugin search 36 | -- is performed only once. 37 | runStackagePlugin :: Text -> [String] -> IO () 38 | runStackagePlugin name args = do 39 | stackage <- findPlugins "stackage" 40 | callPlugin stackage name args 41 | -------------------------------------------------------------------------------- /stackage-cli.cabal: -------------------------------------------------------------------------------- 1 | name: stackage-cli 2 | version: 0.1.0.2 3 | synopsis: 4 | A CLI library for stackage commands 5 | description: 6 | A CLI library for stackage commands 7 | homepage: 8 | https://www.stackage.org/package/stackage-cli 9 | bug-reports: 10 | https://github.com/fpco/stackage-cli/issues 11 | 12 | license: MIT 13 | license-file: LICENSE 14 | author: Dan Burton 15 | maintainer: danburton@fpcomplete.com 16 | copyright: 2015 FP Complete Corporation 17 | 18 | build-type: Custom 19 | cabal-version: >=1.10 20 | category: Development 21 | extra-source-files: README.md ChangeLog.md 22 | 23 | source-repository head 24 | type: git 25 | location: git://github.com/fpco/stackage-cli.git 26 | 27 | library 28 | hs-source-dirs: src/ 29 | exposed-modules: 30 | Stackage.CLI 31 | other-modules: 32 | SimpleOptions 33 | , Plugins 34 | , Plugins.Commands 35 | build-depends: 36 | base >=4.7 && <5 37 | , text 38 | , conduit 39 | , optparse-applicative 40 | , process 41 | , transformers 42 | , split 43 | , filepath 44 | , directory 45 | , hashable 46 | , unordered-containers 47 | , exceptions 48 | , optparse-simple >= 0.0.2 49 | , either 50 | default-language: Haskell2010 51 | 52 | executable stackage 53 | hs-source-dirs: main 54 | main-is: Main.hs 55 | build-depends: 56 | base >=4.7 && <5 57 | , text 58 | , stackage-cli 59 | default-language: Haskell2010 60 | 61 | executable stk 62 | hs-source-dirs: main 63 | main-is: Main.hs 64 | build-depends: 65 | base >=4.7 && <5 66 | , text 67 | , stackage-cli 68 | default-language: Haskell2010 69 | --------------------------------------------------------------------------------