diff --git a/Cabal/src/Distribution/Simple/GHC/Build/Link.hs b/Cabal/src/Distribution/Simple/GHC/Build/Link.hs index 962d0d95c7a..bba36bb3809 100644 --- a/Cabal/src/Distribution/Simple/GHC/Build/Link.hs +++ b/Cabal/src/Distribution/Simple/GHC/Build/Link.hs @@ -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 diff --git a/Cabal/src/Distribution/Simple/Program/GHC.hs b/Cabal/src/Distribution/Simple/Program/GHC.hs index 29b722ad278..17bc27f151c 100644 --- a/Cabal/src/Distribution/Simple/Program/GHC.hs +++ b/Cabal/src/Distribution/Simple/Program/GHC.hs @@ -16,6 +16,7 @@ module Distribution.Simple.Program.GHC , renderGhcOptions , runGHC , runGHCWithResponseFile + , runReplProgram , packageDbArgsDb , normaliseGhcArgs ) where @@ -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 @@ -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 diff --git a/Cabal/src/Distribution/Simple/Setup/Repl.hs b/Cabal/src/Distribution/Simple/Setup/Repl.hs index 4b73cb34b99..ceec4649ad8 100644 --- a/Cabal/src/Distribution/Simple/Setup/Repl.hs +++ b/Cabal/src/Distribution/Simple/Setup/Repl.hs @@ -54,6 +54,7 @@ data ReplOptions = ReplOptions { replOptionsFlags :: [String] , replOptionsNoLoad :: Flag Bool , replOptionsFlagOutput :: Flag FilePath + , replWithRepl :: Flag FilePath } deriving (Show, Generic) @@ -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 @@ -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") ] diff --git a/cabal-install-solver/src/Distribution/Solver/Types/ConstraintSource.hs b/cabal-install-solver/src/Distribution/Solver/Types/ConstraintSource.hs index 3f171b3c6d7..0deb786959b 100644 --- a/cabal-install-solver/src/Distribution/Solver/Types/ConstraintSource.hs +++ b/cabal-install-solver/src/Distribution/Solver/Types/ConstraintSource.hs @@ -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 @@ -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" diff --git a/cabal-install/src/Distribution/Client/CmdRepl.hs b/cabal-install/src/Distribution/Client/CmdRepl.hs index e8c64c6999f..7fb3f700d09 100644 --- a/cabal-install/src/Distribution/Client/CmdRepl.hs +++ b/cabal-install/src/Distribution/Client/CmdRepl.hs @@ -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) @@ -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 @@ -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) @@ -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 @@ -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 diff --git a/cabal-testsuite/PackageTests/WithRepl/SimpleTests/ModuleA.hs b/cabal-testsuite/PackageTests/WithRepl/SimpleTests/ModuleA.hs new file mode 100644 index 00000000000..3a6beb2e2c4 --- /dev/null +++ b/cabal-testsuite/PackageTests/WithRepl/SimpleTests/ModuleA.hs @@ -0,0 +1,4 @@ +module ModuleA where + +x :: Int +x = 42 diff --git a/cabal-testsuite/PackageTests/WithRepl/SimpleTests/cabal-with-repl.cabal b/cabal-testsuite/PackageTests/WithRepl/SimpleTests/cabal-with-repl.cabal new file mode 100644 index 00000000000..852360cf05c --- /dev/null +++ b/cabal-testsuite/PackageTests/WithRepl/SimpleTests/cabal-with-repl.cabal @@ -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 diff --git a/cabal-testsuite/PackageTests/WithRepl/SimpleTests/cabal.test.hs b/cabal-testsuite/PackageTests/WithRepl/SimpleTests/cabal.test.hs new file mode 100644 index 00000000000..71c584120f5 --- /dev/null +++ b/cabal-testsuite/PackageTests/WithRepl/SimpleTests/cabal.test.hs @@ -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 diff --git a/cabal-testsuite/PackageTests/WithRepl/SimpleTests/cabal.with-repl-invalid-path.out b/cabal-testsuite/PackageTests/WithRepl/SimpleTests/cabal.with-repl-invalid-path.out new file mode 100644 index 00000000000..b59b62a074a --- /dev/null +++ b/cabal-testsuite/PackageTests/WithRepl/SimpleTests/cabal.with-repl-invalid-path.out @@ -0,0 +1,10 @@ +# cabal clean +# cabal v2-repl +Resolving dependencies... +Build profile: -w ghc- -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. diff --git a/cabal-testsuite/PackageTests/WithRepl/SimpleTests/cabal.with-repl-valid-path.out b/cabal-testsuite/PackageTests/WithRepl/SimpleTests/cabal.with-repl-valid-path.out new file mode 100644 index 00000000000..f1ca1ffc808 --- /dev/null +++ b/cabal-testsuite/PackageTests/WithRepl/SimpleTests/cabal.with-repl-valid-path.out @@ -0,0 +1,8 @@ +# cabal clean +# cabal v2-repl +Resolving dependencies... +Build profile: -w ghc- -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... diff --git a/cabal-testsuite/PackageTests/WithRepl/WithExe/Main.hs b/cabal-testsuite/PackageTests/WithRepl/WithExe/Main.hs new file mode 100644 index 00000000000..fe589eaec4f --- /dev/null +++ b/cabal-testsuite/PackageTests/WithRepl/WithExe/Main.hs @@ -0,0 +1,4 @@ +import ModuleA + +main :: IO () +main = print $ "My specific executable" diff --git a/cabal-testsuite/PackageTests/WithRepl/WithExe/Main2.hs b/cabal-testsuite/PackageTests/WithRepl/WithExe/Main2.hs new file mode 100644 index 00000000000..77c6e482831 --- /dev/null +++ b/cabal-testsuite/PackageTests/WithRepl/WithExe/Main2.hs @@ -0,0 +1,3 @@ +module Main where + +main = print "My other executable" diff --git a/cabal-testsuite/PackageTests/WithRepl/WithExe/ModuleA.hs b/cabal-testsuite/PackageTests/WithRepl/WithExe/ModuleA.hs new file mode 100644 index 00000000000..8810d78ced9 --- /dev/null +++ b/cabal-testsuite/PackageTests/WithRepl/WithExe/ModuleA.hs @@ -0,0 +1,4 @@ +module ModuleA where + +add :: Num a => a -> a -> a +add x y = x + y diff --git a/cabal-testsuite/PackageTests/WithRepl/WithExe/cabal-with-repl-exe.cabal b/cabal-testsuite/PackageTests/WithRepl/WithExe/cabal-with-repl-exe.cabal new file mode 100644 index 00000000000..5354e28791f --- /dev/null +++ b/cabal-testsuite/PackageTests/WithRepl/WithExe/cabal-with-repl-exe.cabal @@ -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 diff --git a/cabal-testsuite/PackageTests/WithRepl/WithExe/cabal.project b/cabal-testsuite/PackageTests/WithRepl/WithExe/cabal.project new file mode 100644 index 00000000000..e6fdbadb439 --- /dev/null +++ b/cabal-testsuite/PackageTests/WithRepl/WithExe/cabal.project @@ -0,0 +1 @@ +packages: . diff --git a/cabal-testsuite/PackageTests/WithRepl/WithExe/cabal.test.hs b/cabal-testsuite/PackageTests/WithRepl/WithExe/cabal.test.hs new file mode 100644 index 00000000000..fd05e5a0e06 --- /dev/null +++ b/cabal-testsuite/PackageTests/WithRepl/WithExe/cabal.test.hs @@ -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 + diff --git a/cabal-testsuite/src/Test/Cabal/Prelude.hs b/cabal-testsuite/src/Test/Cabal/Prelude.hs index a5a76260514..3a12298608c 100644 --- a/cabal-testsuite/src/Test/Cabal/Prelude.hs +++ b/cabal-testsuite/src/Test/Cabal/Prelude.hs @@ -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 diff --git a/changelog.d/issue-9115.md b/changelog.d/issue-9115.md new file mode 100644 index 00000000000..139f97f8a19 --- /dev/null +++ b/changelog.d/issue-9115.md @@ -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. diff --git a/doc/cabal-commands.rst b/doc/cabal-commands.rst index 8ed8393df62..b9354db992d 100644 --- a/doc/cabal-commands.rst +++ b/doc/cabal-commands.rst @@ -1112,6 +1112,24 @@ configuration from the 'cabal.project', 'cabal.project.local' and other files. Disables the loading of target modules at startup. +.. option:: --with-repl=PATH + + Specifies an alternative program to use when starting the 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. + + Unlike ``--with-ghc`` which affects all GHC invocations (including dependency + compilation), ``--with-repl`` only affects the final REPL invocation, making + it much simpler for wrapper tools to implement. + + Examples: + + :: + + $ cabal repl --with-repl=doctest + $ cabal repl --with-repl=/path/to/custom/ghc + .. option:: -b DEPENDENCIES or -bDEPENDENCIES, --build-depends=DEPENDENCIES A way to experiment with libraries without needing to download