Skip to content

Commit 4daabb3

Browse files
authored
Don't use stdout to communicate arguments (#90)
* Don't use stdout to communicate arguments Now all the cradles communicate by reading the value of `HIE_BIOS_OUTPUT` which is a `FilePath`, and writing the arguments to that temporary file. This makes all the cradles more robust about the build tools spurting things to stdout. * Review comments * Filter out '\r' characters from stdout/stderr streams for windows * Filter out \r characters from response file as well * Add --no-nix-pure
1 parent b37bcb9 commit 4daabb3

File tree

9 files changed

+134
-91
lines changed

9 files changed

+134
-91
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ cradle: {cabal: {component: "lib:haskell-ide-engine"}}
4949
Or you can explicitly state the program which should be used to collect
5050
the options by supplying the path to the program. It is interpreted
5151
relative to the current working directory if it is not an absolute path.
52-
The bios program should return a list of options separated by newline characters.
52+
The bios program should consult the `HIE_BIOS_OUTPUT` env var and write a list of
53+
options to this file separated by newlines. Once the program finishes running `hie-bios`
54+
reads this file and uses the arguments to set up the GHC session. This is how GHC's
55+
build system is able to support `hie-bios`.
5356

5457
```yaml
5558
cradle: {bios: {program: ".hie-bios"}}

hie-bios.cabal

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Library
3737
HIE.Bios.Ghc.Gap
3838
HIE.Bios.Ghc.Load
3939
HIE.Bios.Ghc.Logger
40+
HIE.Bios.Wrappers
4041
Other-Modules: Paths_hie_bios
4142
Build-Depends:
4243
base >= 4.8 && < 5,
@@ -50,7 +51,6 @@ Library
5051
time >= 1.8.0 && < 1.10,
5152
extra >= 1.6.14 && < 1.7,
5253
process >= 1.6.1 && < 1.7,
53-
file-embed >= 0.0.10 && < 0.1,
5454
ghc >= 8.2.2 && < 8.9,
5555
transformers >= 0.5.2 && < 0.6,
5656
temporary >= 1.2 && < 1.4,
@@ -59,7 +59,10 @@ Library
5959
unordered-containers >= 0.2.9 && < 0.3,
6060
vector >= 0.12.0 && < 0.13,
6161
yaml >= 0.8.32 && < 0.12,
62-
hslogger >= 1.2 && < 1.4
62+
hslogger >= 1.2 && < 1.4,
63+
file-embed >= 0.0.11 && < 1,
64+
conduit >= 1.3 && < 2,
65+
conduit-extra >= 1.3 && < 2
6366

6467

6568
Executable hie-bios

src/HIE/Bios/Cradle.hs

Lines changed: 89 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
{-# LANGUAGE TemplateHaskell #-}
21
{-# LANGUAGE TupleSections #-}
2+
{-# LANGUAGE BangPatterns #-}
33
module HIE.Bios.Cradle (
44
findCradle
55
, loadCradle
@@ -15,18 +15,26 @@ import System.Directory hiding (findFile)
1515
import Control.Monad.Trans.Maybe
1616
import System.FilePath
1717
import Control.Monad
18-
import Control.Monad.IO.Class
1918
import System.Info.Extra
19+
import Control.Monad.IO.Class
20+
import System.Environment
2021
import Control.Applicative ((<|>))
21-
import Data.FileEmbed
2222
import System.IO.Temp
2323
import Data.List
2424
import Data.Ord (Down(..))
2525

2626
import System.PosixCompat.Files
27+
import HIE.Bios.Wrappers
28+
import System.IO
29+
import Control.DeepSeq
2730

2831
import Data.Version (showVersion)
2932
import Paths_hie_bios
33+
import Data.Conduit.Process
34+
import qualified Data.Conduit.Combinators as C
35+
import qualified Data.Conduit as C
36+
import qualified Data.Conduit.Text as C
37+
import Data.Text (unpack)
3038
----------------------------------------------------------------
3139

3240
-- | Given root\/foo\/bar.hs, return root\/hie.yaml, or wherever the yaml file was found.
@@ -81,9 +89,9 @@ addCradleDeps deps c =
8189
where
8290
addActionDeps :: CradleAction -> CradleAction
8391
addActionDeps ca =
84-
ca { runCradle = \fp ->
92+
ca { runCradle = \l fp ->
8593
(fmap (\(ComponentOptions os' ds) -> ComponentOptions os' (ds `union` deps)))
86-
<$> runCradle ca fp }
94+
<$> runCradle ca l fp }
8795

8896
implicitConfig :: FilePath -> MaybeT IO (CradleConfig, FilePath)
8997
implicitConfig fp = do
@@ -125,7 +133,7 @@ defaultCradle cur_dir =
125133
{ cradleRootDir = cur_dir
126134
, cradleOptsProg = CradleAction
127135
{ actionName = "default"
128-
, runCradle = const $ return (CradleSuccess (ComponentOptions [] []))
136+
, runCradle = \_ _ -> return (CradleSuccess (ComponentOptions [] []))
129137
}
130138
}
131139

@@ -138,7 +146,7 @@ noneCradle cur_dir =
138146
{ cradleRootDir = cur_dir
139147
, cradleOptsProg = CradleAction
140148
{ actionName = "none"
141-
, runCradle = const $ return CradleNone
149+
, runCradle = \_ _ -> return CradleNone
142150
}
143151
}
144152

@@ -151,18 +159,19 @@ multiCradle cur_dir cs =
151159
{ cradleRootDir = cur_dir
152160
, cradleOptsProg = CradleAction
153161
{ actionName = "multi"
154-
, runCradle = canonicalizePath >=> multiAction cur_dir cs
162+
, runCradle = \l fp -> canonicalizePath fp >>= multiAction cur_dir cs l
155163
}
156164
}
157165

158166
multiAction :: FilePath
159167
-> [(FilePath, CradleConfig)]
168+
-> LoggingFunction
160169
-> FilePath
161170
-> IO (CradleLoadResult ComponentOptions)
162-
multiAction cur_dir cs cur_fp = selectCradle =<< canonicalizeCradles
171+
multiAction cur_dir cs l cur_fp = selectCradle =<< canonicalizeCradles
163172

164173
where
165-
err_msg = unlines $ ["Multi Cradle: No prefixes matched"
174+
err_msg = ["Multi Cradle: No prefixes matched"
166175
, "pwd: " ++ cur_dir
167176
, "filepath" ++ cur_fp
168177
, "prefixes:"
@@ -180,7 +189,7 @@ multiAction cur_dir cs cur_fp = selectCradle =<< canonicalizeCradles
180189
return (CradleFail (CradleError ExitSuccess err_msg))
181190
selectCradle ((p, c): css) =
182191
if p `isPrefixOf` cur_fp
183-
then runCradle (cradleOptsProg (getCradle (c, cur_dir))) cur_fp
192+
then runCradle (cradleOptsProg (getCradle (c, cur_dir))) l cur_fp
184193
else selectCradle css
185194

186195

@@ -192,7 +201,7 @@ directCradle wdir args =
192201
{ cradleRootDir = wdir
193202
, cradleOptsProg = CradleAction
194203
{ actionName = "direct"
195-
, runCradle = const $ return (CradleSuccess (ComponentOptions args []))
204+
, runCradle = \_ _ -> return (CradleSuccess (ComponentOptions args []))
196205
}
197206
}
198207

@@ -214,28 +223,30 @@ biosCradle wdir biosProg biosDepsProg =
214223
biosWorkDir :: FilePath -> MaybeT IO FilePath
215224
biosWorkDir = findFileUpwards (".hie-bios" ==)
216225

217-
biosDepsAction :: Maybe FilePath -> IO [FilePath]
218-
biosDepsAction (Just biosDepsProg) = do
226+
biosDepsAction :: LoggingFunction -> FilePath -> Maybe FilePath -> IO [FilePath]
227+
biosDepsAction l wdir (Just biosDepsProg) = do
219228
biosDeps' <- canonicalizePath biosDepsProg
220-
(ex, sout, serr) <- readProcessWithExitCode biosDeps' [] []
229+
(ex, sout, serr, args) <- readProcessWithOutputFile l wdir biosDeps' []
221230
case ex of
222231
ExitFailure _ -> error $ show (ex, sout, serr)
223-
ExitSuccess -> return (lines sout)
224-
biosDepsAction Nothing = return []
232+
ExitSuccess -> return args
233+
biosDepsAction _ _ Nothing = return []
225234

226235
biosAction :: FilePath
227236
-> FilePath
228237
-> Maybe FilePath
238+
-> LoggingFunction
229239
-> FilePath
230240
-> IO (CradleLoadResult ComponentOptions)
231-
biosAction _wdir bios bios_deps fp = do
241+
biosAction wdir bios bios_deps l fp = do
232242
bios' <- canonicalizePath bios
233-
(ex, res, std) <- readProcessWithExitCode bios' [fp] []
234-
deps <- biosDepsAction bios_deps
235-
-- Output from the program should be delimited by newlines.
243+
(ex, _stdo, std, res) <- readProcessWithOutputFile l wdir bios' [fp]
244+
deps <- biosDepsAction l wdir bios_deps
245+
-- Output from the program should be written to the output file and
246+
-- delimited by newlines.
236247
-- Execute the bios action and add dependencies of the cradle.
237248
-- Removes all duplicates.
238-
return $ makeCradleResult (ex, std, lines res) deps
249+
return $ makeCradleResult (ex, std, res) deps
239250

240251
------------------------------------------------------------------------
241252
-- Cabal Cradle
@@ -262,31 +273,18 @@ findCabalFiles wdir = do
262273
dirContent <- listDirectory wdir
263274
return $ filter ((== ".cabal") . takeExtension) dirContent
264275

265-
cabalWrapper :: String
266-
cabalWrapper = $(embedStringFile "wrappers/cabal")
267276

268-
cabalWrapperHs :: String
269-
cabalWrapperHs = $(embedStringFile "wrappers/cabal.hs")
270-
271-
processCabalWrapperArgs :: String -> Maybe [String]
277+
processCabalWrapperArgs :: [String] -> Maybe [String]
272278
processCabalWrapperArgs args =
273-
case lines args of
274-
[dir, ghc_args] ->
279+
case args of
280+
(dir: ghc_args) ->
275281
let final_args =
276282
removeVerbosityOpts
277283
$ removeInteractive
278284
$ map (fixImportDirs dir)
279-
$ limited ghc_args
285+
$ ghc_args
280286
in Just final_args
281287
_ -> Nothing
282-
where
283-
limited :: String -> [String]
284-
limited = unfoldr $ \argstr ->
285-
if null argstr
286-
then Nothing
287-
else
288-
let (arg, argstr') = break (== '\NUL') argstr
289-
in Just (arg, drop 1 argstr')
290288

291289
-- generate a fake GHC that can be passed to cabal
292290
-- when run with --interactive, it will print out its
@@ -315,20 +313,20 @@ getCabalWrapperTool = do
315313
_check <- readFile wrapper_fp
316314
return wrapper_fp
317315

318-
cabalAction :: FilePath -> Maybe String -> FilePath -> IO (CradleLoadResult ComponentOptions)
319-
cabalAction work_dir mc _fp = do
316+
cabalAction :: FilePath -> Maybe String -> LoggingFunction -> FilePath -> IO (CradleLoadResult ComponentOptions)
317+
cabalAction work_dir mc l _fp = do
320318
wrapper_fp <- getCabalWrapperTool
321-
let cab_args = ["v2-repl", "-v0", "--disable-documentation", "--with-compiler", wrapper_fp]
319+
let cab_args = ["v2-repl", "--with-compiler", wrapper_fp]
322320
++ [component_name | Just component_name <- [mc]]
323-
(ex, args, stde) <-
324-
readProcessWithExitCodeInDirectory work_dir "cabal" cab_args []
325-
321+
(ex, output, stde, args) <-
322+
readProcessWithOutputFile l work_dir "cabal" cab_args
326323
deps <- cabalCradleDependencies work_dir
327324
case processCabalWrapperArgs args of
328325
Nothing -> pure $ CradleFail (CradleError ex
329-
(unlines ["Failed to parse result of calling cabal"
330-
, stde
331-
, args]))
326+
["Failed to parse result of calling cabal"
327+
, unlines output
328+
, unlines stde
329+
, unlines args])
332330
Just final_args -> pure $ makeCradleResult (ex, stde, final_args) deps
333331

334332
removeInteractive :: [String] -> [String]
@@ -367,25 +365,25 @@ stackCradle wdir =
367365

368366
stackCradleDependencies :: FilePath -> IO [FilePath]
369367
stackCradleDependencies wdir = do
370-
cabalFiles <- findCabalFiles wdir
371-
return $ cabalFiles ++ ["package.yaml", "stack.yaml"]
368+
cabalFiles <- findCabalFiles wdir
369+
return $ cabalFiles ++ ["package.yaml", "stack.yaml"]
372370

373-
stackAction :: FilePath -> FilePath -> IO (CradleLoadResult ComponentOptions)
374-
stackAction work_dir fp = do
371+
stackAction :: FilePath -> LoggingFunction -> FilePath -> IO (CradleLoadResult ComponentOptions)
372+
stackAction work_dir l fp = do
375373
-- Same wrapper works as with cabal
376374
wrapper_fp <- getCabalWrapperTool
377-
(ex1, args, stde) <-
378-
readProcessWithExitCodeInDirectory work_dir "stack" ["repl", "--silent", "--no-load", "--with-ghc", wrapper_fp, fp ] []
379-
(ex2, pkg_args, stdr) <-
380-
readProcessWithExitCodeInDirectory work_dir "stack" ["path", "--ghc-package-path"] []
381-
let split_pkgs = splitSearchPath (init pkg_args)
375+
(ex1, _stdo, stde, args) <-
376+
readProcessWithOutputFile l work_dir "stack" ["repl", "--no-nix-pure", "--no-load", "--with-ghc", wrapper_fp, fp ]
377+
(ex2, pkg_args, stdr, _) <-
378+
readProcessWithOutputFile l work_dir "stack" ["path", "--ghc-package-path"]
379+
let split_pkgs = concatMap splitSearchPath pkg_args
382380
pkg_ghc_args = concatMap (\p -> ["-package-db", p] ) split_pkgs
383381
deps <- stackCradleDependencies work_dir
384382
return $ case processCabalWrapperArgs args of
385-
Nothing -> CradleFail (CradleError ex1
386-
(unlines ["Failed to parse result of calling cabal"
387-
, stde
388-
, args]))
383+
Nothing -> CradleFail (CradleError ex1 $
384+
("Failed to parse result of calling cabal":
385+
stde)
386+
++ args)
389387

390388
Just ghc_args -> makeCradleResult (combineExitCodes [ex1, ex2], stde ++ stdr, ghc_args ++ pkg_ghc_args) deps
391389

@@ -435,7 +433,7 @@ rulesHaskellAction work_dir fp = do
435433
setFileMode wrapper_fp accessModes
436434
let rel_path = makeRelative work_dir fp
437435
(ex, args, stde) <-
438-
readProcessWithExitCodeInDirectory work_dir wrapper_fp [rel_path] []
436+
readProcessWithOutputFile work_dir wrapper_fp [rel_path] []
439437
let args' = filter (/= '\'') args
440438
let args'' = filter (/= "\"$GHCI_LOCATION\"") (words args')
441439
deps <- rulesHaskellCradleDependencies work_dir
@@ -471,7 +469,7 @@ obeliskCradle wdir =
471469
obeliskAction :: FilePath -> FilePath -> IO (CradleLoadResult ComponentOptions)
472470
obeliskAction work_dir _fp = do
473471
(ex, args, stde) <-
474-
readProcessWithExitCodeInDirectory work_dir "ob" ["ide-args"] []
472+
readProcessWithOutputFile work_dir "ob" ["ide-args"] []
475473
476474
o_deps <- obeliskCradleDependencies work_dir
477475
return (makeCradleResult (ex, stde, words args) o_deps )
@@ -502,15 +500,34 @@ findFile p dir = do
502500
getFiles = filter p <$> getDirectoryContents dir
503501
doesPredFileExist file = doesFileExist $ dir </> file
504502

505-
-- | Call a process with the given arguments and the given stdin
506-
-- in the given working directory.
507-
readProcessWithExitCodeInDirectory
508-
:: FilePath -> FilePath -> [String] -> String -> IO (ExitCode, String, String)
509-
readProcessWithExitCodeInDirectory work_dir fp args stdin =
510-
let process = (proc fp args) { cwd = Just work_dir }
511-
in readCreateProcessWithExitCode process stdin
512-
513-
makeCradleResult :: (ExitCode, String, [String]) -> [FilePath] -> CradleLoadResult ComponentOptions
503+
-- | Call a process with the given arguments
504+
-- * A special file is created for the process to write to, the process can discover the name of
505+
-- the file by reading the @HIE_BIOS_OUTPUT@ environment variable. The contents of this file is
506+
-- returned by the function.
507+
-- * The logging function is called every time the process emits anything to stdout or stderr.
508+
-- it can be used to report progress of the process to a user.
509+
-- * The process is executed in the given directory
510+
readProcessWithOutputFile
511+
:: LoggingFunction
512+
-> FilePath
513+
-> FilePath
514+
-> [String]
515+
-> IO (ExitCode, [String], [String], [String])
516+
readProcessWithOutputFile l work_dir fp args = withSystemTempFile "bios-output" $ \output_file h -> do
517+
hSetBuffering h LineBuffering
518+
old_env <- getEnvironment
519+
-- Pipe stdout directly into the logger
520+
let process = (proc fp args) { cwd = Just work_dir
521+
, env = Just (("HIE_BIOS_OUTPUT", output_file) : old_env)
522+
}
523+
-- Windows line endings are not converted so you have to filter out `'r` characters
524+
loggingConduit = (C.decodeUtf8 C..| C.lines C..| C.filterE (/= '\r') C..| C.map unpack C..| C.iterM l C..| C.sinkList)
525+
(ex, stdo, stde) <- sourceProcessWithStreams process mempty loggingConduit loggingConduit
526+
!res <- force <$> hGetContents h
527+
return (ex, stdo, stde, lines (filter (/= '\r') res))
528+
529+
530+
makeCradleResult :: (ExitCode, [String], [String]) -> [FilePath] -> CradleLoadResult ComponentOptions
514531
makeCradleResult (ex, err, gopts) deps =
515532
case ex of
516533
ExitFailure _ -> CradleFail (CradleError ex err)

src/HIE/Bios/Flags.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module HIE.Bios.Flags (getCompilerOptions) where
22

33
import HIE.Bios.Types
4+
import HIE.Bios.Internal.Log
45

56

67
-- | Initialize the 'DynFlags' relating to the compilation of a single
@@ -10,7 +11,7 @@ getCompilerOptions ::
1011
-> Cradle
1112
-> IO (CradleLoadResult ComponentOptions)
1213
getCompilerOptions fp cradle =
13-
runCradle (cradleOptsProg cradle) fp
14+
runCradle (cradleOptsProg cradle) logm fp
1415

1516

1617
----------------------------------------------------------------

src/HIE/Bios/Internal/Debug.hs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Data.Maybe (fromMaybe)
77

88
import HIE.Bios.Ghc.Api
99
import HIE.Bios.Types
10+
import HIE.Bios.Flags
1011

1112
----------------------------------------------------------------
1213

@@ -21,7 +22,7 @@ import HIE.Bios.Types
2122
debugInfo :: Cradle
2223
-> IO String
2324
debugInfo cradle = unlines <$> do
24-
res <- runCradle (cradleOptsProg cradle) (cradleRootDir cradle)
25+
res <- getCompilerOptions (cradleRootDir cradle) cradle
2526
case res of
2627
CradleSuccess (ComponentOptions gopts deps) -> do
2728
mglibdir <- liftIO getSystemLibDir
@@ -34,7 +35,7 @@ debugInfo cradle = unlines <$> do
3435
CradleFail (CradleError ext stderr) ->
3536
return ["Cradle failed to load"
3637
, "Exit Code: " ++ show ext
37-
, "Stderr: " ++ stderr]
38+
, "Stderr: " ++ unlines stderr]
3839
CradleNone ->
3940
return ["No cradle"]
4041
where

0 commit comments

Comments
 (0)