{-# LANGUAGE ExplicitForAll #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} -- | Utilities which allows to use @stack@ tools for different -- dependencies stuff. module Importify.Stack ( QueryPackage (..) , LocalPackages (..) , RemotePackages (..) , ghcIncludePath , pkgName , stackListDependencies , stackListPackages , stackProjectRoot , upgradeWithVersions ) where import Universum import Data.List (partition) import Data.Yaml (FromJSON (parseJSON), Parser, Value (Object), decodeEither', prettyPrintParseException, withObject, (.:)) import Path (Abs, Dir, Path, PathException, dirname, fromAbsDir, mkRelDir, parent, parseAbsDir, ()) import Path.IO (doesDirExist) import System.FilePath (splitPath) import Turtle (Line, Shell, inproc, lineToText, linesToText, need) import Extended.System.Wlog (printWarning) import qualified Control.Foldl as Fold (head, list) import qualified Data.HashMap.Strict as HM import qualified Data.Text as T import qualified Turtle (fold) shStack :: [Text] -> Shell Line shStack args = do inNix <- inNixShell inproc "stack" (if inNix then "--nix" : args else args) empty -- | Checks if running in nix shell inNixShell :: MonadIO m => m Bool inNixShell = do ns <- need "IN_NIX_SHELL" pure $ ns == Just "1" pathArgs, rootArgs, depsArgs :: [Text] pathArgs = ["path", "--compiler-bin"] rootArgs = ["path", "--project-root"] depsArgs = ["list-dependencies", "--test", "--bench"] -- | This function finds path to directory where @include@ for ghc lies. -- Filepath looks like this: -- @ -- ~/.stack/programs/x86_64-linux/ghc-8.0.2/lib/ghc-8.0.2/include -- @ -- This function needed to tell dependencies about files like @"MachDeps.h"@. -- -- TODO: use GHC path from project? ghcIncludePath :: MaybeT IO (Path Abs Dir) ghcIncludePath = do ghcBinLine <- MaybeT $ Turtle.fold (shStack pathArgs) Fold.head -- ghcBinText ≡ /home/user/.stack/programs/x86_64-linux/ghc-8.0.2/bin ghcBinText <- parseAbsDir $ toString $ lineToText ghcBinLine let ghcProgram = parent ghcBinText -- w/o bin let ghcName = dirname ghcProgram -- ≡ ghc-8.0.2 -- ghcInclude ≡ /home/user/.stack/programs/x86_64-linux/ghc-8.0.2/lib/ghc-8.0.2/include let ghcIncludeDir = ghcProgram $(mkRelDir "lib/") ghcName $(mkRelDir "include/") guardM $ doesDirExist ghcIncludeDir return ghcIncludeDir -- | Acquires project using @stack path --project-root@ command. stackProjectRoot :: MaybeT IO (Path Abs Dir) stackProjectRoot = do projectRootLine <- MaybeT $ Turtle.fold (shStack rootArgs) Fold.head let projectRoot = lineToText projectRootLine if ".stack/global-project" `T.isSuffixOf` projectRoot then printWarning "importify was executed outside of project" *> empty else case eitherParseRoot projectRoot of Left exception -> printWarning (show exception) *> empty Right projectRootPath -> return projectRootPath where eitherParseRoot :: Text -> Either SomeException (Path Abs Dir) eitherParseRoot = parseAbsDir . toString -- | Extract all dependencies with versions using -- @stack list-dependencies@ shell command. stackListDependencies :: MonadIO m => m (HashMap Text Text) stackListDependencies = do dependencies <- Turtle.fold (shStack depsArgs) Fold.list let wordifyDeps = map (words . lineToText) dependencies let pairifyDeps = pairifyList wordifyDeps return $ HM.fromList pairifyDeps where pairifyList :: [[a]] -> [(a,a)] pairifyList [] = [] pairifyList ([x,y]:xs) = (x,y) : pairifyList xs pairifyList (_:xs) = pairifyList xs -- | Takes mapping from package names to their versions and list of -- packages adding version to each package which is inside dictionary. upgradeWithVersions :: HashMap Text Text -> [Text] -> [Text] upgradeWithVersions versions = go where go [] = [] go (lib:libs) = case HM.lookup lib versions of Nothing -> lib : go libs Just version -> lib <> "-" <> version : go libs -- | Queries list of all local packages for project. If some errors -- occur then warning is printed into console and empty list returned. stackListPackages :: forall m . (MonadIO m, MonadCatch m) => m (LocalPackages, RemotePackages) stackListPackages = do pkgsYaml <- linesToText <$> Turtle.fold (shStack ["query"]) Fold.list let parseRes = decodeEither' $ encodeUtf8 pkgsYaml case parseRes of Left exception -> do printWarning $ toText $ prettyPrintParseException exception return mempty Right (StackQueryResult packages) -> do localPackages <- mapM toPackage packages `catch` parseErrorHandler let (locals, remotes) = partition (isLocalPackage . qpPath) localPackages return (LocalPackages locals, RemotePackages remotes) where toPackage :: (Text, (FilePath, Text)) -> m QueryPackage toPackage (qpName, (path, qpVersion)) = do qpPath <- parseAbsDir path return QueryPackage{..} parseErrorHandler :: PathException -> m [QueryPackage] parseErrorHandler exception = [] <$ printWarning ("'stack query' exception: " <> show exception) isLocalPackage :: Path Abs Dir -> Bool isLocalPackage = notElem ".stack-work/" . splitPath . fromAbsDir -- | This data type represents package returned by @stack query@ command. data QueryPackage = QueryPackage { qpName :: Text -- ^ @importify@ , qpPath :: Path Abs Dir -- ^ @\/home\/user\/importify\/@ , qpVersion :: Text -- ^ 1.0 } deriving (Eq, Show) -- | Show full name of 'QueryPackage' with version. pkgName :: QueryPackage -> Text pkgName QueryPackage{..} = qpName <> "-" <> qpVersion -- | Local subpackages from exactly this project. newtype LocalPackages = LocalPackages [QueryPackage] deriving (Eq, Monoid) -- | Remote packages, from GitHub or other locations. newtype RemotePackages = RemotePackages [QueryPackage] deriving (Eq, Monoid) newtype StackQueryResult = StackQueryResult [(Text, (FilePath, Text))] deriving Show instance FromJSON StackQueryResult where parseJSON = withObject "stack query" $ \obj -> do Just (Object locals) <- pure $ HM.lookup "locals" obj packages <- forM locals $ withObject "package" $ \pkgObj -> do pkgPath :: FilePath <- pkgObj .: "path" pkgVersion :: Text <- pkgObj .: "version" pure (pkgPath, pkgVersion) pure $ StackQueryResult $ HM.toList packages