Skip to content

Commit 728cd30

Browse files
Fix two issues when starting the repl
Issue 1: In 3.14 there was a poor error when starting a repl in a project context without any targets. In 3.16, this error regressed, so cabal just exited cleanly. Issue 2: The repl was broken when started from a global context. Issue 1 is fixed by checking to see if there are any user targets, and issuing a proper error if there are none. Issue 2 is fixed by specifying the correct fake target which is constructed when starting the repl in the global context. Both are reported in #11107 and fixed in this patch. Co-authored-by: Matthew Pickering <[email protected]>
1 parent 1e3c355 commit 728cd30

File tree

34 files changed

+516
-38
lines changed

34 files changed

+516
-38
lines changed

cabal-install/src/Distribution/Client/CmdRepl.hs

Lines changed: 102 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ import Distribution.Simple.Compiler
106106
)
107107
import Distribution.Simple.Program.GHC
108108
import Distribution.Simple.Setup
109-
( ReplOptions (..)
109+
( Flag
110+
, ReplOptions (..)
110111
, commonSetupTempFileOptions
111112
)
112113
import Distribution.Simple.Utils
@@ -170,8 +171,8 @@ import Data.List
170171
import qualified Data.Map as Map
171172
import qualified Data.Set as Set
172173
import Distribution.Client.ProjectConfig
173-
( ProjectConfig (projectConfigShared)
174-
, ProjectConfigShared (projectConfigConstraints, projectConfigMultiRepl)
174+
( ProjectConfig (..)
175+
, ProjectConfigShared (..)
175176
)
176177
import Distribution.Client.ReplFlags
177178
( EnvFlags (envIncludeTransitive, envPackages)
@@ -195,6 +196,8 @@ import System.FilePath
195196
, splitSearchPath
196197
, (</>)
197198
)
199+
import Text.PrettyPrint hiding ((<>))
200+
import Distribution.Types.PackageName.Magic ( fakePackageId )
198201

199202
replCommand :: CommandUI (NixStyleFlags ReplFlags)
200203
replCommand =
@@ -281,17 +284,29 @@ multiReplDecision ctx compiler flags =
281284
-- For more details on how this works, see the module
282285
-- "Distribution.Client.ProjectOrchestration"
283286
replAction :: NixStyleFlags ReplFlags -> [String] -> GlobalFlags -> IO ()
284-
replAction flags@NixStyleFlags{extraFlags = r@ReplFlags{..}, ..} targetStrings globalFlags =
285-
withContextAndSelectors verbosity AcceptNoTargets (Just LibKind) flags targetStrings globalFlags ReplCommand $ \targetCtx ctx targetSelectors -> do
287+
replAction flags@NixStyleFlags{extraFlags = replFlags@ReplFlags{..}, configFlags} targetStrings globalFlags = do
288+
withCtx verbosity targetStrings $ \targetCtx ctx userTargetSelectors -> do
286289
when (buildSettingOnlyDeps (buildSettings ctx)) $
287290
dieWithException verbosity ReplCommandDoesn'tSupport
288291
let projectRoot = distProjectRootDirectory $ distDirLayout ctx
289292
distDir = distDirectory $ distDirLayout ctx
290293

291-
baseCtx <- case targetCtx of
292-
ProjectContext -> return ctx
294+
-- After ther user selectors have been resolved, and it's decided what context
295+
-- we're in, implement repl-specific behaviour.
296+
(baseCtx, targetSelectors) <- case targetCtx of
297+
-- If in the project context, and no selectors are provided
298+
-- then produce an error.
299+
ProjectContext -> do
300+
let projectFile = projectConfigProjectFile . projectConfigShared $ projectConfig ctx
301+
let pkgs = projectPackages $ projectConfig ctx
302+
case userTargetSelectors of
303+
[] -> dieWithException verbosity $
304+
RenderReplTargetProblem [render (reportProjectNoTarget projectFile pkgs)]
305+
_ -> return (ctx, userTargetSelectors)
306+
-- In the global context, construct a fake package which can be used to start
307+
-- a repl with extra arguments if `-b` is given.
293308
GlobalContext -> do
294-
unless (null targetStrings) $
309+
unless (null userTargetSelectors) $
295310
dieWithException verbosity $
296311
ReplTakesNoArguments targetStrings
297312
let
@@ -303,12 +318,18 @@ replAction flags@NixStyleFlags{extraFlags = r@ReplFlags{..}, ..} targetStrings g
303318
library = emptyLibrary{libBuildInfo = lBuildInfo}
304319
lBuildInfo =
305320
emptyBuildInfo
306-
{ targetBuildDepends = [baseDep]
321+
{ targetBuildDepends = [baseDep] ++ envPackages replEnvFlags
307322
, defaultLanguage = Just Haskell2010
308323
}
309324
baseDep = Dependency "base" anyVersion mainLibSet
310325

311-
updateContextAndWriteProjectFile' ctx sourcePackage
326+
-- Write the fake package
327+
updatedCtx <- updateContextAndWriteProjectFile' ctx sourcePackage
328+
-- Specify the selector for this package
329+
let fakeSelector = TargetPackage TargetExplicitNamed [fakePackageId] Nothing
330+
return (updatedCtx, [fakeSelector])
331+
332+
-- For the script context, no special behaviour.
312333
ScriptContext scriptPath scriptExecutable -> do
313334
unless (length targetStrings == 1) $
314335
dieWithException verbosity $
@@ -318,7 +339,8 @@ replAction flags@NixStyleFlags{extraFlags = r@ReplFlags{..}, ..} targetStrings g
318339
dieWithException verbosity $
319340
ReplTakesSingleArgument targetStrings
320341

321-
updateContextAndWriteProjectFile ctx scriptPath scriptExecutable
342+
updatedCtx <- updateContextAndWriteProjectFile ctx scriptPath scriptExecutable
343+
return (updatedCtx, userTargetSelectors)
322344

323345
-- If multi-repl is used, we need a Cabal recent enough to handle it.
324346
-- We need to do this before solving, but the compiler version is only known
@@ -361,7 +383,7 @@ replAction flags@NixStyleFlags{extraFlags = r@ReplFlags{..}, ..} targetStrings g
361383
-- especially in the no-project case.
362384
withInstallPlan (lessVerbose verbosity) baseCtx' $ \elaboratedPlan sharedConfig -> do
363385
-- targets should be non-empty map, but there's no NonEmptyMap yet.
364-
targets <- validatedTargets (projectConfigShared (projectConfig ctx)) (pkgConfigCompiler sharedConfig) elaboratedPlan targetSelectors
386+
targets <- validatedTargets' (projectConfigShared (projectConfig ctx)) (pkgConfigCompiler sharedConfig) elaboratedPlan targetSelectors
365387

366388
let
367389
(unitId, _) = fromMaybe (error "panic: targets should be non-empty") $ safeHead $ Map.toList targets
@@ -385,7 +407,7 @@ replAction flags@NixStyleFlags{extraFlags = r@ReplFlags{..}, ..} targetStrings g
385407
let ProjectBaseContext{..} = baseCtx''
386408

387409
-- Recalculate with updated project.
388-
targets <- validatedTargets (projectConfigShared projectConfig) (pkgConfigCompiler elaboratedShared') elaboratedPlan targetSelectors
410+
targets <- validatedTargets' (projectConfigShared projectConfig) (pkgConfigCompiler elaboratedShared') elaboratedPlan targetSelectors
389411

390412
let
391413
elaboratedPlan' =
@@ -518,31 +540,13 @@ replAction flags@NixStyleFlags{extraFlags = r@ReplFlags{..}, ..} targetStrings g
518540
go m ("PATH", Just s) = foldl' (\m' f -> Map.insertWith (+) f 1 m') m (splitSearchPath s)
519541
go m _ = m
520542

543+
withCtx ctxVerbosity strings =
544+
withContextAndSelectors ctxVerbosity AcceptNoTargets (Just LibKind) flags strings globalFlags ReplCommand
545+
521546
verbosity = cfgVerbosity normal flags
522547
tempFileOptions = commonSetupTempFileOptions $ configCommonFlags configFlags
523548

524-
validatedTargets ctx compiler elaboratedPlan targetSelectors = do
525-
let multi_repl_enabled = multiReplDecision ctx compiler r
526-
-- Interpret the targets on the command line as repl targets
527-
-- (as opposed to say build or haddock targets).
528-
targets <-
529-
either (reportTargetProblems verbosity) return $
530-
resolveTargetsFromSolver
531-
(selectPackageTargets multi_repl_enabled)
532-
selectComponentTarget
533-
elaboratedPlan
534-
Nothing
535-
targetSelectors
536-
537-
-- Reject multiple targets, or at least targets in different
538-
-- components. It is ok to have two module/file targets in the
539-
-- same component, but not two that live in different components.
540-
when (Set.size (distinctTargetComponents targets) > 1 && not (useMultiRepl multi_repl_enabled)) $
541-
reportTargetProblems
542-
verbosity
543-
[multipleTargetsProblem multi_repl_enabled targets]
544-
545-
return targets
549+
validatedTargets' = validatedTargets verbosity replFlags
546550

547551
-- | Create a constraint which requires a later version of Cabal.
548552
-- This is used for commands which require a specific feature from the Cabal library
@@ -555,6 +559,69 @@ requireCabal version source =
555559
, source
556560
)
557561

562+
reportProjectNoTarget :: Flag FilePath -> [String] -> Doc
563+
reportProjectNoTarget projectFile pkgs =
564+
case (null pkgs, projectName) of
565+
(True, Just project) ->
566+
text "There are no packages in"
567+
<+> (project <> char '.')
568+
<+> text "Please add a package to the project and"
569+
<+> pickComponent
570+
(True, Nothing) ->
571+
text "Please add a package to the project and" <+> pickComponent
572+
(False, Just project) ->
573+
text "Please"
574+
<+> pickComponent
575+
<+> text "The packages in"
576+
<+> project
577+
<+> (text "from which to select a component target are" <> colon)
578+
$+$ nest 1 (vcat [text "-" <+> text pkg | pkg <- sort pkgs])
579+
(False, Nothing) ->
580+
text "Please"
581+
<+> pickComponent
582+
<+> (text "The packages from which to select a component in 'cabal.project'" <> comma)
583+
<+> (text "the implicit default as if `--project-file=cabal.project` was added as a command option" <> comma)
584+
<+> (text "are" <> colon)
585+
$+$ nest 1 (vcat [text "-" <+> text pkg | pkg <- sort pkgs])
586+
where
587+
projectName = case projectFile of
588+
Flag "" -> Nothing
589+
Flag n -> Just $ quotes (text n)
590+
_ -> Nothing
591+
pickComponent = text "pick a single [package:][ctype:]component (or all) as target for the REPL command."
592+
593+
-- | Invariant: validatedTargets returns at least one target for the REPL.
594+
validatedTargets
595+
:: Verbosity
596+
-> ReplFlags
597+
-> ProjectConfigShared
598+
-> Compiler
599+
-> ElaboratedInstallPlan
600+
-> [TargetSelector]
601+
-> IO TargetsMap
602+
validatedTargets verbosity replFlags ctx compiler elaboratedPlan targetSelectors = do
603+
let multi_repl_enabled = multiReplDecision ctx compiler replFlags
604+
-- Interpret the targets on the command line as repl targets (as opposed to
605+
-- say build or haddock targets).
606+
targets <-
607+
either (reportTargetProblems verbosity) return $
608+
resolveTargetsFromSolver
609+
(selectPackageTargets multi_repl_enabled)
610+
selectComponentTarget
611+
elaboratedPlan
612+
Nothing
613+
targetSelectors
614+
615+
-- Reject multiple targets, or at least targets in different components. It is
616+
-- ok to have two module/file targets in the same component, but not two that
617+
-- live in different components.
618+
when (Set.size (distinctTargetComponents targets) > 1 && not (useMultiRepl multi_repl_enabled)) $
619+
reportTargetProblems
620+
verbosity
621+
[multipleTargetsProblem multi_repl_enabled targets]
622+
623+
return targets
624+
558625
-- | First version of GHC which supports multiple home packages
559626
minMultipleHomeUnitsVersion :: Version
560627
minMultipleHomeUnitsVersion = mkVersion [9, 4]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module File where
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Test.Cabal.Prelude
2+
3+
main = do
4+
cabalTest' "repl -b option" $ do
5+
cabal' "clean" []
6+
res <- cabalWithStdin "v2-repl" ["-b", "containers"] ":set"
7+
assertOutputContains "Ok, two modules loaded." res
8+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
packages: alt
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module ModuleA where
2+
3+
a :: Int
4+
a = 42
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module ModuleC where
2+
3+
c :: Int
4+
c = 42
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
name: alt
2+
version: 0.1
3+
build-type: Simple
4+
cabal-version: >= 1.10
5+
6+
library
7+
exposed-modules: ModuleA, ModuleC
8+
build-depends: base
9+
default-language: Haskell2010
10+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# cabal clean
2+
# cabal v2-repl
3+
Resolving dependencies...
4+
Build profile: -w ghc-<GHCVER> -O1
5+
In order, the following will be built:
6+
- alt-0.1 (interactive) (lib) (first run)
7+
Configuring library for alt-0.1...
8+
Preprocessing library for alt-0.1...
9+
Build profile: -w ghc-<GHCVER> -O1
10+
In order, the following will be built:
11+
- alt-0.1 (lib) (configuration changed)
12+
Configuring library for alt-0.1...
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# cabal clean
2+
# cabal v2-repl
3+
Resolving dependencies...
4+
Build profile: -w ghc-<GHCVER> -O1
5+
In order, the following will be built:
6+
- alt-0.1 (interactive) (lib) (first run)
7+
Configuring library for alt-0.1...
8+
Preprocessing library for alt-0.1...
9+
Build profile: -w ghc-<GHCVER> -O1
10+
In order, the following will be built:
11+
- alt-0.1 (lib) (configuration changed)
12+
Configuring library for alt-0.1...

cabal-testsuite/PackageTests/ReplOptions/cabal.test.hs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,46 @@
11
import Test.Cabal.Prelude
22

3+
singleOpts = ["--repl-options=-fwrite-interface"]
4+
multiOpts = "--repl-options=-fdefer-typed-holes" : singleOpts
5+
altProject = ("--project-file=alt.project" :)
6+
37
main = do
48
cabalTest' "single-repl-options" $ do
59
cabal' "clean" []
6-
res <- cabalWithStdin "v2-repl" ["--repl-options=-fwrite-interface"] ":set"
10+
res <- cabalWithStdin "v2-repl" singleOpts ":set"
711
assertOutputContains "Ok, two modules loaded." res
8-
assertOutputContains " -fwrite-interface" res
12+
13+
cabalTest' "alt-single-repl-options" $ do
14+
cabal' "clean" []
15+
-- We can 'cabal repl' without a target when the project has a single package.
16+
void $ cabalWithStdin "v2-repl" (altProject singleOpts) ":set"
17+
918
cabalTest' "multiple-repl-options" $ do
1019
cabal' "clean" []
11-
res <- cabalWithStdin "v2-repl" ["--repl-options=-fwrite-interface", "--repl-options=-fdefer-typed-holes"] ":set"
20+
res <- cabalWithStdin "v2-repl" multiOpts ":set"
1221
assertOutputContains "Ok, two modules loaded." res
1322
assertOutputContains " -fwrite-interface" res
1423
assertOutputContains " -fdefer-typed-holes" res
24+
25+
cabalTest' "alt-multiple-repl-options" $ do
26+
cabal' "clean" []
27+
-- We can 'cabal repl' without a target when the project has a single package.
28+
void $ cabalWithStdin "v2-repl" (altProject multiOpts) ":set"
29+
1530
cabalTest' "single-repl-options-multiple-flags" $ do
1631
cabal' "clean" []
1732
res <- cabalWithStdin "v2-repl" ["--repl-options=-fdefer-typed-holes -fwrite-interface"] ":set"
1833
assertOutputContains "Ok, two modules loaded." res
1934
assertOutputContains " -fwrite-interface" res
2035
assertOutputContains " -fdefer-typed-holes" res
36+
2137
cabalTest' "single-repl-options-multiple-flags-negative" $ do
2238
cabal' "clean" []
2339
res <- fails $ cabalWithStdin "v2-repl" ["--repl-options=-fwrite-interface -fdiagnostics-show-baret"] ":set"
2440
assertOutputDoesNotContain "Ok, two modules loaded." res
2541
assertOutputContains "unrecognised flag: -fdiagnostics-show-baret" res
2642
assertOutputContains "did you mean one of:" res
43+
2744
cabalTest' "multiple-repl-options-multiple-flags" $ do
2845
cabal' "clean" []
2946
res <- cabalWithStdin "v2-repl" [

0 commit comments

Comments
 (0)