stackage-setup (empty) → 0.0.0
raw patch · 6 files changed
+698/−0 lines, 6 filesdep +aesondep +basedep +bytestringsetup-changed
Dependencies added: aeson, base, bytestring, classy-prelude-conduit, cryptohash, cryptohash-conduit, filepath, http-client-tls, http-conduit, http-types, optparse-applicative, process, stackage-cli, system-fileio, system-filepath, text, unordered-containers, yaml
Files
- ChangeLog.md +3/−0
- LICENSE +20/−0
- README.md +17/−0
- Setup.hs +2/−0
- main/StackageSetup.hs +607/−0
- stackage-setup.cabal +49/−0
+ ChangeLog.md view
@@ -0,0 +1,3 @@+## 0.0.0++Initial release
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2012 FP Complete, http://www.fpcomplete.com/++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be+included in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ README.md view
@@ -0,0 +1,17 @@+stackage-setup+============++[](https://travis-ci.org/fpco/stackage-setup)++An executable for easily setting up a Haskell work environment. This project is similar in spirit to minGHC. It is currently Linux (64 bit) only, but with hopes of making it cross platform eventually.++Currently includes: ghc, cabal, happy, alex, and various+stackage-cli plugins.++```+$ stackage-setup ghc-7.8+```++```+$ stackage-setup ghc-7.10+```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ main/StackageSetup.hs view
@@ -0,0 +1,607 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE ViewPatterns #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE DeriveDataTypeable #-}++module Main where++import qualified ClassyPrelude.Conduit as ClassyPrelude+import ClassyPrelude.Conduit hiding ((<>))+import Control.Applicative+import Crypto.Hash+import Crypto.Hash.Conduit (sinkHash)+import Data.Aeson (FromJSON(..), withObject, (.:), (.:?), (.!=))+import qualified Data.Aeson as Aeson+import qualified Data.ByteString.Lazy as LByteString+import qualified Data.Char as Char+import qualified Data.HashMap.Strict as HashMap+import qualified Data.List as List+import Data.Monoid+import qualified Data.Text.Lazy as LText+import qualified Data.Text.Lazy.Encoding as LText+import qualified Data.Yaml as Yaml+import Options.Applicative (Parser, strArgument, metavar, value)+import Stackage.CLI+import Network.HTTP.Client.Conduit+import Network.HTTP.Client.TLS (tlsManagerSettings)+import Network.HTTP.Types (hUserAgent)+import Filesystem+import qualified Filesystem.Path.CurrentOS as Path+import System.FilePath (searchPathSeparator, getSearchPath)+import System.Environment (lookupEnv, getEnv, setEnv)+import System.Exit (exitFailure)+import System.Process (callProcess, readProcess)+import qualified Paths_stackage_setup as CabalInfo++import qualified Prelude+import Prelude (Bool(..))++version :: String+version = $(simpleVersion CabalInfo.version)++header :: String+header = "Retrieve ghc, cabal, etc, for use with stackage"++progDesc :: String+progDesc = header++userAgent :: ByteString+userAgent = "stackage-setup"++stackageHostDefault :: String+stackageHostDefault = "https://www.stackage.org"+++main :: IO ()+main = do+ (target,()) <- simpleOptions+ version+ header+ progDesc+ setupTargetParser+ empty+ withManagerSettings tlsManagerSettings+ $ withConfig+ $ curryReaderT R+ $ setup target++data R = R+ { rConfig :: Config+ , rManager :: Manager+ }++instance HasHttpManager R where+ getHttpManager = rManager++data Config = Config+ { configStackageRoot :: FilePath+ , configStackageHost :: String+ }++data StackageConfig = StackageConfig+ { _stackageHost :: String }++instance FromJSON StackageConfig where+ parseJSON = withObject "StackageConfig" $ \obj -> do+ _stackageHost <- obj .:? "stackage-host" .!= stackageHostDefault+ return StackageConfig{..}++-- Check if given processes appear to be present.+-- Halts the program with exit failure if any are missing.+-- TODO: make cross platform+checkDependencies :: [String] -> IO ()+checkDependencies deps = do+ missing <- mapM checkIsMissing deps+ when (or missing) $ do+ hPutStrLn stderr $ asText $+ "Please install missing dependencies and try again."+ exitFailure++-- return True if it appears to be missing+checkIsMissing :: String -> IO Bool+checkIsMissing process = do+ isMissing <- procIsMissing process+ `catch` \(e :: IOException) -> return True+ if isMissing+ then do+ hPutStrLn stderr $+ "Couldn't find required dependency: " <> process+ return True+ else return False++-- return True if it appears to be missing+procIsMissing :: String -> IO Bool+procIsMissing process = do+ output <- readProcess process ["--version"] ""+ return $ null output+++getStackageRoot :: GetConfig m => m FilePath+getStackageRoot = liftM configStackageRoot getConfig++getStackageHost :: GetConfig m => m String+getStackageHost = liftM configStackageHost getConfig++class HasConfig env where+ accessConfig :: env -> Config+instance HasConfig R where+ accessConfig = rConfig++-- TODO: check environment properly+getStackageRootIO :: IO FilePath+getStackageRootIO = do+ stackageRoot <- lookupEnv "STACKAGE_ROOT" >>= \case+ Just dir -> return $ Path.decodeString dir+ Nothing -> do+ -- TODO: windows-ify+ home <- lookupEnv "HOME" >>= \case+ Just homeStr -> return (fromString homeStr)+ Nothing -> throwIO StackageRootNotFound+ return (home </> ".stackage")+ createTree stackageRoot+ return stackageRoot++readFileMay :: FilePath -> IO (Maybe ByteString)+readFileMay path = do+ exists <- isFile path+ if exists+ then Just <$> ClassyPrelude.readFile path+ else return Nothing++getFirstJust :: [IO (Maybe a)] -> IO (Maybe a)+getFirstJust [] = return Nothing+getFirstJust (xIO:xsIO) = do+ xMay <- xIO+ case xMay of+ Just{} -> return xMay+ Nothing -> getFirstJust xsIO++getStackageHostIO :: FilePath -> IO String+getStackageHostIO stackageRoot = do+ bsMay <- getFirstJust+ [ readFileMay (stackageRoot </> "config")+ , readFileMay (stackageRoot </> "config.yaml")+ , readFileMay (stackageRoot </> "config.yml")+ ]+ case bsMay of+ Nothing -> return stackageHostDefault+ Just bs -> case Yaml.decodeEither' bs of+ Left{} -> return stackageHostDefault+ Right stackageConfig -> return (_stackageHost stackageConfig)+++getConfigIO :: IO Config+getConfigIO = do+ configStackageRoot <- getStackageRootIO+ configStackageHost <- getStackageHostIO configStackageRoot+ return Config{..}++class Monad m => GetConfig m where+ getConfig :: m Config+instance GetConfig IO where+ getConfig = getConfigIO+instance (HasConfig env, Monad m)+ => GetConfig (ReaderT env m) where+ getConfig = liftM accessConfig ask+++withConfig :: (MonadIO m)+ => ReaderT Config m a -> m a+withConfig m = do+ config <- liftIO getConfigIO+ runReaderT m config++curryReaderT :: (r1 -> r2 -> env) -> ReaderT env m a -> ReaderT r1 (ReaderT r2 m) a+curryReaderT tup m =+ ReaderT $ \r1 ->+ ReaderT $ \r2 ->+ runReaderT m $ tup r1 r2++arch :: String+arch = "linux64"++ltsSnapshotsReq :: (MonadThrow m, GetConfig m) => m Request+ltsSnapshotsReq = do+ stackageHost <- getStackageHost+ parseUrl $ stackageHost <> "/download/lts-snapshots.json"++ghcMajorVersionReq :: (MonadThrow m, GetConfig m) => Snapshot -> m Request+ghcMajorVersionReq snapshot = do+ stackageHost <- getStackageHost+ parseUrl $ stackageHost <> "/snapshot/" <> snapshot <> "/ghc-major-version"++getLinksReq :: (MonadThrow m, GetConfig m) => GhcMajorVersion -> m Request+getLinksReq ghcMajorVersion = do+ stackageHost <- getStackageHost+ parseUrl $ stackageHost <> "/download/" <> arch <> "/ghc-" <> ghcMajorVersion <> "-links.yaml"++setupTargetParser :: Parser SetupTarget+setupTargetParser = strArgument mods where+ mods = metavar "TARGET" <> value "lts"++refreshLtsSnapshots ::+ ( HasHttpManager env+ , MonadReader env m+ , GetConfig m+ , MonadThrow m+ , MonadIO m+ ) => m (HashMap String String)+refreshLtsSnapshots = do+ stackageRoot <- getStackageRoot+ let path = Path.encodeString $ stackageRoot </> ltsSnapshotsPath++ response <- httpLbs =<< ltsSnapshotsReq+ let lbs = responseBody response+ liftIO $ LByteString.writeFile path lbs++ either (throwM . ParseLtsSnapshotsError) return $ Aeson.eitherDecode lbs+++getLinks ::+ ( MonadIO m+ , MonadThrow m+ , MonadReader env m+ , GetConfig m+ , HasHttpManager env+ ) => GhcMajorVersion -> m [Download]+getLinks ghcMajorVersion = do+ stackageRoot <- getStackageRoot+ response <- httpLbs =<< getLinksReq ghcMajorVersion+ let lbs = responseBody response+ bs = LByteString.toStrict lbs+ path = Path.encodeString $ stackageRoot </> linksPath ghcMajorVersion+ liftIO $ LByteString.writeFile path lbs++ either (throwM . ParseLinksError) return $ Yaml.decodeEither' bs++data Download = Download+ { downloadName :: Text+ , downloadVersion :: Text+ , downloadUrl :: String+ , downloadSha1 :: String+ , downloadInstructions :: [Text]+ }++instance FromJSON Download where+ parseJSON = withObject "Download" $ \obj -> do+ downloadName <- obj .: "name"+ downloadVersion <- obj .: "version"+ downloadUrl <- obj .: "url"+ downloadSha1 <- obj .: "sha1"+ downloadInstructions <- obj .: "instructions"+ return Download{..}++type SetupTarget = String+type GhcMajorVersion = String+type Series = String+type Snapshot = String++data SetupExceptions+ = SeriesNotFound Series+ | ParseLtsSnapshotsError String+ | ParseLinksError Yaml.ParseException+ | StackageRootNotFound+ deriving (Show, Typeable)+instance Exception SetupExceptions++readSeries :: String -> Maybe Series+readSeries s@"lts" = Just s+readSeries s@(stripPrefix "lts-" -> Just sver)+ | all Char.isNumber sver = Just s+readSeries (stripPrefix "lts/" -> Just sver)+ | all Char.isNumber sver = Just $ "lts-" <> sver+readSeries _ = Nothing++readGhcVersion :: String -> Maybe GhcMajorVersion+readGhcVersion (stripPrefix "ghc-" -> Just s) = case break (== '.') s of+ (m1, '.':m2) | all Char.isNumber m1 && all Char.isNumber m2+ -> Just s+ _ -> Nothing+readGhcVersion _ = Nothing++getGhcMajorVersion ::+ ( HasHttpManager env+ , MonadReader env m+ , GetConfig m+ , MonadThrow m+ , MonadIO m+ ) => String -> m GhcMajorVersion+getGhcMajorVersion target = case readGhcVersion target of+ Just version -> return version+ Nothing -> do+ snapshot <- case readSeries target of+ Just series -> lookupSnapshot series+ Nothing -> return target -- just try using it as a snapshot+ putStrLn $ "Setup for snapshot: " <> pack snapshot+ lookupGhcMajorVersion snapshot++lookupSnapshot ::+ ( HasHttpManager env+ , MonadReader env m+ , GetConfig m+ , MonadThrow m+ , MonadIO m+ ) => Series -> m Snapshot+lookupSnapshot series = do+ ltsSnapshots <- refreshLtsSnapshots+ case HashMap.lookup series ltsSnapshots of+ Just snapshot -> return snapshot+ Nothing -> throwM $ SeriesNotFound series+++lookupGhcMajorVersion ::+ ( HasHttpManager env+ , MonadReader env m+ , GetConfig m+ , MonadThrow m+ , MonadIO m+ ) => Snapshot -> m GhcMajorVersion+lookupGhcMajorVersion snapshot = do+ response <- httpLbs =<< ghcMajorVersionReq snapshot+ let lbs = responseBody response+ return $ unpack $ LText.toStrict $ LText.decodeUtf8 lbs+++setup ::+ ( HasHttpManager env+ , MonadReader env m+ , GetConfig m+ , MonadThrow m+ , MonadIO m+ , MonadBaseControl IO m+ ) => SetupTarget -> m ()+setup target = do+ ghcMajorVersion <- getGhcMajorVersion target+ putStrLn $ "Selecting ghc-" <> pack ghcMajorVersion+ links <- getLinks ghcMajorVersion++ stackageRoot <- getStackageRoot+ forM_ links $ \d@Download{..} -> do+ let dir = stackageRoot </> downloadDir downloadName+ liftIO $ createTree dir++ let versionedDir = dir </> downloadPath downloadName downloadVersion+ exists <- liftIO $ isDirectory versionedDir++ if (not exists)+ then do+ liftIO $ checkDependencies (depsFor downloadName)+ download downloadUrl downloadSha1 dir+ postDownload d dir versionedDir+ else putStrLn $ "Already have: " <> downloadName <> "-" <> downloadVersion++ augmentPath (versionedDir </> "bin")++-- The dependencies required for stackage-setup's implementation of+-- the instructions for a given download item.+depsFor :: Text -> [String]+depsFor "ghc" = ["make", "tar", "xz"]+depsFor "cabal" = ["tar", "gzip"]+depsFor "stackage" = ["tar", "xz"]+depsFor "alex" = ["tar", "xz"]+depsFor "happy" = ["tar", "xz"]+depsFor _ = []++-- TODO: ensure this works cross-platform+augmentPath :: MonadIO m => FilePath -> m ()+augmentPath pathHead = liftIO $ do+ pathRest <- getSearchPath+ let paths = fpToString pathHead : pathRest+ path = intercalate [searchPathSeparator] paths+ setEnv "PATH" path+++postDownload :: (MonadIO m)+ => Download -> Path.FilePath -> Path.FilePath -> m ()+postDownload d@Download{..} dir versionedDir = do+ expectInstructions downloadName d dir versionedDir++expectInstructions :: (MonadIO m) => Text -> Download -> Path.FilePath -> Path.FilePath -> m ()+expectInstructions "ghc" = expectGhcInstructions+expectInstructions "cabal" = expectCabalInstructions+expectInstructions "stackage" = justUnpackInstructions+expectInstructions "alex" = justUnpackInstructions+expectInstructions "happy" = justUnpackInstructions+expectInstructions t = unexpectedInstructions t++unexpectedInstructions :: (MonadIO m) => Text -> Download -> FilePath -> FilePath -> m ()+unexpectedInstructions t Download{..} dir _ = do+ putStrLn $ "Unexpected download: " <> t+ when (not $ null downloadInstructions) $ do+ putStrLn $ "Manual instructions:"+ putStrLn $ "$ cd " <> fpToText dir+ mapM_ (putStrLn . ("$ " <>)) downloadInstructions++justUnpackInstructions :: (MonadIO m) => Download -> FilePath -> FilePath -> m ()+justUnpackInstructions Download{..} dir _ = do+ liftIO $ go downloadInstructions+ where+ go :: [Text] -> IO ()+ go [] = return ()+ go ( (stripPrefix "tar xJf " -> Just file)+ : next+ ) = unzipXZ dir (Path.fromText file) >> go next+ go ( (stripPrefix "rm " -> Just _file)+ : next+ ) = go next -- already done in unzipXZ+ go (t:_) = fail $ "command not recognized: " <> unpack t+++expectGhcInstructions :: (MonadIO m) => Download -> Path.FilePath -> Path.FilePath -> m ()+expectGhcInstructions Download{..} dir versionedDir =+ liftIO $ go downloadInstructions+ where+ go :: [Text] -> IO ()+ go [] = return ()+ go ( (stripPrefix "tar xJf " -> Just file)+ : next+ ) = unzipXZ dir (Path.fromText file) >> go next+ go ( (stripPrefix "rm ghc-" -> Just _file)+ : next+ ) = go next -- already done in unzipXZ+ go ( (stripPrefix "cd ghc-" -> Just version)+ : "./configure --prefix=`pwd`"+ : "make install"+ : "cd .."+ : next+ ) | version == downloadVersion+ = ghcConfigureInstall versionedDir >> go next+ go (t:_) = fail $ "command not recognized: " <> unpack t+++inDir :: Path.FilePath -> IO a -> IO a+inDir dir action = do+ workingDir <- getWorkingDirectory+ let enter = setWorkingDirectory dir+ exit = setWorkingDirectory workingDir+ bracket_ enter exit action+++ghcConfigureInstall :: (MonadIO m) => Path.FilePath -> m ()+ghcConfigureInstall versionedDir = liftIO $ do+ let srcDir = versionedDir <.> "src"+ whenM (isDirectory srcDir) $ removeTree srcDir -- TODO: reuse instead+ rename versionedDir srcDir+ createTree versionedDir+ let go = inDir srcDir $ do+ callProcess "./configure" ["--prefix=" <> Path.encodeString versionedDir]+ callProcess "make" ["install"]+ go `onException` removeTree versionedDir+ removeTree srcDir++expectCabalInstructions :: (MonadIO m) => Download -> Path.FilePath -> Path.FilePath -> m ()+expectCabalInstructions Download{..} dir versionedDir =+ liftIO $ rememberingOldCabal $ go downloadInstructions+ where+ go [] = return ()+ go ( (stripPrefix "tar xzf " -> Just file)+ : next+ ) = unzipGZ dir (Path.fromText file) >> go next+ go ( (stripPrefix "rm cabal-install-" ->+ Just (stripSuffix ".tar.gz" -> Just _file))+ : next+ ) = go next -- already done in unzipGZ+ go ( (stripPrefix "cd cabal-install-" -> Just version)+ : "./bootstrap.sh"+ : "cd .."+ : (stripPrefix "rm -r cabal-install-" -> Just version')+ : next+ ) | version == downloadVersion && version == version'+ = cabalBootstrap dir version >> go next+ go ( (stripPrefix "mkdir cabal-" -> Just version)+ : (stripPrefix "mv $HOME/.cabal/bin/cabal cabal-" -> Just _newLoc)+ : next+ ) | version == downloadVersion+ = cabalMoveExe versionedDir >> go next+ go (t:_) = fail $ "command not recognized: " <> unpack t++cabalBootstrap :: FilePath -> Text -> IO ()+cabalBootstrap dir version = do+ let cabalInstallVersionedDir = dir </> Path.fromText ("cabal-install-" <> version)+ inDir cabalInstallVersionedDir $ do+ callProcess "./bootstrap.sh" []+ removeTree cabalInstallVersionedDir++cabalMoveExe :: FilePath -> IO ()+cabalMoveExe versionedDir = do+ let versionedDirBin = versionedDir </> "bin"+ createTree versionedDirBin+ homeCabal <- getHomeCabal+ rename homeCabal (versionedDirBin </> "cabal")++getHomeCabal :: IO FilePath+getHomeCabal = do+ -- TODO: errors+ Just home <- lookupEnv "HOME"+ return $ Path.decodeString home </> ".cabal" </> "bin" </> "cabal"++rememberingOldCabal :: IO a -> IO a+rememberingOldCabal action = do+ homeCabal <- getHomeCabal+ exists <- isFile homeCabal+ if exists+ then do+ let tempCabal = homeCabal <.> "old"+ toTemp = rename homeCabal tempCabal+ fromTemp = rename tempCabal homeCabal+ bracket_ toTemp fromTemp action+ else action++-- TODO: use Text more than String+download ::+ ( MonadIO m+ , MonadBaseControl IO m+ , MonadThrow m+ , MonadReader env m+ , HasHttpManager env+ ) => String -> String -> Path.FilePath -> m ()+download url sha1 dir = do+ -- TODO: deal with partial function last+ let fname = List.drop (List.last slashes + 1) url+ slashes = List.findIndices (== '/') url+ file = dir </> Path.decodeString fname+ putStrLn $ "Downloading: " <> pack fname+ req0 <- parseUrl url+ let req = req0+ { requestHeaders = [(hUserAgent, userAgent)] }+ runResourceT $ withResponse req $ \res -> do+ let source = responseBody res+ let sink = getZipSink $+ ZipSink sinkHash <* ZipSink (sinkFile file)+ hash <- source $$ sink+ let _ = hash :: Digest SHA1+ when (show hash /= sha1) $ do+ fail "Corrupted download"+ putStrLn $ "Verified sha1: " <> pack sha1++-- TODO: make cross platform+unzipDownload :: (MonadIO m)+ => Path.FilePath -> Path.FilePath -> m ()+unzipDownload dir file = case stripSuffix ".tar.xz" (fpToText file) of+ Just{} -> unzipXZ dir file+ _ -> case stripSuffix ".tar.gz" (fpToText file) of+ Just {} -> unzipGZ dir file+ _ -> fail $ "unzipDownload: unknown extension"+ -- TODO: other extensions++-- TODO: make cross platform+unzipXZ :: (MonadIO m)+ => Path.FilePath -> Path.FilePath -> m ()+unzipXZ dir file = liftIO $ inDir dir $ do+ putStrLn "Decompressing XZ archive"+ callProcess "tar"+ ["xJf"+ , Path.encodeString file+ ]+ removeFile file+++-- TODO: make cross platform+unzipGZ :: (MonadIO m)+ => Path.FilePath -> Path.FilePath -> m ()+unzipGZ dir file = liftIO $ inDir dir $ do+ putStrLn "Decompressing GZ archive"+ callProcess "tar"+ ["xzf"+ , Path.encodeString file+ ]+ removeFile file++downloadDir :: Text -> Path.FilePath+downloadDir name =+ "environment" </> Path.fromText name++downloadPath :: Text -> Text -> Path.FilePath+downloadPath name version =+ Path.fromText (name <> "-" <> version)++ltsSnapshotsPath :: Path.FilePath+ltsSnapshotsPath = Path.fromText "lts-snapshots.json"++linksPath :: GhcMajorVersion -> Path.FilePath+linksPath ghcMajorVersion = Path.fromText $ "ghc-" <> pack ghcMajorVersion <> "-links.yaml"
+ stackage-setup.cabal view
@@ -0,0 +1,49 @@+name: stackage-setup+version: 0.0.0+synopsis:+ An executable for downloading a Haskell setup+description:+ An executable for downloading a Haskell setup+homepage:+ https://www.stackage.org/package/stackage-setup+bug-reports:+ https://github.com/fpco/stackage-setup/issues++license: MIT+license-file: LICENSE+author: Dan Burton+maintainer: danburton@fpcomplete.com+copyright: 2015 FP Complete Corporation++build-type: Simple+cabal-version: >=1.10+category: Development+extra-source-files: README.md ChangeLog.md++source-repository head+ type: git+ location: git://github.com/fpco/stackage-setup.git++executable stackage-setup+ hs-source-dirs: main+ main-is: StackageSetup.hs+ build-depends:+ base >=4.7 && <5+ , text+ , stackage-cli+ , system-fileio+ , system-filepath+ , optparse-applicative+ , http-conduit+ , http-client-tls+ , classy-prelude-conduit+ , unordered-containers+ , cryptohash+ , cryptohash-conduit+ , http-types+ , process+ , aeson+ , yaml+ , bytestring+ , filepath+ default-language: Haskell2010