/
Compile.hs
159 lines (136 loc) · 5.9 KB
/
Compile.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
{- |
Compiling the custom executable. The majority of the code actually
deals with error handling, and not the compilation itself /per se/.
-}
module Config.Dyre.Compile ( customCompile, getErrorPath, getErrorString ) where
import Control.Applicative ((<|>))
import Control.Concurrent ( rtsSupportsBoundThreads )
import Control.Monad (when)
import Data.Maybe (fromMaybe)
import Data.List (intercalate)
import System.IO ( IOMode(WriteMode), withFile )
import System.Environment (lookupEnv)
import System.Exit ( ExitCode(..) )
import System.Process ( runProcess, waitForProcess )
import System.FilePath
( (</>), dropTrailingPathSeparator, joinPath, splitPath, takeDirectory )
import System.Directory ( getCurrentDirectory, doesFileExist
, createDirectoryIfMissing
, renameFile, removeFile )
import Config.Dyre.Paths ( PathsConfig(..), getPathsConfig, outputExecutable )
import Config.Dyre.Params ( Params(..) )
-- | Return the path to the error file.
getErrorPath :: Params cfgType a -> IO FilePath
getErrorPath params =
(</> "errors.log") . cacheDirectory <$> getPathsConfig params
-- | If the error file exists and actually has some contents, return
-- 'Just' the error string. Otherwise return 'Nothing'.
getErrorString :: Params cfgType a -> IO (Maybe String)
getErrorString params = do
errorPath <- getErrorPath params
errorsExist <- doesFileExist errorPath
if not errorsExist
then return Nothing
else do errorData <- readFile errorPath
if errorData == ""
then return Nothing
else return . Just $ errorData
-- | Attempts to compile the configuration file. Will return a string
-- containing any compiler output.
customCompile :: Params cfgType a -> IO ()
customCompile params@Params{statusOut = output} = do
paths <- getPathsConfig params
let
tempBinary = customExecutable paths
outFile = outputExecutable tempBinary
configFile' = configFile paths
cacheDir' = cacheDirectory paths
libsDir = libsDirectory paths
output $ "Configuration '" ++ configFile' ++ "' changed. Recompiling."
createDirectoryIfMissing True cacheDir'
-- Compile occurs in here
errFile <- getErrorPath params
result <- withFile errFile WriteMode $ \errHandle -> do
flags <- makeFlags params configFile' outFile cacheDir' libsDir
stackYaml <- do
let stackYamlPath = takeDirectory configFile' </> "stack.yaml"
stackYamlExists <- doesFileExist stackYamlPath
if stackYamlExists
then return $ Just stackYamlPath
else return Nothing
hc' <- lookupEnv "HC"
nix_ghc <- lookupEnv "NIX_GHC"
let hc = fromMaybe "ghc" (hc' <|> nix_ghc)
ghcProc <- maybe (runProcess hc flags (Just cacheDir') Nothing
Nothing Nothing (Just errHandle))
(\stackYaml' -> runProcess "stack" ("ghc" : "--stack-yaml" : stackYaml' : "--" : flags)
Nothing Nothing Nothing Nothing (Just errHandle))
stackYaml
waitForProcess ghcProc
case result of
ExitSuccess -> do
renameFile outFile tempBinary
-- GHC sometimes prints to stderr, even on success.
-- Other parts of dyre infer error if error file exists
-- and is non-empty, so remove it.
removeFileIfExists errFile
output "Program reconfiguration successful."
_ -> do
removeFileIfExists tempBinary
output "Error occurred while loading configuration file."
-- | Assemble the arguments to GHC so everything compiles right.
makeFlags :: Params cfgType a -> FilePath -> FilePath -> FilePath
-> FilePath -> IO [String]
makeFlags params cfgFile outFile cacheDir' libsDir = do
currentDir <- getCurrentDirectory
pure . concat $
[ ["-v0", "-i" ++ libsDir]
, ["-i" ++ currentDir | includeCurrentDirectory params]
, prefix "-hide-package" (hidePackages params)
-- add extra include dirs
, fmap ("-i" ++) (includeDirs params)
, includeDirs params >>= getCabalStoreGhcArgs (projectName params)
, ghcOpts params
-- if the current process uses threaded RTS,
-- also compile custom executable with -threaded
, [ "-threaded" | rtsSupportsBoundThreads ]
, ["--make", cfgFile, "-outputdir", cacheDir', "-o", outFile]
, ["-fforce-recomp" | forceRecomp params] -- Only if force is true
]
where prefix y = concatMap $ \x -> [y,x]
-- | Given a path to lib dir, if it is a package in the Cabal
-- store that matches the projectName, return GHC arguments
-- to enable the Cabal store package database and expose the
-- application's library package.
--
getCabalStoreGhcArgs :: String -> FilePath -> [String]
getCabalStoreGhcArgs proj = mkArgs . go . fmap dropTrailingPathSeparator . splitPath
where
go :: [String] -> Maybe (String {- unit-id -}, [String] {- package-db -})
go (dir : "store" : hc : unit : _)
| dir `elem` [".cabal", "cabal" {- probably under $XDG_STATE_HOME -}]
, pkgNameFromUnitId unit == Just proj
= Just (unit, [dir, "store", hc, "package.db"])
go (h : t@(_cabal : _store : _hc : _unit : _))
= fmap (h:) <$> go t
go _
= Nothing
mkArgs Nothing = []
mkArgs (Just (unitId, pkgDb)) = ["-package-db", joinPath pkgDb, "-package-id", unitId]
-- | Extract package name from a unit-id, or return @Nothing@
-- if the input does not look like a unit-id.
--
pkgNameFromUnitId :: String -> Maybe String
pkgNameFromUnitId = fmap (intercalate "-") . go . splitOn '-'
where
go [s,_,_] = Just [s] -- drop the version and hash
go (s:rest) = (s:) <$> go rest
go [] = Nothing
splitOn :: (Eq a) => a -> [a] -> [[a]]
splitOn a l = case span (/= a) l of
(h, []) -> [h]
(h, _ : t) -> h : splitOn a t
removeFileIfExists :: FilePath -> IO ()
removeFileIfExists path = do
exists <- doesFileExist path
when exists $ removeFile path