packages feed

hie-bios 0.20.0 → 0.21.0

raw patch · 25 files changed

+732/−315 lines, 25 filesdep +asyncdep +filelockdep +processPVP ok

version bump matches the API change (PVP)

Dependencies added: async, filelock, process

API changes (from Hackage documentation)

- HIE.Bios.Types: LogCabalLibraryTooOld :: [String] -> Log
+ HIE.Bios: CradleRunConfig :: Maybe CacheDir -> CradleRunConfig
+ HIE.Bios: [cradleCacheDir] :: CradleRunConfig -> Maybe CacheDir
+ HIE.Bios: data CradleRunConfig
+ HIE.Bios: defaultCradleRunConfig :: CradleRunConfig
+ HIE.Bios: loadCradleWithConfig :: LogAction IO (WithSeverity Log) -> CradleRunConfig -> FilePath -> IO (Cradle Void)
+ HIE.Bios: loadImplicitCradleWithConfig :: Show a => LogAction IO (WithSeverity Log) -> CradleRunConfig -> FilePath -> IO (Cradle a)
+ HIE.Bios.Cradle: CradleRunConfig :: Maybe CacheDir -> CradleRunConfig
+ HIE.Bios.Cradle: [cradleCacheDir] :: CradleRunConfig -> Maybe CacheDir
+ HIE.Bios.Cradle: data CradleRunConfig
+ HIE.Bios.Cradle: defaultCradleRunConfig :: CradleRunConfig
+ HIE.Bios.Cradle: getCradleWithConfig :: Show a => LogAction IO (WithSeverity Log) -> CradleRunConfig -> (b -> CradleAction a) -> (CradleConfig b, FilePath) -> IO (Cradle a)
+ HIE.Bios.Cradle: loadCradleWithConfig :: LogAction IO (WithSeverity Log) -> CradleRunConfig -> FilePath -> IO (Cradle Void)
+ HIE.Bios.Cradle: loadImplicitCradleWithConfig :: Show a => LogAction IO (WithSeverity Log) -> CradleRunConfig -> FilePath -> IO (Cradle a)
+ HIE.Bios.Cradle.Resolved: [cradleCacheDirResolved] :: ResolvedCradles a -> CacheDir
+ HIE.Bios.Environment: resolveCacheDir :: FilePath -> Maybe CacheDir -> IO CacheDir
+ HIE.Bios.Process: cacheFileIn :: CacheDir -> FilePath -> String -> (FilePath -> IO ()) -> IO FilePath
+ HIE.Bios.Types: CacheDir :: FilePath -> CacheDir
+ HIE.Bios.Types: CradleRunConfig :: Maybe CacheDir -> CradleRunConfig
+ HIE.Bios.Types: LogKeepTempFilesNotSupported :: [String] -> Log
+ HIE.Bios.Types: LogWithReplNotSupported :: [String] -> Log
+ HIE.Bios.Types: [cradleCacheDir] :: CradleRunConfig -> Maybe CacheDir
+ HIE.Bios.Types: [unCacheDir] :: CacheDir -> FilePath
+ HIE.Bios.Types: data CradleRunConfig
+ HIE.Bios.Types: defaultCradleRunConfig :: CradleRunConfig
+ HIE.Bios.Types: instance Data.String.IsString HIE.Bios.Types.CacheDir
+ HIE.Bios.Types: instance GHC.Classes.Eq HIE.Bios.Types.CacheDir
+ HIE.Bios.Types: instance GHC.Classes.Eq HIE.Bios.Types.CradleRunConfig
+ HIE.Bios.Types: instance GHC.Show.Show HIE.Bios.Types.CacheDir
+ HIE.Bios.Types: instance GHC.Show.Show HIE.Bios.Types.CradleRunConfig
+ HIE.Bios.Types: newtype CacheDir
- HIE.Bios.Cradle.Cabal: cabalBuildDir :: FilePath -> IO FilePath
+ HIE.Bios.Cradle.Cabal: cabalBuildDir :: CacheDir -> FilePath -> IO FilePath
- HIE.Bios.Cradle.Cabal: withGhcWrapperTool :: LogAction IO (WithSeverity Log) -> GhcProc -> FilePath -> IO FilePath
+ HIE.Bios.Cradle.Cabal: withGhcWrapperTool :: LogAction IO (WithSeverity Log) -> CacheDir -> GhcProc -> FilePath -> IO FilePath
- HIE.Bios.Cradle.Resolved: ResolvedCradles :: FilePath -> [ResolvedCradle a] -> ProgramVersions -> ResolvedCradles a
+ HIE.Bios.Cradle.Resolved: ResolvedCradles :: FilePath -> [ResolvedCradle a] -> ProgramVersions -> CacheDir -> ResolvedCradles a
- HIE.Bios.Environment: getCacheDir :: FilePath -> IO FilePath
+ HIE.Bios.Environment: getCacheDir :: FilePath -> IO CacheDir
- HIE.Bios.Environment: initSession' :: GhcMonad m => Bool -> ComponentOptions -> m [Target]
+ HIE.Bios.Environment: initSession' :: GhcMonad m => Maybe CacheDir -> ComponentOptions -> m [Target]
- HIE.Bios.Ghc.Api: initSessionWithMessage' :: GhcMonad m => Bool -> Maybe Messager -> ComponentOptions -> (m SuccessFlag, ComponentOptions)
+ HIE.Bios.Ghc.Api: initSessionWithMessage' :: GhcMonad m => Maybe CacheDir -> Maybe Messager -> ComponentOptions -> (m SuccessFlag, ComponentOptions)

Files

ChangeLog.md view
@@ -1,5 +1,16 @@ # ChangeLog hie-bios +## 2026-07-30 - 0.21.0++* Add a CI timeout on tests ([#521](https://github.com/haskell/hie-bios/pull/521)).+* Consistently pass '--keep-temp-files' ([#519](https://github.com/haskell/hie-bios/pull/519)).+* Make cacheFile thread-safe using file locks ([#520](https://github.com/haskell/hie-bios/pull/520)).+* Make functions take the used cache directory explicitly ([#520](https://github.com/haskell/hie-bios/pull/520)).+  * Add *WithConfig functions.+  * Adjust existing functions to take a cache directory.+  * Replace thread-unsafety workaround of initSession' and+    initSessionWithMessage' with the cache directory argument.+ ## 2026-06-30 - 0.20.0  * Hide LoadMode in TestM ([#516](https://github.com/haskell/hie-bios/pull/516))
hie-bios.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: hie-bios-version: 0.20.0+version: 0.21.0 author: Matthew Pickering <matthewtpickering@gmail.com>, Hannes Siebenhandl <fendor.haskell@gmail.com> maintainer: Hannes Siebenhandl <fendor.haskell@gmail.com> license: BSD-3-Clause@@ -10,10 +10,24 @@ description: Set up a GHC API session and obtain flags required to compile a source file category: Development build-type: Simple-extra-doc-files: ChangeLog.md-extra-source-files:+extra-doc-files:+  ChangeLog.md   README.md+extra-source-files:   tests/configs/*.yaml+  tests/projects/cabal-with-custom-setup-old/a-with-custom/a-with-custom.cabal+  tests/projects/cabal-with-custom-setup-old/a-with-custom/Setup.hs+  tests/projects/cabal-with-custom-setup-old/a-with-custom/src/MyLib.hs+  tests/projects/cabal-with-custom-setup-old/b/b.cabal+  tests/projects/cabal-with-custom-setup-old/b/src/B.hs+  tests/projects/cabal-with-custom-setup-old/b/tests/Main.hs+  tests/projects/cabal-with-custom-setup-old/cabal.project+  tests/projects/cabal-with-custom-setup/a-with-custom/a-with-custom.cabal+  tests/projects/cabal-with-custom-setup/a-with-custom/Setup.hs+  tests/projects/cabal-with-custom-setup/a-with-custom/src/MyLib.hs+  tests/projects/cabal-with-custom-setup/b/b.cabal+  tests/projects/cabal-with-custom-setup/b/src/B.hs+  tests/projects/cabal-with-custom-setup/cabal.project   tests/projects/cabal-with-ghc-and-project/cabal-with-ghc.cabal   tests/projects/cabal-with-ghc-and-project/cabal.project.extra   tests/projects/cabal-with-ghc-and-project/hie.yaml@@ -191,6 +205,7 @@     exceptions ^>=0.10,     extra >=1.6.14 && <1.9,     file-embed >=0.0.11 && <1,+    filelock >=0.1.1 && <0.2,     filepath >=1.4.1 && <1.6,     ghc >=9.2.1 && <10.2,     prettyprinter ^>=1.6 || ^>=1.7.0,@@ -242,6 +257,7 @@   type: exitcode-stdio-1.0   default-language: Haskell2010   build-depends:+    async,     base,     co-log-core,     directory,@@ -250,6 +266,7 @@     ghc,     hie-bios,     prettyprinter,+    process,     tasty,     tasty-expected-failure,     tasty-hunit,
src/HIE/Bios.hs view
@@ -14,7 +14,11 @@   , CradleError(..)   , findCradle   , loadCradle+  , loadCradleWithConfig   , loadImplicitCradle+  , loadImplicitCradleWithConfig+  , CradleRunConfig(..)+  , defaultCradleRunConfig   , defaultCradle   -- * Compiler Options   , ComponentOptions(..)
src/HIE/Bios/Cradle.hs view
@@ -7,7 +7,11 @@ module HIE.Bios.Cradle (       findCradle     , loadCradle+    , loadCradleWithConfig     , loadImplicitCradle+    , loadImplicitCradleWithConfig+    , CradleRunConfig(..)+    , defaultCradleRunConfig     , yamlConfig     , defaultCradle     , isCabalCradle@@ -19,6 +23,7 @@     , isDefaultCradle     , isOtherCradle     , getCradle+    , getCradleWithConfig     , Process.readProcessWithOutputs     , Process.readProcessWithCwd     , makeCradleResult@@ -49,6 +54,7 @@ import System.IO.Temp  import HIE.Bios.Config+import HIE.Bios.Environment (resolveCacheDir) import HIE.Bios.Types hiding (ActionName(..)) import qualified HIE.Bios.Process as Process import qualified HIE.Bios.Types as Types@@ -77,32 +83,45 @@  -- | Given root\/hie.yaml load the Cradle. loadCradle :: LogAction IO (WithSeverity Log) -> FilePath -> IO (Cradle Void)-loadCradle l = loadCradleWithOpts l absurd+loadCradle l = loadCradleWithConfig l defaultCradleRunConfig +-- | 'loadCradle' with an explicit 'CradleRunConfig'.+loadCradleWithConfig :: LogAction IO (WithSeverity Log) -> CradleRunConfig -> FilePath -> IO (Cradle Void)+loadCradleWithConfig l config = loadCradleWithOpts l config absurd+ -- | Given root\/foo\/bar.hs, load an implicit cradle loadImplicitCradle :: Show a => LogAction IO (WithSeverity Log) -> FilePath -> IO (Cradle a)-loadImplicitCradle l wfile = do+loadImplicitCradle l = loadImplicitCradleWithConfig l defaultCradleRunConfig++-- | 'loadImplicitCradle' with an explicit 'CradleRunConfig'.+loadImplicitCradleWithConfig :: Show a => LogAction IO (WithSeverity Log) -> CradleRunConfig -> FilePath -> IO (Cradle a)+loadImplicitCradleWithConfig l config wfile = do   let wdir = takeDirectory wfile   cfg <- runMaybeT (implicitConfig wdir)   case cfg of-    Just bc -> getCradle l absurd bc+    Just bc -> getCradleWithConfig l config absurd bc     Nothing -> return $ defaultCradle l wdir  -- | Finding 'Cradle'. --   Find a cabal file by tracing ancestor directories. --   Find a sandbox according to a cabal sandbox config --   in a cabal directory.-loadCradleWithOpts :: (Yaml.FromJSON b, Show a) => LogAction IO (WithSeverity Log) -> (b -> CradleAction a) -> FilePath -> IO (Cradle a)-loadCradleWithOpts l buildCustomCradle wfile = do+loadCradleWithOpts :: (Yaml.FromJSON b, Show a) => LogAction IO (WithSeverity Log) -> CradleRunConfig -> (b -> CradleAction a) -> FilePath -> IO (Cradle a)+loadCradleWithOpts l config buildCustomCradle wfile = do     cradleConfig <- readCradleConfig wfile     l <& WithSeverity (LogAny $ T.pack $ "Cradle Config: " ++ show cradleConfig) Debug-    getCradle l buildCustomCradle (cradleConfig, takeDirectory wfile)+    getCradleWithConfig l config buildCustomCradle (cradleConfig, takeDirectory wfile)  getCradle :: Show a => LogAction IO (WithSeverity Log) ->  (b -> CradleAction a) -> (CradleConfig b, FilePath) -> IO (Cradle a)-getCradle l buildCustomCradle (cc, wdir) = do+getCradle l = getCradleWithConfig l defaultCradleRunConfig++-- | 'getCradle' with an explicit 'CradleRunConfig'.+getCradleWithConfig :: Show a => LogAction IO (WithSeverity Log) -> CradleRunConfig -> (b -> CradleAction a) -> (CradleConfig b, FilePath) -> IO (Cradle a)+getCradleWithConfig l config buildCustomCradle (cc, wdir) = do     rcs <- canonicalizeResolvedCradles wdir cs     liftIO $ l <& WithSeverity (LogAny . T.pack $ "Resolved Cradles " ++ show ((fmap . fmap) (const ()) rcs)) Debug-    resolvedCradlesToCradle l buildCustomCradle wdir rcs+    cacheDir <- resolveCacheDir "" (cradleCacheDir config)+    resolvedCradlesToCradle l buildCustomCradle wdir cacheDir rcs   where     cs = resolveCradleTree wdir cc @@ -114,8 +133,8 @@       (\(ComponentOptions os' dir ds) -> CradleSuccess (ComponentOptions os' dir (ds `union` deps)))  -resolvedCradlesToCradle :: Show a => LogAction IO (WithSeverity Log) -> (b -> CradleAction a) -> FilePath -> [ResolvedCradle b] -> IO (Cradle a)-resolvedCradlesToCradle logger buildCustomCradle root cs = mdo+resolvedCradlesToCradle :: Show a => LogAction IO (WithSeverity Log) -> (b -> CradleAction a) -> FilePath -> CacheDir -> [ResolvedCradle b] -> IO (Cradle a)+resolvedCradlesToCradle logger buildCustomCradle root cacheDir cs = mdo   let run_ghc_cmd args =         -- We're being lazy here and just returning the ghc path for the         -- first non-none cradle. This shouldn't matter in practice: all@@ -127,7 +146,7 @@               act               args   versions <- makeVersions logger root run_ghc_cmd-  let rcs = ResolvedCradles root cs versions+  let rcs = ResolvedCradles root cs versions cacheDir       cradleActions = [ (c, resolveCradleAction logger buildCustomCradle rcs root c) | c <- cs ]       err_msg (TargetWithContext fp fps)         = ["Multi Cradle: No prefixes matched"@@ -563,7 +582,7 @@   logCradleHasNoSupportForLoadFileWithContext l loadStyle "stack"   let ghcProc args = proc "stack" (stackYamlProcessArgs syaml <> ["exec", "ghc", "--"] <> args)   -- Same wrapper works as with cabal-  wrapper_fp <- withGhcWrapperTool l ghcProc workDir+  wrapper_fp <- withGhcWrapperTool l (cradleCacheDirResolved cs) ghcProc workDir   let     fallback = [ comp | Just comp <- [mc] ]     componentsToLoad = nubOrd <$> mconcat
src/HIE/Bios/Cradle/Cabal.hs view
@@ -40,7 +40,6 @@ import Data.Version  import HIE.Bios.Config-import HIE.Bios.Environment (getCacheDir) import HIE.Bios.Types hiding (ActionName(..)) import HIE.Bios.Wrappers import qualified HIE.Bios.Process as Process@@ -158,19 +157,11 @@       pure LoadFile     _ -> pure loadStyle -  (cabalArgs, loadingFiles, extraDeps) <- processCabalLoadMode l cradles projectFile workDir mc fp determinedLoadMode--  cabalFeatures <- determineCabalLoadFeature progVersions-  let-    -- Used for @cabal >= 3.15@ but @lib:Cabal <3.15@, in custom setups.-    mkFallbackCabalProc = cabalLoadFilesBefore315 l progVersions projectFile workDir cabalArgs-  cabalProc <- case cabalFeatures of-    CabalWithRepl -> cabalLoadFilesWithRepl l projectFile workDir cabalArgs-    CabalWithGhcShimWrapper -> cabalLoadFilesBefore315 l progVersions projectFile workDir cabalArgs+  (cabalArgs, loadingFiles, extraDeps) <- processCabalLoadMode l cradles progVersions projectFile workDir mc fp determinedLoadMode -  mResult <- runCabalToGetGhcOptions cabalProc mkFallbackCabalProc+  mResult <- runCabalToGetGhcOptions progVersions cabalArgs   case mResult of-    Left (code, errorDetails) -> do+    Left (cabalProc, code, errorDetails) -> 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.@@ -198,10 +189,9 @@             }         Just (componentDir, ghc_args) -> do           deps <- liftIO $ cabalCradleDependencies projectFile workDir componentDir-          usesResponseFiles <- usesResponseFilesForAllGhcOptions progVersions-          final_args <- case usesResponseFiles of-            True -> liftIO $ expandGhcOptionResponseFile ghc_args-            False -> pure ghc_args+          final_args <- case loadModeUsesResponseFiles cabalArgs of+            UsesResponseFiles -> liftIO $ expandGhcOptionResponseFile ghc_args+            NoResponseFiles -> pure ghc_args           CradleLoadResultT $ pure $ CradleSuccess             ComponentOptions               { componentOptions = final_args@@ -209,19 +199,67 @@               , componentDependencies = nubOrd (deps <> extraDeps)               }   where-    -- | Run the given cabal process to obtain ghc options.-    -- In the special case of 'cabal >= 3.15' but 'lib:Cabal <3.15' (via custom-setups),-    -- we gracefully fall back to the given action to create an alternative cabal process which-    -- we use to find the ghc options.+    -- | Try to obtain the ghc options using cabal.+    --+    -- First, we try what the user requested and what cabal supports.+    -- For example, if the user requested to load multiple components at once, and cabal+    -- is recent enough, we try to use @--with-repl --enable-multi-repl@.+    -- We detect when certain cabal features are not supported. For example, for old+    -- lib:Cabal versions, @--with-repl@ is not supported. In this case, we parse the error message+    -- and fall back to the old `CabalWithGhcShimWrapper` version.+    -- The ghc shim @--with-ghc@ might work with @--keep-temp-files@ if no custom cabal package is part+    -- of the repl session, so we try to load that, and as a last resort, fall back to single component loading.     runCabalToGetGhcOptions ::-      Process.CreateProcess ->-      CradleLoadResultT IO Process.CreateProcess ->+      ProgramVersions ->+      LoadModeTargets ->       CradleLoadResultT IO         (Either-          (Int, ProcessErrorDetails)+          (CreateProcess, Int, ProcessErrorDetails)           ([String], ProcessErrorDetails)         )-    runCabalToGetGhcOptions cabalProc mkFallbackCabalProc = do+    runCabalToGetGhcOptions progVersions cabalArgs = do+      cabalFeatures <- determineCabalLoadFeature progVersions+      let+        cacheDir = cradleCacheDirResolved cradles+        -- The preferred way of loading ghc-options. Requires cabal 3.16+        cabalWithReplProc = cabalLoadFilesWithRepl l cacheDir projectFile workDir cabalArgs+        -- Legacy way of loading ghc-options.+        -- Supports multi-repl and the deprecated single file mode.+        cabalWithGhcShimWrapper = cabalLoadFilesBefore315 l cacheDir progVersions projectFile workDir cabalArgs+        -- Legacy way of loading ghc-options.+        -- This should only be used for project with Custom setups that are older than lib:Cabal 3.16.+        cabalWithGhcShimWrapperSingleTarget = cabalLoadFilesBefore315 l cacheDir progVersions projectFile workDir (demoteToSingleLoadModeTarget cabalArgs)++      cabalProc <- case cabalFeatures of+        CabalWithRepl -> cabalWithReplProc+        CabalWithGhcShimWrapper -> cabalWithGhcShimWrapper++      loadResult <- getCabalGhcOptions cabalProc+      case loadResult of+        -- Some fallback+        Left (_, _, details)+          -- Used for @cabal >= 3.15@ but @lib:Cabal <3.15@, in custom setups.+          | isCabalLibraryInProjectTooOld (processStderr details) -> do+              liftIO $ l <& WithSeverity (LogWithReplNotSupported (processStderr details)) Debug+              shimResult <- getCabalGhcOptions =<< cabalWithGhcShimWrapper+              case shimResult of+                Left (_, _, shimDetails)+                  | checkKeepTempFilesIsNotSupported shimDetails -> do+                    liftIO $ l <& WithSeverity (LogKeepTempFilesNotSupported (processStdout shimDetails <> processStderr shimDetails)) Debug+                    getCabalGhcOptions =<< cabalWithGhcShimWrapperSingleTarget+                _ ->+                  pure shimResult++          | checkKeepTempFilesIsNotSupported details -> do+              liftIO $ l <& WithSeverity (LogKeepTempFilesNotSupported (processStdout details <> processStderr details)) Debug+              getCabalGhcOptions =<< cabalWithGhcShimWrapperSingleTarget++        Left codeAndDetails -> do+          pure $ Left codeAndDetails+        Right argsAndDetails ->+          pure $ Right argsAndDetails++    getCabalGhcOptions cabalProc = do       (ex, output, stde, [(_, maybeArgs)]) <- liftIO $ Process.readProcessWithOutputs [hie_bios_output] l workDir cabalProc       let args = fromMaybe [] maybeArgs       let errorDetails = ProcessErrorDetails@@ -232,51 +270,49 @@             , processHieBiosEnvironment = hieBiosProcessEnv cabalProc             }       case ex of-        ExitFailure{} | isCabalLibraryInProjectTooOld stde -> do-          liftIO $ l <& WithSeverity (LogCabalLibraryTooOld stde) Debug-          fallbackCabalProc <- mkFallbackCabalProc-          runCabalToGetGhcOptions fallbackCabalProc mkFallbackCabalProc         ExitFailure code -> do--          pure $ Left (code, errorDetails)+          pure $ Left (cabalProc, code, errorDetails)         ExitSuccess ->           pure $ Right (args, errorDetails) +    checkKeepTempFilesIsNotSupported details =+      -- See https://github.com/haskell/cabal/issues/12178 why we check stdout and stderr.+      -- This is a forward compatible check to make sure this won't break accidentally in the future once+      -- this ticket is fixed.+      isKeepTempFilesNotSupported (processStdout details) || isKeepTempFilesNotSupported (processStderr details)  runCabalGhcCmd :: ResolvedCradles a -> FilePath -> LogAction IO (WithSeverity Log) -> CradleProjectConfig -> [String] -> IO (CradleLoadResult String) runCabalGhcCmd cs wdir l projectFile args = runCradleResultT $ do   let vs = cradleProgramVersions cs-  callCabalPathForCompilerPath l vs wdir projectFile >>= \case+  callCabalPathForCompilerPath l (cradleCacheDirResolved cs) vs wdir projectFile >>= \case     Just p -> Process.readProcessWithCwd_ l wdir p args ""     Nothing -> do-      buildDir <- liftIO $ cabalBuildDir wdir+      buildDir <- liftIO $ cabalBuildDir (cradleCacheDirResolved cs) 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)       liftIO $ createDirectoryIfMissing True (buildDir </> "tmp")       -- Need to pass -v0 otherwise we get "resolving dependencies..."-      cabalProc <- cabalExecGhc l vs projectFile wdir args+      cabalProc <- cabalExecGhc l (cradleCacheDirResolved cs) vs projectFile wdir args       Process.readProcessWithCwd' l cabalProc "" -data LoadUnits = Inferred | FromCradle-  deriving Eq--processCabalLoadMode :: MonadIO m => LogAction IO (WithSeverity Log) -> ResolvedCradles a -> CradleProjectConfig -> [Char] -> Maybe FilePath -> TargetWithContext -> LoadMode -> m ([FilePath], [FilePath], [FilePath])-processCabalLoadMode l cradles projectFile workDir mc fpc loadStyle = do+processCabalLoadMode :: MonadIO m => LogAction IO (WithSeverity Log) -> ResolvedCradles a -> ProgramVersions -> CradleProjectConfig -> [Char] -> Maybe FilePath -> TargetWithContext -> LoadMode -> m (LoadModeTargets, [FilePath], [FilePath])+processCabalLoadMode l cradles progVersions projectFile workDir mc fpc loadStyle = do+  usesResponseFiles <- usesResponseFilesForAllGhcOptions progVersions   (cabalArgs, loadingFiles, extraDeps) <- case loadStyle of-        LoadFile -> pure ([fpModule], [fp], [])+        LoadFile -> pure (SingleTarget fpModule usesResponseFiles, [fp], [])         LoadFileWithContext  -> do           let fps = targetContext fpc           (modPairs, mergedDeps) <- moduleFilesFromSameProject fps-          let allModPairs = nubOrd $ (fpModule, fp) : modPairs-              allModules  = nubOrd $ fmap fst allModPairs-              allFiles    = nubOrd $ fmap snd allModPairs-          pure (["--enable-multi-repl"] ++ allModules, allFiles, mergedDeps)-        LoadUnitsInferred    -> loadUnits Inferred-        LoadUnitsFromCradle  -> loadUnits FromCradle+          let+            extraModules =      fmap fst modPairs+            allFiles     = fp : fmap snd modPairs+          pure (MultipleTargets fpModule extraModules NoExtraComponents usesResponseFiles, allFiles, mergedDeps)+        LoadUnitsInferred    -> loadUnits usesResponseFiles Inferred+        LoadUnitsFromCradle  -> loadUnits usesResponseFiles FromCradle    liftIO $ l <& LogComputedCradleLoadMode "cabal" fpc loadStyle `WithSeverity` Info   liftIO $ l <& LogCabalLoad fp mc (prefix <$> resolvedCradles cradles) loadingFiles `WithSeverity` Debug-  pure (cabalArgs, loadingFiles, extraDeps)+  pure (cabalArgs, nubOrd loadingFiles, extraDeps)   where     fpModule = fromMaybe (fixTargetPath fp) mc     fp = targetFilePath fpc@@ -320,7 +356,7 @@       let mergedDeps = nubOrd $ dynDeps ++ concat [ depsYaml | (_, _, depsYaml) <- selected ]       pure ([comp | (comp,_,_) <- selected], mergedDeps) -    loadUnits whichUnits0 = do+    loadUnits usesResponseFiles whichUnits0 = do       let         componentsToLoad = do           guard (whichUnits0 == FromCradle)@@ -343,32 +379,35 @@        let fps = targetContext fpc       (modPairs, mergedDeps0) <- moduleFilesFromSameProject fps-      let allModPairs = nubOrd $ (fpModule, fp) : modPairs-          allModules  = nubOrd $ fmap fst allModPairs-          allFiles    = nubOrd $ fmap snd allModPairs+      let+        extraModules = nubOrd $      fmap fst modPairs+        allFiles     = nubOrd $ fp : fmap snd modPairs       let mergedDeps = mergedDeps0 ++ compDeps-      let compArgs = case whichUnits of-            (Inferred,_) -> enableFlags ++ ["all"]-              where-                enableFlags = case projectFile of-                  NoExplicitConfig-                    -> ["--enable-tests","--enable-benchmarks"]-                  _ -> []-            (FromCradle,units) -> units-      pure (["--enable-multi-repl"] ++ compArgs ++ allModules, allFiles, mergedDeps)+      let+        extraComponents = case fst whichUnits of+            Inferred -> case projectFile of+              NoExplicitConfig -> LoadTestsAndBenchmarks+              ExplicitConfig{} -> NoExtraComponents+            FromCradle -> NoExtraComponents -cabalLoadFilesWithRepl :: LogAction IO (WithSeverity Log) -> CradleProjectConfig -> FilePath -> [String] -> CradleLoadResultT IO CreateProcess-cabalLoadFilesWithRepl l projectFile workDir args = do-  buildDir <- liftIO $ cabalBuildDir workDir+        withExtraTargets targetFiles = case whichUnits of+          (Inferred, _) -> "all" : targetFiles+          (FromCradle, units) -> units ++ targetFiles++      pure (MultipleTargets fpModule (withExtraTargets extraModules) extraComponents usesResponseFiles, fp:allFiles, mergedDeps)++cabalLoadFilesWithRepl :: LogAction IO (WithSeverity Log) -> CacheDir -> CradleProjectConfig -> FilePath -> LoadModeTargets -> CradleLoadResultT IO CreateProcess+cabalLoadFilesWithRepl l cacheDir projectFile workDir args = do+  buildDir <- liftIO $ cabalBuildDir cacheDir workDir   newEnvironment <- liftIO Process.getCleanEnvironment-  wrapper_fp <- liftIO $ withReplWrapperTool l (proc "ghc") workDir+  wrapper_fp <- liftIO $ withReplWrapperTool l cacheDir (proc "ghc") workDir   let     cabalCommand = "v2-repl"     cabalArgs =       -- Don't clobber the user's 'dist-newstyle': pass --builddir (#501)         [ "--builddir=" <> buildDir-        , cabalCommand, "--keep-temp-files", "--with-repl", wrapper_fp-        ] <> projectFileProcessArgs projectFile <> args+        , cabalCommand, "--with-repl", wrapper_fp+        ] <> projectFileProcessArgs projectFile <> renderLoadModeTargets CabalWithRepl args   pure $     (proc "cabal" cabalArgs)       { env = Just newEnvironment@@ -447,15 +486,10 @@ -- them. -- ---------------------------------------------------------------------------- -cabalLoadFilesBefore315 :: LogAction IO (WithSeverity Log) -> ProgramVersions -> CradleProjectConfig -> [Char] -> [String] -> CradleLoadResultT IO CreateProcess-cabalLoadFilesBefore315 l progVersions projectFile workDir args' = do+cabalLoadFilesBefore315 :: LogAction IO (WithSeverity Log) -> CacheDir -> ProgramVersions -> CradleProjectConfig -> [Char] -> LoadModeTargets -> CradleLoadResultT IO CreateProcess+cabalLoadFilesBefore315 l cacheDir progVersions projectFile workDir args = do   let cabalCommand = "v2-repl"-  cabal_version <- liftIO $ runCachedIO $ cabalVersion progVersions--  let args = case cabal_version of-        Just v | v < makeVersion [3,15] -> "--keep-temp-files" : args'-        _ -> args'-  cabalProcess l progVersions projectFile workDir cabalCommand args `modCradleError` \err -> do+  cabalProcess l cacheDir progVersions projectFile workDir cabalCommand (renderLoadModeTargets CabalWithGhcShimWrapper args) `modCradleError` \err -> do     deps <- cabalCradleDependencies projectFile workDir workDir     pure $ err {cradleErrorDependencies = cradleErrorDependencies err ++ deps} @@ -468,15 +502,15 @@ -- 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 :: LogAction IO (WithSeverity Log) -> ProgramVersions -> CradleProjectConfig -> FilePath -> String -> [String] -> CradleLoadResultT IO CreateProcess-cabalProcess l vs cabalProject workDir command args = do-  ghcDirs@(ghcBin, libdir) <- callCabalPathForCompilerPath l vs workDir cabalProject >>= \case+cabalProcess :: LogAction IO (WithSeverity Log) -> CacheDir -> ProgramVersions -> CradleProjectConfig -> FilePath -> String -> [String] -> CradleLoadResultT IO CreateProcess+cabalProcess l cacheDir vs cabalProject workDir command args = do+  ghcDirs@(ghcBin, libdir) <- callCabalPathForCompilerPath l cacheDir vs workDir cabalProject >>= \case     Just p -> do       libdir <- Process.readProcessWithCwd_ l workDir p ["--print-libdir"] ""       pure (p, trimEnd libdir)     Nothing -> cabalGhcDirs l cabalProject workDir -  ghcPkgPath <- liftIO $ withGhcPkgTool ghcBin libdir+  ghcPkgPath <- liftIO $ withGhcPkgTool cacheDir ghcBin libdir   newEnvironment <- liftIO $ setupEnvironment ghcDirs   cabalProc <- liftIO $ setupCabalCommand ghcPkgPath   pure $ (cabalProc@@ -495,8 +529,8 @@      setupCabalCommand :: FilePath -> IO CreateProcess     setupCabalCommand ghcPkgPath = do-      wrapper_fp <- withGhcWrapperTool l (proc "ghc") workDir-      buildDir <- cabalBuildDir workDir+      wrapper_fp <- withGhcWrapperTool l cacheDir (proc "ghc") workDir+      buildDir <- cabalBuildDir cacheDir workDir       let extraCabalArgs =             [ "--builddir=" <> buildDir             , command@@ -555,8 +589,8 @@ -- -- 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+withGhcPkgTool :: CacheDir -> FilePath -> FilePath -> IO FilePath+withGhcPkgTool cacheDir ghcPathAbs libdir = do   let ghcName = takeFileName ghcPathAbs       -- TODO: check for existence       ghcPkgPath = guessGhcPkgFromGhc ghcName@@ -592,7 +626,7 @@                       ]             ]           srcHash = show (fingerprintString contents)-      cacheFile "ghc-pkg" srcHash $ \wrapperFp -> writeFile wrapperFp contents+      cacheFileIn cacheDir "ghc-pkg" srcHash $ \wrapperFp -> writeFile wrapperFp contents      -- Escape the filepath and trim excess newlines added by 'escapeArgs'     escapeFilePath fp = trimEnd $ escapeArgs [fp]@@ -607,9 +641,9 @@ -- | 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-withGhcWrapperTool :: LogAction IO (WithSeverity Log) -> GhcProc -> FilePath -> IO FilePath-withGhcWrapperTool l mkGhcCall wdir = do-  withWrapperTool l mkGhcCall wdir "wrapper" cabalWrapperHs cabalWrapper+withGhcWrapperTool :: LogAction IO (WithSeverity Log) -> CacheDir -> GhcProc -> FilePath -> IO FilePath+withGhcWrapperTool l cacheDir mkGhcCall wdir = do+  withWrapperTool l cacheDir mkGhcCall wdir "wrapper" cabalWrapperHs cabalWrapper  -- | Generate a script/binary that can be passed to cabal's '--with-repl'. -- On windows, this compiles a Haskell file, while on other systems, we persist@@ -617,16 +651,16 @@ -- -- 'GhcProc' is unused on other platforms. ---withReplWrapperTool :: LogAction IO (WithSeverity Log) -> GhcProc -> FilePath -> IO FilePath-withReplWrapperTool l mkGhcCall wdir =-  withWrapperTool l mkGhcCall wdir "repl-wrapper" cabalWithReplWrapperHs cabalWithReplWrapper+withReplWrapperTool :: LogAction IO (WithSeverity Log) -> CacheDir -> GhcProc -> FilePath -> IO FilePath+withReplWrapperTool l cacheDir mkGhcCall wdir =+  withWrapperTool l cacheDir mkGhcCall wdir "repl-wrapper" cabalWithReplWrapperHs cabalWithReplWrapper -withWrapperTool :: LogAction IO (WithSeverity Log) -> GhcProc -> String -> FilePath -> String -> String -> IO FilePath-withWrapperTool l mkGhcCall wdir baseName windowsWrapper unixWrapper = do+withWrapperTool :: LogAction IO (WithSeverity Log) -> CacheDir -> GhcProc -> String -> FilePath -> String -> String -> IO FilePath+withWrapperTool l cacheDir mkGhcCall wdir baseName windowsWrapper unixWrapper = do   let wrapperContents = if isWindows then windowsWrapper else unixWrapper       withExtension fp = if isWindows then fp <.> "exe" else fp       srcHash = show (fingerprintString wrapperContents)-  cacheFile (withExtension baseName) srcHash $ \wrapper_fp ->+  cacheFileIn cacheDir (withExtension baseName) srcHash $ \wrapper_fp ->     if isWindows     then       withSystemTempDirectory "hie-bios" $ \ tmpDir -> do@@ -657,13 +691,13 @@ -- cabal locations -- ---------------------------------------------------------------------------- --- | Given the root directory, get the build dir we are using for cabal--- In the `hie-bios` cache directory-cabalBuildDir :: FilePath -> IO FilePath-cabalBuildDir workDir = do+-- | Given the cache root and the work directory, get the build dir we are+-- using for cabal.+cabalBuildDir :: CacheDir -> FilePath -> IO FilePath+cabalBuildDir (CacheDir cacheDir) workDir = do   abs_work_dir <- makeAbsolute workDir   let dirHash = show (fingerprintString abs_work_dir)-  getCacheDir ("dist-" <> filter (not . isSpace) (takeBaseName abs_work_dir)<>"-"<>dirHash)+  pure $ cacheDir </> ("dist-" <> filter (not . isSpace) (takeBaseName abs_work_dir)<>"-"<>dirHash)  -- |Find .cabal files in the given directory. --@@ -678,16 +712,16 @@ -- cabal process wrappers and helpers -- ---------------------------------------------------------------------------- -cabalExecGhc :: LogAction IO (WithSeverity Log) -> ProgramVersions -> CradleProjectConfig -> FilePath -> [String] -> CradleLoadResultT IO CreateProcess-cabalExecGhc l vs projectFile wdir args = do-  cabalProcess l vs projectFile wdir "v2-exec" $ ["ghc", "-v0", "--"] ++ args+cabalExecGhc :: LogAction IO (WithSeverity Log) -> CacheDir -> ProgramVersions -> CradleProjectConfig -> FilePath -> [String] -> CradleLoadResultT IO CreateProcess+cabalExecGhc l cacheDir vs projectFile wdir args = do+  cabalProcess l cacheDir vs projectFile wdir "v2-exec" $ ["ghc", "-v0", "--"] ++ args -callCabalPathForCompilerPath :: LogAction IO (WithSeverity Log) -> ProgramVersions -> FilePath -> CradleProjectConfig -> CradleLoadResultT IO (Maybe FilePath)-callCabalPathForCompilerPath l vs workDir projectFile = do+callCabalPathForCompilerPath :: LogAction IO (WithSeverity Log) -> CacheDir -> ProgramVersions -> FilePath -> CradleProjectConfig -> CradleLoadResultT IO (Maybe FilePath)+callCabalPathForCompilerPath l cacheDir vs workDir projectFile = do   isCabalPathSupported vs >>= \case     False -> pure Nothing     True -> do-      buildDir <- liftIO $ cabalBuildDir workDir+      buildDir <- liftIO $ cabalBuildDir cacheDir workDir       let         args = [ "--builddir=" <> buildDir, "path", "--output-format=json" ]             <> projectFileProcessArgs projectFile@@ -705,10 +739,46 @@ -- Version and cabal capability checks -- ---------------------------------------------------------------------------- +data LoadUnits = Inferred | FromCradle+  deriving Eq++data WithTestsAndBenchmarks+  = NoExtraComponents+  | LoadTestsAndBenchmarks++data LoadModeTargets+  = SingleTarget+      String+      -- ^ Main Target FilePath or component+      UsesResponseFiles+      -- ^ Is this going to use top-level response files?+  | MultipleTargets+      String+      -- ^ Main Target FilePath or component+      [String]+      -- ^ Extra targets to load alongside the main component+      WithTestsAndBenchmarks+      -- ^ Should we enable tests and benchmarks as well?+      UsesResponseFiles+      -- ^ Is this going to use top-level response files?++loadModeUsesResponseFiles :: LoadModeTargets -> UsesResponseFiles+loadModeUsesResponseFiles = \ case+  SingleTarget _ usesResponseFiles -> usesResponseFiles+  MultipleTargets _ _ _ usesResponseFiles -> usesResponseFiles++demoteToSingleLoadModeTarget :: LoadModeTargets -> LoadModeTargets+demoteToSingleLoadModeTarget (MultipleTargets mainTarget _ _ _) = SingleTarget mainTarget NoResponseFiles+demoteToSingleLoadModeTarget (SingleTarget mainTarget _) = SingleTarget mainTarget NoResponseFiles+ data CabalLoadFeature   = CabalWithRepl   | CabalWithGhcShimWrapper +data UsesResponseFiles+  = UsesResponseFiles+  | NoResponseFiles+ determineCabalLoadFeature :: MonadIO m => ProgramVersions -> m CabalLoadFeature determineCabalLoadFeature vs = do   cabal_version <- liftIO $ runCachedIO $ cabalVersion vs@@ -738,17 +808,21 @@ -- Then, later on in `cabal-3.17`, we use response files again. -- -- 'usesResponseFilesForAllGhcOptions' encodes all of this history.-usesResponseFilesForAllGhcOptions :: MonadIO m => ProgramVersions -> m Bool+usesResponseFilesForAllGhcOptions :: MonadIO m => ProgramVersions -> m UsesResponseFiles usesResponseFilesForAllGhcOptions vs = do   cabal_version <- liftIO $ runCachedIO $ cabalVersion vs   -- determine which load style is supported by this cabal cradle.   case cabal_version of     Just ver-      | ver >= makeVersion [3, 15] && ver <= makeVersion [3, 16, 0, 0] -> pure True-      | ver >= makeVersion [3, 17] -> pure True-      | otherwise -> pure False-    _ -> pure False+      | ver >= makeVersion [3, 15] && ver <= makeVersion [3, 16, 0, 0] -> pure UsesResponseFiles+      | ver >= makeVersion [3, 17] -> pure UsesResponseFiles+      | otherwise -> pure NoResponseFiles+    _ -> pure NoResponseFiles +renderResponseFileArgs :: UsesResponseFiles -> [[Char]]+renderResponseFileArgs = \ case+  UsesResponseFiles -> ["--keep-temp-files"]+  NoResponseFiles -> []  -- | When @cabal repl --with-repl@ is called in a project with a custom setup which forces -- an older @lib:Cabal@ version, then the error message looks roughly like:@@ -766,8 +840,23 @@ -- by using a @lib:Cabal@ version that doesn't support the @--with-repl@ flag. isCabalLibraryInProjectTooOld :: [String] -> Bool isCabalLibraryInProjectTooOld stderr =-  "constraint from --with-repl requires >=3.15" `isInfixOf` unlines stderr+  any ("constraint from --with-repl requires >=3.15" `isInfixOf`) stderr +-- | Some @cabal@ versions don't support @--keep-temp-files@+--+-- @+--  Configuring a-with-custom-0.1.0.0...+--  unrecognized 'repl' option `--keep-temp-files'+-- @+--+-- Can also occur with 'configure'.+--+-- We do a quick and dirty string comparison to check whether the error message looks like it has been caused+-- by using a @--keep-temp-files@ version that doesn't support the @--keep-temp-files@ flag.+isKeepTempFilesNotSupported :: [String] -> Bool+isKeepTempFilesNotSupported stderr =+  any (\ line -> all (`isInfixOf` line) ["unrecognized", "option `--keep-temp-files'"]) stderr+ isCabalPathSupported :: MonadIO m => ProgramVersions -> m Bool isCabalPathSupported vs = do   v <- liftIO $ runCachedIO $ cabalVersion vs@@ -781,3 +870,24 @@   case (cabal_version, ghc_version) of     (Just cabal, Just ghc) -> pure $ ghc >= makeVersion [9, 4] && cabal >= makeVersion [3, 11]     _ -> pure False++renderWithTestsAndBenchmarks :: WithTestsAndBenchmarks -> [String]+renderWithTestsAndBenchmarks = \ case+  NoExtraComponents -> []+  LoadTestsAndBenchmarks -> ["--enable-tests", "--enable-benchmarks"]++renderLoadModeTargets :: CabalLoadFeature -> LoadModeTargets -> [String]+renderLoadModeTargets CabalWithGhcShimWrapper = \ case+  SingleTarget fp usesResponseFiles ->+    -- If cabal version is recent enough, we even need to pass '--keep-temp-files'+    -- when loading a single target+    fp : renderResponseFileArgs usesResponseFiles+  MultipleTargets mainTarget targets withTestsAndBenchmarks _usesResponseFiles ->+    "--enable-multi-repl" : "--keep-temp-files" : renderWithTestsAndBenchmarks withTestsAndBenchmarks ++ nubOrd (mainTarget:targets)+renderLoadModeTargets CabalWithRepl = \ case+  SingleTarget fp _usesResponseFiles ->+    -- We need to pass `--keep-temp-files` here as well as `with-repl` will invoke the script with a response file.+    ["--keep-temp-files", fp]+  MultipleTargets mainTarget targets withTestsAndBenchmarks _usesResponseFiles ->+    "--enable-multi-repl" : "--keep-temp-files" : renderWithTestsAndBenchmarks withTestsAndBenchmarks ++ nubOrd (mainTarget:targets)+
src/HIE/Bios/Cradle/Resolved.hs view
@@ -7,6 +7,7 @@  import HIE.Bios.Cradle.ProgramVersions import HIE.Bios.Config+import HIE.Bios.Types  -- | The final cradle config that specifies the cradle for -- each prefix we know how to handle@@ -14,6 +15,7 @@  { cradleRoot :: FilePath  , resolvedCradles :: [ResolvedCradle a] -- ^ In order of decreasing specificity  , cradleProgramVersions :: ProgramVersions+ , cradleCacheDirResolved :: CacheDir  }  -- | 'ConcreteCradle' augmented with information on which file the
src/HIE/Bios/Environment.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE RecordWildCards, CPP #-} {-# LANGUAGE TupleSections #-}-module HIE.Bios.Environment (initSession, initSession', getRuntimeGhcLibDir, getRuntimeGhcVersion, makeDynFlagsAbsolute, makeTargetsAbsolute, getCacheDir, addCmdOpts, extractUnits) where+module HIE.Bios.Environment (initSession, initSession', getRuntimeGhcLibDir, getRuntimeGhcVersion, makeDynFlagsAbsolute, makeTargetsAbsolute, getCacheDir, resolveCacheDir, addCmdOpts, extractUnits) where  import GHC (GhcMonad) import qualified GHC as G@@ -37,21 +37,20 @@ initSession :: (GhcMonad m)     => ComponentOptions     -> m [G.Target]-initSession = initSession' False+initSession = initSession' Nothing +-- | 'initSession' with caches placed under the given root instead of resolved+-- from the environment. initSession' :: (GhcMonad m)-    => Bool+    => Maybe CacheDir     -> ComponentOptions     -> m [G.Target]-initSession' workAroundThreadUnsafety ComponentOptions {..} = do+initSession' mCacheRoot ComponentOptions {..} = do     -- Create a unique folder per set of different GHC options, assuming that each different set of     -- GHC options will create incompatible interface files.-    let-      -- There seems to be a race condition when writing interface files-      hash_args = (if workAroundThreadUnsafety then (componentRoot :) else id) componentOptions-      opts_hash = B.unpack $ encode $ H.finalize $ H.updates H.init $ map B.pack hash_args+    let opts_hash = B.unpack $ encode $ H.finalize $ H.updates H.init $ map B.pack componentOptions -    cache_dir <- liftIO $ makeAbsolute =<< getCacheDir opts_hash+    cache_dir <- liftIO $ resolveCacheDir opts_hash mCacheRoot      -- Plan:     -- - Extract `-unit @resp_file` options if present@@ -167,13 +166,22 @@ -- | Prepends the cache directory used by the library to the supplied file path. -- It tries to use the path under the environment variable `$HIE_BIOS_CACHE_DIR` -- and falls back to the standard `$XDG_CACHE_HOME/hie-bios` if the former is not set-getCacheDir :: FilePath -> IO FilePath+getCacheDir :: FilePath -> IO CacheDir getCacheDir fp = do   mbEnvCacheDirectory <- lookupEnv "HIE_BIOS_CACHE_DIR"   cacheBaseDir <- maybe (getXdgDirectory XdgCache cacheDir) return                          mbEnvCacheDirectory-  return (cacheBaseDir </> fp)+  return (CacheDir (cacheBaseDir </> fp)) +-- | Resolve a cache directory root, appending @suffix@ and making it absolute.+-- An explicit path is used as given. 'Nothing' falls back to 'getCacheDir'.+resolveCacheDir :: FilePath -> Maybe CacheDir -> IO CacheDir+resolveCacheDir suffix mCacheRoot = do+  resolved <- case mCacheRoot of+    Nothing -> fmap unCacheDir (getCacheDir suffix)+    Just cacheRoot -> pure ((</> suffix) (unCacheDir cacheRoot))+  fmap CacheDir (makeAbsolute resolved)+ ----------------------------------------------------------------  -- we don't want to generate object code so we compile to bytecode@@ -191,9 +199,9 @@ setVerbosity :: Int -> G.DynFlags -> G.DynFlags setVerbosity n df = df { G.verbosity = n } -writeInterfaceFiles :: Maybe FilePath -> G.DynFlags -> G.DynFlags+writeInterfaceFiles :: Maybe CacheDir -> G.DynFlags -> G.DynFlags writeInterfaceFiles Nothing df = df-writeInterfaceFiles (Just hi_dir) df = setHiDir hi_dir (Gap.gopt_set df G.Opt_WriteInterface)+writeInterfaceFiles (Just (CacheDir hi_dir)) df = setHiDir hi_dir (Gap.gopt_set df G.Opt_WriteInterface)  setHiDir :: FilePath -> G.DynFlags -> G.DynFlags setHiDir f d = d { G.hiDir      = Just f}
src/HIE/Bios/Ghc/Api.hs view
@@ -49,15 +49,17 @@             => Maybe G.Messager             -> ComponentOptions             -> (m G.SuccessFlag, ComponentOptions)-initSessionWithMessage = initSessionWithMessage' False+initSessionWithMessage = initSessionWithMessage' Nothing +-- | 'initSessionWithMessage' with the interface-file cache placed under the+-- given root, see 'initSession''. initSessionWithMessage' :: (GhcMonad m)-            => Bool+            => Maybe CacheDir             -> Maybe G.Messager             -> ComponentOptions             -> (m G.SuccessFlag, ComponentOptions)-initSessionWithMessage' workAroundThreadUnsafety msg compOpts = (do-    targets <- initSession' workAroundThreadUnsafety compOpts+initSessionWithMessage' mCacheRoot msg compOpts = (do+    targets <- initSession' mCacheRoot compOpts     G.setTargets targets     -- Get the module graph using the function `getModuleGraph`     mod_graph <- G.depanal [] True
src/HIE/Bios/Process.hs view
@@ -11,6 +11,7 @@   , getCleanEnvironment   -- * File Caching   , cacheFile+  , cacheFileIn   -- * Find file utilities   , findFileUpwards   , findFileUpwardsPredicate@@ -36,6 +37,7 @@ import Data.Maybe (fromMaybe) import qualified Data.Text as T import System.Environment+import System.FileLock import System.FilePath import System.IO (hClose, hGetContents, hSetBuffering, BufferMode(LineBuffering), withFile, IOMode(..)) import System.IO.Error (isPermissionError)@@ -156,14 +158,24 @@ cacheFile :: FilePath -> String -> (FilePath -> IO ()) -> IO FilePath cacheFile fpName srcHash populate = do   cacheDir <- getCacheDir ""+  cacheFileIn cacheDir fpName srcHash populate++-- | 'cacheFile' with the cache directory given explicitly instead of resolved+-- from the environment.+cacheFileIn :: CacheDir -> FilePath -> String -> (FilePath -> IO ()) -> IO FilePath+cacheFileIn (CacheDir cacheDir) fpName srcHash populate = do   createDirectoryIfMissing True cacheDir   let newFpName = cacheDir </> (dropExtensions fpName <> "-" <> srcHash) <.> takeExtensions fpName-  unlessM (doesFileExist newFpName) $ do-    populate newFpName-    setMode newFpName+  -- Concurrent loads race to create the same cache entry, so serialize them on+  -- a lock. Populate into a temp file and rename to keep it atomic as well.+  withFileLock (newFpName <.> "lock") Exclusive $ \_ ->+    unlessM (doesFileExist newFpName) $+      withTempFile cacheDir (takeFileName newFpName) $ \tmpFile tmpHandle -> do+        hClose tmpHandle+        populate tmpFile+        setFileMode tmpFile accessModes+        renamePath tmpFile newFpName   pure newFpName-  where-    setMode wrapper_fp = setFileMode wrapper_fp accessModes  ------------------------------------------------------------------------------ -- Utilities@@ -219,4 +231,3 @@ removeFileIfExists f = do   yes <- doesFileExist f   when yes (removeFile f)-
src/HIE/Bios/Types.hs view
@@ -3,6 +3,8 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE DeriveTraversable #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} module HIE.Bios.Types where  import           System.Exit@@ -14,6 +16,7 @@ import qualified Control.Monad.Fail as Fail #endif import           Data.Maybe (fromMaybe)+import           Data.String import qualified Data.Text as T import           Prettyprinter import           System.Process.Extra (CreateProcess (env, cmdspec), CmdSpec (..), showCommandForUser)@@ -106,7 +109,8 @@   | LogComputedCradleLoadMode !T.Text !TargetWithContext !LoadMode   | LogLoadModeUnsupported !T.Text LoadMode !(Maybe T.Text)   | LogCabalLoad !FilePath !(Maybe String) ![FilePath] ![FilePath]-  | LogCabalLibraryTooOld [String]+  | LogWithReplNotSupported [String]+  | LogKeepTempFilesNotSupported [String]   | LogCabalPath !T.Text   deriving (Show) @@ -160,9 +164,12 @@           <> line <> indent 4 "from project: " <+> pretty projectFile           <> line <> indent 4 "with prefixes:" <+> pretty prefixes           <> line <> indent 4 "with actual loading files:" <+> pretty crs-  pretty (LogCabalLibraryTooOld err) =-    "'lib:Cabal' is too old to use '--with-repl' flag. Requires 'lib:Cabal' >= 3.15. Original error:" <> line+  pretty (LogWithReplNotSupported err) =+    "'lib:Cabal' is too old to use '--with-repl' flag. Requires 'lib:Cabal' >= 3.15. Likely caused by `build-type: Custom`. Original error:" <> line       <> vcat (fmap pretty err)+  pretty (LogKeepTempFilesNotSupported err) =+    "'lib:Cabal' is too old to use '--keep-temp-files' flag. Likely caused by `build-type: Custom`. Original error:" <> line+    <> vcat (fmap pretty err)   pretty (LogCabalPath err) =     "Could not parse json output of 'cabal path': "       <> line <> indent 4 (pretty err)@@ -203,6 +210,21 @@   | LoadUnitsInferred   | LoadUnitsFromCradle   deriving (Eq,Ord,Enum,Bounded,Show)++newtype CacheDir = CacheDir { unCacheDir :: FilePath }+ deriving (Show, Eq)+ deriving newtype (IsString)++-- | Configuration for constructing and running a 'Cradle'.+data CradleRunConfig = CradleRunConfig+  { cradleCacheDir :: Maybe CacheDir+  -- ^ Root directory for cache artefacts produced while running the cradle.+  -- 'Nothing' falls back to @$HIE_BIOS_CACHE_DIR@, or the XDG cache+  -- directory if that is unset. See 'resolveCacheDir'.+  } deriving (Show, Eq)++defaultCradleRunConfig :: CradleRunConfig+defaultCradleRunConfig = CradleRunConfig { cradleCacheDir = Nothing }  data CradleAction a = CradleAction {                         actionName    :: ActionName a
tests/BiosTests.hs view
@@ -4,35 +4,45 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeApplications #-}+{-# LANGUAGE NumericUnderscores #-} module Main (main) where  import Utils -import Test.Tasty-import Test.Tasty.HUnit-import Test.Tasty.ExpectedFailure-import qualified Test.Tasty.Ingredients as Tasty-import qualified Test.Tasty.Options     as Tasty-import qualified Test.Tasty.Runners     as Tasty-import HIE.Bios-import HIE.Bios.Cradle-import HIE.Bios.Cradle.Cabal (cabalBuildDir)-import HIE.Bios.Types (LoadMode(..))-import Control.Monad (forM_, forM, unless)+import Control.Concurrent (threadDelay)+import Control.Concurrent.Async (replicateConcurrently)+import Control.Exception (SomeException, evaluate, try)+import Control.Monad (forM, forM_, unless, when) import Control.Monad.Extra (unlessM) import Control.Monad.IO.Class import Data.Foldable (for_)-import Data.List ( sort, isPrefixOf, isInfixOf, tails )+import Data.List (isInfixOf, isPrefixOf, sort, tails, last)+import Data.Maybe (isJust) import Data.Typeable-import System.Exit (ExitCode(ExitSuccess, ExitFailure))+import Data.Version+import HIE.Bios+import HIE.Bios.Cradle+import HIE.Bios.Cradle.Cabal (cabalBuildDir)+import HIE.Bios.Cradle.Utils (expandGhcOptionResponseFile)+import HIE.Bios.Environment (extractUnits, resolveCacheDir)+import qualified HIE.Bios.Ghc.Gap as Gap+import HIE.Bios.Process (cacheFileIn)+import HIE.Bios.Types (CacheDir (..), LoadMode (..), TargetWithContext (..)) import System.Directory-import System.FilePath ((</>), makeRelative)-import System.Info.Extra (isWindows)+import System.Exit (ExitCode (ExitFailure, ExitSuccess))+import System.FilePath (makeRelative, (</>)) import System.IO (BufferMode (LineBuffering), hSetBuffering, stderr, stdout)-import qualified HIE.Bios.Ghc.Gap as Gap-import HIE.Bios.Cradle.Utils (expandGhcOptionResponseFile)-import HIE.Bios.Environment (extractUnits)-+import System.IO.Temp+import System.Info.Extra (isWindows)+import System.Process+import Test.Tasty+import Test.Tasty.ExpectedFailure+import Test.Tasty.HUnit+import qualified Test.Tasty.Ingredients as Tasty+import qualified Test.Tasty.Options as Tasty+import qualified Test.Tasty.Runners as Tasty+import Text.ParserCombinators.ReadP (readP_to_S)+import Debug.Trace (traceShowId)  argDynamic :: [String] argDynamic = ["-dynamic" | Gap.hostIsDynamic]@@ -69,6 +79,10 @@   cabalDep <- checkToolIsAvailable "cabal"   extraGhcDep <- checkToolIsAvailable extraGhc +  -- Pre-create the shared extraGhc cabal store. The parallel tests otherwise+  -- race to initialise it and can fail with "package.db already exists".+  when (toolExists cabalDep && toolExists extraGhcDep) warmupExtraGhcStore+   defaultMainWithIngredients (ignoreToolTests:verboseLogging:defaultIngredients) $     -- Run tests sequentially on Windows, to avoid issues with locking of the     -- package database, e.g. errors of the form:@@ -77,14 +91,23 @@     testGroup "Bios-tests"       [ testGroup "Find cradle" findCradleTests       , testGroup "Symlink" symbolicLinkTests+      , testGroup "Cache files" cacheFileTests       , testGroup "Loading tests"         [ testGroup "bios" biosTestCases         , testGroup "direct" directTestCases-        , testGroupWithDependency cabalDep (cabalTestCases extraGhcDep)+        , testGroupWithDependency cabalDep (cabalTestCases cabalDep extraGhcDep)         , ignoreOnUnsupportedGhc $ testGroupWithDependency stackDep stackTestCases         ]       ] +-- | Load a cabal-with-ghc cradle once to create the extraGhc store. The result+-- is ignored; only the store-creation side effect matters. See 'main'.+warmupExtraGhcStore :: IO ()+warmupExtraGhcStore =+  runTestEnv "./cabal-with-ghc"+    (initCradle "src/MyLib.hs" *> loadComponentOptions (TargetWithContext "src/MyLib.hs" []))+    defConfig+ symbolicLinkTests :: [TestTree] symbolicLinkTests =   [ biosTestCase "Can load base module" $ runTestEnv "./symlink-test" $ do@@ -92,7 +115,7 @@       assertCradle isMultiCradle       step "Attempt to load symlinked module A"       do-        loadComponentOptions "./a/A.hs" []+        loadComponentOptions $ TargetWithContext "./a/A.hs" []         assertComponentOptions $ \opts ->           componentOptions opts `shouldMatchList` ["a"] <> argDynamic @@ -106,7 +129,7 @@         liftIO $ createDirectoryLink (rooted "a") (rooted "./b")         liftIO $ unlessM (doesFileExist $ rooted "b/A.hs") $           assertFailure "Test invariant broken, this file must exist."-        loadComponentOptions "./b/A.hs" []+        loadComponentOptions $ TargetWithContext "./b/A.hs" []         assertComponentOptions $ \opts ->           componentOptions opts `shouldMatchList` ["b"] <> argDynamic @@ -120,16 +143,39 @@         liftIO $ createDirectoryLink (rooted "./a") (rooted "./c")         liftIO $ unlessM (doesFileExist $ rooted "c/A.hs") $           assertFailure "Test invariant broken, this file must exist."-        loadComponentOptions "./c/A.hs" []+        loadComponentOptions $ TargetWithContext "./c/A.hs" []         assertLoadNone   ] +-- | Concurrent 'cacheFileIn' calls can race on the same cache entry.+cacheFileTests :: [TestTree]+cacheFileTests =+  [ testCase "cacheFile is atomic under concurrent calls" $ do+      withSystemTempDirectory "hie-bios-cache-file-test" $ \testCacheDir -> do+        let payloadPrefix = "#!/bin/sh\n"+            payloadSuffix = concat (replicate 500 "echo 'cacheFile is atomic under concurrent calls'\n")+            payload = payloadPrefix <> payloadSuffix+            -- Pause mid-write so a non-atomic populate would expose a partial+            -- file, and so all threads pile up on the entry concurrently.+            populate fp = do+              writeFile fp payloadPrefix+              threadDelay 50_000 -- 50ms+              appendFile fp payloadSuffix+        results <- replicateConcurrently 16 $ try @SomeException $ do+          fp <- cacheFileIn (CacheDir testCacheDir) "ghc-pkg" "0123456789abcdef" populate+          contents <- readFile fp+          contents <$ evaluate (length contents)+        forM_ results $ \case+          Left err -> assertFailure $ "cacheFile threw: " <> show err+          Right contents -> assertEqual "cached file contents" payload contents+  ]+ biosTestCases :: [TestTree] biosTestCases =   [ biosTestCase "failing-bios" $ runTestEnv "./failing-bios" $ do       initCradle "B.hs"       assertCradle isBiosCradle-      loadComponentOptions "B.hs" []+      loadComponentOptions $ TargetWithContext "B.hs" []       assertCradleError $ \CradleError {..} -> do         cradleErrorExitCode @?= ExitFailure 1         cradleErrorDependencies `shouldMatchList` ["hie.yaml"]@@ -148,7 +194,7 @@             then "Couldn't execute \"myGhc\"" `isPrefixOf` errorCtx @? "Error message should contain error information"             else "Couldn't execute myGhc"     `isPrefixOf` errorCtx @? "Error message should contain error information"   , biosTestCase "simple-bios-shell" $ runTestEnv "./simple-bios-shell" $ do-      testDirectoryM isBiosCradle "B.hs"+      testDirectoryM isBiosCradle $ single "B.hs"   , biosTestCase "simple-bios-shell-deps" $ runTestEnv "./simple-bios-shell" $ do       biosCradleDeps "B.hs" ["hie.yaml"]   ] <> concat [linuxTestCases | False] -- TODO(fendor), enable again@@ -157,23 +203,23 @@     biosCradleDeps fp deps = do       initCradle fp       assertCradle isBiosCradle-      loadComponentOptions fp []+      loadComponentOptions $ TargetWithContext fp []       assertComponentOptions $ \opts -> do         deps @?= componentDependencies opts      linuxTestCases =       [ biosTestCase "simple-bios" $ runTestEnv "./simple-bios" $-          testDirectoryM isBiosCradle "B.hs"+          testDirectoryM isBiosCradle $ single "B.hs"       , biosTestCase "simple-bios-ghc" $ runTestEnv "./simple-bios-ghc" $-          testDirectoryM isBiosCradle  "B.hs"+          testDirectoryM isBiosCradle $ single  "B.hs"       , biosTestCase "simple-bios-deps" $ runTestEnv "./simple-bios" $ do           biosCradleDeps "B.hs" ["hie-bios.sh", "hie.yaml"]       , biosTestCase "simple-bios-deps-new" $ runTestEnv "./deps-bios-new" $ do           biosCradleDeps "B.hs" ["hie-bios.sh", "hie.yaml"]       ] -cabalTestCases :: ToolDependency -> [TestTree]-cabalTestCases extraGhcDep =+cabalTestCases :: ToolDependency -> ToolDependency -> [TestTree]+cabalTestCases cabalDep extraGhcDep =   [     biosTestCaseAll "failing-cabal" $ runTestEnv "./failing-cabal" $ do       attemptCabalSingleTargetLoad "MyLib.hs"@@ -183,7 +229,7 @@   , biosTestCaseMulti "failing-cabal-multi-repl-with-shrink-error-files" $ runTestEnv "./failing-multi-repl-cabal-project" $ do       attemptCabalLoad "multi-repl-cabal-fail/app/Main.hs" ["multi-repl-cabal-fail/src/Lib.hs", "multi-repl-cabal-fail/src/Fail.hs", "NotInPath.hs"]       root <- askRoot-      multiSupported <- isCabalMultipleCompSupported'+      multiSupported <- isCabalMultipleCompSupportedM       if multiSupported         then           assertCradleError (\CradleError {..} -> do@@ -194,14 +240,16 @@         else assertLoadSuccess >>= \ComponentOptions {} -> do           return ()   , biosTestCaseAll "simple-cabal" $ runTestEnv "./simple-cabal" $ do-      testDirectoryM isCabalCradle "B.hs"+      testDirectoryM isCabalCradle $ single "B.hs"   , biosTestCaseMulti "build-dir" $ runTestEnv "./simple-cabal" $ do       initCradle "B.hs"       assertCradle isCabalCradle       root <- askRoot-      buildDir <- liftIO $ cabalBuildDir root+      buildDir <- liftIO $ do+        cacheDir <- resolveCacheDir "" Nothing+        cabalBuildDir cacheDir root       -- use --multi-repl, as that was the codepath with the bug-      loadFileGhc "B.hs" []+      loadFileGhc $ TargetWithContext "B.hs" []       liftIO $ do         -- Check we aren't trampling over dist-newstyle         distNewstyleExists <- doesDirectoryExist (root </> "dist-newstyle")@@ -209,6 +257,16 @@         -- Check we are using the correct build directory         buildDirExists <- doesDirectoryExist buildDir         assertBool "build dir does not exist" buildDirExists+  , biosTestCase "custom cache dir" $ runTestEnv "./simple-cabal" $+      withSystemTempDirectory "hie-bios-custom-cache-dir" $ \cacheRoot -> do+        initCradleWithConfig defaultCradleRunConfig { cradleCacheDir = Just (CacheDir cacheRoot) } "B.hs"+        assertCradle isCabalCradle+        loadComponentOptions $ TargetWithContext "B.hs" []+        _ <- assertLoadSuccess+        liftIO $ do+          entries <- listDirectory cacheRoot+          assertBool ("cache entries under the configured dir: " <> show entries)+            (any (\e -> "wrapper" `isInfixOf` e || "dist-" `isPrefixOf` e) entries)   , biosTestCaseAll "nested-cabal" $ runTestEnv "./nested-cabal" $ do       attemptCabalSingleTargetLoad "sub-comp/Lib.hs"       mode <- askLoadMode@@ -249,10 +307,10 @@       -- Initialize cradle first, since capability checks use the current cradle.       initCradle "sub-comp/Lib.hs"       assertCradle isCabalCradle-      multiSupported <- isCabalMultipleCompSupported'+      multiSupported <- isCabalMultipleCompSupportedM       if multiSupported         then do-          loadComponentOptions "sub-comp/Lib.hs" ["MyLib.hs"]+          loadComponentOptions $ TargetWithContext "sub-comp/Lib.hs" ["MyLib.hs"]           assertComponentOptions $ \opts -> do             -- Expect both the main component's cabal file and the enclosing cabal for the extra file,             -- plus project files.@@ -264,17 +322,17 @@               ]         else do           -- On older cabal/ghc combos, multi-repl isn't supported; just ensure load succeeds.-          loadComponentOptions "sub-comp/Lib.hs" []+          loadComponentOptions $ TargetWithContext "sub-comp/Lib.hs" []           _ <- assertLoadSuccess           pure ()   , biosTestCaseMulti "nested-cabal multi-mode includes enclosing deps when extra file is subcomp" $ runTestEnv "./nested-cabal" $ do       -- Initialize cradle at the top level, then treat the sub-component file as an extra file.       initCradle "MyLib.hs"       assertCradle isCabalCradle-      multiSupported <- isCabalMultipleCompSupported'+      multiSupported <- isCabalMultipleCompSupportedM       if multiSupported         then do-          loadComponentOptions "MyLib.hs" ["sub-comp/Lib.hs"]+          loadComponentOptions $ TargetWithContext "MyLib.hs" ["sub-comp/Lib.hs"]           assertComponentOptions $ \opts -> do             componentDependencies opts `shouldMatchList`               [ "nested-cabal.cabal"@@ -283,26 +341,26 @@               , "cabal.project.local"               ]         else do-          loadComponentOptions "MyLib.hs" []+          loadComponentOptions $ TargetWithContext "MyLib.hs" []           _ <- assertLoadSuccess           pure ()   , biosTestCaseAll "multi-cabal" $ runTestEnv "./multi-cabal" $ do       {- tests if both components can be loaded -}-      testDirectoryM isCabalCradle "app/Main.hs"-      testDirectoryM isCabalCradle "src/Lib.hs"+      testDirectoryM isCabalCradle $ single "app/Main.hs"+      testDirectoryM isCabalCradle $ single "src/Lib.hs"   , {- issue https://github.com/mpickering/hie-bios/issues/200 -}     biosTestCaseAll "monorepo-cabal" $ runTestEnv "./monorepo-cabal" $ do-      testDirectoryM isCabalCradle "A/Main.hs"-      testDirectoryM isCabalCradle "B/MyLib.hs"+      testDirectoryM isCabalCradle $ single "A/Main.hs"+      testDirectoryM isCabalCradle $ single "B/MyLib.hs"   , testGroup "Implicit cradle tests" $       [ biosTestCaseAll "implicit-cabal" $ runTestEnv "./implicit-cabal" $ do-          testImplicitDirectoryM isCabalCradle "Main.hs"+          testImplicitDirectoryM isCabalCradle $ single "Main.hs"       , biosTestCaseAll "implicit-cabal-no-project" $ runTestEnv "./implicit-cabal-no-project" $ do-          testImplicitDirectoryM isCabalCradle "Main.hs"+          testImplicitDirectoryM isCabalCradle $ single "Main.hs"       , biosTestCaseAll "implicit-cabal-deep-project" $ runTestEnv "./implicit-cabal-deep-project" $ do-          testImplicitDirectoryM isCabalCradle "foo/Main.hs"+          testImplicitDirectoryM isCabalCradle $ single "foo/Main.hs"       , biosTestCase "implicit-cabal-deep-project-with-context" $ runTestModeEnv "./implicit-cabal-deep-project" LoadFileWithContext $ do-          testImplicitDirectoryWithContextM isCabalCradle "foo/Main.hs" ["Main.hs"]+          testImplicitDirectoryM isCabalCradle $ ctx "foo/Main.hs" ["Main.hs"]       ]   , testGroupWithDependency extraGhcDep     [ biosTestCaseAll "Appropriate ghc and libdir" $ runTestEnv "./cabal-with-ghc" $ do@@ -313,88 +371,83 @@         loadRuntimeGhcVersion         assertGhcVersionIs extraGhcVersion         step "Find Component Options"-        loadComponentOptions "src/MyLib.hs" []+        loadComponentOptions $ TargetWithContext "src/MyLib.hs" []         _ <- assertLoadSuccess         pure ()     ]-  , testGroup "Cabal cabalProject"-    [ biosTestCaseAll "cabal-with-project, options propagated" $ runTestEnv "cabal-with-project" $ do+  , biosTestCaseAll "cabal-with-project, options propagated" $ runTestEnv "cabal-with-project" $ do         _opts <- cabalLoadOptions "src/MyLib.hs"         assertOptionsContain  "-O2" Nothing-    , biosTestCaseAll "cabal-with-project, load" $ runTestEnv "cabal-with-project" $ do-        testDirectoryM isCabalCradle "src/MyLib.hs"-    , biosTestCaseAll "multi-cabal-with-project, options propagated" $ runTestEnv "multi-cabal-with-project" $ do-        _optsAppA <- cabalLoadOptions "appA/src/Lib.hs"-        assertOptionsContain "-O2" (Just "appA")-    , biosTestCaseAll "multi-cabal-with-project, options not propagated" $ runTestEnv "multi-cabal-with-project" $ do-        _optsAppB <- cabalLoadOptions "appB/src/Lib.hs"-        assertOptionsDoNotContain "-O2" (Just "appB")-    , biosTestCaseAll "multi-cabal-with-project, load" $ runTestEnv "multi-cabal-with-project" $ do-        testDirectoryM isCabalCradle "appB/src/Lib.hs"-        testDirectoryM isCabalCradle "appB/src/Lib.hs"-    , testGroupWithDependency extraGhcDep-      [ biosTestCaseAll "Honours extra ghc setting" $ runTestEnv "cabal-with-ghc-and-project" $ do-          initCradle "src/MyLib.hs"-          assertCradle isCabalCradle-          loadRuntimeGhcLibDir-          assertLibDirVersionIs extraGhcVersion-          loadRuntimeGhcVersion-          assertGhcVersionIs extraGhcVersion-          step "Find Component Options"-          loadComponentOptions "src/MyLib.hs" []-          _ <- assertLoadSuccess-          pure ()-      ]-    , biosTestCaseAll "force older Cabal version in custom setup" $ runTestEnv "cabal-with-custom-setup" $ do+  , biosTestCaseAll "cabal-with-project, load" $ runTestEnv "cabal-with-project" $ do+      testDirectoryM isCabalCradle $ single "src/MyLib.hs"+  , biosTestCaseAll "multi-cabal-with-project, options propagated" $ runTestEnv "multi-cabal-with-project" $ do+      _optsAppA <- cabalLoadOptions "appA/src/Lib.hs"+      assertOptionsContain "-O2" (Just "appA")+  , biosTestCaseAll "multi-cabal-with-project, options not propagated" $ runTestEnv "multi-cabal-with-project" $ do+      _optsAppB <- cabalLoadOptions "appB/src/Lib.hs"+      assertOptionsDoNotContain "-O2" (Just "appB")+  , biosTestCaseAll "multi-cabal-with-project, load" $ runTestEnv "multi-cabal-with-project" $ do+      testDirectoryM isCabalCradle $ single "appB/src/Lib.hs"+      testDirectoryM isCabalCradle $ single "appB/src/Lib.hs"+  , testGroupWithDependency extraGhcDep+    [ biosTestCaseAll "Honours extra ghc setting" $ runTestEnv "cabal-with-ghc-and-project" $ do+        initCradle "src/MyLib.hs"+        assertCradle isCabalCradle+        loadRuntimeGhcLibDir+        assertLibDirVersionIs extraGhcVersion+        loadRuntimeGhcVersion+        assertGhcVersionIs extraGhcVersion+        step "Find Component Options"+        loadComponentOptions $ TargetWithContext "src/MyLib.hs" []+        _ <- assertLoadSuccess+        pure ()+    ]+  , biosTestCase "multi-cabal-with-load" $ runTestModeEnv "multi-cabal-with-load" LoadUnitsFromCradle $ do+      opts <- componentOptions <$> cabalLoadOptions  "appA/src/Lib.hs"+      liftIO $ do+        unless (any ("appA" `isInfixOf`) opts) $+          assertFailure $ "Missing appA: " ++ unwords opts+        unless (all (not . ("appB" `isInfixOf`)) opts) $+          assertFailure $ "Included appB: " ++ unwords opts+  , biosTestCase "multi-cabal-with-load-inferred" $ runTestModeEnv "multi-cabal-with-load" LoadUnitsInferred $ do+      -- LoadUnitsInferred should be unaffected by componentsToLoad+      opts <- componentOptions <$> cabalLoadOptions "appA/src/Lib.hs"+      liftIO $ do+        unless (any ("appA" `isInfixOf`) opts) $+          assertFailure $ "Missing appA: " ++ unwords opts+        unless (any ("appB" `isInfixOf`) opts) $+          assertFailure $ "Missing appB: " ++ unwords opts+  , biosTestCase "cabal-with-load" $ runTestModeEnv "cabal-with-load" LoadUnitsFromCradle $ do+      opts <- componentOptions <$> cabalLoadOptions "appA/src/Lib.hs"+      liftIO $ do+        unless (any ("appA" `isInfixOf`) opts) $+          assertFailure $ "Missing appA: " ++ unwords opts+        unless (all (not . ("appB" `isInfixOf`)) opts) $+          assertFailure $ "Included appB: " ++ unwords opts+  , biosTestCase "multi-cabal-with-load-superset" $ runTestModeEnv "multi-cabal-with-load-superset" LoadUnitsFromCradle $ do+      opts <- componentOptions <$> cabalLoadOptions "appA/src/Lib.hs"+      liftIO $ do+        unless (any ("appA" `isInfixOf`) opts) $+          assertFailure $ "Missing appA: " ++ unwords opts+        unless (any ("appB" `isInfixOf`) opts) $+          assertFailure $ "Missing appB: " ++ unwords opts+  , testGroup "custom Cabal"+    [ biosTestCaseAll "with-repl fallback" $ runTestEnv "cabal-with-custom-setup-old" $ do         -- Specifically tests whether cabal 3.16 works as expected with         -- an older lib:Cabal version that doesn't support '--with-repl'.         -- This test doesn't hurt for other cases as well, so we enable it for         -- all configurations.-        testDirectoryM isCabalCradle "src/MyLib.hs"-    , biosTestCaseMulti "force older Cabal version in custom setup with multi mode" $ runTestEnv "cabal-with-custom-setup" $ do+        testDirectoryM isCabalCradle $ single "a-with-custom/src/MyLib.hs"+    , expectBrokenOnCabal318 cabalDep $ biosTestCaseAll "loads other packages with multi-repl" $ runTestEnv "cabal-with-custom-setup-old" $ do         -- Specifically tests whether cabal 3.16 works as expected with         -- an older lib:Cabal version that doesn't support '--with-repl'.         -- This test doesn't hurt for other cases as well, so we enable it for         -- all configurations.-        let target = "src/MyLib.hs"-        initCradle target-        assertCradle isCabalCradle-        loadRuntimeGhcLibDir-        assertLibDirVersion-        loadRuntimeGhcVersion-        assertGhcVersion-        -- suffices to force loading cabal's `--enable-multi-repl` codepath-        loadFileGhc target []-    , biosTestCase "multi-cabal-with-load" $ runTestModeEnv "multi-cabal-with-load" LoadUnitsFromCradle $ do-        opts <- componentOptions <$> cabalLoadOptions  "appA/src/Lib.hs"-        liftIO $ do-          unless (any ("appA" `isInfixOf`) opts) $-            assertFailure $ "Missing appA: " ++ unwords opts-          unless (all (not . ("appB" `isInfixOf`)) opts) $-            assertFailure $ "Included appB: " ++ unwords opts-    , biosTestCase "multi-cabal-with-load-inferred" $ runTestModeEnv "multi-cabal-with-load" LoadUnitsInferred $ do-        -- LoadUnitsInferred should be unaffected by componentsToLoad-        opts <- componentOptions <$> cabalLoadOptions "appA/src/Lib.hs"-        liftIO $ do-          unless (any ("appA" `isInfixOf`) opts) $-            assertFailure $ "Missing appA: " ++ unwords opts-          unless (any ("appB" `isInfixOf`) opts) $-            assertFailure $ "Missing appB: " ++ unwords opts-    , biosTestCase "cabal-with-load" $ runTestModeEnv "cabal-with-load" LoadUnitsFromCradle $ do-        opts <- componentOptions <$> cabalLoadOptions "appA/src/Lib.hs"-        liftIO $ do-          unless (any ("appA" `isInfixOf`) opts) $-            assertFailure $ "Missing appA: " ++ unwords opts-          unless (all (not . ("appB" `isInfixOf`)) opts) $-            assertFailure $ "Included appB: " ++ unwords opts-    , biosTestCase "multi-cabal-with-load-superset" $ runTestModeEnv "multi-cabal-with-load-superset" LoadUnitsFromCradle $ do-        opts <- componentOptions <$> cabalLoadOptions "appA/src/Lib.hs"-        liftIO $ do-          unless (any ("appA" `isInfixOf`) opts) $-            assertFailure $ "Missing appA: " ++ unwords opts-          unless (any ("appB" `isInfixOf`) opts) $-            assertFailure $ "Missing appB: " ++ unwords opts+        testDirectoryM isCabalCradle $ ctx "b/src/B.hs" ["b/tests/Main.hs"]+    , biosTestCaseAll "custom package and simple package" $ runTestEnv "cabal-with-custom-setup" $ do+        testDirectoryM isCabalCradle $ single "b/src/B.hs"     ]+   ]   where     attemptCabalSingleTargetLoad fp = attemptCabalLoad fp []@@ -403,13 +456,13 @@     attemptCabalLoad fp fps = do       initCradle fp       assertCradle isCabalCradle-      loadComponentOptions fp fps+      loadComponentOptions $ TargetWithContext fp fps      cabalLoadOptions :: FilePath -> TestM ComponentOptions     cabalLoadOptions fp = do       initCradle fp       assertCradle isCabalCradle-      loadComponentOptions fp []+      loadComponentOptions $ TargetWithContext fp []       assertLoadSuccess  assertOptionsContain :: [Char] -> Maybe String -> TestM ()@@ -452,12 +505,12 @@             cradleErrorExitCode @?= ExitFailure 1             cradleErrorDependencies `shouldMatchList` ["failing-stack.cabal", "stack.yaml", "package.yaml"]   , biosTestCase "simple-stack" $ runTestEnv "./simple-stack" $ do-      testDirectoryM isStackCradle "B.hs"+      testDirectoryM isStackCradle $ single "B.hs"   , biosTestCase "multi-stack" $ runTestEnv "./multi-stack" $ do {- tests if both components can be loaded -}-      testDirectoryM isStackCradle "app/Main.hs"-      testDirectoryM isStackCradle "src/Lib.hs"+      testDirectoryM isStackCradle $ single "app/Main.hs"+      testDirectoryM isStackCradle $ single "src/Lib.hs"   , biosTestCaseMulti "multi-stack-multi-modes" $ runTestEnv "./multi-stack" $ do-      testDirectoryM isStackCradle "app/Main.hs"+      testDirectoryM isStackCradle $ single "app/Main.hs"   , biosTestCase "nested-stack" $ runTestEnv "./nested-stack" $ do       stackAttemptLoad "sub-comp/Lib.hs"       assertComponentOptions $ \opts ->@@ -468,14 +521,14 @@         componentDependencies opts `shouldMatchList` ["nested-stack.cabal", "package.yaml", "stack.yaml"]   , biosTestCase "stack-with-yaml" $ runTestEnv "./stack-with-yaml" $ do       {- tests if both components can be loaded -}-      testDirectoryM isStackCradle "app/Main.hs"-      testDirectoryM isStackCradle "src/Lib.hs"+      testDirectoryM isStackCradle $ single "app/Main.hs"+      testDirectoryM isStackCradle $ single "src/Lib.hs"   , biosTestCase "multi-stack-with-yaml" $ runTestEnv "./multi-stack-with-yaml" $ do       {- tests if both components can be loaded -}-      testDirectoryM isStackCradle "appA/src/Lib.hs"-      testDirectoryM isStackCradle "appB/src/Lib.hs"+      testDirectoryM isStackCradle $ single "appA/src/Lib.hs"+      testDirectoryM isStackCradle $ single "appB/src/Lib.hs"   , biosTestCase "multi-stack-with-load" $ runTestModeEnv "multi-stack-with-load" LoadUnitsFromCradle $ do-      testDirectoryM isStackCradle "appA/src/LibA.hs"+      testDirectoryM isStackCradle $ single "appA/src/LibA.hs"       assertComponentOptions $ \ opts0 -> do         let opts = componentOptions opts0         unless (any ("appA" `isInfixOf`) opts) $@@ -483,7 +536,7 @@         unless (all (not . ("appB" `isInfixOf`)) opts) $           assertFailure $ "Included appB: " ++ unwords opts   , biosTestCase "multi-stack-with-load-inferred" $ runTestModeEnv "multi-stack-with-load" LoadUnitsInferred $ do-      testDirectoryM  isStackCradle "appA/src/LibA.hs"+      testDirectoryM  isStackCradle $ single "appA/src/LibA.hs"       assertComponentOptions $ \ opts0 -> do         let opts = componentOptions opts0         unless (any ("appA" `isInfixOf`) opts) $@@ -494,14 +547,14 @@     -- Test for special characters in the path for parsing of the ghci-scripts.     -- Issue https://github.com/mpickering/hie-bios/issues/162     biosTestCase "space stack" $ runTestEnv "./space stack" $ do-      testDirectoryM isStackCradle "A.hs"-      testDirectoryM isStackCradle "B.hs"+      testDirectoryM isStackCradle $ single "A.hs"+      testDirectoryM isStackCradle $ single "B.hs"   , testGroup "Implicit cradle tests"       [ biosTestCase "implicit-stack" $ runTestModeEnv "./implicit-stack" LoadFile $ do-          testImplicitDirectoryM isStackCradle "Main.hs"+          testImplicitDirectoryM isStackCradle $ single "Main.hs"       , biosTestCase "implicit-stack-multi" $ runTestModeEnv "./implicit-stack-multi" LoadFile $ do-          testImplicitDirectoryM isStackCradle "Main.hs"-          testImplicitDirectoryM  isStackCradle "other-package/Main.hs"+          testImplicitDirectoryM isStackCradle $ single "Main.hs"+          testImplicitDirectoryM  isStackCradle $ single "other-package/Main.hs"       ]   ]   where@@ -509,16 +562,16 @@     stackAttemptLoad fp = do       initCradle fp       assertCradle isStackCradle-      loadComponentOptions fp []+      loadComponentOptions $ TargetWithContext fp []  directTestCases :: [TestTree] directTestCases =   [ biosTestCase "simple-direct" $ runTestEnv  "./simple-direct" $ do-      testDirectoryM isDirectCradle "B.hs"+      testDirectoryM isDirectCradle $ single "B.hs"   , biosTestCase "multi-direct" $ runTestEnv "./multi-direct" $ do       {- tests if both components can be loaded -}-      testDirectoryM isMultiCradle "A.hs"-      testDirectoryM isMultiCradle "B.hs"+      testDirectoryM isMultiCradle $ single "A.hs"+      testDirectoryM isMultiCradle $ single "B.hs"   ]  findCradleTests :: [TestTree]@@ -623,15 +676,25 @@  data ToolDependency = ToolDependency   { toolName :: String-  , toolExists :: Bool+  , toolVersion :: Maybe Version   } +toolExists :: ToolDependency -> Bool+toolExists td = isJust $ toolVersion td+ checkToolIsAvailable :: String -> IO ToolDependency checkToolIsAvailable f = do-  exists <- maybe False (const True) <$> findExecutable f+  mexe <- findExecutable f+  version <- case mexe of+    Nothing -> pure Nothing+    Just exe -> do+      versionStr <- readProcess exe ["--numeric-version"] ""+      pure $ case readP_to_S parseVersion versionStr of+        xs@(_:_) -> Just $ fst $ last xs+        [] -> Nothing   pure ToolDependency     { toolName = f-    , toolExists = exists+    , toolVersion = version     }  testGroupWithDependency :: ToolDependency -> [TestTree] -> TestTree@@ -696,4 +759,16 @@ #if (defined(MIN_VERSION_GLASGOW_HASKELL) && MIN_VERSION_GLASGOW_HASKELL(9,14,0,0))   ignoreTestBecause "Not supported on GHC 9.14" #endif-    tt+  tt++expectBrokenOnCabal318 :: ToolDependency -> TestTree -> TestTree+expectBrokenOnCabal318 td tt =+  if traceShowId (traceShowId (toolVersion td) >= Just (makeVersion [3,17,0,0]))+    then expectFailBecause+            ("cabal 3.18 passes all options using response files. \+            If old lib:Cabal versions are used, we can't pass '--keep-temp-files' because the configure step rejects it. \+            Thus, the response files are deleted before we can parse them. Falling back to the ghc shim wrapper doesn't work either \+            since all arguments are passed via response files and we can't see the first argument to be '--interactive' and the wrapper fails.\+            "+            ) tt+    else tt
tests/Utils.hs view
@@ -41,14 +41,18 @@   step,   normFile,   relFile,+  mainTarget,   findCradleLoc,   initCradle,+  initCradleWithConfig,   initImplicitCradle,   loadComponentOptions,   loadRuntimeGhcLibDir,   loadRuntimeGhcVersion,   loadFileGhc,-  isCabalMultipleCompSupported',+  isCabalMultipleCompSupportedM,+  single,+  ctx,    -- * Assertion helpers   assertCradle,@@ -67,7 +71,6 @@   -- * High-level test helpers   testDirectoryM,   testImplicitDirectoryM,-  testImplicitDirectoryWithContextM,   findCradleForModuleM, ) where @@ -270,6 +273,12 @@ -- Test setup helpers -- --------------------------------------------------------------------------- +single :: FilePath -> TargetWithContext+single fp = TargetWithContext fp []++ctx :: FilePath -> [FilePath] -> TargetWithContext+ctx fp fps = TargetWithContext fp fps+ step :: String -> TestM () step msg = do   s <- askStep@@ -278,6 +287,9 @@ normFile :: FilePath -> TestM FilePath normFile fp = (</> fp) <$> gets testRootDir +mainTarget :: TargetWithContext -> TestM FilePath+mainTarget target = (</> targetFilePath target) <$> gets testRootDir+ relFile :: FilePath -> TestM FilePath relFile fp = (`makeRelative` fp) <$> gets testRootDir @@ -287,7 +299,10 @@   liftIO $ findCradle a_fp  initCradle :: FilePath -> TestM ()-initCradle fp = do+initCradle = initCradleWithConfig defaultCradleRunConfig++initCradleWithConfig :: CradleRunConfig -> FilePath -> TestM ()+initCradleWithConfig config fp = do   a_fp <- normFile fp   step $ "Finding Cradle for: " <> fp   mcfg <- findCradleLoc a_fp@@ -295,8 +310,8 @@   step $ "Loading Cradle: " <> show relMcfg   logger <- askLogger   crd <- case mcfg of-    Just cfg -> liftIO $ loadCradle logger cfg-    Nothing -> liftIO $ loadImplicitCradle logger a_fp+    Just cfg -> liftIO $ loadCradleWithConfig logger config cfg+    Nothing -> liftIO $ loadImplicitCradleWithConfig logger config a_fp   step $ "Cradle: " ++ show crd   setCradle crd @@ -308,8 +323,8 @@   crd <- liftIO $ loadImplicitCradle logger a_fp   setCradle crd -loadComponentOptions :: FilePath -> [FilePath] -> TestM ()-loadComponentOptions fp extraFps = do+loadComponentOptions :: TargetWithContext -> TestM ()+loadComponentOptions (TargetWithContext fp extraFps) = do   a_fp <- normFile fp   a_fps <- traverse normFile extraFps   crd <- askCradle@@ -334,24 +349,27 @@   ghcVersionRes <- liftIO $ getRuntimeGhcVersion crd   setGhcVersionResult ghcVersionRes -isCabalMultipleCompSupported' :: TestM Bool-isCabalMultipleCompSupported' = do+isCabalMultipleCompSupportedM :: TestM Bool+isCabalMultipleCompSupportedM = do   cr <- askCradle   root <- askRoot   versions <- liftIO $ makeVersions (cradleLogger cr) root ((runGhcCmd . cradleOptsProg) cr)   liftIO $ isCabalMultipleCompSupported versions -loadFileGhc :: FilePath -> [FilePath] -> TestM ()-loadFileGhc fp extraFps = do+loadFileGhc :: TargetWithContext -> TestM ()+loadFileGhc target = do   libdir <- askOrLoadLibDir-  a_fp <- normFile fp+  a_fp <- mainTarget target   stepF <- askStep   step "Cradle load"-  loadComponentOptions fp extraFps+  loadComponentOptions target   opts <- assertLoadSuccess+  root <- askRoot+  -- Tests run in parallel, so isolate the interface-file cache per test root.+  let ifaceCacheRoot = CacheDir (root </> "ghc-iface-cache")   liftIO $     G.runGhc (Just libdir) $ do-      let (ini, _) = initSessionWithMessage' True (Just G.batchMsg) opts+      let (ini, _) = initSessionWithMessage' (Just ifaceCacheRoot) (Just G.batchMsg) opts       sf <- ini       case sf of         -- Test resetting the targets@@ -435,28 +453,25 @@ -- High-level, re-usable assertions -- --------------------------------------------------------------------------- -testDirectoryM :: (Cradle Void -> Bool) -> FilePath -> TestM ()-testDirectoryM cradlePred file = do-  initCradle file+testDirectoryM :: (Cradle Void -> Bool) -> TargetWithContext -> TestM ()+testDirectoryM cradlePred target = do+  initCradle $ targetFilePath target   assertCradle cradlePred   loadRuntimeGhcLibDir   assertLibDirVersion   loadRuntimeGhcVersion   assertGhcVersion-  loadFileGhc file []--testImplicitDirectoryM :: (Cradle Void -> Bool) -> FilePath -> TestM ()-testImplicitDirectoryM cradlePred file = testImplicitDirectoryWithContextM cradlePred file []+  loadFileGhc target -testImplicitDirectoryWithContextM :: (Cradle Void -> Bool) -> FilePath -> [FilePath] -> TestM ()-testImplicitDirectoryWithContextM cradlePred file ctxt = do-  initImplicitCradle file+testImplicitDirectoryM :: (Cradle Void -> Bool) -> TargetWithContext -> TestM ()+testImplicitDirectoryM cradlePred target = do+  initImplicitCradle $ targetFilePath target   assertCradle cradlePred   loadRuntimeGhcLibDir   assertLibDirVersion   loadRuntimeGhcVersion   assertGhcVersion-  loadFileGhc file ctxt+  loadFileGhc target  findCradleForModuleM :: FilePath -> Maybe FilePath -> TestM () findCradleForModuleM fp expected' = do
+ tests/projects/cabal-with-custom-setup-old/a-with-custom/Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ tests/projects/cabal-with-custom-setup-old/a-with-custom/a-with-custom.cabal view
@@ -0,0 +1,15 @@+cabal-version:      3.0+name:               a-with-custom+version:            0.1.0.0+build-type:         Custom++custom-setup+    setup-depends:+        Cabal <3.16,+        base++library+    exposed-modules:  MyLib+    build-depends:    base+    hs-source-dirs:   src+    default-language: Haskell2010
+ tests/projects/cabal-with-custom-setup-old/a-with-custom/src/MyLib.hs view
@@ -0,0 +1,4 @@+module MyLib (someFunc) where++someFunc :: IO ()+someFunc = putStrLn "someFunc"
+ tests/projects/cabal-with-custom-setup-old/b/b.cabal view
@@ -0,0 +1,30 @@+cabal-version:      3.0+name:               b+version:            0.1.0.0+-- synopsis:+-- description:+license:            NONE+author:             fendor+maintainer:         fendor@posteo.de+-- copyright:+build-type:         Simple+extra-doc-files:    CHANGELOG.md+-- extra-source-files:++common warnings+    ghc-options: -Wall++library+    import:           warnings+    exposed-modules:  B+    -- other-modules:+    -- other-extensions:+    build-depends:    base, a-with-custom+    hs-source-dirs:   src+    default-language: Haskell2010++test-suite b-tests+  type: exitcode-stdio-1.0+  main-is: Main.hs+  hs-source-dirs: tests+  build-depends: base
+ tests/projects/cabal-with-custom-setup-old/b/src/B.hs view
@@ -0,0 +1,6 @@+module B (someFunc) where++import MyLib ()++someFunc :: IO ()+someFunc = putStrLn "someFunc"
+ tests/projects/cabal-with-custom-setup-old/b/tests/Main.hs view
@@ -0,0 +1,3 @@+module Main where++main = putStrLn "Not implemented"
+ tests/projects/cabal-with-custom-setup-old/cabal.project view
@@ -0,0 +1,5 @@+packages:+    a-with-custom/+    b/++allow-newer: base, containers
+ tests/projects/cabal-with-custom-setup/a-with-custom/Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ tests/projects/cabal-with-custom-setup/a-with-custom/a-with-custom.cabal view
@@ -0,0 +1,15 @@+cabal-version:      3.0+name:               a-with-custom+version:            0.1.0.0+build-type:         Custom++custom-setup+    setup-depends:+        Cabal,+        base++library+    exposed-modules:  MyLib+    build-depends:    base+    hs-source-dirs:   src+    default-language: Haskell2010
+ tests/projects/cabal-with-custom-setup/a-with-custom/src/MyLib.hs view
@@ -0,0 +1,4 @@+module MyLib (someFunc) where++someFunc :: IO ()+someFunc = putStrLn "someFunc"
+ tests/projects/cabal-with-custom-setup/b/b.cabal view
@@ -0,0 +1,24 @@+cabal-version:      3.0+name:               b+version:            0.1.0.0+-- synopsis:+-- description:+license:            NONE+author:             fendor+maintainer:         fendor@posteo.de+-- copyright:+build-type:         Simple+extra-doc-files:    CHANGELOG.md+-- extra-source-files:++common warnings+    ghc-options: -Wall++library+    import:           warnings+    exposed-modules:  B+    -- other-modules:+    -- other-extensions:+    build-depends:    base, a-with-custom+    hs-source-dirs:   src+    default-language: Haskell2010
+ tests/projects/cabal-with-custom-setup/b/src/B.hs view
@@ -0,0 +1,6 @@+module B (someFunc) where++import MyLib ()++someFunc :: IO ()+someFunc = putStrLn "someFunc"
+ tests/projects/cabal-with-custom-setup/cabal.project view
@@ -0,0 +1,5 @@+packages:+    a-with-custom/+    b/++allow-newer: base, containers