-
-
Notifications
You must be signed in to change notification settings - Fork 389
/
Copy pathSession.hs
1237 lines (1132 loc) · 58.3 KB
/
Session.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
{-# LANGUAGE CPP #-}
{-# LANGUAGE TypeFamilies #-}
{-|
The logic for setting up a ghcide session by tapping into hie-bios.
-}
module Development.IDE.Session
(SessionLoadingOptions(..)
,CacheDirs(..)
,loadSessionWithOptions
,getInitialGhcLibDirDefault
,getHieDbLoc
,retryOnSqliteBusy
,retryOnException
,Log(..)
,runWithDb
) where
-- Unfortunately, we cannot use loadSession with ghc-lib since hie-bios uses
-- the real GHC library and the types are incompatible. Furthermore, when
-- building with ghc-lib we need to make this Haskell agnostic, so no hie-bios!
import Control.Concurrent.Strict
import Control.Exception.Safe as Safe
import Control.Monad
import Control.Monad.Extra as Extra
import Control.Monad.IO.Class
import qualified Crypto.Hash.SHA1 as H
import Data.Aeson hiding (Error)
import Data.Bifunctor
import qualified Data.ByteString.Base16 as B16
import qualified Data.ByteString.Char8 as B
import Data.Default
import Data.Either.Extra
import Data.Function
import Data.Hashable hiding (hash)
import qualified Data.HashMap.Strict as HM
import Data.IORef
import Data.List
import Data.List.Extra as L
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NE
import qualified Data.Map.Strict as Map
import Data.Maybe
import Data.Proxy
import qualified Data.Text as T
import Data.Time.Clock
import Data.Version
import Development.IDE.Core.RuleTypes
import Development.IDE.Core.Shake hiding (Log, knownTargets,
withHieDb)
import qualified Development.IDE.GHC.Compat as Compat
import Development.IDE.GHC.Compat.CmdLine
import Development.IDE.GHC.Compat.Core hiding (Target, TargetFile,
TargetModule, Var,
Warning, getOptions)
import qualified Development.IDE.GHC.Compat.Core as GHC
import Development.IDE.GHC.Compat.Env hiding (Logger)
import Development.IDE.GHC.Compat.Units (UnitId)
import Development.IDE.GHC.Util
import Development.IDE.Graph (Action)
import qualified Development.IDE.Session.Implicit as GhcIde
import Development.IDE.Types.Diagnostics
import Development.IDE.Types.Exports
import Development.IDE.Types.HscEnvEq (HscEnvEq, newHscEnvEq)
import Development.IDE.Types.Location
import Development.IDE.Types.Options
import GHC.ResponseFile
import qualified HIE.Bios as HieBios
import HIE.Bios.Environment hiding (getCacheDir)
import HIE.Bios.Types hiding (Log)
import qualified HIE.Bios.Types as HieBios
import Ide.Logger (Pretty (pretty),
Priority (Debug, Error, Info, Warning),
Recorder, WithPriority,
cmapWithPrio, logWith,
nest,
toCologActionWithPrio,
vcat, viaShow, (<+>))
import Ide.Types (SessionLoadingPreferenceConfig (..),
sessionLoading)
import Language.LSP.Protocol.Message
import Language.LSP.Server
import System.Directory
import qualified System.Directory.Extra as IO
import System.FilePath
import System.Info
import Control.Applicative (Alternative ((<|>)))
import Data.Void
import Control.Concurrent.STM.Stats (atomically, modifyTVar',
readTVar, writeTVar)
import Control.Concurrent.STM.TQueue
import Control.DeepSeq
import Control.Exception (evaluate)
import Control.Monad.IO.Unlift (MonadUnliftIO)
import Control.Monad.Trans.Cont (ContT (ContT, runContT))
import Data.Foldable (for_)
import Data.HashMap.Strict (HashMap)
import Data.HashSet (HashSet)
import qualified Data.HashSet as Set
import qualified Data.Set as OS
import Database.SQLite.Simple
import Development.IDE.Core.Tracing (withTrace)
import Development.IDE.Core.WorkerThread (awaitRunInThread,
withWorkerQueue)
import qualified Development.IDE.GHC.Compat.Util as Compat
import Development.IDE.Session.Diagnostics (renderCradleError)
import Development.IDE.Types.Shake (WithHieDb,
WithHieDbShield (..),
toNoFileKey)
import GHC.Data.Graph.Directed
import HieDb.Create
import HieDb.Types
import Ide.PluginUtils (toAbsolute)
import qualified System.Random as Random
import System.Random (RandomGen)
import Text.ParserCombinators.ReadP (readP_to_S)
import GHC.Driver.Env (hsc_all_home_unit_ids)
import GHC.Driver.Errors.Types
import GHC.Types.Error (errMsgDiagnostic,
singleMessage)
import GHC.Unit.State
#if MIN_VERSION_ghc(9,13,0)
import GHC.Driver.Make (checkHomeUnitsClosed)
#endif
data Log
= LogSettingInitialDynFlags
| LogGetInitialGhcLibDirDefaultCradleFail !CradleError !FilePath !(Maybe FilePath) !(Cradle Void)
| LogGetInitialGhcLibDirDefaultCradleNone
| LogHieDbRetry !Int !Int !Int !SomeException
| LogHieDbRetriesExhausted !Int !Int !Int !SomeException
| LogHieDbWriterThreadSQLiteError !SQLError
| LogHieDbWriterThreadException !SomeException
| LogInterfaceFilesCacheDir !FilePath
| LogKnownFilesUpdated !(HashMap Target (HashSet NormalizedFilePath))
| LogMakingNewHscEnv ![UnitId]
| LogDLLLoadError !String
| LogCradlePath !FilePath
| LogCradleNotFound !FilePath
| LogSessionLoadingResult !(Either [CradleError] (ComponentOptions, FilePath, String))
| LogCradle !(Cradle Void)
| LogNoneCradleFound FilePath
| LogNewComponentCache !(([FileDiagnostic], Maybe HscEnvEq), DependencyInfo)
| LogHieBios HieBios.Log
| LogSessionLoadingChanged
deriving instance Show Log
instance Pretty Log where
pretty = \case
LogNoneCradleFound path ->
"None cradle found for" <+> pretty path <+> ", ignoring the file"
LogSettingInitialDynFlags ->
"Setting initial dynflags..."
LogGetInitialGhcLibDirDefaultCradleFail cradleError rootDirPath hieYamlPath cradle ->
nest 2 $
vcat
[ "Couldn't load cradle for ghc libdir."
, "Cradle error:" <+> viaShow cradleError
, "Root dir path:" <+> pretty rootDirPath
, "hie.yaml path:" <+> pretty hieYamlPath
, "Cradle:" <+> viaShow cradle ]
LogGetInitialGhcLibDirDefaultCradleNone ->
"Couldn't load cradle. Cradle not found."
LogHieDbRetry delay maxDelay retriesRemaining e ->
nest 2 $
vcat
[ "Retrying hiedb action..."
, "delay:" <+> pretty delay
, "maximum delay:" <+> pretty maxDelay
, "retries remaining:" <+> pretty retriesRemaining
, "SQLite error:" <+> pretty (displayException e) ]
LogHieDbRetriesExhausted baseDelay maxDelay retriesRemaining e ->
nest 2 $
vcat
[ "Retries exhausted for hiedb action."
, "base delay:" <+> pretty baseDelay
, "maximum delay:" <+> pretty maxDelay
, "retries remaining:" <+> pretty retriesRemaining
, "Exception:" <+> pretty (displayException e) ]
LogHieDbWriterThreadSQLiteError e ->
nest 2 $
vcat
[ "HieDb writer thread SQLite error:"
, pretty (displayException e) ]
LogHieDbWriterThreadException e ->
nest 2 $
vcat
[ "HieDb writer thread exception:"
, pretty (displayException e) ]
LogInterfaceFilesCacheDir path ->
"Interface files cache directory:" <+> pretty path
LogKnownFilesUpdated targetToPathsMap ->
nest 2 $
vcat
[ "Known files updated:"
, viaShow $ (HM.map . Set.map) fromNormalizedFilePath targetToPathsMap
]
LogMakingNewHscEnv inPlaceUnitIds ->
"Making new HscEnv. In-place unit ids:" <+> pretty (map show inPlaceUnitIds)
LogDLLLoadError errorString ->
"Error dynamically loading libm.so.6:" <+> pretty errorString
LogCradlePath path ->
"Cradle path:" <+> pretty path
LogCradleNotFound path ->
vcat
[ "No [cradle](https://github.com/mpickering/hie-bios#hie-bios) found for" <+> pretty path <> "."
, "Proceeding with [implicit cradle](https://hackage.haskell.org/package/implicit-hie)."
, "You should ignore this message, unless you see a 'Multi Cradle: No prefixes matched' error." ]
LogSessionLoadingResult e ->
"Session loading result:" <+> viaShow e
LogCradle cradle ->
"Cradle:" <+> viaShow cradle
LogNewComponentCache componentCache ->
"New component cache HscEnvEq:" <+> viaShow componentCache
LogHieBios msg -> pretty msg
LogSessionLoadingChanged ->
"Session Loading config changed, reloading the full session."
-- | Bump this version number when making changes to the format of the data stored in hiedb
hiedbDataVersion :: String
hiedbDataVersion = "1"
data CacheDirs = CacheDirs
{ hiCacheDir, hieCacheDir, oCacheDir :: Maybe FilePath}
data SessionLoadingOptions = SessionLoadingOptions
{ findCradle :: FilePath -> IO (Maybe FilePath)
-- | Load the cradle with an optional 'hie.yaml' location.
-- If a 'hie.yaml' is given, use it to load the cradle.
-- Otherwise, use the provided project root directory to determine the cradle type.
, loadCradle :: Recorder (WithPriority Log) -> Maybe FilePath -> FilePath -> IO (HieBios.Cradle Void)
-- | Given the project name and a set of command line flags,
-- return the path for storing generated GHC artifacts,
-- or 'Nothing' to respect the cradle setting
, getCacheDirs :: String -> [String] -> IO CacheDirs
-- | Return the GHC lib dir to use for the 'unsafeGlobalDynFlags'
, getInitialGhcLibDir :: Recorder (WithPriority Log) -> FilePath -> IO (Maybe LibDir)
}
instance Default SessionLoadingOptions where
def = SessionLoadingOptions
{findCradle = HieBios.findCradle
,loadCradle = loadWithImplicitCradle
,getCacheDirs = getCacheDirsDefault
,getInitialGhcLibDir = getInitialGhcLibDirDefault
}
-- | Find the cradle for a given 'hie.yaml' configuration.
--
-- If a 'hie.yaml' is given, the cradle is read from the config.
-- If this config does not comply to the "hie.yaml"
-- specification, an error is raised.
--
-- If no location for "hie.yaml" is provided, the implicit config is used
-- using the provided root directory for discovering the project.
-- The implicit config uses different heuristics to determine the type
-- of the project that may or may not be accurate.
loadWithImplicitCradle
:: Recorder (WithPriority Log)
-> Maybe FilePath
-- ^ Optional 'hie.yaml' location. Will be used if given.
-> FilePath
-- ^ Root directory of the project. Required as a fallback
-- if no 'hie.yaml' location is given.
-> IO (HieBios.Cradle Void)
loadWithImplicitCradle recorder mHieYaml rootDir = do
let logger = toCologActionWithPrio (cmapWithPrio LogHieBios recorder)
case mHieYaml of
Just yaml -> HieBios.loadCradle logger yaml
Nothing -> GhcIde.loadImplicitCradle logger rootDir
getInitialGhcLibDirDefault :: Recorder (WithPriority Log) -> FilePath -> IO (Maybe LibDir)
getInitialGhcLibDirDefault recorder rootDir = do
hieYaml <- findCradle def (rootDir </> "a")
cradle <- loadCradle def recorder hieYaml rootDir
libDirRes <- getRuntimeGhcLibDir cradle
case libDirRes of
CradleSuccess libdir -> pure $ Just $ LibDir libdir
CradleFail err -> do
logWith recorder Error $ LogGetInitialGhcLibDirDefaultCradleFail err rootDir hieYaml cradle
pure Nothing
CradleNone -> do
logWith recorder Warning LogGetInitialGhcLibDirDefaultCradleNone
pure Nothing
-- | If the action throws exception that satisfies predicate then we sleep for
-- a duration determined by the random exponential backoff formula,
-- `uniformRandom(0, min (maxDelay, (baseDelay * 2) ^ retryAttempt))`, and try
-- the action again for a maximum of `maxRetryCount` times.
-- `MonadIO`, `MonadCatch` are used as constraints because there are a few
-- HieDb functions that don't return IO values.
retryOnException
:: (MonadIO m, MonadCatch m, RandomGen g, Exception e)
=> (e -> Maybe e) -- ^ only retry on exception if this predicate returns Just
-> Recorder (WithPriority Log)
-> Int -- ^ maximum backoff delay in microseconds
-> Int -- ^ base backoff delay in microseconds
-> Int -- ^ maximum number of times to retry
-> g -- ^ random number generator
-> m a -- ^ action that may throw exception
-> m a
retryOnException exceptionPred recorder maxDelay !baseDelay !maxTimesRetry rng action = do
result <- tryJust exceptionPred action
case result of
Left e
| maxTimesRetry > 0 -> do
-- multiply by 2 because baseDelay is midpoint of uniform range
let newBaseDelay = min maxDelay (baseDelay * 2)
let (delay, newRng) = Random.randomR (0, newBaseDelay) rng
let newMaxTimesRetry = maxTimesRetry - 1
liftIO $ do
logWith recorder Warning $ LogHieDbRetry delay maxDelay newMaxTimesRetry (toException e)
threadDelay delay
retryOnException exceptionPred recorder maxDelay newBaseDelay newMaxTimesRetry newRng action
| otherwise -> do
liftIO $ do
logWith recorder Warning $ LogHieDbRetriesExhausted baseDelay maxDelay maxTimesRetry (toException e)
throwIO e
Right b -> pure b
-- | in microseconds
oneSecond :: Int
oneSecond = 1000000
-- | in microseconds
oneMillisecond :: Int
oneMillisecond = 1000
-- | default maximum number of times to retry hiedb call
maxRetryCount :: Int
maxRetryCount = 10
retryOnSqliteBusy :: (MonadIO m, MonadCatch m, RandomGen g)
=> Recorder (WithPriority Log) -> g -> m a -> m a
retryOnSqliteBusy recorder rng action =
let isErrorBusy e
| SQLError{ sqlError = ErrorBusy } <- e = Just e
| otherwise = Nothing
in
retryOnException isErrorBusy recorder oneSecond oneMillisecond maxRetryCount rng action
makeWithHieDbRetryable :: RandomGen g => Recorder (WithPriority Log) -> g -> HieDb -> WithHieDb
makeWithHieDbRetryable recorder rng hieDb f =
retryOnSqliteBusy recorder rng (f hieDb)
-- | Wraps `withHieDb` to provide a database connection for reading, and a `HieWriterChan` for
-- writing. Actions are picked off one by one from the `HieWriterChan` and executed in serial
-- by a worker thread using a dedicated database connection.
-- This is done in order to serialize writes to the database, or else SQLite becomes unhappy
--
-- Also see Note [Serializing runs in separate thread]
runWithDb :: Recorder (WithPriority Log) -> FilePath -> ContT () IO (WithHieDbShield, IndexQueue)
runWithDb recorder fp = ContT $ \k -> do
-- use non-deterministic seed because maybe multiple HLS start at same time
-- and send bursts of requests
rng <- Random.newStdGen
-- Delete the database if it has an incompatible schema version
retryOnSqliteBusy
recorder
rng
(withHieDb fp (const $ pure ()) `Safe.catch` \IncompatibleSchemaVersion{} -> removeFile fp)
withHieDb fp $ \writedb -> do
-- the type signature is necessary to avoid concretizing the tyvar
-- e.g. `withWriteDbRetryable initConn` without type signature will
-- instantiate tyvar `a` to `()`
let withWriteDbRetryable :: WithHieDb
withWriteDbRetryable = makeWithHieDbRetryable recorder rng writedb
withWriteDbRetryable initConn
-- Clear the index of any files that might have been deleted since the last run
_ <- withWriteDbRetryable deleteMissingRealFiles
_ <- withWriteDbRetryable garbageCollectTypeNames
runContT (withWorkerQueue (writer withWriteDbRetryable)) $ \chan ->
withHieDb fp (\readDb -> k (WithHieDbShield $ makeWithHieDbRetryable recorder rng readDb, chan))
where
writer withHieDbRetryable l = do
-- TODO: probably should let exceptions be caught/logged/handled by top level handler
l withHieDbRetryable
`Safe.catch` \e@SQLError{} -> do
logWith recorder Error $ LogHieDbWriterThreadSQLiteError e
`Safe.catchAny` \f -> do
logWith recorder Error $ LogHieDbWriterThreadException f
getHieDbLoc :: FilePath -> IO FilePath
getHieDbLoc dir = do
let db = intercalate "-" [dirHash, takeBaseName dir, Compat.ghcVersionStr, hiedbDataVersion] <.> "hiedb"
dirHash = B.unpack $ B16.encode $ H.hash $ B.pack dir
cDir <- IO.getXdgDirectory IO.XdgCache cacheDir
createDirectoryIfMissing True cDir
pure (cDir </> db)
-- | Given a root directory, return a Shake 'Action' which setups an
-- 'IdeGhcSession' given a file.
-- Some of the many things this does:
--
-- * Find the cradle for the file
-- * Get the session options,
-- * Get the GHC lib directory
-- * Make sure the GHC compiletime and runtime versions match
-- * Restart the Shake session
--
-- This is the key function which implements multi-component support. All
-- components mapping to the same hie.yaml file are mapped to the same
-- HscEnv which is updated as new components are discovered.
loadSessionWithOptions :: Recorder (WithPriority Log) -> SessionLoadingOptions -> FilePath -> TQueue (IO ()) -> IO (Action IdeGhcSession)
loadSessionWithOptions recorder SessionLoadingOptions{..} rootDir que = do
let toAbsolutePath = toAbsolute rootDir -- see Note [Root Directory]
cradle_files <- newIORef []
-- Mapping from hie.yaml file to HscEnv, one per hie.yaml file
hscEnvs <- newVar Map.empty :: IO (Var HieMap)
-- Mapping from a Filepath to HscEnv
fileToFlags <- newVar Map.empty :: IO (Var FlagsMap)
-- Mapping from a Filepath to its 'hie.yaml' location.
-- Should hold the same Filepaths as 'fileToFlags', otherwise
-- they are inconsistent. So, everywhere you modify 'fileToFlags',
-- you have to modify 'filesMap' as well.
filesMap <- newVar HM.empty :: IO (Var FilesMap)
-- Version of the mappings above
version <- newVar 0
biosSessionLoadingVar <- newVar Nothing :: IO (Var (Maybe SessionLoadingPreferenceConfig))
let returnWithVersion fun = IdeGhcSession fun <$> liftIO (readVar version)
-- This caches the mapping from Mod.hs -> hie.yaml
cradleLoc <- liftIO $ memoIO $ \v -> do
res <- findCradle v
-- Sometimes we get C:, sometimes we get c:, and sometimes we get a relative path
-- try and normalise that
-- e.g. see https://github.com/haskell/ghcide/issues/126
let res' = toAbsolutePath <$> res
return $ normalise <$> res'
return $ do
clientConfig <- getClientConfigAction
extras@ShakeExtras{restartShakeSession, ideNc, knownTargetsVar, lspEnv
} <- getShakeExtras
let invalidateShakeCache = do
void $ modifyVar' version succ
return $ toNoFileKey GhcSessionIO
IdeOptions{ optTesting = IdeTesting optTesting
, optCheckProject = getCheckProject
, optExtensions
} <- getIdeOptions
-- populate the knownTargetsVar with all the
-- files in the project so that `knownFiles` can learn about them and
-- we can generate a complete module graph
let extendKnownTargets newTargets = do
knownTargets <- concatForM newTargets $ \TargetDetails{..} ->
case targetTarget of
TargetFile f -> do
-- If a target file has multiple possible locations, then we
-- assume they are all separate file targets.
-- This happens with '.hs-boot' files if they are in the root directory of the project.
-- GHC reports options such as '-i. A' as 'TargetFile A.hs' instead of 'TargetModule A'.
-- In 'fromTargetId', we dutifully look for '.hs-boot' files and add them to the
-- targetLocations of the TargetDetails. Then we add everything to the 'knownTargetsVar'.
-- However, when we look for a 'Foo.hs-boot' file in 'FindImports.hs', we look for either
--
-- * TargetFile Foo.hs-boot
-- * TargetModule Foo
--
-- If we don't generate a TargetFile for each potential location, we will only have
-- 'TargetFile Foo.hs' in the 'knownTargetsVar', thus not find 'TargetFile Foo.hs-boot'
-- and also not find 'TargetModule Foo'.
fs <- filterM (IO.doesFileExist . fromNormalizedFilePath) targetLocations
pure $ map (\fp -> (TargetFile fp, Set.singleton fp)) (nubOrd (f:fs))
TargetModule _ -> do
found <- filterM (IO.doesFileExist . fromNormalizedFilePath) targetLocations
return [(targetTarget, Set.fromList found)]
hasUpdate <- atomically $ do
known <- readTVar knownTargetsVar
let known' = flip mapHashed known $ \k -> unionKnownTargets k (mkKnownTargets knownTargets)
hasUpdate = if known /= known' then Just (unhashed known') else Nothing
writeTVar knownTargetsVar known'
pure hasUpdate
for_ hasUpdate $ \x ->
logWith recorder Debug $ LogKnownFilesUpdated (targetMap x)
return $ toNoFileKey GetKnownTargets
-- Create a new HscEnv from a hieYaml root and a set of options
let packageSetup :: (Maybe FilePath, NormalizedFilePath, ComponentOptions, FilePath)
-> IO ([ComponentInfo], [ComponentInfo])
packageSetup (hieYaml, cfp, opts, libDir) = do
-- Parse DynFlags for the newly discovered component
hscEnv <- emptyHscEnv ideNc libDir
newTargetDfs <- evalGhcEnv hscEnv $ setOptions cfp opts (hsc_dflags hscEnv) rootDir
let deps = componentDependencies opts ++ maybeToList hieYaml
dep_info <- getDependencyInfo deps
-- Now lookup to see whether we are combining with an existing HscEnv
-- or making a new one. The lookup returns the HscEnv and a list of
-- information about other components loaded into the HscEnv
-- (unitId, DynFlag, Targets)
modifyVar hscEnvs $ \m -> do
-- Just deps if there's already an HscEnv
-- Nothing is it's the first time we are making an HscEnv
let oldDeps = Map.lookup hieYaml m
let -- Add the raw information about this component to the list
-- We will modify the unitId and DynFlags used for
-- compilation but these are the true source of
-- information.
new_deps = fmap (\(df, targets) -> RawComponentInfo (homeUnitId_ df) df targets cfp opts dep_info) newTargetDfs
all_deps = new_deps `NE.appendList` fromMaybe [] oldDeps
-- Get all the unit-ids for things in this component
_inplace = map rawComponentUnitId $ NE.toList all_deps
all_deps' <- forM all_deps $ \RawComponentInfo{..} -> do
let prefix = show rawComponentUnitId
-- See Note [Avoiding bad interface files]
let cacheDirOpts = componentOptions opts
cacheDirs <- liftIO $ getCacheDirs prefix cacheDirOpts
processed_df <- setCacheDirs recorder cacheDirs rawComponentDynFlags
-- The final component information, mostly the same but the DynFlags don't
-- contain any packages which are also loaded
-- into the same component.
pure $ ComponentInfo
{ componentUnitId = rawComponentUnitId
, componentDynFlags = processed_df
, componentTargets = rawComponentTargets
, componentFP = rawComponentFP
, componentCOptions = rawComponentCOptions
, componentDependencyInfo = rawComponentDependencyInfo
}
-- Modify the map so the hieYaml now maps to the newly updated
-- ComponentInfos
-- Returns
-- . The information for the new component which caused this cache miss
-- . The modified information (without -inplace flags) for
-- existing packages
let (new,old) = NE.splitAt (NE.length new_deps) all_deps'
pure (Map.insert hieYaml (NE.toList all_deps) m, (new,old))
let session :: (Maybe FilePath, NormalizedFilePath, ComponentOptions, FilePath)
-> IO (IdeResult HscEnvEq,[FilePath])
session args@(hieYaml, _cfp, _opts, _libDir) = do
(new_deps, old_deps) <- packageSetup args
-- For each component, now make a new HscEnvEq which contains the
-- HscEnv for the hie.yaml file but the DynFlags for that component
-- For GHC's supporting multi component sessions, we create a shared
-- HscEnv but set the active component accordingly
hscEnv <- emptyHscEnv ideNc _libDir
let new_cache = newComponentCache recorder optExtensions _cfp hscEnv
all_target_details <- new_cache old_deps new_deps
this_dep_info <- getDependencyInfo $ maybeToList hieYaml
let (all_targets, this_flags_map, this_options)
= case HM.lookup _cfp flags_map' of
Just this -> (all_targets', flags_map', this)
Nothing -> (this_target_details : all_targets', HM.insert _cfp this_flags flags_map', this_flags)
where all_targets' = concat all_target_details
flags_map' = HM.fromList (concatMap toFlagsMap all_targets')
this_target_details = TargetDetails (TargetFile _cfp) this_error_env this_dep_info [_cfp]
this_flags = (this_error_env, this_dep_info)
this_error_env = ([this_error], Nothing)
this_error = ideErrorWithSource (Just "cradle") (Just DiagnosticSeverity_Error) _cfp
(T.unlines
[ "No cradle target found. Is this file listed in the targets of your cradle?"
, "If you are using a .cabal file, please ensure that this module is listed in either the exposed-modules or other-modules section"
])
Nothing
void $ modifyVar' fileToFlags $ Map.insert hieYaml this_flags_map
void $ modifyVar' filesMap $ flip HM.union (HM.fromList (map ((,hieYaml) . fst) $ concatMap toFlagsMap all_targets))
-- The VFS doesn't change on cradle edits, re-use the old one.
-- Invalidate all the existing GhcSession build nodes by restarting the Shake session
keys2 <- invalidateShakeCache
restartShakeSession VFSUnmodified "new component" [] $ do
keys1 <- extendKnownTargets all_targets
return [keys1, keys2]
-- Typecheck all files in the project on startup
checkProject <- getCheckProject
unless (null new_deps || not checkProject) $ do
cfps' <- liftIO $ filterM (IO.doesFileExist . fromNormalizedFilePath) (concatMap targetLocations all_targets)
void $ shakeEnqueue extras $ mkDelayedAction "InitialLoad" Debug $ void $ do
mmt <- uses GetModificationTime cfps'
let cs_exist = catMaybes (zipWith (<$) cfps' mmt)
modIfaces <- uses GetModIface cs_exist
-- update exports map
shakeExtras <- getShakeExtras
let !exportsMap' = createExportsMap $ mapMaybe (fmap hirModIface) modIfaces
liftIO $ atomically $ modifyTVar' (exportsMap shakeExtras) (exportsMap' <>)
return $ second Map.keys this_options
let consultCradle :: Maybe FilePath -> FilePath -> IO (IdeResult HscEnvEq, [FilePath])
consultCradle hieYaml cfp = do
let lfpLog = makeRelative rootDir cfp
logWith recorder Info $ LogCradlePath lfpLog
when (isNothing hieYaml) $
logWith recorder Warning $ LogCradleNotFound lfpLog
cradle <- loadCradle recorder hieYaml rootDir
when optTesting $ mRunLspT lspEnv $
sendNotification (SMethod_CustomMethod (Proxy @"ghcide/cradle/loaded")) (toJSON cfp)
-- Display a user friendly progress message here: They probably don't know what a cradle is
let progMsg = "Setting up " <> T.pack (takeBaseName (cradleRootDir cradle))
<> " (for " <> T.pack lfpLog <> ")"
eopts <- mRunLspTCallback lspEnv (\act -> withIndefiniteProgress progMsg Nothing NotCancellable (const act)) $
withTrace "Load cradle" $ \addTag -> do
addTag "file" lfpLog
old_files <- readIORef cradle_files
res <- cradleToOptsAndLibDir recorder (sessionLoading clientConfig) cradle cfp old_files
addTag "result" (show res)
return res
logWith recorder Debug $ LogSessionLoadingResult eopts
case eopts of
-- The cradle gave us some options so get to work turning them
-- into and HscEnv.
Right (opts, libDir, version) -> do
let compileTime = fullCompilerVersion
case reverse $ readP_to_S parseVersion version of
[] -> error $ "GHC version could not be parsed: " <> version
((runTime, _):_)
| compileTime == runTime -> do
atomicModifyIORef' cradle_files (\xs -> (cfp:xs,()))
session (hieYaml, toNormalizedFilePath' cfp, opts, libDir)
| otherwise -> return (([renderPackageSetupException cfp GhcVersionMismatch{..}], Nothing),[])
-- Failure case, either a cradle error or the none cradle
Left err -> do
dep_info <- getDependencyInfo (maybeToList hieYaml)
let ncfp = toNormalizedFilePath' cfp
let res = (map (\err' -> renderCradleError err' cradle ncfp) err, Nothing)
void $ modifyVar' fileToFlags $
Map.insertWith HM.union hieYaml (HM.singleton ncfp (res, dep_info))
void $ modifyVar' filesMap $ HM.insert ncfp hieYaml
return (res, maybe [] pure hieYaml ++ concatMap cradleErrorDependencies err)
let
-- | We allow users to specify a loading strategy.
-- Check whether this config was changed since the last time we have loaded
-- a session.
--
-- If the loading configuration changed, we likely should restart the session
-- in its entirety.
didSessionLoadingPreferenceConfigChange :: IO Bool
didSessionLoadingPreferenceConfigChange = do
mLoadingConfig <- readVar biosSessionLoadingVar
case mLoadingConfig of
Nothing -> do
writeVar biosSessionLoadingVar (Just (sessionLoading clientConfig))
pure False
Just loadingConfig -> do
writeVar biosSessionLoadingVar (Just (sessionLoading clientConfig))
pure (loadingConfig /= sessionLoading clientConfig)
-- This caches the mapping from hie.yaml + Mod.hs -> [String]
-- Returns the Ghc session and the cradle dependencies
let sessionOpts :: (Maybe FilePath, FilePath)
-> IO (IdeResult HscEnvEq, [FilePath])
sessionOpts (hieYaml, file) = do
Extra.whenM didSessionLoadingPreferenceConfigChange $ do
logWith recorder Info LogSessionLoadingChanged
-- If the dependencies are out of date then clear both caches and start
-- again.
modifyVar_ fileToFlags (const (return Map.empty))
modifyVar_ filesMap (const (return HM.empty))
-- Don't even keep the name cache, we start from scratch here!
modifyVar_ hscEnvs (const (return Map.empty))
v <- Map.findWithDefault HM.empty hieYaml <$> readVar fileToFlags
let cfp = toAbsolutePath file
case HM.lookup (toNormalizedFilePath' cfp) v of
Just (opts, old_di) -> do
deps_ok <- checkDependencyInfo old_di
if not deps_ok
then do
-- If the dependencies are out of date then clear both caches and start
-- again.
modifyVar_ fileToFlags (const (return Map.empty))
modifyVar_ filesMap (const (return HM.empty))
-- Keep the same name cache
modifyVar_ hscEnvs (return . Map.adjust (const []) hieYaml )
consultCradle hieYaml cfp
else return (opts, Map.keys old_di)
Nothing -> consultCradle hieYaml cfp
-- The main function which gets options for a file. We only want one of these running
-- at a time. Therefore the IORef contains the currently running cradle, if we try
-- to get some more options then we wait for the currently running action to finish
-- before attempting to do so.
let getOptions :: FilePath -> IO (IdeResult HscEnvEq, [FilePath])
getOptions file = do
let ncfp = toNormalizedFilePath' (toAbsolutePath file)
cachedHieYamlLocation <- HM.lookup ncfp <$> readVar filesMap
hieYaml <- cradleLoc file
sessionOpts (join cachedHieYamlLocation <|> hieYaml, file) `Safe.catch` \e ->
return (([renderPackageSetupException file e], Nothing), maybe [] pure hieYaml)
returnWithVersion $ \file -> do
-- see Note [Serializing runs in separate thread]
awaitRunInThread que $ getOptions file
-- | Run the specific cradle on a specific FilePath via hie-bios.
-- This then builds dependencies or whatever based on the cradle, gets the
-- GHC options/dynflags needed for the session and the GHC library directory
cradleToOptsAndLibDir :: Recorder (WithPriority Log) -> SessionLoadingPreferenceConfig -> Cradle Void -> FilePath -> [FilePath]
-> IO (Either [CradleError] (ComponentOptions, FilePath, String))
cradleToOptsAndLibDir recorder loadConfig cradle file old_fps = do
-- let noneCradleFoundMessage :: FilePath -> T.Text
-- noneCradleFoundMessage f = T.pack $ "none cradle found for " <> f <> ", ignoring the file"
-- Start off by getting the session options
logWith recorder Debug $ LogCradle cradle
cradleRes <- HieBios.getCompilerOptions file loadStyle cradle
case cradleRes of
CradleSuccess r -> do
-- Now get the GHC lib dir
libDirRes <- getRuntimeGhcLibDir cradle
versionRes <- getRuntimeGhcVersion cradle
case liftA2 (,) libDirRes versionRes of
-- This is the successful path
(CradleSuccess (libDir, version)) -> pure (Right (r, libDir, version))
CradleFail err -> return (Left [err])
CradleNone -> do
logWith recorder Info $ LogNoneCradleFound file
return (Left [])
CradleFail err -> return (Left [err])
CradleNone -> do
logWith recorder Info $ LogNoneCradleFound file
return (Left [])
where
loadStyle = case loadConfig of
PreferSingleComponentLoading -> LoadFile
PreferMultiComponentLoading -> LoadWithContext old_fps
emptyHscEnv :: NameCache -> FilePath -> IO HscEnv
emptyHscEnv nc libDir = do
-- We call setSessionDynFlags so that the loader is initialised
-- We need to do this before we call initUnits.
env <- runGhc (Just libDir) $
getSessionDynFlags >>= setSessionDynFlags >> getSession
pure $ setNameCache nc (hscSetFlags ((hsc_dflags env){useUnicode = True }) env)
data TargetDetails = TargetDetails
{
targetTarget :: !Target,
targetEnv :: !(IdeResult HscEnvEq),
targetDepends :: !DependencyInfo,
targetLocations :: ![NormalizedFilePath]
}
fromTargetId :: [FilePath] -- ^ import paths
-> [String] -- ^ extensions to consider
-> TargetId
-> IdeResult HscEnvEq
-> DependencyInfo
-> IO [TargetDetails]
-- For a target module we consider all the import paths
fromTargetId is exts (GHC.TargetModule modName) env dep = do
let fps = [i </> moduleNameSlashes modName -<.> ext <> boot
| ext <- exts
, i <- is
, boot <- ["", "-boot"]
]
let locs = fmap toNormalizedFilePath' fps
return [TargetDetails (TargetModule modName) env dep locs]
-- For a 'TargetFile' we consider all the possible module names
fromTargetId _ _ (GHC.TargetFile f _) env deps = do
let nf = toNormalizedFilePath' f
let other
| "-boot" `isSuffixOf` f = toNormalizedFilePath' (L.dropEnd 5 $ fromNormalizedFilePath nf)
| otherwise = toNormalizedFilePath' (fromNormalizedFilePath nf ++ "-boot")
return [TargetDetails (TargetFile nf) env deps [nf, other]]
toFlagsMap :: TargetDetails -> [(NormalizedFilePath, (IdeResult HscEnvEq, DependencyInfo))]
toFlagsMap TargetDetails{..} =
[ (l, (targetEnv, targetDepends)) | l <- targetLocations]
setNameCache :: NameCache -> HscEnv -> HscEnv
setNameCache nc hsc = hsc { hsc_NC = nc }
#if MIN_VERSION_ghc(9,13,0)
-- Moved back to implementation in GHC.
checkHomeUnitsClosed' :: UnitEnv -> OS.Set UnitId -> [DriverMessages]
checkHomeUnitsClosed' ue _ = checkHomeUnitsClosed ue
#elif MIN_VERSION_ghc(9,3,0)
-- This function checks the important property that if both p and q are home units
-- then any dependency of p, which transitively depends on q is also a home unit.
-- GHC had an implementation of this function, but it was horribly inefficient
-- We should move back to the GHC implementation on compilers where
-- https://gitlab.haskell.org/ghc/ghc/-/merge_requests/12162 is included
checkHomeUnitsClosed' :: UnitEnv -> OS.Set UnitId -> Maybe (Compat.MsgEnvelope DriverMessage)
checkHomeUnitsClosed' ue home_id_set
| OS.null bad_unit_ids = Nothing
| otherwise = Just (GHC.mkPlainErrorMsgEnvelope rootLoc $ DriverHomePackagesNotClosed (OS.toList bad_unit_ids))
where
bad_unit_ids = upwards_closure OS.\\ home_id_set
rootLoc = mkGeneralSrcSpan (Compat.fsLit "<command line>")
graph :: Graph (Node UnitId UnitId)
graph = graphFromEdgedVerticesUniq graphNodes
-- downwards closure of graph
downwards_closure
= graphFromEdgedVerticesUniq [ DigraphNode uid uid (OS.toList deps)
| (uid, deps) <- Map.toList (allReachable graph node_key)]
inverse_closure = transposeG downwards_closure
upwards_closure = OS.fromList $ map node_key $ reachablesG inverse_closure [DigraphNode uid uid [] | uid <- OS.toList home_id_set]
all_unit_direct_deps :: UniqMap UnitId (OS.Set UnitId)
all_unit_direct_deps
= unitEnv_foldWithKey go emptyUniqMap $ ue_home_unit_graph ue
where
go rest this this_uis =
plusUniqMap_C OS.union
(addToUniqMap_C OS.union external_depends this (OS.fromList this_deps))
rest
where
external_depends = mapUniqMap (OS.fromList . unitDepends)
#if !MIN_VERSION_ghc(9,7,0)
$ listToUniqMap $ Map.toList
#endif
$ unitInfoMap this_units
this_units = homeUnitEnv_units this_uis
this_deps = [ Compat.toUnitId unit | (unit,Just _) <- explicitUnits this_units]
graphNodes :: [Node UnitId UnitId]
graphNodes = go OS.empty home_id_set
where
go done todo
= case OS.minView todo of
Nothing -> []
Just (uid, todo')
| OS.member uid done -> go done todo'
| otherwise -> case lookupUniqMap all_unit_direct_deps uid of
Nothing -> pprPanic "uid not found" (Compat.ppr (uid, all_unit_direct_deps))
Just depends ->
let todo'' = (depends OS.\\ done) `OS.union` todo'
in DigraphNode uid uid (OS.toList depends) : go (OS.insert uid done) todo''
#endif
-- | Create a mapping from FilePaths to HscEnvEqs
-- This combines all the components we know about into
-- an appropriate session, which is a multi component
-- session on GHC 9.4+
newComponentCache
:: Recorder (WithPriority Log)
-> [String] -- ^ File extensions to consider
-> NormalizedFilePath -- ^ Path to file that caused the creation of this component
-> HscEnv -- ^ An empty HscEnv
-> [ComponentInfo] -- ^ New components to be loaded
-> [ComponentInfo] -- ^ old, already existing components
-> IO [ [TargetDetails] ]
newComponentCache recorder exts _cfp hsc_env old_cis new_cis = do
let cis = Map.unionWith unionCIs (mkMap new_cis) (mkMap old_cis)
-- When we have multiple components with the same uid,
-- prefer the new one over the old.
-- However, we might have added some targets to the old unit
-- (see special target), so preserve those
unionCIs new_ci old_ci = new_ci { componentTargets = componentTargets new_ci ++ componentTargets old_ci }
mkMap = Map.fromListWith unionCIs . map (\ci -> (componentUnitId ci, ci))
let dfs = map componentDynFlags $ Map.elems cis
uids = Map.keys cis
logWith recorder Info $ LogMakingNewHscEnv uids
hscEnv' <- -- Set up a multi component session with the other units on GHC 9.4
Compat.initUnits dfs hsc_env
let closure_errs = maybeToList $ checkHomeUnitsClosed' (hsc_unit_env hscEnv') (hsc_all_home_unit_ids hscEnv')
closure_err_to_multi_err err =
ideErrorWithSource
(Just "cradle") (Just DiagnosticSeverity_Warning) _cfp
(T.pack (Compat.printWithoutUniques (singleMessage err)))
#if MIN_VERSION_ghc(9,5,0)
(Just (fmap GhcDriverMessage err))
#else
Nothing
#endif
multi_errs = map closure_err_to_multi_err closure_errs
bad_units = OS.fromList $ concat $ do
x <- map errMsgDiagnostic closure_errs
DriverHomePackagesNotClosed us <- pure x
pure us
isBad ci = (homeUnitId_ (componentDynFlags ci)) `OS.member` bad_units
-- Whenever we spin up a session on Linux, dynamically load libm.so.6
-- in. We need this in case the binary is statically linked, in which
-- case the interactive session will fail when trying to load
-- ghc-prim, which happens whenever Template Haskell is being
-- evaluated or haskell-language-server's eval plugin tries to run
-- some code. If the binary is dynamically linked, then this will have
-- no effect.
-- See https://github.com/haskell/haskell-language-server/issues/221
-- We need to do this after the call to setSessionDynFlags initialises
-- the loader
when (os == "linux") $ do
initObjLinker hscEnv'
res <- loadDLL hscEnv' "libm.so.6"
case res of
Nothing -> pure ()
Just err -> logWith recorder Error $ LogDLLLoadError err
forM (Map.elems cis) $ \ci -> do
let df = componentDynFlags ci
thisEnv <- do
-- In GHC 9.4 we have multi component support, and we have initialised all the units
-- above.
-- We just need to set the current unit here
pure $ hscSetActiveUnitId (homeUnitId_ df) hscEnv'
henv <- newHscEnvEq thisEnv
let targetEnv = (if isBad ci then multi_errs else [], Just henv)
targetDepends = componentDependencyInfo ci
logWith recorder Debug $ LogNewComponentCache (targetEnv, targetDepends)
evaluate $ liftRnf rwhnf $ componentTargets ci
let mk t = fromTargetId (importPaths df) exts (targetId t) targetEnv targetDepends
ctargets <- concatMapM mk (componentTargets ci)
return (L.nubOrdOn targetTarget ctargets)
{- Note [Avoiding bad interface files]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Originally, we set the cache directory for the various components once
on the first occurrence of the component.
This works fine if these components have no references to each other,
but you have components that depend on each other, the interface files are
updated for each component.
After restarting the session and only opening the component that depended
on the other, suddenly the interface files of this component are stale.
However, from the point of view of `ghcide`, they do not look stale,
thus, not regenerated and the IDE shows weird errors such as:
```
typecheckIface
Declaration for Rep_ClientRunFlags
Axiom branches Rep_ClientRunFlags:
Failed to load interface for ‘Distribution.Simple.Flag’
Use -v to see a list of the files searched for.
```
and
```
expectJust checkFamInstConsistency
CallStack (from HasCallStack):
error, called at compiler\\utils\\Maybes.hs:55:27 in ghc:Maybes
expectJust, called at compiler\\typecheck\\FamInst.hs:461:30 in ghc:FamInst
```
To mitigate this, we set the cache directory for each component dependent
on the components of the current `HscEnv`, additionally to the component options
of the respective components.
Assume two components, c1, c2, where c2 depends on c1, and the options of the
respective components are co1, co2.
If we want to load component c2, followed by c1, we set the cache directory for
each component in this way:
* Load component c2
* (Cache Directory State)
- name of c2 + co2
* Load component c1
* (Cache Directory State)
- name of c2 + name of c1 + co2
- name of c2 + name of c1 + co1
Overall, we created three cache directories. If we opened c1 first, then we
create a fourth cache directory.
This makes sure that interface files are always correctly updated.
Since this causes a lot of recompilation, we only update the cache-directory,
if the dependencies of a component have really changed.
E.g. when you load two executables, they can not depend on each other. They
should be filtered out, such that we dont have to re-compile everything.
-}
-- | Set the cache-directory based on the ComponentOptions and a list of
-- internal packages.
-- For the exact reason, see Note [Avoiding bad interface files].
setCacheDirs :: MonadUnliftIO m => Recorder (WithPriority Log) -> CacheDirs -> DynFlags -> m DynFlags
setCacheDirs recorder CacheDirs{..} dflags = do
logWith recorder Info $ LogInterfaceFilesCacheDir (fromMaybe cacheDir hiCacheDir)
pure $ dflags
& maybe id setHiDir hiCacheDir
& maybe id setHieDir hieCacheDir
& maybe id setODir oCacheDir
-- See Note [Multi Cradle Dependency Info]
type DependencyInfo = Map.Map FilePath (Maybe UTCTime)
type HieMap = Map.Map (Maybe FilePath) [RawComponentInfo]
-- | Maps a "hie.yaml" location to all its Target Filepaths and options.
type FlagsMap = Map.Map (Maybe FilePath) (HM.HashMap NormalizedFilePath (IdeResult HscEnvEq, DependencyInfo))
-- | Maps a Filepath to its respective "hie.yaml" location.
-- It aims to be the reverse of 'FlagsMap'.
type FilesMap = HM.HashMap NormalizedFilePath (Maybe FilePath)