Skip to content

Commit bc502c9

Browse files
authored
Keep track of not loaded files for cabal (#453)
When we fail to load some files X, we want to know exactly what files Y we actually did try to load. Where Y ⊆ X. In the context of HLS, X is the set of files which we already tried to load with the file that we want to additionally load into our HLS session. Y is a subset of X, chosen based on the Cradle type (bios and stack cradles do not support loading a component with the context information), and GHC and cabal-install version. In particular, cabal cradles can make use of the full set of X for initialising a session if and only if the GHC and cabal-install version is recent enough, *and* the user explicitly requested a multi-repl session. By tracking which files we have actually tried to load, HLS can then try to reduce the number of files we put into sequential loading loop once batch load is failed for haskell/haskell-language-server#4445
1 parent b750c11 commit bc502c9

File tree

14 files changed

+188
-71
lines changed

14 files changed

+188
-71
lines changed

exe/Main.hs

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ main = do
8787
res <- forM files $ \fp -> do
8888
res <- getCompilerOptions fp LoadFile cradle
8989
case res of
90-
CradleFail (CradleError _deps _ex err) ->
90+
CradleFail (CradleError _deps _ex err _fps) ->
9191
return $ "Failed to show flags for \""
9292
++ fp
9393
++ "\": " ++ show err

hie-bios.cabal

+5
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ Extra-Source-Files: README.md
138138
tests/projects/stack-with-yaml/hie.yaml
139139
tests/projects/stack-with-yaml/stack-with-yaml.cabal
140140
tests/projects/stack-with-yaml/src/Lib.hs
141+
tests/projects/failing-multi-repl-cabal-project/multi-repl-cabal-fail/app/Main.hs
142+
tests/projects/failing-multi-repl-cabal-project/multi-repl-cabal-fail/multi-repl-cabal-fail.cabal
143+
tests/projects/failing-multi-repl-cabal-project/multi-repl-cabal-fail/src/Fail.hs
144+
tests/projects/failing-multi-repl-cabal-project/multi-repl-cabal-fail/src/Lib.hs
145+
tests/projects/failing-multi-repl-cabal-project/NotInPath.hs
141146

142147
tested-with: GHC ==9.2.8 || ==9.4.8 || ==9.6.5 || ==9.8.2 || ==9.10.1 || ==9.12.1
143148

src/HIE/Bios/Cradle.hs

+84-63
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ module HIE.Bios.Cradle (
2525
, makeCradleResult
2626
-- | Cradle project configuration types
2727
, CradleProjectConfig(..)
28+
29+
-- expose to tests
30+
, makeVersions
31+
, isCabalMultipleCompSupported
32+
, ProgramVersions
2833
) where
2934

3035
import Control.Applicative ((<|>), optional)
@@ -47,9 +52,10 @@ import qualified Data.Conduit.Combinators as C
4752
import qualified Data.Conduit as C
4853
import qualified Data.Conduit.Text as C
4954
import qualified Data.HashMap.Strict as Map
55+
import qualified Data.HashSet as S
5056
import Data.Maybe (fromMaybe, maybeToList)
5157
import Data.List
52-
import Data.List.Extra (trimEnd)
58+
import Data.List.Extra (trimEnd, nubOrd)
5359
import Data.Ord (Down(..))
5460
import qualified Data.Text as T
5561
import System.Environment
@@ -73,6 +79,7 @@ import GHC.ResponseFile (escapeArgs)
7379
import Data.Version
7480
import Data.IORef
7581
import Text.ParserCombinators.ReadP (readP_to_S)
82+
import Data.Tuple.Extra (fst3, snd3, thd3)
7683

7784
----------------------------------------------------------------
7885

@@ -129,6 +136,7 @@ data ConcreteCradle a
129136
| ConcreteOther a
130137
deriving Show
131138

139+
132140
-- | ConcreteCradle augmented with information on which file the
133141
-- cradle applies
134142
data ResolvedCradle a
@@ -243,7 +251,7 @@ resolvedCradlesToCradle logger buildCustomCradle root cs = mdo
243251
case selectCradle (prefix . fst) absfp cradleActions of
244252
Just (rc, act) -> do
245253
addActionDeps (cradleDeps rc) <$> runCradle act fp prev
246-
Nothing -> return $ CradleFail $ CradleError [] ExitSuccess (err_msg fp)
254+
Nothing -> return $ CradleFail $ CradleError [] ExitSuccess (err_msg fp) [fp]
247255
, runGhcCmd = run_ghc_cmd
248256
}
249257
}
@@ -518,7 +526,7 @@ biosAction wdir bios bios_deps l fp loadStyle = do
518526
-- delimited by newlines.
519527
-- Execute the bios action and add dependencies of the cradle.
520528
-- Removes all duplicates.
521-
return $ makeCradleResult (ex, std, wdir, fromMaybe [] res) deps
529+
return $ makeCradleResult (ex, std, wdir, fromMaybe [] res) deps [fp]
522530

523531
callableToProcess :: Callable -> Maybe String -> IO CreateProcess
524532
callableToProcess (Command shellCommand) file = do
@@ -788,6 +796,15 @@ cabalGhcDirs l cabalProject workDir = do
788796
where
789797
projectFileArgs = projectFileProcessArgs cabalProject
790798

799+
isCabalMultipleCompSupported :: MonadIO m => ProgramVersions -> m Bool
800+
isCabalMultipleCompSupported vs = do
801+
cabal_version <- liftIO $ runCachedIO $ cabalVersion vs
802+
ghc_version <- liftIO $ runCachedIO $ ghcVersion vs
803+
-- determine which load style is supported by this cabal cradle.
804+
case (cabal_version, ghc_version) of
805+
(Just cabal, Just ghc) -> pure $ ghc >= makeVersion [9, 4] && cabal >= makeVersion [3, 11]
806+
_ -> pure False
807+
791808
cabalAction
792809
:: ResolvedCradles a
793810
-> FilePath
@@ -798,84 +815,84 @@ cabalAction
798815
-> LoadStyle
799816
-> CradleLoadResultT IO ComponentOptions
800817
cabalAction (ResolvedCradles root cs vs) workDir mc l projectFile fp loadStyle = do
801-
cabal_version <- liftIO $ runCachedIO $ cabalVersion vs
802-
ghc_version <- liftIO $ runCachedIO $ ghcVersion vs
818+
multiCompSupport <- isCabalMultipleCompSupported vs
803819
-- determine which load style is supported by this cabal cradle.
804-
determinedLoadStyle <- case (cabal_version, ghc_version) of
805-
(Just cabal, Just ghc)
806-
-- Multi-component supported from cabal-install 3.11
807-
-- and ghc 9.4
808-
| LoadWithContext _ <- loadStyle ->
809-
if ghc >= makeVersion [9,4] && cabal >= makeVersion [3,11]
810-
then pure loadStyle
811-
else do
812-
liftIO $ l <& WithSeverity
813-
(LogLoadWithContextUnsupported "cabal"
814-
$ Just "cabal or ghc version is too old. We require `cabal >= 3.11` and `ghc >= 9.4`"
815-
)
816-
Warning
817-
pure LoadFile
818-
_ -> pure LoadFile
819-
820-
let cabalArgs = case determinedLoadStyle of
821-
LoadFile -> [fromMaybe (fixTargetPath fp) mc]
822-
LoadWithContext fps -> concat
823-
[ [ "--keep-temp-files"
824-
, "--enable-multi-repl"
825-
, fromMaybe (fixTargetPath fp) mc
826-
]
827-
, [fromMaybe (fixTargetPath old_fp) old_mc
828-
| old_fp <- fps
829-
-- Lookup the component for the old file
830-
, Just (ResolvedCradle{concreteCradle = ConcreteCabal ct}) <- [selectCradle prefix old_fp cs]
831-
-- Only include this file if the old component is in the same project
832-
, (projectConfigFromMaybe root (cabalProjectFile ct)) == projectFile
833-
, let old_mc = cabalComponent ct
834-
]
835-
]
820+
determinedLoadStyle <- case loadStyle of
821+
LoadWithContext _ | not multiCompSupport -> do
822+
liftIO $
823+
l
824+
<& WithSeverity
825+
( LogLoadWithContextUnsupported "cabal" $
826+
Just "cabal or ghc version is too old. We require `cabal >= 3.11` and `ghc >= 9.4`"
827+
)
828+
Warning
829+
pure LoadFile
830+
_ -> pure loadStyle
831+
832+
let fpModule = fromMaybe (fixTargetPath fp) mc
833+
let (cabalArgs, loadingFiles, extraDeps) = case determinedLoadStyle of
834+
LoadFile -> ([fpModule], [fp], [])
835+
LoadWithContext fps ->
836+
let allModulesFpsDeps = ((fpModule, fp, []) : moduleFilesFromSameProject fps)
837+
allModules = nubOrd $ fst3 <$> allModulesFpsDeps
838+
allFiles = nubOrd $ snd3 <$> allModulesFpsDeps
839+
allFpsDeps = nubOrd $ concatMap thd3 allModulesFpsDeps
840+
in (["--keep-temp-files", "--enable-multi-repl"] ++ allModules, allFiles, allFpsDeps)
836841

837842
liftIO $ l <& LogComputedCradleLoadStyle "cabal" determinedLoadStyle `WithSeverity` Info
843+
liftIO $ l <& LogCabalLoad fp mc (prefix <$> cs) loadingFiles `WithSeverity` Debug
838844

839-
let
840-
cabalCommand = "v2-repl"
845+
let cabalCommand = "v2-repl"
841846

842-
cabalProc <- cabalProcess l projectFile workDir cabalCommand cabalArgs `modCradleError` \err -> do
847+
cabalProc <-
848+
cabalProcess l projectFile workDir cabalCommand cabalArgs `modCradleError` \err -> do
843849
deps <- cabalCradleDependencies projectFile workDir workDir
844-
pure $ err { cradleErrorDependencies = cradleErrorDependencies err ++ deps }
850+
pure $ err {cradleErrorDependencies = cradleErrorDependencies err ++ deps}
845851

846852
(ex, output, stde, [(_, maybeArgs)]) <- liftIO $ readProcessWithOutputs [hie_bios_output] l workDir cabalProc
847853
let args = fromMaybe [] maybeArgs
848854

849855
let errorDetails =
850-
["Failed command: " <> prettyCmdSpec (cmdspec cabalProc)
851-
, unlines output
852-
, unlines stde
853-
, unlines $ args
854-
, "Process Environment:"]
855-
<> prettyProcessEnv cabalProc
856+
[ "Failed command: " <> prettyCmdSpec (cmdspec cabalProc),
857+
unlines output,
858+
unlines stde,
859+
unlines args,
860+
"Process Environment:"
861+
]
862+
<> prettyProcessEnv cabalProc
856863

857864
when (ex /= ExitSuccess) $ do
858865
deps <- liftIO $ cabalCradleDependencies projectFile workDir workDir
859866
let cmd = show (["cabal", cabalCommand] <> cabalArgs)
860867
let errorMsg = "Failed to run " <> cmd <> " in directory \"" <> workDir <> "\". Consult the logs for full command and error."
861-
throwCE (CradleError deps ex ([errorMsg] <> errorDetails))
868+
throwCE (CradleError deps ex ([errorMsg] <> errorDetails) loadingFiles)
862869

863870
case processCabalWrapperArgs args of
864871
Nothing -> do
865872
-- Provide some dependencies an IDE can look for to trigger a reload.
866873
-- Best effort. Assume the working directory is the
867874
-- root of the component, so we are right in trivial cases at least.
868875
deps <- liftIO $ cabalCradleDependencies projectFile workDir workDir
869-
throwCE (CradleError deps ex $ ["Failed to parse result of calling cabal" ] <> errorDetails)
876+
throwCE (CradleError (deps <> extraDeps) ex (["Failed to parse result of calling cabal"] <> errorDetails) loadingFiles)
870877
Just (componentDir, final_args) -> do
871878
deps <- liftIO $ cabalCradleDependencies projectFile workDir componentDir
872-
CradleLoadResultT $ pure $ makeCradleResult (ex, stde, componentDir, final_args) deps
879+
CradleLoadResultT $ pure $ makeCradleResult (ex, stde, componentDir, final_args) (deps <> extraDeps) loadingFiles
873880
where
874881
-- Need to make relative on Windows, due to a Cabal bug with how it
875-
-- parses file targets with a C: drive in it
882+
-- parses file targets with a C: drive in it. So we decide to make
883+
-- the paths relative to the working directory.
876884
fixTargetPath x
877885
| isWindows && hasDrive x = makeRelative workDir x
878886
| otherwise = x
887+
moduleFilesFromSameProject fps =
888+
[ (fromMaybe (fixTargetPath file) old_mc, file, deps)
889+
| file <- fps,
890+
-- Lookup the component for the old file
891+
Just (ResolvedCradle {concreteCradle = ConcreteCabal ct, cradleDeps = deps}) <- [selectCradle prefix file cs],
892+
-- Only include this file if the old component is in the same project
893+
(projectConfigFromMaybe root (cabalProjectFile ct)) == projectFile,
894+
let old_mc = cabalComponent ct
895+
]
879896

880897
removeInteractive :: [String] -> [String]
881898
removeInteractive = filter (/= "--interactive")
@@ -928,7 +945,7 @@ cabalWorkDir wdir =
928945
data CradleProjectConfig
929946
= NoExplicitConfig
930947
| ExplicitConfig FilePath
931-
deriving Eq
948+
deriving (Eq, Show)
932949

933950
-- | Create an explicit project configuration. Expects a working directory
934951
-- followed by an optional name of the project configuration.
@@ -987,7 +1004,7 @@ stackAction
9871004
-> FilePath
9881005
-> LoadStyle
9891006
-> IO (CradleLoadResult ComponentOptions)
990-
stackAction workDir mc syaml l _fp loadStyle = do
1007+
stackAction workDir mc syaml l fp loadStyle = do
9911008
logCradleHasNoSupportForLoadWithContext l loadStyle "stack"
9921009
let ghcProcArgs = ("stack", stackYamlProcessArgs syaml <> ["exec", "ghc", "--"])
9931010
-- Same wrapper works as with cabal
@@ -1011,10 +1028,11 @@ stackAction workDir mc syaml l _fp loadStyle = do
10111028
-- the root of the component, so we are right in trivial cases at least.
10121029
deps <- stackCradleDependencies workDir workDir syaml
10131030
pure $ CradleFail
1014-
(CradleError deps ex1 $
1015-
[ "Failed to parse result of calling stack" ]
1031+
(CradleError deps ex1
1032+
([ "Failed to parse result of calling stack" ]
10161033
++ stde
1017-
++ args
1034+
++ args)
1035+
[fp]
10181036
)
10191037

10201038
Just (componentDir, ghc_args) -> do
@@ -1025,6 +1043,7 @@ stackAction workDir mc syaml l _fp loadStyle = do
10251043
, ghc_args ++ pkg_ghc_args
10261044
)
10271045
deps
1046+
[fp]
10281047

10291048
stackProcess :: CradleProjectConfig -> [String] -> CreateProcess
10301049
stackProcess syaml args = proc "stack" $ stackYamlProcessArgs syaml <> args
@@ -1217,10 +1236,10 @@ removeFileIfExists f = do
12171236
yes <- doesFileExist f
12181237
when yes (removeFile f)
12191238

1220-
makeCradleResult :: (ExitCode, [String], FilePath, [String]) -> [FilePath] -> CradleLoadResult ComponentOptions
1221-
makeCradleResult (ex, err, componentDir, gopts) deps =
1239+
makeCradleResult :: (ExitCode, [String], FilePath, [String]) -> [FilePath] -> [FilePath] -> CradleLoadResult ComponentOptions
1240+
makeCradleResult (ex, err, componentDir, gopts) deps loadingFiles =
12221241
case ex of
1223-
ExitFailure _ -> CradleFail (CradleError deps ex err)
1242+
ExitFailure _ -> CradleFail (CradleError deps ex err loadingFiles)
12241243
_ ->
12251244
let compOpts = ComponentOptions gopts componentDir deps
12261245
in CradleSuccess compOpts
@@ -1252,11 +1271,13 @@ readProcessWithCwd' l createdProcess stdin = do
12521271
case mResult of
12531272
Just (ExitSuccess, stdo, _) -> pure stdo
12541273
Just (exitCode, stdo, stde) -> throwCE $
1255-
CradleError [] exitCode $
1256-
["Error when calling " <> cmdString, stdo, stde] <> prettyProcessEnv createdProcess
1274+
CradleError [] exitCode
1275+
(["Error when calling " <> cmdString, stdo, stde] <> prettyProcessEnv createdProcess)
1276+
[]
12571277
Nothing -> throwCE $
1258-
CradleError [] ExitSuccess $
1259-
["Couldn't execute " <> cmdString] <> prettyProcessEnv createdProcess
1278+
CradleError [] ExitSuccess
1279+
(["Couldn't execute " <> cmdString] <> prettyProcessEnv createdProcess)
1280+
[]
12601281

12611282
-- | Log that the cradle has no supported for loading with context, if and only if
12621283
-- 'LoadWithContext' was requested.

src/HIE/Bios/Internal/Debug.hs

+4-2
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,13 @@ debugInfo fp cradle = unlines <$> do
4848
, "Cradle: " ++ crdl
4949
, "Dependencies: " ++ unwords deps
5050
]
51-
CradleFail (CradleError deps ext stderr) ->
51+
CradleFail (CradleError deps ext stderr extraFiles) ->
5252
return ["Cradle failed to load"
5353
, "Deps: " ++ show deps
5454
, "Exit Code: " ++ show ext
55-
, "Stderr: " ++ unlines stderr]
55+
, "Stderr: " ++ unlines stderr
56+
, "Failed: " ++ unlines extraFiles
57+
]
5658
CradleNone ->
5759
return ["No cradle"]
5860
where

src/HIE/Bios/Types.hs

+10
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ data Log
9999
| LogRequestedCradleLoadStyle !T.Text !LoadStyle
100100
| LogComputedCradleLoadStyle !T.Text !LoadStyle
101101
| LogLoadWithContextUnsupported !T.Text !(Maybe T.Text)
102+
| LogCabalLoad !FilePath !(Maybe String) ![FilePath] ![FilePath]
102103
deriving (Show)
103104

104105
instance Pretty Log where
@@ -135,6 +136,11 @@ instance Pretty Log where
135136
Nothing -> "."
136137
Just reason -> ", because:" <+> pretty reason <> "."
137138
<+> "Falling back loading to single file mode."
139+
pretty (LogCabalLoad file prefixes projectFile crs) =
140+
"Cabal Loading file" <+> pretty file
141+
<> line <> indent 4 "from project: " <+> pretty projectFile
142+
<> line <> indent 4 "with prefixes:" <+> pretty prefixes
143+
<> line <> indent 4 "with actual loading files:" <+> pretty crs
138144

139145
-- | The 'LoadStyle' instructs a cradle on how to load a given file target.
140146
data LoadStyle
@@ -266,6 +272,10 @@ data CradleError = CradleError
266272
, cradleErrorStderr :: [String]
267273
-- ^ Standard error output that can be shown to users to explain
268274
-- the loading error.
275+
, cradleErrorLoadingFiles :: [FilePath]
276+
-- ^ files that were attempted to be loaded by the cradle.
277+
-- This can be useful if we are loading multiple files at once,
278+
-- e.g. in a cabal cradle with the multi-repl feature.
269279
}
270280
deriving (Show, Eq)
271281

tests/BiosTests.hs

+22-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import Control.Monad ( forM_ )
1919
import Data.List ( sort, isPrefixOf )
2020
import Data.Typeable
2121
import System.Directory
22-
import System.FilePath ((</>))
22+
import System.FilePath ((</>), makeRelative)
2323
import System.Exit (ExitCode(ExitSuccess, ExitFailure))
2424
import Control.Monad.Extra (unlessM)
2525
import qualified HIE.Bios.Ghc.Gap as Gap
@@ -138,11 +138,25 @@ biosTestCases =
138138

139139
cabalTestCases :: ToolDependency -> [TestTree]
140140
cabalTestCases extraGhcDep =
141-
[ testCaseSteps "failing-cabal" $ runTestEnv "./failing-cabal" $ do
141+
[
142+
testCaseSteps "failing-cabal" $ runTestEnv "./failing-cabal" $ do
142143
cabalAttemptLoad "MyLib.hs"
143144
assertCradleError (\CradleError {..} -> do
144145
cradleErrorExitCode @?= ExitFailure 1
145146
cradleErrorDependencies `shouldMatchList` ["failing-cabal.cabal", "cabal.project", "cabal.project.local"])
147+
, testCaseSteps "failing-cabal-multi-repl-with-shrink-error-files" $ runTestEnv "./failing-multi-repl-cabal-project" $ do
148+
cabalAttemptLoadFiles "multi-repl-cabal-fail/app/Main.hs" ["multi-repl-cabal-fail/src/Lib.hs", "multi-repl-cabal-fail/src/Fail.hs", "NotInPath.hs"]
149+
root <- askRoot
150+
multiSupported <- isCabalMultipleCompSupported'
151+
if multiSupported
152+
then
153+
assertCradleError (\CradleError {..} -> do
154+
cradleErrorExitCode @?= ExitFailure 1
155+
cradleErrorDependencies `shouldMatchList` ["cabal.project","cabal.project.local","multi-repl-cabal-fail.cabal"]
156+
-- NotInPath.hs does not match the cradle for `app/Main.hs`, so it should not be tried.
157+
(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"])
158+
else assertLoadSuccess >>= \ComponentOptions {} -> do
159+
return ()
146160
, testCaseSteps "simple-cabal" $ runTestEnv "./simple-cabal" $ do
147161
testDirectoryM isCabalCradle "B.hs"
148162
, testCaseSteps "nested-cabal" $ runTestEnv "./nested-cabal" $ do
@@ -224,6 +238,12 @@ cabalTestCases extraGhcDep =
224238
assertCradle isCabalCradle
225239
loadComponentOptions fp
226240

241+
cabalAttemptLoadFiles :: FilePath -> [FilePath] -> TestM ()
242+
cabalAttemptLoadFiles fp fps = do
243+
initCradle fp
244+
assertCradle isCabalCradle
245+
loadComponentOptionsMultiStyle fp fps
246+
227247
cabalLoadOptions :: FilePath -> TestM ComponentOptions
228248
cabalLoadOptions fp = do
229249
initCradle fp

0 commit comments

Comments
 (0)