1{-# LANGUAGE FlexibleContexts #-}
2{-# LANGUAGE LambdaCase #-}
3{-# LANGUAGE MultiParamTypeClasses #-}
4{-# LANGUAGE OverloadedStrings #-}
5{-# LANGUAGE ScopedTypeVariables #-}
6module Obelisk.Command.Run where
8import Control.Exception (bracket)
10import Control.Monad.IO.Class (liftIO)
11import Control.Monad.Reader (MonadIO)
13import qualified Data.List as L
14import qualified Data.List.NonEmpty as NE
16import qualified Data.Text as T
17import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
18import Distribution.Parsec.ParseResult (runParseResult)
19import Distribution.Pretty
20import Distribution.Types.BuildInfo
21import Distribution.Types.CondTree
22import Distribution.Types.GenericPackageDescription
23import Distribution.Types.Library
24import Distribution.Utils.Generic
28import Language.Haskell.Extension
29import Network.Socket hiding (Debug)
30import System.Directory
32import System.Process (proc)
33import System.IO.Temp (withSystemTempDirectory)
34import Data.ByteString (ByteString)
36import Obelisk.App (MonadObelisk, ObeliskT)
38 ( CliT (..), HasCliConfig, Severity (..)
39 , callCommand, failWith, getCliConfig, putLog
40 , readProcessAndLogStderr, runCli)
41import Obelisk.Command.Project (inProjectShell, withProjectRoot)
43data CabalPackageInfo = CabalPackageInfo
44 { _cabalPackageInfo_packageRoot :: FilePath
45 , _cabalPackageInfo_sourceDirs :: NE.NonEmpty FilePath
46 -- ^ List of hs src dirs of the library component
47 , _cabalPackageInfo_defaultExtensions :: [Extension]
48 -- ^ List of globally enable extensions of the library component
49 , _cabalPackageInfo_defaultLanguage :: Maybe Language
50 -- ^ List of globally set languages of the library component
53-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
57 withGhciScript pkgs $ \dotGhciPath -> do
58 freePort <- getFreePort
59 assets <- withProjectRoot "." $ \root -> do
60 let importableRoot = if "/" `isInfixOf` root
63 isDerivation <- readProcessAndLogStderr Debug $
68 , "(let a = import " <> importableRoot <> " {}; in toString (a.reflex.nixpkgs.lib.isDerivation a.passthru.staticFilesImpure))"
70 -- `--raw` is not available with old nix-instantiate. It drops quotation
71 -- marks and trailing newline, so is very convenient for shelling out.
73 -- Check whether the impure static files are a derivation (and so must be built)
74 if isDerivation == "1"
75 then fmap T.strip $ readProcessAndLogStderr Debug $ -- Strip whitespace here because nix-build has no --raw option
78 , "-E", "(import " <> importableRoot <> "{}).passthru.staticFilesImpure"
80 else readProcessAndLogStderr Debug $
81 proc "nix" ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
82 putLog Debug $ "Assets impurely loaded from: " <> assets
83 runGhcid dotGhciPath $ Just $ unwords
86 , "(runServeAsset " ++ show assets ++ ")"
91runRepl :: MonadObelisk m => m ()
94 withGhciScript pkgs $ \dotGhciPath -> do
95 runGhciRepl dotGhciPath
97runWatch :: MonadObelisk m => m ()
100 withGhciScript pkgs $ \dotGhciPath -> runGhcid dotGhciPath Nothing
102-- | Relative paths to local packages of an obelisk project
103-- TODO a way to query this
104getLocalPkgs :: Applicative f => f [FilePath]
105getLocalPkgs = pure ["backend", "common", "frontend"]
109 => FilePath -- ^ package directory
110 -> m (Maybe CabalPackageInfo)
111parseCabalPackage dir = do
112 let cabalFp = dir </> (takeBaseName dir <> ".cabal")
113 hpackFp = dir </> "package.yaml"
114 hasCabal <- liftIO $ doesFileExist cabalFp
115 hasHpack <- liftIO $ doesFileExist hpackFp
117 mCabalContents <- if hasCabal
118 then Just <$> liftIO (readUTF8File cabalFp)
121 let decodeOptions = DecodeOptions hpackFp Nothing decodeYaml
122 liftIO (readPackageConfig decodeOptions) >>= \case
124 putLog Error $ T.pack $ "Failed to parse " <> hpackFp <> ": " <> err
126 Right (DecodeResult hpackPackage _ _) -> do
127 return $ Just $ renderPackage [] hpackPackage
130 fmap join $ forM mCabalContents $ \cabalContents -> do
131 let (warnings, result) = runParseResult $ parseGenericPackageDescription $
132 toUTF8BS $ cabalContents
133 mapM_ (putLog Warning) $ fmap (T.pack . show) warnings
137 (_, lib) <- simplifyCondTree (const $ pure True) <$> condLibrary gpkg
138 pure $ CabalPackageInfo
139 { _cabalPackageInfo_packageRoot = takeDirectory cabalFp
140 , _cabalPackageInfo_sourceDirs =
141 fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
142 , _cabalPackageInfo_defaultExtensions =
143 defaultExtensions $ libBuildInfo lib
144 , _cabalPackageInfo_defaultLanguage =
145 defaultLanguage $ libBuildInfo lib
147 Left (_, errors) -> do
148 putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
149 mapM_ (putLog Error) $ fmap (T.pack . show) errors
152withUTF8FileContentsM :: (MonadIO m, HasCliConfig e m) => FilePath -> (ByteString -> CliT e IO a) -> m a
153withUTF8FileContentsM fp f = do
155 liftIO $ withUTF8FileContents fp $ runCli c . f . toUTF8BS
157-- | Create ghci configuration to load the given packages
160 => [FilePath] -- ^ List of packages to load into ghci
161 -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporory .ghci
163withGhciScript pkgs f = do
164 (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM pkgs $ \pkg -> do
165 flip fmap (parseCabalPackage pkg) $ \case
167 Just packageInfo -> Right packageInfo
169 when (null packageInfos) $
170 failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " pkgs
171 unless (null pkgDirErrs) $
172 putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
174 let extensions = packageInfos >>= _cabalPackageInfo_defaultExtensions
175 languageFromPkgs = L.nub $ mapMaybe _cabalPackageInfo_defaultLanguage packageInfos
176 -- NOTE when no default-language is present cabal sets Haskell98
177 language = NE.toList $ fromMaybe (Haskell98 NE.:| []) $ NE.nonEmpty languageFromPkgs
178 extensionsLine = if extensions == mempty
180 else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
182 [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
184 , ":set " <> intercalate " " (("-X" <>) . prettyShow <$> language)
185 , ":load Backend Frontend"
186 , "import Obelisk.Run"
187 , "import qualified Frontend"
188 , "import qualified Backend"
190 warnDifferentLanguages language
191 withSystemTempDirectory "ob-ghci" $ \fp -> do
192 let dotGhciPath = fp </> ".ghci"
193 liftIO $ writeFile dotGhciPath dotGhci
197 rootedSourceDirs pkg = NE.toList $
198 (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
200warnDifferentLanguages :: MonadObelisk m => [Language] -> m ()
201warnDifferentLanguages (_:_:_) = putLog Warning "Different languages detected across packages which may result in errors when loading the repl"
202warnDifferentLanguages _ = return ()
207 => FilePath -- ^ Path to .ghci
209runGhciRepl dotGhci = inProjectShell "ghc" $ unwords $ "ghci" : ["-no-user-package-db", "-ghci-script", dotGhci]
214 => FilePath -- ^ Path to .ghci
215 -> Maybe String -- ^ Optional command to run at every reload
217runGhcid dotGhci mcmd = callCommand $ unwords $ "ghcid" : opts
221 --TODO: The decision of whether to use -fwarn-redundant-constraints should probably be made by the user
222 , "--command='ghci -Wall -ignore-dot-ghci -fwarn-redundant-constraints -no-user-package-db -ghci-script " <> dotGhci <> "' "
224 , "--outputfile=ghcid-output.txt"
226 testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'")
228getFreePort :: MonadIO m => m PortNumber
229getFreePort = liftIO $ withSocketsDo $ do
230 addr:_ <- getAddrInfo (Just defaultHints) (Just "127.0.0.1") (Just "0")
231 bracket (open addr) close socketPort
234 sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
235 bind sock (addrAddress addr)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list