on master help sign in
Merge pull request #626 from obsidiansystems/qa

QA

commitc4e2cebb9b5bbe343766ca01a459de46c08581e7 authorElliot Cameron <3noch@users.noreply.github.com> dateWed 5 Feb 2020, 14:59 (7 years ago) parentd349be35d8b781
29 files changed+875−525
ChangeLog.md+3 README.md+17 all-tests.nix+12−12 default.nix+109−79 dep/reflex-platform/default.nix+7−6 dep/reflex-platform/github.json+4−3 lib/asset/serve-snap/src/Obelisk/Asset/Accept.hs+6−6 lib/backend/src/Obelisk/Backend.hs−2 lib/cliapp/obelisk-cliapp.cabal+2−2 lib/cliapp/src/Obelisk/CliApp.hs+1 lib/cliapp/src/Obelisk/CliApp/Demo.hs−52 lib/cliapp/src/Obelisk/CliApp/Logging.hs+11−10 lib/cliapp/src/Obelisk/CliApp/Process.hs+15 lib/command/obelisk-command.cabal+10−9 lib/command/src/Obelisk/Command.hs+34−60 lib/command/src/Obelisk/Command/Deploy.hs+1−1 lib/command/src/Obelisk/Command/Nix.hs+44−13 lib/command/src/Obelisk/Command/Preprocessor.hs+96 lib/command/src/Obelisk/Command/Project.hs+78−34 lib/command/src/Obelisk/Command/Run.hs+203−116 lib/command/src/Obelisk/Command/Thunk.hs+69−30 lib/command/src/Obelisk/Command/Utils.hs+15−7 lib/executable-config/lookup/src-other/Obelisk/ExecutableConfig/Lookup.hs+1−8 lib/frontend/src/Obelisk/Frontend.hs+2−2 lib/frontend/src/Obelisk/Frontend/Cookie.hs+2−1 lib/selftest/src/Obelisk/SelfTest.hs+131−67 lib/snap-extras/obelisk-snap-extras.cabal+1−1 lib/snap-extras/src/Obelisk/Snap/Extras.hs+1−1 skeleton/backend/src/Backend.hs−3
ChangeLog.md+3
@@ -5,6 +5,9 @@ This project's release branch is `master`. This log is written from the perspect
55## Unreleased
66
77* Add `Obelisk.Route.(?/)`, a convenience function for constructing routes nested in `Maybe`. ([#457](https://github.com/obsidiansystems/obelisk/pull/457))
8+* Add local unpacked packages to the `ob run`, `ob watch`, and `ob repl` sessions. Any `.cabal` or hpack package inside the current obelisk project will be loaded into the session. For `ob run`/`ob watch` this means the session will automatically reload when you save a source file in any of those packages. For `ob repl` it means that `:r` will reload changes to any of those packages. There are some edge cases where this integration is still rough. Report any issues you encounter. ([#489](https://github.com/obsidiansystems/obelisk/pull/489))
9+* Add `ob hoogle` command to start a local [Hoogle](https://hoogle.haskell.org/) server for the project. ([#628](https://github.com/obsidiansystems/obelisk/pull/628))
10+* `ob thunk pack` will now attempt to automatically detect if the thunk is a private or public repo. To avoid this detection, specify `--private` or `--public` manually. ([#607](https://github.com/obsidiansystems/obelisk/pull/607))
811
912## v0.4.0.0 - 2020-01-10
1013
README.md+17
@@ -144,6 +144,23 @@ project ./. ({ pkgs, ... }: {
144144
145145For further information see [the Haskell section](https://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure) of nixpkgs Contributors Guide.
146146
147+### Adding extra local packages
148+
149+If the standard packages (`frontend`, `backend`, and `common`) are not
150+enough, to add more local Haskell packages, define them with the
151+`packages` parameter. The sources of these packages will be
152+automatically reloaded by `ob run`.
153+
154+```nix
155+# ...
156+project ./. ({ pkgs, ... }: {
157+# ...
158+ packages = {
159+ another = ./another;
160+ };
161+# ...
162+```
163+
147164### Running over https
148165
149166To run your app locally over https, update the protocol in `config/common/route` to `https`, and then use `ob run` as normal.
all-tests.nix+12−12
@@ -1,7 +1,7 @@
11{
22 supportedSystems ? [ builtins.currentSystem ]
33}:
4−let
4+let
55 nginxRoot = "/run/nginx";
66 obelisk = import ./default.nix {};
77 # Get NixOS a pre-release 20.03 that contains the python based tests and recursive nix
@@ -21,7 +21,7 @@ in
2121 enable = true;
2222 };
2323 environment.systemPackages = [
24− pkgs.git
24+ pkgs.git
2525 ];
2626 users.users.root.openssh.authorizedKeys.keys = [
2727 snakeOilPublicKey
@@ -38,7 +38,7 @@ in
3838 obelisk.command
3939 obelisk.shell
4040 obelisk-everywhere
41− pkgs.git
41+ pkgs.git
4242 ];
4343 };
4444 };
@@ -64,10 +64,10 @@ in
6464 in ''
6565 start_all()
6666 githost.wait_for_open_port("22")
67−
67+
6868 with subtest("test obelisk is installed"):
6969 client.succeed("ob --help")
70−
70+
7171 with subtest("test the client can access the server via ssh"):
7272 client.succeed("mkdir -p ~/.ssh/")
7373 client.succeed(
@@ -81,11 +81,11 @@ in
8181 "cp ${sshConfigFile} ~/.ssh/config"
8282 )
8383 client.wait_until_succeeds("ssh githost true")
84−
84+
8585 with subtest("test a remote bare repo can be started"):
8686 githost.succeed("mkdir -p ~/myorg/myapp.git")
8787 githost.succeed("cd ~/myorg/myapp.git && git init --bare")
88−
88+
8989 with subtest("test a git project can be configured with a remote using ssh"):
9090 client.succeed("mkdir -p ~/code/myapp")
9191 client.succeed("cd ~/code/myapp && git init")
@@ -99,18 +99,18 @@ in
9999 client.succeed(
100100 "cd ~/code/myapp && git remote add origin root@githost:/root/myorg/myapp.git"
101101 )
102−
103−
102+
103+
104104 with subtest("test pushing code to the remote"):
105105 client.succeed("cd ~/code/myapp && git push -u origin master")
106106 client.succeed("cd ~/code/myapp && git status")
107−
107+
108108 with subtest("test obelisk can pack"):
109109 client.succeed("ob -v thunk pack ~/code/myapp")
110110 client.succeed("grep -qF 'git' ~/code/myapp/default.nix")
111111 client.succeed("grep -qF 'myorg' ~/code/myapp/git.json")
112112 client.succeed("ob -v thunk unpack ~/code/myapp")
113−
113+
114114 with subtest("test obelisk can set the public / private flag"):
115115 client.succeed("ob -v thunk pack ~/code/myapp --private")
116116 client.fail("""grep -qF '"private": practice' ~/code/myapp/git.json""")
@@ -139,7 +139,7 @@ in
139139
140140 with subtest("test obelisk can detect private repos"):
141141 client.succeed("ob -v thunk pack ~/code/myapp")
142− client.succeed("""grep -qF '"private": false' ~/code/myapp/git.json""")
142+ client.succeed("""grep -qF '"private": true' ~/code/myapp/git.json""")
143143 client.succeed("ob -v thunk unpack ~/code/myapp")
144144 '';
145145 }) {}
default.nix+109−79
@@ -71,6 +71,8 @@ let
7171 librarySystemDepends = [
7272 pkgs.cabal-install
7373 pkgs.coreutils
74+ pkgs.git
75+ pkgs.nix
7476 ];
7577 };
7678 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
@@ -126,12 +128,11 @@ in rec {
126128 shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs);
127129
128130 selftest = pkgs.writeScript "selftest" ''
129− #!/usr/bin/env bash
131+ #!${pkgs.runtimeShell}
130132 set -euo pipefail
131133
132134 PATH="${command}/bin:$PATH"
133− export OBELISK_IMPL="$(mktemp -d)"
134− git clone file://${pathGit} $OBELISK_IMPL
135+ cd ${./.}
135136 "${ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" +RTS -N -RTS "$@"
136137 '';
137138 skeleton = pkgs.runCommand "skeleton" {
@@ -257,85 +258,114 @@ in rec {
257258 };
258259 };
259260
260−
261261 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
262− project = base: projectDefinition:
263− let projectOut = sys: (getReflexPlatform sys).project (args@{ nixpkgs, ... }:
264− let mkProject = { android ? null #TODO: Better error when missing
265− , ios ? null #TODO: Better error when missing
266− , packages ? {}
267− , overrides ? _: _: {}
268− , staticFiles ? base + /static
269− , tools ? _: []
270− , shellToolOverrides ? _: _: {}
271− , withHoogle ? false # Setting this to `true` makes shell reloading far slower
272− , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
273− }:
274− let frontendName = "frontend";
275− backendName = "backend";
276− commonName = "common";
277− staticName = "obelisk-generated-static";
278− staticFilesImpure = if lib.isDerivation staticFiles then staticFiles else toString staticFiles;
279− processedStatic = processAssets { src = staticFiles; };
280− # The packages whose names and roles are defined by this package
281− predefinedPackages = lib.filterAttrs (_: x: x != null) {
282− ${frontendName} = nullIfAbsent (base + "/frontend");
283− ${commonName} = nullIfAbsent (base + "/common");
284− ${backendName} = nullIfAbsent (base + "/backend");
285− };
286− combinedPackages = predefinedPackages // packages;
287− projectOverrides = self: super: {
288− ${staticName} = haskellLib.dontHaddock (self.callCabal2nix staticName processedStatic.haskellManifest {});
289− ${backendName} = haskellLib.addBuildDepend super.${backendName} self.obelisk-run;
290− };
291− totalOverrides = lib.composeExtensions projectOverrides overrides;
292− inherit (lib.strings) hasPrefix;
293− privateConfigDirs = ["config/backend"];
294− injectableConfig = builtins.filterSource (path: _:
295− !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
296− );
297− __androidWithConfig = configPath: {
298− ${if android == null then null else frontendName} = {
299− executableName = "frontend";
300− ${if builtins.pathExists staticFiles then "assets" else null} =
301− nixpkgs.obeliskExecutableConfig.platforms.android.inject
302− (injectableConfig configPath)
303− processedStatic.symlinked;
304− } // android;
262+ project = base': projectDefinition:
263+ let
264+ projectOut = sys: let reflexPlatformProject = (getReflexPlatform sys).project; in reflexPlatformProject (args@{ nixpkgs, ... }:
265+ let
266+ inherit (lib.strings) hasPrefix;
267+ mkProject =
268+ { android ? null #TODO: Better error when missing
269+ , ios ? null #TODO: Better error when missing
270+ , packages ? {}
271+ , overrides ? _: _: {}
272+ , staticFiles ? null
273+ , tools ? _: []
274+ , shellToolOverrides ? _: _: {}
275+ , withHoogle ? false # Setting this to `true` makes shell reloading far slower
276+ , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
277+ }:
278+ let
279+ allConfig = nixpkgs.lib.makeExtensible (self: {
280+ base = base';
281+ inherit args;
282+ userSettings = {
283+ inherit android ios packages overrides tools shellToolOverrides withHoogle __closureCompilerOptimizationLevel;
284+ staticFiles = if staticFiles == null then self.base + /static else staticFiles;
285+ };
286+ frontendName = "frontend";
287+ backendName = "backend";
288+ commonName = "common";
289+ staticName = "obelisk-generated-static";
290+ staticFilesImpure = let fs = self.userSettings.staticFiles; in if lib.isDerivation fs then fs else toString fs;
291+ processedStatic = processAssets { src = self.userSettings.staticFiles; };
292+ # The packages whose names and roles are defined by this package
293+ predefinedPackages = lib.filterAttrs (_: x: x != null) {
294+ ${self.frontendName} = nullIfAbsent (self.base + "/frontend");
295+ ${self.commonName} = nullIfAbsent (self.base + "/common");
296+ ${self.backendName} = nullIfAbsent (self.base + "/backend");
297+ };
298+ shellPackages = {};
299+ combinedPackages = self.predefinedPackages // self.userSettings.packages // self.shellPackages;
300+ projectOverrides = self': super': {
301+ ${self.staticName} = haskellLib.dontHaddock (self'.callCabal2nix self.staticName self.processedStatic.haskellManifest {});
302+ ${self.backendName} = haskellLib.addBuildDepend super'.${self.backendName} self'.obelisk-run;
303+ };
304+ totalOverrides = lib.composeExtensions self.projectOverrides self.userSettings.overrides;
305+ privateConfigDirs = ["config/backend"];
306+ injectableConfig = builtins.filterSource (path: _:
307+ !(lib.lists.any (x: hasPrefix (toString self.base + "/" + toString x) (toString path)) self.privateConfigDirs)
308+ );
309+ __androidWithConfig = configPath: {
310+ ${if self.userSettings.android == null then null else self.frontendName} = {
311+ executableName = "frontend";
312+ ${if builtins.pathExists self.userSettings.staticFiles then "assets" else null} =
313+ nixpkgs.obeliskExecutableConfig.platforms.android.inject
314+ (self.injectableConfig configPath)
315+ self.processedStatic.symlinked;
316+ } // self.userSettings.android;
317+ };
318+ __iosWithConfig = configPath: {
319+ ${if self.userSettings.ios == null then null else self.frontendName} = {
320+ executableName = "frontend";
321+ ${if builtins.pathExists self.userSettings.staticFiles then "staticSrc" else null} =
322+ nixpkgs.obeliskExecutableConfig.platforms.ios.inject
323+ (self.injectableConfig configPath)
324+ self.processedStatic.symlinked;
325+ } // self.userSettings.ios;
326+ };
327+
328+ shells-ghc = builtins.attrNames (self.predefinedPackages // self.shellPackages);
329+
330+ shells-ghcjs = [
331+ self.frontendName
332+ self.commonName
333+ ];
334+
335+ shells-ghcSavedSplices = [
336+ self.commonName
337+ self.frontendName
338+ ];
339+
340+ project = reflexPlatformProject ({...}: self.projectConfig);
341+ projectConfig = {
342+ inherit (self.userSettings) shellToolOverrides tools withHoogle;
343+ overrides = self.totalOverrides;
344+ packages = self.combinedPackages;
345+ shells = {
346+ ${if self.userSettings.android == null && self.userSettings.ios == null then null else "ghcSavedSplices"} =
347+ lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcSavedSplices;
348+ ghc = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghc;
349+ ghcjs = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcjs;
305350 };
306− __iosWithConfig = configPath: {
307− ${if ios == null then null else frontendName} = {
308− executableName = "frontend";
309− ${if builtins.pathExists staticFiles then "staticSrc" else null} =
310− nixpkgs.obeliskExecutableConfig.platforms.ios.inject
311− (injectableConfig configPath)
312− processedStatic.symlinked;
313− } // ios;
351+ android = self.__androidWithConfig (self.base + "/config");
352+ ios = self.__iosWithConfig (self.base + "/config");
353+
354+ passthru = {
355+ __unstable__.self = allConfig;
356+ inherit (self)
357+ staticFilesImpure processedStatic
358+ __iosWithConfig __androidWithConfig
359+ ;
360+ inherit (self.userSettings)
361+ android ios overrides packages shellToolOverrides staticFiles tools withHoogle
362+ __closureCompilerOptimizationLevel
363+ ;
314364 };
315− in {
316− inherit shellToolOverrides tools withHoogle;
317− overrides = totalOverrides;
318− packages = combinedPackages;
319− shells = {
320− ${if android == null && ios == null then null else "ghcSavedSplices"} = (lib.filter (x: lib.hasAttr x combinedPackages) [
321− commonName
322− frontendName
323− ]);
324− ghc = (lib.filter (x: lib.hasAttr x combinedPackages) [
325− backendName
326− commonName
327− frontendName
328− ]);
329− ghcjs = lib.filter (x: lib.hasAttr x combinedPackages) [
330− frontendName
331− commonName
332− ];
333365 };
334− android = __androidWithConfig (base + "/config");
335− ios = __iosWithConfig (base + "/config");
336− passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __iosWithConfig __androidWithConfig; };
337− };
338− in mkProject (projectDefinition args));
366+ });
367+ in allConfig;
368+ in (mkProject (projectDefinition args)).projectConfig);
339369 mainProjectOut = projectOut system;
340370 serverOn = projectInst: version: serverExe
341371 projectInst.ghc.backend
@@ -351,7 +381,7 @@ in rec {
351381 exe = serverOn mainProjectOut dummyVersion;
352382 server = args@{ hostName, adminEmail, routeHost, enableHttps, version }:
353383 server (args // { exe = linuxExe version; });
354− obelisk = import (base + "/.obelisk/impl") {};
384+ obelisk = import (base' + "/.obelisk/impl") {};
355385 };
356386 haskellPackageSets = {
357387 inherit (reflex-platform) ghc ghcjs;
dep/reflex-platform/default.nix+7−6
@@ -1,7 +1,8 @@
11# DO NOT HAND-EDIT THIS FILE
2−import ((import <nixpkgs> {}).fetchFromGitHub (
3− let json = builtins.fromJSON (builtins.readFile ./github.json);
4− in { inherit (json) owner repo rev sha256;
5− private = json.private or false;
6− }
7−))
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)))
dep/reflex-platform/github.json+4−3
@@ -1,7 +1,8 @@
11{
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
4− "branch": "develop",
5− "rev": "90351bdb2c99d416ea95fd5b8b474e8f9fab2942",
6− "sha256": "0bbzz4ckdx0ilhhpd3drfbsc0zdb2rnhw2m3nls9villbs3lz3lh"
4+ "branch": "master",
5+ "private": false,
6+ "rev": "5429278830e1555a577f2550e045ce7f7164aa65",
7+ "sha256": "1lp86cgccmim573rarsjny5vh0ygkfp5afq7006li0k9w2sw2d4c"
78}
lib/asset/serve-snap/src/Obelisk/Asset/Accept.hs+6−6
@@ -136,16 +136,16 @@ qvalue = do
136136 skipMany lws
137137 q0 <|> q1
138138 where q0 = do
139− char '0'
139+ _ <- char '0'
140140 decimals <- option [] $ do
141− char '.'
141+ _ <- char '.'
142142 starRule (Just 0) (Just numAllowedDigits) digit
143143 return $ QValue $ MkFixed $ fromIntegral $ (read ('0' : decimals) :: Int) * 10 ^ (numAllowedDigits - length decimals)
144144 q1 = do
145− char '1'
145+ _ <- char '1'
146146 option () $ do
147− char '.'
148− starRule (Just 0) (Just numAllowedDigits) $ char '0'
147+ _ <- char '.'
148+ _ <- starRule (Just 0) (Just numAllowedDigits) $ char '0'
149149 return ()
150150 return $ QValue 1
151151 numAllowedDigits :: Int
@@ -232,7 +232,7 @@ hashRule minNum maxNum element = do
232232lws :: Parser ()
233233lws = do
234234 option () $ cr >> lf
235− starRule (Just 1) Nothing $ sp <|> ht
235+ _ <- starRule (Just 1) Nothing $ sp <|> ht
236236 return ()
237237
238238-- | See http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
lib/backend/src/Obelisk/Backend.hs−2
@@ -27,7 +27,6 @@ import Control.Category
2727import Control.Monad
2828import Control.Monad.Except
2929import Control.Monad.Fail (MonadFail)
30−import Control.Categorical.Bifunctor
3130import Data.ByteString (ByteString)
3231import qualified Data.ByteString.Char8 as BSC8
3332import Data.Default (Default (..))
@@ -38,7 +37,6 @@ import qualified Data.Map as Map
3837import Data.Monoid ((<>))
3938import Data.Text (Text)
4039import qualified Data.Text as T
41−import Data.Text.Encoding
4240import Obelisk.Asset.Serve.Snap (serveAsset)
4341import qualified Obelisk.ExecutableConfig.Lookup as Lookup
4442import Obelisk.Frontend
lib/cliapp/obelisk-cliapp.cabal+2−2
@@ -7,7 +7,8 @@ cabal-version: >=1.2
77library
88 hs-source-dirs: src
99 build-depends:
10− ansi-terminal
10+ aeson
11+ , ansi-terminal
1112 , base
1213 , bytestring
1314 , containers
@@ -24,7 +25,6 @@ library
2425 , transformers
2526 exposed-modules:
2627 Obelisk.CliApp
27− Obelisk.CliApp.Demo
2828 other-modules:
2929 Obelisk.CliApp.Logging
3030 Obelisk.CliApp.Process
lib/cliapp/src/Obelisk/CliApp.hs+1
@@ -45,6 +45,7 @@ module Obelisk.CliApp
4545 , readCreateProcessWithExitCode
4646 , readProcessAndLogOutput
4747 , readProcessAndLogStderr
48+ , readProcessJSONAndLogStderr
4849 , reconstructCommand
4950 , setCwd
5051 , setDelegateCtlc
lib/cliapp/src/Obelisk/CliApp/Demo.hsdeleted−52
@@ -1,52 +0,0 @@
1−{-# LANGUAGE FlexibleContexts #-}
2−{-# LANGUAGE OverloadedStrings #-}
3−{-# LANGUAGE ScopedTypeVariables #-}
4−module Obelisk.CliApp.Demo where
5−
6−import Control.Concurrent (threadDelay)
7−import Control.Monad.Fail
8−import Control.Monad.IO.Class
9−import Data.Semigroup ((<>))
10−import qualified Data.Text as T
11−
12−import Control.Monad.Catch (MonadMask)
13−
14−import Obelisk.CliApp
15−
16−cliDemo
17− :: ( MonadIO m, MonadMask m, MonadFail m
18− , CliLog m, HasCliConfig e m, CliThrow e m, AsProcessFailure e, AsUnstructuredError e)
19− => m ()
20−cliDemo = withSpinner "CLI Demo" $ do
21− putLog Notice "This demo will showcase the CLI library functionality"
22− _ <- withSpinner' "Searching long for something" (Just $ \c -> "Discovered " <> T.pack (show c) <> " objects") $ do
23− delay
24− withSpinner "Nested task" $ do
25− delay
26− putLog Notice "In nested task"
27− -- This is a 'no trail' spinner that won't leave a trail (even its sub spinners)
28− withSpinnerNoTrail "In between, doing something temporary" $ do
29− withSpinner "Runnning something deep inside" $ do
30− delay
31− putLog Notice "You can still log from inside no-trail spinners"
32− delay
33− delay
34− putLog Error "Top nested task finished"
35− delay
36− putLog Error "Some user error while spinning"
37− delay
38− putLog Notice "This is some info mesage"
39− putLog Warning "And now a warning as well"
40− delay
41− return (42 :: Integer)
42− putLog Notice "Now we start a 2nd spinner, run a couple of process, the last of which fails:"
43− withSpinner "Looking around" $ do
44− delay
45− output <- readProcessAndLogStderr Notice $ proc "ls" ["-l", "/"]
46− putLog Notice $ "Output was: " <> output
47− delay
48− callProcessAndLogOutput (Notice, Error) $ proc "ls" ["-l", "/does-not-exist"]
49− delay
50− failWith "Something dangerous happened"
51− where
52− delay = liftIO $ threadDelay 1000000
lib/cliapp/src/Obelisk/CliApp/Logging.hs+11−10
@@ -39,7 +39,6 @@ import Control.Monad.Log (Severity (..), WithSeverity (..), logMessage, runLoggi
3939import Control.Monad.Loops (iterateUntil)
4040import Control.Monad.Reader (MonadIO, ReaderT (..))
4141import Data.IORef (atomicModifyIORef', newIORef, readIORef, writeIORef)
42−import Data.Maybe
4342import Data.Semigroup ((<>))
4443import Data.Text (Text)
4544import qualified Data.Text as T
@@ -67,7 +66,7 @@ newCliConfig sev noColor noSpinner errorLogExitCode = do
6766 tipDisplayed <- newIORef False
6867 stack <- newIORef ([], [])
6968 textEncoding <- hGetEncoding stdout
70− let theme = if fromMaybe False $ supportsUnicode <$> textEncoding
69+ let theme = if maybe False supportsUnicode textEncoding
7170 then unicodeTheme
7271 else noUnicodeTheme
7372 return $ CliConfig level noColor noSpinner lock tipDisplayed stack errorLogExitCode theme
@@ -133,7 +132,7 @@ handleLog' noColor output = do
133132 hFlush stdout
134133 Output_Overwrite ts -> liftIO $ do
135134 width <- TS.getTerminalWidth
136− T.putStr $ "\r" <> (TS.render (not noColor) width ts)
135+ T.putStr $ "\r" <> TS.render (not noColor) width ts
137136 hFlush stdout
138137 Output_ClearLine -> liftIO $ do
139138 -- Go to the first column and clear the whole line
@@ -179,14 +178,16 @@ withExitFailMessage msg f = f `catch` \(e :: ExitCode) -> do
179178
180179-- | Write log to stdout, with colors (unless `noColor`)
181180writeLog :: (MonadIO m, MonadMask m) => Bool -> Bool -> WithSeverity Text -> m ()
182−writeLog withNewLine noColor (WithSeverity severity s)
183− | noColor && severity <= Warning = liftIO $ putFn $ T.pack (show severity) <> ": " <> s
184− | not noColor && severity <= Error = TS.putStrWithSGR errorColors h withNewLine s
185− | not noColor && severity <= Warning = TS.putStrWithSGR warningColors h withNewLine s
186− | not noColor && severity >= Debug = TS.putStrWithSGR debugColors h withNewLine s
187− | otherwise = liftIO $ putFn s
181+writeLog withNewLine noColor (WithSeverity severity s) = if T.null s then pure () else write
188182 where
189− putFn = if withNewLine then (T.hPutStrLn h) else (T.hPutStr h)
183+ write
184+ | noColor && severity <= Warning = liftIO $ putFn $ T.pack (show severity) <> ": " <> s
185+ | not noColor && severity <= Error = TS.putStrWithSGR errorColors h withNewLine s
186+ | not noColor && severity <= Warning = TS.putStrWithSGR warningColors h withNewLine s
187+ | not noColor && severity >= Debug = TS.putStrWithSGR debugColors h withNewLine s
188+ | otherwise = liftIO $ putFn s
189+
190+ putFn = if withNewLine then T.hPutStrLn h else T.hPutStr h
190191 h = if severity <= Error then stderr else stdout
191192 errorColors = [SetColor Foreground Vivid Red]
192193 warningColors = [SetColor Foreground Vivid Yellow]
lib/cliapp/src/Obelisk/CliApp/Process.hs+15
@@ -22,6 +22,7 @@ module Obelisk.CliApp.Process
2222 , readCreateProcessWithExitCode
2323 , readProcessAndLogOutput
2424 , readProcessAndLogStderr
25+ , readProcessJSONAndLogStderr
2526 , reconstructCommand
2627 , setCwd
2728 , setDelegateCtlc
@@ -52,6 +53,7 @@ import qualified System.IO.Streams as Streams
5253import System.IO.Streams.Concurrent (concurrentMerge)
5354import System.Process (CreateProcess, ProcessHandle, StdStream (CreatePipe), std_err, std_out, waitForProcess)
5455import qualified System.Process as Process
56+import qualified Data.Aeson as Aeson
5557
5658import Control.Monad.Log (Severity (..))
5759import Obelisk.CliApp.Logging (putLog, putLogRaw)
@@ -101,6 +103,19 @@ readProcessAndLogStderr sev process = do
101103 streamToLog =<< liftIO (streamHandle sev err)
102104 liftIO $ T.decodeUtf8With lenientDecode <$> BS.hGetContents out
103105
106+readProcessJSONAndLogStderr
107+ :: (Aeson.FromJSON a, MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
108+ => Severity -> ProcessSpec -> m a
109+readProcessJSONAndLogStderr sev process = do
110+ (out, _err) <- withProcess process $ \_out err -> do
111+ streamToLog =<< liftIO (streamHandle sev err)
112+ json <- liftIO $ BS.hGetContents out
113+ case Aeson.eitherDecodeStrict json of
114+ Right a -> pure a
115+ Left err -> do
116+ putLog Error $ "Could not decode process output as JSON: " <> T.pack err
117+ throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess process) 0
118+
104119readCreateProcessWithExitCode
105120 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
106121 => ProcessSpec -> m (ExitCode, String, String)
lib/command/obelisk-command.cabal+10−9
@@ -7,35 +7,36 @@ library
77 hs-source-dirs: src
88 build-depends:
99 aeson
10− , ansi-terminal
11− , Cabal
1210 , aeson-pretty
11+ , ansi-terminal
1312 , base
1413 , base16-bytestring
1514 , binary
1615 , bytestring
17− , obelisk-cliapp
16+ , Cabal
1817 , containers
1918 , data-default
2019 , directory
2120 , either
2221 , errors
22+ , exceptions
23+ , extra
2324 , filepath
25+ , git
2426 , github
2527 , here
26− , git
2728 , hnix
2829 , hpack
2930 , io-streams
30− , exceptions
31− , logging-effect
3231 , lens
32+ , logging-effect
33+ , megaparsec
3334 , modern-uri
3435 , monad-loops
3536 , mtl
36− , megaparsec
3737 , network
3838 , network-uri
39+ , obelisk-cliapp
3940 , optparse-applicative
4041 , placeholders
4142 , process
@@ -58,8 +59,8 @@ library
5859 Obelisk.Command.Thunk
5960 Obelisk.Command.Utils
6061 Obelisk.Command.VmBuilder
61− -- -fobject-code is so that the StaticPointers extension can work in ghci
62− ghc-options: -Wall -fobject-code
62+ Obelisk.Command.Preprocessor
63+ ghc-options: -Wall
6364
6465executable ob
6566 main-is: src-bin/ob.hs
lib/command/src/Obelisk/Command.hs+34−60
@@ -3,8 +3,6 @@
33{-# LANGUAGE MultiParamTypeClasses #-}
44{-# LANGUAGE OverloadedStrings #-}
55{-# LANGUAGE ScopedTypeVariables #-}
6−{-# LANGUAGE StaticPointers #-}
7−{-# LANGUAGE TypeApplications #-}
86module Obelisk.Command where
97
108import Control.Monad
@@ -15,7 +13,6 @@ import qualified Data.ByteString.Base16 as Base16
1513import qualified Data.ByteString.Lazy as LBS
1614import Data.Foldable (for_)
1715import Data.List
18−import Data.Maybe (catMaybes)
1916import qualified Data.Text as T
2017import Data.Text.Encoding
2118import Data.Text.Encoding.Error (lenientDecode)
@@ -31,13 +28,12 @@ import System.Posix.Process (executeFile)
3128
3229import Obelisk.App
3330import Obelisk.CliApp
34−import Obelisk.CliApp.Demo (cliDemo)
3531import Obelisk.Command.Deploy
3632import Obelisk.Command.Project
3733import Obelisk.Command.Run
3834import Obelisk.Command.Thunk
39−import Obelisk.Command.Utils
4035import qualified Obelisk.Command.VmBuilder as VmBuilder
36+import qualified Obelisk.Command.Preprocessor as Preprocessor
4137
4238
4339data Args = Args
@@ -96,53 +92,33 @@ data ObCommand
9692 | ObCommand_Watch
9793 | ObCommand_Shell ShellOpts
9894 | ObCommand_Doc String [String] -- shell and list of packages
95+ | ObCommand_Hoogle String Int -- shell and port
9996 | ObCommand_Internal ObInternal
10097 deriving Show
10198
10299data ObInternal
103− = ObInternal_RunStaticIO StaticKey
104− | ObInternal_CLIDemo
100+ -- the preprocessor argument syntax is also handled outside
101+ -- optparse-applicative, but it shouldn't ever conflict with another syntax
102+ = ObInternal_ApplyPackages String String String [String]
105103 deriving Show
106104
107−inNixShell' :: MonadObelisk m => StaticPtr (ObeliskT IO ()) -> m ()
108−inNixShell' p = withProjectRoot "." $ \root -> do
109− cmd <- liftIO $ unwords <$> mkCmd -- TODO: shell escape instead of unwords
110− projectShell root True "ghc" (Just cmd)
111− where
112− mkCmd = do
113− argsCfg <- getArgsConfig
114− myArgs <- getArgs
115− obArgs <- parseCLIArgs argsCfg myArgs
116− progName <- getObeliskExe
117− return $ progName : catMaybes
118− [ Just "--no-handoff"
119− , bool Nothing (Just "--verbose") $ _args_verbose obArgs
120− , Just "internal"
121− , Just "run-static-io"
122− , Just $ encodeStaticKey $ staticKey p
123− ]
124−
125105obCommand :: ArgsConfig -> Parser ObCommand
126106obCommand cfg = hsubparser
127− (mconcat
128− [ command "init" $ info (ObCommand_Init <$> initSource <*> initForce) $ progDesc "Initialize an Obelisk project"
129− , command "deploy" $ info (ObCommand_Deploy <$> deployCommand cfg) $ progDesc "Prepare a deployment for an Obelisk project"
130− , command "run" $ info (pure ObCommand_Run) $ progDesc "Run current project in development mode"
131− , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
132− , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
133− , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
134− , command "shell" $ info (ObCommand_Shell <$> shellOpts) $ progDesc "Enter a shell with project dependencies"
135− , command "doc" $ info (ObCommand_Doc <$> shellFlags <*> packageNames) $
136− progDesc "List paths to haddock documentation for specified packages"
137− <> footerDoc (Just $
138− text "Hint: To open the documentation you can pipe the output of this command like"
139− <$$> text "ob doc reflex reflex-dom-core | xargs -n1 xdg-open")
140− ])
141− <|> subparser
142− (mconcat
143− [ internal
144− , command "internal" (info (ObCommand_Internal <$> internalCommand) mempty)
145− ])
107+ (mconcat
108+ [ command "init" $ info (ObCommand_Init <$> initSource <*> initForce) $ progDesc "Initialize an Obelisk project"
109+ , command "deploy" $ info (ObCommand_Deploy <$> deployCommand cfg) $ progDesc "Prepare a deployment for an Obelisk project"
110+ , command "run" $ info (pure ObCommand_Run) $ progDesc "Run current project in development mode"
111+ , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
112+ , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
113+ , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
114+ , command "shell" $ info (ObCommand_Shell <$> shellOpts) $ progDesc "Enter a shell with project dependencies"
115+ , command "doc" $ info (ObCommand_Doc <$> shellFlags <*> packageNames) $
116+ progDesc "List paths to haddock documentation for specified packages"
117+ <> footerDoc (Just $
118+ text "Hint: To open the documentation you can pipe the output of this command like"
119+ <$$> text "ob doc reflex reflex-dom-core | xargs -n1 xdg-open")
120+ , command "hoogle" $ info (ObCommand_Hoogle <$> shellFlags <*> portOpt 8080) $ progDesc "Run a hoogle server locally for your project's dependency tree"
121+ ])
146122
147123packageNames :: Parser [String]
148124packageNames = some (strArgument (metavar "PACKAGE-NAME..."))
@@ -207,12 +183,6 @@ data DeployInitOpts = DeployInitOpts
207183 }
208184 deriving Show
209185
210−internalCommand :: Parser ObInternal
211−internalCommand = subparser $ mconcat
212− [ command "run-static-io" $ info (ObInternal_RunStaticIO <$> argument (eitherReader decodeStaticKey) (action "static-key")) mempty
213− , command "clidemo" $ info (pure ObInternal_CLIDemo) mempty
214− ]
215−
216186--TODO: Result should provide normalised path and also original user input for error reporting.
217187thunkDirectoryParser :: Parser FilePath
218188thunkDirectoryParser = fmap (dropTrailingPathSeparator . normalise) . strArgument $ mconcat
@@ -270,13 +240,16 @@ shellOpts = ShellOpts
270240 <$> shellFlags
271241 <*> optional (strArgument (metavar "COMMAND"))
272242
243+portOpt :: Int -> Parser Int
244+portOpt dfault = option auto (long "port" <> short 'p' <> help "Port number for server" <> showDefault <> value dfault <> metavar "INT")
245+
273246parserPrefs :: ParserPrefs
274247parserPrefs = defaultPrefs
275248 { prefShowHelpOnEmpty = True
276249 }
277250
278251parseCLIArgs :: ArgsConfig -> [String] -> IO Args
279−parseCLIArgs cfg as = pure as >>= handleParseResult . execParserPure parserPrefs (argsInfo cfg)
252+parseCLIArgs cfg = handleParseResult . execParserPure parserPrefs (argsInfo cfg)
280253
281254-- | Create an Obelisk config for the current process.
282255mkObeliskConfig :: IO Obelisk
@@ -349,6 +322,9 @@ main' argsCfg = do
349322 liftIO $ executeFile impl False ("--no-handoff" : myArgs) Nothing
350323 case myArgs of
351324 "--no-handoff" : as -> go as -- If we've been told not to hand off, don't hand off
325+ origPath:inPath:outPath:preprocessorName:packagePaths
326+ | preprocessorName == preprocessorIdentifier && any (\c -> c == '.' || c == pathSeparator) origPath ->
327+ ob $ ObCommand_Internal $ ObInternal_ApplyPackages origPath inPath outPath packagePaths
352328 a:as -- Otherwise bash completion would always hand-off even if the user isn't trying to
353329 | "--bash-completion" `isPrefixOf` a
354330 && "--no-handoff" `elem` as -> go (a:as)
@@ -375,7 +351,7 @@ ob = \case
375351 Right (ThunkData_Packed ptr) -> return ptr
376352 Right (ThunkData_Checkout (Just ptr)) -> return ptr
377353 Right (ThunkData_Checkout Nothing) ->
378− getThunkPtr False root
354+ getThunkPtr False root Nothing
379355 let sshKeyPath = _deployInitOpts_sshKey deployOpts
380356 hostname = _deployInitOpts_hostname deployOpts
381357 route = _deployInitOpts_route deployOpts
@@ -389,24 +365,22 @@ ob = \case
389365 Just RemoteBuilder_ObeliskVM -> (:[]) <$> VmBuilder.getNixBuildersArg
390366 DeployCommand_Update -> deployUpdate "."
391367 DeployCommand_Test (platform, extraArgs) -> deployMobile platform extraArgs
392− ObCommand_Run -> inNixShell' $ static run
368+ ObCommand_Run -> run
393369 ObCommand_Thunk tc -> case tc of
394370 ThunkCommand_Update thunks config -> for_ thunks (updateThunkToLatest config)
395371 ThunkCommand_Unpack thunks -> for_ thunks unpackThunk
396372 ThunkCommand_Pack thunks config -> for_ thunks (packThunk config)
397373 ObCommand_Repl -> runRepl
398− ObCommand_Watch -> inNixShell' $ static runWatch
374+ ObCommand_Watch -> runWatch
399375 ObCommand_Shell so -> withProjectRoot "." $ \root ->
400376 projectShell root False (_shellOpts_shell so) (_shellOpts_command so)
401377 ObCommand_Doc shell' pkgs -> withProjectRoot "." $ \root ->
402378 projectShell root False shell' (Just $ haddockCommand pkgs)
379+ ObCommand_Hoogle shell' port -> withProjectRoot "." $ \root -> do
380+ nixShellWithHoogle root True shell' $ Just $ "hoogle server -p " <> show port <> " --local"
403381 ObCommand_Internal icmd -> case icmd of
404− ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case
405− Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
406− Just p -> do
407− c <- getObelisk
408− liftIO $ runObelisk c $ deRefStaticPtr p
409− ObInternal_CLIDemo -> cliDemo
382+ ObInternal_ApplyPackages origPath inPath outPath packagePaths -> do
383+ liftIO $ Preprocessor.applyPackages origPath inPath outPath packagePaths
410384
411385haddockCommand :: [String] -> String
412386haddockCommand pkgs = unwords
lib/command/src/Obelisk/Command/Deploy.hs+1−1
@@ -234,7 +234,7 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
234234 [ "with (import ", srcDir, " {});"
235235 , "android.frontend.override (drv: { "
236236 , "releaseKey = (if builtins.isNull drv.releaseKey then {} else drv.releaseKey) // " <> releaseKey <> "; "
237− , "staticSrc = (passthru.__androidWithConfig ", configDir, ").frontend.staticSrc;"
237+ , "assets = (passthru.__androidWithConfig ", configDir, ").frontend.assets;"
238238 , "})"
239239 ]
240240 return $ Target
lib/command/src/Obelisk/Command/Nix.hs+44−13
@@ -9,22 +9,31 @@
99{-# LANGUAGE TemplateHaskell #-}
1010{-# LANGUAGE TypeApplications #-}
1111module Obelisk.Command.Nix
12− ( nixCmd
13− , NixCmd (..)
14− , nixCmdConfig_target
15− , nixCmdConfig_args
16− , nixCmdConfig_builders
12+ ( Arg (..)
1713 , NixBuildConfig (..)
1814 , nixBuildConfig_common
1915 , nixBuildConfig_outLink
16+ , NixCmd (..)
17+ , nixCmdConfig_args
18+ , nixCmdConfig_builders
19+ , nixCmdConfig_target
20+ , NixCommonConfig (..)
2021 , NixInstantiateConfig (..)
2122 , nixInstantiateConfig_eval
22− , NixCommonConfig (..)
23− , Target (..)
23+ , NixShellConfig (..)
24+ , nixShellConfig_common
25+ , nixShellConfig_pure
26+ , nixShellConfig_run
2427 , OutLink (..)
25− , Arg (..)
28+ , Target (..)
29+ , target_attr
30+ , target_expr
31+ , target_path
32+
2633 , boolArg
34+ , nixCmd
2735 , rawArg
36+ , runNixShellConfig
2837 , strArg
2938 ) where
3039
@@ -62,10 +71,10 @@ data Arg
6271 deriving (Eq, Show)
6372
6473strArg :: String -> String -> Arg
65−strArg k = Arg_Str k
74+strArg = Arg_Str
6675
6776rawArg :: String -> String -> Arg
68−rawArg k = Arg_Expr k
77+rawArg = Arg_Expr
6978
7079boolArg :: String -> Bool -> Arg
7180boolArg k = Arg_Expr k . bool "false" "true"
@@ -85,7 +94,7 @@ makeClassy ''NixCommonConfig
8594instance Default NixCommonConfig where
8695 def = NixCommonConfig def mempty mempty
8796
88−runNixCommonConfig :: NixCommonConfig -> [FilePath]
97+runNixCommonConfig :: NixCommonConfig -> [String]
8998runNixCommonConfig cfg = mconcat [maybeToList path, attrArg, exprArg, args, buildersArg]
9099 where
91100 path = _target_path $ _nixCmdConfig_target cfg
@@ -123,7 +132,7 @@ instance HasNixCommonConfig NixBuildConfig where
123132instance Default NixBuildConfig where
124133 def = NixBuildConfig def def
125134
126−runNixBuildConfig :: NixBuildConfig -> [FilePath]
135+runNixBuildConfig :: NixBuildConfig -> [String]
127136runNixBuildConfig cfg = mconcat
128137 [ runNixCommonConfig $ cfg ^. nixCommonConfig
129138 , case _nixBuildConfig_outLink cfg of
@@ -144,12 +153,26 @@ instance HasNixCommonConfig NixInstantiateConfig where
144153instance Default NixInstantiateConfig where
145154 def = NixInstantiateConfig def False
146155
147−runNixInstantiateConfig :: NixInstantiateConfig -> [FilePath]
156+runNixInstantiateConfig :: NixInstantiateConfig -> [String]
148157runNixInstantiateConfig cfg = mconcat
149158 [ runNixCommonConfig $ cfg ^. nixCommonConfig
150159 , "--eval" <$ guard (_nixInstantiateConfig_eval cfg)
151160 ]
152161
162+data NixShellConfig = NixShellConfig
163+ { _nixShellConfig_common :: NixCommonConfig
164+ , _nixShellConfig_pure :: Bool
165+ , _nixShellConfig_run :: Maybe String
166+ }
167+
168+makeLenses ''NixShellConfig
169+
170+instance HasNixCommonConfig NixShellConfig where
171+ nixCommonConfig = nixShellConfig_common
172+
173+instance Default NixShellConfig where
174+ def = NixShellConfig def False Nothing
175+
153176data NixCmd
154177 = NixCmd_Build NixBuildConfig
155178 | NixCmd_Instantiate NixInstantiateConfig
@@ -157,6 +180,14 @@ data NixCmd
157180instance Default NixCmd where
158181 def = NixCmd_Build def
159182
183+runNixShellConfig :: NixShellConfig -> [String]
184+runNixShellConfig cfg = mconcat
185+ [ runNixCommonConfig $ cfg ^. nixCommonConfig
186+ , [ "--pure" | cfg ^. nixShellConfig_pure ]
187+ ] ++ mconcat [
188+ ["--run", run] | run <- maybeToList $ cfg ^. nixShellConfig_run
189+ ]
190+
160191nixCmd :: MonadObelisk m => NixCmd -> m FilePath
161192nixCmd cmdCfg = withSpinner' ("Running " <> cmd <> desc) (Just $ const $ "Built " <> desc) $ do
162193 output <- readProcessAndLogStderr Debug $ proc (T.unpack cmd) options
lib/command/src/Obelisk/Command/Preprocessor.hsnew file+96
@@ -0,0 +1,96 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+
4+module Obelisk.Command.Preprocessor where
5+
6+import qualified Data.ByteString.Lazy as BL
7+import qualified Data.ByteString.Lazy.Builder as BU
8+import Data.List (intersperse, isPrefixOf, sortOn)
9+import Data.Maybe (fromMaybe)
10+import qualified Data.Text.Lazy.Builder as TL
11+import qualified Data.Text.Lazy.Encoding as TL
12+import Distribution.Compiler (CompilerFlavor (..))
13+import Language.Haskell.Extension (Extension (..), Language(..))
14+import System.IO (IOMode (..), hClose, hPutStrLn, openFile, stderr)
15+import System.FilePath (hasTrailingPathSeparator, joinPath, normalise, splitPath)
16+
17+import Obelisk.Command.Run (CabalPackageInfo (..), parseCabalPackage')
18+
19+applyPackages :: FilePath -> FilePath -> FilePath -> [FilePath] -> IO ()
20+applyPackages origPath inPath outPath packagePaths = do
21+ -- This code is intended to be executed via ghci's -pgmF preprocessor option
22+ -- The command line arguments are passed in via ghc, which dictates the first three options and meanings
23+ -- In order for this code to execute, origPath must contain either a '.' character or a '/' character.
24+ -- (This is to avoid the possibility of the command line syntax conflicting with another ob command)
25+ -- We do have control over the remaining arguments, but they must be the same for all files.
26+ -- Thus, the fourth command line argument must be "apply-packages", which has already been handled.
27+ -- We assume all the remaining arguments passed in are paths to cabal or hpack package specifications.
28+
29+ outFile <- openFile outPath WriteMode
30+ let hPutTextBuilder h = BU.hPutBuilder h . TL.encodeUtf8Builder . TL.toLazyText
31+
32+ -- putStr "--------------------------------------------------------------------------------\n"
33+ -- print args
34+ -- putStr "--------------------------------------------------------------------------------\n"
35+
36+ -- Thus we must select among the packagePaths for the file we are going to parse.
37+
38+ let takeDirs = takeWhile hasTrailingPathSeparator
39+ packageDirs = sortOn (negate . length . takeDirs) $ map (splitPath . normalise) packagePaths
40+ origDir = splitPath $ normalise origPath
41+ matches = [ joinPath d | d <- packageDirs, takeDirs d `isPrefixOf` origDir ]
42+
43+ -- So the first element of matches is going to be the deepest path to a package spec that contains
44+ -- our file as a subdirectory.
45+
46+ case matches of
47+ [] -> hPutTextBuilder outFile (lineNumberPragma origPath) -- TODO: probably should produce a warning
48+ (packagePath:_) -> do
49+ parseCabalPackage' packagePath >>= \case
50+ Left err ->
51+ hPutStrLn stderr $ "Error: Unable to parse cabal package " <> packagePath <> "; Skipping preprocessor on " <> origPath <> ". Error was " <> show err
52+ Right (_warnings, packageInfo) ->
53+ hPutTextBuilder outFile (generateHeader origPath packageInfo)
54+
55+ BL.readFile inPath >>= BL.hPut outFile
56+ hClose outFile
57+
58+
59+ -- I'm pretty sure there's a certain amount of oversimplification in CabalPackageInfo, so I doubt this is fully robust.
60+
61+generateHeader :: FilePath -> CabalPackageInfo -> TL.Builder
62+generateHeader origPath packageInfo =
63+ hsExtensions <> ghcOptions <> lineNumberPragma origPath
64+ where
65+ hsExtensions =
66+ if not (null extList)
67+ then TL.fromText "{-# LANGUAGE "
68+ <> mconcat (intersperse (TL.fromText ", ") extList)
69+ <> TL.fromText " #-}\n"
70+ else mempty
71+ extList = addDefaultLanguage $ concatMap showExt $ _cabalPackageInfo_defaultExtensions packageInfo
72+ addDefaultLanguage =
73+ case _cabalPackageInfo_defaultLanguage packageInfo of
74+ Nothing -> id
75+ Just x -> case x of
76+ UnknownLanguage ext -> ( TL.fromString ext :)
77+ ext -> ( TL.fromString (show ext) :)
78+ showExt = \case
79+ EnableExtension ext -> [TL.fromString (show ext)]
80+ DisableExtension _ -> []
81+ UnknownExtension ext -> [TL.fromString ext]
82+
83+ ghcOptions =
84+ if not (null optList)
85+ then TL.fromText "{-# OPTIONS_GHC "
86+ <> mconcat (intersperse (TL.fromText " ") optList)
87+ <> TL.fromText " #-}\n"
88+ else mempty
89+ optList = map TL.fromString
90+ $ filter (\x -> not (isPrefixOf "-O" x))
91+ $ fromMaybe []
92+ $ lookup GHC (_cabalPackageInfo_compilerOptions packageInfo)
93+
94+lineNumberPragma :: FilePath -> TL.Builder
95+lineNumberPragma origPath =
96+ TL.fromText "{-# LINE 1 \"" <> TL.fromString origPath <> TL.fromText "\" #-}\n"
lib/command/src/Obelisk/Command/Project.hs+78−34
@@ -1,28 +1,39 @@
11{-# LANGUAGE FlexibleContexts #-}
22{-# LANGUAGE LambdaCase #-}
33{-# LANGUAGE OverloadedStrings #-}
4−{-# LANGUAGE TemplateHaskell #-}
54module Obelisk.Command.Project
65 ( InitSource (..)
7− , initProject
86 , findProjectObeliskCommand
97 , findProjectRoot
10− , withProjectRoot
11− , inProjectShell
12− , inImpureProjectShell
8+ , initProject
9+ , nixShellRunConfig
10+ , nixShellRunProc
11+ , nixShellWithHoogle
12+ , nixShellWithPkgs
13+ , obeliskDirName
1314 , projectShell
14−
15− , toObeliskDir
1615 , toImplDir
16+ , toNixPath
17+ , toObeliskDir
18+ , withProjectRoot
1719 ) where
1820
1921import Control.Concurrent.MVar (MVar, newMVar, withMVarMasked)
22+import Control.Lens ((.~), (?~), (<&>))
2023import Control.Monad
2124import Control.Monad.IO.Class (liftIO)
2225import Control.Monad.State
26+import qualified Data.Aeson as Json
2327import Data.Bits
24−import Data.Function (on)
28+import qualified Data.ByteString.Lazy as BSL
29+import Data.Default (def)
30+import Data.Function ((&), on)
31+import Data.List (isInfixOf)
32+import Data.Map (Map)
33+import Data.Text (Text)
2534import qualified Data.Text as T
35+import Data.Text.Encoding (decodeUtf8)
36+import Data.Traversable (for)
2637import System.Directory
2738import System.Environment (lookupEnv)
2839import System.FilePath
@@ -31,14 +42,15 @@ import System.IO.Unsafe (unsafePerformIO)
3142import System.Posix (FileStatus, FileMode, CMode (..), UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
3243import System.Posix.Files
3344import System.Process (waitForProcess)
34−import System.Which (staticWhich)
3545
3646import GitHub.Data.GitData (Branch)
3747import GitHub.Data.Name (Name)
3848
3949import Obelisk.App (MonadObelisk)
4050import Obelisk.CliApp
51+import Obelisk.Command.Nix
4152import Obelisk.Command.Thunk
53+import Obelisk.Command.Utils (nixExePath)
4254
4355--TODO: Make this module resilient to random exceptions
4456
@@ -62,9 +74,12 @@ data InitSource
6274 | InitSource_Symlink FilePath
6375 deriving Show
6476
77+obeliskDirName :: FilePath
78+obeliskDirName = ".obelisk"
79+
6580-- | Path to obelisk directory in given path
6681toObeliskDir :: FilePath -> FilePath
67−toObeliskDir p = p </> ".obelisk"
82+toObeliskDir p = p </> obeliskDirName
6883
6984-- | Path to impl file in given path
7085toImplDir :: FilePath -> FilePath
@@ -234,32 +249,61 @@ filePermissionIsSafe s umask = not fileWorldWritable && fileGroupWritable <= uma
234249 fileGroupWritable = fileMode s .&. 0o020 == 0o020
235250 umaskGroupWritable = umask .&. 0o020 == 0
236251
237−-- | Run a command in the given shell for the current project
238−inProjectShell :: MonadObelisk m => String -> String -> m ()
239−inProjectShell shellName command = withProjectRoot "." $ \root ->
240− projectShell root True shellName (Just command)
252+-- | Nix syntax requires relative paths to be prefixed by @./@ or
253+-- @../@. This will make a 'FilePath' that can be embedded in a Nix
254+-- expression.
255+toNixPath :: FilePath -> FilePath
256+toNixPath root | "/" `isInfixOf` root = root
257+ | otherwise = "./" <> root
241258
242−inImpureProjectShell :: MonadObelisk m => String -> String -> m ()
243−inImpureProjectShell shellName command = withProjectRoot "." $ \root ->
244− projectShell root False shellName (Just command)
259+nixShellRunConfig :: MonadObelisk m => FilePath -> Bool -> Maybe String -> m NixShellConfig
260+nixShellRunConfig root isPure command = do
261+ nixpkgsPath <- fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $
262+ proc nixExePath ["eval", "(import .obelisk/impl {}).nixpkgs.path"]
263+ nixRemote <- liftIO $ lookupEnv "NIX_REMOTE"
264+ pure $ def
265+ & nixShellConfig_pure .~ isPure
266+ & nixShellConfig_common . nixCmdConfig_target .~ (def & target_path .~ Nothing)
267+ & nixShellConfig_run .~ (command <&> \c -> mconcat
268+ [ "export NIX_PATH=nixpkgs=", T.unpack nixpkgsPath, "; "
269+ , maybe "" (\v -> "export NIX_REMOTE=" <> v <> "; ") nixRemote
270+ , c
271+ ])
272+
273+nixShellRunProc :: NixShellConfig -> ProcessSpec
274+nixShellRunProc cfg = setDelegateCtlc True $ proc "nix-shell" $ runNixShellConfig cfg
275+
276+nixShellWithPkgs :: MonadObelisk m => FilePath -> Bool -> Bool -> Map Text FilePath -> Maybe String -> m ()
277+nixShellWithPkgs root isPure chdirToRoot packageNamesAndPaths command = do
278+ packageNamesAndAbsPaths <- liftIO $ for packageNamesAndPaths makeAbsolute
279+ defShellConfig <- nixShellRunConfig root isPure command
280+ let setCwd_ = if chdirToRoot then setCwd (Just root) else id
281+ (_, _, _, ph) <- createProcess_ "nixShellWithPkgs" $ setCwd_ $ nixShellRunProc $ defShellConfig
282+ & nixShellConfig_common . nixCmdConfig_target . target_expr ?~
283+ "{root, pkgs}: ((import root {}).passthru.__unstable__.self.extend (_: _: {\
284+ \shellPackages = builtins.fromJSON pkgs;\
285+ \})).project.shells.ghc"
286+ & nixShellConfig_common . nixCmdConfig_args .~
287+ [ rawArg "root" $ toNixPath $ if chdirToRoot then "." else root
288+ , strArg "pkgs" (T.unpack $ decodeUtf8 $ BSL.toStrict $ Json.encode packageNamesAndAbsPaths)
289+ ]
290+ void $ liftIO $ waitForProcess ph
291+
292+nixShellWithHoogle :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
293+nixShellWithHoogle root isPure shell' command = do
294+ defShellConfig <- nixShellRunConfig root isPure command
295+ (_, _, _, ph) <- createProcess_ "nixShellWithHoogle" $ setCwd (Just root) $ nixShellRunProc $ defShellConfig
296+ & nixShellConfig_common . nixCmdConfig_target . target_expr ?~
297+ "{shell}: ((import ./. {}).passthru.__unstable__.self.extend (_: super: {\
298+ \userSettings = super.userSettings // { withHoogle = true; };\
299+ \})).project.shells.${shell}"
300+ & nixShellConfig_common . nixCmdConfig_args .~ [ strArg "shell" shell' ]
301+ void $ liftIO $ waitForProcess ph
245302
246303projectShell :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
247304projectShell root isPure shellName command = do
248− let nixPath = $(staticWhich "nix")
249− nixpkgsPath <- fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $ proc nixPath ["eval", "(import .obelisk/impl {}).nixpkgs.path"]
250− nixRemote <- liftIO $ lookupEnv "NIX_REMOTE"
251− (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setDelegateCtlc True $ setCwd (Just root) $ proc "nix-shell" $
252− [ "default.nix"] <>
253− ["--pure" | isPure] <>
254− [ "-A"
255− , "shells." <> shellName
256− ] <> case command of
257− Nothing -> []
258− Just c ->
259− -- TODO: Escape nixpkgsPath and nixRemote
260− [ "--run"
261− , "export NIX_PATH=nixpkgs=" <> T.unpack nixpkgsPath <> "; " <>
262− maybe "" (\v -> "export NIX_REMOTE=" <> v <> "; ") nixRemote <>
263− c
264− ]
305+ defShellConfig <- nixShellRunConfig root isPure command
306+ (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCwd (Just root) $ nixShellRunProc $ defShellConfig
307+ & nixShellConfig_common . nixCmdConfig_target . target_path ?~ "default.nix"
308+ & nixShellConfig_common . nixCmdConfig_target . target_attr ?~ ("shells." <> shellName)
265309 void $ liftIO $ waitForProcess ph
lib/command/src/Obelisk/Command/Run.hs+203−116
@@ -3,47 +3,60 @@
33{-# LANGUAGE MultiParamTypeClasses #-}
44{-# LANGUAGE OverloadedStrings #-}
55{-# LANGUAGE ScopedTypeVariables #-}
6−{-# LANGUAGE TemplateHaskell #-}
76{-# LANGUAGE TupleSections #-}
87module Obelisk.Command.Run where
98
10−import Control.Applicative (liftA2)
9+import Control.Arrow ((&&&))
1110import Control.Exception (Exception, bracket)
12−import Control.Monad
11+import Control.Monad (filterM, unless)
1312import Control.Monad.Except (runExceptT, throwError)
1413import Control.Monad.IO.Class (liftIO)
1514import Control.Monad.Reader (MonadIO)
15+import Data.Coerce (coerce)
1616import Data.Either
17−import Data.Foldable (for_)
18−import qualified Data.List as L
17+import Data.Foldable (for_, toList)
18+import Data.List.Extra (dropPrefix)
1919import qualified Data.List.NonEmpty as NE
20+import Data.Map (Map)
21+import qualified Data.Map as Map
2022import Data.Maybe
23+import qualified Data.Set as Set
24+import Data.Text (Text)
2125import qualified Data.Text as T
26+import Data.Traversable (for)
27+import Debug.Trace (trace)
2228import Distribution.Compiler (CompilerFlavor(..))
2329import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
2430import Distribution.Parsec.ParseResult (runParseResult)
25−import Distribution.Pretty
31+import qualified Distribution.System as Dist
2632import Distribution.Types.BuildInfo
2733import Distribution.Types.CondTree
2834import Distribution.Types.GenericPackageDescription
2935import Distribution.Types.Library
3036import Distribution.Utils.Generic
37+import qualified Distribution.Parsec.Common as Dist
3138import qualified Hpack.Config as Hpack
3239import qualified Hpack.Render as Hpack
3340import qualified Hpack.Yaml as Hpack
3441import Language.Haskell.Extension
3542import Network.Socket hiding (Debug)
3643import System.Directory
44+import System.Environment (getExecutablePath)
3745import System.FilePath
46+import qualified System.Info
3847import System.IO.Temp (withSystemTempDirectory)
39−import System.Which (staticWhich)
4048
41−import Obelisk.App (MonadObelisk, ObeliskT)
42−import Obelisk.CliApp (Severity (..) , callCommand, failWith, putLog, proc, readProcessAndLogStderr)
43−import Obelisk.Command.Project (inProjectShell, withProjectRoot)
49+import Obelisk.App (MonadObelisk)
50+import Obelisk.CliApp (Severity (..) , failWith, putLog, proc, readCreateProcessWithExitCode, readProcessAndLogStderr)
51+import Obelisk.Command.Project (obeliskDirName, toObeliskDir, withProjectRoot, nixShellWithPkgs, toNixPath)
52+import Obelisk.Command.Thunk (attrCacheFileName)
53+import Obelisk.Command.Utils (findExePath, ghcidExePath, nixBuildExePath, nixExePath)
4454
4555data CabalPackageInfo = CabalPackageInfo
46− { _cabalPackageInfo_packageRoot :: FilePath
56+ { _cabalPackageInfo_packageFile :: FilePath
57+ , _cabalPackageInfo_packageName :: T.Text
58+ , _cabalPackageInfo_packageRoot :: FilePath
59+ , _cabalPackageInfo_buildable :: Bool
4760 , _cabalPackageInfo_sourceDirs :: NE.NonEmpty FilePath
4861 -- ^ List of hs src dirs of the library component
4962 , _cabalPackageInfo_defaultExtensions :: [Extension]
@@ -54,20 +67,19 @@ data CabalPackageInfo = CabalPackageInfo
5467 -- ^ List of compiler-specific options (e.g., the "ghc-options" field of the cabal file)
5568 }
5669
57−-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
58−run :: ObeliskT IO ()
59−run = do
60− let nixPath = $(staticWhich "nix")
61− nixBuildPath = $(staticWhich "nix-build")
62− pkgs <- getLocalPkgs
63− withGhciScript pkgs $ \dotGhciPath -> do
70+-- | Used to signal to obelisk that it's being invoked as a preprocessor
71+preprocessorIdentifier :: String
72+preprocessorIdentifier = "__preprocessor-apply-packages"
73+
74+run :: MonadObelisk m => m ()
75+run = withProjectRoot "." $ \root -> do
76+ pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
77+ withGhciScript pkgs root $ \dotGhciPath -> do
6478 freePort <- getFreePort
65− assets <- withProjectRoot "." $ \root -> do
66− let importableRoot = if "/" `isInfixOf` root
67− then root
68− else "./" <> root
79+ assets <- do
80+ let importableRoot = toNixPath root
6981 isDerivation <- readProcessAndLogStderr Debug $
70− proc nixPath
82+ proc nixExePath
7183 [ "eval"
7284 , "-f"
7385 , root
@@ -79,83 +91,143 @@ run = do
7991 -- Check whether the impure static files are a derivation (and so must be built)
8092 if isDerivation == "1"
8193 then fmap T.strip $ readProcessAndLogStderr Debug $ -- Strip whitespace here because nix-build has no --raw option
82− proc nixBuildPath
94+ proc nixBuildExePath
8395 [ "--no-out-link"
8496 , "-E", "(import " <> importableRoot <> "{}).passthru.staticFilesImpure"
8597 ]
8698 else readProcessAndLogStderr Debug $
87− proc nixPath ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
99+ proc nixExePath ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
88100 putLog Debug $ "Assets impurely loaded from: " <> assets
89− runGhcid dotGhciPath $ Just $ unwords
101+ runGhcid root True dotGhciPath pkgs $ Just $ unwords
90102 [ "Obelisk.Run.run"
91103 , show freePort
92− , "(runServeAsset " ++ show assets ++ ")"
104+ , "(Obelisk.Run.runServeAsset " ++ show assets ++ ")"
93105 , "Backend.backend"
94106 , "Frontend.frontend"
95107 ]
96108
97109runRepl :: MonadObelisk m => m ()
98−runRepl = do
99− pkgs <- getLocalPkgs
100− withGhciScript pkgs $ \dotGhciPath -> do
101− runGhciRepl dotGhciPath
110+runRepl = withProjectRoot "." $ \root -> do
111+ pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
112+ withGhciScript pkgs "." $ \dotGhciPath ->
113+ runGhciRepl root pkgs dotGhciPath
102114
103115runWatch :: MonadObelisk m => m ()
104−runWatch = do
105− pkgs <- getLocalPkgs
106− withGhciScript pkgs $ \dotGhciPath -> runGhcid dotGhciPath Nothing
116+runWatch = withProjectRoot "." $ \root -> do
117+ pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
118+ withGhciScript pkgs root $ \dotGhciPath ->
119+ runGhcid root True dotGhciPath pkgs Nothing
120+
121+-- | Relative paths to local packages of an obelisk project.
122+--
123+-- These are a combination of the obelisk predefined local packages,
124+-- and any packages that the user has set with the @packages@ argument
125+-- to the Nix @project@ function.
126+getLocalPkgs :: MonadObelisk m => FilePath -> m [FilePath]
127+getLocalPkgs root = do
128+ (_exitCode, out, err) <- readCreateProcessWithExitCode $
129+ proc findExePath ["-L", root, "(", "-name", "*.cabal", "-o", "-name", Hpack.packageConfig, ")", "-a", "-type", "f"]
130+ putLog Debug $ T.strip $ T.pack err
131+
132+ let
133+ -- We ignore any path that has ".obelisk" in it, but keep the root ".obelisk" paths
134+ packagePaths = filter (not . isIgnored) $ map T.unpack $ T.lines $ T.strip $ T.pack out
135+ isIgnored path =
136+ obeliskDirName `elem` dropPrefix (splitPath $ toObeliskDir root) pathSegments
137+ && attrCacheFileName `notElem` pathSegments
138+ where
139+ pathSegments = splitPath path
140+
141+ pure packagePaths
107142
108143data GuessPackageFileError = GuessPackageFileError_Ambiguous [FilePath] | GuessPackageFileError_NotFound
109144 deriving (Eq, Ord, Show)
110145instance Exception GuessPackageFileError
111146
147+newtype HPackFilePath = HPackFilePath { unHPackFilePath :: FilePath } deriving (Eq, Ord, Show)
148+newtype CabalFilePath = CabalFilePath { unCabalFilePath :: FilePath } deriving (Eq, Ord, Show)
149+
112150-- | Given a directory, try to guess what the appropriate @.cabal@ or @package.yaml@ file is for the package.
113151guessCabalPackageFile
114− :: MonadIO m
115− => FilePath -- ^ Directory to search for cabal package
116− -> m (Either GuessPackageFileError (Either FilePath FilePath))
117− -- ^ 'Right' 'Left' for hpack package, 'Right' 'Right' for cabal package; 'FilePath' is relative to given directory.
118−guessCabalPackageFile dir = do
119− candidates <- liftIO $
120− filterM (doesFileExist . (dir </>))
121− =<< filter (liftA2 (||) (== Hpack.packageConfig) (".cabal" `L.isSuffixOf`))
122− <$> listDirectory dir
123− pure $ case L.partition (== Hpack.packageConfig) candidates of
124− ([hpack], _) -> Right $ Left hpack
125− ([], [cabal]) -> Right $ Right cabal
126− ([], []) -> Left GuessPackageFileError_NotFound
127− (hpacks, cabals) -> Left $ GuessPackageFileError_Ambiguous $ hpacks <> cabals
152+ :: (MonadIO m)
153+ => FilePath -- ^ Directory or path to search for cabal package
154+ -> m (Either GuessPackageFileError (Either CabalFilePath HPackFilePath))
155+guessCabalPackageFile pkg = do
156+ liftIO (doesDirectoryExist pkg) >>= \case
157+ False -> case cabalOrHpackFile pkg of
158+ (Just hpack@(Right _)) -> pure $ Right hpack
159+ (Just cabal@(Left (CabalFilePath cabalFilePath))) -> do
160+ -- If the cabal file has a sibling hpack file, we use that instead
161+ -- since running hpack often generates a sibling cabal file
162+ let possibleHpackSibling = takeDirectory cabalFilePath </> Hpack.packageConfig
163+ hasHpackSibling <- liftIO $ doesFileExist possibleHpackSibling
164+ pure $ Right $ if hasHpackSibling then Right (HPackFilePath possibleHpackSibling) else cabal
165+ Nothing -> pure $ Left GuessPackageFileError_NotFound
166+ True -> do
167+ candidates <- liftIO $
168+ filterM (doesFileExist . either unCabalFilePath unHPackFilePath)
169+ =<< mapMaybe (cabalOrHpackFile . (pkg </>)) <$> listDirectory pkg
170+ pure $ case partitionEithers candidates of
171+ ([hpack], _) -> Right $ Left hpack
172+ ([], [cabal]) -> Right $ Right cabal
173+ ([], []) -> Left GuessPackageFileError_NotFound
174+ (hpacks, cabals) -> Left $ GuessPackageFileError_Ambiguous $ coerce hpacks <> coerce cabals
128175
129−-- | Relative paths to local packages of an obelisk project
130−-- TODO a way to query this
131−getLocalPkgs :: Applicative f => f [FilePath]
132−getLocalPkgs = pure ["backend", "common", "frontend"]
176+cabalOrHpackFile :: FilePath -> Maybe (Either CabalFilePath HPackFilePath)
177+cabalOrHpackFile = \case
178+ x | takeExtension x == ".cabal" -> Just (Left $ CabalFilePath x)
179+ | takeFileName x == Hpack.packageConfig -> Just (Right $ HPackFilePath x)
180+ | otherwise -> Nothing
133181
182+-- | Parses the cabal package in a given directory.
183+-- This automatically figures out which .cabal file or package.yaml (hpack) file to use in the given directory.
134184parseCabalPackage
135− :: (MonadObelisk m)
136− => FilePath -- ^ package directory
185+ :: MonadObelisk m
186+ => FilePath -- ^ Package directory
137187 -> m (Maybe CabalPackageInfo)
138−parseCabalPackage dir = either ((Nothing <$) . putLog Error) (pure . Just) <=< runExceptT $ do
139− (cabalContents, packageFile) <- guessCabalPackageFile dir >>= \case
140− Left GuessPackageFileError_NotFound -> throwError $ "No .cabal or package.yaml file found in " <> T.pack dir
141− Left (GuessPackageFileError_Ambiguous _) -> throwError $ "Unable to determine which .cabal file to use in " <> T.pack dir
142− Right (Right cabalFileName) -> let file = dir </> cabalFileName in (, file) <$> liftIO (readUTF8File file)
143− Right (Left hpackFileName) -> do
188+parseCabalPackage dir = parseCabalPackage' dir >>= \case
189+ Left err -> Nothing <$ putLog Error err
190+ Right (warnings, pkgInfo) -> do
191+ for_ warnings $ putLog Warning . T.pack . show
192+ pure $ Just pkgInfo
193+
194+-- | Like 'parseCabalPackage' but returns errors and warnings directly so as to avoid 'MonadObelisk'.
195+parseCabalPackage'
196+ :: (MonadIO m)
197+ => FilePath -- ^ Package directory
198+ -> m (Either T.Text ([Dist.PWarning], CabalPackageInfo))
199+parseCabalPackage' pkg = runExceptT $ do
200+ (cabalContents, packageFile, packageName) <- guessCabalPackageFile pkg >>= \case
201+ Left GuessPackageFileError_NotFound -> throwError $ "No .cabal or package.yaml file found in " <> T.pack pkg
202+ Left (GuessPackageFileError_Ambiguous _) -> throwError $ "Unable to determine which .cabal file to use in " <> T.pack pkg
203+ Right (Left (CabalFilePath file)) -> (, file, takeBaseName file) <$> liftIO (readUTF8File file)
204+ Right (Right (HPackFilePath file)) -> do
144205 let
145− file = dir </> hpackFileName
146206 decodeOptions = Hpack.DecodeOptions (Hpack.ProgramName "ob") file Nothing Hpack.decodeYaml
147207 liftIO (Hpack.readPackageConfig decodeOptions) >>= \case
148208 Left err -> throwError $ T.pack $ "Failed to parse " <> file <> ": " <> err
149− Right (Hpack.DecodeResult hpackPackage _ _ _) -> pure (Hpack.renderPackage [] hpackPackage, file)
209+ Right (Hpack.DecodeResult hpackPackage _ _ _) -> pure (Hpack.renderPackage [] hpackPackage, file, Hpack.packageName hpackPackage)
150210
151− let (warnings, result) = runParseResult $ parseGenericPackageDescription $
152− toUTF8BS cabalContents
153− for_ warnings $ putLog Warning . T.pack . show
211+ let
212+ (warnings, result) = runParseResult $ parseGenericPackageDescription $ toUTF8BS cabalContents
213+ osConfVar = case System.Info.os of
214+ "linux" -> Just Dist.Linux
215+ "darwin" -> Just Dist.OSX
216+ _ -> trace "Unrecgonized System.Info.os" Nothing
217+ archConfVar = Just Dist.X86_64 -- TODO: Actually infer this
218+ evalConfVar v = Right $ case v of
219+ OS osVar -> Just osVar == osConfVar
220+ Arch archVar -> Just archVar == archConfVar
221+ Impl GHC _ -> True -- TODO: Actually check version range
222+ _ -> False
154223 case condLibrary <$> result of
155224 Right (Just condLib) -> do
156− let (_, lib) = simplifyCondTree (const $ pure True) condLib
157− pure $ CabalPackageInfo
158− { _cabalPackageInfo_packageRoot = dir
225+ let (_, lib) = simplifyCondTree evalConfVar condLib
226+ pure $ (warnings,) $ CabalPackageInfo
227+ { _cabalPackageInfo_packageName = T.pack packageName
228+ , _cabalPackageInfo_packageFile = packageFile
229+ , _cabalPackageInfo_packageRoot = takeDirectory packageFile
230+ , _cabalPackageInfo_buildable = buildable $ libBuildInfo lib
159231 , _cabalPackageInfo_sourceDirs =
160232 fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
161233 , _cabalPackageInfo_defaultExtensions =
@@ -168,45 +240,51 @@ parseCabalPackage dir = either ((Nothing <$) . putLog Error) (pure . Just) <=< r
168240 Left (_, errors) ->
169241 throwError $ T.pack $ "Failed to parse " <> packageFile <> ":\n" <> unlines (map show errors)
170242
243+parsePackagesOrFail :: MonadObelisk m => [FilePath] -> m (NE.NonEmpty CabalPackageInfo)
244+parsePackagesOrFail dirs = do
245+ (pkgDirErrs, packageInfos') <- fmap partitionEithers $ for dirs $ \dir -> do
246+ flip fmap (parseCabalPackage dir) $ \case
247+ Just packageInfo
248+ | _cabalPackageInfo_buildable packageInfo -> Right packageInfo
249+ _ -> Left dir
250+
251+ packageInfos <- case NE.nonEmpty packageInfos' of
252+ Nothing -> failWith $ T.pack $ "No valid, buildable packages found in " <> intercalate ", " dirs
253+ Just xs -> pure xs
254+
255+ unless (null pkgDirErrs) $
256+ putLog Warning $ T.pack $ "Failed to find buildable packages in " <> intercalate ", " pkgDirErrs
257+
258+ pure packageInfos
259+
260+packageInfoToNamePathMap :: [CabalPackageInfo] -> Map Text FilePath
261+packageInfoToNamePathMap = Map.fromList . map (_cabalPackageInfo_packageName &&& _cabalPackageInfo_packageRoot)
262+
171263-- | Create ghci configuration to load the given packages
172264withGhciScript
173265 :: MonadObelisk m
174− => [FilePath] -- ^ List of packages to load into ghci
266+ => [CabalPackageInfo] -- ^ List of packages to load into ghci
267+ -> FilePath -- ^ All paths written to the .ghci file will be relative to this path
175268 -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporary .ghci
176269 -> m ()
177−withGhciScript dirs f = do
178− (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM dirs $ \dir -> do
179− flip fmap (parseCabalPackage dir) $ \case
180− Nothing -> Left dir
181− Just packageInfo -> Right packageInfo
182−
183− when (null packageInfos) $
184− failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " dirs
185− unless (null pkgDirErrs) $
186− putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
187−
188− let extensions = packageInfos >>= _cabalPackageInfo_defaultExtensions
189− languageFromPkgs = L.nub $ mapMaybe _cabalPackageInfo_defaultLanguage packageInfos
190− -- NOTE when no default-language is present cabal sets Haskell98
191− language = NE.toList $ fromMaybe (Haskell98 NE.:| []) $ NE.nonEmpty languageFromPkgs
192− extensionsLine = if extensions == mempty
193− then ""
194− else ":set " <> unwords (("-X" <>) . prettyShow <$> extensions)
195− ghcOptions = concat $ mapMaybe (\case (GHC, xs) -> Just xs; _ -> Nothing) $
196− packageInfos >>= _cabalPackageInfo_compilerOptions
197− dotGhci = unlines
198− [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
199− , case ghcOptions of
200− [] -> ""
201− xs -> ":set " <> unwords xs
202− , extensionsLine
203− , ":set " <> unwords (("-X" <>) . prettyShow <$> language)
204− , ":load Backend Frontend"
205− , "import Obelisk.Run"
206− , "import qualified Frontend"
207− , "import qualified Backend"
208− ]
209− warnDifferentLanguages language
270+withGhciScript packageInfos pathBase f = do
271+ selfExe <- liftIO getExecutablePath
272+ let
273+ packageNames = Set.fromList $ map _cabalPackageInfo_packageName packageInfos
274+ modulesToLoad = mconcat
275+ [ [ "Obelisk.Run" | "obelisk-run" `Set.member` packageNames ]
276+ , [ "Backend" | "backend" `Set.member` packageNames ]
277+ , [ "Frontend" | "frontend" `Set.member` packageNames ]
278+ ]
279+ dotGhci = unlines
280+ -- TODO: Shell escape
281+ [ ":set -F -pgmF " <> selfExe <> " -optF " <> preprocessorIdentifier <> " " <> unwords (map (("-optF " <>) . makeRelative pathBase . _cabalPackageInfo_packageFile) packageInfos)
282+ , ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
283+ , if null modulesToLoad then "" else ":load " <> unwords modulesToLoad
284+ , "import qualified Obelisk.Run"
285+ , "import qualified Frontend"
286+ , "import qualified Backend"
287+ ]
210288 withSystemTempDirectory "ob-ghci" $ \fp -> do
211289 let dotGhciPath = fp </> ".ghci"
212290 liftIO $ writeFile dotGhciPath dotGhci
@@ -214,35 +292,44 @@ withGhciScript dirs f = do
214292
215293 where
216294 rootedSourceDirs pkg = NE.toList $
217− (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
218−
219−warnDifferentLanguages :: MonadObelisk m => [Language] -> m ()
220−warnDifferentLanguages (_:_:_) = putLog Warning "Different languages detected across packages which may result in errors when loading the repl"
221−warnDifferentLanguages _ = return ()
295+ makeRelative pathBase . (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
222296
223297-- | Run ghci repl
224298runGhciRepl
225299 :: MonadObelisk m
226− => FilePath -- ^ Path to .ghci
300+ => FilePath -- ^ Path to project root
301+ -> [CabalPackageInfo]
302+ -> FilePath -- ^ Path to .ghci
227303 -> m ()
228−runGhciRepl dotGhci = inProjectShell "ghc" $ "ghci " <> makeBaseGhciOptions dotGhci
304+runGhciRepl root packages dotGhci =
305+ -- NOTE: We do *not* want to use $(staticWhich "ghci") here because we need the
306+ -- ghc that is provided by the shell in the user's project.
307+ nixShellWithPkgs root True False (packageInfoToNamePathMap packages) $ Just $ "ghci " <> makeBaseGhciOptions dotGhci -- TODO: Shell escape
229308
230309-- | Run ghcid
231310runGhcid
232311 :: MonadObelisk m
233− => FilePath -- ^ Path to .ghci
312+ => FilePath -- ^ Path to project root
313+ -> Bool -- ^ Should we chdir to root when running this process?
314+ -> FilePath -- ^ Path to .ghci
315+ -> [CabalPackageInfo]
234316 -> Maybe String -- ^ Optional command to run at every reload
235317 -> m ()
236−runGhcid dotGhci mcmd = callCommand $ unwords $ $(staticWhich "ghcid") : opts
318+runGhcid root chdirToRoot dotGhci packages mcmd =
319+ nixShellWithPkgs root True chdirToRoot (packageInfoToNamePathMap packages) (Just $ unwords $ ghcidExePath : opts) -- TODO: Shell escape
237320 where
238321 opts =
239322 [ "-W"
240− --TODO: The decision of whether to use -fwarn-redundant-constraints should probably be made by the user
241− , "--command='ghci -Wall -ignore-dot-ghci -fwarn-redundant-constraints " <> makeBaseGhciOptions dotGhci <> "' "
242− , "--reload=config"
323+ , "--command='ghci -ignore-dot-ghci " <> makeBaseGhciOptions dotGhci <> "' "
243324 , "--outputfile=ghcid-output.txt"
244− ] <> testCmd
245− testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'")
325+ ] <> map (\x -> "--reload='" <> x <> "'") reloadFiles
326+ <> map (\x -> "--restart='" <> x <> "'") restartFiles
327+ <> testCmd
328+ testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'") -- TODO: Shell escape
329+
330+ adjustRoot x = if chdirToRoot then makeRelative root x else x
331+ reloadFiles = map adjustRoot [root </> "config"]
332+ restartFiles = map (adjustRoot . _cabalPackageInfo_packageFile) packages
246333
247334makeBaseGhciOptions :: FilePath -> String
248335makeBaseGhciOptions dotGhci =
lib/command/src/Obelisk/Command/Thunk.hs+69−30
@@ -18,6 +18,7 @@ module Obelisk.Command.Thunk
1818 , ThunkRev (..)
1919 , ThunkSource (..)
2020 , ThunkUpdateConfig (..)
21+ , attrCacheFileName
2122 , createThunk
2223 , createThunkWithLatest
2324 , getLatestRev
@@ -47,6 +48,7 @@ import qualified Data.Aeson as Aeson
4748import Data.Aeson.Encode.Pretty
4849import qualified Data.Aeson.Types as Aeson
4950import qualified Data.ByteString.Lazy as LBS
51+import Data.Containers.ListUtils (nubOrd)
5052import Data.Default
5153import Data.Either.Combinators (fromRight', rightToMaybe)
5254import Data.Git.Ref (Ref)
@@ -69,6 +71,7 @@ import GitHub
6971import GitHub.Data.Name
7072import Obelisk.Command.Nix
7173import System.Directory
74+import System.Exit
7275import System.FilePath
7376import System.IO.Error
7477import System.IO.Temp
@@ -119,7 +122,7 @@ data GitHubSource = GitHubSource
119122 }
120123 deriving (Show, Eq, Ord)
121124
122−newtype GitUri = GitUri URI.URI deriving (Eq, Ord, Show)
125+newtype GitUri = GitUri { unGitUri :: URI.URI } deriving (Eq, Ord, Show)
123126
124127gitUriToText :: GitUri -> Text
125128gitUriToText (GitUri uri)
@@ -229,6 +232,9 @@ readThunk thunkDir = do
229232 False -> return $ return Nothing
230233 False -> liftIO $ fmap ThunkData_Packed <$> readPackedThunk thunkDir
231234
235+attrCacheFileName :: FilePath
236+attrCacheFileName = ".attr-cache"
237+
232238data ThunkType = ThunkType
233239 { _thunkType_loader :: FilePath
234240 , _thunkType_json :: FilePath
@@ -241,7 +247,7 @@ gitHubThunkType :: ThunkType
241247gitHubThunkType = ThunkType
242248 { _thunkType_loader = "default.nix"
243249 , _thunkType_json = "github.json"
244− , _thunkType_optional = Set.fromList [".attr-cache"]
250+ , _thunkType_optional = Set.fromList [attrCacheFileName]
245251 , _thunkType_loaderVersions = gitHubStandaloneLoaders
246252 , _thunkType_parser = parseThunkPtr $ \v ->
247253 ThunkSource_GitHub <$> parseGitHubSource v <|> ThunkSource_Git <$> parseGitSource v
@@ -251,7 +257,7 @@ gitThunkType :: ThunkType
251257gitThunkType = ThunkType
252258 { _thunkType_loader = "default.nix"
253259 , _thunkType_json = "git.json"
254− , _thunkType_optional = Set.fromList [".attr-cache"]
260+ , _thunkType_optional = Set.fromList [attrCacheFileName]
255261 , _thunkType_loaderVersions = plainGitStandaloneLoaders
256262 , _thunkType_parser = parseThunkPtr $ fmap ThunkSource_Git . parseGitSource
257263 }
@@ -398,7 +404,7 @@ encodeThunkPtrData (ThunkPtr rev src) = case src of
398404
399405createThunk :: MonadIO m => FilePath -> ThunkPtr -> m ()
400406createThunk target thunk = liftIO $ do
401− createDirectoryIfMissing True (target </> ".attr-cache")
407+ createDirectoryIfMissing True (target </> attrCacheFileName)
402408 T.writeFile (target </> "default.nix") (thunkPtrLoader thunk)
403409 let
404410 jsonFileName = case _thunkPtr_source thunk of
@@ -443,7 +449,7 @@ updateThunkToLatest (ThunkUpdateConfig mBranch thunkConfig) target = withSpinner
443449
444450setThunk :: MonadObelisk m => ThunkConfig -> FilePath -> GitSource -> String -> m ()
445451setThunk thunkConfig target gs branch = do
446− newThunkPtr <- uriThunkPtr (_gitSource_url gs) (Just $ T.pack branch) Nothing
452+ newThunkPtr <- uriThunkPtr (_gitSource_url gs) (_thunkConfig_private thunkConfig) (Just $ T.pack branch) Nothing
447453 overwriteThunk target newThunkPtr
448454 updateThunkToLatest (ThunkUpdateConfig Nothing thunkConfig) target
449455
@@ -572,7 +578,7 @@ nixBuildThunkAttrWithCache thunkDir attr = do
572578 let cacheErrHandler e
573579 | isDoesNotExistError e = return Nothing -- expected from a cache miss
574580 | otherwise = putLog Error (T.pack $ displayException e) >> return Nothing
575− cacheDir = thunkDir </> ".attr-cache"
581+ cacheDir = thunkDir </> attrCacheFileName
576582 cachePath = cacheDir </> attr <.> "out"
577583 latestChange <- liftIO $ do
578584 createDirectoryIfMissing False cacheDir
@@ -699,7 +705,7 @@ packThunk' noTrail (ThunkPackConfig force thunkConfig) thunkDir = checkThunkDire
699705 Right (ThunkData_Checkout _) -> do
700706 withSpinner' ("Packing thunk " <> T.pack thunkDir)
701707 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
702− thunkPtr <- modifyThunkPtrByConfig thunkConfig <$> getThunkPtr (not force) thunkDir
708+ thunkPtr <- modifyThunkPtrByConfig thunkConfig <$> getThunkPtr (not force) thunkDir (_thunkConfig_private thunkConfig)
703709 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
704710 liftIO $ createThunk thunkDir thunkPtr
705711 pure thunkPtr
@@ -712,8 +718,8 @@ modifyThunkPtrByConfig (ThunkConfig markPrivate') ptr = case markPrivate' of
712718 ThunkSource_GitHub s -> ThunkSource_GitHub $ s { _gitHubSource_private = markPrivate }
713719 }
714720
715−getThunkPtr :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
716−getThunkPtr checkClean thunkDir = do
721+getThunkPtr :: forall m. MonadObelisk m => Bool -> FilePath -> Maybe Bool -> m ThunkPtr
722+getThunkPtr checkClean thunkDir mPrivate = do
717723 when checkClean $ ensureCleanGitRepo thunkDir True
718724 "thunk pack: thunk checkout contains unsaved modifications"
719725
@@ -727,7 +733,7 @@ getThunkPtr checkClean thunkDir = do
727733 ] ++ T.lines stashOutput
728734 True -> return ()
729735
730− -- Get current branch ``
736+ -- Get current branch
731737 (mCurrentBranch, mCurrentCommit) <- do
732738 b <- listToMaybe
733739 <$> T.lines
@@ -829,7 +835,7 @@ getThunkPtr checkClean thunkDir = do
829835 remoteUri <- case parseGitUri remoteUri' of
830836 Nothing -> failWith $ "Could not identify git remote: " <> remoteUri'
831837 Just uri -> pure uri
832− uriThunkPtr remoteUri mCurrentBranch mCurrentCommit
838+ uriThunkPtr remoteUri mPrivate mCurrentBranch mCurrentCommit
833839
834840-- | Get the latest revision available from the given source
835841getLatestRev :: MonadObelisk m => ThunkSource -> m ThunkRev
@@ -848,12 +854,12 @@ getLatestRev os = do
848854-- performance. If that doesn't work (e.g. authentication issue), we fall back
849855-- on just doing things the normal way for git repos in general, and save it as
850856-- a regular git thunk.
851−uriThunkPtr :: MonadObelisk m => GitUri -> Maybe Text -> Maybe Text -> m ThunkPtr
852−uriThunkPtr uri mbranch mcommit = do
857+uriThunkPtr :: MonadObelisk m => GitUri -> Maybe Bool -> Maybe Text -> Maybe Text -> m ThunkPtr
858+uriThunkPtr uri mPrivate mbranch mcommit = do
853859 commit <- case mcommit of
854860 Nothing -> gitGetCommitBranch uri mbranch >>= return . snd
855861 (Just c) -> return c
856− (src, rev) <- case uriToThunkSource uri mbranch of
862+ (src, rev) <- uriToThunkSource uri mPrivate mbranch >>= \case
857863 ThunkSource_GitHub s -> do
858864 rev <- runExceptT $ githubThunkRev s commit
859865 case rev of
@@ -876,8 +882,8 @@ uriThunkPtr uri mbranch mcommit = do
876882-- If the thunk is a GitHub thunk and fails, we do *not* fall back like with
877883-- `uriThunkPtr`. Unlike a plain URL, a thunk src explicitly states which method
878884-- should be employed, and so we respect that.
879−uriToThunkSource :: GitUri -> Maybe Text -> ThunkSource
880−uriToThunkSource (GitUri u)
885+uriToThunkSource :: MonadObelisk m => GitUri -> Maybe Bool -> Maybe Text -> m ThunkSource
886+uriToThunkSource (GitUri u) mPrivate
881887 | Right uriAuth <- URI.uriAuthority u
882888 , Just scheme <- URI.unRText <$> URI.uriScheme u
883889 , case scheme of
@@ -889,21 +895,53 @@ uriToThunkSource (GitUri u)
889895 s -> s `L.elem` [ "git", "https", "http" ] -- "http:" just redirects to "https:"
890896 && URI.unRText (URI.authHost uriAuth) == "github.com"
891897 , Just (_, owner :| [repoish]) <- URI.uriPath u
892− = \mbranch -> ThunkSource_GitHub $ GitHubSource
893− { _gitHubSource_owner = N $ URI.unRText owner
894− , _gitHubSource_repo = N $ let
895− repoish' = URI.unRText repoish
896− in fromMaybe repoish' $ T.stripSuffix ".git" repoish'
897− , _gitHubSource_branch = N <$> mbranch
898− , _gitHubSource_private = False -- TODO: Can we try something to infer this?
899− }
898+ = \mbranch -> do
899+ isPrivate <- getIsPrivate
900+ pure $ ThunkSource_GitHub $ GitHubSource
901+ { _gitHubSource_owner = N $ URI.unRText owner
902+ , _gitHubSource_repo = N $ let
903+ repoish' = URI.unRText repoish
904+ in fromMaybe repoish' $ T.stripSuffix ".git" repoish'
905+ , _gitHubSource_branch = N <$> mbranch
906+ , _gitHubSource_private = isPrivate
907+ }
900908
901− | otherwise = \mbranch -> ThunkSource_Git $ GitSource
902− { _gitSource_url = GitUri u
903− , _gitSource_branch = N <$> mbranch
904− , _gitSource_fetchSubmodules = False -- TODO: How do we determine if this should be true?
905− , _gitSource_private = False -- TODO: Can we try something to infer this?
906− }
909+ | otherwise = \mbranch -> do
910+ isPrivate <- getIsPrivate
911+ pure $ ThunkSource_Git $ GitSource
912+ { _gitSource_url = GitUri u
913+ , _gitSource_branch = N <$> mbranch
914+ , _gitSource_fetchSubmodules = False -- TODO: How do we determine if this should be true?
915+ , _gitSource_private = isPrivate
916+ }
917+ where
918+ getIsPrivate = maybe (guessGitRepoIsPrivate $ GitUri u) pure mPrivate
919+
920+guessGitRepoIsPrivate :: MonadObelisk m => GitUri -> m Bool
921+guessGitRepoIsPrivate uri = flip fix urisToTry $ \loop -> \case
922+ [] -> pure True
923+ uriAttempt:xs -> do
924+ result <- readCreateProcessWithExitCode $
925+ isolateGitProc $
926+ gitProcNoRepo
927+ [ "ls-remote"
928+ , "--quiet"
929+ , "--exit-code"
930+ , "--symref"
931+ , T.unpack $ gitUriToText uriAttempt
932+ ]
933+ case result of
934+ (ExitSuccess, _, _) -> pure False -- Must be a public repo
935+ _ -> loop xs
936+ where
937+ urisToTry = nubOrd $
938+ -- Include the original URI if it isn't using SSH because SSH will certainly fail.
939+ [uri | fmap URI.unRText (URI.uriScheme (unGitUri uri)) /= Just "ssh"] <>
940+ [changeScheme "https" uri, changeScheme "http" uri, changeScheme "git" uri]
941+ changeScheme scheme (GitUri u) = GitUri $ u
942+ { URI.uriScheme = URI.mkScheme scheme
943+ , URI.uriAuthority = (\x -> x { URI.authUserInfo = Nothing }) <$> URI.uriAuthority u
944+ }
907945
908946-- Funny signature indicates no effects depend on the optional branch name.
909947githubThunkRev
@@ -963,6 +1001,7 @@ gitThunkRev s commit = do
9631001--
9641002-- If the branch name is passed in, it is returned exactly as-is. If it is not
9651003-- passed it, the default branch of the repo is used instead.
1004+
9661005gitGetCommitBranch
9671006 :: MonadObelisk m => GitUri -> Maybe Text -> m (Text, CommitId)
9681007gitGetCommitBranch uri mbranch = withExitFailMessage ("Failure for git remote " <> uriMsg) $ do
lib/command/src/Obelisk/Command/Utils.hs+15−7
@@ -21,8 +21,6 @@ import Data.Maybe (maybeToList)
2121import Data.Text (Text)
2222import qualified Data.Text as T
2323import Data.Void (Void)
24−import System.Directory (canonicalizePath)
25−import System.Environment (getExecutablePath)
2624import System.Exit (ExitCode)
2725import System.Which (staticWhich)
2826import Text.Megaparsec as MP
@@ -31,12 +29,21 @@ import Text.Megaparsec.Char as MP
3129import Obelisk.App (MonadObelisk)
3230import Obelisk.CliApp
3331
34−getObeliskExe :: IO FilePath
35−getObeliskExe = getExecutablePath >>= canonicalizePath
36−
3732cp :: FilePath
3833cp = $(staticWhich "cp")
3934
35+ghcidExePath :: FilePath
36+ghcidExePath = $(staticWhich "ghcid")
37+
38+findExePath :: FilePath
39+findExePath = $(staticWhich "find")
40+
41+nixExePath :: FilePath
42+nixExePath = $(staticWhich "nix")
43+
44+nixBuildExePath :: FilePath
45+nixBuildExePath = $(staticWhich "nix-build")
46+
4047-- Check whether the working directory is clean
4148checkGitCleanStatus :: MonadObelisk m => FilePath -> Bool -> m Bool
4249checkGitCleanStatus repo withIgnored = do
@@ -81,8 +88,9 @@ isolateGitProc = setEnvOverride (overrides <>)
8188 overrides = M.fromList
8289 [ ("HOME", "/dev/null")
8390 , ("GIT_CONFIG_NOSYSTEM", "1")
84− , ("GIT_TERMINAL_PROMPT", "0")
85− , ("GIT_SSH_COMMAND", "ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no -o GSSAPIAuthentication=no")
91+ , ("GIT_TERMINAL_PROMPT", "0") -- git 2.3+
92+ , ("GIT_ASKPASS", "echo") -- pre git 2.3 to just use empty password
93+ , ("GIT_SSH_COMMAND", "ssh -o PreferredAuthentications password -o PubkeyAuthentication no -o GSSAPIAuthentication no")
8694 ]
8795
8896-- | Recursively copy a directory using `cp -a` -- TODO: Should use -rT instead of -a
lib/executable-config/lookup/src-other/Obelisk/ExecutableConfig/Lookup.hs+1−8
@@ -1,18 +1,11 @@
1−{-# LANGUAGE LambdaCase #-}
21{-# LANGUAGE OverloadedStrings #-}
32module Obelisk.ExecutableConfig.Lookup where
43
54import Data.Map (Map)
6−import qualified Data.Map as Map
75import Data.Text (Text)
8−import qualified Data.Text as T
96import Data.ByteString (ByteString)
10−import qualified Data.ByteString as BS
11−import System.Directory
12−import System.FilePath
13−import Control.Monad
147
15−import Obelisk.Configs.Internal.Directory
8+import Obelisk.Configs.Internal.Directory (getConfigsFromDirectory)
169
1710getConfigs :: IO (Map Text ByteString)
1811getConfigs = getConfigsFromDirectory "config"
lib/frontend/src/Obelisk/Frontend.hs+2−2
@@ -5,6 +5,7 @@
55{-# LANGUAGE LambdaCase #-}
66{-# LANGUAGE MultiParamTypeClasses #-}
77{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE PackageImports #-}
89{-# LANGUAGE RankNTypes #-}
910{-# LANGUAGE RecursiveDo #-}
1011{-# LANGUAGE ScopedTypeVariables #-}
@@ -45,7 +46,7 @@ import qualified GHCJS.DOM.History as DOM
4546import qualified GHCJS.DOM.Window as DOM
4647import Language.Javascript.JSaddle (MonadJSM, JSM, jsNull)
4748import GHCJS.DOM (currentDocument)
48−import GHCJS.DOM.Document (getHead)
49+import "ghcjs-dom" GHCJS.DOM.Document (getHead)
4950import GHCJS.DOM.Node (Node, removeChild_)
5051import GHCJS.DOM.NodeList (IsNodeList, item, getLength)
5152import GHCJS.DOM.ParentNode (querySelectorAll)
@@ -197,7 +198,6 @@ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = d
197198 , PrimMonad m
198199 , MonadSample DomTimeline (Performable m)
199200 , DOM.MonadJSM m
200− , Monad (Performable (Client (HydrationDomBuilderT s DomTimeline m)))
201201 , MonadFix (Client (HydrationDomBuilderT s DomTimeline m))
202202 , MonadFix (Performable m)
203203 , MonadFix m
lib/frontend/src/Obelisk/Frontend/Cookie.hs+2−1
@@ -6,6 +6,7 @@
66{-# LANGUAGE FlexibleInstances #-}
77{-# LANGUAGE GeneralizedNewtypeDeriving #-}
88{-# LANGUAGE MultiParamTypeClasses #-}
9+{-# LANGUAGE PackageImports #-}
910{-# LANGUAGE RankNTypes #-}
1011{-# LANGUAGE TypeFamilies #-}
1112{-# LANGUAGE UndecidableInstances #-}
@@ -22,7 +23,7 @@ import Data.Text.Encoding (encodeUtf8)
2223import Reflex
2324import Reflex.Host.Class
2425import Reflex.Dom.Core
25−import GHCJS.DOM.Document (getCookie, Document)
26+import "ghcjs-dom" GHCJS.DOM.Document (getCookie, Document)
2627import GHCJS.DOM.Types (MonadJSM)
2728import Web.Cookie
2829
lib/selftest/src/Obelisk/SelfTest.hs+131−67
@@ -1,3 +1,4 @@
1+{-# LANGUAGE DoAndIfThenElse #-}
12{-# LANGUAGE EmptyCase #-}
23{-# LANGUAGE LambdaCase #-}
34{-# LANGUAGE OverloadedStrings #-}
@@ -23,13 +24,13 @@ import qualified Network.HTTP.Client as HTTP
2324import qualified Network.HTTP.Types as HTTP
2425import qualified Network.Socket as Socket
2526import Shelly hiding (cp)
26−import System.Directory (withCurrentDirectory, getDirectoryContents)
27+import System.Directory (getCurrentDirectory, withCurrentDirectory, getDirectoryContents)
2728import System.Environment
2829import System.Exit (ExitCode (..))
2930import System.FilePath (replaceBaseName, takeBaseName)
3031import System.Which (staticWhich)
3132import qualified System.Info
32−import System.IO (Handle, hClose)
33+import System.IO (Handle, hClose, hIsEOF, hGetContents)
3334import System.IO.Temp
3435import System.Process (readProcessWithExitCode, CreateProcess(cwd), readCreateProcessWithExitCode, proc)
3536import Test.Hspec
@@ -52,11 +53,32 @@ cp = $(staticWhich "cp")
5253cabalPath :: FilePath
5354cabalPath = $(staticWhich "cabal")
5455
56+gitPath :: FilePath
57+gitPath = $(staticWhich "git")
58+
59+chownPath :: FilePath
60+chownPath = $(staticWhich "chown")
61+
62+chmodPath :: FilePath
63+chmodPath = $(staticWhich "chmod")
64+
65+whoamiPath :: FilePath
66+whoamiPath = $(staticWhich "whoami")
67+
68+nixBuildPath :: FilePath
69+nixBuildPath = $(staticWhich "nix-build")
70+
71+lnPath :: FilePath
72+lnPath = $(staticWhich "ln")
73+
74+rmPath :: FilePath
75+rmPath = $(staticWhich "rm")
76+
5577gitUserConfig :: [Text]
5678gitUserConfig = ["-c", "user.name=Obelisk Selftest", "-c", "user.email=noreply@example.com"]
5779
5880commit :: Text -> Sh ()
59−commit msg = void $ run "git" $ gitUserConfig <> [ "commit"
81+commit msg = void $ run gitPath $ gitUserConfig <> [ "commit"
6082 , "--no-gpg-sign"
6183 , "--allow-empty"
6284 , "-m"
@@ -76,6 +98,14 @@ tshow = T.pack . show
7698shellyOb :: MonadIO m => (Sh a -> Sh a) -> Sh a -> m a
7799shellyOb f obTest = shelly $ f obTest
78100
101+-- Set "ob" in a single place so it can be replaced with a
102+-- link to obelisk in the nix store in the future,
103+-- and avoid PATH hacking before calling this script.
104+ob :: FilePath
105+ob = "ob"
106+
107+augmentWithVerbosity :: (String -> [Text] -> a) -> String -> Bool -> [Text] -> a
108+augmentWithVerbosity runner executable isVerbose args = runner executable $ (if isVerbose then ("-v" :) else id) args
79109
80110main :: IO ()
81111main = do
@@ -84,33 +114,43 @@ main = do
84114 unless isVerbose $
85115 putStrLn "Tests may take longer to run if there are unbuilt derivations: use -v for verbose output"
86116 let verbosity = bool silently verbosely isVerbose
87− nixBuild args = run "nix-build" ("--no-out-link" : args)
88− obeliskImplRaw <- fromString <$> getEnv "OBELISK_IMPL"
117+ nixBuild args = run nixBuildPath ("--no-out-link" : args)
118+ obeliskImplDirtyReadOnly <- fromString <$> getCurrentDirectory
119+ let runOb_ = augmentWithVerbosity run_ ob isVerbose
120+ let runOb = augmentWithVerbosity run ob isVerbose
121+ let testObRunInDir' = augmentWithVerbosity testObRunInDir ob isVerbose ["run"]
122+ let testThunkPack' = augmentWithVerbosity testThunkPack ob isVerbose []
89123 let
90− withObeliskImpl f =
124+ withObeliskImplClean f =
125+ withSystemTempDirectory "obelisk-impl-clean" $ \(fromString -> obeliskImpl) -> do
126+ void . shellyOb verbosity $ chdir obeliskImpl $ do
127+ run_ gitPath ["clone", "file://" <> toTextIgnore obeliskImplDirtyReadOnly, toTextIgnore obeliskImpl]
128+ f obeliskImpl
129+
130+ withObeliskImplDirty f =
91131 withSystemTempDirectory "obelisk-impl-copy" $ \(fromString -> obeliskImpl) -> do
92132 void . shellyOb verbosity $ chdir obeliskImpl $ do
93− user <- T.strip <$> run "whoami" []
94− run_ cp ["-rT", toTextIgnore obeliskImplRaw, toTextIgnore obeliskImpl]
95− run_ "chown" ["-R", user, toTextIgnore obeliskImpl]
96− run_ "chmod" ["-R", "g-rw,o-rw", toTextIgnore obeliskImpl]
133+ user <- T.strip <$> run whoamiPath []
134+ run_ cp ["-rT", "--no-preserve=mode", toTextIgnore obeliskImplDirtyReadOnly, toTextIgnore obeliskImpl]
135+ run_ chownPath ["-R", user, toTextIgnore obeliskImpl]
136+ run_ chmodPath ["-R", "g-rw,o-rw", toTextIgnore obeliskImpl]
97137 f obeliskImpl
98138
99139 withInitCache f obeliskImpl =
100140 withSystemTempDirectory "init Cache λ" $ \initCache -> do
101141 -- Setup the ob init cache
102142 void . shellyOb verbosity $ chdir initCache $ do
103− run_ "ob" ["init", "--symlink", toTextIgnore obeliskImpl]
104− run_ "git" ["init"]
143+ runOb_ ["init", "--symlink", toTextIgnore obeliskImpl]
144+ run_ gitPath ["init"]
105145
106146 f initCache
107147
108− withObeliskImplAndInitCache f =
109− withObeliskImpl $ \impl -> withInitCache (f impl) impl
148+ withObeliskImplDirtyAndInitCache f =
149+ withObeliskImplDirty $ \impl -> withInitCache (f impl) impl
110150
111151 httpManager <- HTTP.newManager HTTP.defaultManagerSettings
112152
113− withObeliskImplAndInitCache $ \obeliskImpl initCache ->
153+ withObeliskImplDirtyAndInitCache $ \obeliskImplDirty initCache ->
114154 hspec $ parallel $ do
115155 let shelly_ = void . shellyOb verbosity
116156
@@ -130,41 +170,41 @@ main = do
130170
131171 -- To be used in tests that change the obelisk impl directory
132172 inTmpObInitWithImplCopy f = inTmpObInit $ \dir ->
133− withObeliskImpl $ \(fromString -> implCopy) -> do
134− run_ "rm" [thunk]
135− run_ "ln" ["-s", implCopy, thunk]
173+ withObeliskImplClean $ \(fromString -> implClean) -> do
174+ run_ rmPath [thunk]
175+ run_ lnPath ["-s", implClean, thunk]
136176 f dir
137177
138178 assertRevEQ a b = liftIO . assertEqual "" "" =<< diff a b
139179 assertRevNE a b = liftIO . assertBool "" . (/= "") =<< diff a b
140180
141− revParseHead = T.strip <$> run "git" ["rev-parse", "HEAD"]
181+ revParseHead = T.strip <$> run gitPath ["rev-parse", "HEAD"]
142182
143183 commitAll = do
144− run_ "git" ["add", "."]
184+ run_ gitPath ["add", "."]
145185 commit "checkpoint"
146186 revParseHead
147187
148188 thunk = ".obelisk/impl"
149− update = run "ob" ["thunk", "update", thunk] >> commitAll
150− pack = run "ob" ["thunk", "pack", thunk] >> commitAll
151− unpack = run "ob" ["thunk", "unpack", thunk] >> commitAll
189+ update = runOb ["thunk", "update", thunk] >> commitAll
190+ pack = runOb ["thunk", "pack", thunk] >> commitAll
191+ unpack = runOb ["thunk", "unpack", thunk] >> commitAll
152192
153− diff a b = run "git" ["diff", a, b]
193+ diff a b = run gitPath ["diff", a, b]
154194
155195 describe "ob init" $ parallel $ do
156− it "works with default impl" $ inTmp $ \_ -> run "ob" ["init"]
157− it "works with master branch impl" $ inTmp $ \_ -> run "ob" ["init", "--branch", "master"]
158− it "works with symlink" $ inTmp $ \_ -> run "ob" ["init", "--symlink", toTextIgnore obeliskImpl]
196+ it "works with default impl" $ inTmp $ \_ -> runOb ["init"]
197+ it "works with master branch impl" $ inTmp $ \_ -> runOb ["init", "--branch", "master"]
198+ it "works with symlink" $ inTmp $ \_ -> runOb ["init", "--symlink", toTextIgnore obeliskImplDirty]
159199 it "doesn't silently overwrite existing files" $ withSystemTempDirectory "ob-init λ" $ \dir -> do
160− let p force = (System.Process.proc "ob" $ "--no-handoff" : "init" : ["--force"|force]) { cwd = Just dir }
200+ let p force = (System.Process.proc ob $ "--no-handoff" : "-v" : "init" : ["--force"|force]) { cwd = Just dir }
161201 (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p False) ""
162202 (ExitFailure _, _, _) <- readCreateProcessWithExitCode (p False) ""
163203 (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p True) ""
164204 pure ()
165205
166206 it "doesn't create anything when given an invalid impl" $ inTmp $ \tmp -> do
167− void $ errExit False $ run "ob" ["init", "--symlink", "/dev/null"]
207+ void $ errExit False $ runOb ["init", "--symlink", "/dev/null"]
168208 ls tmp >>= liftIO . assertEqual "" []
169209
170210 it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $
@@ -175,17 +215,17 @@ main = do
175215 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
176216 -- when not run by 'nix-build --attr selftest'
177217 describe "ob run" $ {- NOT parallel $ -} do
178− it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir Nothing httpManager
179− it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir (Just "frontend") httpManager
218+ it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir' Nothing httpManager
219+ it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir' (Just "frontend") httpManager
180220 it "works with differently named cabal files" $ inTmpObInit $ \_ -> do
181221 changeCabalPackageName "backend/backend.cabal" "new-backend"
182− testObRunInDir Nothing httpManager
222+ testObRunInDir' Nothing httpManager
183223
184224 describe "obelisk project" $ parallel $ do
185− it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskImpl]
186− it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", toTextIgnore obeliskImpl]
187− it "can build obelisk shell" $ inTmpObInit $ \_ -> nixBuild ["-A", "shell", toTextIgnore obeliskImpl]
188− it "can build everything" $ inTmpObInit $ \_ -> nixBuild [toTextIgnore obeliskImpl]
225+ it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskImplDirty]
226+ it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", toTextIgnore obeliskImplDirty]
227+ it "can build obelisk shell" $ inTmpObInit $ \_ -> nixBuild ["-A", "shell", toTextIgnore obeliskImplDirty]
228+ it "can build everything" $ inTmpObInit $ \_ -> nixBuild [toTextIgnore obeliskImplDirty]
189229
190230 describe "blank initialized project" $ parallel $ do
191231
@@ -235,76 +275,93 @@ main = do
235275
236276 it "unpacks the correct branch" $ withTmp $ \dir -> do
237277 let branch = "master"
238− run_ "git" ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch]
239− run_ "ob" ["thunk", "pack", toTextIgnore dir]
240− run_ "ob" ["thunk", "unpack", toTextIgnore dir]
241− branch' <- chdir dir $ run "git" ["rev-parse", "--abbrev-ref", "HEAD"]
278+ run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch]
279+ runOb_ ["thunk", "pack", toTextIgnore dir]
280+ runOb_ ["thunk", "unpack", toTextIgnore dir]
281+ branch' <- chdir dir $ run gitPath ["rev-parse", "--abbrev-ref", "HEAD"]
242282 liftIO $ assertEqual "" branch (T.strip branch')
243283
244284 it "can pack and unpack plain git repos" $
245285 shelly_ $ withSystemTempDirectory "git-repo" $ \dir -> do
246286 let repo = toTextIgnore $ dir </> ("repo" :: String)
247− run_ "git" ["clone", "https://github.com/haskell/process.git", repo]
287+ run_ gitPath ["clone", "https://github.com/haskell/process.git", repo]
248288 origHash <- chdir (fromText repo) revParseHead
249289
250− run_ "ob" ["thunk", "pack", repo]
290+ runOb_ ["thunk", "pack", repo]
251291 packedFiles <- Set.fromList <$> ls (fromText repo)
252292 liftIO $ assertEqual "" packedFiles $ Set.fromList $ (repo </>) <$>
253293 ["default.nix", "github.json", ".attr-cache" :: String]
254294
255− run_ "ob" ["thunk", "unpack", repo]
295+ runOb_ ["thunk", "unpack", repo]
256296 chdir (fromText repo) $ do
257297 unpackHash <- revParseHead
258298 assertRevEQ origHash unpackHash
259299
260− testThunkPack $ fromText repo
300+ testThunkPack' $ fromText repo
261301
262− it "aborts thunk pack when there are uncommitted files" $ inTmpObInit $ \dir -> do
263− testThunkPack (dir </> thunk)
302+ it "aborts thunk pack when there are uncommitted files" $ inTmpObInitWithImplCopy $ \dir -> do
303+ testThunkPack' (dir </> thunk)
264304
265305 describe "ob thunk update --branch" $ parallel $ do
266306 it "can change a thunk to the latest version of a desired branch" $ withTmp $ \dir -> do
267307 let branch1 = "master"
268308 branch2 = "develop"
269− run_ "git" ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch1]
270− run_ "ob" ["thunk" , "pack", toTextIgnore dir]
271− run_ "ob" ["thunk", "update", toTextIgnore dir, "--branch", branch2]
309+ run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch1]
310+ runOb_ ["thunk" , "pack", toTextIgnore dir]
311+ runOb_ ["thunk", "update", toTextIgnore dir, "--branch", branch2]
272312
273313 it "doesn't create anything when given an invalid branch" $ withTmp $ \dir -> do
274314 let checkDir dir' = liftIO $ getDirectoryContents $ T.unpack $ toTextIgnore dir'
275− run_ "git" ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", "master"]
276− run_ "ob" ["thunk" , "pack", toTextIgnore dir]
315+ run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", "master"]
316+ runOb_ ["thunk" , "pack", toTextIgnore dir]
277317 startingContents <- checkDir dir
278− void $ errExit False $ run "ob" ["thunk", "update", toTextIgnore dir, "--branch", "dumble-palooza"]
318+ void $ errExit False $ runOb ["thunk", "update", toTextIgnore dir, "--branch", "dumble-palooza"]
279319 checkDir dir >>= liftIO . assertEqual "" startingContents
280320
321+ describe "ob hoogle" $ {- NOT parallel -} do
322+ it "starts a hoogle server on the given port" $ inTmpObInit $ \_ -> do
323+ [p0] <- liftIO $ getFreePorts 1
324+ maskExitSuccess $ runHandle "ob" ["hoogle", "--port", T.pack (show p0)] $ \stdout -> fix $ \loop -> do
325+ ln <- liftIO $ T.hGetLine stdout
326+ echo ln
327+ let search = "Server starting on port " <> T.pack (show p0)
328+ case search `T.isInfixOf` ln of
329+ False -> loop -- keep waiting
330+ True -> do
331+ let req uri = liftIO $ HTTP.parseRequest uri >>= flip HTTP.httpLbs httpManager
332+ req ("http://127.0.0.1:" <> show p0) >>= \r -> case HTTP.responseStatus r == HTTP.ok200 of
333+ False -> errorExit $ "Request to hoogle server failed: " <> T.pack (show r)
334+ True -> exit 0
335+
336+maskExitSuccess :: Sh () -> Sh ()
337+maskExitSuccess = handle_sh (\case ExitSuccess -> pure (); e -> throw e)
281338
282339-- | Run `ob run` in the given directory (maximum of one level deep)
283−testObRunInDir :: Maybe FilePath -> HTTP.Manager -> Sh ()
284−testObRunInDir mdir httpManager = handle_sh (\case ExitSuccess -> pure (); e -> throw e) $ do
340+testObRunInDir :: String -> [Text] -> Maybe FilePath -> HTTP.Manager -> Sh ()
341+testObRunInDir executable extraArgs mdir httpManager = maskExitSuccess $ do
285342 [p0, p1] <- liftIO $ getFreePorts 2
286343 let uri p = "http://localhost:" <> T.pack (show p) <> "/" -- trailing slash required for comparison
287344 writefile "config/common/route" $ uri p0
288− maybe id chdir mdir $ runHandle "ob" ["run", "-v"] $ \stdout -> do
289− firstUri <- handleObRunStdout httpManager stdout
345+ maybe id chdir mdir $ runHandles executable extraArgs [] $ \_stdin stdout stderr -> do
346+ firstUri <- handleObRunStdout httpManager stdout stderr
290347 let newUri = uri p1
291348 when (firstUri == newUri) $ errorExit $
292349 "Startup URI (" <> firstUri <> ") is the same as test URI (" <> newUri <> ")"
293350 maybe id (\_ -> chdir "..") mdir $ alterRouteTo newUri stdout
294− runningUri <- handleObRunStdout httpManager stdout
351+ runningUri <- handleObRunStdout httpManager stdout stderr
295352 if runningUri /= newUri
296353 then errorExit $ "Reloading failed: expected " <> newUri <> " but got " <> runningUri
297354 else exit 0
298355
299−testThunkPack :: FilePath -> Sh ()
300−testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $ \file handle -> do
301− let pack' = readProcessWithExitCode "ob" ["thunk", "pack", T.unpack $ toTextIgnore path'] ""
356+testThunkPack :: String -> [Text] -> FilePath -> Sh ()
357+testThunkPack executable args path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $ \file handle -> do
358+ let pack' = readProcessWithExitCode executable (T.unpack <$> ["thunk", "pack", toTextIgnore path'] ++ args) ""
302359 ensureThunkPackFails q = liftIO $ pack' >>= \case
303360 (code, out, err)
304− | code == ExitSuccess -> fail "ob thunk pack succeeded when it should have failed"
361+ | code == ExitSuccess -> fail $ "ob thunk pack succeeded when it should have failed with error '" <> show q <> "'"
305362 | q `T.isInfixOf` T.pack (out <> err) -> pure ()
306− | otherwise -> fail $ "ob thunk pack failed for an unexpected reason: " <> show out <> "\nstderr: " <> err
307− git = chdir path' . run "git"
363+ | otherwise -> fail $ "ob thunk pack failed for an unexpected reason, expecting '" <> show q <> "', received: " <> show out <> "\nstderr: " <> err
364+ git = chdir path' . run gitPath
308365 -- Untracked files
309366 ensureThunkPackFails "Untracked files"
310367 void $ git ["add", T.pack file]
@@ -338,9 +395,12 @@ alterRouteTo uri stdout = do
338395 "Reloading failed: " <> T.pack (show t)
339396
340397-- | Handle stdout of `ob run`: check that the frontend and backend servers are started correctly
341−handleObRunStdout :: HTTP.Manager -> Handle -> Sh Text
342−handleObRunStdout httpManager stdout = flip fix (ObRunState_Init, []) $ \loop (state, msgs) ->
343− liftIO (T.hGetLine stdout) >>= \t -> case state of
398+handleObRunStdout :: HTTP.Manager -> Handle -> Handle -> Sh Text
399+handleObRunStdout httpManager stdout stderr = flip fix (ObRunState_Init, []) $ \loop (state, msgs) -> do
400+ isEOF <- liftIO $ hIsEOF stdout
401+ if isEOF
402+ then handleObRunError msgs
403+ else liftIO (T.hGetLine stdout) >>= \t -> case state of
344404 ObRunState_Init
345405 | "Running test..." `T.isPrefixOf` t -> loop (ObRunState_BackendStarted, msgs)
346406 ObRunState_Startup
@@ -352,8 +412,12 @@ handleObRunStdout httpManager stdout = flip fix (ObRunState_Init, []) $ \loop (s
352412 obRunCheck httpManager stdout uri
353413 pure uri
354414 | not (T.null t) -> errorExit $ "Started: " <> t -- If theres any other output here, startup failed
355− _ | "Failed" `T.isPrefixOf` t -> errorExit $ "ob run failed: " <> T.unlines (reverse $ t : msgs)
415+ _ | "Failed" `T.isPrefixOf` t -> handleObRunError (t : msgs)
356416 | otherwise -> loop (state, t : msgs)
417+ where
418+ handleObRunError msgs = do
419+ stderrContent <- liftIO $ hGetContents stderr
420+ errorExit $ "ob run failed: " <> T.unlines (reverse msgs) <> " stderr: " <> T.pack stderrContent
357421
358422-- | Make requests to frontend/backend servers to check they are working properly
359423obRunCheck :: HTTP.Manager -> Handle -> Text -> Sh ()
lib/snap-extras/obelisk-snap-extras.cabal+1−1
@@ -1,6 +1,6 @@
11name: obelisk-snap-extras
22version: 0.1
3−synopsys: Extra functionality for Snap that should be considered for upstreaming
3+synopsis: Extra functionality for Snap that should be considered for upstreaming
44license: BSD3
55build-type: Simple
66cabal-version: >=1.2
lib/snap-extras/src/Obelisk/Snap/Extras.hs+1−1
@@ -2,13 +2,13 @@
22module Obelisk.Snap.Extras
33 ( cachePermanently
44 , doNotCache
5+ , ensureSecure
56 , serveFileIfExists
67 , serveFileIfExistsAs
78 ) where
89
910import Control.Monad.IO.Class
1011import Data.ByteString (ByteString)
11−import Data.Monoid
1212import Data.String
1313import Snap.Core
1414import Snap.Util.FileServe
skeleton/backend/src/Backend.hs−3
@@ -1,6 +1,3 @@
1−{-# LANGUAGE FlexibleContexts #-}
2−{-# LANGUAGE RankNTypes #-}
3−{-# LANGUAGE TypeFamilies #-}
41module Backend where
52
63import Common.Route