{-# OPTIONS_GHC -Wall #-}
import Distribution.Simple
import Distribution.Simple.LocalBuildInfo
import Distribution.Simple.Setup
import Distribution.PackageDescription
import System.Process
import System.Exit
import Control.Exception
import Data.List
import Data.Either
import Data.Char
import Data.Time.Clock
import qualified Data.Time.Format as DTF
import Data.Time.LocalTime
import System.Directory
import System.FilePath
import System.IO
import qualified System.Locale as SL
import qualified Data.ByteString.Lazy.Char8 as BS
import qualified Codec.Compression.GZip as GZip
main :: IO ()
main = defaultMainWithHooks (simpleUserHooks { buildHook = generateBuildInfoHook } )
-- Before each build, generate a BuildInfo_Generated module that exports the project version from cabal,
-- the current revision number and the build time.
--
-- Note that in order for this Setup.hs to be used by cabal, the build-type should be Custom.
generateBuildInfoHook :: PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO ()
generateBuildInfoHook pd lbi uh bf =
do { let cabalVersionStr = intercalate "." (map show . versionBranch . pkgVersion . package $ pd)
; gitInfoStr <- getGitInfoStr
; clockTime <- getCurrentTime >>= utcToLocalZonedTime
; let buildTimeStr = DTF.formatTime SL.defaultTimeLocale "%d-%b-%y %H:%M:%S %Z" clockTime
; writeFile (pathFromModuleName buildInfoModuleName) $
buildInfoModule cabalVersionStr gitInfoStr buildTimeStr
; generateStaticFileModule
; putStrLn ""
; (buildHook simpleUserHooks) pd lbi uh bf -- start the build
}
buildInfoModuleName :: String
buildInfoModuleName = "Database.Design.Ampersand.Basics.BuildInfo_Generated"
buildInfoModule :: String -> String -> String -> String
buildInfoModule cabalVersion gitInfo time = unlines
[ "module "++buildInfoModuleName++"(cabalVersionStr, gitInfoStr, buildTimeStr) where"
, ""
, "-- This module is generated automatically by Setup.hs before building. Do not edit!"
, ""
-- Workaround: break pragma start { - #, since it upsets Eclipse :-(
, "{-"++"# NOINLINE cabalVersionStr #-}" -- disable inlining to prevent recompilation of dependent modules on each build
, "cabalVersionStr :: String"
, "cabalVersionStr = \"" ++ cabalVersion ++ "\""
, ""
, "{-"++"# NOINLINE gitInfoStr #-}"
, "gitInfoStr :: String"
, "gitInfoStr = \"" ++ gitInfo ++ "\""
, ""
, "{-"++"# NOINLINE buildTimeStr #-}"
, "buildTimeStr :: String"
, "buildTimeStr = \"" ++ time ++ "\""
, ""
]
getGitInfoStr :: IO String
getGitInfoStr =
do { eSHA <- readProcessEither "git" ["rev-parse", "--short", "HEAD"] ""
; eBranch <- readProcessEither "git" ["rev-parse", "--abbrev-ref", "HEAD"] ""
; (exitCode, _, _) <- readProcessWithExitCode "git" ["diff", "--quiet"] ""
; let isDirty = exitCode /= ExitSuccess -- exit code signals whether branch is dirty
; case (eSHA, eBranch) of
(Right sha, Right branch) ->
return $ strip branch ++ ":" ++ strip sha ++ (if isDirty then "*" else "")
_ ->
do { mapM_ print $ lefts [eSHA, eBranch] -- errors during git execution
; warnNoCommitInfo
}
} `catch` \err -> -- git failed to execute
do { print (err :: IOException)
; warnNoCommitInfo
}
where strip str = reverse . dropWhile isSpace . reverse $ str
readProcessEither :: String -> [String] -> String -> IO (Either String String)
readProcessEither cmd args stdinStr =
do { (exitCode,stdoutStr,stderrStr) <- readProcessWithExitCode cmd args stdinStr
; case exitCode of
ExitSuccess -> return $ Right stdoutStr
ExitFailure _ -> return $ Left stderrStr
}
warnNoCommitInfo :: IO String
warnNoCommitInfo =
do { putStrLn "\n\n\nWARNING: Execution of 'git' command failed."
; putStrLn $ "BuildInfo_Generated.hs will not contain revision information, and therefore\nneither will fatal error messages.\n"++
"Please check your installation\n"
; return "no git info"
}
{- For each file in the directory ampersand/static, we generate a StaticFile 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.
TODO: creating the entire file may be somewhat time-consuming, if this is a problem on slower machines, we may want to cache the
timestamps+names in a file and only generate when there is a change. (using the timestamps from the previously
generated module file is not an option, as a Haskell read on that file is extremely slow)
-}
staticFileModuleName :: String
staticFileModuleName = "Database.Design.Ampersand.Prototype.StaticFiles_Generated"
generateStaticFileModule :: IO ()
generateStaticFileModule =
do { previousModuleContents <- getPreviousStaticFileModuleContents sfModulePath
; currentModuleContents <- readAllStaticFiles
-- simply compare file contents to see if we need to write (to prevent re-compilation)
; if previousModuleContents == currentModuleContents
then
putStrLn $ "Static files unchanged, no need to update "++sfModulePath
else
do { putStrLn $ "Static files have changed, updating "++sfModulePath
; writeFile sfModulePath currentModuleContents
}
}
where sfModulePath = pathFromModuleName staticFileModuleName
getPreviousStaticFileModuleContents :: String -> IO String
getPreviousStaticFileModuleContents sfModulePath =
(withFile sfModulePath ReadMode $ \h ->
do { str <- hGetContents h
--; putStrLn $ "reading old file"
; length str `seq` return () -- lazy IO is :-(
--; putStrLn $ "Done"
; return str
}) `catch` \err -> -- old generated module exists, but we can't read the file or read the contents
do { putStrLn $ "\n\n\nWarning: Cannot read previously generated " ++ sfModulePath ++ ":\n" ++
show (err :: SomeException) ++ "\nThis warning should disappear the next time you build Ampersand. If the error persists, please report this as a bug.\n"
; return []
}
-- Collect all files required to be inside the ampersand.exe
readAllStaticFiles :: IO String
readAllStaticFiles =
do { zwolleFrontEndFiles <- readStaticFiles ZwolleFrontEnd "static/zwolle" "." -- files that define the Zwolle Frontend
; pandocTemplatesFiles <- readStaticFiles PandocTemplates "outputTemplates" "." -- templates for several PANDOC output types
; formalAmpersandFiles <- readStaticFiles FormalAmpersand "AmpersandData/FormalAmpersand" "." --meta information about Ampersand
; return $ mkStaticFileModule $ zwolleFrontEndFiles ++ pandocTemplatesFiles ++ formalAmpersandFiles
}
readStaticFiles :: FileKind -> FilePath -> FilePath -> IO [String]
readStaticFiles fkind base fileOrDirPth =
do { let path = combine base fileOrDirPth
; isDir <- doesDirectoryExist path
; if isDir then
do { fOrDs <- getProperDirectoryContents path
; fmap concat $ mapM (\fOrD -> readStaticFiles fkind base (combine fileOrDirPth fOrD)) fOrDs
}
else
do { timeStamp <- getModificationTime path
; fileContents <- BS.readFile path
; return [ "SF "++show fkind++" "++show fileOrDirPth++" "++utcToEpochTime timeStamp ++
" {-"++show timeStamp++" -} (BS.unpack$ GZip.decompress "++show (GZip.compress fileContents)++")"
]
}
}
where utcToEpochTime :: UTCTime -> String
utcToEpochTime utcTime = DTF.formatTime SL.defaultTimeLocale "%s" utcTime
data FileKind = ZwolleFrontEnd | PandocTemplates | FormalAmpersand deriving (Show, Eq)
mkStaticFileModule :: [String] -> String
mkStaticFileModule sfDeclStrs =
unlines staticFileModuleHeader ++
" [ " ++ intercalate "\n , " sfDeclStrs ++ "\n" ++
" ]\n"
staticFileModuleHeader :: [String]
staticFileModuleHeader =
[ "{-# LANGUAGE OverloadedStrings #-}"
, "module "++staticFileModuleName++" where"
, "import qualified Data.ByteString.Lazy.Char8 as BS"
, "import qualified Codec.Compression.GZip as GZip"
, ""
, "data FileKind = ZwolleFrontEnd | PandocTemplates | FormalAmpersand deriving (Show, Eq)"
, "data StaticFile = SF { fileKind :: FileKind"
, " , filePath :: FilePath -- relative path, including extension"
, " , timeStamp :: Integer -- unix epoch time"
, " , contentString :: String"
, " }"
, ""
, "getStaticFileContent :: FileKind -> FilePath -> Maybe String"
, "getStaticFileContent fk fp ="
, " case filter isRightFile allStaticFiles of"
, " [x] -> Just (contentString x)"
, " _ -> Nothing"
, " where"
, " isRightFile :: StaticFile -> Bool"
, " isRightFile (SF fKind path _ _ ) = fKind == fk && path == \".\\\\\"++fp"
, ""
, "{-"++"# NOINLINE allStaticFiles #-}" -- Workaround: break pragma start { - #, since it upsets Eclipse :-(
, "allStaticFiles :: [StaticFile]"
, "allStaticFiles ="
]
getProperDirectoryContents :: FilePath -> IO [String]
getProperDirectoryContents pth = fmap (filter (`notElem` [".","..",".svn"])) $ getDirectoryContents pth
pathFromModuleName :: String -> String
pathFromModuleName m = "src/" ++ [if c == '.' then '/' else c | c <- m] ++ ".hs"