module Main (main) where import Control.Arrow import Network.HTTP.Client.TLS (newTlsManager) import Control.Exception (try, SomeException) import Control.Monad import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Except import Data.Either import Data.List import qualified Distribution.Hackage.DB as DB import Distribution.Text import Distribution.Types.Version import Network.Socket (withSocketsDo) import Paths_codex (version) import System.Console.AsciiProgress (displayConsoleRegions) import System.Directory import System.Environment import System.Exit import System.FilePath import System.Process (shell, readCreateProcessWithExitCode) import Codex import Codex.Project import Codex.Internal (Builder(..), hackagePathOf, readStackPath) import Main.Config -- TODO Add 'cache dump' to dump all tags in stdout (usecase: pipe to grep) -- TODO Use a mergesort algorithm for `assembly` -- TODO Better error handling and fine grained retry retrying :: Int -> IO (Either a b) -> IO (Either [a] b) retrying n x = retrying' n $ fmap (left (:[])) x where retrying' 0 x' = x' retrying' n' x' = retrying' (n' - 1) $ x' >>= \res -> case res of Left ls -> fmap (left (++ ls)) x' Right r -> return $ Right r hashFile :: Codex -> FilePath hashFile cx = hackagePath cx "codex.hash" cleanCache :: (Builder, Codex) -> IO () cleanCache (bldr, cx) = do -- TODO Delete hash file! xs <- listDirectory hp ys <- builderOp bldr =<< traverseDirectories xs _ <- removeTagFiles $ concat ys return () where hp = hackagePath cx safe = (try :: IO a -> IO (Either SomeException a)) removeTagFiles = traverse (safe . removeFile) . fmap ( "tags") traverseDirectories = fmap rights . traverse (safe . listDirectory) builderOp (Stack _) = traverseDirectories . concat builderOp Cabal = return readCacheHash :: Codex -> IO (Maybe String) readCacheHash cx = do fileExist <- doesFileExist $ hashFile cx if not fileExist then return Nothing else do content <- readFile $ hashFile cx return $ Just content writeCacheHash :: Codex -> String -> IO () writeCacheHash cx = writeFile $ hashFile cx update :: Bool -> Codex -> Builder -> IO () update force cx bldr = displayConsoleRegions $ do (mpid, dependencies, workspaceProjects') <- case bldr of Cabal -> do tb <- DB.hackageTarball resolveCurrentProjectDependencies bldr tb Stack _ -> resolveCurrentProjectDependencies bldr $ hackagePath cx projectHash <- computeCurrentProjectHash cx shouldUpdate <- if null workspaceProjects' then either (const True) id <$> (runExceptT $ isUpdateRequired cx dependencies projectHash) else return True if force || shouldUpdate then do let workspaceProjects = if currentProjectIncluded cx then workspaceProjects' else filter (("." /=) . workspaceProjectPath) workspaceProjects' fileExist <- doesFileExist tagsFile when fileExist $ removeFile tagsFile putStrLn ("Updating: " ++ displayPackages mpid workspaceProjects) s <- newTlsManager tick' <- newProgressBar' "Loading tags" (length dependencies) results <- traverse (retrying 3 . runExceptT . getTags tick' s) dependencies _ <- traverse print . concat $ lefts results res <- runExceptT $ assembly bldr cx dependencies projectHash workspaceProjects tagsFile case res of Left e -> do print e exitFailure Right _ -> pure () else putStrLn "Nothing to update." where tagsFile = tagsFileName cx hp = hackagePathOf bldr cx getTags tick' s i = status hp i >>= \x -> case x of Source Tagged -> tick' >> return () Source Untagged -> tags bldr cx i >> tick' >> getTags tick' s i Archive -> extract hp i >> tick' >> getTags tick' s i Remote -> liftIO $ either ignore return <=< runExceptT $ fetch s hp i >> tick' >> getTags tick' s i where ignore msg = do putStrLn $ concat ["codex: *warning* unable to fetch an archive for ", display i] putStrLn msg return () displayPackages mpid workspaceProjects = case mpid of Just p -> display p Nothing -> unwords (fmap (display . workspaceProjectIdentifier) workspaceProjects) help :: IO () help = putStrLn $ unlines [ "Usage: codex [update] [cache clean] [set tagger [hasktags|ctags]] [set format [vim|emacs|sublime]]" , " [--help]" , " [--version]" , "" , " update Synchronize the tags file in the current cabal project directory" , " update --force Discard tags file hash and force regeneration" , " cache clean Remove all `tags` file from the local hackage cache]" , " set tagger Update the `~/.codex` configuration file for the given tagger (hasktags|ctags)." , " set format Update the `~/.codex` configuration file for the given format (vim|emacs|sublime)." , "" , "By default `hasktags` will be used, and need to be in the `PATH`, the tagger command can be fully customized in `~/.codex`." , "" , "Note: codex will browse the parent directory for cabal projects and use them as dependency over hackage when possible." ] main :: IO () main = withSocketsDo $ do cx <- loadConfig args <- getArgs run cx args where run cx ["cache", "clean"] = toBuilderConfig cx >>= cleanCache run cx ["update"] = withConfig cx (update False) run cx ["update", "--force"] = withConfig cx (update True) run cx ["set", "tagger", "ctags"] = encodeConfig $ cx { tagsCmd = taggerCmd Ctags } run cx ["set", "tagger", "hasktags"] = encodeConfig $ cx { tagsCmd = taggerCmd Hasktags } run cx ["set", "format", "emacs"] = encodeConfig $ cx { tagsCmd = taggerCmd HasktagsEmacs, tagsFileHeader = False, tagsFileSorted = False, tagsFileName = "TAGS" } run cx ["set", "format", "sublime"] = encodeConfig $ cx { tagsCmd = taggerCmd HasktagsExtended, tagsFileHeader = True, tagsFileSorted = True } run cx ["set", "format", "vim"] = encodeConfig $ cx { tagsFileHeader = True, tagsFileSorted = True } run _ ["--version"] = putStrLn $ concat ["codex: ", display (mkVersion' version)] run _ ["--help"] = help run _ [] = help run _ args = fail' $ concat ["codex: '", intercalate " " args,"' is not a codex command. See 'codex --help'."] toBuilderConfig cx' = checkConfig cx' >>= \state -> case state of TaggerNotFound -> fail' $ "codex: tagger not found." Ready -> do stackFileExists <- doesFileExist $ "." "stack.yaml" stackWorkExists <- doesDirectoryExist $ "." ".stack-work" if stackFileExists && stackWorkExists then do (ec, _, _) <- readCreateProcessWithExitCode (shell "which stack") "" case ec of ExitSuccess -> do let opts = stackOpts cx' globalPath <- readStackPath opts "stack-root" binPath <- readStackPath opts "bin-path" path <- getEnv "PATH" setEnv "PATH" $ concat [path, ":", binPath] return (Stack opts, cx' { hackagePath = globalPath "indices" "Hackage" }) _ -> return (Cabal, cx') else return (Cabal, cx') withConfig cx' f = do (bldr, cx) <- toBuilderConfig cx' cacheHash' <- readCacheHash cx case cacheHash' of Just cacheHash -> when (cacheHash /= codexHash cx) $ do putStrLn "codex: configuration has been updated, cleaning cache ..." cleanCache (bldr, cx) Nothing -> return () res <- f cx bldr writeCacheHash cx $ codexHash cx return res fail' msg = do putStrLn $ msg exitWith (ExitFailure 1)