-
Notifications
You must be signed in to change notification settings - Fork 842
/
Ghci.hs
1240 lines (1199 loc) · 47.3 KB
/
Ghci.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 NoImplicitPrelude #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NoFieldSelectors #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Types and functions related to Stack's @ghci@ and @repl@ commands.
module Stack.Ghci
( GhciOpts (..)
, GhciPkgInfo (..)
, GhciException (..)
, GhciPrettyException (..)
, ghciCmd
, ghci
) where
import Control.Monad.Extra ( whenJust )
import Control.Monad.State.Strict ( State, execState, get, modify )
import Data.ByteString.Builder ( byteString )
import qualified Data.ByteString.Char8 as S8
import qualified Data.ByteString.Lazy as LBS
import qualified Data.List as L
import Data.List.Extra ( (!?) )
import qualified Data.Map as Map
import qualified Data.Map.Strict as M
import qualified Data.Set as S
import qualified Data.Text as T
import qualified Distribution.PackageDescription as C
import Path ((</>), parent, parseRelFile )
import Path.Extra ( forgivingResolveFile', toFilePathNoTrailingSep )
import Path.IO
( XdgDirectory (..), doesFileExist, ensureDir, getXdgDir )
import RIO.NonEmpty ( nonEmpty )
import RIO.Process ( exec, withWorkingDir )
import Stack.Build ( buildLocalTargets )
import Stack.Build.Installed ( getInstalled, toInstallMap )
import Stack.Build.Source ( localDependencies, projectLocalPackages )
import Stack.Build.Target ( NeedTargets (..), parseTargets )
import Stack.Constants
( relDirGhciScript, relDirStackProgName, relFileCabalMacrosH
, relFileGhciScript, stackProgName'
)
import Stack.Constants.Config ( ghciDirL, objectInterfaceDirL )
import Stack.Ghci.Script
( GhciScript, ModuleName, cmdAdd, cmdModule
, scriptToLazyByteString
)
import Stack.Package
( buildableExes, buildableForeignLibs, buildableSubLibs
, buildableTestSuites, buildableBenchmarks, getPackageOpts
, hasBuildableMainLibrary, listOfPackageDeps
, packageFromPackageDescription, readDotBuildinfo
, resolvePackageDescription, topSortPackageComponent
)
import Stack.PackageFile ( getPackageFile )
import Stack.Prelude
import Stack.Runners ( ShouldReexec (..), withConfig, withEnvConfig )
import Stack.Types.Build.Exception
( BuildPrettyException (..), pprintTargetParseErrors )
import Stack.Types.BuildConfig
( BuildConfig (..), HasBuildConfig (..), configFileL )
import Stack.Types.BuildOpts ( BuildOpts (..) )
import qualified Stack.Types.BuildOpts as BenchmarkOpts ( BenchmarkOpts (..) )
import qualified Stack.Types.BuildOpts as TestOpts ( TestOpts (..) )
import Stack.Types.BuildOptsCLI
( ApplyCLIFlag (..), BuildOptsCLI (..), defaultBuildOptsCLI )
import Stack.Types.CompilerPaths
( CompilerPaths (..), HasCompiler (..) )
import Stack.Types.Config ( Config (..), HasConfig (..), buildOptsL )
import Stack.Types.Config.Exception ( ConfigPrettyException (..) )
import Stack.Types.EnvConfig
( EnvConfig (..), HasEnvConfig (..), actualCompilerVersionL
, shaPathForBytes
)
import Stack.Types.EnvSettings ( defaultEnvSettings )
import Stack.Types.Installed ( InstallMap, InstalledMap )
import Stack.Types.NamedComponent
( NamedComponent (..), isCLib, isCSubLib, renderComponentTo
, renderPkgComponent
)
import Stack.Types.Package
( BuildInfoOpts (..), LocalPackage (..), Package (..)
, PackageConfig (..), dotCabalCFilePath, dotCabalGetPath
, dotCabalMainPath
)
import Stack.Types.PackageFile ( PackageComponentFile (..) )
import Stack.Types.Platform ( HasPlatform (..) )
import Stack.Types.Runner ( HasRunner, Runner )
import Stack.Types.SourceMap
( CommonPackage (..), DepPackage (..), GlobalPackage
, PackageType (..), ProjectPackage (..), SMActual (..)
, SMTargets (..), SMWanted (..), SourceMap (..), Target (..)
)
import System.IO ( putStrLn )
import System.Permissions ( setScriptPerms )
-- | Type representing exceptions thrown by functions exported by the
-- "Stack.Ghci" module.
data GhciException
= InvalidPackageOption !String
| LoadingDuplicateModules
| MissingFileTarget !String
| Can'tSpecifyFilesAndTargets
| Can'tSpecifyFilesAndMainIs
deriving (Show, Typeable)
instance Exception GhciException where
displayException (InvalidPackageOption name) =
"Error: [S-6716]\n"
++ "Failed to parse '--package' option " ++ name ++ "."
displayException LoadingDuplicateModules = unlines
[ "Error: [S-9632]"
, "Not attempting to start ghci due to these duplicate modules."
, "Use '--no-load' to try to start it anyway, without loading any \
\modules (but these are still likely to cause errors)."
]
displayException (MissingFileTarget name) =
"Error: [S-3600]\n"
++ "Cannot find file target " ++ name ++ "."
displayException Can'tSpecifyFilesAndTargets =
"Error: [S-9906]\n"
++ "Cannot use 'stack ghci' with both file targets and package targets."
displayException Can'tSpecifyFilesAndMainIs =
"Error: [S-5188]\n"
++ "Cannot use 'stack ghci' with both file targets and '--main-is' \
\flag."
-- | Type representing \'pretty\' exceptions thrown by functions exported by the
-- "Stack.Ghci" module.
data GhciPrettyException
= GhciTargetParseException ![StyleDoc]
| CandidatesIndexOutOfRangeBug
deriving (Show, Typeable)
instance Pretty GhciPrettyException where
pretty (GhciTargetParseException errs) =
"[S-6948]"
<> pprintTargetParseErrors errs
<> blankLine
<> fillSep
[ flow "Note that to specify options to be passed to GHCi, use the"
, style Shell "--ghci-options"
, "option."
]
pretty CandidatesIndexOutOfRangeBug = bugPrettyReport "[S-1939]" $
flow "figureOutMainFile: index out of range."
instance Exception GhciPrettyException
-- | Typre respresenting command line options for the @stack ghci@ and
-- @stack repl@ commands.
data GhciOpts = GhciOpts
{ targets :: ![Text]
, args :: ![String]
, ghcOptions :: ![String]
, flags :: !(Map ApplyCLIFlag (Map FlagName Bool))
, ghcCommand :: !(Maybe FilePath)
, noLoadModules :: !Bool
, additionalPackages :: ![String]
, mainIs :: !(Maybe Text)
, loadLocalDeps :: !Bool
, hidePackages :: !(Maybe Bool)
, noBuild :: !Bool
, onlyMain :: !Bool
}
deriving Show
-- | Type representing information required to load a package or its components.
--
-- NOTE: GhciPkgInfo has paths as list instead of a Set to preserve files order
-- as a workaround for bug https://ghc.haskell.org/trac/ghc/ticket/13786
data GhciPkgInfo = GhciPkgInfo
{ name :: !PackageName
, opts :: ![(NamedComponent, BuildInfoOpts)]
, dir :: !(Path Abs Dir)
, modules :: !ModuleMap
, cFiles :: ![Path Abs File] -- ^ C files.
, mainIs :: !(Map NamedComponent [Path Abs File])
, targetFiles :: !(Maybe [Path Abs File])
, package :: !Package
}
deriving Show
-- | Type representing loaded package description and related information.
data GhciPkgDesc = GhciPkgDesc
{ package :: !Package
, cabalFP :: !(Path Abs File)
, target :: !Target
}
-- Mapping from a module name to a map with all of the paths that use that name.
-- Each of those paths is associated with a set of components that contain it.
-- The purpose of this complex structure is for use in
-- 'checkForDuplicateModules'.
type ModuleMap =
Map ModuleName (Map (Path Abs File) (Set (PackageName, NamedComponent)))
unionModuleMaps :: [ModuleMap] -> ModuleMap
unionModuleMaps = M.unionsWith (M.unionWith S.union)
-- | Function underlying the @stack ghci@ and @stack repl@ commands. Run GHCi in
-- the context of a project.
ghciCmd :: GhciOpts -> RIO Runner ()
ghciCmd ghciOpts =
let boptsCLI = defaultBuildOptsCLI
-- using only additional packages, targets then get overridden in `ghci`
{ targetsCLI = map T.pack ghciOpts.additionalPackages
, initialBuildSteps = True
, flags = ghciOpts.flags
, ghcOptions = map T.pack ghciOpts.ghcOptions
}
in withConfig YesReexec $ withEnvConfig AllowNoTargets boptsCLI $ do
bopts <- view buildOptsL
-- override env so running of tests and benchmarks is disabled
let boptsLocal = bopts
{ testOpts = bopts.testOpts { TestOpts.runTests = False }
, benchmarkOpts =
bopts.benchmarkOpts { BenchmarkOpts.runBenchmarks = False }
}
local (set buildOptsL boptsLocal) (ghci ghciOpts)
-- | Launch a GHCi session for the given project package targets with the given
-- options and configure it with the load paths and extensions of those targets.
ghci :: HasEnvConfig env => GhciOpts -> RIO env ()
ghci opts = do
let buildOptsCLI = defaultBuildOptsCLI
{ targetsCLI = []
, flags = opts.flags
}
sourceMap <- view $ envConfigL . to (.sourceMap)
installMap <- toInstallMap sourceMap
locals <- projectLocalPackages
depLocals <- localDependencies
let localMap =
M.fromList [(lp.package.name, lp) | lp <- locals ++ depLocals]
-- FIXME:qrilka this looks wrong to go back to SMActual
sma = SMActual
{ compiler = sourceMap.compiler
, project = sourceMap.project
, deps = sourceMap.deps
, globals = sourceMap.globalPkgs
}
-- Parse --main-is argument.
mainIsTargets <- parseMainIsTargets buildOptsCLI sma opts.mainIs
-- Parse to either file targets or build targets
etargets <- preprocessTargets buildOptsCLI sma opts.targets
(inputTargets, mfileTargets) <- case etargets of
Right packageTargets -> pure (packageTargets, Nothing)
Left rawFileTargets -> do
whenJust mainIsTargets $ \_ -> throwM Can'tSpecifyFilesAndMainIs
-- Figure out targets based on filepath targets
(targetMap, fileInfo, extraFiles) <- findFileTargets locals rawFileTargets
pure (targetMap, Just (fileInfo, extraFiles))
-- Get a list of all the local target packages.
localTargets <- getAllLocalTargets opts inputTargets mainIsTargets localMap
-- Get a list of all the non-local target packages.
nonLocalTargets <- getAllNonLocalTargets inputTargets
let getInternalDependencies target localPackage =
topSortPackageComponent localPackage.package target False
internalDependencies =
M.intersectionWith getInternalDependencies inputTargets localMap
relevantDependencies = M.filter (any isCSubLib) internalDependencies
-- Check if additional package arguments are sensible.
addPkgs <- checkAdditionalPackages opts.additionalPackages
-- Load package descriptions.
pkgDescs <- loadGhciPkgDescs buildOptsCLI localTargets
-- If necessary, ask user about which main module to load.
bopts <- view buildOptsL
mainFile <- if opts.noLoadModules
then pure Nothing
else do
-- Figure out package files, in order to ask the user about which main
-- module to load. See the note below for why this is done again after the
-- build. This could potentially be done more efficiently, because all we
-- need is the location of main modules, not the rest.
pkgs0 <- getGhciPkgInfos installMap addPkgs (fmap fst mfileTargets) pkgDescs
figureOutMainFile bopts mainIsTargets localTargets pkgs0
let pkgTargets pn targets =
case targets of
TargetAll _ -> [T.pack (packageNameString pn)]
TargetComps comps -> [renderPkgComponent (pn, c) | c <- toList comps]
-- Build required dependencies and setup project packages.
buildDepsAndInitialSteps opts $
concatMap (\(pn, (_, t)) -> pkgTargets pn t) localTargets
targetWarnings localTargets nonLocalTargets mfileTargets
-- Load the list of modules _after_ building, to catch changes in
-- unlisted dependencies (#1180)
pkgs <- getGhciPkgInfos installMap addPkgs (fmap fst mfileTargets) pkgDescs
checkForIssues pkgs
-- Finally, do the invocation of ghci
runGhci
opts
localTargets
mainFile
pkgs
(maybe [] snd mfileTargets)
(nonLocalTargets ++ addPkgs)
relevantDependencies
preprocessTargets ::
HasEnvConfig env
=> BuildOptsCLI
-> SMActual GlobalPackage
-> [Text]
-> RIO env (Either [Path Abs File] (Map PackageName Target))
preprocessTargets buildOptsCLI sma rawTargets = do
let (fileTargetsRaw, normalTargetsRaw) =
L.partition
(\t -> ".hs" `T.isSuffixOf` t || ".lhs" `T.isSuffixOf` t)
rawTargets
-- Only use file targets if we have no normal targets.
if not (null fileTargetsRaw) && null normalTargetsRaw
then do
fileTargets <- forM fileTargetsRaw $ \fp0 -> do
let fp = T.unpack fp0
mpath <- forgivingResolveFile' fp
case mpath of
Nothing -> throwM (MissingFileTarget fp)
Just path -> pure path
pure (Left fileTargets)
else do
-- Try parsing targets before checking if both file and
-- module targets are specified (see issue#3342).
let boptsCLI = buildOptsCLI { targetsCLI = normalTargetsRaw }
normalTargets <- parseTargets AllowNoTargets False boptsCLI sma
`catch` \pex@(PrettyException ex) ->
case fromException $ toException ex of
Just (TargetParseException xs) ->
prettyThrowM $ GhciTargetParseException xs
_ -> throwM pex
unless (null fileTargetsRaw) $ throwM Can'tSpecifyFilesAndTargets
pure (Right normalTargets.targets)
parseMainIsTargets ::
HasEnvConfig env
=> BuildOptsCLI
-> SMActual GlobalPackage
-> Maybe Text
-> RIO env (Maybe (Map PackageName Target))
parseMainIsTargets buildOptsCLI sma mtarget = forM mtarget $ \target -> do
let boptsCLI = buildOptsCLI { targetsCLI = [target] }
targets <- parseTargets AllowNoTargets False boptsCLI sma
pure targets.targets
-- | Display PackageName + NamedComponent
displayPkgComponent :: (PackageName, NamedComponent) -> StyleDoc
displayPkgComponent =
style PkgComponent . fromString . T.unpack . renderPkgComponent
findFileTargets ::
HasEnvConfig env
=> [LocalPackage]
-> [Path Abs File]
-> RIO env (Map PackageName Target, Map PackageName [Path Abs File], [Path Abs File])
findFileTargets locals fileTargets = do
filePackages <- forM locals $ \lp -> do
PackageComponentFile _ compFiles _ _ <- getPackageFile lp.package lp.cabalFP
pure (lp, M.map (map dotCabalGetPath) compFiles)
let foundFileTargetComponents :: [(Path Abs File, [(PackageName, NamedComponent)])]
foundFileTargetComponents =
map (\fp -> (fp, ) $ L.sort $
concatMap (\(lp, files) -> map ((lp.package.name,) . fst)
(filter (elem fp . snd) (M.toList files))
) filePackages
) fileTargets
results <- forM foundFileTargetComponents $ \(fp, xs) ->
case xs of
[] -> do
prettyWarnL
[ flow "Couldn't find a component for file target"
, pretty fp <> "."
, flow "This means that the correct GHC options might not be used. \
\Attempting to load the file anyway."
]
pure $ Left fp
[x] -> do
prettyInfoL
[ flow "Using configuration for"
, displayPkgComponent x
, flow "to load"
, pretty fp
]
pure $ Right (fp, x)
(x:_) -> do
prettyWarn $
fillSep
[ flow "Multiple components contain file target"
, pretty fp <> ":"
, fillSep $ punctuate "," (map displayPkgComponent xs)
]
<> line
<> fillSep
[ flow "Guessing the first one,"
, displayPkgComponent x <> "."
]
pure $ Right (fp, x)
let (extraFiles, associatedFiles) = partitionEithers results
targetMap =
foldl' unionTargets M.empty $
map (\(_, (name, comp)) -> M.singleton name (TargetComps (S.singleton comp)))
associatedFiles
infoMap =
foldl' (M.unionWith (<>)) M.empty $
map (\(fp, (name, _)) -> M.singleton name [fp])
associatedFiles
pure (targetMap, infoMap, extraFiles)
getAllLocalTargets ::
HasEnvConfig env
=> GhciOpts
-> Map PackageName Target
-> Maybe (Map PackageName Target)
-> Map PackageName LocalPackage
-> RIO env [(PackageName, (Path Abs File, Target))]
getAllLocalTargets ghciOpts targets0 mainIsTargets localMap = do
-- Use the 'mainIsTargets' as normal targets, for CLI concision. See
-- #1845. This is a little subtle - we need to do the target parsing
-- independently in order to handle the case where no targets are
-- specified.
let targets = maybe targets0 (unionTargets targets0) mainIsTargets
packages <- view $ envConfigL . to (.sourceMap.project)
-- Find all of the packages that are directly demanded by the
-- targets.
let directlyWanted = flip mapMaybe (M.toList packages) $
\(name, pp) ->
case M.lookup name targets of
Just simpleTargets -> Just (name, (pp.cabalFP, simpleTargets))
Nothing -> Nothing
-- Figure out
let extraLoadDeps =
getExtraLoadDeps ghciOpts.loadLocalDeps localMap directlyWanted
if null extraLoadDeps
then pure directlyWanted
else do
let extraList' =
map (fromPackageName . fst) extraLoadDeps :: [StyleDoc]
extraList = mkNarrativeList (Just Current) False extraList'
if ghciOpts.loadLocalDeps
then prettyInfo $
fillSep $
[ flow "The following libraries will also be loaded into \
\GHCi because they are local dependencies of your \
\targets, and you specified"
, style Shell "--load-local-deps" <> ":"
]
<> extraList
else prettyInfo $
fillSep
( flow "The following libraries will also be loaded into \
\GHCi because they are intermediate dependencies of \
\your targets:"
: extraList
)
pure (directlyWanted ++ extraLoadDeps)
getAllNonLocalTargets ::
Map PackageName Target
-> RIO env [PackageName]
getAllNonLocalTargets targets = do
let isNonLocal (TargetAll PTDependency) = True
isNonLocal _ = False
pure $ map fst $ filter (isNonLocal . snd) (M.toList targets)
buildDepsAndInitialSteps :: HasEnvConfig env => GhciOpts -> [Text] -> RIO env ()
buildDepsAndInitialSteps ghciOpts localTargets = do
let targets = localTargets ++ map T.pack ghciOpts.additionalPackages
-- If necessary, do the build, for project packagee targets, only do
-- 'initialBuildSteps'.
whenJust (nonEmpty targets) $ \nonEmptyTargets ->
unless ghciOpts.noBuild $ do
-- only new project package targets could appear here
eres <- buildLocalTargets nonEmptyTargets
case eres of
Right () -> pure ()
Left err -> do
case fromException err of
Just (PrettyException prettyErr) -> prettyError $ pretty prettyErr
Nothing -> prettyError $ fromString (displayException err)
prettyWarn "Build failed, but trying to launch GHCi anyway"
checkAdditionalPackages :: MonadThrow m => [String] -> m [PackageName]
checkAdditionalPackages pkgs = forM pkgs $ \name -> do
let mres = (pkgName <$> parsePackageIdentifier name)
<|> parsePackageNameThrowing name
maybe (throwM $ InvalidPackageOption name) pure mres
runGhci ::
HasEnvConfig env
=> GhciOpts
-> [(PackageName, (Path Abs File, Target))]
-> Maybe (Path Abs File)
-> [GhciPkgInfo]
-> [Path Abs File]
-> [PackageName]
-> Map PackageName (Seq NamedComponent)
-> RIO env ()
runGhci
ghciOpts
targets
mainFile
pkgs
extraFiles
exposePackages
exposeInternalDep
= do
config <- view configL
let subDepsPackageUnhide pName deps =
if null deps then [] else ["-package", fromPackageName pName]
pkgopts = hidePkgOpts ++ genOpts ++ ghcOpts
shouldHidePackages = fromMaybe
(not (null pkgs && null exposePackages))
ghciOpts.hidePackages
hidePkgOpts =
if shouldHidePackages
then
["-hide-all-packages"]
-- This is necessary, because current versions of ghci will
-- entirely fail to start if base isn't visible. This is because
-- it tries to use the interpreter to set buffering options on
-- standard IO.
++ (if null targets then ["-package", "base"] else [])
++ concatMap
(\n -> ["-package", packageNameString n])
exposePackages
++ M.foldMapWithKey subDepsPackageUnhide exposeInternalDep
else []
oneWordOpts bio
| shouldHidePackages = bio.oneWordOpts ++ bio.packageFlags
| otherwise = bio.oneWordOpts
genOpts = nubOrd
(concatMap (concatMap (oneWordOpts . snd) . (.opts)) pkgs)
(omittedOpts, ghcOpts) = L.partition badForGhci $
concatMap (concatMap ((.opts) . snd) . (.opts)) pkgs
++ map
T.unpack
( fold config.ghcOptionsByCat
-- ^ include everything, locals, and targets
++ concatMap (getUserOptions . (.name)) pkgs
)
getUserOptions pkg =
M.findWithDefault [] pkg config.ghcOptionsByName
badForGhci x =
L.isPrefixOf "-O" x
|| elem x (words "-debug -threaded -ticky -static -Werror")
unless (null omittedOpts) $
prettyWarn $
fillSep
( flow "The following GHC options are incompatible with GHCi \
\and have not been passed to it:"
: mkNarrativeList (Just Current) False
(map fromString (nubOrd omittedOpts) :: [StyleDoc])
)
<> line
oiDir <- view objectInterfaceDirL
let odir =
[ "-odir=" <> toFilePathNoTrailingSep oiDir
, "-hidir=" <> toFilePathNoTrailingSep oiDir
]
prettyInfoL
( flow "Configuring GHCi with the following packages:"
: mkNarrativeList (Just Current) False
(map (fromPackageName . (.name)) pkgs :: [StyleDoc])
)
compilerExeName <-
view $ compilerPathsL . to (.compiler) . to toFilePath
let execGhci extras = do
menv <-
liftIO $ config.processContextSettings defaultEnvSettings
withPackageWorkingDir $ withProcessContext menv $ exec
(fromMaybe compilerExeName ghciOpts.ghcCommand)
( ("--interactive" : ) $
-- This initial "-i" resets the include directories to not
-- include CWD. If there aren't any packages, CWD is included.
(if null pkgs then id else ("-i" : )) $
odir
<> pkgopts
<> extras
<> ghciOpts.ghcOptions
<> ghciOpts.args
)
withPackageWorkingDir =
case pkgs of
[pkg] -> withWorkingDir (toFilePath pkg.dir)
_ -> id
-- Since usage of 'exec' does not pure, we cannot do any cleanup on ghci
-- exit. So, instead leave the generated files. To make this more
-- efficient and avoid gratuitous generation of garbage, the file names
-- are determined by hashing. This also has the nice side effect of making
-- it possible to copy the ghci invocation out of the log and have it
-- still work.
tmpDirectory <- getXdgDir XdgCache $
Just (relDirStackProgName </> relDirGhciScript)
ghciDir <- view ghciDirL
ensureDir ghciDir
ensureDir tmpDirectory
macrosOptions <- writeMacrosFile ghciDir pkgs
if ghciOpts.noLoadModules
then execGhci macrosOptions
else do
checkForDuplicateModules pkgs
scriptOptions <-
writeGhciScript
tmpDirectory
(renderScript pkgs mainFile ghciOpts.onlyMain extraFiles)
execGhci (macrosOptions ++ scriptOptions)
writeMacrosFile ::
HasTerm env
=> Path Abs Dir
-> [GhciPkgInfo]
-> RIO env [String]
writeMacrosFile outputDirectory pkgs = do
fps <- fmap (nubOrd . concatMap catMaybes) $
forM pkgs $ \pkg -> forM pkg.opts $ \(_, bio) -> do
let cabalMacros = bio.cabalMacros
exists <- liftIO $ doesFileExist cabalMacros
if exists
then pure $ Just cabalMacros
else do
prettyWarnL ["Didn't find expected autogen file:", pretty cabalMacros]
pure Nothing
files <- liftIO $ mapM (S8.readFile . toFilePath) fps
if null files then pure [] else do
out <- liftIO $ writeHashedFile outputDirectory relFileCabalMacrosH $
S8.concat $ map
(<> "\n#undef CURRENT_PACKAGE_KEY\n#undef CURRENT_COMPONENT_ID\n")
files
pure ["-optP-include", "-optP" <> toFilePath out]
writeGhciScript :: (MonadIO m) => Path Abs Dir -> GhciScript -> m [String]
writeGhciScript outputDirectory script = do
scriptPath <- liftIO $ writeHashedFile outputDirectory relFileGhciScript $
LBS.toStrict $ scriptToLazyByteString script
let scriptFilePath = toFilePath scriptPath
setScriptPerms scriptFilePath
pure ["-ghci-script=" <> scriptFilePath]
writeHashedFile ::
Path Abs Dir
-> Path Rel File
-> ByteString
-> IO (Path Abs File)
writeHashedFile outputDirectory relFile contents = do
relSha <- shaPathForBytes contents
let outDir = outputDirectory </> relSha
outFile = outDir </> relFile
alreadyExists <- doesFileExist outFile
unless alreadyExists $ do
ensureDir outDir
writeBinaryFileAtomic outFile $ byteString contents
pure outFile
renderScript ::
[GhciPkgInfo]
-> Maybe (Path Abs File)
-> Bool
-> [Path Abs File]
-> GhciScript
renderScript pkgs mainFile onlyMain extraFiles = do
let addPhase = cmdAdd $ S.fromList (map Left allModules ++ addMain)
addMain = maybe [] (L.singleton . Right) mainFile
modulePhase = cmdModule $ S.fromList allModules
allModules = nubOrd $ concatMap (M.keys . (.modules)) pkgs
case getFileTargets pkgs <> extraFiles of
[] ->
if onlyMain
then
if isJust mainFile
then cmdAdd (S.fromList addMain)
else mempty
else addPhase <> modulePhase
fileTargets -> cmdAdd (S.fromList (map Right fileTargets))
-- Hacky check if module / main phase should be omitted. This should be
-- improved if / when we have a better per-component load.
getFileTargets :: [GhciPkgInfo] -> [Path Abs File]
getFileTargets = concatMap (concat . maybeToList . (.targetFiles))
-- | Figure out the main-is file to load based on the targets. Asks the user for
-- input if there is more than one candidate main-is file.
figureOutMainFile ::
(HasRunner env, HasTerm env)
=> BuildOpts
-> Maybe (Map PackageName Target)
-> [(PackageName, (Path Abs File, Target))]
-> [GhciPkgInfo]
-> RIO env (Maybe (Path Abs File))
figureOutMainFile bopts mainIsTargets targets0 packages =
case candidates of
[] -> pure Nothing
[c@(_,_,fp)] -> do
prettyInfo $
fillSep
[ "Using"
, style Current "main"
, "module:"
]
<> line
<> renderCandidate c
<> line
pure (Just fp)
candidate:_ -> do
prettyWarn $
fillSep
[ "The"
, style Current "main"
, flow "module to load is ambiguous. Candidates are:"
]
<> line
<> mconcat (L.intersperse line (map renderCandidate candidates))
<> blankLine
<> flow "You can specify which one to pick by:"
<> line
<> bulletedList
[ fillSep
[ flow "Specifying targets to"
, style Shell (flow "stack ghci")
, "e.g."
, style Shell ( fillSep
[ flow "stack ghci"
, sampleTargetArg candidate
]
) <> "."
]
, fillSep
[ flow "Specifying what the"
, style Current "main"
, flow "is e.g."
, style Shell ( fillSep
[ flow "stack ghci"
, sampleMainIsArg candidate
]
) <> "."
]
, flow
$ "Choosing from the candidate above [1.."
<> show (length candidates)
<> "]."
]
<> line
liftIO userOption
where
targets = fromMaybe
(M.fromList $ map (\(k, (_, x)) -> (k, x)) targets0)
mainIsTargets
candidates = do
pkg <- packages
case M.lookup pkg.name targets of
Nothing -> []
Just target -> do
(component,mains) <-
M.toList $
M.filterWithKey (\k _ -> k `S.member` wantedComponents)
pkg.mainIs
main <- mains
pure (pkg.name, component, main)
where
wantedComponents =
wantedPackageComponents bopts target pkg.package
renderCandidate c@(pkgName, namedComponent, mainIs) =
let candidateIndex =
fromString . show . (+1) . fromMaybe 0 . L.elemIndex c
pkgNameText = fromPackageName pkgName
in hang 4
$ fill 4 ( candidateIndex candidates <> ".")
<> fillSep
[ "Package"
, style Current pkgNameText <> ","
, "component"
-- This is the format that can be directly copy-pasted as an
-- argument to `stack ghci`.
, style
PkgComponent
( pkgNameText
<> ":"
<> renderComponentTo namedComponent
)
<> ","
, "with"
, style Shell "main-is"
, "file:"
, pretty mainIs <> "."
]
candidateIndices = take (length candidates) [1 :: Int ..]
userOption = do
option <- prompt "Specify main module to use (press enter to load none): "
let selected = fromMaybe
((+1) $ length candidateIndices)
(readMaybe (T.unpack option) :: Maybe Int)
case L.elemIndex selected candidateIndices of
Nothing -> do
putStrLn
"Not loading any main modules, as no valid module selected"
putStrLn ""
pure Nothing
Just op -> do
(_, _, fp) <- maybe
(prettyThrowIO CandidatesIndexOutOfRangeBug)
pure
(candidates !? op)
putStrLn
("Loading main module from candidate " <>
show (op + 1) <> ", --main-is " <>
toFilePath fp)
putStrLn ""
pure $ Just fp
sampleTargetArg (pkg, comp, _) =
fromPackageName pkg
<> ":"
<> renderComponentTo comp
sampleMainIsArg (pkg, comp, _) =
fillSep
[ "--main-is"
, fromPackageName pkg <> ":" <> renderComponentTo comp
]
loadGhciPkgDescs ::
HasEnvConfig env
=> BuildOptsCLI
-> [(PackageName, (Path Abs File, Target))]
-> RIO env [GhciPkgDesc]
loadGhciPkgDescs buildOptsCLI localTargets =
forM localTargets $ \(name, (cabalFP, target)) ->
loadGhciPkgDesc buildOptsCLI name cabalFP target
-- | Load package description information for a ghci target.
loadGhciPkgDesc ::
HasEnvConfig env
=> BuildOptsCLI
-> PackageName
-> Path Abs File
-> Target
-> RIO env GhciPkgDesc
loadGhciPkgDesc buildOptsCLI name cabalFP target = do
econfig <- view envConfigL
compilerVersion <- view actualCompilerVersionL
let sm = econfig.sourceMap
-- Currently this source map is being build with
-- the default targets
sourceMapGhcOptions = fromMaybe [] $
((.projectCommon.ghcOptions) <$> M.lookup name sm.project)
<|>
((.depCommon.ghcOptions) <$> M.lookup name sm.deps)
sourceMapCabalConfigOpts = fromMaybe [] $
( (.projectCommon.cabalConfigOpts) <$> M.lookup name sm.project)
<|>
((.depCommon.cabalConfigOpts) <$> M.lookup name sm.deps)
sourceMapFlags =
maybe mempty (.projectCommon.flags) $ M.lookup name sm.project
config = PackageConfig
{ enableTests = True
, enableBenchmarks = True
, flags = getCliFlags <> sourceMapFlags
, ghcOptions = sourceMapGhcOptions
, cabalConfigOpts = sourceMapCabalConfigOpts
, compilerVersion = compilerVersion
, platform = view platformL econfig
}
-- TODO we've already parsed this information, otherwise we wouldn't have
-- figured out the cabalFP already. In the future: retain that
-- GenericPackageDescription in the relevant data structures to avoid
-- reparsing.
(gpdio, _name, _cabalFP) <-
loadCabalFilePath (Just stackProgName') (parent cabalFP)
gpkgdesc <- liftIO $ gpdio YesPrintWarnings
-- Source the package's *.buildinfo file created by configure if any. See
-- https://www.haskell.org/cabal/users-guide/developing-packages.html#system-dependent-parameters
buildinfofp <- parseRelFile (packageNameString name ++ ".buildinfo")
hasDotBuildinfo <- doesFileExist (parent cabalFP </> buildinfofp)
let mbuildinfofp
| hasDotBuildinfo = Just (parent cabalFP </> buildinfofp)
| otherwise = Nothing
mbuildinfo <- forM mbuildinfofp readDotBuildinfo
let pdp = resolvePackageDescription config gpkgdesc
package =
packageFromPackageDescription config (C.genPackageFlags gpkgdesc) $
maybe pdp (`C.updatePackageDescription` pdp) mbuildinfo
pure GhciPkgDesc
{ package
, cabalFP
, target
}
where
cliFlags = buildOptsCLI.flags
-- | All CLI Cabal flags for a package.
getCliFlags :: Map FlagName Bool
getCliFlags = Map.unions
[ Map.findWithDefault Map.empty (ACFByName name) cliFlags
, Map.findWithDefault Map.empty ACFAllProjectPackages cliFlags
]
getGhciPkgInfos ::
HasEnvConfig env
=> InstallMap
-> [PackageName]
-> Maybe (Map PackageName [Path Abs File])
-> [GhciPkgDesc]
-> RIO env [GhciPkgInfo]
getGhciPkgInfos installMap addPkgs mfileTargets localTargets = do
(installedMap, _, _, _) <- getInstalled installMap
let localLibs =
[ desc.package.name
| desc <- localTargets
, hasLocalComp isCLib desc.target
]
forM localTargets $ \pkgDesc ->
makeGhciPkgInfo installMap installedMap localLibs addPkgs mfileTargets pkgDesc
-- | Make information necessary to load the given package in GHCi.
makeGhciPkgInfo ::
HasEnvConfig env
=> InstallMap
-> InstalledMap
-> [PackageName]
-> [PackageName]
-> Maybe (Map PackageName [Path Abs File])
-> GhciPkgDesc
-> RIO env GhciPkgInfo
makeGhciPkgInfo installMap installedMap locals addPkgs mfileTargets pkgDesc = do
bopts <- view buildOptsL
let pkg = pkgDesc.package
cabalFP = pkgDesc.cabalFP
target = pkgDesc.target
name = pkg.name
(mods, files, opts) <-
getPackageOpts pkg installMap installedMap locals addPkgs cabalFP
let filteredOpts = filterWanted opts
filterWanted = M.filterWithKey (\k _ -> k `S.member` allWanted)
allWanted = wantedPackageComponents bopts target pkg
pure GhciPkgInfo
{ name
, opts = M.toList filteredOpts
, dir = parent cabalFP
, modules = unionModuleMaps $
map
( \(comp, mp) -> M.map
(\fp -> M.singleton fp (S.singleton (pkg.name, comp)))
mp
)
(M.toList (filterWanted mods))
, mainIs = M.map (mapMaybe dotCabalMainPath) files
, cFiles = mconcat
(M.elems (filterWanted (M.map (mapMaybe dotCabalCFilePath) files)))
, targetFiles = mfileTargets >>= M.lookup name
, package = pkg
}
-- NOTE: this should make the same choices as the components code in
-- 'loadLocalPackage'. Unfortunately for now we reiterate this logic
-- (differently).
wantedPackageComponents :: BuildOpts -> Target -> Package -> Set NamedComponent
wantedPackageComponents _ (TargetComps cs) _ = cs
wantedPackageComponents bopts (TargetAll PTProject) pkg =
( if hasBuildableMainLibrary pkg
then S.insert CLib (S.mapMonotonic CSubLib buildableForeignLibs')
else S.empty
)
<> S.mapMonotonic CExe buildableExes'
<> S.mapMonotonic CSubLib buildableSubLibs'
<> ( if bopts.tests
then S.mapMonotonic CTest buildableTestSuites'
else S.empty
)
<> ( if bopts.benchmarks
then S.mapMonotonic CBench buildableBenchmarks'
else S.empty
)
where
buildableForeignLibs' = buildableForeignLibs pkg
buildableSubLibs' = buildableSubLibs pkg
buildableExes' = buildableExes pkg
buildableTestSuites' = buildableTestSuites pkg
buildableBenchmarks' = buildableBenchmarks pkg
wantedPackageComponents _ _ _ = S.empty
checkForIssues :: HasTerm env => [GhciPkgInfo] -> RIO env ()
checkForIssues pkgs =
when (length pkgs > 1) $ do
-- Cabal flag issues could arise only when there are at least 2 packages
unless (null cabalFlagIssues) $ do
prettyWarn $
flow "There are Cabal flags for this project which may prevent \
\GHCi from loading your code properly. In some cases it \
\can also load some projects which would otherwise fail to \
\build."
<> blankLine
<> mconcat (L.intersperse blankLine cabalFlagIssues)
<> blankLine
<> flow "To resolve, remove the flag(s) from the Cabal file(s) and \
\instead put them at the top of the Haskell files."
<> blankLine
prettyWarnL
[ flow "It isn't yet possible to load multiple packages into GHCi in \
\all cases. For further information, see"
, style Url "https://ghc.haskell.org/trac/ghc/ticket/10827" <> "."
]
where
cabalFlagIssues = concatMap mixedFlag
[ ( "-XNoImplicitPrelude"