@@ -14,7 +14,7 @@ module Obelisk.Command.Run where
14import Control.Arrow ((&&&))
14import Control.Arrow ((&&&))
15import Control.Exception (Exception, bracket)
15import Control.Exception (Exception, bracket)
16import Control.Lens (ifor, (.~), (&))
16import Control.Lens (ifor, (.~), (&))
17−import Control.Monad (filterM, unless, void)
17+import Control.Monad (filterM)
18import Control.Monad.Except (runExceptT, throwError)
18import Control.Monad.Except (runExceptT, throwError)
19import Control.Monad.IO.Class (liftIO)
19import Control.Monad.IO.Class (liftIO)
20import Control.Monad.Reader (MonadIO)
20import Control.Monad.Reader (MonadIO)
@@ -31,7 +31,7 @@ import qualified Data.List.NonEmpty as NE
31import Data.Map (Map)
31import Data.Map (Map)
32import qualified Data.Map as Map
32import qualified Data.Map as Map
33import qualified Data.Map.Monoidal as MMap
33import qualified Data.Map.Monoidal as MMap
34−import Data.Maybe (catMaybes, fromMaybe, mapMaybe, maybeToList)
34+import Data.Maybe (catMaybes, fromMaybe, mapMaybe)
35import Data.Ord (comparing)
35import Data.Ord (comparing)
36import Data.Set (Set)
36import Data.Set (Set)
37import qualified Data.Set as Set
37import qualified Data.Set as Set
@@ -46,7 +46,7 @@ import Distribution.Compiler (CompilerFlavor(..))
46import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
46import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
47import Distribution.Parsec.ParseResult (runParseResult)
47import Distribution.Parsec.ParseResult (runParseResult)
48import qualified Distribution.System as Dist
48import qualified Distribution.System as Dist
49−import Distribution.Types.BuildInfo (buildable, defaultExtensions, defaultLanguage, hsSourceDirs, options)
49+import Distribution.Types.BuildInfo (buildable, cppOptions, defaultExtensions, defaultLanguage, hsSourceDirs, options)
50import Distribution.Types.CondTree (simplifyCondTree)
50import Distribution.Types.CondTree (simplifyCondTree)
51import Distribution.Types.GenericPackageDescription (ConfVar (Arch, Impl, OS), condLibrary)
51import Distribution.Types.GenericPackageDescription (ConfVar (Arch, Impl, OS), condLibrary)
52import Distribution.Types.Library (libBuildInfo)
52import Distribution.Types.Library (libBuildInfo)
@@ -66,19 +66,18 @@ import System.IO.Temp (withSystemTempDirectory)
66import Obelisk.App (MonadObelisk)
66import Obelisk.App (MonadObelisk)
67import Obelisk.CliApp (
67import Obelisk.CliApp (
68 Severity (..),
68 Severity (..),
73 readCreateProcessWithExitCode,
72 readCreateProcessWithExitCode,
74 readProcessAndLogStderr,
73 readProcessAndLogStderr,
76 setDelegateCtlc,
76 setDelegateCtlc,
78 withSpinner,
77 withSpinner,
80import Obelisk.Command.Nix
79import Obelisk.Command.Nix
81−import Obelisk.Command.Project (nixShellWithoutPkgs, withProjectRoot, findProjectAssets)
80+import Obelisk.Command.Project (nixShellWithoutPkgs, withProjectRoot, findProjectAssets, bashEscape)
82import Obelisk.Command.Thunk (attrCacheFileName)
81import Obelisk.Command.Thunk (attrCacheFileName)
83import Obelisk.Command.Utils (findExePath, ghcidExePath)
82import Obelisk.Command.Utils (findExePath, ghcidExePath)
@@ -95,9 +94,11 @@ data CabalPackageInfo = CabalPackageInfo
95 -- ^ List of globally set languages of the library component
94 -- ^ List of globally set languages of the library component
96 , _cabalPackageInfo_compilerOptions :: [(CompilerFlavor, [String])]
95 , _cabalPackageInfo_compilerOptions :: [(CompilerFlavor, [String])]
97 -- ^ List of compiler-specific options (e.g., the "ghc-options" field of the cabal file)
96 -- ^ List of compiler-specific options (e.g., the "ghc-options" field of the cabal file)
97+ , _cabalPackageInfo_cppOptions :: [String]
98+ -- ^ List of CPP (C Preprocessor) options (e.g. the "cpp-options" field of the cabal file)
100−-- | 'Bool' with a better name for it's purpose.
101+-- | 'Bool' with a better name for its purpose.
101data Interpret = Interpret_Interpret | Interpret_NoInterpret deriving (Eq, Ord, Show)
102data Interpret = Interpret_Interpret | Interpret_NoInterpret deriving (Eq, Ord, Show)
103textInterpret :: Interpret -> Text
104textInterpret :: Interpret -> Text
@@ -111,9 +112,6 @@ data PathTree a = PathTree_Node
111 (Map FilePath (PathTree a)) -- Branches to deeper leaves
112 (Map FilePath (PathTree a)) -- Branches to deeper leaves
112 deriving (Eq, Ord, Show, Functor, Foldable, Traversable)
113 deriving (Eq, Ord, Show, Functor, Foldable, Traversable)
114−emptyPathTree :: PathTree a
115−emptyPathTree = PathTree_Node Nothing mempty
117-- | 2D ASCII drawing of a 'PathTree'. Adapted from Data.Tree.draw.
115-- | 2D ASCII drawing of a 'PathTree'. Adapted from Data.Tree.draw.
118drawPathTree :: (a -> Text) -> PathTree a -> Text
116drawPathTree :: (a -> Text) -> PathTree a -> Text
119drawPathTree showA (PathTree_Node _ ts0) = T.intercalate "\n" $ goForest (Map.toList ts0)
117drawPathTree showA (PathTree_Node _ ts0) = T.intercalate "\n" $ goForest (Map.toList ts0)
@@ -153,7 +151,7 @@ profile profileBasePattern rtsFlags = withProjectRoot "." $ \root -> do
153 liftIO $ createDirectoryIfMissing True $ takeDirectory $ root </> profileBaseName
151 liftIO $ createDirectoryIfMissing True $ takeDirectory $ root </> profileBaseName
154 putLog Debug $ "Storing profiled data under base name of " <> T.pack (root </> profileBaseName)
152 putLog Debug $ "Storing profiled data under base name of " <> T.pack (root </> profileBaseName)
155 freePort <- getFreePort
153 freePort <- getFreePort
156− (_, _, _, ph) <- createProcess_ "runProfExe" $ setCwd (Just root) $ setDelegateCtlc True $ proc (outPath </> "bin" </> "ob-run") $
154+ runProcess_ $ setCwd (Just root) $ setDelegateCtlc True $ proc (outPath </> "bin" </> "ob-run") $
157 [ show freePort
155 [ show freePort
158 , T.unpack assets
156 , T.unpack assets
159 , profileBaseName
157 , profileBaseName
@@ -161,17 +159,16 @@ profile profileBasePattern rtsFlags = withProjectRoot "." $ \root -> do
161 , "-po" <> profileBaseName
159 , "-po" <> profileBaseName
162 ] <> rtsFlags
160 ] <> rtsFlags
163 <> [ "-RTS" ]
161 <> [ "-RTS" ]
164− void $ waitForProcess ph
166run :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
163run :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
167run root interpretPaths = do
164run root interpretPaths = do
168 pkgs <- getParsedLocalPkgs root interpretPaths
165 pkgs <- getParsedLocalPkgs root interpretPaths
169− withGhciScript pkgs $ \dotGhciPath -> do
170− assets <- findProjectAssets root
166+ assets <- findProjectAssets root
171− putLog Debug $ "Assets impurely loaded from: " <> assets
167+ putLog Debug $ "Assets impurely loaded from: " <> assets
172− ghciArgs <- getGhciSessionSettings pkgs root True
168+ ghciArgs <- getGhciSessionSettings pkgs root True
173− freePort <- getFreePort
169+ freePort <- getFreePort
170+ withGhciScriptArgs pkgs $ \dotGhciArgs -> do
174− runGhcid root True (ghciArgs <> mkGhciScriptArg dotGhciPath) pkgs $ Just $ unwords
171+ runGhcid root True (ghciArgs <> dotGhciArgs) pkgs $ Just $ unwords
175 [ "Obelisk.Run.run"
172 [ "Obelisk.Run.run"
176 , show freePort
173 , show freePort
177 , "(Obelisk.Run.runServeAsset " ++ show assets ++ ")"
174 , "(Obelisk.Run.runServeAsset " ++ show assets ++ ")"
@@ -183,15 +180,15 @@ runRepl :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
183runRepl root interpretPaths = do
180runRepl root interpretPaths = do
184 pkgs <- getParsedLocalPkgs root interpretPaths
181 pkgs <- getParsedLocalPkgs root interpretPaths
185 ghciArgs <- getGhciSessionSettings pkgs "." True
182 ghciArgs <- getGhciSessionSettings pkgs "." True
186− withGhciScript pkgs $ \dotGhciPath ->
183+ withGhciScriptArgs pkgs $ \dotGhciArgs ->
187− runGhciRepl root pkgs (ghciArgs <> mkGhciScriptArg dotGhciPath)
184+ runGhciRepl root pkgs (ghciArgs <> dotGhciArgs)
189runWatch :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
186runWatch :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
190runWatch root interpretPaths = do
187runWatch root interpretPaths = do
191 pkgs <- getParsedLocalPkgs root interpretPaths
188 pkgs <- getParsedLocalPkgs root interpretPaths
192 ghciArgs <- getGhciSessionSettings pkgs root True
189 ghciArgs <- getGhciSessionSettings pkgs root True
193− withGhciScript pkgs $ \dotGhciPath ->
190+ withGhciScriptArgs pkgs $ \dotGhciArgs ->
194− runGhcid root True (ghciArgs <> mkGhciScriptArg dotGhciPath) pkgs Nothing
191+ runGhcid root True (ghciArgs <> dotGhciArgs) pkgs Nothing
196exportGhciConfig :: MonadObelisk m => Bool -> FilePath -> PathTree Interpret -> m [String]
193exportGhciConfig :: MonadObelisk m => Bool -> FilePath -> PathTree Interpret -> m [String]
197exportGhciConfig useRelativePaths root interpretPaths = do
194exportGhciConfig useRelativePaths root interpretPaths = do
@@ -307,15 +304,16 @@ parseCabalPackage
307 -> m (Maybe CabalPackageInfo)
304 -> m (Maybe CabalPackageInfo)
308parseCabalPackage dir = parseCabalPackage' dir >>= \case
305parseCabalPackage dir = parseCabalPackage' dir >>= \case
309 Left err -> Nothing <$ putLog Error err
306 Left err -> Nothing <$ putLog Error err
310− Right (warnings, pkgInfo) -> do
307+ Right (Just (warnings, pkgInfo)) -> do
311 for_ warnings $ putLog Warning . T.pack . show
308 for_ warnings $ putLog Warning . T.pack . show
312 pure $ Just pkgInfo
309 pure $ Just pkgInfo
310+ Right Nothing -> pure Nothing
314-- | Like 'parseCabalPackage' but returns errors and warnings directly so as to avoid 'MonadObelisk'.
312-- | Like 'parseCabalPackage' but returns errors and warnings directly so as to avoid 'MonadObelisk'.
315parseCabalPackage'
313parseCabalPackage'
316 :: (MonadIO m)
314 :: (MonadIO m)
317 => FilePath -- ^ Package directory
315 => FilePath -- ^ Package directory
318− -> m (Either T.Text ([Dist.PWarning], CabalPackageInfo))
316+ -> m (Either T.Text (Maybe ([Dist.PWarning], CabalPackageInfo)))
319parseCabalPackage' pkg = runExceptT $ do
317parseCabalPackage' pkg = runExceptT $ do
320 (cabalContents, packageFile, packageName) <- guessCabalPackageFile pkg >>= \case
318 (cabalContents, packageFile, packageName) <- guessCabalPackageFile pkg >>= \case
321 Left GuessPackageFileError_NotFound -> throwError $ "No .cabal or package.yaml file found in " <> T.pack pkg
319 Left GuessPackageFileError_NotFound -> throwError $ "No .cabal or package.yaml file found in " <> T.pack pkg
@@ -343,7 +341,7 @@ parseCabalPackage' pkg = runExceptT $ do
343 case condLibrary <$> result of
341 case condLibrary <$> result of
344 Right (Just condLib) -> do
342 Right (Just condLib) -> do
345 let (_, lib) = simplifyCondTree evalConfVar condLib
343 let (_, lib) = simplifyCondTree evalConfVar condLib
346− pure $ (warnings,) $ CabalPackageInfo
344+ pure $ Just $ (warnings,) $ CabalPackageInfo
347 { _cabalPackageInfo_packageName = T.pack packageName
345 { _cabalPackageInfo_packageName = T.pack packageName
348 , _cabalPackageInfo_packageFile = packageFile
346 , _cabalPackageInfo_packageFile = packageFile
349 , _cabalPackageInfo_packageRoot = takeDirectory packageFile
347 , _cabalPackageInfo_packageRoot = takeDirectory packageFile
@@ -355,18 +353,19 @@ parseCabalPackage' pkg = runExceptT $ do
355 , _cabalPackageInfo_defaultLanguage =
353 , _cabalPackageInfo_defaultLanguage =
356 defaultLanguage $ libBuildInfo lib
354 defaultLanguage $ libBuildInfo lib
357 , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
355 , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
356+ , _cabalPackageInfo_cppOptions = cppOptions $ libBuildInfo lib
359− Right Nothing -> throwError "Haskell package has no library component"
358+ Right Nothing -> pure Nothing
360 Left (_, errors) ->
359 Left (_, errors) ->
361 throwError $ T.pack $ "Failed to parse " <> packageFile <> ":\n" <> unlines (map show errors)
360 throwError $ T.pack $ "Failed to parse " <> packageFile <> ":\n" <> unlines (map show errors)
363parsePackagesOrFail :: (MonadObelisk m, Foldable f) => f FilePath -> m (NE.NonEmpty CabalPackageInfo)
362parsePackagesOrFail :: (MonadObelisk m, Foldable f) => f FilePath -> m (NE.NonEmpty CabalPackageInfo)
364parsePackagesOrFail dirs' = do
363parsePackagesOrFail dirs' = do
365− (pkgDirErrs, packageInfos') <- fmap partitionEithers $ for dirs $ \dir -> do
364+ packageInfos' <- fmap catMaybes $ for dirs $ \dir -> do
366 flip fmap (parseCabalPackage dir) $ \case
365 flip fmap (parseCabalPackage dir) $ \case
367 Just packageInfo
366 Just packageInfo
368− | _cabalPackageInfo_buildable packageInfo -> Right packageInfo
367+ | _cabalPackageInfo_buildable packageInfo -> Just packageInfo
369− _ -> Left dir
368+ _ -> Nothing
371 -- Sort duplicate packages such that we prefer shorter paths, but fall back to alphabetical ordering.
370 -- Sort duplicate packages such that we prefer shorter paths, but fall back to alphabetical ordering.
372 let packagesByName = Map.map (NE.sortBy $ comparing $ \p -> let n = _cabalPackageInfo_packageFile p in (length n, n))
371 let packagesByName = Map.map (NE.sortBy $ comparing $ \p -> let n = _cabalPackageInfo_packageFile p in (length n, n))
@@ -388,9 +387,6 @@ parsePackagesOrFail dirs' = do
388 "No valid, buildable packages found" <> (if null dirs then "" else " in " <> intercalate ", " dirs)
387 "No valid, buildable packages found" <> (if null dirs then "" else " in " <> intercalate ", " dirs)
389 Just xs -> pure xs
388 Just xs -> pure xs
391− unless (null pkgDirErrs) $
392− putLog Warning $ T.pack $ "Failed to find buildable packages in " <> intercalate ", " pkgDirErrs
394 pure packageInfos
390 pure packageInfos
396 dirs = toList dirs'
392 dirs = toList dirs'
@@ -398,13 +394,29 @@ parsePackagesOrFail dirs' = do
398packageInfoToNamePathMap :: Foldable f => f CabalPackageInfo -> Map Text FilePath
394packageInfoToNamePathMap :: Foldable f => f CabalPackageInfo -> Map Text FilePath
399packageInfoToNamePathMap = Map.fromList . map (_cabalPackageInfo_packageName &&& _cabalPackageInfo_packageRoot) . toList
395packageInfoToNamePathMap = Map.fromList . map (_cabalPackageInfo_packageName &&& _cabalPackageInfo_packageRoot) . toList
397+-- Like 'withGhciScript' but provides the precise ghci arguments to add to a ghci session
399+ :: (MonadObelisk m, Foldable f)
400+ => f CabalPackageInfo -- ^ List of packages to load into ghci
401+ -> ([String] -> m ()) -- ^ Action to run with the extra ghci arguments
403+withGhciScriptArgs packageInfos f = withGhciScript loadPreludeManually packageInfos $ \fp -> f ["-XNoImplicitPrelude", "-ghci-script", fp]
405+ -- These lines must be first and allow the session to support a custom Prelude when @-XNoImplicitPrelude@
406+ -- is passed to the ghci session.
407+ loadPreludeManually =
408+ [ ":add Prelude" -- @:add@ is used because it's less noisy when there is no custom Prelude
409+ , ":set -XImplicitPrelude" -- Turn the default setting on
401-- | Create ghci configuration to load the given packages
412-- | Create ghci configuration to load the given packages
402withGhciScript
413withGhciScript
403 :: (MonadObelisk m, Foldable f)
414 :: (MonadObelisk m, Foldable f)
415+ => [String] -- ^ Commands to prefix to file
404− => f CabalPackageInfo -- ^ List of packages to load into ghci
416+ -> f CabalPackageInfo -- ^ List of packages to load into ghci
405 -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporary .ghci
417 -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporary .ghci
407−withGhciScript (toList -> packageInfos) f =
419+withGhciScript preCommands (toList -> packageInfos) f =
408 withSystemTempDirectory "ob-ghci" $ \fp -> do
420 withSystemTempDirectory "ob-ghci" $ \fp -> do
409 let dotGhciPath = fp </> ".ghci"
421 let dotGhciPath = fp </> ".ghci"
410 liftIO $ writeFile dotGhciPath dotGhci
422 liftIO $ writeFile dotGhciPath dotGhci
@@ -416,7 +428,8 @@ withGhciScript (toList -> packageInfos) f =
416 , [ "Backend" | "backend" `Set.member` packageNames ]
428 , [ "Backend" | "backend" `Set.member` packageNames ]
417 , [ "Frontend" | "frontend" `Set.member` packageNames ]
429 , [ "Frontend" | "frontend" `Set.member` packageNames ]
419− dotGhci = unlines
431+ dotGhci = unlines $
420 [ if null modulesToLoad then "" else ":load " <> unwords modulesToLoad
433 [ if null modulesToLoad then "" else ":load " <> unwords modulesToLoad
421 , "import qualified Obelisk.Run"
434 , "import qualified Obelisk.Run"
422 , "import qualified Frontend"
435 , "import qualified Frontend"
@@ -469,7 +482,7 @@ runGhciRepl root (toList -> packages) ghciArgs =
469 -- NOTE: We do *not* want to use $(staticWhich "ghci") here because we need the
482 -- NOTE: We do *not* want to use $(staticWhich "ghci") here because we need the
470 -- ghc that is provided by the shell in the user's project.
483 -- ghc that is provided by the shell in the user's project.
471 nixShellWithoutPkgs root True False (packageInfoToNamePathMap packages) "ghc" $
484 nixShellWithoutPkgs root True False (packageInfoToNamePathMap packages) "ghc" $
472− Just $ unwords $ "ghci" : ghciArgs -- TODO: Shell escape
485+ Just $ unwords $ fmap bashEscape $ "ghci" : ghciArgs
474-- | Run ghcid
487-- | Run ghcid
@@ -482,24 +495,20 @@ runGhcid
483runGhcid root chdirToRoot ghciArgs (toList -> packages) mcmd =
496runGhcid root chdirToRoot ghciArgs (toList -> packages) mcmd =
484 nixShellWithoutPkgs root True chdirToRoot (packageInfoToNamePathMap packages) "ghc" $
497 nixShellWithoutPkgs root True chdirToRoot (packageInfoToNamePathMap packages) "ghc" $
485− Just $ unwords $ ghcidExePath : opts -- TODO: Shell escape
498+ Just $ unwords $ fmap bashEscape $ ghcidExePath : opts
487− opts =
500+ opts = concat
489− , "--outputfile=ghcid-output.txt"
502+ , ["--outputfile=ghcid-output.txt"]
490− ] <> map (\x -> "--reload='" <> x <> "'") reloadFiles
503+ , map (\x -> "--reload=" <> x) reloadFiles
491− <> map (\x -> "--restart='" <> x <> "'") restartFiles
504+ , map (\x -> "--restart=" <> x) restartFiles
492− <> testCmd
505+ , maybe [] (\cmd -> ["--test=" <> cmd]) mcmd
493− <> ["--command='" <> unwords ("ghci" : ghciArgs) <> "'"] -- TODO: Shell escape
506+ , ["--command=" <> unwords ("ghci" : ghciArgs)]
494− testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'") -- TODO: Shell escape
507+ ]
496 adjustRoot x = if chdirToRoot then makeRelative root x else x
508 adjustRoot x = if chdirToRoot then makeRelative root x else x
497 reloadFiles = map adjustRoot [root </> "config"]
509 reloadFiles = map adjustRoot [root </> "config"]
498 restartFiles = map (adjustRoot . _cabalPackageInfo_packageFile) packages
510 restartFiles = map (adjustRoot . _cabalPackageInfo_packageFile) packages
500−mkGhciScriptArg :: FilePath -> [String]
501−mkGhciScriptArg dotGhci = ["-ghci-script", dotGhci]
503getFreePort :: MonadIO m => m Socket.PortNumber
512getFreePort :: MonadIO m => m Socket.PortNumber
504getFreePort = liftIO $ Socket.withSocketsDo $ do
513getFreePort = liftIO $ Socket.withSocketsDo $ do
505 addr:_ <- Socket.getAddrInfo (Just Socket.defaultHints) (Just "127.0.0.1") (Just "0")
514 addr:_ <- Socket.getAddrInfo (Just Socket.defaultHints) (Just "127.0.0.1") (Just "0")