{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
module Stack.Tag where
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Data.Traversable as T
import Control.Exception as E
import Control.Monad.Reader
import Data.Either
import Data.Maybe
import Data.Text (Text)
import System.Directory
import System.Exit
import System.Process
import Control.Concurrent.Async.Pool
data StackTagOpts = StackTagOpts {
optsStackYaml :: !(Maybe FilePath)
, optsVerbose :: !Bool
, noCache :: !Bool
} deriving Show
data Tagger = Hasktags
| HotHasktags
| OtherTagger Text
deriving Show
data TagFmt = CTags
| ETags
| Both
| OtherFmt Text
deriving Show
type TagOutput = FilePath
type SourceDir = FilePath
type PkgName = String
data TagCmd = TagCmd Tagger TagFmt TagOutput SourceDir PkgName deriving Show
newtype StackTag a = StackTag {
runStackTag :: ReaderT StackTagOpts IO a
} deriving (
Functor
, Applicative
, Monad
, MonadReader StackTagOpts
, MonadIO
)
defStackOpts :: StackTagOpts
defStackOpts = StackTagOpts Nothing False True
stackTag :: StackTagOpts -> IO ()
stackTag = runReaderT (runStackTag app)
where
app = do chkStackCompatible
chkHaskTags
chkIsStack
sources <- stkPaths
depSources <- stkDepSources
tagSources sources depSources
io :: MonadIO m => IO a -> m a
io = liftIO
p :: String -> StackTag ()
p msg = whenM (ask >>= pure . optsVerbose) $ io (putStrLn msg)
whenM :: Monad m => m Bool -> m () -> m ()
whenM predicate go = predicate >>= flip when go
runStk :: [String] -> StackTag (ExitCode, String, String)
runStk args = io $ readProcessWithExitCode "stack" args []
chkIsStack :: StackTag ()
chkIsStack = do
StackTagOpts {optsStackYaml=stackYaml} <- ask
sYaml <- io $ doesFileExist "stack.yaml"
case stackYaml of
Nothing -> unless sYaml $ error "stack.yaml not found or specified!"
_ -> return ()
chkHaskTags :: StackTag ()
chkHaskTags = do
ht <- io $ findExecutable "hasktags"
case ht of
Just _p -> return ()
Nothing -> error "You must have hasktags installed Run 'stack install hasktags'."
chkStackCompatible :: StackTag ()
chkStackCompatible = do
(exitc, _, _) <- runStk ["ls", "dependencies", "--help"]
case exitc of
ExitSuccess -> return ()
ExitFailure _e ->
p (show exitc) >> error "You need stack version 1.7.1 or higher installed and in your PATH to use stack-tag"
stkPaths :: StackTag [(Text,[Text])]
stkPaths = do
(_,ps,_) <- runStk ["path"]
return (parsePaths ps)
where
parsePaths = map parsePath . T.lines . T.pack
parsePath ps = let (k,vs) = T.breakOn ":" ps
in (k, splitAndStrip vs)
splitAndStrip = filter (not . T.null) . map T.strip . T.splitOn ":"
stkDepSources :: StackTag [String]
stkDepSources = do
(_exitc,ls,_) <- runStk [ "ls", "dependencies", "--external"
, "--include-base", "--test"
, "--bench", "--separator=-"]
return $ lines ls
tagSources :: [(Text,[Text])] -> [FilePath] -> StackTag ()
tagSources srcs depsrcs = do
let srcDir = lookup "project-root" srcs
let tagroot = T.unpack . fromMaybe "." . listToMaybe . fromMaybe [] $ srcDir
depTagFiles <- parTag srcs depsrcs
thisProj <- runTagger (TagCmd Hasktags ETags "stack.tags" tagroot "project-root")
taggedSrcs <- T.traverse (io . readFile) (rights (thisProj : depTagFiles))
let errors = lefts (thisProj : depTagFiles)
unless (null errors) $ do
let pkg_errs = map (\(pkg,err) -> pkg ++ ": " ++ err) $ take 10 errors
error $ unlines $
"[tag:error] stack-tag encountered errors creating tags"
: pkg_errs
let xs = concatMap lines taggedSrcs
ys = if False then (Set.toList . Set.fromList) xs else xs
p $ "[tag:done] built & merged tags for " ++ show (length taggedSrcs) ++ " projects"
io $ writeFile "TAGS" $ unlines ys
parTag :: [(Text, [Text])] -> [FilePath] -> StackTag [Either (PkgName, String) FilePath]
parTag srcs depsrcs = do
o@StackTagOpts {noCache=nocache} <- ask
let worker osrc = flip runReaderT o $ do
runStackTag (tagDependency nocache srcs osrc)
io $ mapPool 3 worker depsrcs
tagDependency :: Bool -> [(Text, [Text])] -> FilePath -> StackTag (Either (PkgName, String) FilePath)
tagDependency nocache stkpaths dep = do
let snapRoot
| Just (sr : _) <- lookup "snapshot-install-root" stkpaths = sr
| otherwise = error ("[tag:error] error tagging "
++ dep
++ ". "
++ "No 'snapshot-install-root' found, aborting.")
dir = T.unpack snapRoot ++ "/packages" ++ "/" ++ dep
tagFile = dir ++ "/TAGS"
_ <- io $ readProcess "rm" ["--preserve-root", "-rf", dep] []
exists <- io $ doesDirectoryExist dir
tagged <- io $ doesFileExist tagFile
unless exists $ void $ do
io $ createDirectoryIfMissing True dir
p $ "[tag:download] " ++ dep
(ec,stout,_) <- runStk ["unpack", dep]
case ec of
ExitFailure _ -> void $ do
p $ "[tag:download] failed to download " ++ dep ++ " - " ++ stout
ExitSuccess -> void $ do
p $ "[tag:download] cp " ++ dep ++ " to snapshot source cache in " ++ dir
io $ readProcess "mv" [dep,dir] []
if tagged && nocache
then do p $ "[tag:nocache] " ++ dep
return (Right tagFile)
else do p $ "[tag:cache] " ++ dep
runTagger (TagCmd Hasktags ETags tagFile dir dep)
runTagger :: TagCmd -> StackTag (Either (PkgName, String) TagOutput)
runTagger (TagCmd t fmt to fp dep) = do
let opts = [ tagFmt fmt
, "-R"
, "--follow-symlinks"
, "--output"
, to
, fp
]
(ec, stout, err) <- hasktags opts
case ec of
ExitFailure _
| null err -> return $ Left (dep, stout)
| otherwise -> return $ Left (dep, err)
ExitSuccess -> return $ Right to
where
hasktags opts = io $
readProcessWithExitCode (tagExe t) opts []
`E.catch` (\(SomeException err) -> return (ExitFailure 1, displayException err, ""))
tagExe :: Tagger -> String
tagExe Hasktags = "hasktags"
tagExe _ = error "Tag command not supported. Feel free to create an issue at https://github.com/creichert/stack-tag"
tagFmt :: TagFmt -> String
tagFmt ETags = "--etags"
tagFmt CTags = "--etags"
tagFmt Both = "--both"
tagFmt _ = error "Tag format not supported. Feel free to create an issue at https://github.com/creichert/stack-tag"