1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
|
{-# LANGUAGE CPP #-}
{-# 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 (Version(..),showVersion)
import HaskellCodeExplorer.PackageInfo (createPackageInfo)
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
, strOption
)
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)
#if MIN_VERSION_GLASGOW_HASKELL(8,6,3,0)
ghcVersion :: Version
ghcVersion = Version {versionBranch = [8, 6, 3, 0], versionTags = []}
#elif MIN_VERSION_GLASGOW_HASKELL(8,4,4,0)
ghcVersion :: Version
ghcVersion = Version {versionBranch = [8, 4, 4, 0], versionTags = []}
#elif MIN_VERSION_GLASGOW_HASKELL(8,4,3,0)
ghcVersion :: Version
ghcVersion = Version {versionBranch = [8, 4, 3, 0], versionTags = []}
#elif MIN_VERSION_GLASGOW_HASKELL(8,2,2,0)
ghcVersion :: Version
ghcVersion = Version {versionBranch = [8, 2, 2, 0], versionTags = []}
#else
ghcVersion :: Version
ghcVersion = Version {versionBranch = [8, 0, 2, 0], versionTags = []}
#endif
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" <>
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
|