on master help sign in
Merge pull request #665 from obsidiansystems/eac@ghcide-compat

IDE compatibility

commit52b3009ec63cc0cad63eeca69212851df3b94f72 authorElliot Cameron <3noch@users.noreply.github.com> dateFri 10 Apr 2020, 19:10 (7 years ago) parent14f6fe9bc81a43
4 files changed+236−71
lib/command/obelisk-command.cabal+1 lib/command/src/Obelisk/Command.hs+76−15 lib/command/src/Obelisk/Command/Project.hs+5−13 lib/command/src/Obelisk/Command/Run.hs+154−43
lib/command/obelisk-command.cabal+1
@@ -31,6 +31,7 @@ library
3131 , megaparsec
3232 , modern-uri
3333 , monad-loops
34+ , monoidal-containers
3435 , mtl
3536 , network
3637 , network-uri
lib/command/src/Obelisk/Command.hs+76−15
@@ -3,15 +3,18 @@
33{-# LANGUAGE MultiParamTypeClasses #-}
44{-# LANGUAGE OverloadedStrings #-}
55{-# LANGUAGE ScopedTypeVariables #-}
6+{-# LANGUAGE TupleSections #-}
67module Obelisk.Command where
78
8−import Control.Monad.IO.Class (liftIO)
9+import Control.Monad.IO.Class (MonadIO, liftIO)
910import Data.Bool (bool)
1011import Data.Foldable (for_)
1112import Data.List (isInfixOf, isPrefixOf)
12−import Data.List.NonEmpty (NonEmpty)
13+import Data.List.NonEmpty (NonEmpty, nonEmpty)
1314import qualified Data.List.NonEmpty as NonEmpty
15+import qualified Data.Map as Map
1416import qualified Data.Text as T
17+import Data.Traversable (for)
1518import Options.Applicative
1619import Options.Applicative.Help.Pretty (text, (<$$>))
1720import System.Directory
@@ -81,11 +84,11 @@ initForce = switch (long "force" <> help "Allow ob init to overwrite files")
8184data ObCommand
8285 = ObCommand_Init InitSource Bool
8386 | ObCommand_Deploy DeployCommand
84− | ObCommand_Run
87+ | ObCommand_Run [(FilePath, Interpret)]
8588 | ObCommand_Profile String [String]
8689 | ObCommand_Thunk ThunkOption
87− | ObCommand_Repl
88− | ObCommand_Watch
90+ | ObCommand_Repl [(FilePath, Interpret)]
91+ | ObCommand_Watch [(FilePath, Interpret)]
8992 | ObCommand_Shell ShellOpts
9093 | ObCommand_Doc String [String] -- shell and list of packages
9194 | ObCommand_Hoogle String Int -- shell and port
@@ -96,6 +99,7 @@ data ObInternal
9699 -- the preprocessor argument syntax is also handled outside
97100 -- optparse-applicative, but it shouldn't ever conflict with another syntax
98101 = ObInternal_ApplyPackages String String String [String]
102+ | ObInternal_ExportGhciConfig [(FilePath, Interpret)]
99103 deriving Show
100104
101105obCommand :: ArgsConfig -> Parser ObCommand
@@ -103,11 +107,11 @@ obCommand cfg = hsubparser
103107 (mconcat
104108 [ command "init" $ info (ObCommand_Init <$> initSource <*> initForce) $ progDesc "Initialize an Obelisk project"
105109 , command "deploy" $ info (ObCommand_Deploy <$> deployCommand cfg) $ progDesc "Prepare a deployment for an Obelisk project"
106− , command "run" $ info (pure ObCommand_Run) $ progDesc "Run current project in development mode"
110+ , command "run" $ info (ObCommand_Run <$> interpretOpts) $ progDesc "Run current project in development mode"
107111 , command "profile" $ info (uncurry ObCommand_Profile <$> profileCommand) $ progDesc "Run current project with profiling enabled"
108112 , command "thunk" $ info (ObCommand_Thunk <$> thunkOption) $ progDesc "Manipulate thunk directories"
109− , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
110− , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
113+ , command "repl" $ info (ObCommand_Repl <$> interpretOpts) $ progDesc "Open an interactive interpreter"
114+ , command "watch" $ info (ObCommand_Watch <$> interpretOpts) $ progDesc "Watch current project for errors and warnings"
111115 , command "shell" $ info (ObCommand_Shell <$> shellOpts) $ progDesc "Enter a shell with project dependencies"
112116 , command "doc" $ info (ObCommand_Doc <$> shellFlags <*> packageNames) $
113117 progDesc "List paths to haddock documentation for specified packages"
@@ -115,8 +119,14 @@ obCommand cfg = hsubparser
115119 text "Hint: To open the documentation you can pipe the output of this command like"
116120 <$$> text "ob doc reflex reflex-dom-core | xargs -n1 xdg-open")
117121 , command "hoogle" $ info (ObCommand_Hoogle <$> shellFlags <*> portOpt 8080) $ progDesc "Run a hoogle server locally for your project's dependency tree"
122+ , command "internal" $ info (ObCommand_Internal <$> internalCommand) $ progDesc "Internal Obelisk commands with unstable APIs"
118123 ])
119124
125+internalCommand :: Parser ObInternal
126+internalCommand = hsubparser $ mconcat
127+ [ command "export-ghci-configuration" $ info (ObInternal_ExportGhciConfig <$> interpretOpts) $ progDesc "Export the GHCi configuration used by ob run, etc.; useful for IDE integration"
128+ ]
129+
120130packageNames :: Parser [String]
121131packageNames = some (strArgument (metavar "PACKAGE-NAME..."))
122132
@@ -237,6 +247,7 @@ thunkOption = hsubparser $ mconcat
237247data ShellOpts
238248 = ShellOpts
239249 { _shellOpts_shell :: String
250+ , _shellOpts_interpretPaths :: [(FilePath, Interpret)]
240251 , _shellOpts_command :: Maybe String
241252 }
242253 deriving Show
@@ -247,9 +258,30 @@ shellFlags =
247258 <|> flag "ghc" "ghcjs" (long "ghcjs" <> help "Enter a shell having ghcjs rather than ghc")
248259 <|> strOption (short 'A' <> long "argument" <> metavar "NIXARG" <> help "Use the environment specified by the given nix argument of `shells'")
249260
261+interpretOpts :: Parser [(FilePath, Interpret)]
262+interpretOpts = many
263+ ( (, Interpret_Interpret) <$>
264+ strOption (common <> long "interpret" <> help
265+ "Don't pre-build packages found in DIR when constructing the package database. The default behavior is \
266+ \'--interpret <project-root>', which will load everything which is unpacked into GHCi. \
267+ \ Use --interpret and --no-interpret multiple times to add or remove multiple trees \
268+ \ from the environment. Settings for right-most directories will \
269+ \ override settings for any identical directories given earlier."
270+ )
271+ <|> (, Interpret_NoInterpret) <$>
272+ strOption (common <> long "no-interpret" <> help
273+ "Make packages found in DIR available in the package database (but only when they are used dependencies). \
274+ \ This will build the packages in DIR before loading GHCi. \
275+ \See help for --interpret for how the two options are related."
276+ )
277+ )
278+ where
279+ common = action "directory" <> metavar "DIR"
280+
250281shellOpts :: Parser ShellOpts
251282shellOpts = ShellOpts
252283 <$> shellFlags
284+ <*> interpretOpts
253285 <*> optional (strArgument (metavar "COMMAND"))
254286
255287portOpt :: Int -> Parser Int
@@ -352,7 +384,7 @@ ob = \case
352384 Just RemoteBuilder_ObeliskVM -> (:[]) <$> VmBuilder.getNixBuildersArg
353385 DeployCommand_Update -> deployUpdate "."
354386 DeployCommand_Test (platform, extraArgs) -> deployMobile platform extraArgs
355− ObCommand_Run -> run
387+ ObCommand_Run interpretPathsList -> withInterpretPaths interpretPathsList run
356388 ObCommand_Profile basePath rtsFlags -> profile basePath rtsFlags
357389 ObCommand_Thunk to -> case _thunkOption_command to of
358390 ThunkCommand_Update config -> for_ thunks (updateThunkToLatest config)
@@ -361,17 +393,27 @@ ob = \case
361393 ThunkCommand_Init -> for_ thunks initThunk
362394 where
363395 thunks = _thunkOption_thunks to
364− ObCommand_Repl -> runRepl
365− ObCommand_Watch -> runWatch
366− ObCommand_Shell so -> withProjectRoot "." $ \root ->
367− projectShell root False (_shellOpts_shell so) (_shellOpts_command so)
368− ObCommand_Doc shell' pkgs -> withProjectRoot "." $ \root ->
369− projectShell root False shell' (Just $ haddockCommand pkgs)
396+ ObCommand_Repl interpretPathsList -> withInterpretPaths interpretPathsList runRepl
397+ ObCommand_Watch interpretPathsList -> withInterpretPaths interpretPathsList runWatch
398+ ObCommand_Shell (ShellOpts shellAttr interpretPathsList cmd) -> withInterpretPaths interpretPathsList $ \root interpretPaths ->
399+ nixShellForInterpretPaths False shellAttr root interpretPaths cmd
400+ ObCommand_Doc shellAttr pkgs -> withProjectRoot "." $ \root ->
401+ nixShellForInterpretPaths True shellAttr root emptyPathTree $ Just $ haddockCommand pkgs
370402 ObCommand_Hoogle shell' port -> withProjectRoot "." $ \root -> do
371403 nixShellWithHoogle root True shell' $ Just $ "hoogle server -p " <> show port <> " --local"
372404 ObCommand_Internal icmd -> case icmd of
373405 ObInternal_ApplyPackages origPath inPath outPath packagePaths -> do
374406 liftIO $ Preprocessor.applyPackages origPath inPath outPath packagePaths
407+ ObInternal_ExportGhciConfig interpretPathsList -> liftIO . putStrLn . unlines =<< withInterpretPaths interpretPathsList exportGhciConfig
408+
409+-- | A helper for the common case that the command you want to run needs the project root and a resolved
410+-- set of interpret paths.
411+withInterpretPaths :: MonadObelisk m => [(FilePath, Interpret)] -> (FilePath -> PathTree Interpret -> m a) -> m a
412+withInterpretPaths interpretPathsList f = withProjectRoot "." $ \root -> do
413+ interpretPaths' <- resolveInterpretPaths $ (root, Interpret_Interpret) : interpretPathsList
414+ case interpretPaths' of
415+ Nothing -> failWith "No paths provided for finding packages"
416+ Just interpretPaths -> f root interpretPaths
375417
376418haddockCommand :: [String] -> String
377419haddockCommand pkgs = unwords
@@ -385,3 +427,22 @@ haddockCommand pkgs = unwords
385427
386428getArgsConfig :: IO ArgsConfig
387429getArgsConfig = pure $ ArgsConfig { _argsConfig_enableVmBuilderByDefault = System.Info.os == "darwin" }
430+
431+-- | Resolves an ordered list of paths for use with @--interpret@/@--no-interpret@ by coalescing
432+-- paths into a non-ambiguous set of paths. Ambiguity is resolved by choosing right-most paths
433+-- over any preceeding identical paths.
434+--
435+-- For example: @a/b=ON a/b/c=OFF@ and @a/b/c=OFF a/b=ON@ are the same.
436+-- @a/b=ON a/b=OFF@ is reduced to @a/b=OFF@. We prefer right-biased choice to increase
437+-- scriptability.
438+--
439+-- N.B. All the paths in the result will be canonicalized. It's impossible to determine path
440+-- overlap otherwise.
441+resolveInterpretPaths :: MonadIO m => [(FilePath, a)] -> m (Maybe (PathTree a))
442+resolveInterpretPaths ps = do
443+ trees <- liftIO $ for ps $ \(p, a) -> pathToTree a <$> canonicalizePath p
444+ pure $ foldr1 mergeTrees <$> nonEmpty trees
445+ where
446+ -- | Merge two 'PathTree's preferring leaves on the right in as much as they overlap with paths on the left.
447+ mergeTrees :: PathTree a -> PathTree a -> PathTree a
448+ mergeTrees (PathTree_Node ax x) (PathTree_Node ay y) = PathTree_Node (ay <|> ax) $ Map.unionWith mergeTrees x y
lib/command/src/Obelisk/Command/Project.hs+5−13
@@ -12,7 +12,6 @@ module Obelisk.Command.Project
1212 , nixShellWithHoogle
1313 , nixShellWithPkgs
1414 , obeliskDirName
15− , projectShell
1615 , toImplDir
1716 , toObeliskDir
1817 , withProjectRoot
@@ -264,19 +263,20 @@ nixShellRunConfig root isPure command = do
264263nixShellRunProc :: NixShellConfig -> ProcessSpec
265264nixShellRunProc cfg = setDelegateCtlc True $ proc "nix-shell" $ runNixShellConfig cfg
266265
267−nixShellWithPkgs :: MonadObelisk m => FilePath -> Bool -> Bool -> Map Text FilePath -> Maybe String -> m ()
268−nixShellWithPkgs root isPure chdirToRoot packageNamesAndPaths command = do
266+nixShellWithPkgs :: MonadObelisk m => FilePath -> Bool -> Bool -> Map Text FilePath -> String -> Maybe String -> m ()
267+nixShellWithPkgs root isPure chdirToRoot packageNamesAndPaths shellAttr command = do
269268 packageNamesAndAbsPaths <- liftIO $ for packageNamesAndPaths makeAbsolute
270269 defShellConfig <- nixShellRunConfig root isPure command
271270 let setCwd_ = if chdirToRoot then setCwd (Just root) else id
272271 (_, _, _, ph) <- createProcess_ "nixShellWithPkgs" $ setCwd_ $ nixShellRunProc $ defShellConfig
273272 & nixShellConfig_common . nixCmdConfig_target . target_expr ?~
274− "{root, pkgs}: ((import root {}).passthru.__unstable__.self.extend (_: _: {\
273+ "{root, pkgs, shell}: ((import root {}).passthru.__unstable__.self.extend (_: _: {\
275274 \shellPackages = builtins.fromJSON pkgs;\
276− \})).project.shells.ghc"
275+ \})).project.shells.${shell}"
277276 & nixShellConfig_common . nixCmdConfig_args .~
278277 [ rawArg "root" $ toNixPath $ if chdirToRoot then "." else root
279278 , strArg "pkgs" (T.unpack $ decodeUtf8 $ BSL.toStrict $ Json.encode packageNamesAndAbsPaths)
279+ , strArg "shell" shellAttr
280280 ]
281281 void $ waitForProcess ph
282282
@@ -291,14 +291,6 @@ nixShellWithHoogle root isPure shell' command = do
291291 & nixShellConfig_common . nixCmdConfig_args .~ [ strArg "shell" shell' ]
292292 void $ waitForProcess ph
293293
294−projectShell :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
295−projectShell root isPure shellName command = do
296− defShellConfig <- nixShellRunConfig root isPure command
297− (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCwd (Just root) $ nixShellRunProc $ defShellConfig
298− & nixShellConfig_common . nixCmdConfig_target . target_path ?~ "default.nix"
299− & nixShellConfig_common . nixCmdConfig_target . target_attr ?~ ("shells." <> shellName)
300− void $ waitForProcess ph
301−
302294findProjectAssets :: MonadObelisk m => FilePath -> m Text
303295findProjectAssets root = do
304296 isDerivation <- readProcessAndLogStderr Debug $ setCwd (Just root) $
lib/command/src/Obelisk/Command/Run.hs+154−43
@@ -1,9 +1,14 @@
1+{-# LANGUAGE DeriveFoldable #-}
2+{-# LANGUAGE DeriveFunctor #-}
3+{-# LANGUAGE DeriveTraversable #-}
14{-# LANGUAGE FlexibleContexts #-}
25{-# LANGUAGE LambdaCase #-}
36{-# LANGUAGE MultiParamTypeClasses #-}
47{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE QuasiQuotes #-}
59{-# LANGUAGE ScopedTypeVariables #-}
610{-# LANGUAGE TupleSections #-}
11+{-# LANGUAGE ViewPatterns #-}
712module Obelisk.Command.Run where
813
914import Control.Arrow ((&&&))
@@ -15,13 +20,18 @@ import Control.Monad.IO.Class (liftIO)
1520import Control.Monad.Reader (MonadIO)
1621import Data.Coerce (coerce)
1722import Data.Default (def)
18−import Data.Either
19−import Data.Foldable (for_, toList)
23+import Data.Either (partitionEithers)
24+import Data.Foldable (fold, for_, toList)
25+import Data.Functor.Identity (runIdentity)
26+import Data.List.NonEmpty (NonEmpty)
2027import qualified Data.List.NonEmpty as NE
2128import Data.Map (Map)
2229import qualified Data.Map as Map
30+import qualified Data.Map.Monoidal as MMap
2331import Data.Maybe
32+import Data.Set (Set)
2433import qualified Data.Set as Set
34+import Data.String.Here.Interpolated (i)
2535import Data.Text (Text)
2636import qualified Data.Text as T
2737import Data.Time.Clock (getCurrentTime)
@@ -83,6 +93,34 @@ data CabalPackageInfo = CabalPackageInfo
8393 -- ^ List of compiler-specific options (e.g., the "ghc-options" field of the cabal file)
8494 }
8595
96+-- | 'Bool' with a better name for it's purpose.
97+data Interpret = Interpret_Interpret | Interpret_NoInterpret deriving (Eq, Ord, Show)
98+
99+textInterpret :: Interpret -> Text
100+textInterpret = \case
101+ Interpret_Interpret -> "Interpret"
102+ Interpret_NoInterpret -> "NoInterpret"
103+
104+-- | Describe a set of 'FilePath's as a tree to facilitate merging them in a convenient way.
105+data PathTree a = PathTree_Node
106+ (Maybe a) -- An optional leaf at this point in the tree
107+ (Map FilePath (PathTree a)) -- Branches to deeper leaves
108+ deriving (Eq, Ord, Show, Functor, Foldable, Traversable)
109+
110+emptyPathTree :: PathTree a
111+emptyPathTree = PathTree_Node Nothing mempty
112+
113+-- | 2D ASCII drawing of a 'PathTree'. Adapted from Data.Tree.draw.
114+drawPathTree :: (a -> Text) -> PathTree a -> Text
115+drawPathTree showA (PathTree_Node _ ts0) = T.intercalate "\n" $ goForest (Map.toList ts0)
116+ where
117+ annotated ma = maybe id (\a b -> b <> " [" <> showA a <> "]") ma . T.pack
118+ goTree (fp, PathTree_Node ma forest) = annotated ma fp : goForest (Map.toList forest)
119+ goForest [] = []
120+ goForest [tree] = shift "└─ " " " (goTree tree)
121+ goForest (tree:forest) = shift "├─ " "│ " (goTree tree) <> goForest forest
122+ shift first other = zipWith (<>) (first : repeat other)
123+
86124-- | Used to signal to obelisk that it's being invoked as a preprocessor
87125preprocessorIdentifier :: String
88126preprocessorIdentifier = "__preprocessor-apply-packages"
@@ -121,11 +159,9 @@ profile profileBasePattern rtsFlags = withProjectRoot "." $ \root -> do
121159 <> [ "-RTS" ]
122160 void $ waitForProcess ph
123161
124−run
125− :: MonadObelisk m
126− => m ()
127−run = withProjectRoot "." $ \root -> do
128− pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
162+run :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
163+run root interpretPaths = do
164+ pkgs <- getParsedLocalPkgs root interpretPaths
129165 withGhciScript pkgs root $ \dotGhciPath -> do
130166 freePort <- getFreePort
131167 assets <- findProjectAssets root
@@ -138,39 +174,85 @@ run = withProjectRoot "." $ \root -> do
138174 , "Frontend.frontend"
139175 ]
140176
141−runRepl :: MonadObelisk m => m ()
142−runRepl = withProjectRoot "." $ \root -> do
143− pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
177+runRepl :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
178+runRepl root interpretPaths = do
179+ pkgs <- getParsedLocalPkgs root interpretPaths
144180 withGhciScript pkgs "." $ \dotGhciPath ->
145181 runGhciRepl root pkgs dotGhciPath
146182
147−runWatch :: MonadObelisk m => m ()
148−runWatch = withProjectRoot "." $ \root -> do
149− pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
183+runWatch :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
184+runWatch root interpretPaths = do
185+ pkgs <- getParsedLocalPkgs root interpretPaths
150186 withGhciScript pkgs root $ \dotGhciPath ->
151187 runGhcid root True dotGhciPath pkgs Nothing
152188
189+exportGhciConfig :: MonadObelisk m => FilePath -> PathTree Interpret -> m [String]
190+exportGhciConfig root interpretPaths = do
191+ pkgs <- getParsedLocalPkgs root interpretPaths
192+ getGhciSessionSettings pkgs "."
193+
194+nixShellForInterpretPaths :: MonadObelisk m => Bool -> String -> FilePath -> PathTree Interpret -> Maybe String -> m ()
195+nixShellForInterpretPaths isPure shell root interpretPaths cmd = do
196+ pkgs <- getParsedLocalPkgs root interpretPaths
197+ nixShellWithPkgs root isPure False (packageInfoToNamePathMap pkgs) shell cmd
198+
199+-- | Like 'getLocalPkgs' but also parses them and fails if any of them can't be parsed.
200+getParsedLocalPkgs :: MonadObelisk m => FilePath -> PathTree Interpret -> m (NonEmpty CabalPackageInfo)
201+getParsedLocalPkgs root interpretPaths = parsePackagesOrFail =<< getLocalPkgs root interpretPaths
202+
153203-- | Relative paths to local packages of an obelisk project.
154204--
155205-- These are a combination of the obelisk predefined local packages,
156206-- and any packages that the user has set with the @packages@ argument
157207-- to the Nix @project@ function.
158−getLocalPkgs :: MonadObelisk m => FilePath -> m [FilePath]
159−getLocalPkgs root = do
160− obeliskPaths <- runFind ["-L", root, "-name", ".obelisk", "-type", "d"]
208+getLocalPkgs :: forall m. MonadObelisk m => FilePath -> PathTree Interpret -> m (Set FilePath)
209+getLocalPkgs root interpretPaths = do
210+ putLog Debug $ [i|Finding packages with root ${root} and interpret paths:|] <> "\n" <> drawPathTree textInterpret interpretPaths
211+ obeliskPackagePaths <- runFind ["-L", root, "-name", ".obelisk", "-type", "d"]
161212
162213 -- We do not want to find packages that are embedded inside other obelisk projects, unless that
163214 -- obelisk project is our own.
164− let exclusions = filter (/= root) $ map takeDirectory obeliskPaths
165− fmap (map (makeRelative ".")) $ runFind $
166− ["-L", root, "(", "-name", "*.cabal", "-o", "-name", Hpack.packageConfig, ")", "-a", "-type", "f"]
167− <> concat [["-not", "-path", p </> "*"] | p <- ("*" </> attrCacheFileName) : exclusions]
215+ obeliskPackageExclusions <- liftIO $ fmap Set.fromList $ traverse canonicalizePath $
216+ filter (/= root) $ map takeDirectory obeliskPackagePaths
217+ putLog Debug [i|Excluding obelisk packages: ${T.pack $ unwords $ Set.toList obeliskPackageExclusions}|]
218+ let rootsAndExclusions = calcIntepretFinds "" interpretPaths
219+
220+ fmap fold $ for (MMap.toAscList rootsAndExclusions) $ \(interpretPathRoot, exclusions) ->
221+ let allExclusions = obeliskPackageExclusions <> exclusions <> Set.singleton ("*" </> attrCacheFileName)
222+ in fmap (Set.fromList . map normalise) $ runFind $
223+ ["-L", interpretPathRoot, "(", "-name", "*.cabal", "-o", "-name", Hpack.packageConfig, ")", "-a", "-type", "f"]
224+ <> concat [["-not", "-path", p </> "*"] | p <- toList allExclusions]
168225 where
169226 runFind args = do
170227 (_exitCode, out, err) <- readCreateProcessWithExitCode $ proc findExePath args
171228 putLog Debug $ T.strip $ T.pack err
172229 pure $ map T.unpack $ T.lines $ T.strip $ T.pack out
173230
231+-- | Calculates a set of root 'FilePath's along with each one's corresponding set of exclusions.
232+-- This is used when constructing a set of @find@ commands to run to produce a set of packages
233+-- that matches the user's @--interpret@/@--no-interpret@ settings.
234+calcIntepretFinds :: FilePath -> PathTree Interpret -> MMap.MonoidalMap FilePath (Set FilePath)
235+calcIntepretFinds treeRoot0 tree0 = runIdentity $ go treeRoot0 tree0
236+ where
237+ go treeRoot tree = foldPathTreeFor (== Interpret_Interpret) treeRoot tree $ \parent children -> do
238+ exclusions <- foldPathTreeFor (== Interpret_NoInterpret) parent children $ \parent' children' ->
239+ pure $ Map.singleton parent' children'
240+ deeperFinds <- Map.traverseWithKey go exclusions
241+ pure $ MMap.singleton parent (Map.keysSet exclusions) <> fold (MMap.MonoidalMap deeperFinds)
242+
243+-- | Traverses a 'PathTree' and folds all leaves matching a given predicate.
244+foldPathTreeFor
245+ :: forall m a b. (Applicative m, Monoid b)
246+ => (a -> Bool)
247+ -> FilePath
248+ -> PathTree a
249+ -> (FilePath -> PathTree a -> m b)
250+ -> m b
251+foldPathTreeFor predicate parent children f = case children of
252+ PathTree_Node (Just x) children' | predicate x -> f parent (PathTree_Node Nothing children')
253+ PathTree_Node _ children' -> fmap fold $ flip Map.traverseWithKey children' $ \k children'' ->
254+ foldPathTreeFor predicate (parent </> k) children'' f
255+
174256data GuessPackageFileError = GuessPackageFileError_Ambiguous [FilePath] | GuessPackageFileError_NotFound
175257 deriving (Eq, Ord, Show)
176258instance Exception GuessPackageFileError
@@ -271,8 +353,8 @@ parseCabalPackage' pkg = runExceptT $ do
271353 Left (_, errors) ->
272354 throwError $ T.pack $ "Failed to parse " <> packageFile <> ":\n" <> unlines (map show errors)
273355
274−parsePackagesOrFail :: MonadObelisk m => [FilePath] -> m (NE.NonEmpty CabalPackageInfo)
275−parsePackagesOrFail dirs = do
356+parsePackagesOrFail :: (MonadObelisk m, Foldable f) => f FilePath -> m (NE.NonEmpty CabalPackageInfo)
357+parsePackagesOrFail dirs' = do
276358 (pkgDirErrs, packageInfos') <- fmap partitionEithers $ for dirs $ \dir -> do
277359 flip fmap (parseCabalPackage dir) $ \case
278360 Just packageInfo
@@ -295,26 +377,29 @@ parsePackagesOrFail dirs = do
295377 pure p
296378
297379 packageInfos <- case NE.nonEmpty $ toList unambiguous of
298− Nothing -> failWith $ T.pack $ "No valid, buildable packages found in " <> intercalate ", " dirs
380+ Nothing -> failWith $ T.pack $
381+ "No valid, buildable packages found" <> (if null dirs then "" else " in " <> intercalate ", " dirs)
299382 Just xs -> pure xs
300383
301384 unless (null pkgDirErrs) $
302385 putLog Warning $ T.pack $ "Failed to find buildable packages in " <> intercalate ", " pkgDirErrs
303386
304387 pure packageInfos
388+ where
389+ dirs = toList dirs'
305390
306−packageInfoToNamePathMap :: [CabalPackageInfo] -> Map Text FilePath
307−packageInfoToNamePathMap = Map.fromList . map (_cabalPackageInfo_packageName &&& _cabalPackageInfo_packageRoot)
391+packageInfoToNamePathMap :: Foldable f => f CabalPackageInfo -> Map Text FilePath
392+packageInfoToNamePathMap = Map.fromList . map (_cabalPackageInfo_packageName &&& _cabalPackageInfo_packageRoot) . toList
308393
309394-- | Create ghci configuration to load the given packages
310395withGhciScript
311− :: MonadObelisk m
312− => [CabalPackageInfo] -- ^ List of packages to load into ghci
396+ :: (MonadObelisk m, Foldable f)
397+ => f CabalPackageInfo -- ^ List of packages to load into ghci
313398 -> FilePath -- ^ All paths written to the .ghci file will be relative to this path
314399 -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporary .ghci
315400 -> m ()
316−withGhciScript packageInfos pathBase f = do
317− selfExe <- liftIO getExecutablePath
401+withGhciScript (toList -> packageInfos) pathBase f = do
402+ ghciSettings <- getGhciSessionSettings packageInfos pathBase
318403 let
319404 packageNames = Set.fromList $ map _cabalPackageInfo_packageName packageInfos
320405 modulesToLoad = mconcat
@@ -323,9 +408,7 @@ withGhciScript packageInfos pathBase f = do
323408 , [ "Frontend" | "frontend" `Set.member` packageNames ]
324409 ]
325410 dotGhci = unlines
326− -- TODO: Shell escape
327− [ ":set -F -pgmF " <> selfExe <> " -optF " <> preprocessorIdentifier <> " " <> unwords (map (("-optF " <>) . makeRelative pathBase . _cabalPackageInfo_packageFile) packageInfos)
328− , ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
411+ [ ":set " <> unwords ghciSettings -- TODO: Shell escape
329412 , if null modulesToLoad then "" else ":load " <> unwords modulesToLoad
330413 , "import qualified Obelisk.Run"
331414 , "import qualified Frontend"
@@ -335,33 +418,53 @@ withGhciScript packageInfos pathBase f = do
335418 liftIO $ writeFile dotGhciPath dotGhci
336419 f dotGhciPath
337420
338− where
339− rootedSourceDirs pkg = NE.toList $
340− makeRelative pathBase . (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
421+-- | Builds a list of options to pass to ghci or set in .ghci file that configures
422+-- the preprocessor and source includes.
423+getGhciSessionSettings
424+ :: (MonadObelisk m, Foldable f)
425+ => f CabalPackageInfo -- ^ List of packages to load into ghci
426+ -> FilePath -- ^ All paths written to the .ghci file will be relative to this path
427+ -> m [String]
428+getGhciSessionSettings (toList -> packageInfos) pathBase = do
429+ -- N.B. ghci settings do NOT support escaping in any way. To minimize the likelihood that
430+ -- paths-with-spaces ruin our day, we first canonicalize everything, and then relativize
431+ -- all paths to 'pathBase'.
432+ selfExe <- liftIO $ canonicalizePath =<< getExecutablePath
433+ canonicalPathBase <- liftIO $ canonicalizePath pathBase
434+
435+ (pkgFiles, pkgSrcPaths :: [NonEmpty FilePath]) <- fmap unzip $ liftIO $ for packageInfos $ \pkg -> do
436+ canonicalSrcDirs <- traverse canonicalizePath $ (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
437+ canonicalPkgFile <- canonicalizePath $ _cabalPackageInfo_packageFile pkg
438+ pure (makeRelative canonicalPathBase canonicalPkgFile, makeRelative canonicalPathBase <$> canonicalSrcDirs)
439+
440+ pure
441+ $ ["-F", "-pgmF", selfExe, "-optF", preprocessorIdentifier]
442+ <> concatMap (\p -> ["-optF", p]) pkgFiles
443+ <> [ "-i" <> intercalate ":" (concatMap toList pkgSrcPaths) ]
341444
342445-- | Run ghci repl
343446runGhciRepl
344− :: MonadObelisk m
447+ :: (MonadObelisk m, Foldable f)
345448 => FilePath -- ^ Path to project root
346− -> [CabalPackageInfo]
449+ -> f CabalPackageInfo
347450 -> FilePath -- ^ Path to .ghci
348451 -> m ()
349−runGhciRepl root packages dotGhci =
452+runGhciRepl root (toList -> packages) dotGhci =
350453 -- NOTE: We do *not* want to use $(staticWhich "ghci") here because we need the
351454 -- ghc that is provided by the shell in the user's project.
352− nixShellWithPkgs root True False (packageInfoToNamePathMap packages) $ Just $ "ghci " <> makeBaseGhciOptions dotGhci -- TODO: Shell escape
455+ nixShellWithPkgs root True False (packageInfoToNamePathMap packages) "ghc" $ Just $ "ghci " <> makeBaseGhciOptions dotGhci -- TODO: Shell escape
353456
354457-- | Run ghcid
355458runGhcid
356− :: MonadObelisk m
459+ :: (MonadObelisk m, Foldable f)
357460 => FilePath -- ^ Path to project root
358461 -> Bool -- ^ Should we chdir to root when running this process?
359462 -> FilePath -- ^ Path to .ghci
360− -> [CabalPackageInfo]
463+ -> f CabalPackageInfo
361464 -> Maybe String -- ^ Optional command to run at every reload
362465 -> m ()
363−runGhcid root chdirToRoot dotGhci packages mcmd =
364− nixShellWithPkgs root True chdirToRoot (packageInfoToNamePathMap packages) (Just $ unwords $ ghcidExePath : opts) -- TODO: Shell escape
466+runGhcid root chdirToRoot dotGhci (toList -> packages) mcmd =
467+ nixShellWithPkgs root True chdirToRoot (packageInfoToNamePathMap packages) "ghc" $ Just $ unwords $ ghcidExePath : opts -- TODO: Shell escape
365468 where
366469 opts =
367470 [ "-W"
@@ -393,3 +496,11 @@ getFreePort = liftIO $ withSocketsDo $ do
393496 sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
394497 bind sock (addrAddress addr)
395498 return sock
499+
500+
501+-- | Convert a 'FilePath' into a 'PathTree'.
502+pathToTree :: a -> FilePath -> PathTree a
503+pathToTree a p = go $ splitDirectories p
504+ where
505+ go [] = PathTree_Node (Just a) mempty
506+ go (x : xs) = PathTree_Node Nothing $ Map.singleton x $ go xs