packages feed

hie-bios 0.8.1 → 0.9.0

raw patch · 11 files changed

+604/−239 lines, 11 filesdep +taggeddep ~aesondep ~exceptionsdep ~ghcPVP ok

version bump matches the API change (PVP)

Dependencies added: tagged

Dependency ranges changed: aeson, exceptions, ghc, text

API changes (from Hackage documentation)

- HIE.Bios.Types: bindIO :: CradleLoadResult a -> (a -> IO (CradleLoadResult b)) -> IO (CradleLoadResult b)
+ HIE.Bios.Types: CradleLoadResultT :: m (CradleLoadResult a) -> CradleLoadResultT m a
+ HIE.Bios.Types: [runCradleResultT] :: CradleLoadResultT m a -> m (CradleLoadResult a)
+ HIE.Bios.Types: cradleLoadResult :: c -> (CradleError -> c) -> (r -> c) -> CradleLoadResult r -> c
+ HIE.Bios.Types: instance (GHC.Base.Monad m, GHC.Base.Applicative m) => GHC.Base.Applicative (HIE.Bios.Types.CradleLoadResultT m)
+ HIE.Bios.Types: instance Control.Monad.Fail.MonadFail m => Control.Monad.Fail.MonadFail (HIE.Bios.Types.CradleLoadResultT m)
+ HIE.Bios.Types: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (HIE.Bios.Types.CradleLoadResultT m)
+ HIE.Bios.Types: instance Control.Monad.Trans.Class.MonadTrans HIE.Bios.Types.CradleLoadResultT
+ HIE.Bios.Types: instance GHC.Base.Functor f => GHC.Base.Functor (HIE.Bios.Types.CradleLoadResultT f)
+ HIE.Bios.Types: instance GHC.Base.Monad m => GHC.Base.Monad (HIE.Bios.Types.CradleLoadResultT m)
+ HIE.Bios.Types: modCradleError :: Monad m => CradleLoadResultT m a -> (CradleError -> m CradleError) -> CradleLoadResultT m a
+ HIE.Bios.Types: newtype CradleLoadResultT m a
+ HIE.Bios.Types: throwCE :: Monad m => CradleError -> CradleLoadResultT m a

Files

ChangeLog.md view
@@ -1,5 +1,11 @@ # ChangeLog hie-bios +## 2022-02-25 - 0.9.0++* Use the proper GHC version given by cabal [#282](https://github.com/haskell/hie-bios/pull/282)+  * In particular, honour the `with-compiler` field in `cabal.project`+* Drop support for GHC 8.4 [#331](https://github.com/haskell/hie-bios/pull/331)+ ## 2022-01-06 - 0.8.1  * Add support for GHC 9.0.2 [#322](https://github.com/haskell/hie-bios/pull/322)
hie-bios.cabal view
@@ -1,6 +1,6 @@ Cabal-Version:          2.2 Name:                   hie-bios-Version:                0.8.1+Version:                0.9.0 Author:                 Matthew Pickering <matthewtpickering@gmail.com> Maintainer:             Matthew Pickering <matthewtpickering@gmail.com> License:                BSD-3-Clause@@ -17,6 +17,10 @@                         wrappers/cabal                         wrappers/cabal.hs                         tests/configs/*.yaml+                        tests/projects/cabal-with-ghc/cabal-with-ghc.cabal+                        tests/projects/cabal-with-ghc/cabal.project+                        tests/projects/cabal-with-ghc/hie.yaml+                        tests/projects/cabal-with-ghc/src/MyLib.hs                         tests/projects/symlink-test/a/A.hs                         tests/projects/symlink-test/hie.yaml                         tests/projects/deps-bios-new/A.hs@@ -121,7 +125,7 @@                         tests/projects/stack-with-yaml/stack-with-yaml.cabal                         tests/projects/stack-with-yaml/src/Lib.hs -tested-with: GHC ==8.4.4 || ==8.6.5 || ==8.8.4 || ==8.10.4 || ==9.0.1 || ==9.0.2 || ==9.2.1+tested-with: GHC ==8.6.5 || ==8.8.4 || ==8.10.4 || ==9.0.1 || ==9.0.2 || ==9.2.1  Library   Default-Language:     Haskell2010@@ -157,12 +161,11 @@                         filepath             >= 1.4.1 && < 1.5,                         time                 >= 1.8.0 && < 1.13,                         extra                >= 1.6.14 && < 1.8,-                        exceptions,                         process              >= 1.6.1 && < 1.7,-                        ghc                  >= 8.4.1 && < 9.3,+                        ghc                  >= 8.6.1 && < 9.3,                         transformers         >= 0.5.2 && < 0.7,                         temporary            >= 1.2 && < 1.4,-                        text                 >= 1.2.3 && < 1.3,+                        text                 >= 1.2.3 && < 2,                         unix-compat          >= 0.5.1 && < 0.6,                         unordered-containers >= 0.2.9 && < 0.3,                         vector               >= 0.12.0 && < 0.13,@@ -217,6 +220,7 @@       filepath,       directory,       temporary,+      tagged,       ghc    hs-source-dirs: tests/
src/HIE/Bios/Cradle.hs view
@@ -1,7 +1,8 @@+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-}-{-# LANGUAGE BangPatterns #-} module HIE.Bios.Cradle (       findCradle     , loadCradle@@ -22,46 +23,48 @@     , makeCradleResult   ) where +import Control.Applicative ((<|>), optional)+import Data.Bifunctor (first)+import Control.DeepSeq import Control.Exception (handleJust) import qualified Data.Yaml as Yaml import Data.Void import Data.Char (isSpace)-import Data.Bifunctor (first)-import System.Process import System.Exit-import HIE.Bios.Types hiding (ActionName(..))-import qualified HIE.Bios.Types as Types-import HIE.Bios.Config-import HIE.Bios.Environment (getCacheDir)-import qualified HIE.Bios.Ghc.Gap as Gap import System.Directory hiding (findFile)+import Control.Monad+import Control.Monad.Extra (unlessM) import Control.Monad.Trans.Cont import Control.Monad.Trans.Maybe-import System.FilePath-import Control.Monad-import System.Info.Extra import Control.Monad.IO.Class-import System.Environment-import Control.Applicative ((<|>), optional)-import System.IO.Temp-import System.IO.Error (isPermissionError)-import Data.List-import Data.Ord (Down(..))--import System.PosixCompat.Files-import HIE.Bios.Wrappers-import System.IO-import Control.DeepSeq- import Data.Conduit.Process import qualified Data.Conduit.Combinators as C import qualified Data.Conduit as C import qualified Data.Conduit.Text as C-import qualified Data.Text as T import qualified Data.HashMap.Strict as Map-import           Data.Maybe (fromMaybe, maybeToList)-import           GHC.Fingerprint (fingerprintString)+import Data.Maybe (fromMaybe, maybeToList)+import Data.List+import Data.List.Extra (trimEnd)+import Data.Ord (Down(..))+import qualified Data.Text as T+import System.Environment+import System.FilePath+import System.PosixCompat.Files+import System.Info.Extra (isWindows)+import System.IO (hClose, hGetContents, hSetBuffering, BufferMode(LineBuffering), withFile, IOMode(..))+import System.IO.Error (isPermissionError)+import System.IO.Temp +import HIE.Bios.Config+import HIE.Bios.Environment (getCacheDir)+import HIE.Bios.Types hiding (ActionName(..))+import HIE.Bios.Wrappers+import qualified HIE.Bios.Types as Types+import qualified HIE.Bios.Ghc.Gap as Gap++import GHC.Fingerprint (fingerprintString)+import GHC.ResponseFile (escapeArgs)+ hie_bios_output :: String hie_bios_output = "HIE_BIOS_OUTPUT" ----------------------------------------------------------------@@ -98,7 +101,7 @@ getCradle buildCustomCradle (cc, wdir) = addCradleDeps cradleDeps $ case cradleType cc of     Cabal CabalType{ cabalComponent = mc } -> cabalCradle wdir mc     CabalMulti dc ms ->-      getCradle buildCustomCradle $+      getCradle buildCustomCradle         (CradleConfig cradleDeps           (Multi [(p, CradleConfig [] (Cabal $ dc <> c)) | (p, c) <- ms])         , wdir)@@ -108,7 +111,7 @@       in         stackCradle wdir mc stackYamlConfig     StackMulti ds ms ->-      getCradle buildCustomCradle $+      getCradle buildCustomCradle         (CradleConfig cradleDeps           (Multi [(p, CradleConfig [] (Stack $ ds <> c)) | (p, c) <- ms])         , wdir)@@ -129,14 +132,12 @@     addActionDeps :: CradleAction a -> CradleAction a     addActionDeps ca =       ca { runCradle = \l fp ->-            runCradle ca l fp-              >>= \case-                CradleSuccess (ComponentOptions os' dir ds) ->-                  pure $ CradleSuccess (ComponentOptions os' dir (ds `union` deps))-                CradleFail err ->-                  pure $ CradleFail-                    (err { cradleErrorDependencies = cradleErrorDependencies err `union` deps })-                CradleNone -> pure CradleNone+            (cradleLoadResult+                CradleNone+                (\err -> CradleFail (err { cradleErrorDependencies = cradleErrorDependencies err `union` deps }))+                (\(ComponentOptions os' dir ds) -> CradleSuccess (ComponentOptions os' dir (ds `union` deps)))+            )+            <$> runCradle ca l fp          }  -- | Try to infer an appropriate implicit cradle type from stuff we can find in the enclosing directories:@@ -334,7 +335,7 @@     canonicalizeCradles :: IO [(FilePath, CradleConfig b)]     canonicalizeCradles =       sortOn (Down . fst)-        <$> mapM (\(p, c) -> (,c) <$> (makeAbsolute (cur_dir </> p))) cs+        <$> mapM (\(p, c) -> (,c) <$> makeAbsolute (cur_dir </> p)) cs      selectCradle [] =       return (CradleFail (CradleError [] ExitSuccess err_msg))@@ -412,7 +413,7 @@ callableToProcess :: Callable -> Maybe String -> IO CreateProcess callableToProcess (Command shellCommand) file = do   old_env <- getEnvironment-  return $ (shell shellCommand) { env = (: old_env) <$> (,) hieBiosArg <$> file }+  return $ (shell shellCommand) { env = (: old_env) . (,) hieBiosArg <$> file }     where       hieBiosArg = "HIE_BIOS_ARG" callableToProcess (Program path) file = do@@ -428,19 +429,118 @@     { cradleRootDir    = wdir     , cradleOptsProg   = CradleAction         { actionName = Types.Cabal-        , runCradle = cabalAction wdir mc-        , runGhcCmd = \args -> do-            buildDir <- cabalBuildDir wdir+        , runCradle = \l f -> runCradleResultT . cabalAction wdir mc l $ f+        , runGhcCmd = \args -> runCradleResultT $ do+            buildDir <- liftIO $ cabalBuildDir wdir             -- Workaround for a cabal-install bug on 3.0.0.0:             -- ./dist-newstyle/tmp/environment.-24811: createDirectory: does not exist (No such file or directory)-            createDirectoryIfMissing True (buildDir </> "tmp")+            liftIO $ createDirectoryIfMissing True (buildDir </> "tmp")             -- Need to pass -v0 otherwise we get "resolving dependencies..."-            wrapper_fp <- withCabalWrapperTool ("ghc", []) wdir-            readProcessWithCwd-              wdir "cabal" (["--builddir="<>buildDir,"v2-exec","--with-compiler", wrapper_fp, "ghc", "-v0", "--"] ++ args) ""+            cabalProc <- cabalProcess wdir "v2-exec" $ ["ghc", "-v0", "--"] ++ args+            readProcessWithCwd' cabalProc ""         }     } +-- | Execute a cabal process in our custom cache-build directory configured+-- with the custom ghc executable.+-- The created process has its working directory set to the given working directory.+--+-- Invokes the cabal process in the given directory.+-- Finds the appropriate @ghc@ version as a fallback and provides the path+-- to the custom ghc wrapper via 'HIE_BIOS_GHC' environment variable which+-- the custom ghc wrapper may use as a fallback if it can not respond to certain+-- queries, such as ghc version or location of the libdir.+cabalProcess :: FilePath -> String -> [String] -> CradleLoadResultT IO CreateProcess+cabalProcess workDir command args = do+  ghcDirs <- cabalGhcDirs workDir+  newEnvironment <- liftIO $ setupEnvironment ghcDirs+  cabalProc <- liftIO $ setupCabalCommand ghcDirs+  pure $ (cabalProc+      { env = Just newEnvironment+      , cwd = Just workDir+      })+  where+    processEnvironment :: (FilePath, FilePath) -> [(String, String)]+    processEnvironment (ghcBin, libdir) =+      [("HIE_BIOS_GHC", ghcBin), ("HIE_BIOS_GHC_ARGS",  "-B" ++ libdir)]++    setupEnvironment :: (FilePath, FilePath) -> IO [(String, String)]+    setupEnvironment ghcDirs = do+      environment <- getCleanEnvironment+      pure $ processEnvironment ghcDirs ++ environment++    setupCabalCommand :: (FilePath, FilePath) -> IO CreateProcess+    setupCabalCommand (ghcBin, libdir) = do+      wrapper_fp <- withGhcWrapperTool ("ghc", []) workDir+      buildDir <- cabalBuildDir workDir+      ghcPkgPath <- withGhcPkgTool ghcBin libdir+      let extraCabalArgs =+            [ "--builddir=" <> buildDir+            , command+            , "--with-compiler", wrapper_fp+            , "--with-hc-pkg", ghcPkgPath+            ]+      pure $ proc "cabal" (extraCabalArgs ++ args)++-- | Discovers the location of 'ghc-pkg' given the absolute path to 'ghc'+-- and its '$libdir' (obtainable by running @ghc --print-libdir@).+--+-- @'withGhcPkgTool' ghcPathAbs libdir@ guesses the location by looking at+-- the filename of 'ghcPathAbs' and expects that 'ghc-pkg' is right next to it,+-- which is guaranteed by the ghc build system. Most OS's follow this+-- convention.+--+-- On unix, there is a high-chance that the obtained 'ghc' location is the+-- "unwrapped" executable, e.g. the executable without a shim that specifies+-- the '$libdir' and other important constants.+-- As such, the executable 'ghc-pkg' is similarly without a wrapper shim and+-- is lacking certain constants such as 'global-package-db'. It is, therefore,+-- not suitable to pass in to other consumers, such as 'cabal'.+--+-- Here, we restore the wrapper-shims, if necessary, thus the returned filepath+-- can be passed to 'cabal' without further modifications.+withGhcPkgTool :: FilePath -> FilePath -> IO FilePath+withGhcPkgTool ghcPathAbs libdir = do+  let ghcName = takeFileName ghcPathAbs+      -- TODO: check for existence+      ghcPkgPath = guessGhcPkgFromGhc ghcName+  if isWindows+    then pure ghcPkgPath+    else withWrapperTool ghcPkgPath+  where+    ghcDir = takeDirectory ghcPathAbs++    guessGhcPkgFromGhc ghcName =+      let ghcPkgName = T.replace "ghc" "ghc-pkg" (T.pack ghcName)+      in ghcDir </> T.unpack ghcPkgName++    -- Only on unix, creates a wrapper script that's hopefully identical+    -- to the wrapper script 'ghc-pkg' usually comes with.+    --+    -- 'ghc-pkg' needs to know the 'global-package-db' location which is+    -- passed in via a wrapper shim that basically wraps 'ghc-pkg' and+    -- only passes in the correct 'global-package-db'.+    -- For an example on how the wrapper script is supposed to look like, take+    -- a look at @cat $(which ghc-pkg)@, assuming 'ghc-pkg' is on your $PATH.+    --+    -- If we used the raw executable, i.e. not wrapped in a shim, then 'cabal'+    -- can not use the given 'ghc-pkg'.+    withWrapperTool ghcPkg = do+      let globalPackageDb = libdir </> "package.conf.d"+          -- This is the same as the wrapper-shims ghc-pkg usually comes with.+          contents = unlines+            [ "#!/bin/sh"+            , unwords ["exec", escapeFilePath ghcPkg+                      , "--global-package-db", escapeFilePath globalPackageDb+                      , "${1+\"$@\"}"+                      ]+            ]+          srcHash = show (fingerprintString contents)+      cacheFile "ghc-pkg" srcHash $ \wrapperFp -> writeFile wrapperFp contents++    -- Escape the filepath and trim excess newlines added by 'escapeArgs'+    escapeFilePath fp = trimEnd $ escapeArgs [fp]+ -- | @'cabalCradleDependencies' rootDir componentDir@. -- Compute the dependencies of the cabal cradle based -- on the cradle root and the component directory.@@ -473,8 +573,7 @@             let final_args =                     removeVerbosityOpts                     $ removeRTS-                    $ removeInteractive-                    $ ghc_args+                    $ removeInteractive ghc_args             in Just (dir, final_args)         _ -> Nothing @@ -483,69 +582,103 @@ -- arguments to the executable. type GhcProc = (FilePath, [String]) --- | Generate a fake GHC that can be passed to cabal+-- | Generate a fake GHC that can be passed to cabal or stack -- when run with --interactive, it will print out its -- command-line arguments and exit-withCabalWrapperTool :: GhcProc -> FilePath -> IO FilePath-withCabalWrapperTool (mbGhc, ghcArgs) wdir = do-    cacheDir <- getCacheDir ""-    createDirectoryIfMissing True cacheDir+withGhcWrapperTool :: GhcProc -> FilePath -> IO FilePath+withGhcWrapperTool (mbGhc, ghcArgs) wdir = do     let wrapperContents = if isWindows then cabalWrapperHs else cabalWrapper-        suffix fp = if isWindows then fp <.> "exe" else fp-    let srcHash = show (fingerprintString wrapperContents)-    let wrapper_name = "wrapper-" ++ srcHash-    let wrapper_fp = suffix $ cacheDir </> wrapper_name-    exists <- doesFileExist wrapper_fp-    unless exists $+        withExtension fp = if isWindows then fp <.> "exe" else fp+        srcHash = show (fingerprintString wrapperContents)+    cacheFile (withExtension "wrapper") srcHash $ \wrapper_fp ->       if isWindows-      then do+      then         withSystemTempDirectory "hie-bios" $ \ tmpDir -> do-          let wrapper_hs = cacheDir </> wrapper_name <.> "hs"+          let wrapper_hs = wrapper_fp -<.> "hs"           writeFile wrapper_hs wrapperContents           let ghc = (proc mbGhc $                       ghcArgs ++ ["-rtsopts=ignore", "-outputdir", tmpDir, "-o", wrapper_fp, wrapper_hs])                       { cwd = Just wdir }           readCreateProcess ghc "" >>= putStr       else writeFile wrapper_fp wrapperContents-    setMode wrapper_fp-    pure wrapper_fp++-- | Create and cache a file in hie-bios's cache directory.+--+-- @'cacheFile' fpName srcHash populate@. 'fpName' is the pattern name of the+-- cached file you want to create. 'srcHash' is the hash that is appended to+-- the file pattern and is expected to change whenever you want to invalidate+-- the cache.+--+-- If the cached file's 'srcHash' changes, then a new file is created, but+-- the old cached file name will not be deleted.+--+-- If the file does not exist yet, 'populate' is invoked with cached file+-- location and it is expected that the caller persists the given filepath in+-- the File System.+cacheFile :: FilePath -> String -> (FilePath -> IO ()) -> IO FilePath+cacheFile fpName srcHash populate = do+  cacheDir <- getCacheDir ""+  createDirectoryIfMissing True cacheDir+  let newFpName = cacheDir </> (dropExtensions fpName <> "-" <> srcHash) <.> takeExtensions fpName+  unlessM (doesFileExist newFpName) $ do+    populate newFpName+    setMode newFpName+  pure newFpName   where     setMode wrapper_fp = setFileMode wrapper_fp accessModes  -- | Given the root directory, get the build dir we are using for cabal -- In the `hie-bios` cache directory cabalBuildDir :: FilePath -> IO FilePath-cabalBuildDir work_dir = do-  abs_work_dir <- makeAbsolute work_dir+cabalBuildDir workDir = do+  abs_work_dir <- makeAbsolute workDir   let dirHash = show (fingerprintString abs_work_dir)-  getCacheDir ("dist-"<>filter (not . isSpace) (takeBaseName abs_work_dir)<>"-"<>dirHash)+  getCacheDir ("dist-" <> filter (not . isSpace) (takeBaseName abs_work_dir)<>"-"<>dirHash) -cabalAction :: FilePath -> Maybe String -> LoggingFunction -> FilePath -> IO (CradleLoadResult ComponentOptions)-cabalAction work_dir mc l fp = do-    wrapper_fp <- withCabalWrapperTool ("ghc", []) work_dir-    buildDir <- cabalBuildDir work_dir-    let cab_args = ["--builddir="<>buildDir,"v2-repl", "--with-compiler", wrapper_fp, fromMaybe (fixTargetPath fp) mc]-    (ex, output, stde, [(_,mb_args)]) <--      readProcessWithOutputs [hie_bios_output] l work_dir (proc "cabal" cab_args)-    let args = fromMaybe [] mb_args-    case processCabalWrapperArgs args of-        Nothing -> do-          -- Best effort. Assume the working directory is the-          -- the root of the component, so we are right in trivial cases at least.-          deps <- cabalCradleDependencies work_dir work_dir-          pure $ CradleFail (CradleError deps ex-                    ["Failed to parse result of calling cabal"-                     , unlines output-                     , unlines stde-                     , unlines $ args])-        Just (componentDir, final_args) -> do-          deps <- cabalCradleDependencies work_dir componentDir-          pure $ makeCradleResult (ex, stde, componentDir, final_args) deps+-- | Discover the location of the ghc binary 'cabal' is going to use together+-- with its libdir location.+-- The ghc executable is an absolute path, but not necessarily canonicalised+-- or normalised. Additionally, the ghc path returned is likely to be the raw+-- executable, i.e. without the usual wrapper shims on non-windows systems.+-- If you want to use the given ghc executable, you should invoke+-- 'withGhcWrapperTool'.+--+-- If cabal can not figure it out, a 'CradleError' is returned.+cabalGhcDirs :: FilePath -> CradleLoadResultT IO (FilePath, FilePath)+cabalGhcDirs workDir = do+  libdir <- readProcessWithCwd_ workDir  "cabal" ["exec", "-v0", "--", "ghc", "--print-libdir"] ""+  exe <- readProcessWithCwd_ workDir  "cabal"+      ["exec", "-v0", "--", "ghc", "-package-env=-", "-e", "do e <- System.Environment.getExecutablePath ; System.IO.putStr e"] ""+  pure (trimEnd exe, trimEnd libdir)++cabalAction :: FilePath -> Maybe String -> LoggingFunction -> FilePath -> CradleLoadResultT IO ComponentOptions+cabalAction workDir mc l fp = do+  cabalProc <- (cabalProcess workDir "v2-repl" [fromMaybe (fixTargetPath fp) mc]) `modCradleError` \err -> do+      deps <- cabalCradleDependencies workDir workDir+      pure $ err { cradleErrorDependencies = cradleErrorDependencies err ++ deps }++  (ex, output, stde, [(_, maybeArgs)]) <- liftIO $ readProcessWithOutputs [hie_bios_output] l workDir cabalProc++  let args = fromMaybe [] maybeArgs+  case processCabalWrapperArgs args of+    Nothing -> do+      -- Provide some dependencies an IDE can look for to trigger a reload.+      -- Best effort. Assume the working directory is the+      -- root of the component, so we are right in trivial cases at least.+      deps <- liftIO $ cabalCradleDependencies workDir workDir+      throwCE (CradleError deps ex+                ["Failed to parse result of calling cabal"+                , unlines output+                , unlines stde+                , unlines $ args])+    Just (componentDir, final_args) -> do+      deps <- liftIO $ cabalCradleDependencies workDir componentDir+      CradleLoadResultT $ pure $ makeCradleResult (ex, stde, componentDir, final_args) deps   where     -- Need to make relative on Windows, due to a Cabal bug with how it     -- parses file targets with a C: drive in it     fixTargetPath x-      | isWindows && hasDrive x = makeRelative work_dir x+      | isWindows && hasDrive x = makeRelative workDir x       | otherwise = x  removeInteractive :: [String] -> [String]@@ -621,14 +754,13 @@     , cradleOptsProg   = CradleAction         { actionName = Types.Stack         , runCradle = stackAction wdir mc syaml-        , runGhcCmd = \args -> do+        , runGhcCmd = \args -> runCradleResultT $ do             -- Setup stack silently, since stack might print stuff to stdout in some cases (e.g. on Win)             -- Issue 242 from HLS: https://github.com/haskell/haskell-language-server/issues/242-            stackSetup <- readProcessWithCwd wdir "stack" (stackYamlProcessArgs syaml <> ["setup", "--silent"]) ""-            stackSetup `bindIO` \_ ->-              readProcessWithCwd wdir "stack"-                (stackYamlProcessArgs syaml <> ["exec", "ghc", "--"] <> args)-                ""+            _ <- readProcessWithCwd_ wdir "stack" (stackYamlProcessArgs syaml <> ["setup", "--silent"]) ""+            readProcessWithCwd_ wdir "stack"+              (stackYamlProcessArgs syaml <> ["exec", "ghc", "--"] <> args)+              ""         }     } @@ -650,26 +782,26 @@     cabalFiles ++ [relFp </> "package.yaml", stackYamlLocationOrDefault syaml]  stackAction :: FilePath -> Maybe String -> StackYaml -> LoggingFunction -> FilePath -> IO (CradleLoadResult ComponentOptions)-stackAction work_dir mc syaml l _fp = do+stackAction workDir mc syaml l _fp = do   let ghcProcArgs = ("stack", stackYamlProcessArgs syaml <> ["exec", "ghc", "--"])   -- Same wrapper works as with cabal-  wrapper_fp <- withCabalWrapperTool ghcProcArgs work_dir-  (ex1, _stdo, stde, [(_, mb_args)]) <--    readProcessWithOutputs [hie_bios_output] l work_dir $+  wrapper_fp <- withGhcWrapperTool ghcProcArgs workDir+  (ex1, _stdo, stde, [(_, maybeArgs)]) <-+    readProcessWithOutputs [hie_bios_output] l workDir $     stackProcess syaml                 $  ["repl", "--no-nix-pure", "--with-ghc", wrapper_fp]                     <> [ comp | Just comp <- [mc] ]   (ex2, pkg_args, stdr, _) <--    readProcessWithOutputs [hie_bios_output] l work_dir $+    readProcessWithOutputs [hie_bios_output] l workDir $       stackProcess syaml ["path", "--ghc-package-path"]   let split_pkgs = concatMap splitSearchPath pkg_args       pkg_ghc_args = concatMap (\p -> ["-package-db", p] ) split_pkgs-      args = fromMaybe [] mb_args+      args = fromMaybe [] maybeArgs   case processCabalWrapperArgs args of       Nothing -> do         -- Best effort. Assume the working directory is the         -- the root of the component, so we are right in trivial cases at least.-        deps <- stackCradleDependencies work_dir work_dir syaml+        deps <- stackCradleDependencies workDir workDir syaml         pure $ CradleFail                   (CradleError deps ex1 $                     [ "Failed to parse result of calling stack" ]@@ -678,7 +810,7 @@                   )        Just (componentDir, ghc_args) -> do-        deps <- stackCradleDependencies work_dir componentDir syaml+        deps <- stackCradleDependencies workDir componentDir syaml         pure $ makeCradleResult                   ( combineExitCodes [ex1, ex2]                   , stde ++ stdr, componentDir@@ -730,15 +862,15 @@ bazelCommand = $(embedStringFile "wrappers/bazel")  rulesHaskellAction :: FilePath -> FilePath -> IO (CradleLoadResult ComponentOptions)-rulesHaskellAction work_dir fp = do+rulesHaskellAction workDir fp = do   wrapper_fp <- writeSystemTempFile "wrapper" bazelCommand   setFileMode wrapper_fp accessModes-  let rel_path = makeRelative work_dir fp+  let rel_path = makeRelative workDir fp   (ex, args, stde) <--      readProcessWithOutputFile work_dir wrapper_fp [rel_path] []+      readProcessWithOutputFile workDir wrapper_fp [rel_path] []   let args'  = filter (/= '\'') args   let args'' = filter (/= "\"$GHCI_LOCATION\"") (words args')-  deps <- rulesHaskellCradleDependencies work_dir+  deps <- rulesHaskellCradleDependencies workDir   return $ makeCradleResult (ex, stde, args'') deps  @@ -769,11 +901,11 @@     }  obeliskAction :: FilePath -> FilePath -> IO (CradleLoadResult ComponentOptions)-obeliskAction work_dir _fp = do+obeliskAction workDir _fp = do   (ex, args, stde) <--      readProcessWithOutputFile work_dir "ob" ["ide-args"] []+      readProcessWithOutputFile workDir "ob" ["ide-args"] [] -  o_deps <- obeliskCradleDependencies work_dir+  o_deps <- obeliskCradleDependencies workDir   return (makeCradleResult (ex, stde, words args) o_deps )  -}@@ -808,13 +940,12 @@     getFiles = filter p <$> getDirectoryContents dir     doesPredFileExist file = doesFileExist $ dir </> file --- Some environments (e.g. stack exec) include GHC_PACKAGE_PATH.--- Cabal v2 *will* complain, even though or precisely because it ignores them+-- | Some environments (e.g. stack exec) include GHC_PACKAGE_PATH.+-- Cabal v2 *will* complain, even though or precisely because it ignores them. -- Unset them from the environment to sidestep this getCleanEnvironment :: IO [(String, String)] getCleanEnvironment = do-  e <- getEnvironment-  return $ Map.toList $ Map.delete "GHC_PACKAGE_PATH" $ Map.fromList e+  Map.toList . Map.delete "GHC_PACKAGE_PATH" . Map.fromList <$> getEnvironment  type Outputs = [OutputName] type OutputName = String@@ -831,16 +962,17 @@   -> FilePath -- ^ Working directory. Process is executed in this directory.   -> CreateProcess -- ^ Parameters for the process to be executed.   -> IO (ExitCode, [String], [String], [(OutputName, Maybe [String])])-readProcessWithOutputs outputNames l work_dir cp = flip runContT return $ do+readProcessWithOutputs outputNames l workDir cp = flip runContT return $ do   old_env <- liftIO getCleanEnvironment   output_files <- traverse (withOutput old_env) outputNames    let process = cp { env = Just $ output_files ++ fromMaybe old_env (env cp),-                     cwd = Just work_dir+                     cwd = Just workDir                     }      -- Windows line endings are not converted so you have to filter out `'r` characters-  let  loggingConduit = C.decodeUtf8  C..| C.lines C..| C.filterE (/= '\r')  C..| C.map T.unpack C..| C.iterM l C..| C.sinkList+  let loggingConduit = C.decodeUtf8  C..| C.lines C..| C.filterE (/= '\r')+        C..| C.map T.unpack C..| C.iterM l C..| C.sinkList   (ex, stdo, stde) <- liftIO $ sourceProcessWithStreams process mempty loggingConduit loggingConduit    res <- forM output_files $ \(name,path) ->@@ -890,15 +1022,31 @@   -- case mResult of   --   Nothing --- | Wrapper around 'readCreateProcess' that sets the working directory+-- | Wrapper around 'readCreateProcess' that sets the working directory and+-- clears the environment, suitable for invoking cabal/stack and raw ghc commands. readProcessWithCwd :: FilePath -> FilePath -> [String] -> String -> IO (CradleLoadResult String)-readProcessWithCwd dir cmd args stdi = do-  cleanEnv <- getCleanEnvironment+readProcessWithCwd dir cmd args stdin = runCradleResultT $ readProcessWithCwd_ dir cmd args stdin++readProcessWithCwd_ :: FilePath -> FilePath -> [String] -> String -> CradleLoadResultT IO String+readProcessWithCwd_ dir cmd args stdin = do+  cleanEnv <- liftIO getCleanEnvironment   let createProc = (proc cmd args) { cwd = Just dir, env = Just cleanEnv }-  mResult <- optional $ readCreateProcessWithExitCode createProc stdi+  readProcessWithCwd' createProc stdin++-- | Wrapper around 'readCreateProcessWithExitCode', wrapping the result in+-- a 'CradleLoadResult'. Provides better error messages than raw 'readCreateProcess'.+readProcessWithCwd' :: CreateProcess -> String -> CradleLoadResultT IO String+readProcessWithCwd' createdProcess stdin = do+  mResult <- liftIO $ optional $ readCreateProcessWithExitCode createdProcess stdin+  let cmdString = prettyCmdSpec $ cmdspec createdProcess   case mResult of-    Just (ExitSuccess, stdo, _) -> pure $ CradleSuccess stdo-    Just (exitCode, stdo, stde) -> pure $ CradleFail $-      CradleError [] exitCode ["Error when calling " <> cmd <> " " <> unwords args, stdo, stde]-    Nothing -> pure $ CradleFail $-      CradleError [] ExitSuccess ["Couldn't execute " <> cmd <> " " <> unwords args]+    Just (ExitSuccess, stdo, _) -> pure stdo+    Just (exitCode, stdo, stde) -> throwCE $+      CradleError [] exitCode ["Error when calling " <> cmdString, stdo, stde]+    Nothing -> throwCE $+      CradleError [] ExitSuccess ["Couldn't execute " <> cmdString]++-- | Prettify 'CmdSpec', so we can show the command to a user+prettyCmdSpec :: CmdSpec -> String+prettyCmdSpec (ShellCommand s) = s+prettyCmdSpec (RawCommand cmd args) = cmd ++ " " ++ unwords args
src/HIE/Bios/Types.hs view
@@ -1,6 +1,9 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DeriveTraversable #-}+{-# LANGUAGE DeriveFoldable #-} {-# OPTIONS_GHC -Wno-orphans #-}  {-# LANGUAGE DeriveFoldable #-}@@ -9,6 +12,12 @@  import           System.Exit import           Control.Exception              ( Exception )+import           Control.Monad+import           Control.Monad.IO.Class+import           Control.Monad.Trans.Class+#if MIN_VERSION_base(4,9,0)+import qualified Control.Monad.Fail as Fail+#endif  data BIOSVerbosity = Silent | Verbose @@ -68,6 +77,10 @@   | CradleNone -- ^ No attempt was made to load the cradle.  deriving (Functor, Foldable, Traversable, Show, Eq) +cradleLoadResult :: c -> (CradleError -> c) -> (r -> c) -> CradleLoadResult r -> c+cradleLoadResult c _ _ CradleNone        = c+cradleLoadResult _ f _ (CradleFail e)    = f e+cradleLoadResult _ _ f (CradleSuccess r) = f r  instance Applicative CradleLoadResult where   pure = CradleSuccess@@ -82,11 +95,65 @@   CradleFail err >>= _ = CradleFail err   CradleNone >>= _ = CradleNone -bindIO :: CradleLoadResult a -> (a -> IO (CradleLoadResult b)) -> IO (CradleLoadResult b)-bindIO  (CradleSuccess r) k = k r-bindIO (CradleFail err) _ = return $ CradleFail err-bindIO CradleNone _ = return CradleNone+newtype CradleLoadResultT m a = CradleLoadResultT { runCradleResultT :: m (CradleLoadResult a) } +instance Functor f => Functor (CradleLoadResultT f) where+  {-# INLINE fmap #-}+  fmap f = CradleLoadResultT . (fmap . fmap) f . runCradleResultT++instance (Monad m, Applicative m) => Applicative (CradleLoadResultT m) where+  {-# INLINE pure #-}+  pure = CradleLoadResultT . pure . CradleSuccess+  {-# INLINE (<*>) #-}+  x <*> f = CradleLoadResultT $ do+              a <- runCradleResultT x+              case a of+                CradleSuccess a' -> do+                  b <- runCradleResultT f+                  case b of+                    CradleSuccess b' -> pure (CradleSuccess (a' b'))+                    CradleFail err -> pure $ CradleFail err+                    CradleNone -> pure CradleNone+                CradleFail err -> pure $ CradleFail err+                CradleNone -> pure CradleNone++instance Monad m => Monad (CradleLoadResultT m) where+  {-# INLINE return #-}+  return = CradleLoadResultT . return . CradleSuccess+  {-# INLINE (>>=) #-}+  x >>= f = CradleLoadResultT $ do+    val <- runCradleResultT x+    case val of+      CradleSuccess r -> runCradleResultT . f $ r+      CradleFail err -> return $ CradleFail err+      CradleNone -> return $ CradleNone+#if !(MIN_VERSION_base(4,13,0))+  fail = CradleLoadResultT . fail+  {-# INLINE fail #-}+#endif+#if MIN_VERSION_base(4,9,0)+instance Fail.MonadFail m => Fail.MonadFail (CradleLoadResultT m) where+  fail = CradleLoadResultT . Fail.fail+  {-# INLINE fail #-}+#endif++instance MonadTrans CradleLoadResultT where+    lift = CradleLoadResultT . liftM CradleSuccess+    {-# INLINE lift #-}++instance (MonadIO m) => MonadIO (CradleLoadResultT m) where+    liftIO = lift . liftIO+    {-# INLINE liftIO #-}++modCradleError :: Monad m => CradleLoadResultT m a -> (CradleError -> m CradleError) -> CradleLoadResultT m a+modCradleError action f = CradleLoadResultT $ do+  a <- runCradleResultT action+  case a of+    CradleFail err -> CradleFail <$> f err+    _ -> pure a++throwCE :: Monad m => CradleError -> CradleLoadResultT m a+throwCE = CradleLoadResultT . return . CradleFail  data CradleError = CradleError   { cradleErrorDependencies :: [FilePath]
tests/BiosTests.hs view
@@ -2,13 +2,15 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE CPP #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeApplications #-} module Main where  import Test.Tasty import Test.Tasty.HUnit-#if __GLASGOW_HASKELL__ < 810 import Test.Tasty.ExpectedFailure-#endif+import qualified Test.Tasty.Options as Tasty+import qualified Test.Tasty.Ingredients as Tasty import qualified GHC as G import HIE.Bios import HIE.Bios.Ghc.Api@@ -18,8 +20,9 @@ import HIE.Bios.Types import Control.Monad.IO.Class import Control.Monad ( forM_, unless )+import Data.List ( sort, isPrefixOf, isInfixOf )+import Data.Typeable import Data.Void-import Data.List ( sort, isPrefixOf ) import System.Directory import System.FilePath (addTrailingPathSeparator,  makeRelative, (</>) ) import System.Info.Extra ( isWindows )@@ -31,10 +34,25 @@ argDynamic :: [String] argDynamic = ["-dynamic" | Gap.hostIsDynamic] +-- | This ghc version is assumed to be tested by CI to validate+-- the "with-compiler" field is honoured by hie-bios.+--+-- If you change this version, make sure to also update 'cabal.project'+-- in 'tests\/projects\/cabal-with-ghc'.+extraGhcVersion :: String+extraGhcVersion = "8.10.7"++extraGhc :: String+extraGhc = "ghc-" ++ extraGhcVersion+ main :: IO () main = do   writeStackYamlFiles-  defaultMain $+  stackDep <- checkToolIsAvailable "stack"+  cabalDep <- checkToolIsAvailable "cabal"+  extraGhcDep <- checkToolIsAvailable extraGhc++  defaultMainWithIngredients (ignoreToolTests:defaultIngredients) $     testGroup "Bios-tests"       [ testGroup "Find cradle"         [ testCaseSteps "simple-cabal"@@ -93,89 +111,10 @@                     _ -> assertFailure "Cradle loaded symlink"         ]       , testGroup "Loading tests"-        $ linuxExlusiveTestCases-        ++-          [ testCaseSteps "failing-cabal" $ testDirectoryFail isCabalCradle "./tests/projects/failing-cabal" "MyLib.hs"-            (\CradleError {..} -> do-                cradleErrorExitCode @?= ExitFailure 1-                cradleErrorDependencies `shouldMatchList` ["failing-cabal.cabal", "cabal.project", "cabal.project.local"])-          , testCaseSteps "failing-bios" $ testDirectoryFail isBiosCradle "./tests/projects/failing-bios" "B.hs"-            (\CradleError {..} -> do-                cradleErrorExitCode @?= ExitFailure 1-                cradleErrorDependencies `shouldMatchList` ["hie.yaml"])-          , testCaseSteps "failing-bios-ghc" $ testGetGhcVersionFail isBiosCradle "./tests/projects/failing-bios-ghc" "B.hs"-            (\CradleError {..} -> do-                cradleErrorExitCode @?= ExitSuccess-                cradleErrorDependencies `shouldMatchList` []-                length cradleErrorStderr @?= 1-                "Couldn't execute myGhc" `isPrefixOf` head cradleErrorStderr @? "Error message should contain basic information" )-          , testCaseSteps "simple-bios-shell" $ testDirectory isBiosCradle "./tests/projects/simple-bios-shell" "B.hs"-          , testCaseSteps "simple-bios-shell-deps" $ testLoadCradleDependencies isBiosCradle "./tests/projects/simple-bios-shell" "B.hs" (assertEqual "dependencies" ["hie.yaml"])-          , testCaseSteps "simple-cabal" $ testDirectory isCabalCradle "./tests/projects/simple-cabal" "B.hs"-          , testCaseSteps "simple-direct" $ testDirectory isDirectCradle "./tests/projects/simple-direct" "B.hs"-          , testCaseSteps "nested-cabal" $ testLoadCradleDependencies isCabalCradle "./tests/projects/nested-cabal" "sub-comp/Lib.hs"-            (\deps -> deps `shouldMatchList` ["sub-comp" </> "sub-comp.cabal", "cabal.project", "cabal.project.local"]-            )-          , testCaseSteps "nested-cabal2" $ testLoadCradleDependencies isCabalCradle "./tests/projects/nested-cabal" "MyLib.hs"-            (\deps -> deps `shouldMatchList` ["nested-cabal.cabal", "cabal.project", "cabal.project.local"]-            )-          , testCaseSteps "multi-direct" {- tests if both components can be loaded -}-                        $  testDirectory isMultiCradle "./tests/projects/multi-direct" "A.hs"-                        >> testDirectory isMultiCradle "./tests/projects/multi-direct" "B.hs"-          , testCaseSteps "multi-cabal" {- tests if both components can be loaded -}-                        $  testDirectory isCabalCradle "./tests/projects/multi-cabal" "app/Main.hs"-                        >> testDirectory isCabalCradle "./tests/projects/multi-cabal" "src/Lib.hs"-          , testCaseSteps "monorepo-cabal" {- issue https://github.com/mpickering/hie-bios/issues/200 -}-                        $  testDirectory isCabalCradle "./tests/projects/monorepo-cabal" "A/Main.hs"-                        >> testDirectory isCabalCradle "./tests/projects/monorepo-cabal" "B/MyLib.hs"--- TODO: Remove once stack and ghc-8.10.1 play well--- https://github.com/bubba/hie-bios/runs/811271872?check_suite_focus=true-#if __GLASGOW_HASKELL__ < 810-          , expectFailBecause "stack repl does not fail on an invalid cabal file" $-              testCaseSteps "failing-stack" $ testDirectoryFail isStackCradle "./tests/projects/failing-stack" "src/Lib.hs"-                (\CradleError {..} -> do-                    cradleErrorExitCode @?= ExitFailure 1-                    cradleErrorDependencies `shouldMatchList` ["failing-stack.cabal", "stack.yaml", "package.yaml"])-          , testCaseSteps "simple-stack" $ testDirectory isStackCradle "./tests/projects/simple-stack" "B.hs"-          , testCaseSteps "multi-stack" {- tests if both components can be loaded -}-                        $  testDirectory isStackCradle "./tests/projects/multi-stack" "app/Main.hs"-                        >> testDirectory isStackCradle "./tests/projects/multi-stack" "src/Lib.hs"-          , testCaseSteps "nested-stack" $ testLoadCradleDependencies isStackCradle "./tests/projects/nested-stack" "sub-comp/Lib.hs"-                (\deps -> deps `shouldMatchList`-                  ["sub-comp" </> "sub-comp.cabal", "sub-comp" </> "package.yaml", "stack.yaml"]-                )-          , testCaseSteps "nested-stack2" $ testLoadCradleDependencies isStackCradle "./tests/projects/nested-stack" "MyLib.hs"-              (\deps -> deps `shouldMatchList` ["nested-stack.cabal", "package.yaml", "stack.yaml"]-              )-          , testCaseSteps "stack-with-yaml" {- tests if both components can be loaded -}-                        $  testDirectory isStackCradle "./tests/projects/stack-with-yaml" "app/Main.hs"-                        >> testDirectory isStackCradle "./tests/projects/stack-with-yaml" "src/Lib.hs"-          , testCaseSteps "multi-stack-with-yaml" {- tests if both components can be loaded -}-                        $  testDirectory isStackCradle "./tests/projects/multi-stack-with-yaml" "appA/src/Lib.hs"-                        >> testDirectory isStackCradle "./tests/projects/multi-stack-with-yaml" "appB/src/Lib.hs"-          ,-          -- Test for special characters in the path for parsing of the ghci-scripts.-          -- Issue https://github.com/mpickering/hie-bios/issues/162-          testCaseSteps "space stack"-                        $  testDirectory isStackCradle "./tests/projects/space stack" "A.hs"-                        >> testDirectory isStackCradle "./tests/projects/space stack" "B.hs"-#endif-        ]-      , testGroup "Implicit cradle tests" $-        [ testCaseSteps "implicit-cabal" $-            testImplicitCradle "./tests/projects/implicit-cabal" "Main.hs" Cabal-        , testCaseSteps "implicit-cabal-no-project" $-            testImplicitCradle "./tests/projects/implicit-cabal-no-project" "Main.hs" Cabal-        , testCaseSteps "implicit-cabal-deep-project" $-            testImplicitCradle "./tests/projects/implicit-cabal-deep-project" "foo/Main.hs" Cabal--- TODO: same here-#if __GLASGOW_HASKELL__ < 810-        , testCaseSteps "implicit-stack" $-            testImplicitCradle "./tests/projects/implicit-stack" "Main.hs" Stack-        , testCaseSteps "implicit-stack-multi"-            $ testImplicitCradle "./tests/projects/implicit-stack-multi" "Main.hs" Stack-            >> testImplicitCradle "./tests/projects/implicit-stack-multi" "other-package/Main.hs" Stack-#endif+        [ testGroup "bios" biosTestCases+        , testGroup "direct" directTestCases+        , testGroupWithDependency cabalDep (cabalTestCases extraGhcDep)+        , ignoreOnGhc9AndNewer $ testGroupWithDependency stackDep stackTestCases         ]       ] @@ -190,6 +129,107 @@   | otherwise   = [] +cabalTestCases :: ToolDependency -> [TestTree]+cabalTestCases extraGhcDep =+  [ testCaseSteps "failing-cabal" $ testDirectoryFail isCabalCradle "./tests/projects/failing-cabal" "MyLib.hs"+    (\CradleError {..} -> do+        cradleErrorExitCode @?= ExitFailure 1+        cradleErrorDependencies `shouldMatchList` ["failing-cabal.cabal", "cabal.project", "cabal.project.local"])+  , testCaseSteps "simple-cabal" $ testDirectory isCabalCradle "./tests/projects/simple-cabal" "B.hs"+  , testCaseSteps "nested-cabal" $ testLoadCradleDependencies isCabalCradle "./tests/projects/nested-cabal" "sub-comp/Lib.hs"+    (\deps -> deps `shouldMatchList` ["sub-comp" </> "sub-comp.cabal", "cabal.project", "cabal.project.local"]+    )+  , testCaseSteps "nested-cabal2" $ testLoadCradleDependencies isCabalCradle "./tests/projects/nested-cabal" "MyLib.hs"+    (\deps -> deps `shouldMatchList` ["nested-cabal.cabal", "cabal.project", "cabal.project.local"]+    )+  , testCaseSteps "multi-cabal" {- tests if both components can be loaded -}+                $  testDirectory isCabalCradle "./tests/projects/multi-cabal" "app/Main.hs"+                >> testDirectory isCabalCradle "./tests/projects/multi-cabal" "src/Lib.hs"+  , testCaseSteps "monorepo-cabal" {- issue https://github.com/mpickering/hie-bios/issues/200 -}+                $  testDirectory isCabalCradle "./tests/projects/monorepo-cabal" "A/Main.hs"+                >> testDirectory isCabalCradle "./tests/projects/monorepo-cabal" "B/MyLib.hs"+  , testGroup "Implicit cradle tests" $+        [ testCaseSteps "implicit-cabal" $+            testImplicitCradle "./tests/projects/implicit-cabal" "Main.hs" Cabal+        , testCaseSteps "implicit-cabal-no-project" $+            testImplicitCradle "./tests/projects/implicit-cabal-no-project" "Main.hs" Cabal+        , testCaseSteps "implicit-cabal-deep-project" $+            testImplicitCradle "./tests/projects/implicit-cabal-deep-project" "foo/Main.hs" Cabal+        ]+  , testGroupWithDependency extraGhcDep+    [ testCaseSteps "Appropriate ghc and libdir" $ \step -> do+        fp <- canonicalizePath "./tests/projects/cabal-with-ghc/src/MyLib.hs"+        crd <- initialiseCradle isCabalCradle fp step+        step "Get runtime GHC library directory"+        testGetGhcLibDir crd extraGhcVersion+        step "Get runtime GHC version"+        testGetGhcVersion crd extraGhcVersion+    ]+  ]++stackTestCases :: [TestTree]+stackTestCases =+  [ expectFailBecause "stack repl does not fail on an invalid cabal file" $+      testCaseSteps "failing-stack" $ testDirectoryFail isStackCradle "./tests/projects/failing-stack" "src/Lib.hs"+        (\CradleError {..} -> do+            cradleErrorExitCode @?= ExitFailure 1+            cradleErrorDependencies `shouldMatchList` ["failing-stack.cabal", "stack.yaml", "package.yaml"])+  , testCaseSteps "simple-stack" $ testDirectory isStackCradle "./tests/projects/simple-stack" "B.hs"+  , testCaseSteps "multi-stack" {- tests if both components can be loaded -}+                $  testDirectory isStackCradle "./tests/projects/multi-stack" "app/Main.hs"+                >> testDirectory isStackCradle "./tests/projects/multi-stack" "src/Lib.hs"+  , testCaseSteps "nested-stack" $ testLoadCradleDependencies isStackCradle "./tests/projects/nested-stack" "sub-comp/Lib.hs"+        (\deps -> deps `shouldMatchList`+          ["sub-comp" </> "sub-comp.cabal", "sub-comp" </> "package.yaml", "stack.yaml"]+        )+  , testCaseSteps "nested-stack2" $ testLoadCradleDependencies isStackCradle "./tests/projects/nested-stack" "MyLib.hs"+      (\deps -> deps `shouldMatchList` ["nested-stack.cabal", "package.yaml", "stack.yaml"]+      )+  , testCaseSteps "stack-with-yaml" {- tests if both components can be loaded -}+                $  testDirectory isStackCradle "./tests/projects/stack-with-yaml" "app/Main.hs"+                >> testDirectory isStackCradle "./tests/projects/stack-with-yaml" "src/Lib.hs"+  , testCaseSteps "multi-stack-with-yaml" {- tests if both components can be loaded -}+                $  testDirectory isStackCradle "./tests/projects/multi-stack-with-yaml" "appA/src/Lib.hs"+                >> testDirectory isStackCradle "./tests/projects/multi-stack-with-yaml" "appB/src/Lib.hs"+  ,+    -- Test for special characters in the path for parsing of the ghci-scripts.+    -- Issue https://github.com/mpickering/hie-bios/issues/162+    testCaseSteps "space stack"+                  $  testDirectory isStackCradle "./tests/projects/space stack" "A.hs"+                  >> testDirectory isStackCradle "./tests/projects/space stack" "B.hs"+  , testGroup "Implicit cradle tests"+      [ testCaseSteps "implicit-stack" $+          testImplicitCradle "./tests/projects/implicit-stack" "Main.hs" Stack+      , testCaseSteps "implicit-stack-multi"+          $ testImplicitCradle "./tests/projects/implicit-stack-multi" "Main.hs" Stack+          >> testImplicitCradle "./tests/projects/implicit-stack-multi" "other-package/Main.hs" Stack+      ]+  ]++biosTestCases :: [TestTree]+biosTestCases =+  [ testCaseSteps "failing-bios" $ testDirectoryFail isBiosCradle "./tests/projects/failing-bios" "B.hs"+    (\CradleError {..} -> do+        cradleErrorExitCode @?= ExitFailure 1+        cradleErrorDependencies `shouldMatchList` ["hie.yaml"])+  , testCaseSteps "failing-bios-ghc" $ testGetGhcVersionFail isBiosCradle "./tests/projects/failing-bios-ghc" "B.hs"+    (\CradleError {..} -> do+        cradleErrorExitCode @?= ExitSuccess+        cradleErrorDependencies `shouldMatchList` []+        length cradleErrorStderr @?= 1+        "Couldn't execute myGhc" `isPrefixOf` head cradleErrorStderr @? "Error message should contain basic information" )+  , testCaseSteps "simple-bios-shell" $ testDirectory isBiosCradle "./tests/projects/simple-bios-shell" "B.hs"+  , testCaseSteps "simple-bios-shell-deps" $ testLoadCradleDependencies isBiosCradle "./tests/projects/simple-bios-shell" "B.hs" (assertEqual "dependencies" ["hie.yaml"])+  ]++directTestCases :: [TestTree]+directTestCases =+  [ testCaseSteps "simple-direct" $ testDirectory isDirectCradle "./tests/projects/simple-direct" "B.hs"+  , testCaseSteps "multi-direct" {- tests if both components can be loaded -}+      $  testDirectory isMultiCradle "./tests/projects/multi-direct" "A.hs"+      >> testDirectory isMultiCradle "./tests/projects/multi-direct" "B.hs"+  ]+ testDirectory :: (Cradle Void -> Bool) -> FilePath -> FilePath -> (String -> IO ()) -> IO () testDirectory cradlePred rootDir file step =   -- We need to copy over the directory to somewhere outside the source tree@@ -199,29 +239,34 @@     fp <- canonicalizePath (rootDir' </> file)     crd <- initialiseCradle cradlePred fp step     step "Get runtime GHC library directory"-    testGetGhcLibDir crd+    testGetGhcLibDir crd VERSION_ghc     step "Get runtime GHC version"-    testGetGhcVersion crd+    testGetGhcVersion crd VERSION_ghc     step "Initialise Flags"     testLoadFile crd fp step  -- | Here we are testing that the cradle's method of obtaining the ghcLibDir -- always works.-testGetGhcLibDir :: Cradle a -> IO ()-testGetGhcLibDir crd = do+testGetGhcLibDir :: Cradle a -> String -> IO ()+testGetGhcLibDir crd ghcVersion = do   libDirRes <- getRuntimeGhcLibDir crd-  isSuccess libDirRes @? "Must succeed loading ghc lib directory"-  where isSuccess (CradleSuccess _) = True-        isSuccess _ = False+  isSuccess libDirRes+  where+    -- heuristically test that the produced $libdir makes sense.+    isSuccess (CradleSuccess path) =+      ghcVersion `isInfixOf` path @? "Expected \"" ++ ghcVersion+                                     ++ "\" to be infix of: " ++ path+    isSuccess _ =+      assertFailure "Must succeed loading ghc lib directory"  -- | Here we are testing that the cradle's method of getting the runtime ghc -- version is correct - which while testing, should be the version that we have -- built the tests with. This will fail if you compiled the tests with a ghc -- that doesn't equal the ghc on your path though :(-testGetGhcVersion :: Cradle a -> IO ()-testGetGhcVersion crd = do+testGetGhcVersion :: Cradle a -> String -> IO ()+testGetGhcVersion crd ghcVersion = do   version <- getRuntimeGhcVersion crd-  version @?= CradleSuccess VERSION_ghc+  version @?= CradleSuccess ghcVersion  testGetGhcVersionFail :: (Cradle Void -> Bool) -> FilePath -> FilePath -> (CradleError -> Assertion) -> (String -> IO ()) -> IO () testGetGhcVersionFail cradlePred rootDir file cradleFailPred step =@@ -393,10 +438,76 @@   "lts-14.27" -- GHC 8.6.5 #elif (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(8,6,4,0)))   "lts-13.19" -- GHC 8.6.4-#elif (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(8,4,4,0)))-  "lts-12.26" -- GHC 8.4.4-#elif (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(8,4,3,0)))-  "lts-12.14" -- GHC 8.4.3-#elif (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(8,2,2,0)))-  "lts-11.22" -- GHC 8.2.2 #endif++-- ------------------------------------------------------------------+-- Most tests have some run-time tool dependencies.+-- We only want to run tests if these tools are available.+-- ------------------------------------------------------------------++data ToolDependency = ToolDependency+  { toolName :: String+  , toolExists :: Bool+  }++checkToolIsAvailable :: String -> IO ToolDependency+checkToolIsAvailable f = do+  exists <- maybe False (const True) <$> findExecutable f+  pure ToolDependency+    { toolName = f+    , toolExists = exists+    }++testGroupWithDependency :: ToolDependency -> [TestTree] -> TestTree+testGroupWithDependency td tc = askOption @IgnoreToolDeps (\case+  IgnoreToolDeps ignoreToolDep+    | ignoreToolDep || toolExists td -> tg+    | otherwise -> itg+  )+  where+    tg = testGroup (toolName td) tc++    itg =+      ignoreTestBecause+        ("These tests require that the following" +++        " tool can be found on the path: " ++ toolName td)+        tg++-- ------------------------------------------------------------------+-- Run test-suite ignoring run-time tool dependencies.+-- Can be used to force CI to run the whole test-suite.+-- Makes sure that the full test-suite is being run on a properly configured+-- environment.+-- ------------------------------------------------------------------++-- | This option, when set to 'True', specifies that we should run in the+-- «list tests» mode+newtype IgnoreToolDeps = IgnoreToolDeps Bool+  deriving (Eq, Ord, Typeable)++instance Tasty.IsOption IgnoreToolDeps where+  defaultValue = IgnoreToolDeps False+  parseValue = fmap IgnoreToolDeps . Tasty.safeReadBool+  optionName = pure "ignore-tool-deps"+  optionHelp = pure "Run tests whether their tool dependencies exist or not"+  optionCLParser = Tasty.flagCLParser Nothing (IgnoreToolDeps True)++-- | The ingredient that provides the test listing functionality+ignoreToolTests :: Tasty.Ingredient+ignoreToolTests = Tasty.TestManager [Tasty.Option (Proxy :: Proxy IgnoreToolDeps)] $+  \opts _tree ->+    case Tasty.lookupOption opts of+      IgnoreToolDeps False -> Nothing+      IgnoreToolDeps True -> Just $ pure True++-- ------------------------------------------------------------------+-- Ignore test group if built with GHC 9 or newer+-- ------------------------------------------------------------------++ignoreOnGhc9AndNewer :: TestTree -> TestTree+ignoreOnGhc9AndNewer tt =+#if (defined(MIN_VERSION_GLASGOW_HASKELL) && MIN_VERSION_GLASGOW_HASKELL(9,0,0,0))+  ignoreTestBecause "Not supported on GHC >= 9"+#endif+  tt+
+ tests/projects/cabal-with-ghc/cabal-with-ghc.cabal view
@@ -0,0 +1,9 @@+cabal-version:      2.4+name:               cabal-with-ghc+version:            0.1.0.0++library+    exposed-modules:  MyLib+    build-depends:    base+    hs-source-dirs:   src+    default-language: Haskell2010
+ tests/projects/cabal-with-ghc/cabal.project view
@@ -0,0 +1,12 @@+packages: .++-- It is intended that this ghc version is different to at least one ghc version+-- that is tested in CI.+--+-- Project validates that hie-bios honours this field.+--+-- If you change this, make sure to also change the 'extraGhc' constant in+-- 'tests/BiosTests.hs'.+--+-- Additionally, CI needs a proper setup to execute this test-case.+with-compiler: ghc-8.10.7
+ tests/projects/cabal-with-ghc/hie.yaml view
@@ -0,0 +1,2 @@+cradle:+  cabal:
+ tests/projects/cabal-with-ghc/src/MyLib.hs view
@@ -0,0 +1,4 @@+module MyLib (someFunc) where++someFunc :: IO ()+someFunc = putStrLn "someFunc"
wrappers/cabal view
@@ -10,5 +10,5 @@     out "$arg"   done else-  "ghc" "$@"+  "${HIE_BIOS_GHC:-ghc}" "${HIE_BIOS_GHC_ARGS}" "$@" fi
wrappers/cabal.hs view
@@ -1,7 +1,8 @@ module Main (main) where +import Data.Maybe (fromMaybe) import System.Directory (getCurrentDirectory)-import System.Environment (getArgs, getEnv)+import System.Environment (getArgs, getEnv, lookupEnv) import System.Exit (exitWith) import System.Process (spawnProcess, waitForProcess) import System.IO (openFile, hClose, hPutStrLn, IOMode(..))@@ -17,6 +18,7 @@       mapM_ (hPutStrLn h) args       hClose h     _ -> do-      ph <- spawnProcess "ghc" (args)+      ghc_path <- fromMaybe "ghc" <$> lookupEnv "HIE_BIOS_GHC"+      ph <- spawnProcess ghc_path (args)       code <- waitForProcess ph       exitWith code