-
Notifications
You must be signed in to change notification settings - Fork 8
/
Setup.hs
316 lines (294 loc) · 13.7 KB
/
Setup.hs
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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
{-# LANGUAGE OverloadedStrings #-}
-- | Before each build, generate a BuildInfo_Generated module that exports the project version from cabal,
-- the current revision number and the build time. Also generate a file that contains files that
-- are being included into the ampersand.exe file
-- Note that in order for this Setup.hs to be used by cabal, the build-type should be Custom.
module Main where
-- import qualified Codec.Compression.GZip as GZip --TODO replace by Codec.Archive.Zip from package zip-archive. This reduces the amount of packages. (We now use two for zipping/unzipping)
import Codec.Archive.Zip
import Distribution.PackageDescription
import Distribution.Pretty (prettyShow)
import Distribution.Simple
import Distribution.Simple.LocalBuildInfo
import Distribution.Simple.Setup
import RIO
import RIO.Char
import qualified RIO.List as L
import qualified RIO.NonEmpty as NE
import qualified RIO.Text as T
import RIO.Time
import System.Directory
import System.Environment (getEnvironment)
import qualified System.Exit as SE
import System.FilePath
import System.Process (readProcessWithExitCode)
import Prelude (print, putStrLn)
main :: IO ()
main =
defaultMainWithHooks
simpleUserHooks
{ buildHook = customBuildHook,
replHook = customReplHook
}
-- | Generate Haskell modules that are required for the build and start the build
customBuildHook :: PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO ()
customBuildHook pd lbi uh bf = do
generateBuildInfoModule (T.pack . prettyShow . pkgVersion . package $ pd)
generateStaticFileModule
buildHook simpleUserHooks pd lbi uh bf -- start the build
-- | Generate Haskell modules that are required for the build and start the build
customReplHook :: PackageDescription -> LocalBuildInfo -> UserHooks -> ReplFlags -> [String] -> IO ()
customReplHook pd lbi uh rf args = do
generateBuildInfoModule (T.pack . prettyShow . pkgVersion . package $ pd)
generateStaticFileModule
replHook simpleUserHooks pd lbi uh rf args -- start the build
generateBuildInfoModule :: Text -> IO ()
-- | Generate a Haskell module that contains information that is available
-- only during build time.
generateBuildInfoModule cabalVersionStr = do
content <-
buildInfoModule cabalVersionStr
<$> getGitInfoStr
<*> ( T.pack
. formatTime defaultTimeLocale "%d-%b-%y %H:%M:%S %Z"
<$> (getCurrentTime >>= utcToLocalZonedTime)
)
writeFileUtf8 (pathFromModuleName buildInfoModuleName) content
where
buildInfoModuleName :: Text
buildInfoModuleName = "Ampersand.Basics.BuildInfo_Generated"
buildInfoModule :: Text -> Text -> Text -> Text
buildInfoModule cabalVersion gitInfo time =
T.unlines
[ "{-# LANGUAGE OverloadedStrings #-}",
"-- | This module is generated automatically by Setup.hs before building. Do not edit!",
"-- It contains some functions that are supposed to grab information at the time of",
"-- building the ampersand executable.",
"module " <> buildInfoModuleName <> "(",
" cabalVersionStr",
" , gitInfoStr",
" , buildTimeStr",
" ) where",
"import RIO.Text (Text)",
"",
"{-" <> "# NOINLINE cabalVersionStr #-}", -- disable inlining to prevent recompilation of dependent modules on each build
"-- | The version of Ampersand as it is stated in the package.yaml file.",
"cabalVersionStr :: Text",
"cabalVersionStr = \"" <> cabalVersion <> "\"",
"",
"{-" <> "# NOINLINE gitInfoStr #-}",
"-- | The version of Ampersand as seen by Git.",
"gitInfoStr :: Text",
"gitInfoStr = \"" <> gitInfo <> "\"",
"",
"{-" <> "# NOINLINE buildTimeStr #-}",
"-- | The time of the build.",
"buildTimeStr :: Text",
"buildTimeStr = \"" <> time <> "\"",
""
]
getGitInfoStr :: IO Text
getGitInfoStr = getInfoStr `catch` warnGracefully
where
getInfoStr :: IO Text
getInfoStr = do
eSHA <- readProcessEither "git" ["rev-parse", "--short", "HEAD"] ""
eBranch <- readProcessEither "git" ["rev-parse", "--abbrev-ref", "HEAD"] ""
(exitCode, _, _) <- readProcessWithExitCode "git" ["diff", "--quiet"] ""
let isDirty = exitCode /= SE.ExitSuccess -- exit code signals whether branch is dirty
case (eSHA, eBranch) of
(Right sha, Right branch) ->
return $ gitInfoStr sha branch isDirty
_ -> do
-- ci/cd will create some custom environment variables.
-- This is required in case of usage of cabal 2.4 or greater.
-- (See https://github.com/haskell/cabal/issues/5934 for
-- the discussion)
env <- getEnvironment
case ( lookup "GIT_SHA" env,
lookup "GIT_Branch" env
) of
(Just sha, Just branch) ->
return $ gitInfoStr (T.pack branch) (T.pack sha) False
_ -> do
mapM_ print $ lefts [eSHA, eBranch] -- errors during git execution
warnNoCommitInfo
warnGracefully :: IOException -> IO Text
warnGracefully err = do
print (err :: IOException)
warnNoCommitInfo
gitInfoStr :: Text -> Text -> Bool -> Text
gitInfoStr sha branch isDirty =
strip branch <> ":" <> strip sha <> (if isDirty then "*" else "")
strip :: Text -> Text
strip = T.reverse . T.dropWhile isSpace . T.reverse
readProcessEither :: FilePath -> [Text] -> Text -> IO (Either Text Text)
readProcessEither cmd args stdinStr = do
(exitCode, stdoutStr, stderrStr) <- readProcessWithExitCode cmd (map T.unpack args) (T.unpack stdinStr)
case exitCode of
SE.ExitSuccess -> return . Right . T.pack $ stdoutStr
SE.ExitFailure _ -> return . Left . T.pack $ stderrStr
warnNoCommitInfo :: IO Text
warnNoCommitInfo = do
putStrLn ""
putStrLn ""
putStrLn "WARNING: Execution of 'git' command failed."
putStrLn "BuildInfo_Generated.hs will not contain revision information, and"
putStrLn " therefore neither will fatal error messages."
putStrLn " Please check your installation."
return "no git info"
-- | datatype for several kinds of files to be included into the ampersand executable
data FileKind
= -- | Pandoc template files for the generation of documents in different pandoc formats.
PandocTemplates
| -- | The adl script files for formal ampersand
FormalAmpersand
| -- | The adl script files for the prototype context
PrototypeContext
deriving (Show, Eq, Bounded, Enum)
generateStaticFileModule :: IO ()
-- | For each set of files (by FileKind), we generate an Archive value,
-- which contains the information necessary for Ampersand to create the file at run-time.
--
-- To prevent compiling the generated module (which can get rather big) on each build, we compare the contents
-- the file we are about to generate with the contents of the already generated file and only write if there is a difference.
-- This complicates the build process, but seems the only way to handle large amounts of diverse static
-- files, until Cabal's data-files mechanism is updated to allow fully recursive patterns.
generateStaticFileModule = do
previousModuleContents <- getPreviousModuleContents
currentModuleContents <- readAllStaticFiles
let updateRequired = previousModuleContents == currentModuleContents
if updateRequired
then putStrLn $ "Static files unchanged, no need to update " <> sfModulePath
else do
putStrLn $ "Static files have changed, updating " <> sfModulePath
writeFileUtf8 sfModulePath currentModuleContents
where
staticFileModuleName :: Text
staticFileModuleName = "Ampersand.Prototype.StaticFiles_Generated"
sfModulePath = pathFromModuleName staticFileModuleName
getPreviousModuleContents :: IO Text
getPreviousModuleContents = reader `catch` errorHandler
where
reader :: IO Text
reader = readFileUtf8 sfModulePath
errorHandler err = do
-- old generated module exists, but we can't read the file or read the contents
putStrLn
$ unlines
[ "",
"Info: No cache for static files: " <> show (err :: SomeException),
""
]
return mempty
readAllStaticFiles :: IO Text
readAllStaticFiles = do
pandocTemplatesFiles <- readStaticFiles PandocTemplates "." -- templates for several PANDOC output types
formalAmpersandFiles <- readStaticFiles FormalAmpersand "." -- meta information about Ampersand
systemContextFiles <- readStaticFiles PrototypeContext "." -- Context for prototypes that Ampersand generates.
return $ mkStaticFileModule $ pandocTemplatesFiles <> formalAmpersandFiles <> systemContextFiles
readStaticFiles :: FileKind -> FilePath -> IO [(FileKind, Entry)]
readStaticFiles fkind fileOrDirPth = do
let path = base </> fileOrDirPth
isDir <- doesDirectoryExist path
if isDir
then do
fOrDs <- getProperDirectoryContents path
fmap concat $ mapM (\fOrD -> readStaticFiles fkind (fileOrDirPth </> fOrD)) fOrDs
else do
entry <- removeBase <$> readEntry [OptVerbose] (base </> fileOrDirPth)
return [(fkind, entry)]
where
removeBase :: Entry -> Entry
removeBase entry = entry {eRelativePath = rpWithoutBase}
where
rpWithoutBase = stripbase (eRelativePath entry)
stripbase :: FilePath -> FilePath
stripbase fp = case L.stripPrefix (base ++ "/") fp of
Just stripped -> stripped
Nothing ->
error
. L.intercalate "\n"
$ [ "ERROR: Reading static files failed:",
" base: " <> base,
" fp : " <> fp
]
base = case fkind of
PandocTemplates -> "outputTemplates"
FormalAmpersand -> "AmpersandData/FormalAmpersand"
PrototypeContext -> "AmpersandData/PrototypeContext"
mkStaticFileModule :: [(FileKind, Entry)] -> Text
mkStaticFileModule xs =
T.unlines staticFileModuleHeader
<> " [ "
<> T.intercalate "\n , " (map toText archives)
<> "\n"
<> " ]\n"
where
toText :: (FileKind, Archive) -> Text
toText (fk, archive) = "SF " <> tshow fk <> " " <> tshow archive
archives :: [(FileKind, Archive)]
archives = map mkArchive $ NE.groupBy tst xs
where
tst :: (FileKind, a) -> (FileKind, a) -> Bool
tst a b = fst a == fst b
mkArchive :: NE.NonEmpty (FileKind, Entry) -> (FileKind, Archive)
mkArchive entries =
( fst . NE.head $ entries,
foldr addEntryToArchive emptyArchive $ snd <$> NE.toList entries
)
staticFileModuleHeader :: [Text]
staticFileModuleHeader =
[ "{-# LANGUAGE OverloadedStrings #-}",
"module " <> staticFileModuleName,
" ( FileKind(..)",
" , getStaticFileContent",
" )",
"where",
"import Ampersand.Basics",
"import Codec.Archive.Zip",
"import qualified RIO.ByteString as B",
"import qualified RIO.ByteString.Lazy as BL",
"import qualified RIO.Text as T",
"",
"data FileKind = PandocTemplates | FormalAmpersand | PrototypeContext deriving (Show, Eq)",
"data StaticFile = SF FileKind Archive",
"",
"getStaticFileContent :: FileKind -> FilePath -> Maybe B.ByteString",
"getStaticFileContent fk fp = BL.toStrict <$>",
" case filter isRightArchive allStaticFiles of",
" [SF _ a] -> case findEntryByPath fp a of",
" Just entry -> Just $ fromEntry entry",
" Nothing -> fatal . T.intercalate \"\\n\" $ ",
" [ \"Looking for file: \"<>tshow fp",
" , \"in archive: \"<>tshow fk",
" , \"Archive found. it contains:\"",
" ]++map ((\" \" <>) . showEntry) (zEntries a)",
" xs -> fatal . T.intercalate \"\\n\" $",
" [ \"Looking for file: \"<>tshow fp",
" , \"in archive: \"<>tshow fk",
" ]++showArchives xs",
" where",
" isRightArchive :: StaticFile -> Bool",
" isRightArchive (SF fKind _) = fKind == fk",
" showEntry :: Entry -> Text",
" showEntry = tshow . eRelativePath ",
" showArchives :: [StaticFile] -> [Text]",
" showArchives xs = ",
" ( \"Number of archives: \"<>tshow (length xs)",
" ):",
" concatMap showSF xs",
" where",
" showSF :: StaticFile -> [Text]",
" showSF (SF fKind archive) = ",
" [ \" Archive: \"<>tshow fKind<>\" (\"<>(tshow . length . zEntries $ archive)<>\" entries)\"",
" ]",
"",
"{-" <> "# NOINLINE allStaticFiles #-}", -- Workaround: break pragma start { - #, since it upsets Eclipse :-(
"allStaticFiles :: [StaticFile]",
"allStaticFiles = "
]
getProperDirectoryContents :: FilePath -> IO [FilePath]
getProperDirectoryContents fp = filter (`notElem` [".", "..", ".git"]) <$> getDirectoryContents fp
pathFromModuleName :: Text -> FilePath
pathFromModuleName m = T.unpack $ "src/" <> T.map (\c -> if c == '.' then '/' else c) m <> ".hs"