diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,43 @@
 # ChangeLog hie-bios
 
+## 2026-06-30 - 0.20.0
+
+* Hide LoadMode in TestM ([#516](https://github.com/haskell/hie-bios/pull/516))
+* "componentsToLoad" field ([#515](https://github.com/haskell/hie-bios/pull/515))
+* Fail hard if component(s) field have wrong type ([#513](https://github.com/haskell/hie-bios/pull/513))
+* Allow to build with GHC 10.1  ([#510](https://github.com/haskell/hie-bios/pull/510))
+* New Load Modes for upfront loading of cradle/project ([#508](https://github.com/haskell/hie-bios/pull/508))
+  * Implement full project loading
+
+## 2026-04-15 - 0.19.0
+
+* Pass --builddir to avoid clobbering dist-newstyle ([#503](https://github.com/haskell/hie-bios/pull/503))
+
+## 2026-02-06 - 0.18.0
+
+* Adapt to OsPath change ([#493](https://github.com/haskell/hie-bios/pull/493))
+* Parallel test suite ([#487](https://github.com/haskell/hie-bios/pull/487))
+* Fix processCabalLoadStyle to include dyn dep for extra files ([#484](https://github.com/haskell/hie-bios/pull/484))
+* Improve hie-bios cli interface ([#480](https://github.com/haskell/hie-bios/pull/480))
+* Only pass --keep-temp-files once ([#477](https://github.com/haskell/hie-bios/pull/477))
+
+## 2025-08-07 - 0.17.0
+
+* Add support for cabal 3.16.1.0 [#470](https://github.com/haskell/hie-bios/pull/470)
+
+## 2025-07-09 - 0.16.0
+
+### Bugfix
+
+* Always specify '--with-hc-pkg' with cabal [#465](https://github.com/haskell/hie-bios/pull/465)
+
+### Changes
+
+* Add regression test for [hie-bios/pull/465](https://github.com/haskell/hie-bios/pull/465) [#468](https://github.com/haskell/hie-bios/pull/468)
+* Cabal cradle needs to use --with-repl option [#466](https://github.com/haskell/hie-bios/pull/466)
+* fix: rewrite findFileUpwards as a non predicate function [#464](https://github.com/haskell/hie-bios/pull/464)
+* Add support for bios multi-cradles [#437](https://github.com/haskell/hie-bios/pull/437)
+
 ## 2025-04-25 - 0.15.0
 
 * Use consistent logging for test cases [#461](https://github.com/haskell/hie-bios/pull/461)
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -552,6 +552,33 @@
                             , { path: "./tests", component: "parser-tests" } ] } }
 ```
 
+## Selecting which components to load
+
+For `cabal` and `stack` cradles, `hie-bios` supports asking for flags to load
+the components in the cradle at once. This is much faster than loading
+components on-demand at the cost of more memory usage if a component isn't
+actually needed. Support is conditional on underlying tool versions, and
+`hie-bios` will fall back to legacy loading otherwise.
+
+The optional field `componentsToLoad` specifies which components should be loaded together, it defaults to all the explicitly listed components.
+
+```yaml
+cradle:
+  cabal:
+    componentsToLoad: ["lib:foo"]
+    components:
+      - {"path": "./src", "component": "lib:foo"}
+      - {"path": "./app", "component": "exe:foo"}
+```
+
+The elements of `componentsToLoad` do not need to be also listed as individual components, though doing so might improve dependency tracking.
+
+```yaml
+cradle:
+  stack:
+    componentsToLoad: ["foo:lib","foo:exe"]
+```
+
 ## Cradle Dependencies
 
 Sometimes it is necessary to reload a component, for example when a package
@@ -609,9 +636,21 @@
 ```yaml
 cradle:
   cabal:
+    cabalProject: "optional path to project description"
+    componentsToLoad: ["cabal target1","cabal target2"]
+    -- one or none of the following
     component: "optional component name"
+    components:
+      - path: "path to package dir"
+        component: "cabal target"
   stack:
+    stackYaml: "optional path to stack yaml file"
+    componentsToLoad: ["stack target1","stack target2"]
+    -- one or none of the following
     component: "optional component name"
+    components:
+      - path: "path to package dir"
+        component: "stack target"
   bios:
     program: "program to run"
     dependency-program: "optional program to run"
diff --git a/exe/Main.hs b/exe/Main.hs
--- a/exe/Main.hs
+++ b/exe/Main.hs
@@ -8,7 +8,7 @@
 import Data.Version (showVersion)
 import Prettyprinter
 import Options.Applicative
-import System.Directory (getCurrentDirectory)
+import System.Directory (getCurrentDirectory, makeAbsolute)
 import System.IO (stdout, hSetEncoding, utf8)
 import System.FilePath( (</>) )
 
@@ -16,46 +16,73 @@
 import HIE.Bios.Ghc.Check
 import HIE.Bios.Ghc.Gap as Gap
 import HIE.Bios.Internal.Debug
-import HIE.Bios.Types (LoadStyle(LoadFile))
+import HIE.Bios.Types (LoadMode(..), TargetWithContext (..), singleTarget)
 import Paths_hie_bios
+import Data.Void (Void)
+import Data.Function
 
 ----------------------------------------------------------------
 
 progVersion :: String
 progVersion = "hie-bios version " ++ showVersion version ++ " compiled by GHC " ++ Gap.ghcVersion ++ "\n"
 
+data UseLoadMode
+  = UseSingleFile
+  | UseMultiFile
+  | UseUnitsInferred
+  | UseUnitsFromCradle
+
+data Cli = Cli
+  { logLevel :: Maybe L.Severity
+  , biosCommand :: Command
+  }
+
 data Command
   = Check { checkTargetFiles :: [FilePath] }
   | Flags { flagTargetFiles :: [FilePath] }
-  | Debug { debugComponents :: FilePath }
+  | Debug { debugUseMultiLoadMode :: UseLoadMode, debugComponents :: [FilePath] }
   | ConfigInfo { configFiles :: [FilePath] }
   | CradleInfo { cradleFiles :: [FilePath] }
   | Root
   | Version
 
 
-filepathParser :: Parser [FilePath]
-filepathParser = some (argument str ( metavar "TARGET_FILES..."))
+filepathParser :: Parser FilePath
+filepathParser = argument str ( metavar "TARGET_FILES...")
 
-progInfo :: ParserInfo Command
-progInfo = info (progParser <**> helper)
+progInfo :: ParserInfo Cli
+progInfo = info (cliParser <**> helper)
   ( fullDesc
   <> progDesc "hie-bios is the way to specify how haskell-language-server and ghcide set up a GHC API session.\
               \Delivers the full set of flags to pass to GHC in order to build the project."
   <> header progVersion
   <> footer "You can report issues/contribute at https://github.com/mpickering/hie-bios")
 
+cliParser :: Parser Cli
+cliParser = Cli
+  <$> optional sevParser
+  <*> progParser
+
+sevParser :: Parser L.Severity
+sevParser =
+  flag' L.Debug (short 'v')
+
 progParser :: Parser Command
-progParser = subparser
-    (command "check" (info (Check <$> filepathParser) (progDesc "Try to load modules into the GHC API."))
-    <> command "flags" (info (Flags <$> filepathParser) (progDesc "Print out the options that hie-bios thinks you will need to load a file."))
-    <> command "debug" (info (Debug <$> argument str ( metavar "TARGET_FILES...")) (progDesc "Print out the options that hie-bios thinks you will need to load a file."))
-    <> command "config" (info (ConfigInfo <$> filepathParser) (progDesc "Print out the cradle config."))
-    <> command "cradle" (info (CradleInfo <$> filepathParser) (progDesc "."))
+progParser = hsubparser
+    (command "check" (info (Check <$> some filepathParser) (progDesc "Try to load modules into the GHC API."))
+    <> command "flags" (info (Flags <$> some filepathParser) (progDesc "Print out the options that hie-bios thinks you will need to load a file."))
+    <> command "debug" (info (Debug <$> loadModeParser <*> many filepathParser) (progDesc "Print out the options that hie-bios thinks you will need to load a file."))
+    <> command "config" (info (ConfigInfo <$> some filepathParser) (progDesc "Print out the cradle config location."))
+    <> command "cradle" (info (CradleInfo <$> some filepathParser) (progDesc "Print out only the cradle type."))
     <> command "root" (info (pure Root) (progDesc "Display the path towards the selected hie.yaml."))
     <> command "version" (info (pure Version) (progDesc "Print version and exit."))
     )
 
+loadModeParser :: Parser UseLoadMode
+loadModeParser =
+  flag UseSingleFile UseMultiFile (long "multi" <> help "Load all targets in bulk if supported")
+  <|> flag UseSingleFile UseUnitsInferred (long "inferred-units" <> help "Load all units in bulk if supported")
+  <|> flag UseSingleFile UseUnitsFromCradle (long "cradle-units" <> help "Load all units from cradle components in bulk if supported")
 
 ----------------------------------------------------------------
 
@@ -63,11 +90,15 @@
 main = do
     hSetEncoding stdout utf8
     cwd <- getCurrentDirectory
-    cmd <- execParser progInfo
+    cli <- execParser progInfo
     let
       printLog (L.WithSeverity l sev) = "[" ++ show sev ++ "] " ++ show (pretty l)
       logger :: forall a . Pretty a => L.LogAction IO (L.WithSeverity a)
-      logger = L.cmap printLog L.logStringStderr
+      logger = L.logStringStderr
+        & L.cmap printLog
+        & L.cfilter (\msg -> case logLevel cli of
+            Nothing -> False
+            Just lvl -> L.getSeverity msg >= lvl )
 
     cradle <-
         -- find cradle does a takeDirectory on the argument, so make it into a file
@@ -75,17 +106,17 @@
           Just yaml -> loadCradle logger yaml
           Nothing -> loadImplicitCradle logger (cwd </> "File.hs")
 
-    res <- case cmd of
+    res <- case biosCommand cli of
       Check targetFiles -> checkSyntax logger cradle targetFiles
-      Debug files -> case files of
-        [] -> debugInfo (cradleRootDir cradle) cradle
-        fp -> debugInfo fp cradle
+      Debug useMultiStyle files -> do
+        absFiles <- traverse makeAbsolute files
+        debugFiles absFiles useMultiStyle cradle
       Flags files -> case files of
         -- TODO force optparse to acquire one
         [] -> error "too few arguments"
         _ -> do
           res <- forM files $ \fp -> do
-                  res <- getCompilerOptions fp LoadFile cradle
+                  res <- getCompilerOptions (TargetWithContext fp []) LoadFile cradle
                   case res of
                       CradleFail (CradleError _deps _ex err _fps) ->
                         return $ "Failed to show flags for \""
@@ -102,3 +133,18 @@
       Root    -> rootInfo cradle
       Version -> return progVersion
     putStr res
+
+debugFiles :: [FilePath] -> UseLoadMode -> Cradle Void -> IO String
+debugFiles fps useLoadMode cradle = case useLoadMode of
+  UseSingleFile -> debugSingle
+  UseMultiFile -> debugBulk LoadFileWithContext
+  UseUnitsInferred -> debugBulk LoadUnitsInferred
+  UseUnitsFromCradle -> debugBulk LoadUnitsFromCradle
+  where
+    debugSingle = case fps of
+      [] -> debugInfo (singleTarget $ cradleRootDir cradle) LoadFile cradle
+      _ -> concat <$> traverse (\fp -> debugInfo (singleTarget fp) LoadFile cradle) fps
+
+    debugBulk mode = case fps of
+      [] -> debugInfo (TargetWithContext (cradleRootDir cradle) []) mode cradle
+      fp:otherFps -> debugInfo (TargetWithContext fp otherFps) mode cradle
diff --git a/hie-bios.cabal b/hie-bios.cabal
--- a/hie-bios.cabal
+++ b/hie-bios.cabal
@@ -1,254 +1,275 @@
-Cabal-Version:          2.2
-Name:                   hie-bios
-Version:                0.15.0
-Author:                 Matthew Pickering <matthewtpickering@gmail.com>
-Maintainer:             Matthew Pickering <matthewtpickering@gmail.com>
-License:                BSD-3-Clause
-License-File:           LICENSE
-Homepage:               https://github.com/haskell/hie-bios
-Synopsis:               Set up a GHC API session
-Description:            Set up a GHC API session and obtain flags required to compile a source file
-
-Category:               Development
-Build-Type:             Simple
--- No glob syntax until GHC 8.6 because of stack
-extra-doc-files:        ChangeLog.md
-Extra-Source-Files:     README.md
-                        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/cabal-with-ghc-and-project/cabal-with-ghc.cabal
-                        tests/projects/cabal-with-ghc-and-project/cabal.project.9.2.8
-                        tests/projects/cabal-with-ghc-and-project/hie.yaml
-                        tests/projects/cabal-with-ghc-and-project/src/MyLib.hs
-                        tests/projects/cabal-with-project/cabal-with-project.cabal
-                        tests/projects/cabal-with-project/cabal.project.9.2.8
-                        tests/projects/cabal-with-project/hie.yaml
-                        tests/projects/cabal-with-project/src/MyLib.hs
-                        tests/projects/symlink-test/a/A.hs
-                        tests/projects/symlink-test/hie.yaml
-                        tests/projects/deps-bios-new/A.hs
-                        tests/projects/deps-bios-new/B.hs
-                        tests/projects/deps-bios-new/hie-bios.sh
-                        tests/projects/deps-bios-new/hie.yaml
-                        tests/projects/multi-direct/A.hs
-                        tests/projects/multi-direct/B.hs
-                        tests/projects/multi-direct/hie.yaml
-                        tests/projects/multi-cabal/app/Main.hs
-                        tests/projects/multi-cabal/cabal.project
-                        tests/projects/multi-cabal/hie.yaml
-                        tests/projects/multi-cabal/multi-cabal.cabal
-                        tests/projects/multi-cabal/src/Lib.hs
-                        tests/projects/multi-cabal-with-project/appA/appA.cabal
-                        tests/projects/multi-cabal-with-project/appA/src/Lib.hs
-                        tests/projects/multi-cabal-with-project/appB/appB.cabal
-                        tests/projects/multi-cabal-with-project/appB/src/Lib.hs
-                        tests/projects/multi-cabal-with-project/cabal.project.9.2.8
-                        tests/projects/multi-cabal-with-project/hie.yaml
-                        tests/projects/monorepo-cabal/cabal.project
-                        tests/projects/monorepo-cabal/hie.yaml
-                        tests/projects/monorepo-cabal/A/Main.hs
-                        tests/projects/monorepo-cabal/A/A.cabal
-                        tests/projects/monorepo-cabal/B/MyLib.hs
-                        tests/projects/monorepo-cabal/B/B.cabal
-                        tests/projects/multi-stack/app/Main.hs
-                        tests/projects/multi-stack/cabal.project
-                        tests/projects/multi-stack/hie.yaml
-                        tests/projects/multi-stack/multi-stack.cabal
-                        tests/projects/multi-stack/src/Lib.hs
-                        tests/projects/failing-bios/A.hs
-                        tests/projects/failing-bios/B.hs
-                        tests/projects/failing-bios/hie.yaml
-                        tests/projects/failing-bios-ghc/A.hs
-                        tests/projects/failing-bios-ghc/B.hs
-                        tests/projects/failing-bios-ghc/hie.yaml
-                        tests/projects/failing-cabal/failing-cabal.cabal
-                        tests/projects/failing-cabal/hie.yaml
-                        tests/projects/failing-cabal/MyLib.hs
-                        tests/projects/failing-stack/failing-stack.cabal
-                        tests/projects/failing-stack/hie.yaml
-                        tests/projects/failing-stack/src/Lib.hs
-                        tests/projects/nested-cabal/nested-cabal.cabal
-                        tests/projects/nested-cabal/cabal.project
-                        tests/projects/nested-cabal/hie.yaml
-                        tests/projects/nested-cabal/MyLib.hs
-                        tests/projects/nested-cabal/sub-comp/sub-comp.cabal
-                        tests/projects/nested-cabal/sub-comp/Lib.hs
-                        tests/projects/nested-stack/nested-stack.cabal
-                        tests/projects/nested-stack/hie.yaml
-                        tests/projects/nested-stack/MyLib.hs
-                        tests/projects/nested-stack/sub-comp/sub-comp.cabal
-                        tests/projects/nested-stack/sub-comp/Lib.hs
-                        tests/projects/simple-bios/A.hs
-                        tests/projects/simple-bios/B.hs
-                        tests/projects/simple-bios/hie-bios.sh
-                        tests/projects/simple-bios/hie-bios-deps.sh
-                        tests/projects/simple-bios/hie.yaml
-                        tests/projects/simple-bios-ghc/A.hs
-                        tests/projects/simple-bios-ghc/B.hs
-                        tests/projects/simple-bios-ghc/hie-bios.sh
-                        tests/projects/simple-bios-ghc/hie.yaml
-                        tests/projects/simple-bios-shell/A.hs
-                        tests/projects/simple-bios-shell/B.hs
-                        tests/projects/simple-bios-shell/hie.yaml
-                        tests/projects/simple-cabal/A.hs
-                        tests/projects/simple-cabal/B.hs
-                        tests/projects/simple-cabal/cabal.project
-                        tests/projects/simple-cabal/hie.yaml
-                        tests/projects/simple-cabal/simple-cabal.cabal
-                        tests/projects/simple-direct/A.hs
-                        tests/projects/simple-direct/B.hs
-                        tests/projects/simple-direct/hie.yaml
-                        tests/projects/simple-stack/A.hs
-                        tests/projects/simple-stack/B.hs
-                        tests/projects/simple-stack/cabal.project
-                        tests/projects/simple-stack/hie.yaml
-                        tests/projects/simple-stack/simple-stack.cabal
-                        "tests/projects/space stack/A.hs"
-                        "tests/projects/space stack/B.hs"
-                        "tests/projects/space stack/hie.yaml"
-                        "tests/projects/space stack/stackproj.cabal"
-                        tests/projects/implicit-cabal/cabal.project
-                        tests/projects/implicit-cabal/implicit-cabal.cabal
-                        tests/projects/implicit-cabal/Main.hs
-                        tests/projects/implicit-cabal-no-project/implicit-cabal-no-project.cabal
-                        tests/projects/implicit-cabal-no-project/Main.hs
-                        tests/projects/implicit-cabal-deep-project/README
-                        tests/projects/implicit-cabal-deep-project/implicit-cabal-deep-project.cabal
-                        tests/projects/implicit-cabal-deep-project/Main.hs
-                        tests/projects/implicit-cabal-deep-project/cabal.project
-                        tests/projects/implicit-cabal-deep-project/foo/foo.cabal
-                        tests/projects/implicit-cabal-deep-project/foo/Main.hs
-                        tests/projects/implicit-stack/implicit-stack.cabal
-                        tests/projects/implicit-stack/Main.hs
-                        tests/projects/implicit-stack-multi/implicit-stack-multi.cabal
-                        tests/projects/implicit-stack-multi/Main.hs
-                        tests/projects/implicit-stack-multi/other-package/other-package.cabal
-                        tests/projects/implicit-stack-multi/other-package/Main.hs
-                        tests/projects/multi-stack-with-yaml/appA/appA.cabal
-                        tests/projects/multi-stack-with-yaml/appA/src/Lib.hs
-                        tests/projects/multi-stack-with-yaml/appB/appB.cabal
-                        tests/projects/multi-stack-with-yaml/appB/src/Lib.hs
-                        tests/projects/multi-stack-with-yaml/hie.yaml
-                        tests/projects/stack-with-yaml/app/Main.hs
-                        tests/projects/stack-with-yaml/hie.yaml
-                        tests/projects/stack-with-yaml/stack-with-yaml.cabal
-                        tests/projects/stack-with-yaml/src/Lib.hs
-                        tests/projects/failing-multi-repl-cabal-project/multi-repl-cabal-fail/app/Main.hs
-                        tests/projects/failing-multi-repl-cabal-project/multi-repl-cabal-fail/multi-repl-cabal-fail.cabal
-                        tests/projects/failing-multi-repl-cabal-project/multi-repl-cabal-fail/src/Fail.hs
-                        tests/projects/failing-multi-repl-cabal-project/multi-repl-cabal-fail/src/Lib.hs
-                        tests/projects/failing-multi-repl-cabal-project/NotInPath.hs
+cabal-version: 2.2
+name: hie-bios
+version: 0.20.0
+author: Matthew Pickering <matthewtpickering@gmail.com>, Hannes Siebenhandl <fendor.haskell@gmail.com>
+maintainer: Hannes Siebenhandl <fendor.haskell@gmail.com>
+license: BSD-3-Clause
+license-file: LICENSE
+homepage: https://github.com/haskell/hie-bios
+synopsis: Set up a GHC API session
+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:
+  README.md
+  tests/configs/*.yaml
+  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
+  tests/projects/cabal-with-ghc-and-project/src/MyLib.hs
+  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/cabal-with-project/cabal-with-project.cabal
+  tests/projects/cabal-with-project/cabal.project.extra
+  tests/projects/cabal-with-project/hie.yaml
+  tests/projects/cabal-with-project/src/MyLib.hs
+  tests/projects/deps-bios-new/A.hs
+  tests/projects/deps-bios-new/B.hs
+  tests/projects/deps-bios-new/hie-bios.sh
+  tests/projects/deps-bios-new/hie.yaml
+  tests/projects/failing-bios-ghc/A.hs
+  tests/projects/failing-bios-ghc/B.hs
+  tests/projects/failing-bios-ghc/hie.yaml
+  tests/projects/failing-bios/A.hs
+  tests/projects/failing-bios/B.hs
+  tests/projects/failing-bios/hie.yaml
+  tests/projects/failing-cabal/failing-cabal.cabal
+  tests/projects/failing-cabal/hie.yaml
+  tests/projects/failing-cabal/MyLib.hs
+  tests/projects/failing-multi-repl-cabal-project/multi-repl-cabal-fail/app/Main.hs
+  tests/projects/failing-multi-repl-cabal-project/multi-repl-cabal-fail/multi-repl-cabal-fail.cabal
+  tests/projects/failing-multi-repl-cabal-project/multi-repl-cabal-fail/src/Fail.hs
+  tests/projects/failing-multi-repl-cabal-project/multi-repl-cabal-fail/src/Lib.hs
+  tests/projects/failing-multi-repl-cabal-project/NotInPath.hs
+  tests/projects/failing-stack/failing-stack.cabal
+  tests/projects/failing-stack/hie.yaml
+  tests/projects/failing-stack/src/Lib.hs
+  tests/projects/implicit-cabal-deep-project/cabal.project
+  tests/projects/implicit-cabal-deep-project/foo/foo.cabal
+  tests/projects/implicit-cabal-deep-project/foo/Main.hs
+  tests/projects/implicit-cabal-deep-project/implicit-cabal-deep-project.cabal
+  tests/projects/implicit-cabal-deep-project/Main.hs
+  tests/projects/implicit-cabal-deep-project/README
+  tests/projects/implicit-cabal-no-project/implicit-cabal-no-project.cabal
+  tests/projects/implicit-cabal-no-project/Main.hs
+  tests/projects/implicit-cabal/cabal.project
+  tests/projects/implicit-cabal/implicit-cabal.cabal
+  tests/projects/implicit-cabal/Main.hs
+  tests/projects/implicit-stack-multi/implicit-stack-multi.cabal
+  tests/projects/implicit-stack-multi/Main.hs
+  tests/projects/implicit-stack-multi/other-package/Main.hs
+  tests/projects/implicit-stack-multi/other-package/other-package.cabal
+  tests/projects/implicit-stack/implicit-stack.cabal
+  tests/projects/implicit-stack/Main.hs
+  tests/projects/monorepo-cabal/A/A.cabal
+  tests/projects/monorepo-cabal/A/Main.hs
+  tests/projects/monorepo-cabal/B/B.cabal
+  tests/projects/monorepo-cabal/B/MyLib.hs
+  tests/projects/monorepo-cabal/cabal.project
+  tests/projects/monorepo-cabal/hie.yaml
+  tests/projects/multi-cabal-with-project/appA/appA.cabal
+  tests/projects/multi-cabal-with-project/appA/src/Lib.hs
+  tests/projects/multi-cabal-with-project/appB/appB.cabal
+  tests/projects/multi-cabal-with-project/appB/src/Lib.hs
+  tests/projects/multi-cabal-with-project/cabal.project.extra
+  tests/projects/multi-cabal-with-project/hie.yaml
+  tests/projects/multi-cabal/app/Main.hs
+  tests/projects/multi-cabal/cabal.project
+  tests/projects/multi-cabal/hie.yaml
+  tests/projects/multi-cabal/multi-cabal.cabal
+  tests/projects/multi-cabal/src/Lib.hs
+  tests/projects/multi-direct/A.hs
+  tests/projects/multi-direct/B.hs
+  tests/projects/multi-direct/hie.yaml
+  tests/projects/multi-stack-with-yaml/appA/appA.cabal
+  tests/projects/multi-stack-with-yaml/appA/src/Lib.hs
+  tests/projects/multi-stack-with-yaml/appB/appB.cabal
+  tests/projects/multi-stack-with-yaml/appB/src/Lib.hs
+  tests/projects/multi-stack-with-yaml/hie.yaml
+  tests/projects/multi-stack/app/Main.hs
+  tests/projects/multi-stack/cabal.project
+  tests/projects/multi-stack/hie.yaml
+  tests/projects/multi-stack/multi-stack.cabal
+  tests/projects/multi-stack/src/Lib.hs
+  tests/projects/nested-cabal/cabal.project
+  tests/projects/nested-cabal/hie.yaml
+  tests/projects/nested-cabal/MyLib.hs
+  tests/projects/nested-cabal/nested-cabal.cabal
+  tests/projects/nested-cabal/sub-comp/Lib.hs
+  tests/projects/nested-cabal/sub-comp/sub-comp.cabal
+  tests/projects/nested-stack/hie.yaml
+  tests/projects/nested-stack/MyLib.hs
+  tests/projects/nested-stack/nested-stack.cabal
+  tests/projects/nested-stack/sub-comp/Lib.hs
+  tests/projects/nested-stack/sub-comp/sub-comp.cabal
+  tests/projects/simple-bios-ghc/A.hs
+  tests/projects/simple-bios-ghc/B.hs
+  tests/projects/simple-bios-ghc/hie-bios.sh
+  tests/projects/simple-bios-ghc/hie.yaml
+  tests/projects/simple-bios-shell/A.hs
+  tests/projects/simple-bios-shell/B.hs
+  tests/projects/simple-bios-shell/hie.yaml
+  tests/projects/simple-bios/A.hs
+  tests/projects/simple-bios/B.hs
+  tests/projects/simple-bios/hie-bios-deps.sh
+  tests/projects/simple-bios/hie-bios.sh
+  tests/projects/simple-bios/hie.yaml
+  tests/projects/simple-cabal/A.hs
+  tests/projects/simple-cabal/B.hs
+  tests/projects/simple-cabal/cabal.project
+  tests/projects/simple-cabal/hie.yaml
+  tests/projects/simple-cabal/simple-cabal.cabal
+  tests/projects/simple-direct/A.hs
+  tests/projects/simple-direct/B.hs
+  tests/projects/simple-direct/hie.yaml
+  tests/projects/simple-stack/A.hs
+  tests/projects/simple-stack/B.hs
+  tests/projects/simple-stack/cabal.project
+  tests/projects/simple-stack/hie.yaml
+  tests/projects/simple-stack/simple-stack.cabal
+  "tests/projects/space stack/A.hs"
+  "tests/projects/space stack/B.hs"
+  "tests/projects/space stack/hie.yaml"
+  "tests/projects/space stack/stackproj.cabal"
+  tests/projects/stack-with-yaml/app/Main.hs
+  tests/projects/stack-with-yaml/hie.yaml
+  tests/projects/stack-with-yaml/src/Lib.hs
+  tests/projects/stack-with-yaml/stack-with-yaml.cabal
+  tests/projects/symlink-test/a/A.hs
+  tests/projects/symlink-test/hie.yaml
+  wrappers/cabal
+  wrappers/cabal-with-repl
+  wrappers/cabal-with-repl.hs
+  wrappers/cabal.hs
 
-tested-with: GHC ==9.4.8 || ==9.6.7 || ==9.8.4 || ==9.10.1 || ==9.12.2
+tested-with: ghc ==9.6.7 || ==9.8.4 || ==9.10.1 || ==9.12.2 || ==9.14.1
 
-Library
-  Default-Language:     Haskell2010
-  GHC-Options:          -Wall
-  HS-Source-Dirs:       src
-  Exposed-Modules:      HIE.Bios
-                        HIE.Bios.Config
-                        HIE.Bios.Config.YAML
-                        HIE.Bios.Cradle
-                        HIE.Bios.Environment
-                        HIE.Bios.Internal.Debug
-                        HIE.Bios.Flags
-                        HIE.Bios.Types
-                        HIE.Bios.Ghc.Api
-                        HIE.Bios.Ghc.Check
-                        HIE.Bios.Ghc.Doc
-                        HIE.Bios.Ghc.Gap
-                        HIE.Bios.Ghc.Load
-                        HIE.Bios.Ghc.Logger
-                        HIE.Bios.Wrappers
-  Other-Modules:        Paths_hie_bios
-  autogen-modules:      Paths_hie_bios
-  Build-Depends:
-                        base                 >= 4.16 && < 5,
-                        aeson                >= 1.4.4 && < 2.3,
-                        base16-bytestring    >= 0.1.1 && < 1.1,
-                        bytestring           >= 0.10.8 && < 0.13,
-                        co-log-core          ^>= 0.3.0,
-                        deepseq              >= 1.4.3 && < 1.6,
-                        exceptions           ^>= 0.10,
-                        cryptohash-sha1      >= 0.11.100 && < 0.12,
-                        directory            >= 1.3.0 && < 1.4,
-                        filepath             >= 1.4.1 && < 1.6,
-                        time                 >= 1.8.0 && < 1.15,
-                        extra                >= 1.6.14 && < 1.9,
-                        prettyprinter        ^>= 1.6 || ^>= 1.7.0,
-                        ghc                  >= 9.2.1 && < 9.13,
-                        transformers         >= 0.5.2 && < 0.7,
-                        temporary            >= 1.2 && < 1.4,
-                        template-haskell     >= 2.18 && <2.24,
-                        text                 >= 1.2.3 && < 2.2,
-                        unix-compat          >= 0.5.1 && < 0.8,
-                        unordered-containers >= 0.2.9 && < 0.3,
-                        yaml                 >= 0.10.0 && < 0.12,
-                        file-embed           >= 0.0.11 && < 1,
-                        conduit              >= 1.3 && < 2,
-                        conduit-extra        >= 1.3 && < 2
+library
+  default-language: Haskell2010
+  ghc-options: -Wall
+  hs-source-dirs: src
+  exposed-modules:
+    HIE.Bios
+    HIE.Bios.Config
+    HIE.Bios.Config.YAML
+    HIE.Bios.Cradle
+    HIE.Bios.Cradle.Cabal
+    HIE.Bios.Cradle.ProgramVersions
+    HIE.Bios.Cradle.ProjectConfig
+    HIE.Bios.Cradle.Resolved
+    HIE.Bios.Cradle.Utils
+    HIE.Bios.Environment
+    HIE.Bios.Flags
+    HIE.Bios.Ghc.Api
+    HIE.Bios.Ghc.Check
+    HIE.Bios.Ghc.Doc
+    HIE.Bios.Ghc.Gap
+    HIE.Bios.Ghc.Load
+    HIE.Bios.Ghc.Logger
+    HIE.Bios.Internal.Debug
+    HIE.Bios.Process
+    HIE.Bios.Types
+    HIE.Bios.Wrappers
 
+  other-modules: Paths_hie_bios
+  autogen-modules: Paths_hie_bios
+  build-depends:
+    aeson >=1.4.4 && <2.4,
+    base >=4.16 && <5,
+    base16-bytestring >=0.1.1 && <1.1,
+    bytestring >=0.10.8 && <0.13,
+    co-log-core ^>=0.3.0,
+    conduit >=1.3 && <2,
+    conduit-extra >=1.3 && <2,
+    containers >=0.6.2 && <0.9,
+    cryptohash-sha1 >=0.11.100 && <0.12,
+    deepseq >=1.4.3 && <1.6,
+    directory >=1.3.0 && <1.4,
+    exceptions ^>=0.10,
+    extra >=1.6.14 && <1.9,
+    file-embed >=0.0.11 && <1,
+    filepath >=1.4.1 && <1.6,
+    ghc >=9.2.1 && <10.2,
+    prettyprinter ^>=1.6 || ^>=1.7.0,
+    template-haskell >=2.18 && <2.25,
+    temporary >=1.2 && <1.4,
+    text >=1.2.3 && <2.2,
+    time >=1.8.0 && <1.17,
+    transformers >=0.5.2 && <0.7,
+    unix-compat >=0.5.1 && <0.8,
+    unordered-containers >=0.2.9 && <0.3,
+    yaml >=0.10.0 && <0.12,
 
-Executable hie-bios
-  Default-Language:     Haskell2010
-  Main-Is:              Main.hs
-  Other-Modules:        Paths_hie_bios
-  autogen-modules:      Paths_hie_bios
-  GHC-Options:          -Wall
-  HS-Source-Dirs:       exe
-  Build-Depends:        base >= 4.16 && < 5
-                      , co-log-core
-                      , directory
-                      , filepath
-                      , hie-bios
-                      , optparse-applicative >= 0.18.1 && < 0.19
-                      , prettyprinter
+executable hie-bios
+  default-language: Haskell2010
+  main-is: Main.hs
+  other-modules: Paths_hie_bios
+  autogen-modules: Paths_hie_bios
+  ghc-options: -Wall
+  hs-source-dirs: exe
+  build-depends:
+    base >=4.16 && <5,
+    co-log-core,
+    directory,
+    filepath,
+    hie-bios,
+    optparse-applicative >=0.17.1 && <0.20,
+    prettyprinter,
 
 test-suite parser-tests
   type: exitcode-stdio-1.0
   default-language: Haskell2010
   build-depends:
-      base,
-      aeson,
-      filepath,
-      hie-bios,
-      tasty,
-      tasty-hunit,
-      yaml
+    aeson,
+    base,
+    filepath,
+    hie-bios,
+    tasty,
+    tasty-hunit,
+    yaml,
 
   hs-source-dirs: tests/
-  ghc-options: -threaded -Wall
+  ghc-options:
+    -threaded
+    -Wall
+
   main-is: ParserTests.hs
 
 test-suite bios-tests
   type: exitcode-stdio-1.0
   default-language: Haskell2010
   build-depends:
-      base,
-      co-log-core,
-      extra,
-      transformers,
-      tasty,
-      tasty-hunit,
-      tasty-expected-failure,
-      hie-bios,
-      filepath,
-      directory,
-      prettyprinter,
-      temporary,
-      text,
-      ghc
+    base,
+    co-log-core,
+    directory,
+    extra,
+    filepath,
+    ghc,
+    hie-bios,
+    prettyprinter,
+    tasty,
+    tasty-expected-failure,
+    tasty-hunit,
+    temporary,
+    text,
+    transformers,
 
   hs-source-dirs: tests/
-  ghc-options: -threaded -Wall
   main-is: BiosTests.hs
   other-modules: Utils
+  ghc-options:
+    -threaded
+    -Wall
 
-Source-Repository head
-  Type:                 git
-  Location:             https://github.com/haskell/hie-bios.git
+  -- There seems to be a race condition on windows
+  if !os(windows)
+    ghc-options:
+      -rtsopts
+      -with-rtsopts=-N
+
+source-repository head
+  type: git
+  location: https://github.com/haskell/hie-bios.git
diff --git a/src/HIE/Bios/Config.hs b/src/HIE/Bios/Config.hs
--- a/src/HIE/Bios/Config.hs
+++ b/src/HIE/Bios/Config.hs
@@ -11,10 +11,12 @@
     pattern CabalType,
     cabalComponent,
     cabalProjectFile,
+    cabalComponentsToLoad,
     StackType,
     pattern StackType,
     stackComponent,
     stackYaml,
+    stackComponentsToLoad,
     CradleTree(..),
     Callable(..)
     ) where
@@ -67,34 +69,42 @@
 -- 'cabal.project'. We allow to override that name to have an HLS specific
 -- project configuration file.
 data CabalType
-    = CabalType_ { _cabalComponent :: !(Last String), _cabalProjectFile :: !(Last FilePath) }
+    = CabalType_
+        { _cabalComponent :: !(Last String)
+        , _cabalProjectFile :: !(Last FilePath)
+        , _cabalComponentsToLoad :: !(Maybe [String])
+        }
     deriving (Eq)
 
 instance Semigroup CabalType where
-    CabalType_ cr cpr <> CabalType_ cl cpl = CabalType_ (cr <> cl) (cpr <> cpl)
+    CabalType_ cr cpr csr <> CabalType_ cl cpl csl = CabalType_ (cr <> cl) (cpr <> cpl) (csr <> csl)
 
 instance Monoid CabalType where
-    mempty = CabalType_ mempty mempty
+    mempty = CabalType_ mempty mempty mempty
 
-pattern CabalType :: Maybe String -> Maybe FilePath -> CabalType
-pattern CabalType { cabalComponent, cabalProjectFile } = CabalType_ (Last cabalComponent) (Last cabalProjectFile)
+pattern CabalType :: Maybe String -> Maybe FilePath -> Maybe [String] -> CabalType
+pattern CabalType { cabalComponent, cabalProjectFile, cabalComponentsToLoad } = CabalType_ (Last cabalComponent) (Last cabalProjectFile) cabalComponentsToLoad
 {-# COMPLETE CabalType #-}
 
 instance Show CabalType where
   show = show . Cabal
 
 data StackType
-    = StackType_ { _stackComponent :: !(Last String) , _stackYaml :: !(Last String) }
+    = StackType_
+        { _stackComponent :: !(Last String)
+        , _stackYaml :: !(Last String)
+        , _stackComponentsToLoad :: !(Maybe [String])
+        }
     deriving (Eq)
 
 instance Semigroup StackType where
-    StackType_ cr yr <> StackType_ cl yl = StackType_ (cr <> cl) (yr <> yl)
+    StackType_ cr yr zr <> StackType_ cl yl zl = StackType_ (cr <> cl) (yr <> yl) (zr <> zl)
 
 instance Monoid StackType where
-    mempty = StackType_ mempty mempty
+    mempty = StackType_ mempty mempty mempty
 
-pattern StackType :: Maybe String -> Maybe FilePath -> StackType
-pattern StackType { stackComponent, stackYaml } = StackType_ (Last stackComponent) (Last stackYaml)
+pattern StackType :: Maybe String -> Maybe FilePath -> Maybe [String] -> StackType
+pattern StackType { stackComponent, stackYaml, stackComponentsToLoad } = StackType_ (Last stackComponent) (Last stackYaml) stackComponentsToLoad
 {-# COMPLETE StackType #-}
 
 instance Show StackType where
@@ -126,7 +136,11 @@
     deriving (Eq, Functor)
 
 instance Show (CradleTree a) where
-    show (Cabal comp) = "Cabal {component = " ++ show (cabalComponent comp) ++ "}"
+    show (Cabal comp) = concat $
+      [ "Cabal { component = ", show (cabalComponent comp)
+      , ", projectFile = ", show (cabalProjectFile comp)
+      , "}"
+      ]
     show (CabalMulti d a) = "CabalMulti {defaultCabal = " ++ show d ++ ", subCabalComponents = " ++ show a ++ "}"
     show (Stack comp) = "Stack {component = " ++ show (stackComponent comp) ++ ", stackYaml = " ++ show (stackYaml comp) ++ "}"
     show (StackMulti d a) = "StackMulti {defaultStack = " ++ show d ++ ", subStackComponents = "  ++ show a ++ "}"
@@ -159,20 +173,21 @@
 toCradleTree :: YAML.CradleComponent a -> CradleTree a
 toCradleTree (YAML.Multi cpts)  =
   Multi $ (\(YAML.MultiSubComponent fp' cfg) -> (fp', cradle $ fromYAMLConfig cfg)) <$> cpts
-toCradleTree (YAML.Stack (YAML.StackConfig yaml cpts)) =
+toCradleTree (YAML.Stack (YAML.StackConfig yaml csToLoad cpts)) =
   case cpts of
-    YAML.NoComponent          -> Stack $ StackType Nothing yaml
-    (YAML.SingleComponent c)  -> Stack $ StackType (Just c) yaml
-    (YAML.ManyComponents cs)  -> StackMulti (StackType Nothing yaml)
+    YAML.NoComponent          -> Stack $ StackType Nothing yaml csToLoad
+    (YAML.SingleComponent c)  -> Stack $ StackType (Just c) yaml csToLoad
+    (YAML.ManyComponents cs)  -> StackMulti (StackType Nothing yaml csToLoad)
                                             ((\(YAML.StackComponent fp' c cYAML) ->
-                                              (fp', StackType (Just c) cYAML)) <$> cs)
-toCradleTree (YAML.Cabal (YAML.CabalConfig prjFile cpts)) =
+                                              (fp', StackType (Just c) cYAML
+                                                    Nothing)) <$> cs)
+toCradleTree (YAML.Cabal (YAML.CabalConfig prjFile csToLoad cpts)) =
   case cpts of
-    YAML.NoComponent          -> Cabal $ CabalType Nothing prjFile
-    (YAML.SingleComponent c)  -> Cabal $ CabalType (Just c) prjFile
-    (YAML.ManyComponents cs)  -> CabalMulti (CabalType Nothing prjFile)
+    YAML.NoComponent          -> Cabal $ CabalType Nothing prjFile csToLoad
+    (YAML.SingleComponent c)  -> Cabal $ CabalType (Just c) prjFile csToLoad
+    (YAML.ManyComponents cs)  -> CabalMulti (CabalType Nothing prjFile csToLoad)
                                             ((\(YAML.CabalComponent fp' c cPrjFile) ->
-                                              (fp', CabalType (Just c) cPrjFile)) <$> cs)
+                                              (fp', CabalType (Just c) cPrjFile Nothing)) <$> cs)
 toCradleTree (YAML.Direct cfg)  = Direct (YAML.arguments cfg)
 toCradleTree (YAML.Bios cfg)    = Bios  (toCallable $ YAML.callable cfg)
                                         (toCallable <$> YAML.depsCallable cfg)
diff --git a/src/HIE/Bios/Config/YAML.hs b/src/HIE/Bios/Config/YAML.hs
--- a/src/HIE/Bios/Config/YAML.hs
+++ b/src/HIE/Bios/Config/YAML.hs
@@ -24,8 +24,9 @@
 import           Control.Applicative ((<|>))
 import           Data.Aeson
 #if MIN_VERSION_aeson(2,0,0)
-import           Data.Aeson.KeyMap   (keys)
+import           Data.Aeson.KeyMap   (keys,lookup)
 #else
+import           Data.HashMap.Strict (lookup)
 import qualified Data.HashMap.Strict as Map
 import qualified Data.Text           as T
 #endif
@@ -33,6 +34,7 @@
 import qualified Data.Char           as C (toLower)
 import           Data.List           ((\\))
 import           GHC.Generics        (Generic)
+import           Prelude             hiding (lookup)
 
 #if !MIN_VERSION_aeson(2,0,0)
 -- | Backwards compatible type-def for Key
@@ -103,16 +105,18 @@
 
 data CabalConfig
   = CabalConfig { cabalProject    :: Maybe FilePath
+                , cabalComponentsToLoad :: Maybe [String]
                 , cabalComponents :: OneOrManyComponents CabalComponent
                 }
 
 instance FromJSON CabalConfig where
-  parseJSON v@(Array _)     = CabalConfig Nothing . ManyComponents <$> parseJSON v
-  parseJSON v@(Object obj)  = (checkObjectKeys ["cabalProject", "component", "components"] obj)
+  parseJSON v@(Array _)     = CabalConfig Nothing Nothing . ManyComponents <$> parseJSON v
+  parseJSON v@(Object obj)  = (checkObjectKeys ["cabalProject", "component", "components", "componentsToLoad"] obj)
                                 *> (CabalConfig
                                   <$> obj .:? "cabalProject"
+                                  <*> obj .:? "componentsToLoad"
                                   <*> parseJSON v)
-  parseJSON Null            = pure $ CabalConfig Nothing NoComponent
+  parseJSON Null            = pure $ CabalConfig Nothing Nothing NoComponent
   parseJSON v               = typeMismatch "CabalConfig" v
 
 data CabalComponent
@@ -133,6 +137,7 @@
 
 data StackConfig
   = StackConfig { stackYaml       :: Maybe FilePath
+                , stackComponentsToLoad :: Maybe [String]
                 , stackComponents :: OneOrManyComponents StackComponent
                 }
 
@@ -143,13 +148,14 @@
                    }
 
 instance FromJSON StackConfig where
-  parseJSON v@(Array _)     = StackConfig Nothing . ManyComponents <$> parseJSON v
-  parseJSON v@(Object obj)  = (checkObjectKeys ["component", "components", "stackYaml"] obj)
+  parseJSON v@(Array _)     = StackConfig Nothing Nothing . ManyComponents <$> parseJSON v
+  parseJSON v@(Object obj)  = (checkObjectKeys ["component", "components", "stackYaml", "componentsToLoad"] obj)
                                 *> (StackConfig
                                       <$> obj .:? "stackYaml"
+                                      <*> obj .:? "componentsToLoad"
                                       <*> parseJSON v
                                     )
-  parseJSON Null            = pure $ StackConfig Nothing NoComponent
+  parseJSON Null            = pure $ StackConfig Nothing Nothing NoComponent
   parseJSON v               = typeMismatch "StackConfig" v
 
 instance FromJSON StackComponent where
@@ -166,12 +172,13 @@
   | ManyComponents [component]
   | NoComponent
 
-instance FromJSON component => FromJSON (OneOrManyComponents component) where
-  parseJSON =
-    let parseComponents o = (parseSingleComponent o <|> parseSubComponents o <|> pure NoComponent)
-        parseSingleComponent o = SingleComponent <$> o .: "component"
-        parseSubComponents   o = ManyComponents <$> o .: "components"
-     in withObject "Components" parseComponents
+instance FromJSON a => FromJSON (OneOrManyComponents a) where
+  parseJSON (Object obj)
+    | Just v <- lookup "component"  obj = SingleComponent <$> parseJSON v
+    | Just v <- lookup "components" obj = ManyComponents <$> parseJSON v
+    | otherwise                         = pure NoComponent
+  parseJSON Null = pure NoComponent
+  parseJSON v    = typeMismatch "OneOrManyComponents" v
 
 data DirectConfig
   = DirectConfig { arguments :: [String] }
diff --git a/src/HIE/Bios/Cradle.hs b/src/HIE/Bios/Cradle.hs
--- a/src/HIE/Bios/Cradle.hs
+++ b/src/HIE/Bios/Cradle.hs
@@ -1,4 +1,3 @@
-{-# LANGUAGE BangPatterns #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TupleSections #-}
@@ -20,70 +19,46 @@
     , isDefaultCradle
     , isOtherCradle
     , getCradle
-    , readProcessWithOutputs
-    , readProcessWithCwd
+    , Process.readProcessWithOutputs
+    , Process.readProcessWithCwd
     , makeCradleResult
     -- | Cradle project configuration types
     , CradleProjectConfig(..)
-
-    -- expose to tests
-    , makeVersions
-    , isCabalMultipleCompSupported
-    , ProgramVersions
   ) where
 
-import Control.Applicative ((<|>), optional)
-import Control.DeepSeq
-import Control.Exception (handleJust)
-import qualified Data.Yaml as Yaml
-import Data.Void
-import Data.Char (isSpace)
-import System.Exit
-import System.Directory hiding (findFile)
 import Colog.Core (LogAction (..), WithSeverity (..), Severity (..), (<&))
+import Control.Applicative ((<|>))
 import Control.Monad
-import Control.Monad.Extra (unlessM)
+import Control.Monad.IO.Class
 import Control.Monad.Trans.Cont
 import Control.Monad.Trans.Maybe
-import Control.Monad.IO.Class
-import Data.Aeson ((.:))
-import qualified Data.Aeson as Aeson
-import qualified Data.Aeson.Types as Aeson
+import qualified Data.Yaml as Yaml
+import Data.Version
+import Data.Void
 import Data.Bifunctor (first)
-import qualified Data.ByteString as BS
 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.HashMap.Strict as Map
-import Data.Maybe (fromMaybe, maybeToList)
+import Data.Maybe (fromMaybe)
 import Data.List
-import Data.List.Extra (trimEnd, nubOrd)
 import Data.Ord (Down(..))
 import qualified Data.Text as T
-import qualified Data.Text.Encoding as T
 import System.Environment
+import System.Exit
 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.Directory
+import System.IO (hClose, hPutStr)
 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.Process as Process
 import qualified HIE.Bios.Types as Types
 import qualified HIE.Bios.Ghc.Gap as Gap
-
-import GHC.Fingerprint (fingerprintString)
-import GHC.ResponseFile (escapeArgs)
-
-import Data.Version
-import Data.IORef
-import Text.ParserCombinators.ReadP (readP_to_S)
-import Data.Tuple.Extra (fst3, snd3, thd3)
+import HIE.Bios.Cradle.ProjectConfig
+import HIE.Bios.Cradle.Utils
+import HIE.Bios.Cradle.Cabal as Cabal
+import HIE.Bios.Cradle.Resolved
+import HIE.Bios.Cradle.ProgramVersions
+import Data.List.Extra (nubOrd)
 
 ----------------------------------------------------------------
 
@@ -120,102 +95,17 @@
 loadCradleWithOpts :: (Yaml.FromJSON b, Show a) => LogAction IO (WithSeverity Log) -> (b -> CradleAction a) -> FilePath -> IO (Cradle a)
 loadCradleWithOpts l buildCustomCradle wfile = do
     cradleConfig <- readCradleConfig wfile
+    l <& WithSeverity (LogAny $ T.pack $ "Cradle Config: " ++ show cradleConfig) Debug
     getCradle l 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
     rcs <- canonicalizeResolvedCradles wdir cs
+    liftIO $ l <& WithSeverity (LogAny . T.pack $ "Resolved Cradles " ++ show ((fmap . fmap) (const ()) rcs)) Debug
     resolvedCradlesToCradle l buildCustomCradle wdir rcs
   where
     cs = resolveCradleTree wdir cc
 
-
--- | The actual type of action we will be using to process a file
-data ConcreteCradle a
-  = ConcreteCabal CabalType
-  | ConcreteStack StackType
-  | ConcreteBios Callable (Maybe Callable) (Maybe FilePath)
-  | ConcreteDirect [String]
-  | ConcreteNone
-  | ConcreteOther a
-  deriving Show
-
-
--- | ConcreteCradle augmented with information on which file the
--- cradle applies
-data ResolvedCradle a
- = ResolvedCradle
- { prefix :: FilePath -- ^ the prefix to match files
- , cradleDeps :: [FilePath] -- ^ accumulated dependencies
- , concreteCradle :: ConcreteCradle a
- } deriving Show
-
--- | The final cradle config that specifies the cradle for
--- each prefix we know how to handle
-data ResolvedCradles a
- = ResolvedCradles
- { cradleRoot :: FilePath
- , resolvedCradles :: [ResolvedCradle a] -- ^ In order of decreasing specificity
- , cradleProgramVersions :: ProgramVersions
- }
-
-data ProgramVersions =
-  ProgramVersions { cabalVersion  :: CachedIO (Maybe Version)
-                  , stackVersion  :: CachedIO (Maybe Version)
-                  , ghcVersion    :: CachedIO (Maybe Version)
-                  }
-
-newtype CachedIO a = CachedIO (IORef (Either (IO a) a))
-
-makeCachedIO :: IO a -> IO (CachedIO a)
-makeCachedIO act = CachedIO <$> newIORef (Left act)
-
-runCachedIO :: CachedIO a -> IO a
-runCachedIO (CachedIO ref) =
-  readIORef ref >>= \case
-    Right x -> pure x
-    Left act -> do
-      x <- act
-      writeIORef ref (Right x)
-      pure x
-
-makeVersions :: LogAction IO (WithSeverity Log) -> FilePath -> ([String] -> IO (CradleLoadResult String)) -> IO ProgramVersions
-makeVersions l wdir ghc = do
-  cabalVersion <- makeCachedIO $ getCabalVersion l wdir
-  stackVersion <- makeCachedIO $ getStackVersion l wdir
-  ghcVersion   <- makeCachedIO $ getGhcVersion ghc
-  pure ProgramVersions{..}
-
-getCabalVersion :: LogAction IO (WithSeverity Log) -> FilePath -> IO (Maybe Version)
-getCabalVersion l wdir = do
-  res <- readProcessWithCwd l wdir "cabal" ["--numeric-version"] ""
-  case res of
-    CradleSuccess stdo ->
-      pure $ versionMaybe stdo
-    _ -> pure Nothing
-
-getStackVersion :: LogAction IO (WithSeverity Log) -> FilePath -> IO (Maybe Version)
-getStackVersion l wdir = do
-  res <- readProcessWithCwd l wdir "stack" ["--numeric-version"] ""
-  case res of
-    CradleSuccess stdo ->
-      pure $ versionMaybe stdo
-    _ -> pure Nothing
-
-getGhcVersion :: ([String] -> IO (CradleLoadResult String)) -> IO (Maybe Version)
-getGhcVersion ghc = do
-  res <- ghc ["--numeric-version"]
-  case res of
-    CradleSuccess stdo ->
-      pure $ versionMaybe stdo
-    _ -> pure Nothing
-
-versionMaybe :: String -> Maybe Version
-versionMaybe xs = case reverse $ readP_to_S parseVersion xs of
-  [] -> Nothing
-  (x:_) -> Just (fst x)
-
-
 addActionDeps :: [FilePath] -> CradleLoadResult ComponentOptions -> CradleLoadResult ComponentOptions
 addActionDeps deps =
   cradleLoadResult
@@ -239,10 +129,11 @@
   versions <- makeVersions logger root run_ghc_cmd
   let rcs = ResolvedCradles root cs versions
       cradleActions = [ (c, resolveCradleAction logger buildCustomCradle rcs root c) | c <- cs ]
-      err_msg fp
+      err_msg (TargetWithContext fp fps)
         = ["Multi Cradle: No prefixes matched"
           , "pwd: " ++ root
           , "filepath: " ++ fp
+          , "context: " ++ intercalate ", " fps
           , "prefixes:"
           ] ++ [show (prefix pf, actionName cc) | (pf, cc) <- cradleActions]
   pure $ Cradle
@@ -250,12 +141,12 @@
     , cradleLogger = logger
     , cradleOptsProg = CradleAction
       { actionName = multiActionName
-      , runCradle  = \fp prev -> do
-          absfp <- makeAbsolute fp
+      , runCradle  = \fpc prev -> do
+          absfp <- makeAbsolute (targetFilePath fpc)
           case selectCradle (prefix . fst) absfp cradleActions of
             Just (rc, act) -> do
-              addActionDeps (cradleDeps rc) <$> runCradle act fp prev
-            Nothing -> return $ CradleFail $ CradleError [] ExitSuccess (err_msg fp) [fp]
+              addActionDeps (cradleDeps rc) <$> runCradle act fpc prev
+            Nothing -> return $ CradleFail $ CradleError [] ExitSuccess (err_msg fpc) (targetFilePath fpc: targetContext fpc)
       , runGhcCmd = run_ghc_cmd
       }
     }
@@ -297,20 +188,20 @@
 
 
 resolveCradleAction :: Show a => LogAction IO (WithSeverity Log) -> (b -> CradleAction a) -> ResolvedCradles b -> FilePath -> ResolvedCradle b -> CradleAction a
-resolveCradleAction l buildCustomCradle cs root cradle = addLoadStyleLogToCradleAction $
+resolveCradleAction l buildCustomCradle cs root cradle = addLoadModeLogToCradleAction $
   case concreteCradle cradle of
     ConcreteCabal t -> cabalCradle l cs root (cabalComponent t) (projectConfigFromMaybe root (cabalProjectFile t))
-    ConcreteStack t -> stackCradle l root (stackComponent t) (projectConfigFromMaybe root (stackYaml t))
-    ConcreteBios bios deps mbGhc -> biosCradle l root bios deps mbGhc
+    ConcreteStack t -> stackCradle l cs root (stackComponent t) (projectConfigFromMaybe root (stackYaml t))
+    ConcreteBios bios deps mbGhc -> biosCradle l cs root bios deps mbGhc
     ConcreteDirect xs -> directCradle l root xs
     ConcreteNone -> noneCradle
     ConcreteOther a -> buildCustomCradle a
   where
     -- Add a log message to each loading operation.
-    addLoadStyleLogToCradleAction crdlAct = crdlAct
-      { runCradle = \fp ls -> do
-          l <& LogRequestedCradleLoadStyle (T.pack $ show $ actionName crdlAct) ls `WithSeverity` Debug
-          runCradle crdlAct fp ls
+    addLoadModeLogToCradleAction crdlAct = crdlAct
+      { runCradle = \fpc ls -> do
+          l <& LogRequestedCradleLoadMode (T.pack $ show $ actionName crdlAct) fpc ls `WithSeverity` Debug
+          runCradle crdlAct fpc ls
       }
 
 resolveCradleTree :: FilePath -> CradleConfig a -> [ResolvedCradle a]
@@ -342,9 +233,9 @@
   where
   maybeItsBios = (\wdir -> (Bios (Program $ wdir </> ".hie-bios") Nothing Nothing, wdir)) <$> biosWorkDir fp
 
-  maybeItsStack = stackExecutable >> (Stack $ StackType Nothing Nothing,) <$> stackWorkDir fp
+  maybeItsStack = stackExecutable >> (Stack $ StackType Nothing Nothing Nothing,) <$> stackWorkDir fp
 
-  maybeItsCabal = (Cabal $ CabalType Nothing Nothing,) <$> cabalWorkDir fp
+  maybeItsCabal = (Cabal $ CabalType Nothing Nothing Nothing,) <$> cabalWorkDir fp
 
   -- maybeItsObelisk = (Obelisk,) <$> obeliskWorkDir fp
 
@@ -364,7 +255,7 @@
   return (configDir </> configFileName)
 
 yamlConfigDirectory :: FilePath -> MaybeT IO FilePath
-yamlConfigDirectory = findFileUpwards (configFileName ==)
+yamlConfigDirectory = Process.findFileUpwards configFileName
 
 readCradleConfig :: Yaml.FromJSON b => FilePath -> IO (CradleConfig b)
 readCradleConfig yamlHie = do
@@ -454,7 +345,7 @@
 
 ---------------------------------------------------------------
 -- | The multi cradle selects a cradle based on the filepath
-
+--
 -- Canonicalize the relative paths present in the multi-cradle and
 -- also order the paths by most specific first. In the cradle selection
 -- function we want to choose the most specific cradle possible.
@@ -463,14 +354,6 @@
   sortOn (Down . prefix)
     <$> mapM (\c -> (\abs_fp -> c {prefix = abs_fp}) <$> makeAbsolute (cur_dir </> prefix c)) cs
 
-selectCradle :: (a -> FilePath) -> FilePath -> [a] -> Maybe a
-selectCradle _ _ [] = Nothing
-selectCradle k cur_fp (c: css) =
-    if k c `isPrefixOf` cur_fp
-      then Just c
-      else selectCradle k cur_fp css
-
-
 -------------------------------------------------------------------------
 
 directCradle :: LogAction IO (WithSeverity Log) -> FilePath -> [String] -> CradleAction a
@@ -478,7 +361,7 @@
   = CradleAction
       { actionName = Types.Direct
       , runCradle = \_ loadStyle -> do
-          logCradleHasNoSupportForLoadWithContext l loadStyle "direct"
+          logCradleHasNoSupportForLoadFileWithContext l loadStyle "direct"
           return (CradleSuccess (ComponentOptions (args ++ argDynamic) wdir []))
       , runGhcCmd = runGhcCmdOnPath l wdir
       }
@@ -489,67 +372,124 @@
 
 -- | Find a cradle by finding an executable `hie-bios` file which will
 -- be executed to find the correct GHC options to use.
-biosCradle :: LogAction IO (WithSeverity Log) -> FilePath -> Callable -> Maybe Callable -> Maybe FilePath -> CradleAction a
-biosCradle l wdir biosCall biosDepsCall mbGhc
+biosCradle :: LogAction IO (WithSeverity Log) -> ResolvedCradles b -> FilePath -> Callable -> Maybe Callable -> Maybe FilePath -> CradleAction a
+biosCradle l rc wdir biosCall biosDepsCall mbGhc
   = CradleAction
       { actionName = Types.Bios
-      , runCradle = biosAction wdir biosCall biosDepsCall l
-      , runGhcCmd = \args -> readProcessWithCwd l wdir (fromMaybe "ghc" mbGhc) args ""
+      , runCradle = biosAction rc wdir biosCall biosDepsCall l
+      , runGhcCmd = \args -> Process.readProcessWithCwd l wdir (fromMaybe "ghc" mbGhc) args ""
       }
 
 biosWorkDir :: FilePath -> MaybeT IO FilePath
-biosWorkDir = findFileUpwards (".hie-bios" ==)
+biosWorkDir = Process.findFileUpwards ".hie-bios"
 
-biosDepsAction :: LogAction IO (WithSeverity Log) -> FilePath -> Maybe Callable -> FilePath -> LoadStyle -> IO [FilePath]
-biosDepsAction l wdir (Just biosDepsCall) fp _prevs = do
-  biosDeps' <- callableToProcess biosDepsCall (Just fp) -- TODO multi pass the previous files too
-  (ex, sout, serr, [(_, args)]) <- readProcessWithOutputs [hie_bios_output] l wdir biosDeps'
+-- | shared between @biosDepsAction@ and @biosAction@.
+biosFilePaths :: TargetWithContext -> LoadMode -> [FilePath]
+biosFilePaths fpc = \case
+  LoadFile -> [targetFilePath fpc]
+  LoadFileWithContext -> targetAndContext fpc
+  -- Exhaustive matching to trigger type errors for datatype changes.
+  LoadUnitsInferred -> targetAndContext fpc
+  LoadUnitsFromCradle -> targetAndContext fpc
+
+biosDepsAction :: LogAction IO (WithSeverity Log) -> FilePath -> Maybe Callable -> TargetWithContext -> LoadMode -> IO [FilePath]
+biosDepsAction l wdir (Just biosDepsCall) fpc loadStyle = do
+
+  let fps = biosFilePaths fpc loadStyle
+
+  (ex, sout, serr, [(_, args)]) <-
+    runContT (withCallableToProcess biosDepsCall fps) $ \biosDeps' ->
+      Process.readProcessWithOutputs [hie_bios_output] l wdir biosDeps'
   case ex of
     ExitFailure _ ->  error $ show (ex, sout, serr)
     ExitSuccess -> return $ fromMaybe [] args
 biosDepsAction _ _ Nothing _ _ = return []
 
 biosAction
-  :: FilePath
+  :: ResolvedCradles a
+  -> FilePath
   -> Callable
   -> Maybe Callable
   -> LogAction IO (WithSeverity Log)
-  -> FilePath
-  -> LoadStyle
+  -> TargetWithContext
+  -> LoadMode
   -> IO (CradleLoadResult ComponentOptions)
-biosAction wdir bios bios_deps l fp loadStyle = do
-  logCradleHasNoSupportForLoadWithContext l loadStyle "bios"
-  bios' <- callableToProcess bios (Just fp) -- TODO pass all the files instead of listToMaybe
+biosAction rc wdir bios bios_deps l fpc loadStyle = do
+  let fp = targetFilePath fpc
+  ghc_version <- liftIO $ runCachedIO $ ghcVersion $ cradleProgramVersions rc
+  let warnUnsupportedLoadMode msg =
+        liftIO $ l <& WithSeverity
+          (LogLoadModeUnsupported "bios" loadStyle (Just msg))
+          Warning
+  determinedLoadMode <- case ghc_version of
+      Just ghc
+        | ghc >= makeVersion [9,4] -> do
+          -- Multi-component supported from ghc 9.4
+          -- We trust the assertion for a bios program, as we have no way of
+          -- checking its version
+          pure $ case loadStyle of
+            LoadFile -> LoadFile
+            LoadFileWithContext -> LoadFileWithContext
+            LoadUnitsFromCradle -> LoadFileWithContext
+            LoadUnitsInferred   -> LoadFileWithContext
+        | loadStyle /= LoadFile -> do
+          warnUnsupportedLoadMode "ghc version is too old. We require `ghc >= 9.4`"
+          pure LoadFile
+      _ -> do
+        warnUnsupportedLoadMode "Unable to determine ghc version. We require `ghc >= 9.4`"
+        pure LoadFile
+
+  let fps = biosFilePaths fpc determinedLoadMode
+
   (ex, _stdo, std, [(_, res),(_, mb_deps)]) <-
-    readProcessWithOutputs [hie_bios_output, hie_bios_deps] l wdir bios'
+    runContT (withCallableToProcess bios fps) $ \bios' ->
+      Process.readProcessWithOutputs [hie_bios_output, hie_bios_deps] l wdir bios'
 
   deps <- case mb_deps of
     Just x  -> return x
-    Nothing -> biosDepsAction l wdir bios_deps fp loadStyle
+    Nothing -> biosDepsAction l wdir bios_deps fpc determinedLoadMode
         -- Output from the program should be written to the output file and
         -- delimited by newlines.
         -- Execute the bios action and add dependencies of the cradle.
         -- Removes all duplicates.
   return $ makeCradleResult (ex, std, wdir, fromMaybe [] res) deps [fp]
 
-callableToProcess :: Callable -> Maybe String -> IO CreateProcess
-callableToProcess (Command shellCommand) file = do
+withCallableToProcess :: Callable -> [String] -> ContT a IO CreateProcess
+withCallableToProcess (Command shellCommand) files = ContT $ \action -> do
   old_env <- getEnvironment
-  return $ (shell shellCommand) { env = (: old_env) . (,) hie_bios_arg <$> file }
-callableToProcess (Program path) file = do
+  case files of
+    [] -> action $ (shell shellCommand) {env = Nothing}
+    (x : _) ->
+      runContT (withHieBiosMultiArg files) $ \multi_file -> do
+        let updated_env = Just $
+              [ (hie_bios_multi_arg, multi_file)
+              , (hie_bios_arg, x)
+              ] ++
+              old_env
+        action $ (shell shellCommand){env = updated_env}
+withCallableToProcess (Program path) files = ContT $ \action -> do
   canon_path <- canonicalizePath path
-  return $ proc canon_path (maybeToList file)
-
-------------------------------------------------------------------------
+  old_env <- getEnvironment
+  case files of
+    [] -> action $ (proc canon_path []){env = Nothing}
+    (x : _) ->
+      runContT (withHieBiosMultiArg files) $ \multi_file -> do
+        let updated_env = Just $
+              (hie_bios_multi_arg, multi_file) : old_env
+        action $ (proc canon_path [x]){env = updated_env}
 
-projectFileProcessArgs :: CradleProjectConfig -> [String]
-projectFileProcessArgs (ExplicitConfig prjFile) = ["--project-file", prjFile]
-projectFileProcessArgs NoExplicitConfig = []
+withHieBiosMultiArg :: [String] -> ContT a IO FilePath
+withHieBiosMultiArg files = ContT $ \action -> do
+  withSystemTempFile hie_bios_multi_arg $ \file h -> do
+    case files of
+      [] -> hClose h >> action file
+      (f0 : rest) -> do
+        hPutStr h f0
+        forM_ rest $ \f -> hPutStr h "\x00" >> hPutStr h f
+        hClose h
+        action file
 
-projectLocationOrDefault :: CradleProjectConfig -> [FilePath]
-projectLocationOrDefault = \case
-  NoExplicitConfig -> ["cabal.project", "cabal.project.local"]
-  (ExplicitConfig prjFile) -> [prjFile, prjFile <.> "local"]
+------------------------------------------------------------------------
 
 -- |Cabal Cradle
 -- Works for new-build by invoking `v2-repl`.
@@ -557,443 +497,14 @@
 cabalCradle l cs wdir mc projectFile
   = CradleAction
     { actionName = Types.Cabal
-    , runCradle = \fp -> runCradleResultT . cabalAction cs wdir mc l projectFile fp
-    , runGhcCmd = \args -> runCradleResultT $ do
-        let vs = cradleProgramVersions cs
-        callCabalPathForCompilerPath l vs wdir projectFile >>= \case
-          Just p -> readProcessWithCwd_ l wdir p args ""
-          Nothing -> 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)
-            liftIO $ createDirectoryIfMissing True (buildDir </> "tmp")
-            -- Need to pass -v0 otherwise we get "resolving dependencies..."
-            cabalProc <- cabalProcess l vs projectFile wdir "v2-exec" $ ["ghc", "-v0", "--"] ++ args
-            readProcessWithCwd' l cabalProc ""
+    , runCradle = \fpc -> runCradleResultT . cabalAction cs wdir mc l projectFile fpc
+    , runGhcCmd = runCabalGhcCmd cs wdir l projectFile
     }
 
-
--- | 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 :: LogAction IO (WithSeverity Log) -> ProgramVersions -> CradleProjectConfig -> FilePath -> String -> [String] -> CradleLoadResultT IO CreateProcess
-cabalProcess l vs cabalProject workDir command args = do
-  (ghcDirs, ghcPkgPath) <- callCabalPathForCompilerPath l vs workDir cabalProject >>= \case
-    Just p -> do
-      libdir <- readProcessWithCwd_ l workDir p ["--print-libdir"] ""
-      pure ((p, trimEnd libdir), Nothing)
-    Nothing -> do
-      ghcDirs@(ghcBin, libdir) <- cabalGhcDirs l cabalProject workDir
-      ghcPkgPath <- liftIO $ withGhcPkgTool ghcBin libdir
-      pure (ghcDirs, Just ghcPkgPath)
-
-  newEnvironment <- liftIO $ setupEnvironment ghcDirs
-  cabalProc <- liftIO $ setupCabalCommand ghcPkgPath
-  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 :: Maybe FilePath -> IO CreateProcess
-    setupCabalCommand ghcPkgPath = do
-      wrapper_fp <- withGhcWrapperTool l ("ghc", []) workDir
-      buildDir <- cabalBuildDir workDir
-      let hcPkgArgs = case ghcPkgPath of
-            Nothing -> []
-            Just p -> ["--with-hc-pkg", p]
-
-          extraCabalArgs =
-            [ "--builddir=" <> buildDir
-            , command
-            , "--with-compiler", wrapper_fp
-            ]
-            <> hcPkgArgs
-            <> projectFileProcessArgs cabalProject
-      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' projectFile rootDir componentDir@.
--- Compute the dependencies of the cabal cradle based
--- on cabal project configuration, the cradle root and the component directory.
---
--- The @projectFile@ and @projectFile <> ".local"@ are always added to the list
--- of dependencies.
---
--- Directory 'componentDir' is a sub-directory where we look for
--- package specific cradle dependencies, such as a '.cabal' file.
---
--- Found dependencies are relative to 'rootDir'.
-cabalCradleDependencies :: CradleProjectConfig -> FilePath -> FilePath -> IO [FilePath]
-cabalCradleDependencies projectFile rootDir componentDir = do
-    let relFp = makeRelative rootDir componentDir
-    cabalFiles' <- findCabalFiles componentDir
-    let cabalFiles = map (relFp </>) cabalFiles'
-    return $ map normalise $ cabalFiles ++ projectLocationOrDefault projectFile
-
--- |Find .cabal files in the given directory.
---
--- Might return multiple results,biosAction as we can not know in advance
--- which one is important to the user.
-findCabalFiles :: FilePath -> IO [FilePath]
-findCabalFiles wdir = do
-  dirContent <- listDirectory wdir
-  return $ filter ((== ".cabal") . takeExtension) dirContent
-
-
-processCabalWrapperArgs :: [String] -> Maybe (FilePath, [String])
-processCabalWrapperArgs args =
-    case args of
-        (dir: ghc_args) ->
-            let final_args =
-                    removeVerbosityOpts
-                    $ removeRTS
-                    $ removeInteractive ghc_args
-            in Just (dir, final_args)
-        _ -> Nothing
-
--- | GHC process information.
--- Consists of the filepath to the ghc executable and
--- arguments to the executable.
-type GhcProc = (FilePath, [String])
-
--- | 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 (mbGhc, ghcArgs) wdir = do
-    let wrapperContents = if isWindows then cabalWrapperHs else cabalWrapper
-        withExtension fp = if isWindows then fp <.> "exe" else fp
-        srcHash = show (fingerprintString wrapperContents)
-    cacheFile (withExtension "wrapper") srcHash $ \wrapper_fp ->
-      if isWindows
-      then
-        withSystemTempDirectory "hie-bios" $ \ tmpDir -> do
-          let wrapper_hs = wrapper_fp -<.> "hs"
-          writeFile wrapper_hs wrapperContents
-          let ghcArgsWithExtras = ghcArgs ++ ["-rtsopts=ignore", "-outputdir", tmpDir, "-o", wrapper_fp, wrapper_hs]
-          let ghcProc = (proc mbGhc ghcArgsWithExtras)
-                      { cwd = Just wdir
-                      }
-          l <& LogCreateProcessRun ghcProc `WithSeverity` Debug
-          readCreateProcess ghcProc "" >>= putStr
-      else writeFile wrapper_fp wrapperContents
-
--- | 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 workDir = do
-  abs_work_dir <- makeAbsolute workDir
-  let dirHash = show (fingerprintString abs_work_dir)
-  getCacheDir ("dist-" <> filter (not . isSpace) (takeBaseName abs_work_dir)<>"-"<>dirHash)
-
--- | 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 :: LogAction IO (WithSeverity Log) -> CradleProjectConfig -> FilePath -> CradleLoadResultT IO (FilePath, FilePath)
-cabalGhcDirs l cabalProject workDir = do
-  libdir <- readProcessWithCwd_ l workDir "cabal"
-      (["exec"] ++
-       projectFileArgs ++
-       ["-v0", "--", "ghc", "--print-libdir"]
-      )
-      ""
-  exe <- readProcessWithCwd_ l workDir "cabal"
-      -- DON'T TOUCH THIS CODE
-      -- This works with 'NoImplicitPrelude', with 'RebindableSyntax' and other shenanigans.
-      -- @-package-env=-@ doesn't work with ghc prior 8.4.x
-      ([ "exec"] ++
-       projectFileArgs ++
-       [ "-v0", "--" , "ghc", "-package-env=-", "-ignore-dot-ghci", "-e"
-       , "Control.Monad.join (Control.Monad.fmap System.IO.putStr System.Environment.getExecutablePath)"
-       ]
-      )
-      ""
-  pure (trimEnd exe, trimEnd libdir)
-  where
-    projectFileArgs = projectFileProcessArgs cabalProject
-
-callCabalPathForCompilerPath :: LogAction IO (WithSeverity Log) -> ProgramVersions -> FilePath -> CradleProjectConfig -> CradleLoadResultT IO (Maybe FilePath)
-callCabalPathForCompilerPath l vs workDir projectFile = do
-  isCabalPathSupported vs >>= \case
-    False -> pure Nothing
-    True -> do
-      let
-        args = ["path", "--output-format=json"] <> projectFileProcessArgs projectFile
-        bs = BS.fromStrict . T.encodeUtf8 . T.pack
-        parse_compiler_path = Aeson.parseEither ((.: "compiler") >=>  (.: "path")) <=< Aeson.eitherDecode
-
-      compiler_info <- readProcessWithCwd_ l workDir "cabal" args ""
-      case parse_compiler_path (bs compiler_info) of
-        Left err -> do
-          liftIO $ l <& WithSeverity (LogCabalPath $ T.pack err) Warning
-          pure Nothing
-        Right a -> pure a
-
-isCabalPathSupported :: MonadIO m => ProgramVersions -> m Bool
-isCabalPathSupported vs = do
-  v <- liftIO $ runCachedIO $ cabalVersion vs
-  pure $ maybe False (>= makeVersion [3,14]) v
-
-isCabalMultipleCompSupported :: MonadIO m => ProgramVersions -> m Bool
-isCabalMultipleCompSupported vs = do
-  cabal_version <- liftIO $ runCachedIO $ cabalVersion vs
-  ghc_version <- liftIO $ runCachedIO $ ghcVersion vs
-  -- determine which load style is supported by this cabal cradle.
-  case (cabal_version, ghc_version) of
-    (Just cabal, Just ghc) -> pure $ ghc >= makeVersion [9, 4] && cabal >= makeVersion [3, 11]
-    _ -> pure False
-
-cabalAction
-  :: ResolvedCradles a
-  -> FilePath
-  -> Maybe String
-  -> LogAction IO (WithSeverity Log)
-  -> CradleProjectConfig
-  -> FilePath
-  -> LoadStyle
-  -> CradleLoadResultT IO ComponentOptions
-cabalAction (ResolvedCradles root cs vs) workDir mc l projectFile fp loadStyle = do
-  multiCompSupport <- isCabalMultipleCompSupported vs
-  -- determine which load style is supported by this cabal cradle.
-  determinedLoadStyle <- case loadStyle of
-    LoadWithContext _ | not multiCompSupport -> do
-      liftIO $
-        l
-          <& WithSeverity
-            ( LogLoadWithContextUnsupported "cabal" $
-                Just "cabal or ghc version is too old. We require `cabal >= 3.11` and `ghc >= 9.4`"
-            )
-            Warning
-      pure LoadFile
-    _ -> pure loadStyle
-
-  let fpModule = fromMaybe (fixTargetPath fp) mc
-  let (cabalArgs, loadingFiles, extraDeps) = case determinedLoadStyle of
-        LoadFile -> ([fpModule], [fp], [])
-        LoadWithContext fps ->
-          let allModulesFpsDeps = ((fpModule, fp, []) : moduleFilesFromSameProject fps)
-              allModules = nubOrd $ fst3 <$> allModulesFpsDeps
-              allFiles = nubOrd $ snd3 <$> allModulesFpsDeps
-              allFpsDeps = nubOrd $ concatMap thd3 allModulesFpsDeps
-           in (["--keep-temp-files", "--enable-multi-repl"] ++ allModules, allFiles, allFpsDeps)
-
-  liftIO $ l <& LogComputedCradleLoadStyle "cabal" determinedLoadStyle `WithSeverity` Info
-  liftIO $ l <& LogCabalLoad fp mc (prefix <$> cs) loadingFiles `WithSeverity` Debug
-
-  let cabalCommand = "v2-repl"
-
-  cabalProc <-
-    cabalProcess l vs projectFile workDir cabalCommand cabalArgs `modCradleError` \err -> do
-      deps <- cabalCradleDependencies projectFile workDir workDir
-      pure $ err {cradleErrorDependencies = cradleErrorDependencies err ++ deps}
-
-  (ex, output, stde, [(_, maybeArgs)]) <- liftIO $ readProcessWithOutputs [hie_bios_output] l workDir cabalProc
-  let args = fromMaybe [] maybeArgs
-
-  let errorDetails =
-        [ "Failed command: " <> prettyCmdSpec (cmdspec cabalProc),
-          unlines output,
-          unlines stde,
-          unlines args,
-          "Process Environment:"
-        ]
-          <> prettyProcessEnv cabalProc
-
-  when (ex /= ExitSuccess) $ do
-    deps <- liftIO $ cabalCradleDependencies projectFile workDir workDir
-    let cmd = show (["cabal", cabalCommand] <> cabalArgs)
-    let errorMsg = "Failed to run " <> cmd <> " in directory \"" <> workDir <> "\". Consult the logs for full command and error."
-    throwCE (CradleError deps ex ([errorMsg] <> errorDetails) loadingFiles)
-
-  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 projectFile workDir workDir
-      throwCE (CradleError (deps <> extraDeps) ex (["Failed to parse result of calling cabal"] <> errorDetails) loadingFiles)
-    Just (componentDir, final_args) -> do
-      deps <- liftIO $ cabalCradleDependencies projectFile workDir componentDir
-      CradleLoadResultT $ pure $ makeCradleResult (ex, stde, componentDir, final_args) (deps <> extraDeps) loadingFiles
-  where
-    -- Need to make relative on Windows, due to a Cabal bug with how it
-    -- parses file targets with a C: drive in it. So we decide to make
-    -- the paths relative to the working directory.
-    fixTargetPath x
-      | isWindows && hasDrive x = makeRelative workDir x
-      | otherwise = x
-    moduleFilesFromSameProject fps =
-      [ (fromMaybe (fixTargetPath file) old_mc, file, deps)
-      | file <- fps,
-        -- Lookup the component for the old file
-        Just (ResolvedCradle {concreteCradle = ConcreteCabal ct, cradleDeps = deps}) <- [selectCradle prefix file cs],
-        -- Only include this file if the old component is in the same project
-        (projectConfigFromMaybe root (cabalProjectFile ct)) == projectFile,
-        let old_mc = cabalComponent ct
-      ]
-
-removeInteractive :: [String] -> [String]
-removeInteractive = filter (/= "--interactive")
-
--- | Strip out any ["+RTS", ..., "-RTS"] sequences in the command string list.
-data InRTS = OutsideRTS | InsideRTS
-
--- | Strip out any ["+RTS", ..., "-RTS"] sequences in the command string list.
---
--- >>> removeRTS ["option1", "+RTS -H32m -RTS", "option2"]
--- ["option1", "option2"]
---
--- >>> removeRTS ["option1", "+RTS", "-H32m", "-RTS", "option2"]
--- ["option1", "option2"]
---
--- >>> removeRTS ["option1", "+RTS -H32m"]
--- ["option1"]
---
--- >>> removeRTS ["option1", "+RTS -H32m", "-RTS", "option2"]
--- ["option1", "option2"]
---
--- >>> removeRTS ["option1", "+RTS -H32m", "-H32m -RTS", "option2"]
--- ["option1", "option2"]
-removeRTS :: [String] -> [String]
-removeRTS = go OutsideRTS
-  where
-    go :: InRTS -> [String] -> [String]
-    go _ [] = []
-    go OutsideRTS (y:ys)
-      | "+RTS" `isPrefixOf` y = go (if "-RTS" `isSuffixOf` y then OutsideRTS else InsideRTS) ys
-      | otherwise = y : go OutsideRTS ys
-    go InsideRTS (y:ys) = go (if "-RTS" `isSuffixOf` y then OutsideRTS else InsideRTS) ys
-
-
-removeVerbosityOpts :: [String] -> [String]
-removeVerbosityOpts = filter ((&&) <$> (/= "-v0") <*> (/= "-w"))
-
-
 cabalWorkDir :: FilePath -> MaybeT IO FilePath
 cabalWorkDir wdir =
-      findFileUpwards (== "cabal.project") wdir
-  <|> findFileUpwards (\fp -> takeExtension fp == ".cabal") wdir
-
-
-------------------------------------------------------------------------
-
--- | Explicit data-type for project configuration location.
--- It is basically a 'Maybe' type, but helps to document the API
--- and helps to avoid incorrect usage.
-data CradleProjectConfig
-  = NoExplicitConfig
-  | ExplicitConfig FilePath
-  deriving (Eq, Show)
-
--- | Create an explicit project configuration. Expects a working directory
--- followed by an optional name of the project configuration.
-projectConfigFromMaybe :: FilePath -> Maybe FilePath -> CradleProjectConfig
-projectConfigFromMaybe _wdir Nothing = NoExplicitConfig
-projectConfigFromMaybe wdir (Just fp) = ExplicitConfig (wdir </> fp)
+      Process.findFileUpwards "cabal.project" wdir
+  <|> Process.findFileUpwardsPredicate (\fp -> takeExtension fp == ".cabal") wdir
 
 ------------------------------------------------------------------------
 
@@ -1007,16 +518,16 @@
 
 -- | Stack Cradle
 -- Works for by invoking `stack repl` with a wrapper script
-stackCradle :: LogAction IO (WithSeverity Log) ->  FilePath -> Maybe String -> CradleProjectConfig -> CradleAction a
-stackCradle l wdir mc syaml =
+stackCradle :: LogAction IO (WithSeverity Log) -> ResolvedCradles b -> FilePath -> Maybe String -> CradleProjectConfig -> CradleAction a
+stackCradle l cs wdir mc syaml =
   CradleAction
     { actionName = Types.Stack
-    , runCradle = stackAction wdir mc syaml l
+    , runCradle = stackAction wdir mc syaml l cs
     , 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
-        _ <- readProcessWithCwd_ l wdir "stack" (stackYamlProcessArgs syaml <> ["setup", "--silent"]) ""
-        readProcessWithCwd_ l wdir "stack"
+        _ <- Process.readProcessWithCwd_ l wdir "stack" (stackYamlProcessArgs syaml <> ["setup", "--silent"]) ""
+        Process.readProcessWithCwd_ l wdir "stack"
           (stackYamlProcessArgs syaml <> ["exec", "ghc", "--"] <> args)
           ""
     }
@@ -1043,22 +554,43 @@
   -> Maybe String
   -> CradleProjectConfig
   -> LogAction IO (WithSeverity Log)
-  -> FilePath
-  -> LoadStyle
+  -> ResolvedCradles a
+  -> TargetWithContext
+  -> LoadMode
   -> IO (CradleLoadResult ComponentOptions)
-stackAction workDir mc syaml l fp loadStyle = do
-  logCradleHasNoSupportForLoadWithContext l loadStyle "stack"
-  let ghcProcArgs = ("stack", stackYamlProcessArgs syaml <> ["exec", "ghc", "--"])
+stackAction workDir mc syaml l cs fpc loadStyle = do
+  let fp = targetFilePath fpc
+  logCradleHasNoSupportForLoadFileWithContext l loadStyle "stack"
+  let ghcProc args = proc "stack" (stackYamlProcessArgs syaml <> ["exec", "ghc", "--"] <> args)
   -- Same wrapper works as with cabal
-  wrapper_fp <- withGhcWrapperTool l ghcProcArgs workDir
+  wrapper_fp <- withGhcWrapperTool l ghcProc workDir
+  let
+    fallback = [ comp | Just comp <- [mc] ]
+    componentsToLoad = nubOrd <$> mconcat
+      [ comps
+      | ResolvedCradle{concreteCradle = ConcreteStack
+          (StackType {stackComponentsToLoad = comps})} <- resolvedCradles cs ]
+    allComponents = [ comp
+      | ResolvedCradle{concreteCradle = ConcreteStack
+          (StackType {stackComponent = Just comp})} <- resolvedCradles cs ]
+    -- for stack we do not include --test --bench to avoid triggering stack repl limitations.
+    inferred = []
+    components = case loadStyle of
+      LoadUnitsFromCradle
+        | Just comps <- componentsToLoad -> comps
+        | null allComponents -> inferred
+        | otherwise -> allComponents
+      LoadUnitsInferred -> inferred
+      LoadFile -> fallback
+      LoadFileWithContext -> fallback
   (ex1, _stdo, stde, [(_, maybeArgs)]) <-
-    readProcessWithOutputs [hie_bios_output] l workDir
+    Process.readProcessWithOutputs [hie_bios_output] l workDir
       $ stackProcess syaml
           $  ["repl", "--no-nix-pure", "--with-ghc", wrapper_fp]
-          <> [ comp | Just comp <- [mc] ]
+             <> components
 
   (ex2, pkg_args, stdr, _) <-
-    readProcessWithOutputs [hie_bios_output] l workDir
+    Process.readProcessWithOutputs [hie_bios_output] l workDir
       $ stackProcess syaml ["path", "--ghc-package-path"]
 
   let split_pkgs = concatMap splitSearchPath pkg_args
@@ -1100,9 +632,7 @@
 stackExecutable = MaybeT $ findExecutable "stack"
 
 stackWorkDir :: FilePath -> MaybeT IO FilePath
-stackWorkDir = findFileUpwards isStack
-  where
-    isStack name = name == "stack.yaml"
+stackWorkDir = Process.findFileUpwards "stack.yaml"
 
 {-
 -- Support removed for 0.3 but should be added back in the future
@@ -1112,7 +642,7 @@
 --
 rulesHaskellWorkDir :: FilePath -> MaybeT IO FilePath
 rulesHaskellWorkDir fp =
-  findFileUpwards (== "WORKSPACE") fp
+  findFileUpwards "WORKSPACE" fp
 
 rulesHaskellCradle :: FilePath -> Cradle
 rulesHaskellCradle wdir =
@@ -1178,106 +708,7 @@
   return (makeCradleResult (ex, stde, words args) o_deps )
 
 -}
-------------------------------------------------------------------------------
--- Utilities
 
-
--- | Searches upwards for the first directory containing a file to match
--- the predicate.
-findFileUpwards :: (FilePath -> Bool) -> FilePath -> MaybeT IO FilePath
-findFileUpwards p dir = do
-  cnts <-
-    liftIO
-    $ handleJust
-        -- Catch permission errors
-        (\(e :: IOError) -> if isPermissionError e then Just [] else Nothing)
-        pure
-        (findFile p dir)
-
-  case cnts of
-    [] | dir' == dir -> fail "No cabal files"
-            | otherwise   -> findFileUpwards p dir'
-    _ : _ -> return dir
-  where dir' = takeDirectory dir
-
--- | Sees if any file in the directory matches the predicate
-findFile :: (FilePath -> Bool) -> FilePath -> IO [FilePath]
-findFile p dir = do
-  b <- doesDirectoryExist dir
-  if b then getFiles >>= filterM doesPredFileExist else return []
-  where
-    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.
--- Unset them from the environment to sidestep this
-getCleanEnvironment :: IO [(String, String)]
-getCleanEnvironment = do
-  Map.toList . Map.delete "GHC_PACKAGE_PATH" . Map.fromList <$> getEnvironment
-
-type Outputs = [OutputName]
-type OutputName = String
-
--- | Call a given process with temp files for the process to write to.
--- * The process can discover the temp files paths by reading the environment.
--- * The contents of the temp files are returned by this function, if any.
--- * The logging function is called every time the process emits anything to stdout or stderr.
--- it can be used to report progress of the process to a user.
--- * The process is executed in the given directory.
-readProcessWithOutputs
-  :: Outputs  -- ^ Names of the outputs produced by this process
-  -> LogAction IO (WithSeverity Log) -- ^ Output of the process is emitted as logs.
-  -> 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 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 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 (\msg -> l <& LogProcessOutput msg `WithSeverity` Debug) C..| C.sinkList
-  liftIO $ l <& LogCreateProcessRun process `WithSeverity` Info
-  (ex, stdo, stde) <- liftIO $ sourceProcessWithStreams process mempty loggingConduit loggingConduit
-
-  res <- forM output_files $ \(name,path) ->
-          liftIO $ (name,) <$> readOutput path
-
-  return (ex, stdo, stde, res)
-
-    where
-      readOutput :: FilePath -> IO (Maybe [String])
-      readOutput path = do
-        haveFile <- doesFileExist path
-        if haveFile
-          then withFile path ReadMode $ \handle -> do
-            hSetBuffering handle LineBuffering
-            !res <- force <$> hGetContents handle
-            return $ Just $ lines $ filter (/= '\r') res
-          else
-            return Nothing
-
-      withOutput :: [(String,String)] -> OutputName -> ContT a IO (OutputName, String)
-      withOutput env' name =
-        case lookup name env' of
-          Just file@(_:_) -> ContT $ \action -> do
-            removeFileIfExists file
-            action (name, file)
-          _ -> ContT $ \action -> withSystemTempFile name $ \ file h -> do
-            hClose h
-            removeFileIfExists file
-            action (name, file)
-
-removeFileIfExists :: FilePath -> IO ()
-removeFileIfExists f = do
-  yes <- doesFileExist f
-  when yes (removeFile f)
-
 makeCradleResult :: (ExitCode, [String], FilePath, [String]) -> [FilePath] -> [FilePath] -> CradleLoadResult ComponentOptions
 makeCradleResult (ex, err, componentDir, gopts) deps loadingFiles =
   case ex of
@@ -1288,46 +719,15 @@
 
 -- | Calls @ghc --print-libdir@, with just whatever's on the PATH.
 runGhcCmdOnPath :: LogAction IO (WithSeverity Log) -> FilePath -> [String] -> IO (CradleLoadResult String)
-runGhcCmdOnPath l wdir args = readProcessWithCwd l wdir "ghc" args ""
-  -- case mResult of
-  --   Nothing
-
--- | Wrapper around 'readCreateProcess' that sets the working directory and
--- clears the environment, suitable for invoking cabal/stack and raw ghc commands.
-readProcessWithCwd :: LogAction IO (WithSeverity Log) -> FilePath -> FilePath -> [String] -> String -> IO (CradleLoadResult String)
-readProcessWithCwd l dir cmd args stdin = runCradleResultT $ readProcessWithCwd_ l dir cmd args stdin
-
-readProcessWithCwd_ :: LogAction IO (WithSeverity Log) -> FilePath -> FilePath -> [String] -> String -> CradleLoadResultT IO String
-readProcessWithCwd_ l dir cmd args stdin = do
-  cleanEnv <- liftIO getCleanEnvironment
-  let createdProc' = (proc cmd args) { cwd = Just dir, env = Just cleanEnv }
-  readProcessWithCwd' l createdProc' stdin
-
--- | Wrapper around 'readCreateProcessWithExitCode', wrapping the result in
--- a 'CradleLoadResult'. Provides better error messages than raw 'readCreateProcess'.
-readProcessWithCwd' :: LogAction IO (WithSeverity Log) -> CreateProcess -> String -> CradleLoadResultT IO String
-readProcessWithCwd' l createdProcess stdin = do
-  mResult <- liftIO $ optional $ readCreateProcessWithExitCode createdProcess stdin
-  liftIO $ l <& LogCreateProcessRun createdProcess `WithSeverity` Debug
-  let cmdString = prettyCmdSpec $ cmdspec createdProcess
-  case mResult of
-    Just (ExitSuccess, stdo, _) -> pure stdo
-    Just (exitCode, stdo, stde) -> throwCE $
-      CradleError [] exitCode
-        (["Error when calling " <> cmdString, stdo, stde] <> prettyProcessEnv createdProcess)
-        []
-    Nothing -> throwCE $
-      CradleError [] ExitSuccess
-        (["Couldn't execute " <> cmdString] <> prettyProcessEnv createdProcess)
-        []
+runGhcCmdOnPath l wdir args = Process.readProcessWithCwd l wdir "ghc" args ""
 
 -- | Log that the cradle has no supported for loading with context, if and only if
--- 'LoadWithContext' was requested.
-logCradleHasNoSupportForLoadWithContext :: Applicative m => LogAction m (WithSeverity Log) -> LoadStyle -> T.Text -> m ()
-logCradleHasNoSupportForLoadWithContext l (LoadWithContext _) crdlName =
+-- 'LoadFileWithContext' was requested.
+logCradleHasNoSupportForLoadFileWithContext :: Applicative m => LogAction m (WithSeverity Log) -> LoadMode -> T.Text -> m ()
+logCradleHasNoSupportForLoadFileWithContext l LoadFileWithContext crdlName =
   l <& WithSeverity
-        (LogLoadWithContextUnsupported crdlName
+        (LogLoadModeUnsupported crdlName LoadFileWithContext
           $ Just $ crdlName <> " doesn't support loading multiple components at once"
         )
         Info
-logCradleHasNoSupportForLoadWithContext _ _ _ = pure ()
+logCradleHasNoSupportForLoadFileWithContext _ _ _ = pure ()
diff --git a/src/HIE/Bios/Cradle/Cabal.hs b/src/HIE/Bios/Cradle/Cabal.hs
new file mode 100644
--- /dev/null
+++ b/src/HIE/Bios/Cradle/Cabal.hs
@@ -0,0 +1,783 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+module HIE.Bios.Cradle.Cabal
+  (
+  -- * Cabal Cradle interface
+  cabalAction,
+  runCabalGhcCmd,
+  -- * Locations
+  findCabalFiles,
+  -- * Wrappers
+  withGhcWrapperTool,
+  -- * Argument processing
+  processCabalWrapperArgs,
+  -- * Internals (exposed for tests)
+  isCabalMultipleCompSupported,
+  cabalBuildDir,
+  )
+  where
+
+import Data.Char (isSpace)
+import System.Exit
+import System.Directory
+import Colog.Core (LogAction (..), WithSeverity (..), Severity (..), (<&))
+import Control.Monad
+import Control.Monad.Extra (concatMapM)
+import Control.Monad.IO.Class
+import Data.Aeson ((.:))
+import qualified Data.Aeson as Aeson
+import qualified Data.Aeson.Types as Aeson
+import qualified Data.ByteString as BS
+import Data.Conduit.Process
+import Data.Maybe (fromMaybe)
+import Data.List
+import Data.List.Extra (trimEnd, nubOrd)
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+import System.FilePath
+import System.Info.Extra (isWindows)
+import System.IO.Temp
+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
+import HIE.Bios.Cradle.ProjectConfig
+import HIE.Bios.Cradle.Utils
+import HIE.Bios.Cradle.ProgramVersions
+import HIE.Bios.Cradle.Resolved
+import HIE.Bios.Process
+
+import GHC.Fingerprint (fingerprintString)
+import GHC.ResponseFile (escapeArgs)
+
+{- Note [Finding ghc-options with cabal]
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+We want to know how to compile a cabal component with GHC.
+There are two main ways to obtain the ghc-options:
+
+1. `cabal --with-ghc <ghc-shim>` (for exe:cabal <3.15 or lib:Cabal <3.15)
+
+In this approach, we generate a <ghc-shim> which is passed to the exe:cabal process.
+If a package needs to be compiled, we compile the package with the same GHC process that
+exe:cabal would have used.
+
+If the first argument is `--interactive`, then we do not launch the GHCi process,
+but record all the arguments for later processing.
+
+2. `cabal --with-repl <repl-shim>` (for exe:cabal >=3.15 and lib:Cabal >=3.15)
+
+The <repl-shim> is notably simpler than the <ghc-shim>, as `--with-repl` invokes
+<repl-shim> *only* as the final GHCi process, not for compiling dependencies or
+executing preprocessors.
+
+Thus, <repl-shim> merely needs to log all arguments that are passed to <repl-shim>.
+
+This is the simpler, more maintainable approach, with fewer unintended side-effects.
+
+=== Finding the GHC process cabal uses to compile a project with
+
+We want HLS and hie-bios to honour the `with-compiler` field in `cabal.project` files.
+Again, we identify two ways to find the exact GHC program that is going to be invoked by cabal.
+
+1. `cabal exec -- ghc --interactive -e System.Environment.getExecutablePath` (for exe:cabal <3.14)
+
+Ignoring a couple of details, we can get the path to the raw executable by asking
+the GHCi process for its executable path.
+The issue is that on linux, the executable path is insufficient, the GHC executable
+invoked by the user is "wrapped" in a shim that specifies the libdir location, e.g.:
+
+    > cat /home/hugin/.ghcup/bin/ghc
+    #!/bin/sh
+    exedir="/home/hugin/.ghcup/ghc/9.6.7/lib/ghc-9.6.7/bin"
+    exeprog="./ghc-9.6.7"
+    executablename="/home/hugin/.ghcup/ghc/9.6.7/lib/ghc-9.6.7/bin/./ghc-9.6.7"
+    bindir="/home/hugin/.ghcup/ghc/9.6.7/bin"
+    libdir="/home/hugin/.ghcup/ghc/9.6.7/lib/ghc-9.6.7/lib"
+    docdir="/home/hugin/.ghcup/ghc/9.6.7/share/doc/ghc-9.6.7"
+    includedir="/home/hugin/.ghcup/ghc/9.6.7/include"
+
+    exec "$executablename" -B"$libdir" ${1+"$@"}
+
+We find the libdir by asking GHC via `cabal exec -- ghc --print-libdir`.
+Once we have these two paths, we also need to find the `ghc-pkg` location,
+otherwise cabal will use the `ghc-pkg` that is found on PATH, which is not correct
+if the user overwrites the compiler field via `with-compiler`.
+
+To find `ghc-pkg`, we assume it is going to be located next to the `libdir`, and then
+reconstruct the wrapper shim for `ghc-pkg`.
+
+Then we reconstructed both the ghc and ghc-pkg program that is going to be used by cabal
+and can use it in the <ghc-shim> and `cabal repl --with-compiler <ghc-shim> --with-hc-pkg <hc-pkg-shim>`.
+
+Calling `cabal exec` can be very slow on a large codebase, over 1 second per invocation.
+
+2. `cabal path` (for exe:cabal >= 3.14)
+
+We can skip the reconstruction of the GHC shim by using the output of `cabal path --compiler-info`.
+This gives us the location of the GHC executable shim, so we don't need to reconstruct any shims.
+
+However, we still have to reconstruct the ghc-pkg shim when using `cabal repl --with-compiler`.
+
+`cabal path` is incredibly fast to invoke, as it circumvents running the cabal solver.
+It is easier to maintain as well.
+-}
+
+-- | Main entry point into the cabal cradle invocation.
+--
+-- This function does a lot of work, supporting multiple cabal-install versions and
+-- different ways of obtaining the component options.
+--
+-- See Note [Finding ghc-options with cabal] for a detailed elaboration.
+cabalAction ::
+  ResolvedCradles a ->
+  FilePath ->
+  Maybe String ->
+  LogAction IO (WithSeverity Log) ->
+  CradleProjectConfig ->
+  TargetWithContext ->
+  LoadMode ->
+  CradleLoadResultT IO ComponentOptions
+cabalAction cradles workDir mc l projectFile fp loadStyle = do
+  let progVersions = cradleProgramVersions cradles
+  multiCompSupport <- isCabalMultipleCompSupported progVersions
+  -- determine which load style is supported by this cabal cradle.
+  determinedLoadMode <- case loadStyle of
+    LoadFile -> pure LoadFile
+    -- all other modes need multi component support.
+    _ | not multiCompSupport -> do
+      liftIO $
+        l
+          <& WithSeverity
+            ( LogLoadModeUnsupported "cabal" loadStyle $
+                Just "cabal or ghc version is too old. We require `cabal >= 3.11` and `ghc >= 9.4`"
+            )
+            Warning
+      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
+
+  mResult <- runCabalToGetGhcOptions cabalProc mkFallbackCabalProc
+  case mResult of
+    Left (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.
+      deps <- liftIO $ cabalCradleDependencies projectFile workDir workDir
+      let cmd = prettyCmdSpec (cmdspec cabalProc)
+      let errorMsg = "Failed to run " <> cmd <> " in directory \"" <> workDir <> "\". Consult the logs for full command and error."
+      throwCE CradleError
+        { cradleErrorDependencies = nubOrd (deps <> extraDeps)
+        , cradleErrorExitCode = ExitFailure code
+        , cradleErrorStderr = [errorMsg] <> prettyProcessErrorDetails errorDetails
+        , cradleErrorLoadingFiles = loadingFiles
+        }
+    Right (args, errorDetails) -> do
+      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 projectFile workDir workDir
+          throwCE CradleError
+            { cradleErrorDependencies = nubOrd (deps <> extraDeps)
+            , cradleErrorExitCode = ExitSuccess
+            , cradleErrorStderr = ["Failed to parse result of calling cabal"] <> prettyProcessErrorDetails errorDetails
+            , cradleErrorLoadingFiles = loadingFiles
+            }
+        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
+          CradleLoadResultT $ pure $ CradleSuccess
+            ComponentOptions
+              { componentOptions = final_args
+              , componentRoot = componentDir
+              , 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.
+    runCabalToGetGhcOptions ::
+      Process.CreateProcess ->
+      CradleLoadResultT IO Process.CreateProcess ->
+      CradleLoadResultT IO
+        (Either
+          (Int, ProcessErrorDetails)
+          ([String], ProcessErrorDetails)
+        )
+    runCabalToGetGhcOptions cabalProc mkFallbackCabalProc = do
+      (ex, output, stde, [(_, maybeArgs)]) <- liftIO $ Process.readProcessWithOutputs [hie_bios_output] l workDir cabalProc
+      let args = fromMaybe [] maybeArgs
+      let errorDetails = ProcessErrorDetails
+            { processCmd = cmdspec cabalProc
+            , processStdout = output
+            , processStderr = stde
+            , processGhcOptions = args
+            , 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)
+        ExitSuccess ->
+          pure $ Right (args, errorDetails)
+
+
+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
+    Just p -> Process.readProcessWithCwd_ l wdir p args ""
+    Nothing -> 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)
+      liftIO $ createDirectoryIfMissing True (buildDir </> "tmp")
+      -- Need to pass -v0 otherwise we get "resolving dependencies..."
+      cabalProc <- cabalExecGhc l 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
+  (cabalArgs, loadingFiles, extraDeps) <- case loadStyle of
+        LoadFile -> pure ([fpModule], [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
+
+  liftIO $ l <& LogComputedCradleLoadMode "cabal" fpc loadStyle `WithSeverity` Info
+  liftIO $ l <& LogCabalLoad fp mc (prefix <$> resolvedCradles cradles) loadingFiles `WithSeverity` Debug
+  pure (cabalArgs, loadingFiles, extraDeps)
+  where
+    fpModule = fromMaybe (fixTargetPath fp) mc
+    fp = targetFilePath fpc
+    -- Need to make relative on Windows, due to a Cabal bug with how it
+    -- parses file targets with a C: drive in it. So we decide to make
+    -- the paths relative to the working directory.
+    fixTargetPath x
+      | isWindows && hasDrive x = makeRelative workDir x
+      | otherwise = x
+    -- Return (moduleTarget,file) pairs for each context file, plus a merged dependency list across all of them.
+    moduleFilesFromSameProject :: MonadIO m => [FilePath] -> m ([(FilePath, FilePath)], [FilePath])
+    moduleFilesFromSameProject fps = do
+      -- First, select eligible files and collect their componentDir and YAML deps
+      let selected =
+            [ (file, depsYaml, prefix rc, cabalComponent ct)
+            | file <- fps
+            , Just rc@(ResolvedCradle {concreteCradle = ConcreteCabal ct, cradleDeps = depsYaml}) <- [selectCradle prefix file (resolvedCradles cradles)]
+            , (projectConfigFromMaybe (cradleRoot cradles) (cabalProjectFile ct)) == projectFile
+            ]
+      -- Compute dynamic deps
+      let compDirs = nubOrd $ takeDirectory fp : [ takeDirectory f | (f, _, _, _) <- selected ]
+      dynDeps <- concatMapM (liftIO . cabalCradleDependenciesEnclosing projectFile (cradleRoot cradles)) compDirs
+      -- Combine YAML deps with dynamic deps
+      let mergedDeps = nubOrd $ dynDeps ++ concat [ depsYaml | (_, depsYaml, _ , _) <- selected ]
+      let modPairs = [ (fromMaybe (fixTargetPath file) old_mc, file)
+                     | (file, _, _, old_mc) <- selected ]
+      pure (modPairs, mergedDeps)
+
+    cabalComponentsFromSameProject :: MonadIO m => Maybe [String] -> m ([String],[FilePath])
+    cabalComponentsFromSameProject mToLoad = do
+
+      let selected =
+            [ (comp, prefix rc, depsYaml)
+            | rc@(ResolvedCradle {concreteCradle = ConcreteCabal ct, cradleDeps = depsYaml}) <- (resolvedCradles cradles)
+            , Just comp <- [cabalComponent ct]
+            , maybe True (comp `elem`) mToLoad
+            , (projectConfigFromMaybe (cradleRoot cradles) (cabalProjectFile ct)) == projectFile
+            ]
+      let compDirs = nubOrd $ [dir | (_,dir,_) <- selected]
+      dynDeps <- concatMapM (liftIO . cabalCradleDependenciesEnclosing projectFile (cradleRoot cradles)) compDirs
+      let mergedDeps = nubOrd $ dynDeps ++ concat [ depsYaml | (_, _, depsYaml) <- selected ]
+      pure ([comp | (comp,_,_) <- selected], mergedDeps)
+
+    loadUnits whichUnits0 = do
+      let
+        componentsToLoad = do
+          guard (whichUnits0 == FromCradle)
+          nubOrd <$> mconcat
+            [ cs
+            | ResolvedCradle{concreteCradle = ConcreteCabal
+                (CabalType {cabalComponentsToLoad = cs})} <- resolvedCradles cradles ]
+
+      -- compDeps includes the .cabal files of the specified components (if the prefixes are there and accurate)
+      -- These are used as dependencies for both Inferred and FromCradle modes.
+      -- They might not be accurate for `Inferred`, but we have no easy way to query the cabal project for the complete set.
+      (cradleComponents,compDeps) <- cabalComponentsFromSameProject componentsToLoad
+      let
+        whichUnits
+          | Just units <- componentsToLoad
+                                  = (whichUnits0, units)
+          -- Note we default to Inferred only if componentsToLoad is not declared.
+          | null cradleComponents = (Inferred   , [])
+          | otherwise             = (whichUnits0, cradleComponents)
+
+      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 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)
+
+cabalLoadFilesWithRepl :: LogAction IO (WithSeverity Log) -> CradleProjectConfig -> FilePath -> [String] -> CradleLoadResultT IO CreateProcess
+cabalLoadFilesWithRepl l projectFile workDir args = do
+  buildDir <- liftIO $ cabalBuildDir workDir
+  newEnvironment <- liftIO Process.getCleanEnvironment
+  wrapper_fp <- liftIO $ withReplWrapperTool l (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
+  pure $
+    (proc "cabal" cabalArgs)
+      { env = Just newEnvironment
+      , cwd = Just workDir
+      }
+
+-- | @'cabalCradleDependencies' projectFile rootDir componentDir@.
+-- Compute the dependencies of the cabal cradle based
+-- on cabal project configuration, the cradle root and the component directory.
+--
+-- The @projectFile@ and @projectFile <> ".local"@ are always added to the list
+-- of dependencies.
+--
+-- Directory 'componentDir' is a sub-directory where we look for
+-- package specific cradle dependencies, such as a '.cabal' file.
+--
+-- Found dependencies are relative to 'rootDir'.
+cabalCradleDependencies :: CradleProjectConfig -> FilePath -> FilePath -> IO [FilePath]
+cabalCradleDependencies projectFile rootDir componentDir = do
+    let relFp = makeRelative rootDir componentDir
+    cabalFiles' <- findCabalFiles componentDir
+    let cabalFiles = map (relFp </>) cabalFiles'
+    return $ map normalise $ cabalFiles ++ projectLocationOrDefault projectFile
+
+-- | @'cabalCradleDependenciesEnclosing' projectFile rootDir startDir@.
+-- Find cradle dependency files by walking upwards from a starting directory.
+--
+-- This is similar to 'cabalCradleDependencies', but instead of looking only in a
+-- specific component directory, it searches for @.cabal@ files in @startDir@ and
+-- its ancestor directories, stopping at (and not traversing above) @rootDir@ or
+-- the filesystem root. All discovered @.cabal@ files are returned relative to
+-- @rootDir@, along with the cabal project configuration files determined by
+-- 'projectLocationOrDefault'.
+--
+-- Inputs
+-- - projectFile: the cradle's cabal project configuration (explicit file or default).
+-- - rootDir: absolute cradle root; used as the boundary for the upward search and
+--   to relativize returned paths.
+-- - startDir: directory from which to begin searching for enclosing @.cabal@ files.
+--
+-- Output
+-- - A list of normalised, relative file paths that should be watched to trigger
+--   a reload when changed.
+cabalCradleDependenciesEnclosing :: CradleProjectConfig -> FilePath -> FilePath -> IO [FilePath]
+cabalCradleDependenciesEnclosing projectFile rootDir fp = do
+    cabalFiles' <- findCabalFilesEnclosing fp
+    let relCabalFiles = map (makeRelative rootDir) cabalFiles'
+    return $ map normalise $ relCabalFiles ++ projectLocationOrDefault projectFile
+    where
+      -- find the cabal file upwards from fp to rootDir
+      findCabalFilesEnclosing :: FilePath -> IO [FilePath]
+      findCabalFilesEnclosing dir = do
+            cfs <- map (dir </>) <$> findCabalFiles dir
+            if not (null cfs)
+              then return cfs
+              else
+                let parentDir = takeDirectory dir
+                in if parentDir == dir || length parentDir < length rootDir
+                   then return []
+                   else findCabalFilesEnclosing parentDir
+
+processCabalWrapperArgs :: [String] -> Maybe (FilePath, [String])
+processCabalWrapperArgs args =
+    case args of
+        (dir: ghc_args) ->
+            let final_args =
+                    removeVerbosityOpts
+                    $ removeRTS
+                    $ removeInteractive ghc_args
+            in Just (dir, final_args)
+        _ -> Nothing
+
+-- ----------------------------------------------------------------------------
+-- Legacy cabal commands to obtain ghc-options.
+-- These commands are obsolete in the latest cabal version, but we still support
+-- them.
+-- ----------------------------------------------------------------------------
+
+cabalLoadFilesBefore315 :: LogAction IO (WithSeverity Log) -> ProgramVersions -> CradleProjectConfig -> [Char] -> [String] -> CradleLoadResultT IO CreateProcess
+cabalLoadFilesBefore315 l 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
+    deps <- cabalCradleDependencies projectFile workDir workDir
+    pure $ err {cradleErrorDependencies = cradleErrorDependencies err ++ deps}
+
+-- | 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 :: 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
+    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
+  newEnvironment <- liftIO $ setupEnvironment ghcDirs
+  cabalProc <- liftIO $ setupCabalCommand ghcPkgPath
+  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 <- Process.getCleanEnvironment
+      pure $ processEnvironment ghcDirs ++ environment
+
+    setupCabalCommand :: FilePath -> IO CreateProcess
+    setupCabalCommand ghcPkgPath = do
+      wrapper_fp <- withGhcWrapperTool l (proc "ghc") workDir
+      buildDir <- cabalBuildDir workDir
+      let extraCabalArgs =
+            [ "--builddir=" <> buildDir
+            , command
+            , "--with-compiler", wrapper_fp
+            , "--with-hc-pkg", ghcPkgPath
+            ]
+            <> projectFileProcessArgs cabalProject
+      pure $ proc "cabal" (extraCabalArgs ++ args)
+
+-- | 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 :: LogAction IO (WithSeverity Log) -> CradleProjectConfig -> FilePath -> CradleLoadResultT IO (FilePath, FilePath)
+cabalGhcDirs l cabalProject workDir = do
+  libdir <- Process.readProcessWithCwd_ l workDir "cabal"
+      (["exec"] ++
+       projectFileArgs ++
+       ["-v0", "--", "ghc", "--print-libdir"]
+      )
+      ""
+  exe <- Process.readProcessWithCwd_ l workDir "cabal"
+      -- DON'T TOUCH THIS CODE
+      -- This works with 'NoImplicitPrelude', with 'RebindableSyntax' and other shenanigans.
+      -- @-package-env=-@ doesn't work with ghc prior 8.4.x
+      ([ "exec"] ++
+       projectFileArgs ++
+       [ "-v0", "--" , "ghc", "-package-env=-", "-ignore-dot-ghci", "-e"
+       , "Control.Monad.join (Control.Monad.fmap System.IO.putStr System.Environment.getExecutablePath)"
+       ]
+      )
+      ""
+  pure (trimEnd exe, trimEnd libdir)
+  where
+    projectFileArgs = projectFileProcessArgs cabalProject
+
+-- | 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 withGhcPkgShim 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'.
+    withGhcPkgShim 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]
+
+-- ----------------------------------------------------------------------------
+-- Wrapper Tools
+-- ----------------------------------------------------------------------------
+
+-- | GHC process that accepts GHC arguments.
+type GhcProc = [String] -> CreateProcess
+
+-- | 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
+
+-- | 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
+-- a haskell source file and ad-hoc compile it with 'GhcProc'.
+--
+-- '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
+
+withWrapperTool :: LogAction IO (WithSeverity Log) -> GhcProc -> String -> FilePath -> String -> String -> IO FilePath
+withWrapperTool l 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 ->
+    if isWindows
+    then
+      withSystemTempDirectory "hie-bios" $ \ tmpDir -> do
+        let wrapper_hs = wrapper_fp -<.> "hs"
+        writeFile wrapper_hs wrapperContents
+        let ghcArgs = ["-rtsopts=ignore", "-outputdir", tmpDir, "-o", wrapper_fp, wrapper_hs]
+        let ghcProc = (mkGhcCall ghcArgs)
+                    { cwd = Just wdir
+                    }
+        l <& LogCreateProcessRun ghcProc `WithSeverity` Debug
+        readCreateProcess ghcProc "" >>= putStr
+    else writeFile wrapper_fp wrapperContents
+
+-- ----------------------------------------------------------------------------
+-- 'cabal.project' options
+-- ----------------------------------------------------------------------------
+
+projectFileProcessArgs :: CradleProjectConfig -> [String]
+projectFileProcessArgs (ExplicitConfig prjFile) = ["--project-file", prjFile]
+projectFileProcessArgs NoExplicitConfig = []
+
+projectLocationOrDefault :: CradleProjectConfig -> [FilePath]
+projectLocationOrDefault = \case
+  NoExplicitConfig -> ["cabal.project", "cabal.project.local"]
+  (ExplicitConfig prjFile) -> [prjFile, prjFile <.> "local"]
+
+-- ----------------------------------------------------------------------------
+-- 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
+  abs_work_dir <- makeAbsolute workDir
+  let dirHash = show (fingerprintString abs_work_dir)
+  getCacheDir ("dist-" <> filter (not . isSpace) (takeBaseName abs_work_dir)<>"-"<>dirHash)
+
+-- |Find .cabal files in the given directory.
+--
+-- Might return multiple results,biosAction as we can not know in advance
+-- which one is important to the user.
+findCabalFiles :: FilePath -> IO [FilePath]
+findCabalFiles wdir = do
+  dirContent <- listDirectory wdir
+  return $ filter ((== ".cabal") . takeExtension) dirContent
+
+-- ----------------------------------------------------------------------------
+-- 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
+
+callCabalPathForCompilerPath :: LogAction IO (WithSeverity Log) -> ProgramVersions -> FilePath -> CradleProjectConfig -> CradleLoadResultT IO (Maybe FilePath)
+callCabalPathForCompilerPath l vs workDir projectFile = do
+  isCabalPathSupported vs >>= \case
+    False -> pure Nothing
+    True -> do
+      buildDir <- liftIO $ cabalBuildDir workDir
+      let
+        args = [ "--builddir=" <> buildDir, "path", "--output-format=json" ]
+            <> projectFileProcessArgs projectFile
+        bs = BS.fromStrict . T.encodeUtf8 . T.pack
+        parse_compiler_path = Aeson.parseEither ((.: "compiler") >=>  (.: "path")) <=< Aeson.eitherDecode
+
+      compiler_info <- Process.readProcessWithCwd_ l workDir "cabal" args ""
+      case parse_compiler_path (bs compiler_info) of
+        Left err -> do
+          liftIO $ l <& WithSeverity (LogCabalPath $ T.pack err) Warning
+          pure Nothing
+        Right a -> pure a
+
+-- ----------------------------------------------------------------------------
+-- Version and cabal capability checks
+-- ----------------------------------------------------------------------------
+
+data CabalLoadFeature
+  = CabalWithRepl
+  | CabalWithGhcShimWrapper
+
+determineCabalLoadFeature :: MonadIO m => ProgramVersions -> m CabalLoadFeature
+determineCabalLoadFeature 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] -> pure CabalWithRepl
+      | otherwise -> pure CabalWithGhcShimWrapper
+    _ -> pure CabalWithGhcShimWrapper
+
+-- | As `cabal repl` started to hit maximum cli invocation length, we changed how repl arguments are
+-- passed to GHC. In `cabal 3.15`, `cabal 3.16.0.0`, `cabal 3.17` and onwards, ghc options are
+-- passed to GHC via response files.
+--
+-- This breaks HLS release binary distributions before 2.12, as neither HLS nor hie-bios are capable of handling
+-- response files at the top-level.
+--
+-- In particular, `cabal 3.16.0.0` was released with this change and no HLS bindist before 2.12 works
+-- with `cabal 3.16.0.0`.
+-- To make `cabal 3.16.*` series compatible with released HLS binaries, we reverted the response file
+-- change for the ghc options. This change will apply once `cabal 3.16.1.*` is released.
+-- Note, in `cabal 3.17`, i.e. cabal HEAD, we still pass the arguments via response files and will do that for the
+-- `cabal 3.18` release.
+--
+-- So, we have a weird matrix now, between some commit in `cabal 3.15` and `cabal 3.16`, ghc arguments
+-- are supplied encoded in a response file, while in `>= cabal 3.16.1`, the arguments are passed verbatim.
+-- 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 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
+
+
+-- | 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:
+--
+-- @
+--   Error: [Cabal-7107]
+--   Could not resolve dependencies:
+--   [__0] trying: cabal-with-custom-setup-0.1.0.0 (user goal)
+--   [__1] next goal: cabal-with-custom-setup:setup.Cabal (dependency of cabal-with-custom-setup)
+--   [__1] rejecting: cabal-with-custom-setup:setup.Cabal; 3.10.3.0/installed-3.10.3.0, ... (constraint from --with-repl requires >=3.15)
+--   ...
+-- @
+--
+-- We do a quick and dirty string comparison to check whether the error message looks like it has been caused
+-- 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
+
+isCabalPathSupported :: MonadIO m => ProgramVersions -> m Bool
+isCabalPathSupported vs = do
+  v <- liftIO $ runCachedIO $ cabalVersion vs
+  pure $ maybe False (>= makeVersion [3,14]) v
+
+isCabalMultipleCompSupported :: MonadIO m => ProgramVersions -> m Bool
+isCabalMultipleCompSupported vs = do
+  cabal_version <- liftIO $ runCachedIO $ cabalVersion vs
+  ghc_version <- liftIO $ runCachedIO $ ghcVersion vs
+  -- determine which load style is supported by this cabal cradle.
+  case (cabal_version, ghc_version) of
+    (Just cabal, Just ghc) -> pure $ ghc >= makeVersion [9, 4] && cabal >= makeVersion [3, 11]
+    _ -> pure False
diff --git a/src/HIE/Bios/Cradle/ProgramVersions.hs b/src/HIE/Bios/Cradle/ProgramVersions.hs
new file mode 100644
--- /dev/null
+++ b/src/HIE/Bios/Cradle/ProgramVersions.hs
@@ -0,0 +1,72 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE RecordWildCards #-}
+module HIE.Bios.Cradle.ProgramVersions
+  ( ProgramVersions(..)
+  , makeVersions
+  , runCachedIO
+  ) where
+
+
+import HIE.Bios.Types
+import qualified HIE.Bios.Process as Process
+
+import Colog.Core (LogAction (..), WithSeverity (..))
+import Data.Version
+import Data.IORef
+import Text.ParserCombinators.ReadP (readP_to_S)
+
+data ProgramVersions =
+  ProgramVersions { cabalVersion  :: CachedIO (Maybe Version)
+                  , stackVersion  :: CachedIO (Maybe Version)
+                  , ghcVersion    :: CachedIO (Maybe Version)
+                  }
+
+newtype CachedIO a = CachedIO (IORef (Either (IO a) a))
+
+makeCachedIO :: IO a -> IO (CachedIO a)
+makeCachedIO act = CachedIO <$> newIORef (Left act)
+
+runCachedIO :: CachedIO a -> IO a
+runCachedIO (CachedIO ref) =
+  readIORef ref >>= \case
+    Right x -> pure x
+    Left act -> do
+      x <- act
+      writeIORef ref (Right x)
+      pure x
+
+makeVersions :: LogAction IO (WithSeverity Log) -> FilePath -> ([String] -> IO (CradleLoadResult String)) -> IO ProgramVersions
+makeVersions l wdir ghc = do
+  cabalVersion <- makeCachedIO $ getCabalVersion l wdir
+  stackVersion <- makeCachedIO $ getStackVersion l wdir
+  ghcVersion   <- makeCachedIO $ getGhcVersion ghc
+  pure ProgramVersions{..}
+
+getCabalVersion :: LogAction IO (WithSeverity Log) -> FilePath -> IO (Maybe Version)
+getCabalVersion l wdir = do
+  res <- Process.readProcessWithCwd l wdir "cabal" ["--numeric-version"] ""
+  case res of
+    CradleSuccess stdo ->
+      pure $ versionMaybe stdo
+    _ -> pure Nothing
+
+getStackVersion :: LogAction IO (WithSeverity Log) -> FilePath -> IO (Maybe Version)
+getStackVersion l wdir = do
+  res <- Process.readProcessWithCwd l wdir "stack" ["--numeric-version"] ""
+  case res of
+    CradleSuccess stdo ->
+      pure $ versionMaybe stdo
+    _ -> pure Nothing
+
+getGhcVersion :: ([String] -> IO (CradleLoadResult String)) -> IO (Maybe Version)
+getGhcVersion ghc = do
+  res <- ghc ["--numeric-version"]
+  case res of
+    CradleSuccess stdo ->
+      pure $ versionMaybe stdo
+    _ -> pure Nothing
+
+versionMaybe :: String -> Maybe Version
+versionMaybe xs = case reverse $ readP_to_S parseVersion xs of
+  [] -> Nothing
+  (x:_) -> Just (fst x)
diff --git a/src/HIE/Bios/Cradle/ProjectConfig.hs b/src/HIE/Bios/Cradle/ProjectConfig.hs
new file mode 100644
--- /dev/null
+++ b/src/HIE/Bios/Cradle/ProjectConfig.hs
@@ -0,0 +1,17 @@
+module HIE.Bios.Cradle.ProjectConfig where
+
+import System.FilePath
+
+-- | Explicit data-type for project configuration location.
+-- It is basically a 'Maybe' type, but helps to document the API
+-- and helps to avoid incorrect usage.
+data CradleProjectConfig
+  = NoExplicitConfig
+  | ExplicitConfig FilePath
+  deriving (Eq, Show)
+
+-- | Create an explicit project configuration. Expects a working directory
+-- followed by an optional name of the project configuration.
+projectConfigFromMaybe :: FilePath -> Maybe FilePath -> CradleProjectConfig
+projectConfigFromMaybe _wdir Nothing = NoExplicitConfig
+projectConfigFromMaybe wdir (Just fp) = ExplicitConfig (wdir </> fp)
diff --git a/src/HIE/Bios/Cradle/Resolved.hs b/src/HIE/Bios/Cradle/Resolved.hs
new file mode 100644
--- /dev/null
+++ b/src/HIE/Bios/Cradle/Resolved.hs
@@ -0,0 +1,35 @@
+{-# LANGUAGE DeriveFunctor #-}
+module HIE.Bios.Cradle.Resolved
+  ( ResolvedCradles(..)
+  , ResolvedCradle(..)
+  , ConcreteCradle(..)
+  ) where
+
+import HIE.Bios.Cradle.ProgramVersions
+import HIE.Bios.Config
+
+-- | The final cradle config that specifies the cradle for
+-- each prefix we know how to handle
+data ResolvedCradles a = ResolvedCradles
+ { cradleRoot :: FilePath
+ , resolvedCradles :: [ResolvedCradle a] -- ^ In order of decreasing specificity
+ , cradleProgramVersions :: ProgramVersions
+ }
+
+-- | 'ConcreteCradle' augmented with information on which file the
+-- cradle applies
+data ResolvedCradle a = ResolvedCradle
+ { prefix :: FilePath -- ^ the prefix to match files
+ , cradleDeps :: [FilePath] -- ^ accumulated dependencies
+ , concreteCradle :: ConcreteCradle a
+ } deriving (Show, Functor)
+
+-- | The actual type of action we will be using to process a file
+data ConcreteCradle a
+  = ConcreteCabal CabalType
+  | ConcreteStack StackType
+  | ConcreteBios Callable (Maybe Callable) (Maybe FilePath)
+  | ConcreteDirect [String]
+  | ConcreteNone
+  | ConcreteOther a
+  deriving (Show, Functor)
diff --git a/src/HIE/Bios/Cradle/Utils.hs b/src/HIE/Bios/Cradle/Utils.hs
new file mode 100644
--- /dev/null
+++ b/src/HIE/Bios/Cradle/Utils.hs
@@ -0,0 +1,79 @@
+module HIE.Bios.Cradle.Utils
+  (
+  -- * Helper for process errors
+    ProcessErrorDetails(..)
+  , prettyProcessErrorDetails
+  -- * Cradle utils
+  , selectCradle
+  -- * Processing of ghc-options
+  , removeInteractive
+  , removeRTS
+  , removeVerbosityOpts
+  , expandGhcOptionResponseFile
+  )
+  where
+
+import HIE.Bios.Types (prettyCmdSpec)
+
+import Data.List
+import System.Process.Extra
+import GHC.ResponseFile (expandResponse)
+import HIE.Bios.Ghc.Gap (removeRTS)
+
+-- ----------------------------------------------------------------------------
+-- Process error details
+-- ----------------------------------------------------------------------------
+
+data ProcessErrorDetails = ProcessErrorDetails
+  { processCmd :: CmdSpec
+  -- ^ The 'CmdSpec' of the command.
+  , processStdout :: [String]
+  -- ^ The stdout of the command.
+  , processStderr :: [String]
+  -- ^ The stderr of the command.
+  , processGhcOptions :: [String]
+  -- ^ The ghc-options that were obtained via the command
+  , processHieBiosEnvironment :: [(String, String)]
+  -- ^ Environment variables populated by 'hie-bios' and their respective value.
+  }
+
+prettyProcessErrorDetails :: ProcessErrorDetails -> [String]
+prettyProcessErrorDetails p  =
+  [ "Failed command: " <> prettyCmdSpec (processCmd p),
+    unlines (processStdout p),
+    unlines (processStderr p),
+    unlines (processGhcOptions p),
+    "Process Environment:"
+  ] <> [ key <> ": " <> value
+  | (key, value) <- processHieBiosEnvironment p
+  ]
+
+-- ----------------------------------------------------------------------------
+-- Cradle utils
+-- ----------------------------------------------------------------------------
+
+-- | Given a list of cradles, try to find the most likely cradle that
+-- this 'FilePath' belongs to.
+-- TODO: this finds the first cradle that matches, while hie-bios README says most-specific cradle would match, unless the inputs are assumed sorted by specificity?
+selectCradle :: (a -> FilePath) -> FilePath -> [a] -> Maybe a
+selectCradle _ _ [] = Nothing
+selectCradle k cur_fp (c: css) =
+    if k c `isPrefixOf` cur_fp
+      then Just c
+      else selectCradle k cur_fp css
+
+
+-- ----------------------------------------------------------------------------
+-- Cradle utils
+-- ----------------------------------------------------------------------------
+
+removeInteractive :: [String] -> [String]
+removeInteractive = filter (/= "--interactive")
+
+removeVerbosityOpts :: [String] -> [String]
+removeVerbosityOpts = filter ((&&) <$> (/= "-v0") <*> (/= "-w"))
+
+expandGhcOptionResponseFile :: [String] -> IO [String]
+expandGhcOptionResponseFile args = do
+  expanded_args <- expandResponse args
+  pure $ removeInteractive expanded_args
diff --git a/src/HIE/Bios/Environment.hs b/src/HIE/Bios/Environment.hs
--- a/src/HIE/Bios/Environment.hs
+++ b/src/HIE/Bios/Environment.hs
@@ -1,5 +1,6 @@
 {-# LANGUAGE RecordWildCards, CPP #-}
-module HIE.Bios.Environment (initSession, getRuntimeGhcLibDir, getRuntimeGhcVersion, makeDynFlagsAbsolute, makeTargetsAbsolute, getCacheDir, addCmdOpts) where
+{-# LANGUAGE TupleSections #-}
+module HIE.Bios.Environment (initSession, initSession', getRuntimeGhcLibDir, getRuntimeGhcVersion, makeDynFlagsAbsolute, makeTargetsAbsolute, getCacheDir, addCmdOpts, extractUnits) where
 
 import GHC (GhcMonad)
 import qualified GHC as G
@@ -21,6 +22,14 @@
 
 import HIE.Bios.Types
 import qualified HIE.Bios.Ghc.Gap as Gap
+import qualified System.OsPath as OsPath
+import qualified GHC.Plugins as G
+import qualified Data.List.NonEmpty as NE
+import qualified GHC.Driver.Monad as G
+import Data.IORef (newIORef, readIORef)
+import qualified GHC.Unit.Env as G
+import GHC.Driver.Env (HscEnv(hsc_unit_env))
+import Data.Maybe (fromMaybe)
 
 -- | Start a GHC session and set some sensible options for tooling to use.
 -- Creates a folder in the cache directory to cache interface files to make
@@ -28,27 +37,83 @@
 initSession :: (GhcMonad m)
     => ComponentOptions
     -> m [G.Target]
-initSession  ComponentOptions {..} = do
-    df <- G.getSessionDynFlags
+initSession = initSession' False
+
+initSession' :: (GhcMonad m)
+    => Bool
+    -> ComponentOptions
+    -> m [G.Target]
+initSession' workAroundThreadUnsafety 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 opts_hash = B.unpack $ encode $ H.finalize $ H.updates H.init (map B.pack componentOptions)
-    cache_dir <- liftIO $ getCacheDir opts_hash
-    -- Add the user specified options to a fresh GHC session.
-    (df', targets) <- addCmdOpts componentOptions df
-    let df'' = makeDynFlagsAbsolute componentRoot df'
-    void $ G.setSessionDynFlags
-        (disableOptimisation -- Compile with -O0 as we are not going to produce object files.
-        $ setIgnoreInterfacePragmas            -- Ignore any non-essential information in interface files such as unfoldings changing.
-        $ writeInterfaceFiles (Just cache_dir) -- Write interface files to the cache
-        $ setVerbosity 0                       -- Set verbosity to zero just in case the user specified `-vx` in the options.
-        $ Gap.setWayDynamicIfHostIsDynamic     -- Add dynamic way if GHC is built with dynamic linking
-        $ setLinkerOptions df''                -- Set `-fno-code` to avoid generating object files, unless we have to.
-        )
+    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 targets' = makeTargetsAbsolute componentRoot targets
+    cache_dir <- liftIO $ makeAbsolute =<< getCacheDir opts_hash
+
+    -- Plan:
+    -- - Extract `-unit @resp_file` options if present
+    -- - Add the user specified options to a fresh GHC session
+    -- - Make all dflags absolute
+    -- - Set flags for interactive session
+    -- - Reconstruct targets and make them absolute
+
+    let (units,rest) = extractUnits componentOptions
+
+    let liftGhc m = do
+          env <- G.getSession
+          s <- liftIO (newIORef env)
+          x <- liftIO $ G.reflectGhc m (G.Session s)
+          G.setSession =<< liftIO (readIORef s)
+          pure x
+    let
+      setFlags df'' =
+        void $ G.setSessionDynFlags
+          (disableOptimisation -- Compile with -O0 as we are not going to produce object files.
+          $ setIgnoreInterfacePragmas            -- Ignore any non-essential information in interface files such as unfoldings changing.
+          $ writeInterfaceFiles (Just cache_dir) -- Write interface files to the cache
+          $ setVerbosity 0                       -- Set verbosity to zero just in case the user specified `-vx` in the options.
+          $ Gap.setWayDynamicIfHostIsDynamic     -- Add dynamic way if GHC is built with dynamic linking
+          $ setLinkerOptions df''                -- Set `-fno-code` to avoid generating object files, unless we have to.
+          )
+
+    targets <- case NE.nonEmpty units of
+      Nothing -> do
+        (df',targets) <- addCmdOpts componentOptions =<< G.getSessionDynFlags
+        setFlags $ makeDynFlagsAbsolute componentRoot df'
+        pure targets
+      Just ne_units -> do
+        logger <- Gap.getLogger <$> G.getSession
+        df <- G.getSessionDynFlags
+        -- leftovers won't have targets when units are present.
+        (df', _leftovers, _warns) <- Gap.parseDynamicFlags logger df (map G.noLoc rest)
+        setFlags $ makeDynFlagsAbsolute componentRoot df'
+
+        hs_srcs <- liftGhc $ Gap.initMulti ne_units (\ _ _ _ _ -> return ())
+
+        G.modifySession $ G.hscUpdateHUG $ fmap (\ ue -> ue {G.homeUnitEnv_dflags = makeDynFlagsAbsolute componentRoot (G.homeUnitEnv_dflags ue)})
+
+        -- This should be an enforced invariant.
+        G.modifySession $ \ hsc_env -> G.hscSetActiveUnitId (G.ue_current_unit $ G.hsc_unit_env hsc_env) hsc_env
+
+        mapM (\(src, uid, phase) -> G.guessTarget src uid phase) hs_srcs
+
+    let mkTgtAbs env tgt = tgt {G.targetId = makeTargetIdAbsolute wdir (G.targetId tgt)}
+          where
+            wdir = fromMaybe componentRoot $
+              G.workingDirectory
+              . G.homeUnitEnv_dflags . G.ue_findHomeUnitEnv uid
+              $ hsc_unit_env env
+            uid = G.targetUnitId tgt
+
+    env <- G.getSession
+    let targets' = map (mkTgtAbs env) targets
+
     -- Unset the default log action to avoid output going to stdout.
     Gap.unsetLogAction
+
     return targets'
 
 ----------------------------------------------------------------
@@ -133,7 +198,15 @@
 setHiDir :: FilePath -> G.DynFlags -> G.DynFlags
 setHiDir f d = d { G.hiDir      = Just f}
 
+extractUnits :: [String] -> ([String], [String])
+extractUnits = go [] []
+  where
+    -- TODO: we should likely use the 'processCmdLineP' instead
+    go units rest ("-unit" : x : xs) = go (x : units) rest xs
+    go units rest (x : xs)           = go units (x : rest) xs
+    go units rest []                 = (reverse units, reverse rest)
 
+
 -- | Interpret and set the specific command line options.
 -- A lot of this code is just copied from ghc/Main.hs
 -- It would be good to move this code into a library module so we can just use it
@@ -161,16 +234,21 @@
   $ df
     { G.importPaths = map makeAbs (G.importPaths df)
     , G.packageDBFlags =
-        map (Gap.overPkgDbRef makeAbs) (G.packageDBFlags df)
+        map (Gap.overPkgDbRef makeAbsOs) (G.packageDBFlags df)
+    , G.workingDirectory = (root </>) <$> G.workingDirectory df
     }
   where
     makeAbs =
-#if __GLASGOW_HASKELL__ >= 903
       case G.workingDirectory df of
         Just fp -> ((root </> fp) </>)
         Nothing ->
-#endif
           (root </>)
+
+    makeAbsOs p =
+      case G.workingDirectory df of
+        Just fp -> Gap.unsafeEncodeUtf (root </> fp) OsPath.</> p
+        Nothing ->
+          Gap.unsafeEncodeUtf root OsPath.</> p
 
 -- --------------------------------------------------------
 
diff --git a/src/HIE/Bios/Flags.hs b/src/HIE/Bios/Flags.hs
--- a/src/HIE/Bios/Flags.hs
+++ b/src/HIE/Bios/Flags.hs
@@ -6,11 +6,9 @@
 
 -- | Initialize the 'DynFlags' relating to the compilation of a single
 -- file or GHC session according to the provided 'Cradle'.
-getCompilerOptions
-  :: FilePath -- ^ The file we are loading it because of
-  -> LoadStyle -- ^ previous files we might want to include in the build
-  -> Cradle a
-  -> IO (CradleLoadResult ComponentOptions)
-getCompilerOptions fp loadStyle cradle = do
+
+-- | Note: @getCompilerOptions (TargetWithContext path cxt) LoadFile@ will warn if `cxt` is non-empty.
+getCompilerOptions :: TargetWithContext -> LoadMode -> Cradle a -> IO (CradleLoadResult ComponentOptions)
+getCompilerOptions target loadMode cradle = do
   (cradleLogger cradle) <& LogProcessOutput "invoking build tool to determine build flags (this may take some time depending on the cache)" `WithSeverity` Info
-  runCradle (cradleOptsProg cradle) fp loadStyle
+  runCradle (cradleOptsProg cradle) target loadMode
diff --git a/src/HIE/Bios/Ghc/Api.hs b/src/HIE/Bios/Ghc/Api.hs
--- a/src/HIE/Bios/Ghc/Api.hs
+++ b/src/HIE/Bios/Ghc/Api.hs
@@ -7,6 +7,7 @@
   , withDynFlags
   -- For test purposes
   , initSessionWithMessage
+  , initSessionWithMessage'
   ) where
 
 import GHC (LoadHowMuch(..), DynFlags, GhcMonad)
@@ -39,7 +40,7 @@
   -> Cradle a  -- ^ The cradle we want to load
   -> m (CradleLoadResult (m G.SuccessFlag, ComponentOptions)) -- ^ Whether we actually loaded the cradle or not.
 initializeFlagsWithCradleWithMessage msg fp cradle =
-    fmap (initSessionWithMessage msg) <$> liftIO (getCompilerOptions fp LoadFile cradle)
+    fmap (initSessionWithMessage msg) <$> liftIO (getCompilerOptions (TargetWithContext fp []) LoadFile cradle)
 
 -- | Actually perform the initialisation of the session. Initialising the session corresponds to
 -- parsing the command line flags, setting the targets for the session and then attempting to load
@@ -48,8 +49,15 @@
             => Maybe G.Messager
             -> ComponentOptions
             -> (m G.SuccessFlag, ComponentOptions)
-initSessionWithMessage msg compOpts = (do
-    targets <- initSession compOpts
+initSessionWithMessage = initSessionWithMessage' False
+
+initSessionWithMessage' :: (GhcMonad m)
+            => Bool
+            -> Maybe G.Messager
+            -> ComponentOptions
+            -> (m G.SuccessFlag, ComponentOptions)
+initSessionWithMessage' workAroundThreadUnsafety msg compOpts = (do
+    targets <- initSession' workAroundThreadUnsafety compOpts
     G.setTargets targets
     -- Get the module graph using the function `getModuleGraph`
     mod_graph <- G.depanal [] True
diff --git a/src/HIE/Bios/Ghc/Gap.hs b/src/HIE/Bios/Ghc/Gap.hs
--- a/src/HIE/Bios/Ghc/Gap.hs
+++ b/src/HIE/Bios/Ghc/Gap.hs
@@ -1,4 +1,6 @@
 {-# LANGUAGE FlexibleInstances, CPP, PatternSynonyms #-}
+{-# LANGUAGE NondecreasingIndentation #-}
+{-# LANGUAGE TupleSections #-}
 -- | All the CPP for GHC version compability should live in this module.
 module HIE.Bios.Ghc.Gap (
   ghcVersion
@@ -11,6 +13,8 @@
   , G.modifySession
   , G.reflectGhc
   , G.Session(..)
+  -- * Compat shims
+  , typecheckModule
   -- * Hsc Monad
   , getHscEnv
   -- * Driver compat
@@ -41,6 +45,9 @@
   , HIE.Bios.Ghc.Gap.parseDynamicFlags
   -- * Platform constants
   , hostIsDynamic
+  -- * OsPath Compat
+  , unsafeEncodeUtf
+  , unsafeDecodeUtf
   -- * misc
   , getTyThing
   , fixInfo
@@ -52,12 +59,15 @@
   , load'
   , homeUnitId_
   , getDynFlags
+  , initMulti
+  , InRTS(..)
+  , removeRTS
   ) where
 
 import Control.Monad.IO.Class
 import qualified Control.Monad.Catch as E
 
-import GHC
+import GHC hiding (typecheckModule,parseTargetFiles)
 import qualified GHC as G
 
 ----------------------------------------------------------------
@@ -73,14 +83,16 @@
 import qualified GHC.Platform.Ways as Platform
 import qualified GHC.Runtime.Loader as DynamicLoading (initializePlugins)
 import qualified GHC.Tc.Types as Tc
+import GHC.Unit.Types (UnitId)
 import GHC.Utils.Logger
 import GHC.Utils.Outputable
 import qualified GHC.Utils.Ppr as Ppr
 import qualified GHC.Driver.Make as G
+import System.OsPath (OsPath)
+import qualified System.OsPath as OsPath
+import Data.Maybe (fromMaybe)
+import GHC.Stack.Types (HasCallStack)
 
-#if __GLASGOW_HASKELL__ > 903
-import GHC.Unit.Types (UnitId)
-#endif
 #if __GLASGOW_HASKELL__ < 904
 import qualified GHC.Driver.Main as G
 #endif
@@ -91,6 +103,21 @@
 #if __GLASGOW_HASKELL__ < 907
 import GHC.Driver.CmdLine as CmdLine
 #endif
+#if __GLASGOW_HASKELL__ >= 914
+import qualified GHC.Driver.Session.Units as G
+#else
+import GHC.Unit.Home.ModInfo (emptyHomePackageTable)
+import GHC.Unit.Env
+import Control.Monad
+import GHC.ResponseFile (expandResponse)
+import Data.List (partition)
+import GHC.Driver.Phases (isHaskellishTarget)
+import qualified GHC.Unit.State as State
+import System.FilePath (isRelative, (</>))
+import qualified Data.Map as Map
+#endif
+import qualified Data.List.NonEmpty as NE
+import Data.List (isPrefixOf, isSuffixOf)
 
 ghcVersion :: String
 ghcVersion = VERSION_ghc
@@ -106,6 +133,13 @@
 load' _ a b c = G.load' a b c
 #endif
 
+typecheckModule :: GhcMonad m => ParsedModule -> m TypecheckedModule
+#if MIN_VERSION_ghc(10, 1, 0)
+typecheckModule = G.typecheckModule G.StartAndStopTcMPlugins
+#else
+typecheckModule = G.typecheckModule
+#endif
+
 bracket :: E.MonadMask m => m a -> (a -> m c) -> (a -> m b) -> m b
 bracket =
   E.bracket
@@ -136,21 +170,22 @@
 set_hsc_dflags :: DynFlags -> HscEnv -> HscEnv
 set_hsc_dflags dflags hsc_env = hsc_env { G.hsc_dflags = dflags }
 
-overPkgDbRef :: (FilePath -> FilePath) -> G.PackageDBFlag -> G.PackageDBFlag
+overPkgDbRef :: (OsPath -> OsPath) -> G.PackageDBFlag -> G.PackageDBFlag
 overPkgDbRef f (G.PackageDB pkgConfRef) = G.PackageDB $ case pkgConfRef of
-    G.PkgDbPath fp -> G.PkgDbPath (f fp)
+    G.PkgDbPath fp ->
+#if __GLASGOW_HASKELL__ >= 915
+      G.PkgDbPath (f fp)
+#else
+      G.PkgDbPath (unsafeDecodeUtf $ f $ unsafeEncodeUtf fp)
+#endif
+
     conf -> conf
 overPkgDbRef _f db = db
 
 ----------------------------------------------------------------
 
-#if __GLASGOW_HASKELL__ >= 903
 guessTarget :: GhcMonad m => String -> Maybe UnitId -> Maybe G.Phase -> m G.Target
 guessTarget a b c = G.guessTarget a b c
-#else
-guessTarget :: GhcMonad m => String -> a -> Maybe G.Phase -> m G.Target
-guessTarget a _ b = G.guessTarget a b
-#endif
 
 ----------------------------------------------------------------
 
@@ -180,9 +215,7 @@
       G.IncludeSpecs
           (map f $ G.includePathsQuote  (includePaths df))
           (map f $ G.includePathsGlobal (includePaths df))
-#if MIN_VERSION_GLASGOW_HASKELL(9,0,2,0)
           (map f $ G.includePathsQuoteImplicit (includePaths df))
-#endif
   }
 
 ----------------------------------------------------------------
@@ -195,11 +228,7 @@
     setSession env
 
 noopLogger :: LogAction
-#if __GLASGOW_HASKELL__ >= 903
 noopLogger = (\_wr _s _ss _m -> return ())
-#else
-noopLogger = (\_df _wr _s _ss _m -> return ())
-#endif
 
 -- --------------------------------------------------------
 -- Doc Compat functions
@@ -217,11 +246,7 @@
 -- --------------------------------------------------------
 
 numLoadedPlugins :: HscEnv -> Int
-#if __GLASGOW_HASKELL__ >= 903
 numLoadedPlugins = length . Plugins.pluginsWithArgs . hsc_plugins
-#else
-numLoadedPlugins = length . Plugins.plugins
-#endif
 
 initializePluginsForModSummary :: HscEnv -> ModSummary -> IO (Int, [G.ModuleName], ModSummary)
 initializePluginsForModSummary hsc_env' mod_summary = do
@@ -292,3 +317,129 @@
 
 hostIsDynamic :: Bool
 hostIsDynamic = Platform.hostIsDynamic
+
+-- --------------------------------------------------------
+-- OsPath Compat
+-- --------------------------------------------------------
+
+unsafeEncodeUtf :: HasCallStack => FilePath -> OsPath
+unsafeEncodeUtf fp =
+#if MIN_VERSION_filepath(1,5,0)
+  OsPath.unsafeEncodeUtf fp
+#else
+  fromMaybe (error "unsafeEncodeUtf") $ OsPath.encodeUtf fp
+#endif
+
+unsafeDecodeUtf :: HasCallStack => OsPath -> FilePath
+unsafeDecodeUtf = fromMaybe (error "unsafeDecodeUtf") . OsPath.decodeUtf
+
+initMulti :: NE.NonEmpty String -> (DynFlags     -> [(String, Maybe Phase)] -> [String] -> [String] -> IO ()) -> Ghc [(String, Maybe UnitId, Maybe Phase)]
+#if __GLASGOW_HASKELL__ >= 914
+initMulti = G.initMulti
+#else
+
+-- taken from ghc-9.6.7, dropping some checks and logging.
+initMulti unitArgsFiles _check = do
+  hsc_env <- GHC.getSession
+  let logger = hsc_logger hsc_env
+  initial_dflags <- GHC.getSessionDynFlags
+
+  dynFlagsAndSrcs <- forM unitArgsFiles $ \f -> do
+    when (verbosity initial_dflags > 2) (liftIO $ print f)
+    args <- liftIO $ expandResponse [f]
+    (dflags2, fileish_args, _warns) <- parseDynamicFlagsCmdLine initial_dflags (map (mkGeneralLocated f) (removeRTS args))
+
+    let (dflags3, srcs, _objs) = parseTargetFiles dflags2 (map unLoc fileish_args)
+        dflags4 = offsetDynFlags dflags3
+
+    let (hs_srcs, _non_hs_srcs) = partition isHaskellishTarget srcs
+    pure (dflags4, hs_srcs)
+
+  let
+    unitDflags = NE.map fst dynFlagsAndSrcs
+    srcs = NE.map (\(dflags, lsrcs) -> map (uncurry (,Just $ homeUnitId_ dflags,)) lsrcs) dynFlagsAndSrcs
+    (hs_srcs, _non_hs_srcs) = unzip (map (partition (\(file, _uid, phase) -> isHaskellishTarget (file, phase))) (NE.toList srcs))
+
+  let (initial_home_graph, mainUnitId) = createUnitEnvFromFlags unitDflags
+      home_units = unitEnv_keys initial_home_graph
+
+  home_unit_graph <- forM initial_home_graph $ \homeUnitEnv -> do
+    let cached_unit_dbs = homeUnitEnv_unit_dbs homeUnitEnv
+        hue_flags = homeUnitEnv_dflags homeUnitEnv
+        dflags = homeUnitEnv_dflags homeUnitEnv
+    (dbs,unit_state,home_unit,mconstants) <- liftIO $ State.initUnits logger hue_flags cached_unit_dbs home_units
+
+    updated_dflags <- liftIO $ updatePlatformConstants dflags mconstants
+    pure $ HomeUnitEnv
+      { homeUnitEnv_units = unit_state
+      , homeUnitEnv_unit_dbs = Just dbs
+      , homeUnitEnv_dflags = updated_dflags
+      , homeUnitEnv_hpt = emptyHomePackageTable
+      , homeUnitEnv_home_unit = Just home_unit
+      }
+
+  let dflags = homeUnitEnv_dflags $ unitEnv_lookup mainUnitId home_unit_graph
+  unitEnv <- assertUnitEnvInvariant <$> (liftIO $ initUnitEnv mainUnitId home_unit_graph (ghcNameVersion dflags) (targetPlatform dflags))
+  let final_hsc_env = hsc_env { hsc_unit_env = unitEnv }
+
+  GHC.setSession final_hsc_env
+
+  return $ concat hs_srcs
+
+offsetDynFlags :: DynFlags -> DynFlags
+offsetDynFlags dflags =
+  dflags { hiDir = c hiDir
+         , objectDir  = c objectDir
+         , stubDir = c stubDir
+         , hieDir  = c hieDir
+         , dumpDir = c dumpDir  }
+
+  where
+    c f = augment_maybe (f dflags)
+
+    augment_maybe Nothing = Nothing
+    augment_maybe (Just f) = Just (augment f)
+    augment f | isRelative f, Just offset <- workingDirectory dflags = offset </> f
+              | otherwise = f
+
+
+createUnitEnvFromFlags :: NE.NonEmpty DynFlags -> (HomeUnitGraph, UnitId)
+createUnitEnvFromFlags unitDflags =
+  let
+    newInternalUnitEnv dflags = mkHomeUnitEnv dflags emptyHomePackageTable Nothing
+    unitEnvList = NE.map (\dflags -> (homeUnitId_ dflags, newInternalUnitEnv dflags)) unitDflags
+    activeUnit = fst $ NE.head unitEnvList
+  in
+    (unitEnv_new (Map.fromList (NE.toList (unitEnvList))), activeUnit)
+
+
+#endif
+
+-- | Strip out any ["+RTS", ..., "-RTS"] sequences in the command string list.
+data InRTS = OutsideRTS | InsideRTS
+
+-- | Strip out any ["+RTS", ..., "-RTS"] sequences in the command string list.
+--
+-- >>> removeRTS ["option1", "+RTS -H32m -RTS", "option2"]
+-- ["option1", "option2"]
+--
+-- >>> removeRTS ["option1", "+RTS", "-H32m", "-RTS", "option2"]
+-- ["option1", "option2"]
+--
+-- >>> removeRTS ["option1", "+RTS -H32m"]
+-- ["option1"]
+--
+-- >>> removeRTS ["option1", "+RTS -H32m", "-RTS", "option2"]
+-- ["option1", "option2"]
+--
+-- >>> removeRTS ["option1", "+RTS -H32m", "-H32m -RTS", "option2"]
+-- ["option1", "option2"]
+removeRTS :: [String] -> [String]
+removeRTS = go OutsideRTS
+  where
+    go :: InRTS -> [String] -> [String]
+    go _ [] = []
+    go OutsideRTS (y:ys)
+      | "+RTS" `isPrefixOf` y = go (if "-RTS" `isSuffixOf` y then OutsideRTS else InsideRTS) ys
+      | otherwise = y : go OutsideRTS ys
+    go InsideRTS (y:ys) = go (if "-RTS" `isSuffixOf` y then OutsideRTS else InsideRTS) ys
diff --git a/src/HIE/Bios/Ghc/Load.hs b/src/HIE/Bios/Ghc/Load.hs
--- a/src/HIE/Bios/Ghc/Load.hs
+++ b/src/HIE/Bios/Ghc/Load.hs
@@ -20,12 +20,7 @@
 import qualified GHC.Driver.Main as G
 
 import qualified HIE.Bios.Ghc.Gap as Gap
-#if __GLASGOW_HASKELL__ > 903
 import GHC.Fingerprint
-#endif
-#if __GLASGOW_HASKELL__ < 903
-import Data.Time.Clock
-#endif
 
 data Log =
   LogLoaded FilePath FilePath
@@ -118,16 +113,9 @@
 -- fool the recompilation checker so that we can get the typechecked modules
 updateTime :: MonadIO m => [Target] -> ModuleGraph -> m ModuleGraph
 updateTime ts graph = liftIO $ do
-#if __GLASGOW_HASKELL__ < 903
-  cur_time <- getCurrentTime
-#endif
   let go ms
         | any (msTargetIs ms) ts =
-#if __GLASGOW_HASKELL__ >= 903
             ms {ms_hs_hash = fingerprint0}
-#else
-            ms {ms_hs_date = cur_time}
-#endif
         | otherwise = ms
   pure $ Gap.mapMG go graph
 
@@ -175,7 +163,7 @@
   -> Gap.Hsc Gap.FrontendResult
 astHook logger tc_ref ms = ghcInHsc $ do
   p <- G.parseModule =<< initializePluginsGhc logger ms
-  tcm <- G.typecheckModule p
+  tcm <- Gap.typecheckModule p
   let tcg_env = fst (tm_internals_ tcm)
   liftIO $ modifyIORef tc_ref (tcm :)
   return $ Gap.FrontendTypecheck tcg_env
diff --git a/src/HIE/Bios/Ghc/Logger.hs b/src/HIE/Bios/Ghc/Logger.hs
--- a/src/HIE/Bios/Ghc/Logger.hs
+++ b/src/HIE/Bios/Ghc/Logger.hs
@@ -24,10 +24,8 @@
 import HIE.Bios.Ghc.Api (withDynFlags)
 import qualified HIE.Bios.Ghc.Gap as Gap
 
-#if __GLASGOW_HASKELL__ >= 903
 import GHC.Types.Error
 import GHC.Driver.Errors.Types
-#endif
 
 ----------------------------------------------------------------
 
@@ -46,9 +44,7 @@
 
 appendLogRef :: DynFlags -> Gap.PprStyle -> LogRef -> LogAction
 appendLogRef df style (LogRef ref) _
-#if __GLASGOW_HASKELL__ < 903
-    _ sev
-#elif __GLASGOW_HASKELL__ < 905
+#if __GLASGOW_HASKELL__ < 905
     (MCDiagnostic sev _)
 #else
     (MCDiagnostic sev _ _)
@@ -56,6 +52,7 @@
   src msg = do
         let !l = ppMsg src sev df style msg
         modifyIORef ref (\b -> b . (l:))
+appendLogRef _ _ _ _ _ _ _ = pure ()
 
 ----------------------------------------------------------------
 
@@ -88,14 +85,9 @@
 sourceError err = do
     dflag <- getSessionDynFlags
     style <- getStyle dflag
-#if __GLASGOW_HASKELL__ >= 903
     let ret = unlines . errBagToStrList dflag style . getMessages . srcErrorMessages $ err
-#else
-    let ret = unlines . errBagToStrList dflag style . srcErrorMessages $ err
-#endif
     return (Left ret)
 
-#if __GLASGOW_HASKELL__ >= 903
 errBagToStrList :: DynFlags -> Gap.PprStyle -> Bag (MsgEnvelope GhcMessage) -> [String]
 errBagToStrList dflag style = map (ppErrMsg dflag style) . reverse . bagToList
 
@@ -108,19 +100,6 @@
      msg = pprLocMsgEnvelope (defaultDiagnosticOpts @GhcMessage) err
 #else
      msg = pprLocMsgEnvelope err
-#endif
-     -- fixme
-#else
-errBagToStrList :: DynFlags -> Gap.PprStyle -> Bag (MsgEnvelope DecoratedSDoc) -> [String]
-errBagToStrList dflag style = map (ppErrMsg dflag style) . reverse . bagToList
-
-
-ppErrMsg :: DynFlags -> Gap.PprStyle -> MsgEnvelope DecoratedSDoc -> String
-ppErrMsg dflag style err = ppMsg spn SevError dflag style msg -- ++ ext
-   where
-     spn = errMsgSpan err
-     msg = pprLocMsgEnvelope err
-     -- fixme
 #endif
 
 ppMsg :: SrcSpan -> G.Severity-> DynFlags -> Gap.PprStyle -> SDoc -> String
diff --git a/src/HIE/Bios/Internal/Debug.hs b/src/HIE/Bios/Internal/Debug.hs
--- a/src/HIE/Bios/Internal/Debug.hs
+++ b/src/HIE/Bios/Internal/Debug.hs
@@ -25,12 +25,14 @@
 --
 -- Otherwise, shows the error message and exit-code.
 debugInfo :: Show a
-          => FilePath
+          => TargetWithContext
+          -> LoadMode
           -> Cradle a
           -> IO String
-debugInfo fp cradle = unlines <$> do
+debugInfo fpc loadStyle cradle = unlines <$> do
+    let fp = targetFilePath fpc
     let logger = cradleLogger cradle
-    res <- getCompilerOptions fp LoadFile cradle
+    res <- getCompilerOptions fpc loadStyle cradle
     canonFp <- canonicalizePath fp
     conf <- findConfig canonFp
     crdl <- findCradle' logger canonFp
diff --git a/src/HIE/Bios/Process.hs b/src/HIE/Bios/Process.hs
new file mode 100644
--- /dev/null
+++ b/src/HIE/Bios/Process.hs
@@ -0,0 +1,222 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE TupleSections #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+module HIE.Bios.Process
+  ( CreateProcess(..)
+  -- * Run processes with extra environment variables
+  , readProcessWithCwd
+  , readProcessWithCwd_
+  , readProcessWithCwd'
+  , readProcessWithOutputs
+  , getCleanEnvironment
+  -- * File Caching
+  , cacheFile
+  -- * Find file utilities
+  , findFileUpwards
+  , findFileUpwardsPredicate
+  , findFile
+  )
+  where
+
+import Control.Applicative (optional)
+import Control.DeepSeq
+import Control.Exception (handleJust)
+import System.Exit
+import System.Directory hiding (findFile)
+import Colog.Core (LogAction (..), WithSeverity (..), Severity (..), (<&))
+import Control.Monad
+import Control.Monad.Trans.Cont
+import Control.Monad.Trans.Maybe
+import Control.Monad.IO.Class
+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.HashMap.Strict as Map
+import Data.Maybe (fromMaybe)
+import qualified Data.Text as T
+import System.Environment
+import System.FilePath
+import System.IO (hClose, hGetContents, hSetBuffering, BufferMode(LineBuffering), withFile, IOMode(..))
+import System.IO.Error (isPermissionError)
+import System.IO.Temp
+
+import HIE.Bios.Types
+import Control.Monad.Extra (unlessM)
+import System.PosixCompat (setFileMode, accessModes)
+import HIE.Bios.Environment (getCacheDir)
+
+-- | Wrapper around 'readCreateProcess' that sets the working directory and
+-- clears the environment, suitable for invoking cabal/stack and raw ghc commands.
+readProcessWithCwd :: LogAction IO (WithSeverity Log) -> FilePath -> FilePath -> [String] -> String -> IO (CradleLoadResult String)
+readProcessWithCwd l dir cmd args stdin = runCradleResultT $ readProcessWithCwd_ l dir cmd args stdin
+
+readProcessWithCwd_ :: LogAction IO (WithSeverity Log) -> FilePath -> FilePath -> [String] -> String -> CradleLoadResultT IO String
+readProcessWithCwd_ l dir cmd args stdin = do
+  cleanEnv <- liftIO getCleanEnvironment
+  let createdProc' = (proc cmd args) { cwd = Just dir, env = Just cleanEnv }
+  readProcessWithCwd' l createdProc' stdin
+
+-- | Wrapper around 'readCreateProcessWithExitCode', wrapping the result in
+-- a 'CradleLoadResult'. Provides better error messages than raw 'readCreateProcess'.
+readProcessWithCwd' :: LogAction IO (WithSeverity Log) -> CreateProcess -> String -> CradleLoadResultT IO String
+readProcessWithCwd' l createdProcess stdin = do
+  mResult <- liftIO $ optional $ readCreateProcessWithExitCode createdProcess stdin
+  liftIO $ l <& LogCreateProcessRun createdProcess `WithSeverity` Debug
+  let cmdString = prettyCmdSpec $ cmdspec createdProcess
+  case mResult of
+    Just (ExitSuccess, stdo, _) -> pure stdo
+    Just (exitCode, stdo, stde) -> throwCE $
+      CradleError [] exitCode
+        (["Error when calling " <> cmdString, stdo, stde] <> prettyProcessEnv createdProcess)
+        []
+    Nothing -> throwCE $
+      CradleError [] ExitSuccess
+        (["Couldn't execute " <> cmdString] <> prettyProcessEnv createdProcess)
+        []
+
+
+-- | 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
+  Map.toList . Map.delete "GHC_PACKAGE_PATH" . Map.fromList <$> getEnvironment
+
+type Outputs = [OutputName]
+type OutputName = String
+
+-- | Call a given process with temp files for the process to write to.
+-- * The process can discover the temp files paths by reading the environment.
+-- * The contents of the temp files are returned by this function, if any.
+-- * The logging function is called every time the process emits anything to stdout or stderr.
+-- it can be used to report progress of the process to a user.
+-- * The process is executed in the given directory.
+readProcessWithOutputs
+  :: Outputs  -- ^ Names of the outputs produced by this process
+  -> LogAction IO (WithSeverity Log) -- ^ Output of the process is emitted as logs.
+  -> 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 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 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 (\msg -> l <& LogProcessOutput msg `WithSeverity` Debug) C..| C.sinkList
+  liftIO $ l <& LogCreateProcessRun process `WithSeverity` Info
+  (ex, stdo, stde) <- liftIO $ sourceProcessWithStreams process mempty loggingConduit loggingConduit
+
+  res <- forM output_files $ \(name,path) ->
+          liftIO $ (name,) <$> readOutput path
+
+  return (ex, stdo, stde, res)
+
+    where
+      readOutput :: FilePath -> IO (Maybe [String])
+      readOutput path = do
+        haveFile <- doesFileExist path
+        if haveFile
+          then withFile path ReadMode $ \handle -> do
+            hSetBuffering handle LineBuffering
+            !res <- force <$> hGetContents handle
+            return $ Just $ lines $ filter (/= '\r') res
+          else
+            return Nothing
+
+      withOutput :: [(String,String)] -> OutputName -> ContT a IO (OutputName, String)
+      withOutput env' name =
+        case lookup name env' of
+          Just file@(_:_) -> ContT $ \action -> do
+            removeFileIfExists file
+            action (name, file)
+          _ -> ContT $ \action -> withSystemTempFile name $ \ file h -> do
+            hClose h
+            removeFileIfExists file
+            action (name, file)
+
+-- | 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
+
+------------------------------------------------------------------------------
+-- Utilities
+
+
+-- | Searches upwards for the first directory containing a file.
+findFileUpwards :: FilePath -> FilePath -> MaybeT IO FilePath
+findFileUpwards filename dir = do
+  cnts <-
+    liftIO
+    $ handleJust
+        -- Catch permission errors
+        (\(e :: IOError) -> if isPermissionError e then Just False else Nothing)
+        pure
+        (doesFileExist (dir </> filename))
+  case cnts of
+    False | dir' == dir -> fail "No cabal files"
+            | otherwise   -> findFileUpwards filename dir'
+    True -> return dir
+  where dir' = takeDirectory dir
+
+-- | Searches upwards for the first directory containing a file to match
+-- the predicate.
+--
+-- *WARNING*, this scans all the files of all the directories upward. If
+-- appliable, prefer to use 'findFileUpwards'
+findFileUpwardsPredicate :: (FilePath -> Bool) -> FilePath -> MaybeT IO FilePath
+findFileUpwardsPredicate p dir = do
+  cnts <-
+    liftIO
+    $ handleJust
+        -- Catch permission errors
+        (\(e :: IOError) -> if isPermissionError e then Just [] else Nothing)
+        pure
+        (findFile p dir)
+
+  case cnts of
+    [] | dir' == dir -> fail "No cabal files"
+            | otherwise   -> findFileUpwardsPredicate p dir'
+    _ : _ -> return dir
+  where dir' = takeDirectory dir
+
+-- | Sees if any file in the directory matches the predicate
+findFile :: (FilePath -> Bool) -> FilePath -> IO [FilePath]
+findFile p dir = do
+  b <- doesDirectoryExist dir
+  if b then getFiles >>= filterM doesPredFileExist else return []
+  where
+    getFiles = filter p <$> getDirectoryContents dir
+    doesPredFileExist file = doesFileExist $ dir </> file
+
+removeFileIfExists :: FilePath -> IO ()
+removeFileIfExists f = do
+  yes <- doesFileExist f
+  when yes (removeFile f)
+
diff --git a/src/HIE/Bios/Types.hs b/src/HIE/Bios/Types.hs
--- a/src/HIE/Bios/Types.hs
+++ b/src/HIE/Bios/Types.hs
@@ -16,7 +16,7 @@
 import           Data.Maybe (fromMaybe)
 import qualified Data.Text as T
 import           Prettyprinter
-import           System.Process.Extra (CreateProcess (env, cmdspec), CmdSpec (..))
+import           System.Process.Extra (CreateProcess (env, cmdspec), CmdSpec (..), showCommandForUser)
 
 ----------------------------------------------------------------
 -- Environment variables used by hie-bios.
@@ -49,6 +49,12 @@
 hie_bios_arg :: String
 hie_bios_arg = "HIE_BIOS_ARG"
 
+-- | Environment variable pointing to the multiple source files location that
+-- caused the cradle action to be executed. Differs from hie_bios_arg for
+-- backwards compatibility.
+hie_bios_multi_arg :: String
+hie_bios_multi_arg = "HIE_BIOS_MULTI_ARG"
+
 -- | Environment variable pointing to a filepath to which dependencies
 -- of a cradle can be written to by the cradle action.
 hie_bios_deps :: String
@@ -96,13 +102,20 @@
   | LogProcessOutput String
   | LogCreateProcessRun CreateProcess
   | LogProcessRun FilePath [FilePath]
-  | LogRequestedCradleLoadStyle !T.Text !LoadStyle
-  | LogComputedCradleLoadStyle !T.Text !LoadStyle
-  | LogLoadWithContextUnsupported !T.Text !(Maybe T.Text)
+  | LogRequestedCradleLoadMode !T.Text !TargetWithContext !LoadMode
+  | LogComputedCradleLoadMode !T.Text !TargetWithContext !LoadMode
+  | LogLoadModeUnsupported !T.Text LoadMode !(Maybe T.Text)
   | LogCabalLoad !FilePath !(Maybe String) ![FilePath] ![FilePath]
+  | LogCabalLibraryTooOld [String]
   | LogCabalPath !T.Text
   deriving (Show)
 
+instance Pretty LoadMode where
+  pretty = viaShow
+
+instance Pretty TargetWithContext where
+  pretty (TargetWithContext fp fps) = pretty fp <> list (map pretty fps)
+
 instance Pretty Log where
   pretty (LogAny s) = pretty s
   pretty (LogProcessOutput s) = pretty s
@@ -122,32 +135,56 @@
           ]
     where
       envText = map (indent 2 . pretty) $ prettyProcessEnv cp
-  pretty (LogRequestedCradleLoadStyle crdlName ls) =
+  pretty (LogRequestedCradleLoadMode crdlName fpc ls) =
     "Requested to load" <+> pretty crdlName <+> "cradle" <+> case ls of
-      LoadFile -> "using single file mode"
-      LoadWithContext fps -> "using all files (multi-components):" <> line <> indent 4 (pretty fps)
-  pretty (LogComputedCradleLoadStyle crdlName ls) =
+      LoadFile -> "using single file mode" <+> parens ("target =" <+> pretty fpc)
+      LoadFileWithContext -> "using all files (multi-components):" <> line <> indent 4 (pretty fpc)
+      LoadUnitsFromCradle -> "using all units specified in cradle" <+> fallback
+      LoadUnitsInferred   -> "using all units from project" <+> fallback
+    where
+      fallback = parens $ "fallback info: " <+> pretty fpc
+  pretty (LogComputedCradleLoadMode crdlName _fpc ls) =
     "Load" <+> pretty crdlName <+> "cradle" <+> case ls of
       LoadFile -> "using single file"
-      LoadWithContext _ -> "using all files (multi-components)"
-
-  pretty (LogLoadWithContextUnsupported crdlName mReason) =
+      LoadFileWithContext -> "using all files (multi-components)"
+      LoadUnitsFromCradle -> "using all units specified in cradle"
+      LoadUnitsInferred   -> "using all units from project"
+  pretty (LogLoadModeUnsupported crdlName mode mReason) =
     pretty crdlName <+> "cradle doesn't support loading using all files (multi-components)" <>
       case mReason of
         Nothing -> "."
         Just reason -> ", because:" <+> pretty reason <> "."
-      <+> "Falling back loading to single file mode."
+      <+> "Falling back" <+> parens ("from" <+> pretty mode ) <+> "loading to single file mode."
   pretty (LogCabalLoad file prefixes projectFile crs) =
     "Cabal Loading file" <+> pretty file
           <> 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
+      <> vcat (fmap pretty err)
   pretty (LogCabalPath err) =
     "Could not parse json output of 'cabal path': "
       <> line <> indent 4 (pretty err)
 
--- | The 'LoadStyle' instructs a cradle on how to load a given file target.
-data LoadStyle
+type Component = String
+
+-- | @TargetWithContext@ is only directly relevant for @LoadFile@ and @LoadFileWithContext@ modes.
+--  Other modes though can fallback on those two, so relevant regardless.
+data TargetWithContext = TargetWithContext
+    { targetFilePath :: !FilePath
+    , targetContext :: ![FilePath]
+    }
+  deriving (Show)
+
+singleTarget :: FilePath -> TargetWithContext
+singleTarget fp = TargetWithContext fp []
+
+targetAndContext :: TargetWithContext -> [FilePath]
+targetAndContext (TargetWithContext fp fps) = fp:fps
+
+-- | The 'LoadMode' instructs a cradle on how to load a given file target.
+data LoadMode
   = LoadFile
   -- ^ Instruct the cradle to load the given file target.
   --
@@ -155,7 +192,7 @@
   -- will configure the whole component the file target belongs to, and produce
   -- component options to load the component, which is the minimal unit of code in cabal repl.
   -- A 'default' cradle, on the other hand, will only load the given filepath.
-  | LoadWithContext [FilePath]
+  | LoadFileWithContext
   -- ^ Give a cradle additional context for loading a file target.
   --
   -- The context instructs the cradle to load the file target, while also loading
@@ -163,12 +200,14 @@
   -- This is useful for cradles that support loading multiple code units at once,
   -- e.g. cabal cradles can use the 'multi-repl' feature to set up a multiple home unit
   -- session in GHC.
-  deriving (Show, Eq, Ord)
+  | LoadUnitsInferred
+  | LoadUnitsFromCradle
+  deriving (Eq,Ord,Enum,Bounded,Show)
 
 data CradleAction a = CradleAction {
                         actionName    :: ActionName a
                       -- ^ Name of the action.
-                      , runCradle     :: FilePath -> LoadStyle -> IO (CradleLoadResult ComponentOptions)
+                      , runCradle     :: TargetWithContext -> LoadMode -> IO (CradleLoadResult ComponentOptions)
                       -- ^ Options to compile the given file with.
                       , runGhcCmd     :: [String] -> IO (CradleLoadResult String)
                       -- ^ Executes the @ghc@ binary that is usually used to
@@ -310,12 +349,18 @@
 -- | 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
+prettyCmdSpec (RawCommand cmd args) = showCommandForUser cmd args
 
 -- | Pretty print hie-bios's relevant environment variables.
 prettyProcessEnv :: CreateProcess -> [String]
 prettyProcessEnv p =
   [ key <> ": " <> value
+  | (key, value) <- hieBiosProcessEnv p
+  ]
+
+hieBiosProcessEnv :: CreateProcess -> [(String, String)]
+hieBiosProcessEnv p =
+  [ (key, value)
   | (key, value) <- fromMaybe [] (env p)
   , key `elem` [ hie_bios_output
                , hie_bios_ghc
diff --git a/src/HIE/Bios/Wrappers.hs b/src/HIE/Bios/Wrappers.hs
--- a/src/HIE/Bios/Wrappers.hs
+++ b/src/HIE/Bios/Wrappers.hs
@@ -1,17 +1,24 @@
-{-# LANGUAGE TemplateHaskell #-}
 {-# LANGUAGE QuasiQuotes #-}
-{-# LANGUAGE CPP #-}
-module HIE.Bios.Wrappers (cabalWrapper, cabalWrapperHs) where
+{-# LANGUAGE TemplateHaskell #-}
 
-import Data.FileEmbed
-#if __GLASGOW_HASKELL__ >= 903
-  hiding (makeRelativeToProject)
-#endif
-import Language.Haskell.TH.Syntax
+module HIE.Bios.Wrappers (
+  cabalWrapper,
+  cabalWithReplWrapper,
+  cabalWrapperHs,
+  cabalWithReplWrapperHs,
+) where
 
+import Data.FileEmbed ( embedStringFile )
+import Language.Haskell.TH.Syntax ( makeRelativeToProject )
+
 cabalWrapper :: String
 cabalWrapper = $(makeRelativeToProject "wrappers/cabal" >>= embedStringFile)
 
 cabalWrapperHs :: String
 cabalWrapperHs = $(makeRelativeToProject "wrappers/cabal.hs" >>= embedStringFile)
 
+cabalWithReplWrapper :: String
+cabalWithReplWrapper = $(makeRelativeToProject "wrappers/cabal-with-repl" >>= embedStringFile)
+
+cabalWithReplWrapperHs :: String
+cabalWithReplWrapperHs = $(makeRelativeToProject "wrappers/cabal-with-repl.hs" >>= embedStringFile)
diff --git a/tests/BiosTests.hs b/tests/BiosTests.hs
--- a/tests/BiosTests.hs
+++ b/tests/BiosTests.hs
@@ -4,27 +4,36 @@
 {-# LANGUAGE CPP #-}
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE TypeApplications #-}
-module Main where
+module Main (main) where
 
 import Utils
 
 import Test.Tasty
 import Test.Tasty.HUnit
 import Test.Tasty.ExpectedFailure
-import qualified Test.Tasty.Options as Tasty
 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 Control.Monad ( forM_ )
-import Data.List ( sort, isPrefixOf )
+import HIE.Bios.Cradle.Cabal (cabalBuildDir)
+import HIE.Bios.Types (LoadMode(..))
+import Control.Monad (forM_, forM, unless)
+import Control.Monad.Extra (unlessM)
+import Control.Monad.IO.Class
+import Data.Foldable (for_)
+import Data.List ( sort, isPrefixOf, isInfixOf, tails )
 import Data.Typeable
+import System.Exit (ExitCode(ExitSuccess, ExitFailure))
 import System.Directory
 import System.FilePath ((</>), makeRelative)
-import System.Exit (ExitCode(ExitSuccess, ExitFailure))
-import Control.Monad.Extra (unlessM)
+import System.Info.Extra (isWindows)
+import System.IO (BufferMode (LineBuffering), hSetBuffering, stderr, stdout)
 import qualified HIE.Bios.Ghc.Gap as Gap
-import Control.Monad.IO.Class
+import HIE.Bios.Cradle.Utils (expandGhcOptionResponseFile)
+import HIE.Bios.Environment (extractUnits)
 
+
 argDynamic :: [String]
 argDynamic = ["-dynamic" | Gap.hostIsDynamic]
 
@@ -34,7 +43,7 @@
 -- If you change this version, make sure to also update 'cabal.project'
 -- in 'tests\/projects\/cabal-with-ghc'.
 extraGhcVersion :: String
-extraGhcVersion = "9.2.8"
+extraGhcVersion = "9.4.8"
 
 extraGhc :: String
 extraGhc = "ghc-" ++ extraGhcVersion
@@ -54,12 +63,17 @@
 -- to avoid recompilation.
 main :: IO ()
 main = do
+  for_ [stderr, stdout] (`hSetBuffering` LineBuffering)
   writeStackYamlFiles
   stackDep <- checkToolIsAvailable "stack"
   cabalDep <- checkToolIsAvailable "cabal"
   extraGhcDep <- checkToolIsAvailable extraGhc
 
   defaultMainWithIngredients (ignoreToolTests:verboseLogging:defaultIngredients) $
+    -- Run tests sequentially on Windows, to avoid issues with locking of the
+    -- package database, e.g. errors of the form:
+    --   package.db/package.cache.lock: openBinaryFile: resource busy (file is locked)
+    (if isWindows then localOption (Tasty.NumThreads 1) else id) $
     testGroup "Bios-tests"
       [ testGroup "Find cradle" findCradleTests
       , testGroup "Symlink" symbolicLinkTests
@@ -77,8 +91,8 @@
       initCradle "doesNotExist.hs"
       assertCradle isMultiCradle
       step "Attempt to load symlinked module A"
-      inCradleRootDir $ do
-        loadComponentOptions "./a/A.hs"
+      do
+        loadComponentOptions "./a/A.hs" []
         assertComponentOptions $ \opts ->
           componentOptions opts `shouldMatchList` ["a"] <> argDynamic
 
@@ -86,22 +100,27 @@
       initCradle "doesNotExist.hs"
       assertCradle isMultiCradle
       step "Attempt to load symlinked module A"
-      inCradleRootDir $ do
-        liftIO $ createDirectoryLink "./a" "./b"
-        liftIO $ unlessM (doesFileExist "./b/A.hs") $
+      do
+        cradle <- askCradle
+        let rooted = (cradleRootDir cradle </>)
+        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 "./b/A.hs" []
         assertComponentOptions $ \opts ->
           componentOptions opts `shouldMatchList` ["b"] <> argDynamic
+
   , biosTestCase "Can not load symlinked module that is ignored" $ runTestEnv "./symlink-test" $ do
       initCradle "doesNotExist.hs"
       assertCradle isMultiCradle
       step "Attempt to load symlinked module A"
-      inCradleRootDir $ do
-        liftIO $ createDirectoryLink "./a" "./c"
-        liftIO $ unlessM (doesFileExist "./c/A.hs") $
+      do
+        cradle <- askCradle
+        let rooted = (cradleRootDir cradle </>)
+        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 "./c/A.hs" []
         assertLoadNone
   ]
 
@@ -110,7 +129,7 @@
   [ biosTestCase "failing-bios" $ runTestEnv "./failing-bios" $ do
       initCradle "B.hs"
       assertCradle isBiosCradle
-      loadComponentOptions "B.hs"
+      loadComponentOptions "B.hs" []
       assertCradleError $ \CradleError {..} -> do
         cradleErrorExitCode @?= ExitFailure 1
         cradleErrorDependencies `shouldMatchList` ["hie.yaml"]
@@ -123,7 +142,11 @@
         cradleErrorExitCode @?= ExitSuccess
         cradleErrorDependencies `shouldMatchList` []
         length cradleErrorStderr @?= 1
-        "Couldn't execute myGhc" `isPrefixOf` head cradleErrorStderr @? "Error message should contain basic information"
+        forM_ cradleErrorStderr $ \errorCtx ->
+          -- On windows, this error message contains '"' around the executable name
+          if isWindows
+            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"
   , biosTestCase "simple-bios-shell-deps" $ runTestEnv "./simple-bios-shell" $ do
@@ -134,7 +157,7 @@
     biosCradleDeps fp deps = do
       initCradle fp
       assertCradle isBiosCradle
-      loadComponentOptions fp
+      loadComponentOptions fp []
       assertComponentOptions $ \opts -> do
         deps @?= componentDependencies opts
 
@@ -152,13 +175,13 @@
 cabalTestCases :: ToolDependency -> [TestTree]
 cabalTestCases extraGhcDep =
   [
-    biosTestCase "failing-cabal" $ runTestEnv "./failing-cabal" $ do
-      cabalAttemptLoad "MyLib.hs"
+    biosTestCaseAll "failing-cabal" $ runTestEnv "./failing-cabal" $ do
+      attemptCabalSingleTargetLoad "MyLib.hs"
       assertCradleError (\CradleError {..} -> do
         cradleErrorExitCode @?= ExitFailure 1
         cradleErrorDependencies `shouldMatchList` ["failing-cabal.cabal", "cabal.project", "cabal.project.local"])
-  , biosTestCase "failing-cabal-multi-repl-with-shrink-error-files" $ runTestEnv "./failing-multi-repl-cabal-project" $ do
-      cabalAttemptLoadFiles "multi-repl-cabal-fail/app/Main.hs" ["multi-repl-cabal-fail/src/Lib.hs", "multi-repl-cabal-fail/src/Fail.hs", "NotInPath.hs"]
+  , 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'
       if multiSupported
@@ -170,100 +193,256 @@
             (makeRelative root <$> cradleErrorLoadingFiles) `shouldMatchList` ["multi-repl-cabal-fail/app/Main.hs","multi-repl-cabal-fail/src/Fail.hs","multi-repl-cabal-fail/src/Lib.hs"])
         else assertLoadSuccess >>= \ComponentOptions {} -> do
           return ()
-  , biosTestCase "simple-cabal" $ runTestEnv "./simple-cabal" $ do
+  , biosTestCaseAll "simple-cabal" $ runTestEnv "./simple-cabal" $ do
       testDirectoryM isCabalCradle "B.hs"
-  , biosTestCase "nested-cabal" $ runTestEnv "./nested-cabal" $ do
-      cabalAttemptLoad "sub-comp/Lib.hs"
+  , biosTestCaseMulti "build-dir" $ runTestEnv "./simple-cabal" $ do
+      initCradle "B.hs"
+      assertCradle isCabalCradle
+      root <- askRoot
+      buildDir <- liftIO $ cabalBuildDir root
+      -- use --multi-repl, as that was the codepath with the bug
+      loadFileGhc "B.hs" []
+      liftIO $ do
+        -- Check we aren't trampling over dist-newstyle
+        distNewstyleExists <- doesDirectoryExist (root </> "dist-newstyle")
+        assertBool "dist-newstyle was created" (not distNewstyleExists)
+        -- Check we are using the correct build directory
+        buildDirExists <- doesDirectoryExist buildDir
+        assertBool "build dir does not exist" buildDirExists
+  , biosTestCaseAll "nested-cabal" $ runTestEnv "./nested-cabal" $ do
+      attemptCabalSingleTargetLoad "sub-comp/Lib.hs"
+      mode <- askLoadMode
       assertComponentOptions $ \opts -> do
-        componentDependencies opts `shouldMatchList`
-          [ "sub-comp" </> "sub-comp.cabal"
-          , "cabal.project"
-          , "cabal.project.local"
-          ]
-  , biosTestCase "nested-cabal2" $ runTestEnv "./nested-cabal" $ do
-      cabalAttemptLoad "MyLib.hs"
+        let
+          expectedDeps
+            | mode `elem` [LoadUnitsFromCradle,LoadUnitsInferred]
+            = [ "nested-cabal.cabal"
+              , "sub-comp" </> "sub-comp.cabal"
+              , "cabal.project"
+              , "cabal.project.local"
+              ]
+            | otherwise =
+              [ "sub-comp" </> "sub-comp.cabal"
+              , "cabal.project"
+              , "cabal.project.local"
+              ]
+        componentDependencies opts `shouldMatchList` expectedDeps
+  , biosTestCaseAll "nested-cabal2" $ runTestEnv "./nested-cabal" $ do
+      attemptCabalSingleTargetLoad "MyLib.hs"
+      mode <- askLoadMode
       assertComponentOptions $ \opts -> do
-        componentDependencies opts `shouldMatchList`
-          [ "nested-cabal.cabal"
-          , "cabal.project"
-          , "cabal.project.local"
-          ]
-  , biosTestCase "multi-cabal" $ runTestEnv "./multi-cabal" $ do
+        let
+          expectedDeps
+            | mode `elem` [LoadUnitsFromCradle,LoadUnitsInferred]
+            = [ "nested-cabal.cabal"
+              , "sub-comp" </> "sub-comp.cabal"
+              , "cabal.project"
+              , "cabal.project.local"
+              ]
+            | otherwise
+            = [ "nested-cabal.cabal"
+              , "cabal.project"
+              , "cabal.project.local"
+              ]
+        componentDependencies opts `shouldMatchList` expectedDeps
+  , biosTestCaseMulti "nested-cabal multi-mode includes enclosing deps for extra files" $ runTestEnv "./nested-cabal" $ do
+      -- Initialize cradle first, since capability checks use the current cradle.
+      initCradle "sub-comp/Lib.hs"
+      assertCradle isCabalCradle
+      multiSupported <- isCabalMultipleCompSupported'
+      if multiSupported
+        then do
+          loadComponentOptions "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.
+            componentDependencies opts `shouldMatchList`
+              [ "sub-comp" </> "sub-comp.cabal"
+              , "nested-cabal.cabal"
+              , "cabal.project"
+              , "cabal.project.local"
+              ]
+        else do
+          -- On older cabal/ghc combos, multi-repl isn't supported; just ensure load succeeds.
+          loadComponentOptions "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'
+      if multiSupported
+        then do
+          loadComponentOptions "MyLib.hs" ["sub-comp/Lib.hs"]
+          assertComponentOptions $ \opts -> do
+            componentDependencies opts `shouldMatchList`
+              [ "nested-cabal.cabal"
+              , "sub-comp" </> "sub-comp.cabal"
+              , "cabal.project"
+              , "cabal.project.local"
+              ]
+        else do
+          loadComponentOptions "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"
   , {- issue https://github.com/mpickering/hie-bios/issues/200 -}
-    biosTestCase "monorepo-cabal" $ runTestEnv "./monorepo-cabal" $ do
+    biosTestCaseAll "monorepo-cabal" $ runTestEnv "./monorepo-cabal" $ do
       testDirectoryM isCabalCradle "A/Main.hs"
       testDirectoryM isCabalCradle "B/MyLib.hs"
   , testGroup "Implicit cradle tests" $
-      [ biosTestCase "implicit-cabal" $ runTestEnv "./implicit-cabal" $ do
+      [ biosTestCaseAll "implicit-cabal" $ runTestEnv "./implicit-cabal" $ do
           testImplicitDirectoryM isCabalCradle "Main.hs"
-      , biosTestCase "implicit-cabal-no-project" $ runTestEnv "./implicit-cabal-no-project" $ do
+      , biosTestCaseAll "implicit-cabal-no-project" $ runTestEnv "./implicit-cabal-no-project" $ do
           testImplicitDirectoryM isCabalCradle "Main.hs"
-      , biosTestCase "implicit-cabal-deep-project" $ runTestEnv "./implicit-cabal-deep-project" $ do
+      , biosTestCaseAll "implicit-cabal-deep-project" $ runTestEnv "./implicit-cabal-deep-project" $ do
           testImplicitDirectoryM isCabalCradle "foo/Main.hs"
+      , biosTestCase "implicit-cabal-deep-project-with-context" $ runTestModeEnv "./implicit-cabal-deep-project" LoadFileWithContext $ do
+          testImplicitDirectoryWithContextM isCabalCradle "foo/Main.hs" ["Main.hs"]
       ]
   , testGroupWithDependency extraGhcDep
-    [ biosTestCase "Appropriate ghc and libdir" $ runTestEnvLocal "./cabal-with-ghc" $ do
+    [ biosTestCaseAll "Appropriate ghc and libdir" $ runTestEnv "./cabal-with-ghc" $ do
         initCradle "src/MyLib.hs"
         assertCradle isCabalCradle
         loadRuntimeGhcLibDir
         assertLibDirVersionIs extraGhcVersion
         loadRuntimeGhcVersion
         assertGhcVersionIs extraGhcVersion
+        step "Find Component Options"
+        loadComponentOptions "src/MyLib.hs" []
+        _ <- assertLoadSuccess
+        pure ()
     ]
   , testGroup "Cabal cabalProject"
-    [ biosTestCase "cabal-with-project, options propagated" $ runTestEnv "cabal-with-project" $ do
-        opts <- cabalLoadOptions "src/MyLib.hs"
-        liftIO $ do
-          "-O2" `elem` componentOptions opts
-            @? "Options must contain '-O2'"
-    , biosTestCase "cabal-with-project, load" $ 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"
-    , biosTestCase "multi-cabal-with-project, options propagated" $ runTestEnv "multi-cabal-with-project" $ do
-        optsAppA <- cabalLoadOptions "appA/src/Lib.hs"
-        liftIO $ do
-          "-O2" `elem` componentOptions optsAppA
-            @? "Options must contain '-O2'"
-        optsAppB <- cabalLoadOptions "appB/src/Lib.hs"
-        liftIO $ do
-          "-O2" `notElem` componentOptions optsAppB
-            @? "Options must not contain '-O2'"
-    , biosTestCase "multi-cabal-with-project, load" $ runTestEnv "multi-cabal-with-project" $ do
+    , 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
-      [ biosTestCase "Honours extra ghc setting" $ runTestEnv "cabal-with-ghc-and-project" $ do
+      [ 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
+        -- 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
+        -- 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
     ]
   ]
   where
-    cabalAttemptLoad :: FilePath -> TestM ()
-    cabalAttemptLoad fp = do
-      initCradle fp
-      assertCradle isCabalCradle
-      loadComponentOptions fp
+    attemptCabalSingleTargetLoad fp = attemptCabalLoad fp []
 
-    cabalAttemptLoadFiles :: FilePath -> [FilePath] -> TestM ()
-    cabalAttemptLoadFiles fp fps = do
+    attemptCabalLoad :: FilePath -> [FilePath] -> TestM ()
+    attemptCabalLoad fp fps = do
       initCradle fp
       assertCradle isCabalCradle
-      loadComponentOptionsMultiStyle fp fps
+      loadComponentOptions fp fps
 
     cabalLoadOptions :: FilePath -> TestM ComponentOptions
     cabalLoadOptions fp = do
       initCradle fp
       assertCradle isCabalCradle
-      loadComponentOptions fp
+      loadComponentOptions fp []
       assertLoadSuccess
 
+assertOptionsContain :: [Char] -> Maybe String -> TestM ()
+assertOptionsContain flag munit = do
+  doesContain <- hasOption flag munit
+  liftIO $ doesContain @? ("Options must contain '" ++ flag ++ "'")
+
+assertOptionsDoNotContain :: [Char] -> Maybe String -> TestM ()
+assertOptionsDoNotContain flag munit = do
+  doesContain <- hasOption flag munit
+  liftIO $ not doesContain @? ("Options must not contain '" ++ flag ++ "'")
+
+hasOption :: [Char] -> Maybe String -> TestM Bool
+hasOption flag munit = elem flag <$> do
+  options <- componentOptions <$> assertLoadSuccess
+  mode <- askLoadMode
+  let
+    common | Nothing <- munit = do
+        expandGhcOptionResponseFile options
+      | Just unit <- munit = do
+        let (units, rest) = extractUnits options
+        fmap (concat . (rest:)) . forM units $ \ resp_file -> do
+          unit_flags <- expandGhcOptionResponseFile [resp_file]
+          if and [ unit `isInfixOf` uid | ("-this-unit-id":uid:_) <- tails unit_flags
+              ]
+            then pure unit_flags
+            else pure []
+  liftIO $ case mode of
+    LoadFile -> return options
+    LoadFileWithContext -> expandGhcOptionResponseFile options
+    LoadUnitsFromCradle -> common
+    LoadUnitsInferred -> common
+
 stackTestCases :: [TestTree]
 stackTestCases =
   [ expectFailBecause "stack repl does not fail on an invalid cabal file" $
@@ -277,6 +456,8 @@
   , biosTestCase "multi-stack" $ runTestEnv "./multi-stack" $ do {- tests if both components can be loaded -}
       testDirectoryM isStackCradle "app/Main.hs"
       testDirectoryM isStackCradle "src/Lib.hs"
+  , biosTestCaseMulti "multi-stack-multi-modes" $ runTestEnv "./multi-stack" $ do
+      testDirectoryM isStackCradle "app/Main.hs"
   , biosTestCase "nested-stack" $ runTestEnv "./nested-stack" $ do
       stackAttemptLoad "sub-comp/Lib.hs"
       assertComponentOptions $ \opts ->
@@ -293,6 +474,22 @@
       {- tests if both components can be loaded -}
       testDirectoryM isStackCradle "appA/src/Lib.hs"
       testDirectoryM isStackCradle "appB/src/Lib.hs"
+  , biosTestCase "multi-stack-with-load" $ runTestModeEnv "multi-stack-with-load" LoadUnitsFromCradle $ do
+      testDirectoryM isStackCradle "appA/src/LibA.hs"
+      assertComponentOptions $ \ opts0 -> do
+        let opts = componentOptions opts0
+        unless (any ("appA" `isInfixOf`) opts) $
+          assertFailure $ "Missing appA: " ++ unwords opts
+        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"
+      assertComponentOptions $ \ opts0 -> do
+        let opts = componentOptions opts0
+        unless (any ("appA" `isInfixOf`) opts) $
+          assertFailure $ "Missing appA: " ++ unwords opts
+        unless (any ("appB" `isInfixOf`) opts) $
+          assertFailure $ "Missing appB: " ++ unwords opts
   ,
     -- Test for special characters in the path for parsing of the ghci-scripts.
     -- Issue https://github.com/mpickering/hie-bios/issues/162
@@ -300,11 +497,11 @@
       testDirectoryM isStackCradle "A.hs"
       testDirectoryM isStackCradle "B.hs"
   , testGroup "Implicit cradle tests"
-      [ biosTestCase "implicit-stack" $ runTestEnv "./implicit-stack" $
+      [ biosTestCase "implicit-stack" $ runTestModeEnv "./implicit-stack" LoadFile $ do
           testImplicitDirectoryM isStackCradle "Main.hs"
-      , biosTestCase "implicit-stack-multi" $ runTestEnv "./implicit-stack-multi" $ do
+      , biosTestCase "implicit-stack-multi" $ runTestModeEnv "./implicit-stack-multi" LoadFile $ do
           testImplicitDirectoryM isStackCradle "Main.hs"
-          testImplicitDirectoryM isStackCradle "other-package/Main.hs"
+          testImplicitDirectoryM  isStackCradle "other-package/Main.hs"
       ]
   ]
   where
@@ -312,7 +509,7 @@
     stackAttemptLoad fp = do
       initCradle fp
       assertCradle isStackCradle
-      loadComponentOptions fp
+      loadComponentOptions fp []
 
 directTestCases :: [TestTree]
 directTestCases =
@@ -353,12 +550,27 @@
 
 infix 1 `shouldMatchList`
 
-biosTestCase :: TestName -> (Bool -> Assertion) -> TestTree
+biosTestCase :: TestName -> (TestConfig -> Assertion) -> TestTree
 biosTestCase name assertion = askOption @VerboseLogging (\case
-  VerboseLogging verbose -> testCase name (assertion verbose)
+  VerboseLogging verbose ->
+      testCaseSteps name (\ stepF -> testWrapper verbose stepF)
   )
+  where
+    testWrapper verbose stepF =
+      assertion defConfig{testVerbose=verbose,testStepFunction=stepF}
 
+biosTestCaseMulti :: TestName -> (TestConfig -> Assertion) -> TestTree
+biosTestCaseMulti name act = testGroup name $ biosTestsForAllModes [LoadFileWithContext .. ] act
 
+biosTestCaseAll :: TestName -> (TestConfig -> Assertion) -> TestTree
+biosTestCaseAll name act = testGroup name $ biosTestsForAllModes [minBound .. ] act
+
+biosTestsForAllModes :: [LoadMode] -> (TestConfig -> Assertion) -> [TestTree]
+biosTestsForAllModes modes assertion =
+  [ biosTestCase (show mode) (\ conf -> assertion (conf{testLoadModeConfig=mode}))
+  | mode <- modes
+  ]
+
 -- ------------------------------------------------------------------
 -- Stack related helper functions
 -- ------------------------------------------------------------------
@@ -380,6 +592,7 @@
   , ("tests" </> "projects" </> "implicit-stack-multi", "stack.yaml", ["."])
   , ("tests" </> "projects" </> "multi-stack-with-yaml", "stack-alt.yaml", ["appA", "appB"])
   , ("tests" </> "projects" </> "stack-with-yaml", "stack-alt.yaml", ["."])
+  , ("tests" </> "projects" </> "multi-stack-with-load", "stack.yaml", ["appA", "appB"])
   ]
 
 stackYaml :: String -> [FilePath] -> String
@@ -389,12 +602,14 @@
 
 stackYamlResolver :: String
 stackYamlResolver =
-#if (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(9,10,0,0)))
-  "nightly-2025-04-24" -- GHC 9.10.1
+#if (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(9,12,0,0)))
+  "nightly-2025-08-07" -- GHC 9.12.2
+#elif (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(9,10,0,0)))
+  "lts-24.3" -- GHC 9.10.2
 #elif (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(9,8,0,0)))
   "lts-23.19" -- GHC 9.8.4
 #elif (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(9,6,0,0)))
-  "lts-22.43" -- GHC 9.6.6
+  "lts-22.44" -- GHC 9.6.7
 #elif (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(9,4,0,0)))
   "lts-21.25" -- GHC 9.4.8
 #elif (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(9,2,0,0)))
@@ -444,7 +659,7 @@
 -- | This option, when set to 'True', specifies that we should run in the
 -- «list tests» mode
 newtype IgnoreToolDeps = IgnoreToolDeps Bool
-  deriving (Eq, Ord, Typeable)
+  deriving (Eq, Ord)
 
 instance Tasty.IsOption IgnoreToolDeps where
   defaultValue = IgnoreToolDeps False
@@ -473,13 +688,12 @@
   \_opts _tree -> Nothing
 
 -- ------------------------------------------------------------------
--- Ignore test group if built with GHC 9.6.7 and GHC 9.12.2
+-- Ignore test group if not supported by any stackage snapshot
 -- ------------------------------------------------------------------
 
 ignoreOnUnsupportedGhc :: TestTree -> TestTree
 ignoreOnUnsupportedGhc tt =
-#if (defined(MIN_VERSION_GLASGOW_HASKELL) && (MIN_VERSION_GLASGOW_HASKELL(9,6,7,0) && !MIN_VERSION_GLASGOW_HASKELL(9,6,8,0)) || (MIN_VERSION_GLASGOW_HASKELL(9,12,2,0) && !MIN_VERSION_GLASGOW_HASKELL(9,12,3,0)))
-  ignoreTestBecause "Not supported on 9.6.7 and 9.12.2"
+#if (defined(MIN_VERSION_GLASGOW_HASKELL) && MIN_VERSION_GLASGOW_HASKELL(9,14,0,0))
+  ignoreTestBecause "Not supported on GHC 9.14"
 #endif
-  tt
-
+    tt
diff --git a/tests/ParserTests.hs b/tests/ParserTests.hs
--- a/tests/ParserTests.hs
+++ b/tests/ParserTests.hs
@@ -25,76 +25,90 @@
 
 main :: IO ()
 main = defaultMain $
-  testCase "Parser Tests" $ do
-    assertParser "cabal-1.yaml" (noDeps (Cabal $ CabalType (Just "lib:hie-bios") Nothing))
-    assertParser "stack-config.yaml" (noDeps (Stack $ StackType Nothing Nothing))
-    --assertParser "bazel.yaml" (noDeps Bazel)
-    assertParser "bios-1.yaml" (noDeps (Bios (Program "program") Nothing Nothing))
-    assertParser "bios-2.yaml" (noDeps (Bios (Program "program") (Just (Program "dep-program")) Nothing))
-    assertParser "bios-3.yaml" (noDeps (Bios (Command "shellcommand") Nothing Nothing))
-    assertParser "bios-4.yaml" (noDeps (Bios (Command "shellcommand") (Just (Command "dep-shellcommand")) Nothing))
-    assertParser "bios-5.yaml" (noDeps (Bios (Command "shellcommand") (Just (Program "dep-program")) Nothing))
-    assertParser "dependencies.yaml" (Config (CradleConfig ["depFile"] (Cabal $ CabalType (Just "lib:hie-bios") Nothing)))
-    assertParser "direct.yaml" (noDeps (Direct ["list", "of", "arguments"]))
-    assertParser "none.yaml" (noDeps None)
-    --assertParser "obelisk.yaml" (noDeps Obelisk)
-    assertParser "multi.yaml" (noDeps (Multi [("./src", CradleConfig [] (Cabal $ CabalType (Just "lib:hie-bios") Nothing))
-                                             ,("./test", CradleConfig [] (Cabal $ CabalType (Just "test") Nothing))]))
+  testGroup "Parser Tests"
+    [ assertParser "cabal-1.yaml" (noDeps (Cabal $ CabalType (Just "lib:hie-bios") Nothing Nothing))
+    , assertParser "stack-config.yaml" (noDeps (Stack $ StackType Nothing Nothing Nothing))
+    --, assertParser "bazel.yaml" (noDeps Bazel)
+    , assertParser "bios-1.yaml" (noDeps (Bios (Program "program") Nothing Nothing))
+    , assertParser "bios-2.yaml" (noDeps (Bios (Program "program") (Just (Program "dep-program")) Nothing))
+    , assertParser "bios-3.yaml" (noDeps (Bios (Command "shellcommand") Nothing Nothing))
+    , assertParser "bios-4.yaml" (noDeps (Bios (Command "shellcommand") (Just (Command "dep-shellcommand")) Nothing))
+    , assertParser "bios-5.yaml" (noDeps (Bios (Command "shellcommand") (Just (Program "dep-program")) Nothing))
+    , assertParser "dependencies.yaml" (Config (CradleConfig ["depFile"] (Cabal $ CabalType (Just "lib:hie-bios") Nothing Nothing)))
+    , assertParser "direct.yaml" (noDeps (Direct ["list", "of", "arguments"]))
+    , assertParser "none.yaml" (noDeps None)
+    --, assertParser "obelisk.yaml" (noDeps Obelisk)
+    , assertParser "multi.yaml" (noDeps (Multi [("./src", CradleConfig [] (Cabal $ CabalType (Just "lib:hie-bios") Nothing Nothing))
+                                             ,("./test", CradleConfig [] (Cabal $ CabalType (Just "test") Nothing Nothing))]))
 
-    assertParser "cabal-multi.yaml" (noDeps (CabalMulti (CabalType Nothing Nothing)
-                                                        [("./src", CabalType (Just "lib:hie-bios") Nothing)
-                                                        ,("./", CabalType (Just "lib:hie-bios") Nothing)]))
+    , assertParser "cabal-multi.yaml" (noDeps (CabalMulti (CabalType Nothing Nothing Nothing)
+                                                        [("./src", CabalType (Just "lib:hie-bios") Nothing Nothing)
+                                                        ,("./", CabalType (Just "lib:hie-bios") Nothing Nothing)]))
 
-    assertParser "stack-multi.yaml" (noDeps (StackMulti (StackType Nothing Nothing)
-                                                        [("./src", StackType (Just "lib:hie-bios") Nothing)
-                                                        ,("./", StackType (Just"lib:hie-bios") Nothing)]))
+    , assertParser "stack-multi.yaml" (noDeps (StackMulti (StackType Nothing Nothing Nothing)
+                                                        [("./src", StackType (Just "lib:hie-bios") Nothing Nothing)
+                                                        ,("./", StackType (Just"lib:hie-bios") Nothing Nothing)]))
 
-    assertParser "nested-cabal-multi.yaml" (noDeps (Multi [("./test/testdata", CradleConfig [] None)
+    , assertParser "nested-cabal-multi.yaml" (noDeps (Multi [("./test/testdata", CradleConfig [] None)
                                                           ,("./", CradleConfig [] (
-                                                                    CabalMulti (CabalType Nothing Nothing)
-                                                                               [("./src", CabalType (Just "lib:hie-bios") Nothing)
-                                                                               ,("./tests", CabalType (Just "parser-tests") Nothing)]))]))
+                                                                    CabalMulti (CabalType Nothing Nothing Nothing)
+                                                                               [("./src", CabalType (Just "lib:hie-bios") Nothing Nothing)
+                                                                               ,("./tests", CabalType (Just "parser-tests") Nothing Nothing)]))]))
 
-    assertParser "nested-stack-multi.yaml" (noDeps (Multi [("./test/testdata", CradleConfig [] None)
+    , assertParser "nested-stack-multi.yaml" (noDeps (Multi [("./test/testdata", CradleConfig [] None)
                                                           ,("./", CradleConfig [] (
-                                                                    StackMulti (StackType Nothing Nothing)
-                                                                               [("./src", StackType (Just "lib:hie-bios") Nothing)
-                                                                               ,("./tests", StackType (Just "parser-tests") Nothing)]))]))
+                                                                    StackMulti (StackType Nothing Nothing Nothing)
+                                                                               [("./src", StackType (Just "lib:hie-bios") Nothing Nothing)
+                                                                               ,("./tests", StackType (Just "parser-tests") Nothing Nothing)]))]))
     -- Assertions for cabal.project files
-    assertParser "cabal-with-project.yaml"
-      (noDeps (Cabal $ CabalType Nothing (Just "cabal.project.9.2.8")))
-    assertParser "cabal-with-both.yaml"
-      (noDeps (Cabal $ CabalType (Just "hie-bios:hie") (Just "cabal.project.9.2.8")))
-    assertParser "multi-cabal-with-project.yaml"
-      (noDeps (CabalMulti (CabalType Nothing (Just "cabal.project.9.2.8"))
-                          [("./src", CabalType (Just "lib:hie-bios") Nothing)
-                          ,("./vendor", CabalType (Just "parser-tests") Nothing)]))
+    , assertParser "cabal-with-project.yaml"
+      (noDeps (Cabal $ CabalType Nothing (Just "cabal.project.extra") Nothing))
+    , assertParser "cabal-with-both.yaml"
+      (noDeps (Cabal $ CabalType (Just "hie-bios:hie") (Just "cabal.project.extra") Nothing))
+    , assertParser "multi-cabal-with-project.yaml"
+      (noDeps (CabalMulti (CabalType Nothing (Just "cabal.project.extra") Nothing)
+                          [("./src", CabalType (Just "lib:hie-bios") Nothing Nothing)
+                          ,("./vendor", CabalType (Just "parser-tests") Nothing Nothing)]))
+    , assertParser "multi-cabal-with-load.yaml"
+      (noDeps (CabalMulti (CabalType Nothing (Just "cabal.project.extra") (Just ["lib:hie-bios","parser-tests","some-other"]))
+                          [("./src", CabalType (Just "lib:hie-bios") Nothing Nothing)
+                          ,("./vendor", CabalType (Just "parser-tests") Nothing Nothing)]))
+    , assertParser "cabal-with-load.yaml"
+      (noDeps (Cabal (CabalType Nothing Nothing (Just ["lib:hie-bios","parser-tests","some-other"]))))
     -- Assertions for stack.yaml files
-    assertParser "stack-with-yaml.yaml"
-      (noDeps (Stack $ StackType Nothing (Just "stack-8.8.3.yaml")))
-    assertParser "stack-with-both.yaml"
-      (noDeps (Stack $ StackType (Just "hie-bios:hie") (Just "stack-8.8.3.yaml")))
-    assertParser "multi-stack-with-yaml.yaml"
-      (noDeps (StackMulti (StackType Nothing (Just "stack-8.8.3.yaml"))
-                          [("./src", StackType (Just "lib:hie-bios") Nothing)
-                          ,("./vendor", StackType (Just "parser-tests") Nothing)]))
+    , assertParser "stack-with-yaml.yaml"
+      (noDeps (Stack $ StackType Nothing (Just "stack-8.8.3.yaml") Nothing))
+    , assertParser "stack-with-both.yaml"
+      (noDeps (Stack $ StackType (Just "hie-bios:hie") (Just "stack-8.8.3.yaml") Nothing))
+    , assertParser "multi-stack-with-yaml.yaml"
+      (noDeps (StackMulti (StackType Nothing (Just "stack-8.8.3.yaml") Nothing)
+                          [("./src", StackType (Just "lib:hie-bios") Nothing Nothing)
+                          ,("./vendor", StackType (Just "parser-tests") Nothing Nothing)]))
 
-    assertCustomParser "ch-cabal.yaml"
+    , assertCustomParser "ch-cabal.yaml"
       (noDeps (Other CabalHelperCabal $ simpleCabalHelperYaml "cabal"))
-    assertCustomParser "ch-stack.yaml"
+    , assertCustomParser "ch-stack.yaml"
       (noDeps (Other CabalHelperStack $ simpleCabalHelperYaml "stack"))
-    assertCustomParser "multi-ch.yaml"
+    , assertCustomParser "multi-ch.yaml"
       (noDeps (Multi
         [ ("./src", CradleConfig [] (Other CabalHelperStack $ simpleCabalHelperYaml "stack"))
         , ("./input", CradleConfig [] (Other CabalHelperCabal $ simpleCabalHelperYaml "cabal"))
-        , ("./test", CradleConfig [] (Cabal $ CabalType (Just "test") Nothing))
+        , ("./test", CradleConfig [] (Cabal $ CabalType (Just "test") Nothing Nothing))
         , (".", CradleConfig [] None)
         ]))
-    assertParserFails "keys-not-unique-fails.yaml" invalidYamlException
-    assertParser "cabal-empty-config.yaml" (noDeps (Cabal $ CabalType Nothing Nothing))
+    , assertParserFails "keys-not-unique-fails.yaml" invalidYamlException
+    , assertParser "cabal-empty-config.yaml" (noDeps (Cabal $ CabalType Nothing Nothing Nothing))
+    , assertParserFails "single-components.yaml" aesonException
+    , assertParser "multi-stack-with-load.yaml"
+      (noDeps (StackMulti (StackType Nothing (Just "stack.extra.yaml") (Just ["lib:hie-bios","parser-tests","some-other"]))
+                          [("./src", StackType (Just "lib:hie-bios") Nothing Nothing)
+                          ,("./vendor", StackType (Just "parser-tests") Nothing Nothing)]))
+    , assertParser "stack-with-load.yaml"
+      (noDeps (Stack (StackType Nothing Nothing (Just ["lib:hie-bios","parser-tests","some-other"]))))
+    ]
 
-assertParser :: FilePath -> Config Void -> Assertion
-assertParser fp cc = do
+assertParser :: FilePath -> Config Void -> TestTree
+assertParser fp cc = testCase fp $ do
   conf <- readConfig (configDir </> fp)
   (conf == cc) @? (unlines [("Parser Failed: " ++ fp)
                            , "Expected: " ++ show cc
@@ -104,11 +118,15 @@
 invalidYamlException (InvalidYaml (Just _)) = True
 invalidYamlException _ = False
 
-assertParserFails :: Exception e => FilePath -> Selector e -> Assertion
-assertParserFails fp es = (readConfig (configDir </> fp) :: IO (Config Void)) `shouldThrow` es
+aesonException :: Selector ParseException
+aesonException (AesonException _) = True
+aesonException _ = False
 
-assertCustomParser :: FilePath -> Config CabalHelper -> Assertion
-assertCustomParser fp cc = do
+assertParserFails :: Exception e => FilePath -> Selector e -> TestTree
+assertParserFails fp es = testCase fp $ (readConfig (configDir </> fp) :: IO (Config Void)) `shouldThrow` es
+
+assertCustomParser :: FilePath -> Config CabalHelper -> TestTree
+assertCustomParser fp cc = testCase fp $ do
   conf <- readConfig (configDir </> fp)
   (conf == cc) @? (unlines [("Parser Failed: " ++ fp)
                           , "Expected: " ++ show cc
diff --git a/tests/Utils.hs b/tests/Utils.hs
--- a/tests/Utils.hs
+++ b/tests/Utils.hs
@@ -12,6 +12,7 @@
 
   -- * Run Tests
   runTestEnv,
+  runTestModeEnv,
   runTestEnv',
   runTestEnvLocal,
 
@@ -28,6 +29,7 @@
   askStep,
   askLogger,
   askCradle,
+  askLoadMode,
   askLoadResult,
   askOrLoadLibDir,
   askLibDir,
@@ -43,10 +45,8 @@
   initCradle,
   initImplicitCradle,
   loadComponentOptions,
-  loadComponentOptionsMultiStyle,
   loadRuntimeGhcLibDir,
   loadRuntimeGhcVersion,
-  inCradleRootDir,
   loadFileGhc,
   isCabalMultipleCompSupported',
 
@@ -67,6 +67,7 @@
   -- * High-level test helpers
   testDirectoryM,
   testImplicitDirectoryM,
+  testImplicitDirectoryWithContextM,
   findCradleForModuleM,
 ) where
 
@@ -78,6 +79,8 @@
 import Data.Void
 import qualified GHC as G
 import HIE.Bios.Cradle
+import HIE.Bios.Cradle.Cabal (isCabalMultipleCompSupported)
+import HIE.Bios.Cradle.ProgramVersions (makeVersions)
 import HIE.Bios.Environment
 import HIE.Bios.Flags
 import HIE.Bios.Ghc.Api
@@ -92,6 +95,8 @@
 import Colog.Core
 import qualified Data.Text as Text
 import Data.Function ((&))
+import qualified Control.Exception as C
+import System.Environment (lookupEnv)
 
 -- ---------------------------------------------------------------------------
 -- Test configuration and information
@@ -103,8 +108,9 @@
   { useTemporaryDirectory :: Bool
   , testProjectRoots :: FilePath
   , testVerbose :: Bool
+  , testStepFunction :: String -> IO ()
+  , testLoadModeConfig :: LoadMode
   }
-  deriving (Eq, Show, Ord)
 
 data TestEnv ext = TestEnv
   { testCradleType :: Maybe (Cradle ext)
@@ -113,26 +119,39 @@
   , testGhcVersionResult :: Maybe (CradleLoadResult String)
   , testRootDir :: FilePath
   , testLogger :: L.LogAction IO (L.WithSeverity HIE.Log)
+  , testLoadMode :: LoadMode
   }
 
 defConfig :: TestConfig
-defConfig = TestConfig True "./tests/projects" False
+defConfig =
+  TestConfig
+    { useTemporaryDirectory = True
+    , testProjectRoots = "./tests/projects"
+    , testVerbose = False
+    , testStepFunction = unLogAction logStringStderr
+    , testLoadModeConfig = LoadFile
+    }
 
-runTestEnv :: FilePath -> TestM a -> Bool -> IO a
-runTestEnv fp act verbose = runTestEnv' defConfig{testVerbose=verbose} fp act
+runTestEnv :: FilePath -> TestM a -> TestConfig -> IO a
+runTestEnv fp act testConf = runTestEnv' testConf fp act
 
-runTestEnvLocal :: FilePath -> TestM a -> Bool -> IO a
-runTestEnvLocal fp act verbose = runTestEnv' defConfig{useTemporaryDirectory = False,testVerbose=verbose} fp act
+runTestModeEnv :: FilePath -> LoadMode -> TestM a -> TestConfig -> IO a
+runTestModeEnv fp loadMode act testConf = runTestEnv' testConf{testLoadModeConfig=loadMode} fp act
 
+runTestEnvLocal :: FilePath -> TestM a -> TestConfig -> IO a
+runTestEnvLocal fp act testConf = runTestEnv' testConf{useTemporaryDirectory = False} fp act
+
 runTestEnv' :: TestConfig -> FilePath -> TestM a -> IO a
 runTestEnv' config root act = do
   -- We need to copy over the directory to somewhere outside the source tree
   -- when we test, since the cabal.project/stack.yaml/hie.yaml file in the root
   -- of this repository interferes with the test cradles!
-  let wrapper =
+  let wrapper r cont =
         if useTemporaryDirectory config
-          then withTempCopy
-          else \r cont -> cont r
+          then do
+            mkeep <- lookupEnv "KEEP_TEMP_DIRS"
+            withTempCopy (mkeep == Just "true") r cont
+          else cont r
       mkEnv root' =
         TestEnv
           { testCradleType = Nothing
@@ -141,13 +160,14 @@
           , testGhcVersionResult = Nothing
           , testRootDir = root'
           , testLogger = init_logger
+          , testLoadMode = testLoadModeConfig config
           }
-      realRoot = testProjectRoots config </> root
+  realRoot <- makeAbsolute $ testProjectRoots config </> root
   wrapper realRoot $ \root' -> flip evalStateT (mkEnv root') $ do
     step $ "Run test in: " <> root'
     act
   where
-    init_logger = L.logStringStderr
+    init_logger = LogAction (testStepFunction config)
       & L.cmap printLog
       & L.filterBySeverity (if testVerbose config then Debug else Error) getSeverity
     printLog (L.WithSeverity l sev) = "[" ++ show sev ++ "] " ++ show (pretty l)
@@ -243,6 +263,9 @@
 askLogger :: TestM (L.LogAction IO (L.WithSeverity HIE.Log))
 askLogger = gets testLogger
 
+askLoadMode :: TestM LoadMode
+askLoadMode = gets testLoadMode
+
 -- ---------------------------------------------------------------------------
 -- Test setup helpers
 -- ---------------------------------------------------------------------------
@@ -274,6 +297,7 @@
   crd <- case mcfg of
     Just cfg -> liftIO $ loadCradle logger cfg
     Nothing -> liftIO $ loadImplicitCradle logger a_fp
+  step $ "Cradle: " ++ show crd
   setCradle crd
 
 initImplicitCradle :: FilePath -> TestM ()
@@ -284,21 +308,16 @@
   crd <- liftIO $ loadImplicitCradle logger a_fp
   setCradle crd
 
-loadComponentOptions :: FilePath -> TestM ()
-loadComponentOptions fp = do
-  a_fp <- normFile fp
-  crd <- askCradle
-  step $ "Initialise flags for: " <> fp
-  clr <- liftIO $ getCompilerOptions a_fp LoadFile crd
-  setLoadResult clr
-
-loadComponentOptionsMultiStyle :: FilePath -> [FilePath] -> TestM ()
-loadComponentOptionsMultiStyle fp fps = do
+loadComponentOptions :: FilePath -> [FilePath] -> TestM ()
+loadComponentOptions fp extraFps = do
   a_fp <- normFile fp
-  a_fps <- mapM normFile fps
+  a_fps <- traverse normFile extraFps
   crd <- askCradle
-  step $ "Initialise flags for: " <> fp <> " and " <> show fps
-  clr <- liftIO $ getCompilerOptions a_fp (LoadWithContext a_fps) crd
+  mode <- askLoadMode
+  step $ "Initialise flags for " <> fp <> " using " <> show mode
+  when (not $ null extraFps) $ do
+    step $ "Provided context: " <> show extraFps
+  clr <- liftIO $ getCompilerOptions (TargetWithContext a_fp a_fps) mode crd
   setLoadResult clr
 
 loadRuntimeGhcLibDir :: TestM ()
@@ -322,26 +341,17 @@
   versions <- liftIO $ makeVersions (cradleLogger cr) root ((runGhcCmd . cradleOptsProg) cr)
   liftIO $ isCabalMultipleCompSupported versions
 
-inCradleRootDir :: TestM a -> TestM a
-inCradleRootDir act = do
-  crd <- askCradle
-  prev <- liftIO getCurrentDirectory
-  liftIO $ setCurrentDirectory (cradleRootDir crd)
-  a <- act
-  liftIO $ setCurrentDirectory prev
-  pure a
-
-loadFileGhc :: FilePath -> TestM ()
-loadFileGhc fp = do
+loadFileGhc :: FilePath -> [FilePath] -> TestM ()
+loadFileGhc fp extraFps = do
   libdir <- askOrLoadLibDir
   a_fp <- normFile fp
   stepF <- askStep
   step "Cradle load"
-  loadComponentOptions fp
+  loadComponentOptions fp extraFps
   opts <- assertLoadSuccess
   liftIO $
     G.runGhc (Just libdir) $ do
-      let (ini, _) = initSessionWithMessage (Just G.batchMsg) opts
+      let (ini, _) = initSessionWithMessage' True (Just G.batchMsg) opts
       sf <- ini
       case sf of
         -- Test resetting the targets
@@ -433,17 +443,20 @@
   assertLibDirVersion
   loadRuntimeGhcVersion
   assertGhcVersion
-  loadFileGhc file
+  loadFileGhc file []
 
 testImplicitDirectoryM :: (Cradle Void -> Bool) -> FilePath -> TestM ()
-testImplicitDirectoryM cradlePred file = do
+testImplicitDirectoryM cradlePred file = testImplicitDirectoryWithContextM cradlePred file []
+
+testImplicitDirectoryWithContextM :: (Cradle Void -> Bool) -> FilePath -> [FilePath] -> TestM ()
+testImplicitDirectoryWithContextM cradlePred file ctxt = do
   initImplicitCradle file
   assertCradle cradlePred
   loadRuntimeGhcLibDir
   assertLibDirVersion
   loadRuntimeGhcVersion
   assertGhcVersion
-  loadFileGhc file
+  loadFileGhc file ctxt
 
 findCradleForModuleM :: FilePath -> Maybe FilePath -> TestM ()
 findCradleForModuleM fp expected' = do
@@ -456,13 +469,21 @@
 -- Copy Directory system utilities
 -- ---------------------------------------------------------------------------
 
-withTempCopy :: FilePath -> (FilePath -> IO a) -> IO a
-withTempCopy srcDir f =
-  withSystemTempDirectory "hie-bios-test" $ \newDir -> do
-    exists <- doesDirectoryExist srcDir
-    when exists $ do
-      copyDir srcDir newDir
-    f newDir
+withTempCopy :: Bool -> FilePath -> (FilePath -> IO a) -> IO a
+withTempCopy keep srcDir f = getCanonicalTemporaryDirectory >>= \targetDir -> do
+  C.bracket
+    (liftIO (createTempDirectory targetDir template))
+    cleanup $ \newDir -> do
+      exists <- doesDirectoryExist srcDir
+      when exists $ do
+        copyDir srcDir newDir
+      f newDir
+  where
+    template = "hie-bios-test"
+    cleanup dir | keep = return ()
+       | otherwise = (liftIO . ignoringIOErrors . removeDirectoryRecursive) dir
+    ignoringIOErrors ioe = ioe `C.catch` (\e -> const (return ()) (e :: IOError))
+
 
 copyDir :: FilePath -> FilePath -> IO ()
 copyDir src dst = do
diff --git a/tests/configs/cabal-with-both.yaml b/tests/configs/cabal-with-both.yaml
--- a/tests/configs/cabal-with-both.yaml
+++ b/tests/configs/cabal-with-both.yaml
@@ -1,4 +1,4 @@
 cradle:
   cabal:
-    cabalProject: "cabal.project.9.2.8"
+    cabalProject: "cabal.project.extra"
     component: "hie-bios:hie"
diff --git a/tests/configs/cabal-with-load.yaml b/tests/configs/cabal-with-load.yaml
new file mode 100644
--- /dev/null
+++ b/tests/configs/cabal-with-load.yaml
@@ -0,0 +1,3 @@
+cradle:
+  cabal:
+    componentsToLoad: ["lib:hie-bios","parser-tests","some-other"]
diff --git a/tests/configs/cabal-with-project.yaml b/tests/configs/cabal-with-project.yaml
--- a/tests/configs/cabal-with-project.yaml
+++ b/tests/configs/cabal-with-project.yaml
@@ -1,3 +1,3 @@
 cradle:
   cabal:
-    cabalProject: "cabal.project.9.2.8"
+    cabalProject: "cabal.project.extra"
diff --git a/tests/configs/multi-cabal-with-load.yaml b/tests/configs/multi-cabal-with-load.yaml
new file mode 100644
--- /dev/null
+++ b/tests/configs/multi-cabal-with-load.yaml
@@ -0,0 +1,9 @@
+cradle:
+  cabal:
+    componentsToLoad: ["lib:hie-bios","parser-tests","some-other"]
+    cabalProject: "cabal.project.extra"
+    components:
+      - path: "./src"
+        component: "lib:hie-bios"
+      - path: "./vendor"
+        component: "parser-tests"
diff --git a/tests/configs/multi-cabal-with-project.yaml b/tests/configs/multi-cabal-with-project.yaml
--- a/tests/configs/multi-cabal-with-project.yaml
+++ b/tests/configs/multi-cabal-with-project.yaml
@@ -1,6 +1,6 @@
 cradle:
   cabal:
-    cabalProject: "cabal.project.9.2.8"
+    cabalProject: "cabal.project.extra"
     components:
       - path: "./src"
         component: "lib:hie-bios"
diff --git a/tests/configs/multi-stack-with-load.yaml b/tests/configs/multi-stack-with-load.yaml
new file mode 100644
--- /dev/null
+++ b/tests/configs/multi-stack-with-load.yaml
@@ -0,0 +1,9 @@
+cradle:
+  stack:
+    componentsToLoad: ["lib:hie-bios","parser-tests","some-other"]
+    stackYaml: "stack.extra.yaml"
+    components:
+      - path: "./src"
+        component: "lib:hie-bios"
+      - path: "./vendor"
+        component: "parser-tests"
diff --git a/tests/configs/single-components.yaml b/tests/configs/single-components.yaml
new file mode 100644
--- /dev/null
+++ b/tests/configs/single-components.yaml
@@ -0,0 +1,2 @@
+cabal:
+  components: "exe:t3"
diff --git a/tests/configs/stack-with-load.yaml b/tests/configs/stack-with-load.yaml
new file mode 100644
--- /dev/null
+++ b/tests/configs/stack-with-load.yaml
@@ -0,0 +1,3 @@
+cradle:
+  stack:
+    componentsToLoad: ["lib:hie-bios","parser-tests","some-other"]
diff --git a/tests/projects/cabal-with-ghc-and-project/cabal.project.9.2.8 b/tests/projects/cabal-with-ghc-and-project/cabal.project.9.2.8
deleted file mode 100644
--- a/tests/projects/cabal-with-ghc-and-project/cabal.project.9.2.8
+++ /dev/null
@@ -1,12 +0,0 @@
-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-9.2.8
diff --git a/tests/projects/cabal-with-ghc-and-project/cabal.project.extra b/tests/projects/cabal-with-ghc-and-project/cabal.project.extra
new file mode 100644
--- /dev/null
+++ b/tests/projects/cabal-with-ghc-and-project/cabal.project.extra
@@ -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-9.4.8
diff --git a/tests/projects/cabal-with-ghc-and-project/hie.yaml b/tests/projects/cabal-with-ghc-and-project/hie.yaml
--- a/tests/projects/cabal-with-ghc-and-project/hie.yaml
+++ b/tests/projects/cabal-with-ghc-and-project/hie.yaml
@@ -1,3 +1,3 @@
 cradle:
   cabal:
-    cabalProject: cabal.project.9.2.8
+    cabalProject: cabal.project.extra
diff --git a/tests/projects/cabal-with-ghc/cabal.project b/tests/projects/cabal-with-ghc/cabal.project
--- a/tests/projects/cabal-with-ghc/cabal.project
+++ b/tests/projects/cabal-with-ghc/cabal.project
@@ -9,4 +9,4 @@
 -- 'tests/BiosTests.hs'.
 --
 -- Additionally, CI needs a proper setup to execute this test-case.
-with-compiler: ghc-9.2.8
+with-compiler: ghc-9.4.8
diff --git a/tests/projects/cabal-with-project/cabal.project.9.2.8 b/tests/projects/cabal-with-project/cabal.project.9.2.8
deleted file mode 100644
--- a/tests/projects/cabal-with-project/cabal.project.9.2.8
+++ /dev/null
@@ -1,5 +0,0 @@
--- Test file.
-packages: ./
-
-package cabal-with-project
-    ghc-options: -O2
diff --git a/tests/projects/cabal-with-project/cabal.project.extra b/tests/projects/cabal-with-project/cabal.project.extra
new file mode 100644
--- /dev/null
+++ b/tests/projects/cabal-with-project/cabal.project.extra
@@ -0,0 +1,5 @@
+-- Test file.
+packages: ./
+
+package cabal-with-project
+    ghc-options: -O2
diff --git a/tests/projects/cabal-with-project/hie.yaml b/tests/projects/cabal-with-project/hie.yaml
--- a/tests/projects/cabal-with-project/hie.yaml
+++ b/tests/projects/cabal-with-project/hie.yaml
@@ -1,3 +1,3 @@
 cradle:
   cabal:
-    cabalProject: cabal.project.9.2.8
+    cabalProject: cabal.project.extra
diff --git a/tests/projects/multi-cabal-with-project/cabal.project.9.2.8 b/tests/projects/multi-cabal-with-project/cabal.project.9.2.8
deleted file mode 100644
--- a/tests/projects/multi-cabal-with-project/cabal.project.9.2.8
+++ /dev/null
@@ -1,6 +0,0 @@
-packages: ./appA ./appB
-
--- Only appA gets the config
-package appA
-    ghc-options: -O2
-
diff --git a/tests/projects/multi-cabal-with-project/cabal.project.extra b/tests/projects/multi-cabal-with-project/cabal.project.extra
new file mode 100644
--- /dev/null
+++ b/tests/projects/multi-cabal-with-project/cabal.project.extra
@@ -0,0 +1,6 @@
+packages: ./appA ./appB
+
+-- Only appA gets the config
+package appA
+    ghc-options: -O2
+
diff --git a/tests/projects/multi-cabal-with-project/hie.yaml b/tests/projects/multi-cabal-with-project/hie.yaml
--- a/tests/projects/multi-cabal-with-project/hie.yaml
+++ b/tests/projects/multi-cabal-with-project/hie.yaml
@@ -6,11 +6,11 @@
           cabal:
             - path: appA/src
               component: appA:lib
-              cabalProject: cabal.project.9.2.8
+              cabalProject: cabal.project.extra
     - path: "appB"
       config:
         cradle:
           cabal:
             - path: appB/src
               component: appB:lib
-              cabalProject: cabal.project.9.2.8
+              cabalProject: cabal.project.extra
diff --git a/tests/projects/multi-cabal/multi-cabal.cabal b/tests/projects/multi-cabal/multi-cabal.cabal
--- a/tests/projects/multi-cabal/multi-cabal.cabal
+++ b/tests/projects/multi-cabal/multi-cabal.cabal
@@ -1,4 +1,4 @@
-cabal-version:       >=2.0
+cabal-version:       2.0
 name:                multi-cabal
 version:             0.1.0.0
 build-type:          Simple
diff --git a/wrappers/cabal-with-repl b/wrappers/cabal-with-repl
new file mode 100644
--- /dev/null
+++ b/wrappers/cabal-with-repl
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+function out(){
+  echo "$1" >> "$HIE_BIOS_OUTPUT"
+}
+
+out "$(pwd)"
+for arg in "$@"; do
+  out "$arg"
+done
diff --git a/wrappers/cabal-with-repl.hs b/wrappers/cabal-with-repl.hs
new file mode 100644
--- /dev/null
+++ b/wrappers/cabal-with-repl.hs
@@ -0,0 +1,14 @@
+module Main (main) where
+
+import System.Directory (getCurrentDirectory)
+import System.Environment (getArgs, getEnv)
+import System.IO (openFile, hClose, hPutStrLn, IOMode(..))
+
+main :: IO ()
+main = do
+  args <- getArgs
+  output_file <- getEnv "HIE_BIOS_OUTPUT"
+  h <- openFile output_file AppendMode
+  getCurrentDirectory >>= hPutStrLn h
+  mapM_ (hPutStrLn h) args
+  hClose h
