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