on master help sign in
Merge branch 'develop' into alexfmpe-patch-1
commit164819e90eb8cf4157bd90f63316e70a286ab715 authorElliot Cameron <3noch@users.noreply.github.com> dateMon 8 Jun 2020, 16:28 (6 years ago) parent5e01165637c65f
27 files changed+313−172
ChangeLog.md+13−1 default.nix+16−12 dep/.gitignore−5 dep/reflex-platform/default.nix+1−7 dep/reflex-platform/github.json+3−3 dep/reflex-platform/thunk.nix+9 guides/app-deploy/README.md+14−8 haskell-overlays/obelisk.nix+1 lib/asset/manifest/obelisk-asset-manifest.cabal+1−1 lib/asset/manifest/src/Obelisk/Asset/Symlink.hs+1−1 lib/asset/serve-snap/obelisk-asset-serve-snap.cabal+1−1 lib/asset/serve-snap/src/Obelisk/Asset/Serve/Snap.hs+1−1 lib/cliapp/obelisk-cliapp.cabal+2 lib/cliapp/src/Obelisk/CliApp.hs+3 lib/cliapp/src/Obelisk/CliApp/Process.hs+47−27 lib/command/obelisk-command.cabal+3−1 lib/command/src/Obelisk/Command.hs+12−8 lib/command/src/Obelisk/Command/Deploy.hs+1−1 lib/command/src/Obelisk/Command/Project.hs+16−11 lib/command/src/Obelisk/Command/Run.hs+60−50 lib/command/src/Obelisk/Command/Thunk.hs+3−3 lib/command/src/Obelisk/Command/Utils.hs+6 lib/frontend/src/Obelisk/Frontend.hs+1−1 lib/route/src/Obelisk/Route.hs+70−27 lib/selftest/src/Obelisk/SelfTest.hs+24 lib/snap-extras/obelisk-snap-extras.cabal−1 skeleton/frontend/frontend.cabal+4−2
ChangeLog.md+13−1
@@ -2,9 +2,21 @@
22
33This project's release branch is `master`. This log is written from the perspective of the release branch: when changes hit `master`, they are considered released.
44
5+## Unreleased
6+
7+* ([#715](https://github.com/obsidiansystems/obelisk/pull/715)) In `Obelisk.Route` deprecate `isoEncoder` and `prismEncoder` in favor of more precisely named `viewEncoder` and `reviewEncoder` (respectively) and improve documentation regarding contravariance of `reviewEncoder`.
8+* ([#739](https://github.com/obsidiansystems/obelisk/pull/739)) Improve `ob shell` by allowing commands to be passed verbatim after a `--` argument. For example, `ob shell 'run command'` can now be written `ob shell -- run command`.
9+* ([#735](https://github.com/obsidiansystems/obelisk/pull/735)) Fix regression causing custom `Prelude`s to break `ob run`/`ob watch`/`ob repl`.
10+* ([#737](https://github.com/obsidiansystems/obelisk/pull/737)) Fix bug causing custom `Prelude`s to break `ob profile`.
11+* ([#752](https://github.com/obsidiansystems/obelisk/pull/752)) Fix the `ob` command-line tool to return non-zero exit code when underlying processes fail.
12+* ([#742](https://github.com/obsidiansystems/obelisk/pull/742), [#57](https://github.com/obsidiansystems/obelisk/pull/57), [#406](https://github.com/obsidiansystems/obelisk/pull/406)) Enable `-dedupe` and `-DGHCJS_BROWSER` in GHCJS builds to make JavaScript output considerably smaller leading to faster load/parse times and faster build times.
13+ * **Migration:** New Obelisk projects will automatically benefit from this change, but existing projects need to apply a change similar to [this one](https://github.com/obsidiansystems/obelisk/blob/371cb3302085601c5ec73e9574d51c8b95e3e493/skeleton/frontend/frontend.cabal#L32-L34).
14+* ([#742](https://github.com/obsidiansystems/obelisk/pull/742)) Update `reflex-platform` which includes:
15+ * A new version of GHCJS where `-dedupe` is fixed.
16+
517## v0.8.0.0
618
7−* ([#674](https://github.com/obsidiansystems/obelisk/pull/674), [#711](https://github.com/obsidiansystems/obelisk/pull/711)) Introduce a new thunk format to support accessing the thunk's source directly when packed. When packed, thunks have an additional file called `thunk.nix` and `default.nix` is now a think wrapper around that.
19+* ([#674](https://github.com/obsidiansystems/obelisk/pull/674), [#711](https://github.com/obsidiansystems/obelisk/pull/711)) Introduce a new thunk format to support accessing the thunk's source directly when packed. When packed, thunks have an additional file called `thunk.nix` and `default.nix` is now a thin wrapper around that.
820* ([#665](https://github.com/obsidiansystems/obelisk/pull/665)) Add `--interpret` and `--no-interpret` options to `ob run`/`ob watch`/`ob repl`/`ob shell`. These options allow you to pick which paths will be pre-compiled by `nix` when entering the shell/session and which won't. For example `ob run --no-interpret dep` will ensure that any dependencies found in `./dep` will be built by `nix` before loading the rest of the project into the `ghci` session. The same configuration for `ob shell` will ensure that those packages are built and available in the `ghc` package database inside the shell.
921
1022 **NOTE:** `ob shell`'s default behavior is now different. By default it now behaves like `ob run`/`ob watch`/`ob repl` in that it does *not* pre-build any packages whose `.cabal` or `package.yaml` files are found in the project. To regain the previous behavior, use `ob shell --no-interpret . --interpret backend --interpret common --interpret frontend` from the project root.
default.nix+16−12
@@ -321,29 +321,33 @@ in rec {
321321 __unstable__.profiledObRun = let
322322 profiled = projectOut { inherit system; enableLibraryProfiling = true; };
323323 exeSource = builtins.toFile "ob-run.hs" ''
324+ {-# LANGUAGE NoImplicitPrelude #-}
325+ {-# LANGUAGE PackageImports #-}
324326 module Main where
325327
326− import Control.Exception
327− import Reflex.Profiled
328− import System.Environment
328+ -- Explicitly import Prelude from base lest there be multiple modules called Prelude
329+ import "base" Prelude (IO, (++), read)
329330
330− import qualified Obelisk.Run
331+ import "base" Control.Exception (finally)
332+ import "reflex" Reflex.Profiled (writeProfilingData)
333+ import "base" System.Environment (getArgs)
334+
335+ import qualified "obelisk-run" Obelisk.Run
331336 import qualified Frontend
332337 import qualified Backend
333338
334339 main :: IO ()
335340 main = do
336− args <- getArgs
337− let port = read $ args !! 0
338− assets = args !! 1
339− profileFile = (args !! 2) <> ".rprof"
340− Obelisk.Run.run port (Obelisk.Run.runServeAsset assets) Backend.backend Frontend.frontend `finally` writeProfilingData profileFile
341+ [portStr, assets, profFileName] <- getArgs
342+ Obelisk.Run.run (read portStr) (Obelisk.Run.runServeAsset assets) Backend.backend Frontend.frontend
343+ `finally` writeProfilingData (profFileName ++ ".rprof")
341344 '';
342345 in nixpkgs.runCommand "ob-run" {
343− buildInputs = [ (profiled.ghc.ghcWithPackages (p: [ p.backend p.frontend])) ];
346+ buildInputs = [ (profiled.ghc.ghcWithPackages (p: [p.backend p.frontend])) ];
344347 } ''
345− mkdir -p $out/bin/
346− ghc -x hs -prof -fno-prof-auto -threaded ${exeSource} -o $out/bin/ob-run
348+ cp ${exeSource} ob-run.hs
349+ mkdir -p $out/bin
350+ ghc -x hs -prof -fno-prof-auto -threaded ob-run.hs -o $out/bin/ob-run
347351 '';
348352
349353 linuxExeConfigurable = linuxExe;
dep/.gitignoredeleted−5
@@ -1,5 +0,0 @@
1−*
2−!*/
3−!.gitignore
4−!*/*.json
5−!*/default.nix
dep/reflex-platform/default.nix+1−7
@@ -1,8 +1,2 @@
11# DO NOT HAND-EDIT THIS FILE
2−let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:
3− if !fetchSubmodules && !private then builtins.fetchTarball {
4− url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256;
5− } else (import <nixpkgs> {}).fetchFromGitHub {
6− inherit owner repo rev sha256 fetchSubmodules private;
7− };
8−in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))
2+import (import ./thunk.nix)
dep/reflex-platform/github.json+3−3
@@ -1,8 +1,8 @@
11{
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
4− "branch": "release/0.5.3.0",
4+ "branch": "develop",
55 "private": false,
6− "rev": "9779ea3b4c87fdc971057c561262696d5e9b1847",
7− "sha256": "0v87ilal9355xwz8y9m0zh14pm9c0f7pqch0854kkj92ybc5l62q"
6+ "rev": "c21ec7036c8351185cd2feef9ce577b6a61c4f22",
7+ "sha256": "0m4sdpz6qvngralpyprrbvsyi2zmfh2l3q5vhihwfp3842blvbfg"
88}
dep/reflex-platform/thunk.nixnew file+9
@@ -0,0 +1,9 @@
1+# DO NOT HAND-EDIT THIS FILE
2+let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:
3+ if !fetchSubmodules && !private then builtins.fetchTarball {
4+ url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256;
5+ } else (import <nixpkgs> {}).fetchFromGitHub {
6+ inherit owner repo rev sha256 fetchSubmodules private;
7+ };
8+ json = builtins.fromJSON (builtins.readFile ./github.json);
9+in fetch json
guides/app-deploy/README.md+14−8
@@ -1,11 +1,12 @@
11# Obelisk Application Deployment Guide
2+
23This document serves two purposes, the first one is to guide new users that want to deploy Obelisk/Reflex apps and the second one to provide a walk-through for test driving Obelisk releases.
34
4−> **Note:** To complete this *entire* guide you need access to a macOS machine running the latest OSX release, a Linux machine (can be a VM on that mac) with Nix installed, an iPhone or iPad, and a recent Android device. It also assumes that you have Nix correctly set up and that your system is set up to fetch from binary caches instead of building everything on your machine. If you notice any of the commands below takes more than 5 minutes, then caches are not likely enabled.
5+> **Note:** This guide can be completed on a Linux host with Nix installed or a macOS host with Nix installed. Each section is labeled regarding which operating system hosts it supports. When going through the guide, start at the beginning and go through all sections that are supported on your host and merely skip sections that are not supported. The sections for iOS deployments require an iPhone or iPad device and the sections for Android deployments require an Android device. This guide assumes that you have Nix correctly set up and that your system is configured to fetch from binary caches instead of building everything on your machine. If you notice any of the commands below takes more than 5 minutes, then caches are not likely enabled.
56>
67> Refer to the [Obelisk Installation Documentation](https://github.com/obsidiansystems/obelisk#installing-obelisk) for instructions on configuring the binary caches for any Nix-compatible setup.
78
8−## a) Get Obelisk
9+## a) Get Obelisk (Linux, Mac)
910
1011There are several ways to install Obelisk, but the one we are going to use here allows us to test a single version of Obelisk in different ways. The approach is to clone Obelisk from it's GitHub repository and start a project that uses that local check out.
1112
@@ -39,7 +40,7 @@ ob init --branch "$OBELISK_BRANCH"
3940
4041> **Note:** If the `--branch` parameter is not used, then Obelisk sets your project up to look at the `master` branch from GitHub. This is quite handy for real life projects but distracting for our test drive since we want to make sure any changes we do to the Obelisk codebase (like testing a pull request) are immediately picked up.
4142
42−## b) Run the Obelisk app on localhost
43+## b) Run the Obelisk app on localhost (Linux, Mac)
4344
4445Let's test that we can run a server on localhost. This is quite easy:
4546
@@ -52,9 +53,10 @@ Now open a browser and point it to http://localhost:8000 (or just click on this
5253
5354![](assets/app-deploy.png)
5455
55−## c) Deploy the web app on a remote machine
56+## c) Prepare a VirtualBox deploy target (Linux, Mac)
5657
5758### Import the NixOS VirtualBox appliance
59+
5860Install [VirtualBox](https://www.virtualbox.org/) on your machine.
5961
6062On NixOS, do this by adding the following line to your `/etc/nixos/configuration.nix` before the last line containing `}`:
@@ -201,7 +203,7 @@ git remote add origin "$WORKDIR/myapp-git-remote"
201203git push -u origin master
202204```
203205
204−### Deploy
206+### Initialize deployment directory
205207
206208With that, we can come back to the Obelisk app and initialize the deployment:
207209
@@ -216,7 +218,11 @@ Then configure the deployment for VirtualBox:
216218echo '{nixosPkgs, ...}: {...}: { imports = [ (nixosPkgs.path + /nixos/modules/virtualisation/virtualbox-image.nix) ]; }' > "$WORKDIR/myappdeploy/module.nix"
217219```
218220
219−And deploy:
221+## d) Deploy to VirtualBox target (Linux only)
222+
223+> Deploying requires a Linux host in order to build the Linux binaries.
224+
225+Now we can deploy to the VirtualBox VM.
220226
221227```bash
222228cd "$WORKDIR/myappdeploy"
@@ -243,7 +249,7 @@ It should look like this:
243249
244250Congratulations! You have deployed an Obelisk application to a remote server via SSH.
245251
246−## d) Deploy an Android app (Linux host only)
252+## e) Deploy an Android app (Linux only)
247253
248254> **Note:** This section requires that you be running a Linux *host*.
249255
@@ -286,7 +292,7 @@ When the deployment is complete opening the app should look something like
286292
287293Congratulations! You have deployed an Obelisk Android app via USB.
288294
289−## e) Deploy an iOS app (macOS host only)
295+## f) Deploy an iOS app (Mac only)
290296
291297> **Note:** This section requires that you be running a macOS *host*.
292298
haskell-overlays/obelisk.nix+1
@@ -20,6 +20,7 @@ in
2020 librarySystemDepends = [
2121 pkgs.jre
2222 pkgs.nix
23+ pkgs.nix-prefetch-git
2324 (haskellLib.justStaticExecutables self.ghcid)
2425 ];
2526 };
lib/asset/manifest/obelisk-asset-manifest.cabal+1−1
@@ -24,7 +24,7 @@ library
2424 , template-haskell
2525 , text
2626 , transformers
27− , unix
27+ , unix-compat
2828 , vector
2929 exposed-modules:
3030 Obelisk.Asset.Gather
lib/asset/manifest/src/Obelisk/Asset/Symlink.hs+1−1
@@ -7,7 +7,7 @@ import Data.Map (Map)
77import qualified Data.Map as Map
88import System.Directory
99import System.FilePath.Posix
10−import System.Posix.Files
10+import System.PosixCompat.Files
1111
1212-- | Copy the given hashed files from one location to hashed paths in another
1313-- location, and symlink the unhashed paths to the hashed ones
lib/asset/serve-snap/obelisk-asset-serve-snap.cabal+1−1
@@ -23,7 +23,7 @@ library
2323 , obelisk-snap-extras
2424 , text
2525 , transformers
26− , unix
26+ , unix-compat
2727 exposed-modules:
2828 Obelisk.Asset.Accept
2929 Obelisk.Asset.Serve.Snap
lib/asset/serve-snap/src/Obelisk/Asset/Serve/Snap.hs+1−1
@@ -31,7 +31,7 @@ import Data.Text.Encoding (decodeUtf8, encodeUtf8)
3131import System.Directory (getDirectoryContents)
3232import System.FilePath ((</>), splitFileName, takeDirectory)
3333import System.IO.Error (isDoesNotExistError)
34−import System.Posix (getFileStatus, fileSize)
34+import System.PosixCompat.Files (getFileStatus, fileSize)
3535
3636-- | Serve static assets from an asset directory generated via @assets.nix@ or, failing that, from a regular directory.
3737--
lib/cliapp/obelisk-cliapp.cabal+2
@@ -19,10 +19,12 @@ library
1919 , monad-loops
2020 , mtl
2121 , process
22+ , shell-escape
2223 , terminal-size
2324 , text
2425 , time
2526 , transformers
27+ , utf8-string
2628 exposed-modules:
2729 Obelisk.CliApp
2830 other-modules:
lib/cliapp/src/Obelisk/CliApp.hs+3
@@ -39,7 +39,9 @@ module Obelisk.CliApp
3939 , callCommand
4040 , callProcess
4141 , callProcessAndLogOutput
42+ , createProcess
4243 , createProcess_
44+ , exitCodeToException
4345 , overCreateProcess
4446 , proc
4547 , readCreateProcessWithExitCode
@@ -47,6 +49,7 @@ module Obelisk.CliApp
4749 , readProcessAndLogStderr
4850 , readProcessJSONAndLogStderr
4951 , reconstructCommand
52+ , runProcess_
5053 , setCwd
5154 , setDelegateCtlc
5255 , setEnvOverride
lib/cliapp/src/Obelisk/CliApp/Process.hs+47−27
@@ -17,6 +17,7 @@ module Obelisk.CliApp.Process
1717 , callProcessAndLogOutput
1818 , createProcess
1919 , createProcess_
20+ , exitCodeToException
2021 , overCreateProcess
2122 , proc
2223 , readCreateProcessWithExitCode
@@ -24,6 +25,7 @@ module Obelisk.CliApp.Process
2425 , readProcessAndLogStderr
2526 , readProcessJSONAndLogStderr
2627 , reconstructCommand
28+ , runProcess_
2729 , setCwd
2830 , setDelegateCtlc
2931 , setEnvOverride
@@ -32,12 +34,14 @@ module Obelisk.CliApp.Process
3234 ) where
3335
3436import Control.Monad ((<=<), join, void)
37+import Control.Monad.Catch (MonadMask, bracketOnError)
3538import Control.Monad.Except (throwError)
3639import Control.Monad.Fail
3740import Control.Monad.IO.Class (MonadIO, liftIO)
3841import Control.Lens (Prism', review)
3942import qualified Data.ByteString as BS
4043import qualified Data.ByteString.Char8 as BSC
44+import qualified Data.ByteString.UTF8 as BSU
4145import Data.Function (fix)
4246import Data.Map (Map)
4347import qualified Data.Map as Map
@@ -54,6 +58,7 @@ import qualified System.IO.Streams as Streams
5458import System.IO.Streams.Concurrent (concurrentMerge)
5559import System.Process (CreateProcess, ProcessHandle, StdStream (CreatePipe), std_err, std_out)
5660import qualified System.Process as Process
61+import Text.ShellEscape (bash, bytes)
5762import qualified Data.Aeson as Aeson
5863
5964import Control.Monad.Log (Severity (..))
@@ -97,7 +102,7 @@ instance AsProcessFailure ProcessFailure where
97102 asProcessFailure = id
98103
99104readProcessAndLogStderr
100− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
105+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m, MonadMask m)
101106 => Severity -> ProcessSpec -> m Text
102107readProcessAndLogStderr sev process = do
103108 (out, _err) <- withProcess process $ \_out err -> do
@@ -105,7 +110,7 @@ readProcessAndLogStderr sev process = do
105110 liftIO $ T.decodeUtf8With lenientDecode <$> BS.hGetContents out
106111
107112readProcessJSONAndLogStderr
108− :: (Aeson.FromJSON a, MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
113+ :: (Aeson.FromJSON a, MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m, MonadMask m)
109114 => Severity -> ProcessSpec -> m a
110115readProcessJSONAndLogStderr sev process = do
111116 (out, _err) <- withProcess process $ \_out err -> do
@@ -128,10 +133,10 @@ readCreateProcessWithExitCode procSpec = do
128133-- | Like `System.Process.readProcess` but logs the combined output (stdout and stderr)
129134-- with the corresponding severity.
130135--
131−-- Usually this function is called as `callProcessAndLogOutput (Debug, Error)`. However
136+-- Usually this function is called as `readProcessAndLogOutput (Debug, Error)`. However
132137-- some processes are known to spit out diagnostic or informative messages in stderr, in
133138-- which case it is advisable to call it with a non-Error severity for stderr, like
134−-- `callProcessAndLogOutput (Debug, Debug)`.
139+-- `readProcessAndLogOutput (Debug, Debug)`.
135140readProcessAndLogOutput
136141 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
137142 => (Severity, Severity) -> ProcessSpec -> m Text
@@ -144,9 +149,7 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
144149 outText <- liftIO $ T.decodeUtf8With lenientDecode <$> BS.hGetContents out
145150 putLogRaw sev_out outText
146151
147− waitForProcess p >>= \case
148− ExitSuccess -> pure outText
149− ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess process) code
152+ outText <$ (exitCodeToException process =<< waitForProcess p)
150153
151154-- | Like 'System.Process.callProcess' but logs the combined output (stdout and stderr)
152155-- with the corresponding severity.
@@ -156,7 +159,7 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
156159-- which case it is advisable to call it with a non-Error severity for stderr, like
157160-- `callProcessAndLogOutput (Debug, Debug)`.
158161callProcessAndLogOutput
159− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
162+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m, MonadMask m)
160163 => (Severity, Severity) -> ProcessSpec -> m ()
161164callProcessAndLogOutput (sev_out, sev_err) process =
162165 void $ withProcess process $ \out err -> do
@@ -186,12 +189,11 @@ createProcess_ name procSpec = do
186189 liftIO $ Process.createProcess_ name p
187190
188191mkCreateProcess :: MonadIO m => ProcessSpec -> m Process.CreateProcess
189−mkCreateProcess (ProcessSpec p override') = do
190− case override' of
191− Nothing -> pure p
192− Just override -> do
193− procEnv <- Map.fromList <$> maybe (liftIO getEnvironment) pure (Process.env p)
194− pure $ p { Process.env = Just $ Map.toAscList (override procEnv) }
192+mkCreateProcess (ProcessSpec p override') = case override' of
193+ Nothing -> pure p
194+ Just override -> do
195+ procEnv <- Map.fromList <$> maybe (liftIO getEnvironment) pure (Process.env p)
196+ pure $ p { Process.env = Just $ Map.toAscList (override procEnv) }
195197
196198-- | Like `System.Process.callProcess` but also logs (debug) the process being run
197199callProcess
@@ -210,18 +212,30 @@ callCommand cmd = do
210212 liftIO $ Process.callCommand cmd
211213
212214withProcess
213− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
215+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m, MonadMask m)
214216 => ProcessSpec -> (Handle -> Handle -> m ()) -> m (Handle, Handle)
215−withProcess process f = do -- TODO: Use bracket.
216− -- FIXME: Using `withCreateProcess` here leads to something operating illegally on closed handles.
217− (_, Just out, Just err, p) <- createProcess $ overCreateProcess
218− (\x -> x { std_out = CreatePipe , std_err = CreatePipe }) process
219−
220− f out err -- Pass the handles to the passed function
221−
222− waitForProcess p >>= \case
223− ExitSuccess -> return (out, err)
224− ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess process) code
217+withProcess process f =
218+ bracketOnError
219+ (createProcess $ overCreateProcess
220+ (\x -> x { std_out = CreatePipe , std_err = CreatePipe }) process
221+ )
222+ (liftIO . Process.cleanupProcess)
223+ (\case
224+ (_, Just out, Just err, p) -> do
225+ f out err
226+ (out, err) <$ (exitCodeToException process =<< waitForProcess p)
227+ _ -> error "withProcess: createProcess did not provide handles for CreatePipe as expected"
228+ )
229+
230+-- | Runs a process to completion failing if it does not exit cleanly.
231+runProcess_
232+ :: (MonadIO m, CliLog m, CliThrow e m, MonadMask m, AsProcessFailure e, MonadFail m)
233+ => ProcessSpec -> m ()
234+runProcess_ process =
235+ bracketOnError
236+ (createProcess process)
237+ (liftIO . Process.cleanupProcess)
238+ (\(_, _, _, ph) -> exitCodeToException process =<< waitForProcess ph)
225239
226240-- Create an input stream from the file handle, associating each item with the given severity.
227241streamHandle :: Severity -> Handle -> IO (InputStream (Severity, BSC.ByteString))
@@ -240,14 +254,20 @@ streamToLog stream = fix $ \loop -> do
240254waitForProcess :: MonadIO m => ProcessHandle -> m ExitCode
241255waitForProcess = liftIO . Process.waitForProcess
242256
257+-- | Converts an 'ExitCode' to an exception when it's non-zero.
258+exitCodeToException :: (CliThrow e m, AsProcessFailure e) => ProcessSpec -> ExitCode -> m ()
259+exitCodeToException spec = \case
260+ ExitSuccess -> pure ()
261+ ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess spec) code
262+
243263-- | Pretty print a 'CmdSpec'
244264reconstructCommand :: Process.CmdSpec -> Text
245265reconstructCommand p = case p of
246266 Process.ShellCommand str -> T.pack str
247267 Process.RawCommand c as -> processToShellString c as
248268 where
249− processToShellString cmd args = T.unwords $ map quoteAndEscape (cmd : args)
250− quoteAndEscape x = "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
269+ processToShellString cmd args = T.pack $ unwords $
270+ map (BSU.toString . bytes . bash . BSU.fromString) (cmd : args)
251271
252272reconstructProcSpec :: ProcessSpec -> Text
253273reconstructProcSpec = reconstructCommand . Process.cmdspec . _processSpec_createProcess
lib/command/obelisk-command.cabal+3−1
@@ -39,13 +39,15 @@ library
3939 , optparse-applicative
4040 , placeholders
4141 , process
42+ , shell-escape
4243 , temporary
4344 , terminal-size
4445 , text
4546 , time
4647 , transformers
47− , unix
48+ , unix-compat
4849 , unordered-containers
50+ , utf8-string
4951 , which
5052 , yaml
5153 exposed-modules:
lib/command/src/Obelisk/Command.hs+12−8
@@ -22,7 +22,7 @@ import System.Environment
2222import System.FilePath
2323import qualified System.Info
2424import System.IO (hIsTerminalDevice, stdout)
25−import System.Posix.Process (executeFile)
25+import System.Process (rawSystem)
2626
2727import Obelisk.App
2828import Obelisk.CliApp
@@ -114,7 +114,7 @@ obCommand cfg = hsubparser
114114 , command "thunk" $ info (ObCommand_Thunk <$> thunkOption) $ progDesc "Manipulate thunk directories"
115115 , command "repl" $ info (ObCommand_Repl <$> interpretOpts) $ progDesc "Open an interactive interpreter"
116116 , command "watch" $ info (ObCommand_Watch <$> interpretOpts) $ progDesc "Watch current project for errors and warnings"
117− , command "shell" $ info (ObCommand_Shell <$> shellOpts) $ progDesc "Enter a shell with project dependencies"
117+ , command "shell" $ info (ObCommand_Shell <$> shellOpts) $ progDesc "Enter a shell with project dependencies or run a command in such a shell. E.g. ob shell -- ghc-pkg list"
118118 , command "doc" $ info (ObCommand_Doc <$> shellFlags <*> packageNames) $
119119 progDesc "List paths to haddock documentation for specified packages"
120120 <> footerDoc (Just $
@@ -285,7 +285,10 @@ shellOpts :: Parser ShellOpts
285285shellOpts = ShellOpts
286286 <$> shellFlags
287287 <*> interpretOpts
288− <*> optional (strArgument (metavar "COMMAND"))
288+ -- This funny construction is used to support optparse-applicative's @--@ parsing.
289+ -- All arguments after @--@ are left unparsed and instead provided to the last positional parser
290+ -- which must therefore be 'many' in order to consume the rest of the input.
291+ <*> ((\xs -> if null xs then Nothing else Just $ unwords xs) <$> many (strArgument (metavar "COMMAND")))
289292
290293portOpt :: Int -> Parser Int
291294portOpt dfault = option auto (long "port" <> short 'p' <> help "Port number for server" <> showDefault <> value dfault <> metavar "INT")
@@ -363,7 +366,8 @@ main' argsCfg = do
363366 Just impl -> do
364367 -- Invoke the real implementation, using --no-handoff to prevent infinite recursion
365368 putLog Debug $ "Handing off to " <> T.pack impl
366− liftIO $ executeFile impl False ("--no-handoff" : myArgs) Nothing
369+ _ <- liftIO $ rawSystem impl ("--no-handoff" : myArgs)
370+ return ()
367371 case myArgs of
368372 "--no-handoff" : as -> go as -- If we've been told not to hand off, don't hand off
369373 origPath:inPath:outPath:preprocessorName:packagePaths
@@ -398,11 +402,11 @@ ob = \case
398402 ObCommand_Repl interpretPathsList -> withInterpretPaths interpretPathsList runRepl
399403 ObCommand_Watch interpretPathsList -> withInterpretPaths interpretPathsList runWatch
400404 ObCommand_Shell (ShellOpts shellAttr interpretPathsList cmd) -> withInterpretPaths interpretPathsList $ \root interpretPaths ->
401− nixShellForInterpretPaths False shellAttr root interpretPaths cmd
402− ObCommand_Doc shellAttr pkgs -> withProjectRoot "." $ \root ->
403− nixShellForInterpretPaths True shellAttr root emptyPathTree $ Just $ haddockCommand pkgs
405+ nixShellForInterpretPaths False shellAttr root interpretPaths cmd -- N.B. We do NOT bash escape here; we want to run the command as-is
406+ ObCommand_Doc shellAttr pkgs -> withInterpretPaths [] $ \root interpretPaths ->
407+ nixShellForInterpretPaths True shellAttr root interpretPaths $ Just $ haddockCommand pkgs
404408 ObCommand_Hoogle shell' port -> withProjectRoot "." $ \root -> do
405− nixShellWithHoogle root True shell' $ Just $ "hoogle server -p " <> show port <> " --local"
409+ nixShellWithHoogle root True shell' $ Just $ "hoogle server -p" <> show port <> " --local"
406410 ObCommand_Internal icmd -> case icmd of
407411 ObInternal_ApplyPackages origPath inPath outPath packagePaths -> do
408412 liftIO $ Preprocessor.applyPackages origPath inPath outPath packagePaths
lib/command/src/Obelisk/Command/Deploy.hs+1−1
@@ -25,7 +25,7 @@ import GHC.Generics
2525import System.Directory
2626import System.FilePath
2727import System.IO
28−import System.Posix.Files
28+import System.PosixCompat.Files
2929import Text.URI (URI)
3030import qualified Text.URI as URI
3131import Text.URI.Lens
lib/command/src/Obelisk/Command/Project.hs+16−11
@@ -15,6 +15,7 @@ module Obelisk.Command.Project
1515 , toImplDir
1616 , toObeliskDir
1717 , withProjectRoot
18+ , bashEscape
1819 ) where
1920
2021import Control.Concurrent.MVar (MVar, newMVar, withMVarMasked)
@@ -23,6 +24,7 @@ import Control.Monad
2324import Control.Monad.IO.Class (liftIO)
2425import Control.Monad.State
2526import qualified Data.Aeson as Json
27+import qualified Data.ByteString.UTF8 as BSU
2628import Data.Bits
2729import qualified Data.ByteString.Lazy as BSL
2830import Data.Default (def)
@@ -30,15 +32,17 @@ import Data.Function ((&), on)
3032import Data.Map (Map)
3133import Data.Text (Text)
3234import qualified Data.Text as T
33−import Data.Text.Encoding (decodeUtf8)
35+import Data.Text.Encoding (decodeUtf8, encodeUtf8)
3436import Data.Traversable (for)
3537import System.Directory
3638import System.Environment (lookupEnv)
3739import System.FilePath
3840import System.IO.Temp
3941import System.IO.Unsafe (unsafePerformIO)
40−import System.Posix (FileStatus, FileMode, CMode (..), UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
41−import System.Posix.Files
42+import System.PosixCompat.Files
43+import System.PosixCompat.Types
44+import System.PosixCompat.User
45+import Text.ShellEscape (bash, bytes)
4246
4347import GitHub.Data.GitData (Branch)
4448import GitHub.Data.Name (Name)
@@ -278,12 +282,15 @@ nixShellRunConfig root isPure command = do
278282 pure $ def
279283 & nixShellConfig_pure .~ isPure
280284 & nixShellConfig_common . nixCmdConfig_target .~ (def & target_path .~ Nothing)
281− & nixShellConfig_run .~ (command <&> \c -> mconcat
282− [ "export NIX_PATH=nixpkgs=", T.unpack nixpkgsPath, "; "
283− , maybe "" (\v -> "export NIX_REMOTE=" <> v <> "; ") nixRemote
284− , c
285+ & nixShellConfig_run .~ (command <&> \cs -> unwords $ concat
286+ [ ["export", BSU.toString . bytes . bash $ "NIX_PATH=nixpkgs=" <> encodeUtf8 nixpkgsPath, ";"]
287+ , maybe [] (\v -> ["export", BSU.toString . bytes . bash $ "NIX_REMOTE=" <> encodeUtf8 (T.pack v), ";"]) nixRemote
288+ , [cs]
285289 ])
286290
291+bashEscape :: String -> String
292+bashEscape = BSU.toString . bytes . bash . BSU.fromString
293+
287294nixShellRunProc :: NixShellConfig -> ProcessSpec
288295nixShellRunProc cfg = setDelegateCtlc True $ proc "nix-shell" $ runNixShellConfig cfg
289296
@@ -300,7 +307,7 @@ nixShellWithoutPkgs root isPure chdirToRoot packageNamesAndPaths shellAttr comma
300307 packageNamesAndAbsPaths <- liftIO $ for packageNamesAndPaths makeAbsolute
301308 defShellConfig <- nixShellRunConfig root isPure command
302309 let setCwd_ = if chdirToRoot then setCwd (Just root) else id
303− (_, _, _, ph) <- createProcess_ "nixShellWithoutPkgs" $ setCwd_ $ nixShellRunProc $ defShellConfig
310+ runProcess_ $ setCwd_ $ nixShellRunProc $ defShellConfig
304311 & nixShellConfig_common . nixCmdConfig_target . target_expr ?~
305312 "{root, pkgs, shell}: ((import root {}).passthru.__unstable__.self.extend (_: _: {\
306313 \shellPackages = builtins.fromJSON pkgs;\
@@ -310,18 +317,16 @@ nixShellWithoutPkgs root isPure chdirToRoot packageNamesAndPaths shellAttr comma
310317 , strArg "pkgs" (T.unpack $ decodeUtf8 $ BSL.toStrict $ Json.encode packageNamesAndAbsPaths)
311318 , strArg "shell" shellAttr
312319 ]
313− void $ waitForProcess ph
314320
315321nixShellWithHoogle :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
316322nixShellWithHoogle root isPure shell' command = do
317323 defShellConfig <- nixShellRunConfig root isPure command
318− (_, _, _, ph) <- createProcess_ "nixShellWithHoogle" $ setCwd (Just root) $ nixShellRunProc $ defShellConfig
324+ runProcess_ $ setCwd (Just root) $ nixShellRunProc $ defShellConfig
319325 & nixShellConfig_common . nixCmdConfig_target . target_expr ?~
320326 "{shell}: ((import ./. {}).passthru.__unstable__.self.extend (_: super: {\
321327 \userSettings = super.userSettings // { withHoogle = true; };\
322328 \})).project.shells.${shell}"
323329 & nixShellConfig_common . nixCmdConfig_args .~ [ strArg "shell" shell' ]
324− void $ waitForProcess ph
325330
326331findProjectAssets :: MonadObelisk m => FilePath -> m Text
327332findProjectAssets root = do
lib/command/src/Obelisk/Command/Run.hs+60−50
@@ -14,7 +14,7 @@ module Obelisk.Command.Run where
1414import Control.Arrow ((&&&))
1515import Control.Exception (Exception, bracket)
1616import Control.Lens (ifor, (.~), (&))
17−import Control.Monad (filterM, unless, void)
17+import Control.Monad (filterM, unless)
1818import Control.Monad.Except (runExceptT, throwError)
1919import Control.Monad.IO.Class (liftIO)
2020import Control.Monad.Reader (MonadIO)
@@ -25,12 +25,14 @@ import Data.Default (def)
2525import Data.Either (partitionEithers)
2626import Data.Foldable (fold, for_, toList)
2727import Data.Functor.Identity (runIdentity)
28+import Data.List (intercalate)
2829import Data.List.NonEmpty (NonEmpty)
2930import qualified Data.List.NonEmpty as NE
3031import Data.Map (Map)
3132import qualified Data.Map as Map
3233import qualified Data.Map.Monoidal as MMap
33−import Data.Maybe
34+import Data.Maybe (catMaybes, fromMaybe, mapMaybe)
35+import Data.Ord (comparing)
3436import Data.Set (Set)
3537import qualified Data.Set as Set
3638import Data.String.Here.Interpolated (i)
@@ -44,17 +46,17 @@ import Distribution.Compiler (CompilerFlavor(..))
4446import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
4547import Distribution.Parsec.ParseResult (runParseResult)
4648import qualified Distribution.System as Dist
47−import Distribution.Types.BuildInfo
48−import Distribution.Types.CondTree
49−import Distribution.Types.GenericPackageDescription
50−import Distribution.Types.Library
51−import Distribution.Utils.Generic
49+import Distribution.Types.BuildInfo (buildable, defaultExtensions, defaultLanguage, hsSourceDirs, options)
50+import Distribution.Types.CondTree (simplifyCondTree)
51+import Distribution.Types.GenericPackageDescription (ConfVar (Arch, Impl, OS), condLibrary)
52+import Distribution.Types.Library (libBuildInfo)
53+import Distribution.Utils.Generic (toUTF8BS, readUTF8File)
5254import qualified Distribution.Parsec.Common as Dist
5355import qualified Hpack.Config as Hpack
5456import qualified Hpack.Render as Hpack
5557import qualified Hpack.Yaml as Hpack
56−import Language.Haskell.Extension
57−import Network.Socket hiding (Debug)
58+import Language.Haskell.Extension (Extension, Language)
59+import qualified Network.Socket as Socket
5860import System.Directory
5961import System.Environment (getExecutablePath)
6062import System.FilePath
@@ -64,19 +66,18 @@ import System.IO.Temp (withSystemTempDirectory)
6466import Obelisk.App (MonadObelisk)
6567import Obelisk.CliApp (
6668 Severity (..),
67− createProcess_,
6869 failWith,
6970 proc,
7071 putLog,
7172 readCreateProcessWithExitCode,
7273 readProcessAndLogStderr,
74+ runProcess_,
7375 setCwd,
7476 setDelegateCtlc,
75− waitForProcess,
7677 withSpinner,
7778 )
7879import Obelisk.Command.Nix
79−import Obelisk.Command.Project (nixShellWithoutPkgs, withProjectRoot, findProjectAssets)
80+import Obelisk.Command.Project (nixShellWithoutPkgs, withProjectRoot, findProjectAssets, bashEscape)
8081import Obelisk.Command.Thunk (attrCacheFileName)
8182import Obelisk.Command.Utils (findExePath, ghcidExePath)
8283
@@ -109,9 +110,6 @@ data PathTree a = PathTree_Node
109110 (Map FilePath (PathTree a)) -- Branches to deeper leaves
110111 deriving (Eq, Ord, Show, Functor, Foldable, Traversable)
111112
112−emptyPathTree :: PathTree a
113−emptyPathTree = PathTree_Node Nothing mempty
114−
115113-- | 2D ASCII drawing of a 'PathTree'. Adapted from Data.Tree.draw.
116114drawPathTree :: (a -> Text) -> PathTree a -> Text
117115drawPathTree showA (PathTree_Node _ ts0) = T.intercalate "\n" $ goForest (Map.toList ts0)
@@ -151,7 +149,7 @@ profile profileBasePattern rtsFlags = withProjectRoot "." $ \root -> do
151149 liftIO $ createDirectoryIfMissing True $ takeDirectory $ root </> profileBaseName
152150 putLog Debug $ "Storing profiled data under base name of " <> T.pack (root </> profileBaseName)
153151 freePort <- getFreePort
154− (_, _, _, ph) <- createProcess_ "runProfExe" $ setCwd (Just root) $ setDelegateCtlc True $ proc (outPath </> "bin" </> "ob-run") $
152+ runProcess_ $ setCwd (Just root) $ setDelegateCtlc True $ proc (outPath </> "bin" </> "ob-run") $
155153 [ show freePort
156154 , T.unpack assets
157155 , profileBaseName
@@ -159,17 +157,16 @@ profile profileBasePattern rtsFlags = withProjectRoot "." $ \root -> do
159157 , "-po" <> profileBaseName
160158 ] <> rtsFlags
161159 <> [ "-RTS" ]
162− void $ waitForProcess ph
163160
164161run :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
165162run root interpretPaths = do
166163 pkgs <- getParsedLocalPkgs root interpretPaths
167− withGhciScript pkgs $ \dotGhciPath -> do
168− assets <- findProjectAssets root
169− putLog Debug $ "Assets impurely loaded from: " <> assets
170− ghciArgs <- getGhciSessionSettings pkgs root True
171− freePort <- getFreePort
172− runGhcid root True (ghciArgs <> mkGhciScriptArg dotGhciPath) pkgs $ Just $ unwords
164+ assets <- findProjectAssets root
165+ putLog Debug $ "Assets impurely loaded from: " <> assets
166+ ghciArgs <- getGhciSessionSettings pkgs root True
167+ freePort <- getFreePort
168+ withGhciScriptArgs pkgs $ \dotGhciArgs -> do
169+ runGhcid root True (ghciArgs <> dotGhciArgs) pkgs $ Just $ unwords
173170 [ "Obelisk.Run.run"
174171 , show freePort
175172 , "(Obelisk.Run.runServeAsset " ++ show assets ++ ")"
@@ -181,15 +178,15 @@ runRepl :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
181178runRepl root interpretPaths = do
182179 pkgs <- getParsedLocalPkgs root interpretPaths
183180 ghciArgs <- getGhciSessionSettings pkgs "." True
184− withGhciScript pkgs $ \dotGhciPath ->
185− runGhciRepl root pkgs (ghciArgs <> mkGhciScriptArg dotGhciPath)
181+ withGhciScriptArgs pkgs $ \dotGhciArgs ->
182+ runGhciRepl root pkgs (ghciArgs <> dotGhciArgs)
186183
187184runWatch :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
188185runWatch root interpretPaths = do
189186 pkgs <- getParsedLocalPkgs root interpretPaths
190187 ghciArgs <- getGhciSessionSettings pkgs root True
191− withGhciScript pkgs $ \dotGhciPath ->
192− runGhcid root True (ghciArgs <> mkGhciScriptArg dotGhciPath) pkgs Nothing
188+ withGhciScriptArgs pkgs $ \dotGhciArgs ->
189+ runGhcid root True (ghciArgs <> dotGhciArgs) pkgs Nothing
193190
194191exportGhciConfig :: MonadObelisk m => Bool -> FilePath -> PathTree Interpret -> m [String]
195192exportGhciConfig useRelativePaths root interpretPaths = do
@@ -396,13 +393,29 @@ parsePackagesOrFail dirs' = do
396393packageInfoToNamePathMap :: Foldable f => f CabalPackageInfo -> Map Text FilePath
397394packageInfoToNamePathMap = Map.fromList . map (_cabalPackageInfo_packageName &&& _cabalPackageInfo_packageRoot) . toList
398395
396+-- Like 'withGhciScript' but provides the precise ghci arguments to add to a ghci session
397+withGhciScriptArgs
398+ :: (MonadObelisk m, Foldable f)
399+ => f CabalPackageInfo -- ^ List of packages to load into ghci
400+ -> ([String] -> m ()) -- ^ Action to run with the extra ghci arguments
401+ -> m ()
402+withGhciScriptArgs packageInfos f = withGhciScript loadPreludeManually packageInfos $ \fp -> f ["-XNoImplicitPrelude", "-ghci-script", fp]
403+ where
404+ -- These lines must be first and allow the session to support a custom Prelude when @-XNoImplicitPrelude@
405+ -- is passed to the ghci session.
406+ loadPreludeManually =
407+ [ ":add Prelude" -- @:add@ is used because it's less noisy when there is no custom Prelude
408+ , ":set -XImplicitPrelude" -- Turn the default setting on
409+ ]
410+
399411-- | Create ghci configuration to load the given packages
400412withGhciScript
401413 :: (MonadObelisk m, Foldable f)
402− => f CabalPackageInfo -- ^ List of packages to load into ghci
414+ => [String] -- ^ Commands to prefix to file
415+ -> f CabalPackageInfo -- ^ List of packages to load into ghci
403416 -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporary .ghci
404417 -> m ()
405−withGhciScript (toList -> packageInfos) f =
418+withGhciScript preCommands (toList -> packageInfos) f =
406419 withSystemTempDirectory "ob-ghci" $ \fp -> do
407420 let dotGhciPath = fp </> ".ghci"
408421 liftIO $ writeFile dotGhciPath dotGhci
@@ -414,7 +427,8 @@ withGhciScript (toList -> packageInfos) f =
414427 , [ "Backend" | "backend" `Set.member` packageNames ]
415428 , [ "Frontend" | "frontend" `Set.member` packageNames ]
416429 ]
417− dotGhci = unlines
430+ dotGhci = unlines $
431+ preCommands <>
418432 [ if null modulesToLoad then "" else ":load " <> unwords modulesToLoad
419433 , "import qualified Obelisk.Run"
420434 , "import qualified Frontend"
@@ -467,7 +481,7 @@ runGhciRepl root (toList -> packages) ghciArgs =
467481 -- NOTE: We do *not* want to use $(staticWhich "ghci") here because we need the
468482 -- ghc that is provided by the shell in the user's project.
469483 nixShellWithoutPkgs root True False (packageInfoToNamePathMap packages) "ghc" $
470− Just $ unwords $ "ghci" : ghciArgs -- TODO: Shell escape
484+ Just $ unwords $ fmap bashEscape $ "ghci" : ghciArgs
471485
472486-- | Run ghcid
473487runGhcid
@@ -480,32 +494,28 @@ runGhcid
480494 -> m ()
481495runGhcid root chdirToRoot ghciArgs (toList -> packages) mcmd =
482496 nixShellWithoutPkgs root True chdirToRoot (packageInfoToNamePathMap packages) "ghc" $
483− Just $ unwords $ ghcidExePath : opts -- TODO: Shell escape
497+ Just $ unwords $ fmap bashEscape $ ghcidExePath : opts
484498 where
485− opts =
486− [ "-W"
487− , "--outputfile=ghcid-output.txt"
488− ] <> map (\x -> "--reload='" <> x <> "'") reloadFiles
489− <> map (\x -> "--restart='" <> x <> "'") restartFiles
490− <> testCmd
491− <> ["--command='" <> unwords ("ghci" : ghciArgs) <> "'"] -- TODO: Shell escape
492− testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'") -- TODO: Shell escape
493−
499+ opts = concat
500+ [ ["-W"]
501+ , ["--outputfile=ghcid-output.txt"]
502+ , map (\x -> "--reload=" <> x) reloadFiles
503+ , map (\x -> "--restart=" <> x) restartFiles
504+ , maybe [] (\cmd -> ["--test=" <> cmd]) mcmd
505+ , ["--command=" <> unwords ("ghci" : ghciArgs)]
506+ ]
494507 adjustRoot x = if chdirToRoot then makeRelative root x else x
495508 reloadFiles = map adjustRoot [root </> "config"]
496509 restartFiles = map (adjustRoot . _cabalPackageInfo_packageFile) packages
497510
498−mkGhciScriptArg :: FilePath -> [String]
499−mkGhciScriptArg dotGhci = ["-ghci-script", dotGhci]
500−
501−getFreePort :: MonadIO m => m PortNumber
502−getFreePort = liftIO $ withSocketsDo $ do
503− addr:_ <- getAddrInfo (Just defaultHints) (Just "127.0.0.1") (Just "0")
504− bracket (open addr) close socketPort
511+getFreePort :: MonadIO m => m Socket.PortNumber
512+getFreePort = liftIO $ Socket.withSocketsDo $ do
513+ addr:_ <- Socket.getAddrInfo (Just Socket.defaultHints) (Just "127.0.0.1") (Just "0")
514+ bracket (open addr) Socket.close Socket.socketPort
505515 where
506516 open addr = do
507− sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
508− bind sock (addrAddress addr)
517+ sock <- Socket.socket (Socket.addrFamily addr) (Socket.addrSocketType addr) (Socket.addrProtocol addr)
518+ Socket.bind sock (Socket.addrAddress addr)
509519 return sock
510520
511521
lib/command/src/Obelisk/Command/Thunk.hs+3−3
@@ -56,7 +56,7 @@ import System.Exit
5656import System.FilePath
5757import System.IO.Error
5858import System.IO.Temp
59−import System.Posix (getSymbolicLinkStatus, modificationTime)
59+import System.PosixCompat.Files (getSymbolicLinkStatus, modificationTime)
6060import qualified Text.URI as URI
6161
6262import Obelisk.App (MonadObelisk)
@@ -178,14 +178,14 @@ getNixSha256ForUriUnpacked
178178getNixSha256ForUriUnpacked uri =
179179 withExitFailMessage ("nix-prefetch-url: Failed to determine sha256 hash of URL " <> gitUriToText uri) $ do
180180 [hash] <- fmap T.lines $ readProcessAndLogOutput (Debug, Debug) $
181− proc "nix-prefetch-url" ["--unpack", "--type", "sha256", T.unpack $ gitUriToText uri]
181+ proc nixPrefetchUrlPath ["--unpack", "--type", "sha256", T.unpack $ gitUriToText uri]
182182 pure hash
183183
184184nixPrefetchGit :: MonadObelisk m => GitUri -> Text -> Bool -> m NixSha256
185185nixPrefetchGit uri rev fetchSubmodules =
186186 withExitFailMessage ("nix-prefetch-git: Failed to determine sha256 hash of Git repo " <> gitUriToText uri <> " at " <> rev) $ do
187187 out <- readProcessAndLogStderr Debug $
188− proc "nix-prefetch-git" $ filter (/="")
188+ proc nixPrefetchGitPath $ filter (/="")
189189 [ "--url", T.unpack $ gitUriToText uri
190190 , "--rev", T.unpack rev
191191 , if fetchSubmodules then "--fetch-submodules" else ""
lib/command/src/Obelisk/Command/Utils.hs+6
@@ -54,6 +54,12 @@ nixBuildExePath = $(staticWhich "nix-build")
5454jreKeyToolPath :: FilePath
5555jreKeyToolPath = $(staticWhich "keytool")
5656
57+nixPrefetchGitPath :: FilePath
58+nixPrefetchGitPath = $(staticWhich "nix-prefetch-git")
59+
60+nixPrefetchUrlPath :: FilePath
61+nixPrefetchUrlPath = $(staticWhich "nix-prefetch-url")
62+
5763-- | Nix syntax requires relative paths to be prefixed by @./@ or
5864-- @../@. This will make a 'FilePath' that can be embedded in a Nix
5965-- expression.
lib/frontend/src/Obelisk/Frontend.hs+1−1
@@ -182,7 +182,7 @@ runFrontendWithConfigsAndCurrentRoute
182182 -> Frontend (R frontendRoute)
183183 -> JSM ()
184184runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = do
185− let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _FullRoute_Frontend . _ObeliskRoute_App))
185+ let ve = validFullEncoder . hoistParse errorLeft (reviewEncoder (rPrism $ _FullRoute_Frontend . _ObeliskRoute_App))
186186 errorLeft = \case
187187 Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
188188 Right x -> Identity x
lib/route/src/Obelisk/Route.hs+70−27
@@ -1,3 +1,4 @@
1+{-# LANGUAGE CPP #-}
12{-# LANGUAGE ApplicativeDo #-}
23{-# LANGUAGE ConstraintKinds #-}
34{-# LANGUAGE EmptyCase #-}
@@ -61,6 +62,7 @@ module Obelisk.Route
6162 , justEncoder
6263 , nothingEncoder
6364 , isoEncoder
65+ , viewEncoder
6466 , wrappedEncoder
6567 , unwrappedEncoder
6668 , listToNonEmptyEncoder
@@ -69,6 +71,7 @@ module Obelisk.Route
6971 , toListMapEncoder
7072 , shadowEncoder
7173 , prismEncoder
74+ , reviewEncoder
7275 , rPrism
7376 , _R
7477 , obeliskRouteEncoder
@@ -133,6 +136,7 @@ import Control.Lens
133136 , Prism'
134137 , prism'
135138 , re
139+ , review
136140 , view
137141 , Wrapped (..)
138142 )
@@ -221,7 +225,19 @@ hoistR f (x :=> Identity y) = f x :/ y
221225infixr 5 :.
222226type (:.) = (,)
223227
228+#ifdef __GLASGOW_HASKELL__
229+#if __GLASGOW_HASKELL__ >= 810
224230{-# COMPLETE (:.) #-}
231+#else
232+{-# WARNING (:.)
233+ [ "Use of this pattern in GHC < 8.10 will result in spurious non-exhaustive warnings at every use site."
234+ , "We cannot provide a COMPLETE pragma to silence these due to a GHC bug: https://gitlab.haskell.org/ghc/ghc/issues/17729."
235+ , "The bug hides incompleteness warnings for all two tuples when the COMPLETE pattern is in scope."
236+ , "Instead, you should use (,) directly until you can switch to GHC >= 8.10, where the COMPLETE pragma is reinstated."
237+ ]
238+ #-}
239+#endif
240+#endif
225241pattern (:.) :: a -> b -> a :. b
226242pattern a :. b = (a, b)
227243
@@ -340,7 +356,7 @@ instance Monad parse => Bifunctor (,) (EncoderImpl parse) (EncoderImpl parse) (E
340356 }
341357
342358instance (Monad parse, Applicative check) => Braided (Encoder check parse) (,) where
343− braid = isoEncoder (iso swap swap)
359+ braid = viewEncoder (iso swap swap)
344360
345361
346362instance (Applicative check, Monad parse) => PFunctor (,) (Encoder check parse) (Encoder check parse) where
@@ -376,11 +392,11 @@ instance (Monad parse, Applicative check) => Bifunctor Either (Encoder check par
376392 bimap f g = Encoder $ liftA2 bimap (unEncoder f) (unEncoder g)
377393
378394instance (Applicative check, Monad parse) => Associative (Encoder check parse) Either where
379− associate = isoEncoder (iso (associate @(->) @Either) disassociate)
380− disassociate = isoEncoder (iso disassociate associate)
395+ associate = viewEncoder (iso (associate @(->) @Either) disassociate)
396+ disassociate = viewEncoder (iso disassociate associate)
381397
382398instance (Monad parse, Applicative check) => Braided (Encoder check parse) Either where
383− braid = isoEncoder (iso swap swap)
399+ braid = viewEncoder (iso swap swap)
384400
385401
386402
@@ -434,17 +450,17 @@ instance (Applicative check, Monad parse) => Monoidal (Encoder check parse) (,)
434450--------------------------------------------------------------------------------
435451
436452-- | Given a valid 'Iso' from lens, construct an 'Encoder'
437−isoEncoder :: (Applicative check, Applicative parse) => Iso' a b -> Encoder check parse a b
438−isoEncoder f = unsafeMkEncoder $ EncoderImpl
453+viewEncoder :: (Applicative check, Applicative parse) => Iso' a b -> Encoder check parse a b
454+viewEncoder f = unsafeMkEncoder $ EncoderImpl
439455 { _encoderImpl_encode = view f
440456 , _encoderImpl_decode = pure . view (from f)
441457 }
442458
443459wrappedEncoder :: (Wrapped a, Applicative check, Applicative parse) => Encoder check parse (Unwrapped a) a
444−wrappedEncoder = isoEncoder $ from _Wrapped'
460+wrappedEncoder = viewEncoder $ from _Wrapped'
445461
446462unwrappedEncoder :: (Wrapped a, Applicative check, Applicative parse) => Encoder check parse a (Unwrapped a)
447−unwrappedEncoder = isoEncoder _Wrapped'
463+unwrappedEncoder = viewEncoder $ _Wrapped'
448464
449465maybeToEitherEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Maybe a) (Either () a)
450466maybeToEitherEncoder = unsafeMkEncoder $ EncoderImpl
@@ -469,11 +485,11 @@ maybeEncoder f g = shadowEncoder f g . maybeToEitherEncoder
469485
470486-- | Encode a value by simply applying 'Just'
471487justEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a (Maybe a)
472−justEncoder = prismEncoder _Just
488+justEncoder = reviewEncoder _Just
473489
474490-- | Encode () to 'Nothing'.
475491nothingEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse () (Maybe a)
476−nothingEncoder = prismEncoder _Nothing
492+nothingEncoder = reviewEncoder _Nothing
477493
478494someConstEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Const a)) a
479495someConstEncoder = unsafeMkEncoder $ EncoderImpl
@@ -827,12 +843,19 @@ _R variant = dSumGEqPrism variant . iso runIdentity Identity
827843
828844-- | An encoder that only works on the items available via the prism. An error will be thrown in the parse monad
829845-- if the prism doesn't match.
830−prismEncoder :: (Applicative check, MonadError Text parse) => Prism' b a -> Encoder check parse a b
831−prismEncoder p = unsafeMkEncoder $ EncoderImpl
832− { _encoderImpl_encode = (^. re p)
846+--
847+-- Note that a 'Prism' from @a@ to @b@ will produce an 'Encoder' from @b@ to @a@
848+-- (i.e. 'reviewEncoder' is a contravariant functor from the category of prisms to the category of encoders),
849+-- just like 'review' produces a function @b -> a@. This is because 'Prism's extract values, in a way that might
850+-- fail, in their forward direction and inject values, in a way that cannot fail, in their reverse direction;
851+-- whereas 'Encoder's encode, which cannot fail, in their forward direction, and decode, which can fail, in their
852+-- reverse direction. In short @reviewEncoder (f . g) = reviewEncoder g . reviewEncoder f@.
853+reviewEncoder :: (Applicative check, MonadError Text parse) => Prism' b a -> Encoder check parse a b
854+reviewEncoder p = unsafeMkEncoder $ EncoderImpl
855+ { _encoderImpl_encode = review p
833856 , _encoderImpl_decode = \r -> case r ^? p of
834857 Just a -> pure a
835− Nothing -> throwError "prismEncoder: value is not present in the prism"
858+ Nothing -> throwError "reviewEncoder: value is not present in the prism"
836859 }
837860
838861-- | A URL path and query string, in which trailing slashes don't matter in the path
@@ -1028,17 +1051,6 @@ void1Encoder = Encoder $ pure $ EncoderImpl
10281051instance GShow Void1 where
10291052 gshowsPrec _ = \case {}
10301053
1031−concat <$> mapM deriveRouteComponent
1032− [ ''ResourceRoute
1033− , ''JSaddleWarpRoute
1034− , ''IndexOnlyRoute
1035− ]
1036−
1037−makePrisms ''ObeliskRoute
1038−makePrisms ''FullRoute
1039−deriveGEq ''Void1
1040−deriveGCompare ''Void1
1041−
10421054-- | Given a backend route and a checked route encoder, render the route (path
10431055-- and query string). See 'checkEncoder' for how to produce a checked encoder.
10441056renderBackendRoute
@@ -1071,11 +1083,11 @@ readShowEncoder :: (MonadError Text parse, Read a, Show a, Applicative check) =>
10711083readShowEncoder = singlePathSegmentEncoder . unsafeTshowEncoder
10721084
10731085integralEncoder :: (MonadError Text parse, Applicative check, Integral a) => Encoder check parse a Integer
1074−integralEncoder = prismEncoder (Numeric.Lens.integral)
1086+integralEncoder = reviewEncoder (Numeric.Lens.integral)
10751087
10761088pathSegmentEncoder :: (MonadError Text parse, Applicative check, Cons as as a a) =>
10771089 Encoder check parse (a, (as, b)) (as, b)
1078−pathSegmentEncoder = first (prismEncoder _Cons) . disassociate
1090+pathSegmentEncoder = first (reviewEncoder _Cons) . disassociate
10791091
10801092newtype Decoder check parse b a = Decoder { toEncoder :: Encoder check parse a b }
10811093
@@ -1182,3 +1194,34 @@ byteStringsToPageName p q =
11821194 in decode pageNameEncoder' (T.unpack (T.decodeUtf8 p), T.unpack (T.decodeUtf8 q))
11831195
11841196--TODO: decodeURIComponent as appropriate
1197+
1198+
1199+{-# DEPRECATED isoEncoder "Instead of 'isoEncoder f', use 'viewEncoder f'" #-}
1200+-- | Given a valid 'Iso' from lens, construct an 'Encoder'
1201+isoEncoder :: (Applicative check, Applicative parse) => Iso' a b -> Encoder check parse a b
1202+isoEncoder = viewEncoder
1203+
1204+{-# DEPRECATED prismEncoder "Instead of 'prismEncoder f', use 'reviewEncoder f'" #-}
1205+-- | An encoder that only works on the items available via the prism. An error will be thrown in the parse monad
1206+-- if the prism doesn't match.
1207+--
1208+-- Note that a 'Prism' from @a@ to @b@ will produce an 'Encoder' from @b@ to @a@
1209+-- (i.e. 'prismEncoder' is a contravariant functor from the category of prisms to the category of encoders),
1210+-- just like 'review' produces a function @b -> a@. This is because 'Prism's extract values, in a way that might
1211+-- fail, in their forward direction and inject values, in a way that cannot fail, in their reverse direction;
1212+-- whereas 'Encoder's encode, which cannot fail, in their forward direction, and decode, which can fail, in their
1213+-- reverse direction. In short @prismEncoder (f . g) = prismEncoder g . prismEncoder f@.
1214+prismEncoder :: (Applicative check, MonadError Text parse) => Prism' b a -> Encoder check parse a b
1215+prismEncoder = reviewEncoder
1216+
1217+
1218+concat <$> mapM deriveRouteComponent
1219+ [ ''ResourceRoute
1220+ , ''JSaddleWarpRoute
1221+ , ''IndexOnlyRoute
1222+ ]
1223+
1224+makePrisms ''ObeliskRoute
1225+makePrisms ''FullRoute
1226+deriveGEq ''Void1
1227+deriveGCompare ''Void1
lib/selftest/src/Obelisk/SelfTest.hs+24
@@ -19,6 +19,7 @@ import Data.Bool (bool)
1919import qualified Data.ByteString.Lazy as LBS
2020import Data.Foldable (for_)
2121import Data.Function (fix)
22+import Data.List (isInfixOf)
2223import qualified Data.Map as Map
2324import Data.Semigroup ((<>))
2425import qualified Data.Set as Set
@@ -205,6 +206,22 @@ main' isVerbose httpManager obeliskRepoReadOnly = withInitCache $ \initCache ->
205206 it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir' Nothing httpManager
206207 it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir' (Just "frontend") httpManager
207208
209+ describe "ob repl" $ do
210+ it "accepts stdin commands" $ inTmpObInit $ \_ -> do
211+ setStdin "print 3\n:q"
212+ output <- runOb ["repl"]
213+ liftIO $ assertBool "" $
214+ [ "*Obelisk.Run Obelisk.Run Frontend Backend> 3"
215+ , "*Obelisk.Run Obelisk.Run Frontend Backend> Leaving GHCi."
216+ ] `isInfixOf` T.lines (T.strip output)
217+ it "works with custom Prelude" $ inTmpObInit $ \_ -> do
218+ writefile "common/src/Prelude.hs"
219+ "{-# LANGUAGE PackageImports #-} module Prelude (module X) where import \"base\" Prelude as X"
220+ setStdin ":q"
221+ output <- runOb ["repl"]
222+ liftIO $ assertBool "" $
223+ "*Obelisk.Run Obelisk.Run Frontend Backend> Leaving GHCi." `T.isInfixOf` output
224+
208225 describe "obelisk project" $ parallel $ do
209226 it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskRepoReadOnly]
210227 it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", toTextIgnore obeliskRepoReadOnly]
@@ -243,6 +260,8 @@ main' isVerbose httpManager obeliskRepoReadOnly = withInitCache $ \initCache ->
243260 uu <- update
244261 assertRevEQ u uu
245262
263+ it "can run 'ob doc'" $ inTmpObInit $ \_ -> runOb_ ["doc", "reflex"]
264+
246265 describe "ob thunk pack/unpack" $ parallel $ do
247266 it "has thunk pack and unpack inverses" $ inTmpObInitWithImplCopy $ \_ -> do
248267
@@ -304,6 +323,11 @@ main' isVerbose httpManager obeliskRepoReadOnly = withInitCache $ \initCache ->
304323 void $ errExit False $ runOb ["thunk", "update", toTextIgnore dir, "--branch", "dumble-palooza"]
305324 checkDir dir >>= liftIO . assertEqual "" startingContents
306325
326+ describe "ob shell" $ parallel $ do
327+ it "works with --" $ inTmpObInit $ \_ -> do
328+ output <- runOb ["shell", "--", "ghc-pkg", "list"]
329+ liftIO $ assertBool "Unexpected output from ob shell" $ ("Cabal-" `T.isInfixOf` output) && ("ghc-" `T.isInfixOf` output)
330+
307331 describe "ob hoogle" $ {- NOT parallel -} do
308332 it "starts a hoogle server on the given port" $ inTmpObInit $ \_ -> do
309333 [p0] <- liftIO $ getFreePorts 1
lib/snap-extras/obelisk-snap-extras.cabal−1
@@ -12,7 +12,6 @@ library
1212 , base
1313 , bytestring
1414 , snap-core
15− , unix
1615 , directory
1716 , filepath
1817 , text
skeleton/frontend/frontend.cabal+4−2
@@ -28,7 +28,9 @@ executable frontend
2828 , reflex-dom
2929 , obelisk-generated-static
3030 , frontend
31− --TODO: Make these ghc-options optional
3231 ghc-options: -threaded
32+ if impl(ghcjs)
33+ ghc-options: -dedupe
34+ cpp-options: -DGHCJS_BROWSER
3335 if os(darwin)
34− ghc-options: -dynamic
36+ ghc-options: -dynamic