Skip to content

Commit dd6c7ec

Browse files
committed
Merge pull request #2457 from AndrewRademacher/refactoring-ghci-support
Adding support for rendering GHCi scripts targeting different GHCi like applications.
2 parents 77a110d + 25e89bb commit dd6c7ec

File tree

15 files changed

+673
-54
lines changed

15 files changed

+673
-54
lines changed

src/Stack/Exec.hs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import System.Process.Log
2121
import Control.Exception.Lifted
2222
import Data.Streaming.Process (ProcessExitedUnsuccessfully(..))
2323
import System.Exit
24-
import System.Process.Run (callProcess, Cmd(..))
24+
import System.Process.Run (callProcess, callProcessObserveStdout, Cmd(..))
2525
#ifdef WINDOWS
2626
import System.Process.Read (EnvOverride)
2727
#else
@@ -78,3 +78,12 @@ execSpawn menv cmd0 args = do
7878
liftIO $ case e of
7979
Left (ProcessExitedUnsuccessfully _ ec) -> exitWith ec
8080
Right () -> exitSuccess
81+
82+
execObserve :: (MonadIO m, MonadLogger m, MonadBaseControl IO m)
83+
=> EnvOverride -> String -> [String] -> m String
84+
execObserve menv cmd0 args = do
85+
e <- $withProcessTimeLog cmd0 args $
86+
try (callProcessObserveStdout (Cmd Nothing cmd0 menv args))
87+
case e of
88+
Left (ProcessExitedUnsuccessfully _ ec) -> liftIO $ exitWith ec
89+
Right s -> return s

src/Stack/Ghci.hs

Lines changed: 71 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ module Stack.Ghci
1414
, GhciException(..)
1515
, ghciSetup
1616
, ghci
17+
18+
-- TODO: Address what should and should not be exported.
19+
, renderScriptGhci
20+
, renderScriptIntero
1721
) where
1822

1923
import Control.Applicative
@@ -30,7 +34,6 @@ import Data.Either
3034
import Data.Function
3135
import Data.List
3236
import Data.List.Extra (nubOrd)
33-
import Data.List.Split (splitOn)
3437
import Data.Map.Strict (Map)
3538
import qualified Data.Map.Strict as M
3639
import Data.Maybe
@@ -42,7 +45,6 @@ import Data.Traversable (forM)
4245
import Data.Text (Text)
4346
import qualified Data.Text as T
4447
import Data.Typeable (Typeable)
45-
import Distribution.ModuleName (ModuleName)
4648
import Distribution.PackageDescription (updatePackageDescription)
4749
import Distribution.Text (display)
4850
import Network.HTTP.Client.Conduit
@@ -56,6 +58,7 @@ import Stack.Build.Source
5658
import Stack.Build.Target
5759
import Stack.Constants
5860
import Stack.Exec
61+
import Stack.Ghci.Script
5962
import Stack.Package
6063
import Stack.Types.PackageIdentifier
6164
import Stack.Types.PackageName
@@ -64,7 +67,6 @@ import Stack.Types.Build
6467
import Stack.Types.Package
6568
import Stack.Types.Compiler
6669
import Stack.Types.Internal
67-
import System.FilePath (takeBaseName)
6870
import Text.Read (readMaybe)
6971

7072
#ifndef WINDOWS
@@ -142,25 +144,8 @@ ghci opts@GhciOpts{..} = do
142144
$logWarn
143145
("The following GHC options are incompatible with GHCi and have not been passed to it: " <>
144146
T.unwords (map T.pack (nubOrd omittedOpts)))
145-
allModules <- checkForDuplicateModules ghciNoLoadModules pkgs
147+
mainFile <- figureOutMainFile bopts mainIsTargets targets pkgs
146148
oiDir <- objectInterfaceDir bconfig
147-
(modulesToLoad, mainFile) <- if ghciNoLoadModules then return ([], Nothing) else do
148-
mmainFile <- figureOutMainFile bopts mainIsTargets targets pkgs
149-
modulesToLoad <- case mmainFile of
150-
Just mainFile -> do
151-
let (_, mfDirs, mfName) = filePathPieces mainFile
152-
mainPathPieces = map toFilePath mfDirs ++ [takeBaseName (toFilePath mfName)]
153-
liftM catMaybes $ forM allModules $ \mn -> do
154-
let matchesModule = splitOn "." mn `isSuffixOf` mainPathPieces
155-
if matchesModule
156-
then do
157-
$logWarn $ "Warning: Omitting load of module " <> T.pack mn <>
158-
", because it matches the filepath of the Main target, " <>
159-
T.pack (toFilePath mainFile)
160-
return Nothing
161-
else return (Just mn)
162-
Nothing -> return allModules
163-
return (modulesToLoad, mmainFile)
164149
let odir =
165150
[ "-odir=" <> toFilePathNoTrailingSep oiDir
166151
, "-hidir=" <> toFilePathNoTrailingSep oiDir ]
@@ -176,20 +161,68 @@ ghci opts@GhciOpts{..} = do
176161
-- include CWD.
177162
"-i" :
178163
odir <> pkgopts <> ghciArgs <> extras)
179-
withSystemTempDir "ghci" $ \tmpDir -> do
180-
let macrosFile = tmpDir </> $(mkRelFile "cabal_macros.h")
181-
macrosOpts <- preprocessCabalMacros pkgs macrosFile
182-
if ghciNoLoadModules
183-
then execGhci macrosOpts
184-
else do
185-
let scriptPath = tmpDir </> $(mkRelFile "ghci-script")
186-
fp = toFilePath scriptPath
187-
loadModules = ":add " <> unwords (map quoteFileName modulesToLoad)
188-
addMainFile = maybe "" ((":add " <>) . quoteFileName . toFilePath) mainFile
189-
bringIntoScope = ":module + " <> unwords modulesToLoad
190-
liftIO (writeFile fp (unlines [loadModules,addMainFile,bringIntoScope]))
191-
setScriptPerms fp
192-
execGhci (macrosOpts ++ ["-ghci-script=" <> fp])
164+
interrogateExeForRenderFunction = do
165+
menv <- liftIO $ configEnvOverride config defaultEnvSettings
166+
output <- execObserve menv (fromMaybe (compilerExeName wc) ghciGhcCommand) ["--version"]
167+
if "Intero" `isPrefixOf` output
168+
then return renderScriptIntero
169+
else return renderScriptGhci
170+
171+
withSystemTempDir "ghci" $ \tmpDirectory -> do
172+
macrosOptions <- writeMacrosFile tmpDirectory pkgs
173+
if ghciNoLoadModules
174+
then execGhci macrosOptions
175+
else do
176+
checkForDuplicateModules pkgs
177+
renderFn <- interrogateExeForRenderFunction
178+
scriptPath <- writeGhciScript tmpDirectory (renderFn pkgs mainFile)
179+
execGhci (macrosOptions ++ ["-ghci-script=" <> toFilePath scriptPath])
180+
181+
writeMacrosFile :: (MonadIO m) => Path Abs Dir -> [GhciPkgInfo] -> m [String]
182+
writeMacrosFile tmpDirectory packages = do
183+
macrosOptions <- preprocessCabalMacros packages macrosFile
184+
return macrosOptions
185+
where
186+
macrosFile = tmpDirectory </> $(mkRelFile "cabal_macros.h")
187+
188+
writeGhciScript :: (MonadIO m) => Path Abs Dir -> GhciScript -> m (Path Abs File)
189+
writeGhciScript tmpDirectory script = do
190+
liftIO $ scriptToFile scriptPath script
191+
setScriptPerms scriptFilePath
192+
return scriptPath
193+
where
194+
scriptPath = tmpDirectory </> $(mkRelFile "ghci-script")
195+
scriptFilePath = toFilePath scriptPath
196+
197+
findOwningPackageForMain :: [GhciPkgInfo] -> Path Abs File -> Maybe GhciPkgInfo
198+
findOwningPackageForMain pkgs mainFile =
199+
find (\pkg -> toFilePath (ghciPkgDir pkg) `isPrefixOf` toFilePath mainFile) pkgs
200+
201+
renderScriptGhci :: [GhciPkgInfo] -> Maybe (Path Abs File) -> GhciScript
202+
renderScriptGhci pkgs mainFile =
203+
let addPhase = mconcat $ fmap renderPkg pkgs
204+
mainPhase = case mainFile of
205+
Just path -> cmdAddFile path
206+
Nothing -> mempty
207+
modulePhase = cmdModule $ foldl' S.union S.empty (fmap ghciPkgModules pkgs)
208+
in addPhase <> mainPhase <> modulePhase
209+
where
210+
renderPkg pkg = cmdAdd (ghciPkgModules pkg)
211+
212+
renderScriptIntero :: [GhciPkgInfo] -> Maybe (Path Abs File) -> GhciScript
213+
renderScriptIntero pkgs mainFile =
214+
let addPhase = mconcat $ fmap renderPkg pkgs
215+
mainPhase = case mainFile of
216+
Just path ->
217+
case findOwningPackageForMain pkgs path of
218+
Just mainPkg -> cmdCdGhc (ghciPkgDir mainPkg) <> cmdAddFile path
219+
Nothing -> cmdAddFile path
220+
Nothing -> mempty
221+
modulePhase = cmdModule $ foldl' S.union S.empty (fmap ghciPkgModules pkgs)
222+
in addPhase <> mainPhase <> modulePhase
223+
where
224+
renderPkg pkg = cmdCdGhc (ghciPkgDir pkg)
225+
<> cmdAdd (ghciPkgModules pkg)
193226

194227
-- | Figure out the main-is file to load based on the targets. Sometimes there
195228
-- is none, sometimes it's unambiguous, sometimes it's
@@ -503,15 +536,14 @@ borderedWarning f = do
503536
$logWarn ""
504537
return x
505538

506-
checkForDuplicateModules :: (MonadThrow m, MonadLogger m) => Bool -> [GhciPkgInfo] -> m [String]
507-
checkForDuplicateModules noLoadModules pkgs = do
539+
checkForDuplicateModules :: (MonadThrow m, MonadLogger m) => [GhciPkgInfo] -> m ()
540+
checkForDuplicateModules pkgs = do
508541
unless (null duplicates) $ do
509542
borderedWarning $ do
510543
$logWarn "The following modules are present in multiple packages:"
511544
forM_ duplicates $ \(mn, pns) -> do
512545
$logWarn (" * " <> T.pack mn <> " (in " <> T.intercalate ", " (map packageNameText pns) <> ")")
513-
unless noLoadModules $ throwM LoadingDuplicateModules
514-
return (map fst allModules)
546+
throwM LoadingDuplicateModules
515547
where
516548
duplicates, allModules :: [(String, [PackageName])]
517549
duplicates = filter (not . null . tail . snd) allModules
@@ -584,13 +616,6 @@ setScriptPerms fp = do
584616
]
585617
#endif
586618

587-
filePathPieces :: Path Abs File -> (Path Abs Dir, [Path Rel Dir], Path Rel File)
588-
filePathPieces x0 = go (parent x0, [], filename x0)
589-
where
590-
go (x, dirs, fp)
591-
| parent x == x = (x, dirs, fp)
592-
| otherwise = (parent x, dirname x : dirs, fp)
593-
594619
{- Copied from Stack.Ide, may be useful in the future
595620
596621
-- | Get options and target files for the given package info.
@@ -632,10 +657,3 @@ targetsCmd target go@GlobalOpts{..} =
632657
(mapM (getPackageOptsAndTargetFiles pwd) pkgs)
633658
forM_ targets (liftIO . putStrLn)
634659
-}
635-
636-
-- | Make sure that a filename with spaces in it gets the proper quotes.
637-
quoteFileName :: String -> String
638-
quoteFileName x =
639-
if any (==' ') x
640-
then show x
641-
else x

src/Stack/Ghci/Script.hs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
module Stack.Ghci.Script
4+
( GhciScript
5+
, ModuleName
6+
7+
, cmdAdd
8+
, cmdAddFile
9+
, cmdCdGhc
10+
, cmdModule
11+
12+
, scriptToLazyByteString
13+
, scriptToBuilder
14+
, scriptToFile
15+
) where
16+
17+
import Control.Exception
18+
import Data.ByteString.Lazy (ByteString)
19+
import Data.ByteString.Builder
20+
import Data.Monoid
21+
import Data.List
22+
import Data.Set (Set)
23+
import qualified Data.Set as S
24+
import Data.Text (Text)
25+
import Data.Text.Encoding (encodeUtf8Builder)
26+
import Path
27+
import System.IO
28+
29+
import Distribution.ModuleName hiding (toFilePath)
30+
31+
newtype GhciScript = GhciScript { unGhciScript :: [GhciCommand] }
32+
33+
instance Monoid GhciScript where
34+
mempty = GhciScript []
35+
(GhciScript xs) `mappend` (GhciScript ys) = GhciScript (ys <> xs)
36+
37+
data GhciCommand
38+
= Add (Set ModuleName)
39+
| AddFile (Path Abs File)
40+
| CdGhc (Path Abs Dir)
41+
| Module (Set ModuleName)
42+
deriving (Show)
43+
44+
cmdAdd :: Set ModuleName -> GhciScript
45+
cmdAdd = GhciScript . (:[]) . Add
46+
47+
cmdAddFile :: Path Abs File -> GhciScript
48+
cmdAddFile = GhciScript . (:[]) . AddFile
49+
50+
cmdCdGhc :: Path Abs Dir -> GhciScript
51+
cmdCdGhc = GhciScript . (:[]) . CdGhc
52+
53+
cmdModule :: Set ModuleName -> GhciScript
54+
cmdModule = GhciScript . (:[]) . Module
55+
56+
scriptToLazyByteString :: GhciScript -> ByteString
57+
scriptToLazyByteString = toLazyByteString . scriptToBuilder
58+
59+
scriptToBuilder :: GhciScript -> Builder
60+
scriptToBuilder backwardScript = mconcat $ fmap commandToBuilder script
61+
where
62+
script = reverse $ unGhciScript backwardScript
63+
64+
scriptToFile :: Path Abs File -> GhciScript -> IO ()
65+
scriptToFile path script =
66+
bracket (openFile filepath WriteMode) hClose
67+
$ \hdl -> do hSetBuffering hdl (BlockBuffering Nothing)
68+
hSetBinaryMode hdl True
69+
hPutBuilder hdl (scriptToBuilder script)
70+
where
71+
filepath = toFilePath path
72+
73+
-- Command conversion
74+
75+
fromText :: Text -> Builder
76+
fromText = encodeUtf8Builder
77+
78+
commandToBuilder :: GhciCommand -> Builder
79+
80+
commandToBuilder (Add modules)
81+
| S.null modules = mempty
82+
| otherwise =
83+
fromText ":add "
84+
<> (mconcat $ intersperse (fromText " ")
85+
$ fmap (stringUtf8 . quoteFileName . mconcat . intersperse "." . components)
86+
$ S.toAscList modules)
87+
<> fromText "\n"
88+
89+
commandToBuilder (AddFile path) =
90+
fromText ":add " <> stringUtf8 (quoteFileName (toFilePath path)) <> fromText "\n"
91+
92+
commandToBuilder (CdGhc path) =
93+
fromText ":cd-ghc " <> stringUtf8 (quoteFileName (toFilePath path)) <> fromText "\n"
94+
95+
commandToBuilder (Module modules)
96+
| S.null modules = fromText ":module +\n"
97+
| otherwise =
98+
fromText ":module + "
99+
<> (mconcat $ intersperse (fromText " ")
100+
$ fmap (stringUtf8 . quoteFileName . mconcat . intersperse "." . components)
101+
$ S.toAscList modules)
102+
<> fromText "\n"
103+
104+
-- | Make sure that a filename with spaces in it gets the proper quotes.
105+
quoteFileName :: String -> String
106+
quoteFileName x =
107+
if any (==' ') x
108+
then show x
109+
else x

src/System/Process/Run.hs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module System.Process.Run
1414
,callProcess
1515
,callProcess'
1616
,callProcessInheritStderrStdout
17+
,callProcessObserveStdout
1718
,createProcess'
1819
,ProcessExitedUnsuccessfully
1920
,Cmd(..)
@@ -112,6 +113,20 @@ callProcessInheritStderrStdout cmd = do
112113
let inheritOutput cp = cp { std_in = CreatePipe, std_out = Inherit, std_err = Inherit }
113114
callProcess' inheritOutput cmd
114115

116+
callProcessObserveStdout :: (MonadIO m, MonadLogger m) => Cmd -> m String
117+
callProcessObserveStdout cmd = do
118+
c <- liftM modCP (cmdToCreateProcess cmd)
119+
$logCreateProcess c
120+
liftIO $ do
121+
(_, Just hStdout, _, p) <- System.Process.createProcess c
122+
hSetBuffering hStdout NoBuffering
123+
exit_code <- waitForProcess p
124+
case exit_code of
125+
ExitSuccess -> hGetLine hStdout
126+
ExitFailure _ -> throwIO (ProcessExitedUnsuccessfully c exit_code)
127+
where
128+
modCP c = c { std_in = CreatePipe, std_out = CreatePipe, std_err = Inherit }
129+
115130
-- | Like 'System.Process.Internal.createProcess_', but taking a 'Cmd'.
116131
-- Note that the 'Handle's provided by 'UseHandle' are not closed
117132
-- automatically.

0 commit comments

Comments
 (0)