1{-# LANGUAGE FlexibleContexts #-}
2{-# LANGUAGE LambdaCase #-}
3{-# LANGUAGE MultiParamTypeClasses #-}
4{-# LANGUAGE OverloadedStrings #-}
5{-# LANGUAGE ScopedTypeVariables #-}
6{-# LANGUAGE TemplateHaskell #-}
7module Obelisk.Command.Run where
9import Control.Exception (bracket)
11import Control.Monad.IO.Class (liftIO)
12import Control.Monad.Reader (MonadIO)
13import Data.ByteString (ByteString)
14import qualified Data.ByteString.UTF8 as BSU
16import qualified Data.List as L
17import qualified Data.List.NonEmpty as NE
19import qualified Data.Text as T
20import Distribution.Compiler (CompilerFlavor(..))
21import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
22import Distribution.Parsec.ParseResult (runParseResult)
23import Distribution.Pretty
24import Distribution.Types.BuildInfo
25import Distribution.Types.CondTree
26import Distribution.Types.GenericPackageDescription
27import Distribution.Types.Library
28import Distribution.Utils.Generic
32import Language.Haskell.Extension
33import Network.Socket hiding (Debug)
34import System.Directory
36import System.Process (proc)
37import System.IO.Temp (withSystemTempDirectory)
38import System.Which (staticWhich)
39import Text.ShellEscape (bash)
41import Obelisk.App (MonadObelisk, ObeliskT)
43 ( CliT (..), HasCliConfig, Severity (..)
44 , callCommand, failWith, getCliConfig, putLog
45 , readProcessAndLogStderr, runCli)
46import Obelisk.Command.Project (inProjectProc, withProjectRoot)
48data CabalPackageInfo = CabalPackageInfo
49 { _cabalPackageInfo_packageRoot :: FilePath
50 , _cabalPackageInfo_sourceDirs :: NE.NonEmpty FilePath
51 -- ^ List of hs src dirs of the library component
52 , _cabalPackageInfo_defaultExtensions :: [Extension]
53 -- ^ List of globally enable extensions of the library component
54 , _cabalPackageInfo_defaultLanguage :: Maybe Language
55 -- ^ List of globally set languages of the library component
56 , _cabalPackageInfo_compilerOptions :: [(CompilerFlavor, [String])]
57 -- ^ List of compiler-specific options (e.g., the "ghc-options" field of the cabal file)
60-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
63 let nixPath = $(staticWhich "nix")
64 nixBuildPath = $(staticWhich "nix-build")
66 withGhciScript pkgs $ \dotGhciPath -> do
67 freePort <- getFreePort
68 assets <- withProjectRoot "." $ \root -> do
69 let importableRoot = if "/" `isInfixOf` root
72 isDerivation <- readProcessAndLogStderr Debug $
77 , "(let a = import " <> importableRoot <> " {}; in toString (a.reflex.nixpkgs.lib.isDerivation a.passthru.staticFilesImpure))"
79 -- `--raw` is not available with old nix-instantiate. It drops quotation
80 -- marks and trailing newline, so is very convenient for shelling out.
82 -- Check whether the impure static files are a derivation (and so must be built)
83 if isDerivation == "1"
84 then fmap T.strip $ readProcessAndLogStderr Debug $ -- Strip whitespace here because nix-build has no --raw option
87 , "-E", "(import " <> importableRoot <> "{}).passthru.staticFilesImpure"
89 else readProcessAndLogStderr Debug $
90 proc nixPath ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
91 putLog Debug $ "Assets impurely loaded from: " <> assets
92 runGhcid dotGhciPath $ Just $ unwords
95 , "(runServeAsset " ++ show assets ++ ")"
100runRepl :: MonadObelisk m => m ()
103 withGhciScript pkgs $ \dotGhciPath -> do
104 runGhciRepl dotGhciPath
106runWatch :: MonadObelisk m => m ()
109 withGhciScript pkgs $ \dotGhciPath -> runGhcid dotGhciPath Nothing
111-- | Relative paths to local packages of an obelisk project
112-- TODO a way to query this
113getLocalPkgs :: Applicative f => f [FilePath]
114getLocalPkgs = pure ["backend", "common", "frontend"]
118 => FilePath -- ^ package directory
119 -> m (Maybe CabalPackageInfo)
120parseCabalPackage dir = do
121 let cabalFp = dir </> (takeBaseName dir <> ".cabal")
122 hpackFp = dir </> "package.yaml"
123 hasCabal <- liftIO $ doesFileExist cabalFp
124 hasHpack <- liftIO $ doesFileExist hpackFp
126 mCabalContents <- if hasCabal
127 then Just <$> liftIO (readUTF8File cabalFp)
130 let decodeOptions = DecodeOptions (ProgramName "ob") hpackFp Nothing decodeYaml
131 liftIO (readPackageConfig decodeOptions) >>= \case
133 putLog Error $ T.pack $ "Failed to parse " <> hpackFp <> ": " <> err
135 Right (DecodeResult hpackPackage _ _ _) -> do
136 return $ Just $ renderPackage [] hpackPackage
139 fmap join $ forM mCabalContents $ \cabalContents -> do
140 let (warnings, result) = runParseResult $ parseGenericPackageDescription $
141 toUTF8BS $ cabalContents
142 mapM_ (putLog Warning) $ fmap (T.pack . show) warnings
146 (_, lib) <- simplifyCondTree (const $ pure True) <$> condLibrary gpkg
147 pure $ CabalPackageInfo
148 { _cabalPackageInfo_packageRoot = takeDirectory cabalFp
149 , _cabalPackageInfo_sourceDirs =
150 fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
151 , _cabalPackageInfo_defaultExtensions =
152 defaultExtensions $ libBuildInfo lib
153 , _cabalPackageInfo_defaultLanguage =
154 defaultLanguage $ libBuildInfo lib
155 , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
157 Left (_, errors) -> do
158 putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
159 mapM_ (putLog Error) $ fmap (T.pack . show) errors
162withUTF8FileContentsM :: (MonadIO m, HasCliConfig e m) => FilePath -> (ByteString -> CliT e IO a) -> m a
163withUTF8FileContentsM fp f = do
165 liftIO $ withUTF8FileContents fp $ runCli c . f . toUTF8BS
167-- | Create ghci configuration to load the given packages
170 => [FilePath] -- ^ List of packages to load into ghci
171 -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporory .ghci
173withGhciScript pkgs f = do
174 (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM pkgs $ \pkg -> do
175 flip fmap (parseCabalPackage pkg) $ \case
177 Just packageInfo -> Right packageInfo
179 when (null packageInfos) $
180 failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " pkgs
181 unless (null pkgDirErrs) $
182 putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
184 let extensions = packageInfos >>= _cabalPackageInfo_defaultExtensions
185 languageFromPkgs = L.nub $ mapMaybe _cabalPackageInfo_defaultLanguage packageInfos
186 -- NOTE when no default-language is present cabal sets Haskell98
187 language = NE.toList $ fromMaybe (Haskell98 NE.:| []) $ NE.nonEmpty languageFromPkgs
188 extensionsLine = if extensions == mempty
190 else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
191 ghcOptions = concat $ mapMaybe (\case (GHC, xs) -> Just xs; _ -> Nothing) $
192 packageInfos >>= _cabalPackageInfo_compilerOptions
194 [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
197 xs -> ":set " <> intercalate " " xs
199 , ":set " <> intercalate " " (("-X" <>) . prettyShow <$> language)
200 , ":load Backend Frontend"
201 , "import Obelisk.Run"
202 , "import qualified Frontend"
203 , "import qualified Backend"
205 warnDifferentLanguages language
206 withSystemTempDirectory "ob-ghci" $ \fp -> do
207 let dotGhciPath = fp </> ".ghci"
208 liftIO $ writeFile dotGhciPath dotGhci
212 rootedSourceDirs pkg = NE.toList $
213 (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
215warnDifferentLanguages :: MonadObelisk m => [Language] -> m ()
216warnDifferentLanguages (_:_:_) = putLog Warning "Different languages detected across packages which may result in errors when loading the repl"
217warnDifferentLanguages _ = return ()
222 => FilePath -- ^ Path to .ghci
224runGhciRepl dotGhci = inProjectProc "ghc" $ fmap (bash . BSU.fromString) $ "ghci" : ["-no-user-package-db", "-ghci-script", dotGhci]
229 => FilePath -- ^ Path to .ghci
230 -> Maybe String -- ^ Optional command to run at every reload
232runGhcid dotGhci mcmd = callCommand $ unwords $ $(staticWhich "ghcid") : opts
236 --TODO: The decision of whether to use -fwarn-redundant-constraints should probably be made by the user
237 , "--command='ghci -Wall -ignore-dot-ghci -fwarn-redundant-constraints -no-user-package-db -ghci-script " <> dotGhci <> "' "
239 , "--outputfile=ghcid-output.txt"
241 testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'")
243getFreePort :: MonadIO m => m PortNumber
244getFreePort = liftIO $ withSocketsDo $ do
245 addr:_ <- getAddrInfo (Just defaultHints) (Just "127.0.0.1") (Just "0")
246 bracket (open addr) close socketPort
249 sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
250 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