{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE OverloadedStrings #-} module Main where import Codec.Compression.GZip ( compress ) import Control.Exception ( SomeException , handle ) import Control.Monad ( when ) import Control.Monad.Logger ( LogLevel(..) , runLoggingT ) import qualified Data.Aeson as A import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString.Lazy.Char8 as BSC import qualified Data.HashMap.Strict as HM import Data.Maybe ( fromMaybe ) import Data.Semigroup ( (<>) ) import qualified Data.Serialize as S import qualified Data.Text as T import qualified Data.Text.Encoding as TE import Data.Time ( getZonedTime ) import Data.Version ( showVersion ) import HaskellCodeExplorer.PackageInfo ( createPackageInfo , ghcVersion ) import qualified HaskellCodeExplorer.Types as HCE import Network.URI.Encode ( encode ) import Options.Applicative ( (<|>) , Parser , execParser , flag , fullDesc , help , helper , info , long , many , metavar , optional , progDesc , short , showDefault , strOption , value ) import Paths_haskell_code_explorer as HSE ( version ) import System.Directory ( createDirectoryIfMissing ) import System.Exit ( ExitCode(..) , exitWith ) import System.FilePath ( () ) import System.Log.FastLogger ( LoggerSet , ToLogStr(..) , defaultBufSize , fromLogStr , newFileLoggerSet , newStdoutLoggerSet , pushLogStrLn , rmLoggerSet ) data IndexerConfig = IndexerConfig { configPackageDirectoryPath :: FilePath , configPackageDistDirRelativePath :: Maybe FilePath , configOutputDirectoryName :: Maybe String , configLog :: !HCE.Log , configMinLogLevel :: !LogLevel , configSourceCodePreprocessing :: !HCE.SourceCodePreprocessing , configCompression :: !Compression , configGhcOptions :: [String] , configIgnoreDirectories :: [String] } deriving (Show, Eq) data Compression = Gzip | NoCompression deriving (Show, Eq) versionInfo :: String versionInfo = "haskell-code-indexer version " ++ showVersion version ++ ", GHC version " ++ showVersion ghcVersion main :: IO () main = do let description = "haskell-code-indexer collects and saves information about the source code of a Cabal package. " ++ versionInfo config <- execParser $ info (helper <*> configParser) (fullDesc <> progDesc description) loggerSet <- case configLog config of HCE.ToFile logfile -> newFileLoggerSet defaultBufSize logfile HCE.StdOut -> newStdoutLoggerSet defaultBufSize let minLogLevel = configMinLogLevel config logger loggerSet minLogLevel LevelInfo versionInfo logger loggerSet minLogLevel LevelDebug $ show config handle (\(e :: SomeException) -> do logger loggerSet minLogLevel LevelError (show e) rmLoggerSet loggerSet exitWith (ExitFailure 1) ) $ do packageInfo <- runLoggingT (createPackageInfo (configPackageDirectoryPath config) (configPackageDistDirRelativePath config) (configSourceCodePreprocessing config) (configGhcOptions config) (configIgnoreDirectories config) ) (\_loc _source level msg -> logger loggerSet minLogLevel level msg) let outputDir = configPackageDirectoryPath config fromMaybe HCE.defaultOutputDirectoryName (configOutputDirectoryName config) createDirectoryIfMissing False outputDir logger loggerSet minLogLevel LevelDebug $ "Output directory : " ++ outputDir BS.writeFile (outputDir HCE.packageInfoBinaryFileName) (S.encode $ HCE.toCompactPackageInfo packageInfo) mapM_ (\(HCE.HaskellModulePath path, modInfo) -> let (compressFunction, compressExtension) = case configCompression config of Gzip -> (compress, ".gz") NoCompression -> (id, "") filePath = outputDir ( encode (T.unpack path) ++ ".json" ++ compressExtension ) in BSL.writeFile filePath . compressFunction . A.encode $ modInfo ) . HM.toList $ HCE.moduleMap (packageInfo :: HCE.PackageInfo HCE.ModuleInfo) BSL.writeFile (outputDir HCE.packageInfoJsonFileName) (A.encode packageInfo) BSL.writeFile (outputDir "version.txt") (BSC.pack $ showVersion version) logger loggerSet minLogLevel LevelInfo ("Finished" :: T.Text) rmLoggerSet loggerSet configParser :: Parser IndexerConfig configParser = IndexerConfig <$> strOption ( long "package" <> short 'p' <> metavar "PATH" <> value "." <> showDefault <> help "Path to a Cabal package" ) <*> optional (strOption (long "dist" <> metavar "RELATIVE_PATH" <> help "Relative path to a dist directory" ) ) <*> optional (strOption (long "output" <> metavar "DIRECTORY_NAME" <> help "Output directory (default is '.haskell-code-explorer')" ) ) <*> ( pure HCE.StdOut <|> (HCE.ToFile <$> strOption (long "logfile" <> metavar "PATH" <> help "Path to a log file (by default log is written to stdout)" ) ) ) <*> flag LevelInfo LevelDebug (long "verbose" <> short 'v' <> help "Write debug information to a log" ) <*> flag HCE.AfterPreprocessing HCE.BeforePreprocessing ( long "before-preprocessing" <> help "Index source code before preprocessor pass (by default source code after preprocessing is indexed)" ) <*> flag Gzip NoCompression ( long "no-compression" <> help "Do not compress json files (by default json files are compressed using gzip)" ) <*> many (strOption (long "ghc" <> metavar "OPTIONS" <> help "Command-line options for GHC" ) ) <*> many (strOption (long "ignore" <> metavar "DIRECTORY_NAME" <> help "Directories to ignore (e.g. node_modules)" ) ) logger :: ToLogStr msg => LoggerSet -> LogLevel -> LogLevel -> msg -> IO () logger loggerSet minLogLevel logLevel msg = when (logLevel >= minLogLevel) $ do time <- getZonedTime let showLogLevel :: LogLevel -> T.Text showLogLevel LevelDebug = "[debug]" showLogLevel LevelInfo = "[info]" showLogLevel LevelWarn = "[warn]" showLogLevel LevelError = "[error]" showLogLevel (LevelOther t) = T.concat ["[", t, "]"] text = T.concat [ T.pack $ show time , " : " , showLogLevel logLevel , " " , TE.decodeUtf8 . fromLogStr . toLogStr $ msg ] pushLogStrLn loggerSet $ toLogStr text