Skip to content

Commit 0dc1b43

Browse files
committed
Load session using cabal's --with-repl command
This avoids quite a lot of hacky implementations to find the correct GHC version, libdir and ghc-pkg location, as well as global package db stuff.
1 parent 5715478 commit 0dc1b43

File tree

12 files changed

+214
-81
lines changed

12 files changed

+214
-81
lines changed

.github/workflows/haskell.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,20 @@ jobs:
1515
strategy:
1616
fail-fast: false
1717
matrix:
18-
cabal: ['3.14.1.0', '3.10.2.0']
19-
ghc: ['9.12.2', '9.10.1', '9.8.4', '9.6.7', '9.4.8']
18+
cabal: ['head', '3.14.1.0', '3.10.2.0']
19+
ghc: ['9.12.2', '9.10.1', '9.8.4', '9.6.7']
2020
os: [ubuntu-latest, macOS-latest, windows-latest]
21+
exclude:
22+
- cabal: 'head'
23+
os: macOS-latest
2124

2225
steps:
2326
- uses: actions/checkout@v4
2427
- id: extra-ghc
2528
uses: haskell-actions/setup@v2
2629
with:
2730
cabal-version: ${{ matrix.cabal }}
28-
ghc-version: '9.2.8' # This needs to be in-sync with 'extraGhcVersion'
31+
ghc-version: '9.4.8' # This needs to be in-sync with 'extraGhcVersion'
2932

3033
- uses: haskell-actions/setup@v2
3134
with:
@@ -35,7 +38,7 @@ jobs:
3538

3639
- name: Print extra ghc version
3740
# Needs to be in sync with 'extraGhcVersion'
38-
run: ghc-9.2.8 --version
41+
run: ghc-9.4.8 --version
3942

4043
- name: Print normal ghc version
4144
run: ghc --version
@@ -93,3 +96,6 @@ jobs:
9396
ghc-version: ${{ matrix.ghc }}
9497
- name: Compile Windows wrapper
9598
run: ghc -hide-all-packages -package base -package directory -package process -Wall -Werror wrappers/cabal.hs -o CabalWrapper
99+
100+
- name: Compile `--with-repl` Windows wrapper
101+
run: ghc -hide-all-packages -package base -package directory -package process -Wall -Werror wrappers/cabal-with-repl.hs -o CabalWithReplWrapper

hie-bios.cabal

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ Build-Type: Simple
1515
extra-doc-files: ChangeLog.md
1616
Extra-Source-Files: README.md
1717
wrappers/cabal
18+
wrappers/cabal-with-repl
1819
wrappers/cabal.hs
20+
wrappers/cabal-with-repl.hs
1921
tests/configs/*.yaml
2022
tests/projects/cabal-with-ghc/cabal-with-ghc.cabal
2123
tests/projects/cabal-with-ghc/cabal.project
@@ -144,7 +146,7 @@ Extra-Source-Files: README.md
144146
tests/projects/failing-multi-repl-cabal-project/multi-repl-cabal-fail/src/Lib.hs
145147
tests/projects/failing-multi-repl-cabal-project/NotInPath.hs
146148

147-
tested-with: GHC ==9.4.8 || ==9.6.7 || ==9.8.4 || ==9.10.1 || ==9.12.2
149+
tested-with: GHC ==9.6.7 || ==9.8.4 || ==9.10.1 || ==9.12.2
148150

149151
Library
150152
Default-Language: Haskell2010

src/HIE/Bios/Cradle.hs

Lines changed: 121 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ import qualified HIE.Bios.Types as Types
7070
import qualified HIE.Bios.Ghc.Gap as Gap
7171

7272
import GHC.Fingerprint (fingerprintString)
73-
import GHC.ResponseFile (escapeArgs)
73+
import GHC.ResponseFile (escapeArgs, expandResponse)
7474

7575
import Data.Version
7676
import Data.IORef
@@ -612,10 +612,64 @@ cabalCradle l cs wdir mc projectFile
612612
-- ./dist-newstyle/tmp/environment.-24811: createDirectory: does not exist (No such file or directory)
613613
liftIO $ createDirectoryIfMissing True (buildDir </> "tmp")
614614
-- Need to pass -v0 otherwise we get "resolving dependencies..."
615-
cabalProc <- cabalProcess l vs projectFile wdir "v2-exec" $ ["ghc", "-v0", "--"] ++ args
615+
cabalProc <- cabalExecGhc l vs projectFile wdir args
616616
Process.readProcessWithCwd' l cabalProc ""
617617
}
618618

619+
cabalExecGhc :: LogAction IO (WithSeverity Log) -> ProgramVersions -> CradleProjectConfig -> FilePath -> [String] -> CradleLoadResultT IO CreateProcess
620+
cabalExecGhc l vs projectFile wdir args = do
621+
cabalProcess l vs projectFile wdir "v2-exec" $ ["ghc", "-v0", "--"] ++ args
622+
623+
processCabalLoadStyle :: MonadIO m => LogAction IO (WithSeverity Log) -> ResolvedCradles a -> CradleProjectConfig -> [Char] -> Maybe FilePath -> [Char] -> LoadStyle -> m ([FilePath], [FilePath], [FilePath])
624+
processCabalLoadStyle l cradles projectFile workDir mc fp loadStyle = do
625+
let fpModule = fromMaybe (fixTargetPath fp) mc
626+
let (cabalArgs, loadingFiles, extraDeps) = case loadStyle of
627+
LoadFile -> ([fpModule], [fp], [])
628+
LoadWithContext fps ->
629+
let allModulesFpsDeps = ((fpModule, fp, []) : moduleFilesFromSameProject fps)
630+
allModules = nubOrd $ fst3 <$> allModulesFpsDeps
631+
allFiles = nubOrd $ snd3 <$> allModulesFpsDeps
632+
allFpsDeps = nubOrd $ concatMap thd3 allModulesFpsDeps
633+
in (["--keep-temp-files", "--enable-multi-repl"] ++ allModules, allFiles, allFpsDeps)
634+
635+
liftIO $ l <& LogComputedCradleLoadStyle "cabal" loadStyle `WithSeverity` Info
636+
liftIO $ l <& LogCabalLoad fp mc (prefix <$> resolvedCradles cradles) loadingFiles `WithSeverity` Debug
637+
pure (cabalArgs, loadingFiles, extraDeps)
638+
where
639+
-- Need to make relative on Windows, due to a Cabal bug with how it
640+
-- parses file targets with a C: drive in it. So we decide to make
641+
-- the paths relative to the working directory.
642+
fixTargetPath x
643+
| isWindows && hasDrive x = makeRelative workDir x
644+
| otherwise = x
645+
moduleFilesFromSameProject fps =
646+
[ (fromMaybe (fixTargetPath file) old_mc, file, deps)
647+
| file <- fps,
648+
-- Lookup the component for the old file
649+
Just (ResolvedCradle {concreteCradle = ConcreteCabal ct, cradleDeps = deps}) <- [selectCradle prefix file (resolvedCradles cradles)],
650+
-- Only include this file if the old component is in the same project
651+
(projectConfigFromMaybe (cradleRoot cradles) (cabalProjectFile ct)) == projectFile,
652+
let old_mc = cabalComponent ct
653+
]
654+
655+
cabalLoadFilesBefore315 :: LogAction IO (WithSeverity Log) -> ProgramVersions -> CradleProjectConfig -> [Char] -> [String] -> CradleLoadResultT IO CreateProcess
656+
cabalLoadFilesBefore315 l progVersions projectFile workDir args = do
657+
let cabalCommand = "v2-repl"
658+
659+
cabalProcess l progVersions projectFile workDir cabalCommand args `modCradleError` \err -> do
660+
deps <- cabalCradleDependencies projectFile workDir workDir
661+
pure $ err {cradleErrorDependencies = cradleErrorDependencies err ++ deps}
662+
663+
cabalLoadFilesWithRepl :: LogAction IO (WithSeverity Log) -> CradleProjectConfig -> FilePath -> [String] -> CradleLoadResultT IO CreateProcess
664+
cabalLoadFilesWithRepl l projectFile workDir args = do
665+
let cabalCommand = "v2-repl"
666+
667+
newEnvironment <- liftIO Process.getCleanEnvironment
668+
wrapper_fp <- liftIO $ withReplWrapperTool l (proc "ghc") workDir
669+
pure $ (proc "cabal" ([cabalCommand, "--keep-temp-files", "--with-repl", wrapper_fp] <> projectFileProcessArgs projectFile <> args))
670+
{ env = Just newEnvironment
671+
, cwd = Just workDir
672+
}
619673

620674
-- | Execute a cabal process in our custom cache-build directory configured
621675
-- with the custom ghc executable.
@@ -653,7 +707,7 @@ cabalProcess l vs cabalProject workDir command args = do
653707

654708
setupCabalCommand :: FilePath -> IO CreateProcess
655709
setupCabalCommand ghcPkgPath = do
656-
wrapper_fp <- withGhcWrapperTool l ("ghc", []) workDir
710+
wrapper_fp <- withGhcWrapperTool l (proc "ghc") workDir
657711
buildDir <- cabalBuildDir workDir
658712
let extraCabalArgs =
659713
[ "--builddir=" <> buildDir
@@ -688,7 +742,7 @@ withGhcPkgTool ghcPathAbs libdir = do
688742
ghcPkgPath = guessGhcPkgFromGhc ghcName
689743
if isWindows
690744
then pure ghcPkgPath
691-
else withWrapperTool ghcPkgPath
745+
else withGhcPkgShim ghcPkgPath
692746
where
693747
ghcDir = takeDirectory ghcPathAbs
694748

@@ -707,7 +761,7 @@ withGhcPkgTool ghcPathAbs libdir = do
707761
--
708762
-- If we used the raw executable, i.e. not wrapped in a shim, then 'cabal'
709763
-- can not use the given 'ghc-pkg'.
710-
withWrapperTool ghcPkg = do
764+
withGhcPkgShim ghcPkg = do
711765
let globalPackageDb = libdir </> "package.conf.d"
712766
-- This is the same as the wrapper-shims ghc-pkg usually comes with.
713767
contents = unlines
@@ -762,32 +816,40 @@ processCabalWrapperArgs args =
762816
in Just (dir, final_args)
763817
_ -> Nothing
764818

765-
-- | GHC process information.
766-
-- Consists of the filepath to the ghc executable and
767-
-- arguments to the executable.
768-
type GhcProc = (FilePath, [String])
819+
-- | GHC process that accepts GHC arguments.
820+
type GhcProc = [String] -> CreateProcess
769821

770822
-- | Generate a fake GHC that can be passed to cabal or stack
771823
-- when run with --interactive, it will print out its
772824
-- command-line arguments and exit
773825
withGhcWrapperTool :: LogAction IO (WithSeverity Log) -> GhcProc -> FilePath -> IO FilePath
774-
withGhcWrapperTool l (mbGhc, ghcArgs) wdir = do
775-
let wrapperContents = if isWindows then cabalWrapperHs else cabalWrapper
776-
withExtension fp = if isWindows then fp <.> "exe" else fp
777-
srcHash = show (fingerprintString wrapperContents)
778-
cacheFile (withExtension "wrapper") srcHash $ \wrapper_fp ->
779-
if isWindows
780-
then
781-
withSystemTempDirectory "hie-bios" $ \ tmpDir -> do
782-
let wrapper_hs = wrapper_fp -<.> "hs"
783-
writeFile wrapper_hs wrapperContents
784-
let ghcArgsWithExtras = ghcArgs ++ ["-rtsopts=ignore", "-outputdir", tmpDir, "-o", wrapper_fp, wrapper_hs]
785-
let ghcProc = (proc mbGhc ghcArgsWithExtras)
786-
{ cwd = Just wdir
787-
}
788-
l <& LogCreateProcessRun ghcProc `WithSeverity` Debug
789-
readCreateProcess ghcProc "" >>= putStr
790-
else writeFile wrapper_fp wrapperContents
826+
withGhcWrapperTool l mkGhcCall wdir = do
827+
withWrapperTool l mkGhcCall wdir "wrapper" cabalWrapperHs cabalWrapper
828+
829+
-- | Generate a script/binary that can be passed to cabal's '--with-repl'.
830+
-- On windows, this compiles a Haskell file, while on other systems, we persist
831+
withReplWrapperTool :: LogAction IO (WithSeverity Log) -> GhcProc -> FilePath -> IO FilePath
832+
withReplWrapperTool l mkGhcCall wdir =
833+
withWrapperTool l mkGhcCall wdir "repl-wrapper" cabalWithReplWrapperHs cabalWithReplWrapper
834+
835+
withWrapperTool :: LogAction IO (WithSeverity Log) -> GhcProc -> String -> FilePath -> String -> String -> IO FilePath
836+
withWrapperTool l mkGhcCall wdir baseName windowsWrapper unixWrapper = do
837+
let wrapperContents = if isWindows then windowsWrapper else unixWrapper
838+
withExtension fp = if isWindows then fp <.> "exe" else fp
839+
srcHash = show (fingerprintString wrapperContents)
840+
cacheFile (withExtension baseName) srcHash $ \wrapper_fp ->
841+
if isWindows
842+
then
843+
withSystemTempDirectory "hie-bios" $ \ tmpDir -> do
844+
let wrapper_hs = wrapper_fp -<.> "hs"
845+
writeFile wrapper_hs wrapperContents
846+
let ghcArgs = ["-rtsopts=ignore", "-outputdir", tmpDir, "-o", wrapper_fp, wrapper_hs]
847+
let ghcProc = (mkGhcCall ghcArgs)
848+
{ cwd = Just wdir
849+
}
850+
l <& LogCreateProcessRun ghcProc `WithSeverity` Debug
851+
readCreateProcess ghcProc "" >>= putStr
852+
else writeFile wrapper_fp wrapperContents
791853

792854
-- | Create and cache a file in hie-bios's cache directory.
793855
--
@@ -885,6 +947,20 @@ isCabalMultipleCompSupported vs = do
885947
(Just cabal, Just ghc) -> pure $ ghc >= makeVersion [9, 4] && cabal >= makeVersion [3, 11]
886948
_ -> pure False
887949

950+
data CabalLoadFeature
951+
= CabalWithRepl
952+
| CabalWithGhcShimWrapper
953+
954+
determineCabalLoadFeature :: MonadIO m => ProgramVersions -> m CabalLoadFeature
955+
determineCabalLoadFeature vs = do
956+
cabal_version <- liftIO $ runCachedIO $ cabalVersion vs
957+
-- determine which load style is supported by this cabal cradle.
958+
case cabal_version of
959+
Just ver
960+
| ver >= makeVersion [3, 15] -> pure CabalWithRepl
961+
| otherwise -> pure CabalWithGhcShimWrapper
962+
_ -> pure CabalWithGhcShimWrapper
963+
888964
cabalAction
889965
:: ResolvedCradles a
890966
-> FilePath
@@ -895,7 +971,8 @@ cabalAction
895971
-> LoadStyle
896972
-> CradleLoadResultT IO ComponentOptions
897973
cabalAction cradles workDir mc l projectFile fp loadStyle = do
898-
multiCompSupport <- isCabalMultipleCompSupported (cradleProgramVersions cradles)
974+
let progVersions = cradleProgramVersions cradles
975+
multiCompSupport <- isCabalMultipleCompSupported progVersions
899976
-- determine which load style is supported by this cabal cradle.
900977
determinedLoadStyle <- case loadStyle of
901978
LoadWithContext _ | not multiCompSupport -> do
@@ -909,25 +986,12 @@ cabalAction cradles workDir mc l projectFile fp loadStyle = do
909986
pure LoadFile
910987
_ -> pure loadStyle
911988

912-
let fpModule = fromMaybe (fixTargetPath fp) mc
913-
let (cabalArgs, loadingFiles, extraDeps) = case determinedLoadStyle of
914-
LoadFile -> ([fpModule], [fp], [])
915-
LoadWithContext fps ->
916-
let allModulesFpsDeps = ((fpModule, fp, []) : moduleFilesFromSameProject fps)
917-
allModules = nubOrd $ fst3 <$> allModulesFpsDeps
918-
allFiles = nubOrd $ snd3 <$> allModulesFpsDeps
919-
allFpsDeps = nubOrd $ concatMap thd3 allModulesFpsDeps
920-
in (["--keep-temp-files", "--enable-multi-repl"] ++ allModules, allFiles, allFpsDeps)
921-
922-
liftIO $ l <& LogComputedCradleLoadStyle "cabal" determinedLoadStyle `WithSeverity` Info
923-
liftIO $ l <& LogCabalLoad fp mc (prefix <$> resolvedCradles cradles) loadingFiles `WithSeverity` Debug
924-
925-
let cabalCommand = "v2-repl"
989+
(cabalArgs, loadingFiles, extraDeps) <- processCabalLoadStyle l cradles projectFile workDir mc fp determinedLoadStyle
926990

927-
cabalProc <-
928-
cabalProcess l (cradleProgramVersions cradles) projectFile workDir cabalCommand cabalArgs `modCradleError` \err -> do
929-
deps <- cabalCradleDependencies projectFile workDir workDir
930-
pure $ err {cradleErrorDependencies = cradleErrorDependencies err ++ deps}
991+
cabalFeatures <- determineCabalLoadFeature progVersions
992+
cabalProc <- case cabalFeatures of
993+
CabalWithRepl -> cabalLoadFilesWithRepl l projectFile workDir cabalArgs
994+
CabalWithGhcShimWrapper -> cabalLoadFilesBefore315 l progVersions projectFile workDir cabalArgs
931995

932996
(ex, output, stde, [(_, maybeArgs)]) <- liftIO $ Process.readProcessWithOutputs [hie_bios_output] l workDir cabalProc
933997
let args = fromMaybe [] maybeArgs
@@ -943,7 +1007,7 @@ cabalAction cradles workDir mc l projectFile fp loadStyle = do
9431007

9441008
when (ex /= ExitSuccess) $ do
9451009
deps <- liftIO $ cabalCradleDependencies projectFile workDir workDir
946-
let cmd = show (["cabal", cabalCommand] <> cabalArgs)
1010+
let cmd = prettyCmdSpec (cmdspec cabalProc)
9471011
let errorMsg = "Failed to run " <> cmd <> " in directory \"" <> workDir <> "\". Consult the logs for full command and error."
9481012
throwCE (CradleError deps ex ([errorMsg] <> errorDetails) loadingFiles)
9491013

@@ -954,25 +1018,17 @@ cabalAction cradles workDir mc l projectFile fp loadStyle = do
9541018
-- root of the component, so we are right in trivial cases at least.
9551019
deps <- liftIO $ cabalCradleDependencies projectFile workDir workDir
9561020
throwCE (CradleError (deps <> extraDeps) ex (["Failed to parse result of calling cabal"] <> errorDetails) loadingFiles)
957-
Just (componentDir, final_args) -> do
1021+
Just (componentDir, ghc_args) -> do
9581022
deps <- liftIO $ cabalCradleDependencies projectFile workDir componentDir
1023+
final_args <- case cabalFeatures of
1024+
CabalWithRepl -> liftIO $ processCabalGhcResponseFile ghc_args
1025+
CabalWithGhcShimWrapper -> pure ghc_args
9591026
CradleLoadResultT $ pure $ makeCradleResult (ex, stde, componentDir, final_args) (deps <> extraDeps) loadingFiles
960-
where
961-
-- Need to make relative on Windows, due to a Cabal bug with how it
962-
-- parses file targets with a C: drive in it. So we decide to make
963-
-- the paths relative to the working directory.
964-
fixTargetPath x
965-
| isWindows && hasDrive x = makeRelative workDir x
966-
| otherwise = x
967-
moduleFilesFromSameProject fps =
968-
[ (fromMaybe (fixTargetPath file) old_mc, file, deps)
969-
| file <- fps,
970-
-- Lookup the component for the old file
971-
Just (ResolvedCradle {concreteCradle = ConcreteCabal ct, cradleDeps = deps}) <- [selectCradle prefix file (resolvedCradles cradles)],
972-
-- Only include this file if the old component is in the same project
973-
(projectConfigFromMaybe (cradleRoot cradles) (cabalProjectFile ct)) == projectFile,
974-
let old_mc = cabalComponent ct
975-
]
1027+
1028+
processCabalGhcResponseFile :: [String] -> IO [String]
1029+
processCabalGhcResponseFile args = do
1030+
expanded_args <- expandResponse args
1031+
pure $ removeInteractive expanded_args
9761032

9771033
removeInteractive :: [String] -> [String]
9781034
removeInteractive = filter (/= "--interactive")
@@ -1086,9 +1142,9 @@ stackAction
10861142
-> IO (CradleLoadResult ComponentOptions)
10871143
stackAction workDir mc syaml l fp loadStyle = do
10881144
logCradleHasNoSupportForLoadWithContext l loadStyle "stack"
1089-
let ghcProcArgs = ("stack", stackYamlProcessArgs syaml <> ["exec", "ghc", "--"])
1145+
let ghcProc args = proc "stack" (stackYamlProcessArgs syaml <> ["exec", "ghc", "--"] <> args)
10901146
-- Same wrapper works as with cabal
1091-
wrapper_fp <- withGhcWrapperTool l ghcProcArgs workDir
1147+
wrapper_fp <- withGhcWrapperTool l ghcProc workDir
10921148
(ex1, _stdo, stde, [(_, maybeArgs)]) <-
10931149
Process.readProcessWithOutputs [hie_bios_output] l workDir
10941150
$ stackProcess syaml

src/HIE/Bios/Types.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import qualified Control.Monad.Fail as Fail
1616
import Data.Maybe (fromMaybe)
1717
import qualified Data.Text as T
1818
import Prettyprinter
19-
import System.Process.Extra (CreateProcess (env, cmdspec), CmdSpec (..))
19+
import System.Process.Extra (CreateProcess (env, cmdspec), CmdSpec (..), showCommandForUser)
2020

2121
----------------------------------------------------------------
2222
-- Environment variables used by hie-bios.
@@ -316,7 +316,7 @@ data ComponentOptions = ComponentOptions {
316316
-- | Prettify 'CmdSpec', so we can show the command to a user
317317
prettyCmdSpec :: CmdSpec -> String
318318
prettyCmdSpec (ShellCommand s) = s
319-
prettyCmdSpec (RawCommand cmd args) = cmd ++ " " ++ unwords args
319+
prettyCmdSpec (RawCommand cmd args) = showCommandForUser cmd args
320320

321321
-- | Pretty print hie-bios's relevant environment variables.
322322
prettyProcessEnv :: CreateProcess -> [String]

src/HIE/Bios/Wrappers.hs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
1-
{-# LANGUAGE TemplateHaskell #-}
21
{-# LANGUAGE QuasiQuotes #-}
3-
{-# LANGUAGE CPP #-}
4-
module HIE.Bios.Wrappers (cabalWrapper, cabalWrapperHs) where
2+
{-# LANGUAGE TemplateHaskell #-}
53

6-
import Data.FileEmbed
7-
#if __GLASGOW_HASKELL__ >= 903
8-
hiding (makeRelativeToProject)
9-
#endif
10-
import Language.Haskell.TH.Syntax
4+
module HIE.Bios.Wrappers (
5+
cabalWrapper,
6+
cabalWithReplWrapper,
7+
cabalWrapperHs,
8+
cabalWithReplWrapperHs,
9+
) where
10+
11+
import Data.FileEmbed ( embedStringFile )
12+
import Language.Haskell.TH.Syntax ( makeRelativeToProject )
1113

1214
cabalWrapper :: String
1315
cabalWrapper = $(makeRelativeToProject "wrappers/cabal" >>= embedStringFile)
1416

1517
cabalWrapperHs :: String
1618
cabalWrapperHs = $(makeRelativeToProject "wrappers/cabal.hs" >>= embedStringFile)
1719

20+
cabalWithReplWrapper :: String
21+
cabalWithReplWrapper = $(makeRelativeToProject "wrappers/cabal-with-repl" >>= embedStringFile)
22+
23+
cabalWithReplWrapperHs :: String
24+
cabalWithReplWrapperHs = $(makeRelativeToProject "wrappers/cabal-with-repl.hs" >>= embedStringFile)

0 commit comments

Comments
 (0)