on ce62bc7 help sign in
8 files changed+181−50
since splitsnapshots
unifiedsplit
README.md+44−4 default.nix+11−2 haskell-overlays/ghcide.nix+34 lib/command/src/Obelisk/Command/Project.hs+12−4 lib/command/src/Obelisk/Command/Run.hs+73−39 skeleton/default.nix+3−1 skeleton/hie-bios.sh+3 skeleton/hie.yaml+1
README.md+44−4
@@ -10,7 +10,10 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
1010- [Developing an Obelisk project](#developing-an-obelisk-project)
1111 - [Adding Packages](#adding-packages)
1212 - [Adding Package Overrides](#adding-package-overrides)
13− - [Running over https](#running-over-https)
13+ - [Running over HTTPS](#running-over-https)
14+ - [IDE Support](#ide-support)
15+ - [Terminal-based feedback](#terminal-based-feedback)
16+ - [**Experimental** `ghcide` support](#experimental-ghcide-support)
1417- [Deploying](#deploying)
1518 - [Locally](#locally)
1619 - [Default EC2 Deployment](#default-ec2-deployment)
@@ -169,9 +172,46 @@ project ./. ({ pkgs, ... }: {
169172
170173### Running over HTTPS
171174
172−To run your app locally over https, update the protocol in `config/common/route` to `https`, and then use `ob run` as normal.
173−
174−Since Obelisk generates a self-signed certificate for running https, the browser will issue a warning about using an invalid certificate. On Chrome, you can go to `chrome://flags/#allow-insecure-localhost` to enable invalid certificates for localhost.
175+To run your app locally over HTTPS, update the protocol in `config/common/route` to `https`, and then use `ob run` as normal.
176+
177+Since Obelisk generates a self-signed certificate for running HTTPS, the browser will issue a warning about using an invalid certificate. On Chrome, you can go to `chrome://flags/#allow-insecure-localhost` to enable invalid certificates for localhost.
178+
179+### IDE Support
180+
181+#### Terminal-based feedback
182+
183+Obelisk officially supports terminal-based feedback (akin to [`ghcid`](https://github.com/ndmitchell/ghcid)) in `ob run` and `ob watch`.
184+
185+#### **Experimental** `ghcide` support
186+
187+> **NOTE:** `ghcide` support is strictly **experimental** and **not officially supported**. There is no guarantee that it will work for you or that this support will be maintained over time.
188+
189+To try out the **experimental** [`ghcide`](https://github.com/digital-asset/ghcide) support:
190+
191+ * Install a `ghcide` plugin for your editor.
192+ * Add `__withGhcide = true;` to your Obelisk project's `default.nix` (inside the call to `project`). With this setting, `ob shell` will provide `ghcide` as well.
193+ * Add `hie-bios.sh` to the root of your project:
194+ ```shell
195+ cat > hie-bios.sh <<'EOF'
196+ #!/usr/bin/env bash
197+ source "$HOME/.bash_profile"; # NOTE: Some editors need help finding 'ob' and this assumes that `ob` is made available on your `$PATH` in `.bash_profile`
198+ ob internal export-ghci-configuration > "$HIE_BIOS_OUTPUT"
199+ EOF
200+ chmod +x hie-bios.sh
201+ ```
202+ * Add `hie.yaml` to the root of your project:
203+ ```shell
204+ echo 'cradle: { bios: { program: hie-bios.sh } }' > hie.yaml
205+ ```
206+ * Test the configuration with `ob shell ghcide`. Assuming your project compiles and works with `ghcide` otherwise, this should succeed.
207+ * Start your editor in a way that it can access the appropriate `ghcide`:
208+ * This can sometimes be achieved with `ob shell 'start my editor'`.
209+ * If you can't easily start your editor in a way that it inherits your shell, you may need customize how your `ghcide` plugin finds `ghcide` or create a custom `ghcide` replacement script that you install globally. For example, for Visual Studio Code on macOS adding a script called `ghcide` to your `PATH` like the one below will casue `ghcide` from each project's `ob shell` to be used:
210+ ```shell
211+ #!/usr/bin/env bash
212+ source "$HOME/.bash_profile" # NOTE: This assumes that `ob` is made available on your `$PATH` in `.bash_profile`
213+ ob shell "ghcide $@"
214+ ```
175215
176216## Deploying
177217
default.nix+11−2
@@ -207,13 +207,14 @@ in rec {
207207 , shellToolOverrides ? _: _: {}
208208 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
209209 , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
210+ , __withGhcide ? false
210211 }:
211212 let
212213 allConfig = nixpkgs.lib.makeExtensible (self: {
213214 base = base';
214215 inherit args;
215216 userSettings = {
216− inherit android ios packages overrides tools shellToolOverrides withHoogle __closureCompilerOptimizationLevel;
217+ inherit android ios packages overrides tools shellToolOverrides withHoogle __closureCompilerOptimizationLevel __withGhcide;
217218 staticFiles = if staticFiles == null then self.base + /static else staticFiles;
218219 };
219220 frontendName = "frontend";
@@ -270,9 +271,17 @@ in rec {
270271 self.frontendName
271272 ];
272273
274+ shellToolOverrides = lib.composeExtensions
275+ self.userSettings.shellToolOverrides
276+ (if self.userSettings.__withGhcide
277+ then (import ./haskell-overlays/ghcide.nix)
278+ else (_: _: {})
279+ );
280+
273281 project = reflexPlatformProject ({...}: self.projectConfig);
274282 projectConfig = {
275− inherit (self.userSettings) shellToolOverrides tools withHoogle;
283+ inherit (self) shellToolOverrides;
284+ inherit (self.userSettings) tools withHoogle;
276285 overrides = self.totalOverrides;
277286 packages = self.combinedPackages;
278287 shells = {
haskell-overlays/ghcide.nixnew file+34
@@ -0,0 +1,34 @@
1+self: super:
2+ let
3+ pkgs = self.callPackage ({ pkgs }: pkgs) {};
4+ inherit (pkgs.haskell.lib) dontCheck;
5+ in {
6+ ghcide = pkgs.haskell.lib.justStaticExecutables (dontCheck ((self.override {
7+ overrides = self: super: {
8+ ghc-check = self.callHackageDirect {
9+ pkg = "ghc-check";
10+ ver = "0.1.0.3";
11+ sha256 = "038llbvryk5y27jbdpbshp0zw5lw1j6m7qk7vx1n96ykqdzkh649";
12+ } {};
13+ lsp-test = dontCheck (self.callHackage "lsp-test" "0.6.1.0" {});
14+ haddock-library = dontCheck (self.callHackage "haddock-library" "1.8.0" {});
15+ haskell-lsp = dontCheck (self.callHackage "haskell-lsp" "0.19.0.0" {});
16+ haskell-lsp-types = dontCheck (self.callHackage "haskell-lsp-types" "0.19.0.0" {});
17+ regex-posix = dontCheck (self.callHackage "regex-posix" "0.96.0.0" {});
18+ test-framework = dontCheck (self.callHackage "test-framework" "0.8.2.0" {});
19+ regex-base = dontCheck (self.callHackage "regex-base" "0.94.0.0" {});
20+ regex-tdfa = dontCheck (self.callHackage "regex-tdfa" "1.3.1.0" {});
21+ shake = dontCheck (self.callHackage "shake" "0.18.4" {});
22+ hie-bios = dontCheck (self.callHackageDirect {
23+ pkg = "hie-bios";
24+ ver = "0.4.0";
25+ sha256 = "19lpg9ymd9656cy17vna8wr1hvzfal94gpm2d3xpnw1d5qr37z7x";
26+ } {});
27+ };
28+ }).callCabal2nix "ghcide" (pkgs.fetchFromGitHub {
29+ owner = "digital-asset";
30+ repo = "ghcide";
31+ rev = "v0.1.0";
32+ sha256 = "1kf71iix46hvyxviimrcv7kvsj67hcnnqlpdsmazmlmybf7wbqbb";
33+ }) {}));
34+}
lib/command/src/Obelisk/Command/Project.hs+12−4
@@ -10,7 +10,7 @@ module Obelisk.Command.Project
1010 , nixShellRunConfig
1111 , nixShellRunProc
1212 , nixShellWithHoogle
13− , nixShellWithPkgs
13+ , nixShellWithoutPkgs
1414 , obeliskDirName
1515 , toImplDir
1616 , toObeliskDir
@@ -287,12 +287,20 @@ nixShellRunConfig root isPure command = do
287287nixShellRunProc :: NixShellConfig -> ProcessSpec
288288nixShellRunProc cfg = setDelegateCtlc True $ proc "nix-shell" $ runNixShellConfig cfg
289289
290−nixShellWithPkgs :: MonadObelisk m => FilePath -> Bool -> Bool -> Map Text FilePath -> String -> Maybe String -> m ()
291−nixShellWithPkgs root isPure chdirToRoot packageNamesAndPaths shellAttr command = do
290+nixShellWithoutPkgs
291+ :: MonadObelisk m
292+ => FilePath -- ^ Path to project root
293+ -> Bool -- ^ Should this be a pure shell?
294+ -> Bool -- ^ Should we chdir to the package root in the shell?
295+ -> Map Text FilePath -- ^ Package names mapped to their paths
296+ -> String -- ^ Shell attribute to use (e.g. @"ghc"@, @"ghcjs"@, etc.)
297+ -> Maybe String -- ^ If 'Just' run the given command; otherwise just open the interactive shell
298+ -> m ()
299+nixShellWithoutPkgs root isPure chdirToRoot packageNamesAndPaths shellAttr command = do
292300 packageNamesAndAbsPaths <- liftIO $ for packageNamesAndPaths makeAbsolute
293301 defShellConfig <- nixShellRunConfig root isPure command
294302 let setCwd_ = if chdirToRoot then setCwd (Just root) else id
295− (_, _, _, ph) <- createProcess_ "nixShellWithPkgs" $ setCwd_ $ nixShellRunProc $ defShellConfig
303+ (_, _, _, ph) <- createProcess_ "nixShellWithoutPkgs" $ setCwd_ $ nixShellRunProc $ defShellConfig
296304 & nixShellConfig_common . nixCmdConfig_target . target_expr ?~
297305 "{root, pkgs, shell}: ((import root {}).passthru.__unstable__.self.extend (_: _: {\
298306 \shellPackages = builtins.fromJSON pkgs;\
lib/command/src/Obelisk/Command/Run.hs+73−39
@@ -18,6 +18,8 @@ import Control.Monad (filterM, unless, void)
1818import Control.Monad.Except (runExceptT, throwError)
1919import Control.Monad.IO.Class (liftIO)
2020import Control.Monad.Reader (MonadIO)
21+import Data.Bifoldable (bifoldr1)
22+import Data.Bifunctor (bimap)
2123import Data.Coerce (coerce)
2224import Data.Default (def)
2325import Data.Either (partitionEithers)
@@ -74,7 +76,7 @@ import Obelisk.CliApp (
7476 withSpinner,
7577 )
7678import Obelisk.Command.Nix
77−import Obelisk.Command.Project (nixShellWithPkgs, withProjectRoot, findProjectAssets)
79+import Obelisk.Command.Project (nixShellWithoutPkgs, withProjectRoot, findProjectAssets)
7880import Obelisk.Command.Thunk (attrCacheFileName)
7981import Obelisk.Command.Utils (findExePath, ghcidExePath)
8082
@@ -162,11 +164,12 @@ profile profileBasePattern rtsFlags = withProjectRoot "." $ \root -> do
162164run :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
163165run root interpretPaths = do
164166 pkgs <- getParsedLocalPkgs root interpretPaths
165− withGhciScript pkgs root $ \dotGhciPath -> do
166− freePort <- getFreePort
167+ withGhciScript pkgs $ \dotGhciPath -> do
167168 assets <- findProjectAssets root
168169 putLog Debug $ "Assets impurely loaded from: " <> assets
169− runGhcid root True dotGhciPath pkgs $ Just $ unwords
170+ ghciArgs <- getGhciSessionSettings pkgs root
171+ freePort <- getFreePort
172+ runGhcid root True (ghciArgs <> mkGhciScriptArg dotGhciPath) pkgs $ Just $ unwords
170173 [ "Obelisk.Run.run"
171174 , show freePort
172175 , "(Obelisk.Run.runServeAsset " ++ show assets ++ ")"
@@ -177,14 +180,16 @@ run root interpretPaths = do
177180runRepl :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
178181runRepl root interpretPaths = do
179182 pkgs <- getParsedLocalPkgs root interpretPaths
180− withGhciScript pkgs "." $ \dotGhciPath ->
181− runGhciRepl root pkgs dotGhciPath
183+ ghciArgs <- getGhciSessionSettings pkgs "."
184+ withGhciScript pkgs $ \dotGhciPath ->
185+ runGhciRepl root pkgs (ghciArgs <> mkGhciScriptArg dotGhciPath)
182186
183187runWatch :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
184188runWatch root interpretPaths = do
185189 pkgs <- getParsedLocalPkgs root interpretPaths
186− withGhciScript pkgs root $ \dotGhciPath ->
187− runGhcid root True dotGhciPath pkgs Nothing
190+ ghciArgs <- getGhciSessionSettings pkgs root
191+ withGhciScript pkgs $ \dotGhciPath ->
192+ runGhcid root True (ghciArgs <> mkGhciScriptArg dotGhciPath) pkgs Nothing
188193
189194exportGhciConfig :: MonadObelisk m => FilePath -> PathTree Interpret -> m [String]
190195exportGhciConfig root interpretPaths = do
@@ -194,7 +199,7 @@ exportGhciConfig root interpretPaths = do
194199nixShellForInterpretPaths :: MonadObelisk m => Bool -> String -> FilePath -> PathTree Interpret -> Maybe String -> m ()
195200nixShellForInterpretPaths isPure shell root interpretPaths cmd = do
196201 pkgs <- getParsedLocalPkgs root interpretPaths
197− nixShellWithPkgs root isPure False (packageInfoToNamePathMap pkgs) shell cmd
202+ nixShellWithoutPkgs root isPure False (packageInfoToNamePathMap pkgs) shell cmd
198203
199204-- | Like 'getLocalPkgs' but also parses them and fails if any of them can't be parsed.
200205getParsedLocalPkgs :: MonadObelisk m => FilePath -> PathTree Interpret -> m (NonEmpty CabalPackageInfo)
@@ -395,12 +400,14 @@ packageInfoToNamePathMap = Map.fromList . map (_cabalPackageInfo_packageName &&&
395400withGhciScript
396401 :: (MonadObelisk m, Foldable f)
397402 => f CabalPackageInfo -- ^ List of packages to load into ghci
398− -> FilePath -- ^ All paths written to the .ghci file will be relative to this path
399403 -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporary .ghci
400404 -> m ()
401−withGhciScript (toList -> packageInfos) pathBase f = do
402− ghciSettings <- getGhciSessionSettings packageInfos pathBase
403− let
405+withGhciScript (toList -> packageInfos) f =
406+ withSystemTempDirectory "ob-ghci" $ \fp -> do
407+ let dotGhciPath = fp </> ".ghci"
408+ liftIO $ writeFile dotGhciPath dotGhci
409+ f dotGhciPath
410+ where
404411 packageNames = Set.fromList $ map _cabalPackageInfo_packageName packageInfos
405412 modulesToLoad = mconcat
406413 [ [ "Obelisk.Run" | "obelisk-run" `Set.member` packageNames ]
@@ -408,22 +415,18 @@ withGhciScript (toList -> packageInfos) pathBase f = do
408415 , [ "Frontend" | "frontend" `Set.member` packageNames ]
409416 ]
410417 dotGhci = unlines
411− [ ":set " <> unwords ghciSettings -- TODO: Shell escape
412− , if null modulesToLoad then "" else ":load " <> unwords modulesToLoad
418+ [ if null modulesToLoad then "" else ":load " <> unwords modulesToLoad
413419 , "import qualified Obelisk.Run"
414420 , "import qualified Frontend"
415− , "import qualified Backend" ]
416− withSystemTempDirectory "ob-ghci" $ \fp -> do
417− let dotGhciPath = fp </> ".ghci"
418− liftIO $ writeFile dotGhciPath dotGhci
419− f dotGhciPath
421+ , "import qualified Backend"
422+ ]
420423
421424-- | Builds a list of options to pass to ghci or set in .ghci file that configures
422425-- the preprocessor and source includes.
423426getGhciSessionSettings
424427 :: (MonadObelisk m, Foldable f)
425428 => f CabalPackageInfo -- ^ List of packages to load into ghci
426− -> FilePath -- ^ All paths written to the .ghci file will be relative to this path
429+ -> FilePath -- ^ All paths will be relative to this path
427430 -> m [String]
428431getGhciSessionSettings (toList -> packageInfos) pathBase = do
429432 -- N.B. ghci settings do NOT support escaping in any way. To minimize the likelihood that
@@ -435,57 +438,62 @@ getGhciSessionSettings (toList -> packageInfos) pathBase = do
435438 (pkgFiles, pkgSrcPaths :: [NonEmpty FilePath]) <- fmap unzip $ liftIO $ for packageInfos $ \pkg -> do
436439 canonicalSrcDirs <- traverse canonicalizePath $ (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
437440 canonicalPkgFile <- canonicalizePath $ _cabalPackageInfo_packageFile pkg
438− pure (makeRelative canonicalPathBase canonicalPkgFile, makeRelative canonicalPathBase <$> canonicalSrcDirs)
441+ pure (canonicalPkgFile `relativeTo` canonicalPathBase, (`relativeTo` canonicalPathBase) <$> canonicalSrcDirs)
439442
440443 pure
441− $ ["-F", "-pgmF", selfExe, "-optF", preprocessorIdentifier]
444+ $ baseGhciOptions
445+ <> ["-F", "-pgmF", selfExe, "-optF", preprocessorIdentifier]
442446 <> concatMap (\p -> ["-optF", p]) pkgFiles
443447 <> [ "-i" <> intercalate ":" (concatMap toList pkgSrcPaths) ]
444448
449+baseGhciOptions :: [String]
450+baseGhciOptions =
451+ [ "-ignore-dot-ghci"
452+ , "-no-user-package-db"
453+ , "-package-env", "-"
454+ ]
455+
445456-- | Run ghci repl
446457runGhciRepl
447458 :: (MonadObelisk m, Foldable f)
448459 => FilePath -- ^ Path to project root
449− -> f CabalPackageInfo
450− -> FilePath -- ^ Path to .ghci
460+ -> f CabalPackageInfo -- ^ Packages to keep unbuilt
461+ -> [String] -- ^ GHCi arguments
451462 -> m ()
452−runGhciRepl root (toList -> packages) dotGhci =
463+runGhciRepl root (toList -> packages) ghciArgs =
453464 -- NOTE: We do *not* want to use $(staticWhich "ghci") here because we need the
454465 -- ghc that is provided by the shell in the user's project.
455− nixShellWithPkgs root True False (packageInfoToNamePathMap packages) "ghc" $ Just $ "ghci " <> makeBaseGhciOptions dotGhci -- TODO: Shell escape
466+ nixShellWithoutPkgs root True False (packageInfoToNamePathMap packages) "ghc" $
467+ Just $ unwords $ "ghci" : ghciArgs -- TODO: Shell escape
456468
457469-- | Run ghcid
458470runGhcid
459471 :: (MonadObelisk m, Foldable f)
460472 => FilePath -- ^ Path to project root
461473 -> Bool -- ^ Should we chdir to root when running this process?
462− -> FilePath -- ^ Path to .ghci
463− -> f CabalPackageInfo
474+ -> [String] -- ^ GHCi arguments
475+ -> f CabalPackageInfo -- ^ Packages to keep unbuilt
464476 -> Maybe String -- ^ Optional command to run at every reload
465477 -> m ()
466−runGhcid root chdirToRoot dotGhci (toList -> packages) mcmd =
467− nixShellWithPkgs root True chdirToRoot (packageInfoToNamePathMap packages) "ghc" $ Just $ unwords $ ghcidExePath : opts -- TODO: Shell escape
478+runGhcid root chdirToRoot ghciArgs (toList -> packages) mcmd =
479+ nixShellWithoutPkgs root True chdirToRoot (packageInfoToNamePathMap packages) "ghc" $
480+ Just $ unwords $ ghcidExePath : opts -- TODO: Shell escape
468481 where
469482 opts =
470483 [ "-W"
471− , "--command='ghci -ignore-dot-ghci " <> makeBaseGhciOptions dotGhci <> "' "
472484 , "--outputfile=ghcid-output.txt"
473485 ] <> map (\x -> "--reload='" <> x <> "'") reloadFiles
474486 <> map (\x -> "--restart='" <> x <> "'") restartFiles
475487 <> testCmd
488+ <> ["--command='" <> unwords ("ghci" : ghciArgs) <> "'"] -- TODO: Shell escape
476489 testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'") -- TODO: Shell escape
477490
478491 adjustRoot x = if chdirToRoot then makeRelative root x else x
479492 reloadFiles = map adjustRoot [root </> "config"]
480493 restartFiles = map (adjustRoot . _cabalPackageInfo_packageFile) packages
481494
482−makeBaseGhciOptions :: FilePath -> String
483−makeBaseGhciOptions dotGhci =
484− unwords
485− [ "-no-user-package-db"
486− , "-package-env -"
487− , "-ghci-script " <> dotGhci
488− ]
495+mkGhciScriptArg :: FilePath -> [String]
496+mkGhciScriptArg dotGhci = ["-ghci-script", dotGhci]
489497
490498getFreePort :: MonadIO m => m PortNumber
491499getFreePort = liftIO $ withSocketsDo $ do
@@ -504,3 +512,29 @@ pathToTree a p = go $ splitDirectories p
504512 where
505513 go [] = PathTree_Node (Just a) mempty
506514 go (x : xs) = PathTree_Node Nothing $ Map.singleton x $ go xs
515+
516+-- | Like 'zipWith' but pads with a padding value instead of stopping on the shortest list.
517+zipDefaultWith :: a -> b -> (a -> b -> c) -> [a] -> [b] -> [c]
518+zipDefaultWith _da _db _f [] [] = []
519+zipDefaultWith da db f (a:as) [] = f a db : zipDefaultWith da db f as []
520+zipDefaultWith da db f [] (b:bs) = f da b : zipDefaultWith da db f [] bs
521+zipDefaultWith da db f (a:as) (b:bs) = f a b : zipDefaultWith da db f as bs
522+
523+-- | Makes the first absolute path relative to the second absolute path.
524+--
525+-- Both input paths MUST be absolute.
526+--
527+-- Unlike 'makeRelative' this does not merely strip prefixes. It will introduce
528+-- enough @..@ paths to make the resulting path truly relative in virtually every
529+-- case. The only exception is on Windows when the two paths are on different
530+-- drives. In this case the resulting path may be absolute.
531+relativeTo :: FilePath -> FilePath -> FilePath
532+relativeTo dir base
533+ = bifoldr1 (</>)
534+ $ bimap (collapse . (".." <$) . catMaybes) (collapse . catMaybes)
535+ $ unzip
536+ $ dropWhile (\(a,b) -> a == b)
537+ $ zipDefaultWith Nothing Nothing (,)
538+ (map Just $ splitDirectories base)
539+ (map Just $ splitDirectories dir)
540+ where collapse = foldr (</>) ""
skeleton/default.nix+3−1
@@ -8,9 +8,11 @@
88 }
99}:
1010with obelisk;
11−project ./. ({ ... }: {
11+project ./. ({ pkgs, ... }: {
1212 android.applicationId = "systems.obsidian.obelisk.examples.minimal";
1313 android.displayName = "Obelisk Minimal Example";
1414 ios.bundleIdentifier = "systems.obsidian.obelisk.examples.minimal";
1515 ios.bundleName = "Obelisk Minimal Example";
16+
17+ __withGhcide = true;
1618})
skeleton/hie-bios.shnew file+3
@@ -0,0 +1,3 @@
1+#!/usr/bin/env bash
2+. $HOME/.bash_profile;
3+ob internal export-ghci-configuration > "$HIE_BIOS_OUTPUT"
skeleton/hie.yamlnew file+1
@@ -0,0 +1 @@
1+cradle: { bios: { program: hie-bios.sh } }