Skip to content

Add --with-repl flag to modify program the "repl" starts with #10996

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cabal/src/Distribution/Simple/GHC/Build/Link.hs
Original file line number Diff line number Diff line change
Expand Up @@ -751,10 +751,10 @@ runReplOrWriteFlags ghcProg lbi rflags ghcOpts pkg_name target =
verbosity = fromFlag $ setupVerbosity common
tempFileOptions = commonSetupTempFileOptions common
in case replOptionsFlagOutput (replReplOptions rflags) of
NoFlag ->
runGHCWithResponseFile
"ghc.rsp"
Nothing
NoFlag -> do
-- If a specific GHC implementation is specified, use it
runReplProgram
(flagToMaybe $ replWithRepl (replReplOptions rflags))
tempFileOptions
verbosity
ghcProg
Expand Down
51 changes: 20 additions & 31 deletions Cabal/src/Distribution/Simple/Program/GHC.hs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module Distribution.Simple.Program.GHC
, renderGhcOptions
, runGHC
, runGHCWithResponseFile
, runReplProgram
, packageDbArgsDb
, normaliseGhcArgs
) where
Expand Down Expand Up @@ -668,37 +669,7 @@ runGHCWithResponseFile fileNameTemplate encoding tempFileOptions verbosity ghcPr

args = progInvokeArgs invocation

-- Don't use response files if the first argument is `--interactive`, for
-- two related reasons.
--
-- `hie-bios` relies on a hack to intercept the command-line that `Cabal`
-- supplies to `ghc`. Specifically, `hie-bios` creates a script around
-- `ghc` that detects if the first option is `--interactive` and if so then
-- instead of running `ghc` it prints the command-line that `ghc` was given
-- instead of running the command:
--
-- https://github.com/haskell/hie-bios/blob/ce863dba7b57ded20160b4f11a487e4ff8372c08/wrappers/cabal#L7
--
-- … so we can't store that flag in the response file, otherwise that will
-- break. However, even if we were to add a special-case to keep that flag
-- out of the response file things would still break because `hie-bios`
-- stores the arguments to `ghc` that the wrapper script outputs and reuses
-- them later. That breaks if you use a response file because it will
-- store an argument like `@…/ghc36000-0.rsp` which is a temporary path
-- that no longer exists after the wrapper script completes.
--
-- The work-around here is that we don't use a response file at all if the
-- first argument (and only the first argument) to `ghc` is
-- `--interactive`. This ensures that `hie-bios` and all downstream
-- utilities (e.g. `haskell-language-server`) continue working.
--
--
useResponseFile =
case args of
"--interactive" : _ -> False
_ -> compilerSupportsResponseFiles

if not useResponseFile
if not compilerSupportsResponseFiles
then runProgramInvocation verbosity invocation
else do
let (rtsArgs, otherArgs) = splitRTSArgs args
Expand All @@ -721,6 +692,24 @@ runGHCWithResponseFile fileNameTemplate encoding tempFileOptions verbosity ghcPr

runProgramInvocation verbosity newInvocation

-- Start the repl. Either use `ghc`, or the program specified by the --with-repl flag.
runReplProgram
:: Maybe FilePath
-- ^ --with-repl argument
-> TempFileOptions
-> Verbosity
-> ConfiguredProgram
-> Compiler
-> Platform
-> Maybe (SymbolicPath CWD (Dir Pkg))
-> GhcOptions
-> IO ()
runReplProgram withReplProg tempFileOptions verbosity ghcProg comp platform mbWorkDir ghcOpts =
let replProg = case withReplProg of
Just path -> ghcProg{programLocation = FoundOnSystem path}
Nothing -> ghcProg
in runGHCWithResponseFile "ghci.rsp" Nothing tempFileOptions verbosity replProg comp platform mbWorkDir ghcOpts

ghcInvocation
:: Verbosity
-> ConfiguredProgram
Expand Down
10 changes: 9 additions & 1 deletion Cabal/src/Distribution/Simple/Setup/Repl.hs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ data ReplOptions = ReplOptions
{ replOptionsFlags :: [String]
, replOptionsNoLoad :: Flag Bool
, replOptionsFlagOutput :: Flag FilePath
, replWithRepl :: Flag FilePath
}
deriving (Show, Generic)

Expand Down Expand Up @@ -85,7 +86,7 @@ instance Binary ReplOptions
instance Structured ReplOptions

instance Monoid ReplOptions where
mempty = ReplOptions mempty (Flag False) NoFlag
mempty = ReplOptions mempty (Flag False) NoFlag NoFlag
mappend = (<>)

instance Semigroup ReplOptions where
Expand Down Expand Up @@ -229,4 +230,11 @@ replOptions _ =
replOptionsFlagOutput
(\p flags -> flags{replOptionsFlagOutput = p})
(reqArg "DIR" (succeedReadE Flag) flagToList)
, option
[]
["with-repl"]
"Give the path to a program to use for REPL"
replWithRepl
(\v flags -> flags{replWithRepl = v})
(reqArgFlag "PATH")
]
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ data ConstraintSource =
-- from Cabal >= 3.11
| ConstraintSourceMultiRepl

-- | Constraint introduced by --with-repl, which requires features
-- from Cabal >= 3.15
| ConstraintSourceWithRepl

-- | Constraint introduced by --enable-profiling-shared, which requires features
-- from Cabal >= 3.13
| ConstraintSourceProfiledDynamic
Expand Down Expand Up @@ -81,6 +85,8 @@ instance Pretty ConstraintSource where
text "config file, command line flag, or user target"
ConstraintSourceMultiRepl ->
text "--enable-multi-repl"
ConstraintSourceWithRepl ->
text "--with-repl"
ConstraintSourceProfiledDynamic ->
text "--enable-profiling-shared"
ConstraintSourceUnknown -> text "unknown source"
Expand Down
58 changes: 37 additions & 21 deletions cabal-install/src/Distribution/Client/CmdRepl.hs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ import Distribution.Simple.Utils
, wrapText
)
import Distribution.Solver.Types.ConstraintSource
( ConstraintSource (ConstraintSourceMultiRepl)
( ConstraintSource (ConstraintSourceMultiRepl, ConstraintSourceWithRepl)
)
import Distribution.Solver.Types.PackageConstraint
( PackageProperty (PackagePropertyVersion)
Expand Down Expand Up @@ -180,12 +180,10 @@ import Distribution.Client.ReplFlags
, topReplOptions
)
import Distribution.Compat.Binary (decode)
import Distribution.Simple.Flag (fromFlagOrDefault, pattern Flag)
import Distribution.Simple.Flag (flagToMaybe, fromFlagOrDefault, pattern Flag)
import Distribution.Simple.Program.Builtin (ghcProgram)
import Distribution.Simple.Program.Db (requireProgram)
import Distribution.Simple.Program.Types
( ConfiguredProgram (programOverrideEnv)
)
import System.Directory
( doesFileExist
, getCurrentDirectory
Expand Down Expand Up @@ -325,15 +323,34 @@ replAction flags@NixStyleFlags{extraFlags = r@ReplFlags{..}, ..} targetStrings g
-- We need to do this before solving, but the compiler version is only known
-- after solving (phaseConfigureCompiler), so instead of using
-- multiReplDecision we just check the flag.
let baseCtx' =
if fromFlagOrDefault False $
let multiReplEnabled =
fromFlagOrDefault False $
projectConfigMultiRepl (projectConfigShared $ projectConfig baseCtx)
<> replUseMulti

withReplEnabled =
isJust $ flagToMaybe $ replWithRepl configureReplOptions

addConstraintWhen cond constraint base_ctx =
if cond
then
baseCtx
base_ctx
& lProjectConfig . lProjectConfigShared . lProjectConfigConstraints
%~ (multiReplCabalConstraint :)
else baseCtx
%~ (constraint :)
else base_ctx

-- This is the constraint setup.Cabal>=3.11. 3.11 is when Cabal options
-- used for multi-repl were introduced.
-- Idelly we'd apply this constraint only on the closure of repl targets,
-- but that would require another solver run for marginal advantages that
-- will further shrink as 3.11 is adopted.
addMultiReplConstraint = addConstraintWhen multiReplEnabled $ requireCabal [3, 11] ConstraintSourceMultiRepl

-- Similarly, if you use `--with-repl` then your version of `Cabal` needs to
-- support the `--with-repl` flag.
addWithReplConstraint = addConstraintWhen withReplEnabled $ requireCabal [3, 15] ConstraintSourceWithRepl

baseCtx' = addMultiReplConstraint $ addWithReplConstraint baseCtx

(originalComponent, baseCtx'') <-
if null (envPackages replEnvFlags)
Expand Down Expand Up @@ -481,7 +498,7 @@ replAction flags@NixStyleFlags{extraFlags = r@ReplFlags{..}, ..} targetStrings g
}

-- run ghc --interactive with
runGHCWithResponseFile "ghci_multi.rsp" Nothing tempFileOptions verbosity ghcProg' compiler platform Nothing ghc_opts
runReplProgram (flagToMaybe $ replWithRepl replOpts') tempFileOptions verbosity ghcProg' compiler platform Nothing ghc_opts
else do
-- single target repl
replOpts'' <- case targetCtx of
Expand Down Expand Up @@ -526,17 +543,16 @@ replAction flags@NixStyleFlags{extraFlags = r@ReplFlags{..}, ..} targetStrings g

return targets

-- This is the constraint setup.Cabal>=3.11. 3.11 is when Cabal options
-- used for multi-repl were introduced.
-- Idelly we'd apply this constraint only on the closure of repl targets,
-- but that would require another solver run for marginal advantages that
-- will further shrink as 3.11 is adopted.
multiReplCabalConstraint =
( UserConstraint
(UserAnySetupQualifier (mkPackageName "Cabal"))
(PackagePropertyVersion $ orLaterVersion $ mkVersion [3, 11])
, ConstraintSourceMultiRepl
)
-- | Create a constraint which requires a later version of Cabal.
-- This is used for commands which require a specific feature from the Cabal library
-- such as multi-repl or the --with-repl flag.
requireCabal :: [Int] -> ConstraintSource -> (UserConstraint, ConstraintSource)
requireCabal version source =
( UserConstraint
(UserAnySetupQualifier (mkPackageName "Cabal"))
(PackagePropertyVersion $ orLaterVersion $ mkVersion version)
, source
)

-- | First version of GHC which supports multiple home packages
minMultipleHomeUnitsVersion :: Version
Expand Down
4 changes: 4 additions & 0 deletions cabal-testsuite/PackageTests/WithRepl/SimpleTests/ModuleA.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module ModuleA where

x :: Int
x = 42
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
cabal-version: 2.4
name: cabal-with-repl
version: 0.1.0.0
build-type: Simple

library
exposed-modules: ModuleA
build-depends: base
default-language: Haskell2010
19 changes: 19 additions & 0 deletions cabal-testsuite/PackageTests/WithRepl/SimpleTests/cabal.test.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Test.Cabal.Prelude
import System.Directory (getCurrentDirectory)
import System.FilePath ((</>))

main = do
-- Test that --with-repl works with a valid GHC path
cabalTest' "with-repl-valid-path" $ do
cabal' "clean" []
-- Get the path to the system GHC
ghc_prog <- requireProgramM ghcProgram
res <- cabalWithStdin "v2-repl" ["--with-repl=" ++ programPath ghc_prog] ""
assertOutputContains "Ok, one module loaded." res
assertOutputContains "GHCi, version" res

-- Test that --with-repl fails with an invalid path
cabalTest' "with-repl-invalid-path" $ do
cabal' "clean" []
res <- fails $ cabalWithStdin "v2-repl" ["--with-repl=/nonexistent/path/to/ghc"] ""
assertOutputContains "does not exist" res
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# cabal clean
# cabal v2-repl
Resolving dependencies...
Build profile: -w ghc-<GHCVER> -O1
In order, the following will be built:
- cabal-with-repl-0.1.0.0 (interactive) (lib) (first run)
Configuring library for cabal-with-repl-0.1.0.0...
Preprocessing library for cabal-with-repl-0.1.0.0...
Error: [Cabal-7125]
repl failed for cabal-with-repl-0.1.0.0-inplace.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# cabal clean
# cabal v2-repl
Resolving dependencies...
Build profile: -w ghc-<GHCVER> -O1
In order, the following will be built:
- cabal-with-repl-0.1.0.0 (interactive) (lib) (first run)
Configuring library for cabal-with-repl-0.1.0.0...
Preprocessing library for cabal-with-repl-0.1.0.0...
4 changes: 4 additions & 0 deletions cabal-testsuite/PackageTests/WithRepl/WithExe/Main.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import ModuleA

main :: IO ()
main = print $ "My specific executable"
3 changes: 3 additions & 0 deletions cabal-testsuite/PackageTests/WithRepl/WithExe/Main2.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module Main where

main = print "My other executable"
4 changes: 4 additions & 0 deletions cabal-testsuite/PackageTests/WithRepl/WithExe/ModuleA.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module ModuleA where

add :: Num a => a -> a -> a
add x y = x + y
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
cabal-version: 2.4
name: cabal-with-repl-exe
version: 0.1.0.0
build-type: Simple

executable test-exe
main-is: Main.hs
build-depends: base
default-language: Haskell2010

executable test-exe2
main-is: Main2.hs
build-depends: base
default-language: Haskell2010
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages: .
32 changes: 32 additions & 0 deletions cabal-testsuite/PackageTests/WithRepl/WithExe/cabal.test.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{-# LANGUAGE OverloadedStrings #-}
import Distribution.Simple.Program
import Distribution.Simple.Program.GHC
import Distribution.Simple.Utils
import Test.Cabal.Prelude

-- On windows this test passed but then fails in CI with
--
-- D:\a\_temp\cabal-testsuite-12064\cabal-multi.dist\work\.\dist\multi-out-63884\paths\cabal-with-repl-exe-0.1.0.0-inplace-test-exe: removeDirectoryRecursive:removeContentsRecursive:removePathRecursive:removeContentsRecursive:removePathRecursive:DeleteFile "\\\\?\\D:\\a\\_temp\\cabal-testsuite-12064\\cabal-multi.dist\\work\\dist\\multi-out-63884\\paths\\cabal-with-repl-exe-0.1.0.0-inplace-test-exe": permission denied (The process cannot access the file because it is being used by another process.)
--

main = do
mkTest "normal-repl" $ \exePath -> do
-- Try using the executable with --with-repl
res <- cabalWithStdin "v2-repl" ["--with-repl=" ++ exePath, "test-exe"] ""
assertOutputContains "My specific executable" res
mkTest "multi-repl" $ \exePath -> do
requireGhcSupportsMultiRepl
res <- cabalWithStdin "v2-repl" ["--enable-multi-repl", "--with-repl=" ++ exePath, "all"] ""
assertOutputContains "My specific executable" res


mkTest name act = do
skipIfCIAndWindows 11026
cabalTest' name $ recordMode DoNotRecord $ do
-- Build the executable
cabal' "v2-build" ["test-exe"]
-- Get the path to the built executable
withPlan $ do
exePath <- planExePath "cabal-with-repl-exe" "test-exe"
act exePath

4 changes: 4 additions & 0 deletions cabal-testsuite/src/Test/Cabal/Prelude.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,10 @@ skipUnlessJavaScript = skipUnlessIO "needs the JavaScript backend" isJavaScript
skipIfJavaScript :: IO ()
skipIfJavaScript = skipIfIO "incompatible with the JavaScript backend" isJavaScript

requireGhcSupportsMultiRepl :: TestM ()
requireGhcSupportsMultiRepl =
skipUnlessGhcVersion ">= 9.4"

isWindows :: Bool
isWindows = buildOS == Windows

Expand Down
26 changes: 26 additions & 0 deletions changelog.d/issue-9115.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
synopsis: Add --with-repl flag to specify alternative REPL program
packages: [cabal-install, Cabal]
prs: [10996]
issues: [9115]
---

Added a new `--with-repl` command-line option that allows specifying an alternative
program to use when starting a REPL session, instead of the default GHC.

This is particularly useful for tools like `doctest` and `hie-bios` that need to
intercept the REPL session to perform their own operations. Previously, these tools
had to use `--with-ghc` which required them to proxy all GHC invocations, including
dependency compilation, making the implementation more complex.

The `--with-repl` option only affects the final REPL invocation, simplifying the
implementation of such wrapper tools.

Example usage:
```bash
cabal repl --with-repl=doctest
cabal repl --with-repl=/path/to/custom/ghc
```

This change also removes the special handling for response files with `--interactive`
mode, as tools are now expected to handle response files appropriately.
Loading
Loading