on master help sign in
Merge remote-tracking branch 'origin/qa' into autodetectPrivateRepo
commit4d6bbfe03aa923330488fbc85749436fe87927b9 authorElliot Cameron <eacameron@gmail.com> dateThu 30 Jan 2020, 23:52 (7 years ago) parent99887d0f1c4331
33 files changed+961−568
.github/pull_request_template.md+8 ChangeLog.md+7 README.md+21−10 all-builds.nix+61−52 all-tests.nix+41−3 default.nix+126−89 dep/reflex-platform/github.json+3−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+11−10 lib/command/src/Obelisk/Command.hs+33−59 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+197−116 lib/command/src/Obelisk/Command/Utils.hs+12−5 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/route/src/Obelisk/Route.hs+11−4 lib/selftest/src/Obelisk/SelfTest.hs+153−80 lib/snap-extras/obelisk-snap-extras.cabal+1−1 lib/snap-extras/src/Obelisk/Snap/Extras.hs+1−1 release.nix+3−1 skeleton/backend/src/Backend.hs−3 skeleton/frontend/frontend.cabal+1 skeleton/frontend/src/Frontend.hs+12
.github/pull_request_template.mdnew file+8
@@ -0,0 +1,8 @@
1+[Provide a clear overview of your changes.]
2+
3+I have:
4+
5+ - [ ] Based work on latest `develop` branch
6+ - [ ] Looked for lint in my changes with `hlint .` (lint found code you did not write can be left alone)
7+ - [ ] Run the test suite: `$(nix-build -A selftest --no-out-link)`
8+ - [ ] (Optional) Run CI tests locally: `nix-build release.nix -A build.x86_64-linux --no-out-link` (or `x86_64-darwin` on macOS)
ChangeLog.md+7
@@ -4,6 +4,12 @@ This project's release branch is `master`. This log is written from the perspect
44
55## Unreleased
66
7+* 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+
11+## v0.4.0.0 - 2020-01-10
12+
713* Bump reflex-platform which, notably, bumps nixpkgs to 19.09. ([#585](https://github.com/obsidiansystems/obelisk/pull/585))
814* Add new thunk loader for Git repositories that supports `file://` Git remotes and supports private repositories via `builtins.fetchGit` for private repositories (when the `git.json` file specifies `"private": true`). ([#594](https://github.com/obsidiansystems/obelisk/pull/594))
915* Add a new thunk loader for GitHub repositories that uses `builtins.fetchTarball` for public repositories to increase loader performance and uses `fetchFromGitHub` for private repositories (when the `github.json` file specifies `"private": true`). Note that `fetchFromGitHub` requires some Nix configuration for the Nix builder to access the repository. If `ob thunk pack` fails in this case, use `-v` to see Nix's helpful message. ([#594](https://github.com/obsidiansystems/obelisk/pull/594))
@@ -11,6 +17,7 @@ This project's release branch is `master`. This log is written from the perspect
1117* Improve error messaging when a dependency doesn't have the expected `.cabal` or `package.yaml` file. ([#597](https://github.com/obsidiansystems/obelisk/pull/597))
1218* Improve the skeleton in small ways. ([#593](https://github.com/obsidiansystems/obelisk/pull/593), [#589](https://github.com/obsidiansystems/obelisk/pull/589))
1319* Fix `ob` commands to again support running from any subdirectory of an obelisk project ([#591](https://github.com/obsidiansystems/obelisk/pull/591))
20+* Add `reflex-platform-func` argument to Obelisk's `default.nix`. It defaults to it's prior behavior of using the reflex-platform in in `dep`. ([#612](https://github.com/obsidiansystems/obelisk/pull/612))
1421
1522## v0.3.0.0 - 2019-12-20
1623
README.md+21−10
@@ -4,7 +4,6 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
44
55- [Installing Obelisk](#installing-obelisk)
66- [Developing an Obelisk project](#developing-an-obelisk-project)
7− - [Hoogle](#hoogle)
87 - [Adding Packages](#adding-packages)
98 - [Adding Package Overrides](#adding-package-overrides)
109 - [Running over https](#running-over-https)
@@ -23,14 +22,15 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
23221. Set up nix caches
2423 1. If you are running NixOS, add this to `/etc/nixos/configuration.nix`:
2524 ```
26− nix.binaryCaches = [ "https://cache.nixos.org/" "https://nixcache.reflex-frp.org" ];
25+ nix.binaryCaches = [ "https://nixcache.reflex-frp.org" ];
2726 nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];
2827 ```
2928 and rebuild your NixOS configuration (e.g. `sudo nixos-rebuild switch`).
3029 1. If you are using another operating system or linux distribution, ensure that these lines are present in your Nix configuration file (`/etc/nix/nix.conf` on most systems; [see full list](https://nixos.org/nix/manual/#sec-conf-file)):
3130 ```
32− substituters = https://cache.nixos.org https://nixcache.reflex-frp.org
33− trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
31+ binary-caches = https://cache.nixos.org https://nixcache.reflex-frp.org
32+ binary-cache-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
33+ binary-caches-parallel-connections = 40
3434 ```
3535 * other Linux: enable sandboxing (see these [issue172](https://github.com/obsidiansystems/obelisk/issues/172#issuecomment-411507818) or [issue6](https://github.com/obsidiansystems/obelisk/issues/6) if you run into build problems)
3636 ```
@@ -111,12 +111,6 @@ Firefox will not be able to properly run the development website due to [issue 4
111111
112112Every time you change the Haskell source files in frontend, common or backend, `ob run` will automatically recompile the modified files and reload the server. Furthermore, it will display on screen compilation errors and warnings if any.
113113
114−### Hoogle
115−
116−To enter a nix-shell from which you can run the hoogle command-line client or a hoogle server for your project:
117−
118−`nix-shell -A shells.ghc --arg withHoogle true`
119−
120114### Adding packages
121115
122116In order to add package dependencies, declare them under the build-depends field in the appropriate cabal files (backend, common, and frontend each have their own). The corresponding Nix packages will automatically be selected when building.
@@ -150,6 +144,23 @@ project ./. ({ pkgs, ... }: {
150144
151145For further information see [the Haskell section](https://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure) of nixpkgs Contributors Guide.
152146
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+
153164### Running over https
154165
155166To run your app locally over https, update the protocol in `config/common/route` to `https`, and then use `ob run` as normal.
all-builds.nix+61−52
@@ -3,7 +3,10 @@
33 iosSdkVersion = "10.2";
44 }
55, local-self ? import ./. self-args
6−, supportedSystems ? [ "x86_64-linux" "x86_64-darwin" ]
6+, supportedSystems ? [
7+ "x86_64-linux"
8+ "x86_64-darwin"
9+ ]
710}:
811
912let
@@ -35,59 +38,65 @@ let
3538 (name: { inherit name; value = pkgsSet.${name}; })
3639 pnames);
3740
38− concatDepends = let
39− extractDeps = x: (x.override {
40− mkDerivation = drv: {
41− out = builtins.concatLists [
42− (drv.buildDepends or [])
43− (drv.libraryHaskellDepends or [])
44− (drv.executableHaskellDepends or [])
45− ];
46− };
47− }).out;
48− in pkgAttrs: builtins.concatLists (map extractDeps (builtins.attrValues pkgAttrs));
41+ collect = v:
42+ if lib.isDerivation v then [v]
43+ else if lib.isAttrs v then lib.concatMap collect (builtins.attrValues v)
44+ else if lib.isList v then lib.concatMap collect v
45+ else [];
4946
5047 perPlatform = lib.genAttrs cacheBuildSystems (system: let
51− obelisk = import ./. (self-args // { inherit system; });
52− reflex-platform = obelisk.reflex-platform;
53− ghc = pnameToAttrs
54− obelisk.haskellPackageSets.ghc
55− obeliskPackagesBackend;
56− ghcjs = pnameToAttrs
57− obelisk.haskellPackageSets.ghcjs
58− obeliskPackagesCommon;
59− cachePackages = builtins.concatLists [
60− (builtins.attrValues ghc)
61− (builtins.attrValues ghcjs)
62− (concatDepends ghc)
63− (concatDepends ghcjs)
64− (lib.optional reflex-platform.androidSupport androidSkeleton)
65− (lib.optional reflex-platform.iosSupport iosSkeleton)
66− [ command serverSkeletonExe serverSkeletonShell ]
67− ];
68− command = obelisk.command;
69− skeleton = import ./skeleton { inherit obelisk; };
70− serverSkeletonExe = skeleton.exe;
71− # TODO fix nixpkgs so it doesn't try to run the result of haskell shells as setup hooks.
72− serverSkeletonShell = local-self.nixpkgs.runCommand "shell-safe-for-dep" {} ''
73− touch "$out"
74− echo "return" >> "$out"
75− cat "${skeleton.shells.ghc}" >> "$out"
76− '';
77− androidSkeleton = (import ./skeleton { inherit obelisk; }).android.frontend;
78− iosSkeleton = (import ./skeleton { inherit obelisk; }).ios.frontend;
79− in {
80− inherit
81− command
82− ghc ghcjs
83− serverSkeletonExe
84− serverSkeletonShell
85− ;
86− cache = reflex-platform.pinBuildInputs "obelisk-${system}" cachePackages;
87− } // lib.optionalAttrs reflex-platform.androidSupport {
88− inherit androidSkeleton;
89− } // lib.optionalAttrs reflex-platform.iosSupport {
90− inherit iosSkeleton;
48+ reflex-platform = import ./dep/reflex-platform { inherit system; };
49+
50+ mkPerProfiling = profiling: let
51+ obelisk = import ./. (self-args // { inherit system profiling; });
52+ ghc = pnameToAttrs
53+ obelisk.haskellPackageSets.ghc
54+ obeliskPackagesBackend;
55+ ghcjs = pnameToAttrs
56+ obelisk.haskellPackageSets.ghcjs
57+ obeliskPackagesCommon;
58+ command = obelisk.command;
59+ skeleton = import ./skeleton { inherit obelisk; };
60+ serverSkeletonExe = skeleton.exe;
61+ # TODO fix nixpkgs so it doesn't try to run the result of haskell shells as setup hooks.
62+ serverSkeletonShell = local-self.nixpkgs.runCommand "shell-safe-for-dep" {} ''
63+ touch "$out"
64+ echo "return" >> "$out"
65+ cat "${skeleton.shells.ghc}" >> "$out"
66+ '';
67+ androidSkeleton = (import ./skeleton { inherit obelisk; }).android.frontend;
68+ iosSkeleton = (import ./skeleton { inherit obelisk; }).ios.frontend;
69+ nameSuffix = if profiling then "profiled" else "unprofiled";
70+ packages = {
71+ inherit
72+ command
73+ serverSkeletonShell
74+ ghc
75+ ;
76+ } // lib.optionalAttrs (!profiling) {
77+ inherit
78+ ghcjs
79+ serverSkeletonExe
80+ ;
81+ } // lib.optionalAttrs reflex-platform.androidSupport {
82+ inherit androidSkeleton;
83+ } // lib.optionalAttrs reflex-platform.iosSupport {
84+ inherit iosSkeleton;
85+ };
86+ in packages // {
87+ cache = reflex-platform.pinBuildInputs
88+ "obelisk-${system}-${nameSuffix}"
89+ (collect packages);
90+ };
91+
92+ perProfiling = {
93+ profiled = mkPerProfiling true;
94+ unprofiled = mkPerProfiling false;
95+ };
96+ in perProfiling // {
97+ cache = reflex-platform.pinBuildInputs
98+ "obelisk-${system}"
99+ (map (p: p.cache) (builtins.attrValues perProfiling));
91100 });
92101
93102 metaCache = local-self.reflex-platform.pinBuildInputs
all-tests.nix+41−3
@@ -46,6 +46,12 @@ in
4646 testScript =
4747 let
4848 privateKeyFile = pkgs.writeText "id_rsa" ''${snakeOilPrivateKey}'';
49+ thunkableSample = pkgs.writeText "default.nix" ''
50+ let pkgs = import <nixpkgs> {}; in pkgs.git
51+ '';
52+ invalidThunkableSample = pkgs.writeText "default.nix" ''
53+ let pkgs = import <nixpkgs> {}; in pkgtypo.git
54+ '';
4955 sshConfigFile = pkgs.writeText "ssh_config" ''
5056 Host *
5157 StrictHostKeyChecking no
@@ -83,7 +89,9 @@ in
8389 with subtest("test a git project can be configured with a remote using ssh"):
8490 client.succeed("mkdir -p ~/code/myapp")
8591 client.succeed("cd ~/code/myapp && git init")
86− client.succeed("cd ~/code/myapp && touch README")
92+ client.succeed(
93+ "cp ${thunkableSample} ~/code/myapp/default.nix"
94+ )
8795 client.succeed("cd ~/code/myapp && git add .")
8896 client.succeed('git config --global user.email "you@example.com"')
8997 client.succeed('git config --global user.name "Your Name"')
@@ -92,16 +100,46 @@ in
92100 "cd ~/code/myapp && git remote add origin root@githost:/root/myorg/myapp.git"
93101 )
94102
103+
95104 with subtest("test pushing code to the remote"):
96105 client.succeed("cd ~/code/myapp && git push -u origin master")
97106 client.succeed("cd ~/code/myapp && git status")
98107
99108 with subtest("test obelisk can pack"):
100− client.succeed("ob thunk pack ~/code/myapp")
109+ client.succeed("ob -v thunk pack ~/code/myapp")
101110 client.succeed("grep -qF 'git' ~/code/myapp/default.nix")
102111 client.succeed("grep -qF 'myorg' ~/code/myapp/git.json")
112+ client.succeed("ob -v thunk unpack ~/code/myapp")
113+
114+ with subtest("test obelisk can set the public / private flag"):
115+ client.succeed("ob -v thunk pack ~/code/myapp --private")
116+ client.fail("""grep -qF '"private": practice' ~/code/myapp/git.json""")
117+ client.succeed("""grep -qF '"private": true' ~/code/myapp/git.json""")
118+ client.succeed("nix-build ~/code/myapp")
119+ client.succeed("ob -v thunk unpack ~/code/myapp")
120+ client.succeed("ob -v thunk pack ~/code/myapp --public")
121+ client.succeed("""grep -qF '"private": false' ~/code/myapp/git.json""")
122+ client.succeed("nix-build ~/code/myapp")
123+ client.succeed("ob -v thunk unpack ~/code/myapp")
124+
125+ with subtest("test building an invalid thunk fails"):
126+ client.succeed("cd ~/code/myapp && git checkout -b bad")
127+ client.succeed(
128+ "cp ${invalidThunkableSample} ~/code/myapp/default.nix"
129+ )
130+ client.succeed("cd ~/code/myapp && git add .")
131+ client.succeed('git config --global user.email "you@example.com"')
132+ client.succeed('git config --global user.name "Your Name"')
133+ client.succeed('cd ~/code/myapp && git commit -m "Bad commit"')
134+ client.succeed("cd ~/code/myapp && git push -u origin bad")
135+ client.succeed("ob -v thunk pack ~/code/myapp --public")
136+ client.fail("nix-build ~/code/myapp")
137+ client.succeed("ob -v thunk unpack ~/code/myapp")
138+ client.succeed("cd ~/code/myapp && git checkout master")
103139
104140 with subtest("test obelisk can detect private repos"):
141+ client.succeed("ob -v thunk pack ~/code/myapp")
105142 client.succeed("""grep -qF '"private": true' ~/code/myapp/git.json""")
106− '';
143+ client.succeed("ob -v thunk unpack ~/code/myapp")
144+ '';
107145 }) {}
default.nix+126−89
@@ -67,7 +67,14 @@ let
6767 haskellLib.overrideCabal pkg (drv: { librarySystemDepends = [ pkgs.iproute ]; })
6868 );
6969 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
70− obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {};
70+ obelisk-selftest = haskellLib.overrideCabal (self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {}) {
71+ librarySystemDepends = [
72+ pkgs.cabal-install
73+ pkgs.coreutils
74+ pkgs.git
75+ pkgs.nix
76+ ];
77+ };
7178 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
7279 tabulation = self.callCabal2nix "tabulation" (cleanSource ./lib/tabulation) {};
7380 })
@@ -121,12 +128,11 @@ in rec {
121128 shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs);
122129
123130 selftest = pkgs.writeScript "selftest" ''
124− #!/usr/bin/env bash
131+ #!${pkgs.runtimeShell}
125132 set -euo pipefail
126133
127134 PATH="${command}/bin:$PATH"
128− export OBELISK_IMPL="$(mktemp -d)"
129− git clone file://${pathGit} $OBELISK_IMPL
135+ cd ${./.}
130136 "${ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" +RTS -N -RTS "$@"
131137 '';
132138 skeleton = pkgs.runCommand "skeleton" {
@@ -150,6 +156,7 @@ in rec {
150156 compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
151157 mkdir $out
152158 cd $out
159+ # TODO profiling + static shouldn't break and need an ad-hoc workaround like that
153160 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
154161 ${if optimizationLevel == null then ''
155162 ln -s all.unminified.js all.js
@@ -232,7 +239,7 @@ in rec {
232239 pkgs.runCommand "serverExe" {} ''
233240 mkdir $out
234241 set -eux
235− ln -s "${haskellLib.justStaticExecutables backend}"/bin/* $out/
242+ ln -s "${if profiling then backend else haskellLib.justStaticExecutables backend}"/bin/* $out/
236243 ln -s "${mkAssets assets}" $out/static.assets
237244 ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
238245 echo ${version} > $out/version
@@ -251,100 +258,130 @@ in rec {
251258 };
252259 };
253260
254−
255261 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
256− project = base: projectDefinition:
257− let projectOut = sys: (getReflexPlatform sys).project (args@{ nixpkgs, ... }:
258− let mkProject = { android ? null #TODO: Better error when missing
259− , ios ? null #TODO: Better error when missing
260− , packages ? {}
261− , overrides ? _: _: {}
262− , staticFiles ? base + /static
263− , tools ? _: []
264− , shellToolOverrides ? _: _: {}
265− , withHoogle ? false # Setting this to `true` makes shell reloading far slower
266− , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
267− }:
268− let frontendName = "frontend";
269− backendName = "backend";
270− commonName = "common";
271− staticName = "obelisk-generated-static";
272− staticFilesImpure = if lib.isDerivation staticFiles then staticFiles else toString staticFiles;
273− processedStatic = processAssets { src = staticFiles; };
274− # The packages whose names and roles are defined by this package
275− predefinedPackages = lib.filterAttrs (_: x: x != null) {
276− ${frontendName} = nullIfAbsent (base + "/frontend");
277− ${commonName} = nullIfAbsent (base + "/common");
278− ${backendName} = nullIfAbsent (base + "/backend");
279− };
280− combinedPackages = predefinedPackages // packages;
281− projectOverrides = self: super: {
282− ${staticName} = haskellLib.dontHaddock (self.callCabal2nix staticName processedStatic.haskellManifest {});
283− ${backendName} = haskellLib.addBuildDepend super.${backendName} self.obelisk-run;
284− };
285− totalOverrides = lib.composeExtensions projectOverrides overrides;
286− inherit (lib.strings) hasPrefix;
287− privateConfigDirs = ["config/backend"];
288− injectableConfig = builtins.filterSource (path: _:
289− !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
290− );
291− __androidWithConfig = configPath: {
292− ${if android == null then null else frontendName} = {
293− executableName = "frontend";
294− ${if builtins.pathExists staticFiles then "assets" else null} =
295− nixpkgs.obeliskExecutableConfig.platforms.android.inject
296− (injectableConfig configPath)
297− processedStatic.symlinked;
298− } // 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;
299350 };
300− __iosWithConfig = configPath: {
301− ${if ios == null then null else frontendName} = {
302− executableName = "frontend";
303− ${if builtins.pathExists staticFiles then "staticSrc" else null} =
304− nixpkgs.obeliskExecutableConfig.platforms.ios.inject
305− (injectableConfig configPath)
306− processedStatic.symlinked;
307− } // 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+ ;
308364 };
309− in {
310− inherit shellToolOverrides tools withHoogle;
311− overrides = totalOverrides;
312− packages = combinedPackages;
313− shells = {
314− ${if android == null && ios == null then null else "ghcSavedSplices"} = (lib.filter (x: lib.hasAttr x combinedPackages) [
315− commonName
316− frontendName
317− ]);
318− ghc = (lib.filter (x: lib.hasAttr x combinedPackages) [
319− backendName
320− commonName
321− frontendName
322− ]);
323− ghcjs = lib.filter (x: lib.hasAttr x combinedPackages) [
324− frontendName
325− commonName
326− ];
327365 };
328− android = __androidWithConfig (base + "/config");
329− ios = __iosWithConfig (base + "/config");
330− passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __iosWithConfig __androidWithConfig; };
331− };
332− in mkProject (projectDefinition args));
333− serverOn = sys: version: serverExe
334− (projectOut sys).ghc.backend
335− (projectOut system).ghcjs.frontend
336− (projectOut sys).passthru.staticFiles
337− (projectOut sys).passthru.__closureCompilerOptimizationLevel
366+ });
367+ in allConfig;
368+ in (mkProject (projectDefinition args)).projectConfig);
369+ mainProjectOut = projectOut system;
370+ serverOn = projectInst: version: serverExe
371+ projectInst.ghc.backend
372+ mainProjectOut.ghcjs.frontend
373+ projectInst.passthru.staticFiles
374+ projectInst.passthru.__closureCompilerOptimizationLevel
338375 version;
339− linuxExe = serverOn "x86_64-linux";
376+ linuxExe = serverOn (projectOut "x86_64-linux");
340377 dummyVersion = "Version number is only available for deployments";
341− in projectOut system // {
378+ in mainProjectOut // {
342379 linuxExeConfigurable = linuxExe;
343380 linuxExe = linuxExe dummyVersion;
344− exe = serverOn system dummyVersion;
381+ exe = serverOn mainProjectOut dummyVersion;
345382 server = args@{ hostName, adminEmail, routeHost, enableHttps, version }:
346383 server (args // { exe = linuxExe version; });
347− obelisk = import (base + "/.obelisk/impl") {};
384+ obelisk = import (base' + "/.obelisk/impl") {};
348385 };
349386 haskellPackageSets = {
350387 inherit (reflex-platform) ghc ghcjs;
dep/reflex-platform/github.json+3−3
@@ -1,7 +1,7 @@
11{
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
4− "branch": "master",
5− "rev": "72ca177da290dc5311c08683b8d167a205c9322e",
6− "sha256": "06pqml1iqc5fawwsqb7wq77gh5v07bgb92hz8r1hf0ysr0abafcg"
4+ "branch": "develop",
5+ "rev": "90351bdb2c99d416ea95fd5b8b474e8f9fab2942",
6+ "sha256": "0bbzz4ckdx0ilhhpd3drfbsc0zdb2rnhw2m3nls9villbs3lz3lh"
77}
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+11−10
@@ -1,5 +1,5 @@
11name: obelisk-command
2−version: 0.3
2+version: 0.4
33cabal-version: >= 1.8
44build-type: Simple
55
@@ -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+33−59
@@ -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)
@@ -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+197−116
@@ -3,47 +3,59 @@
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.Utils (findExePath, ghcidExePath, nixBuildExePath, nixExePath)
4453
4554data CabalPackageInfo = CabalPackageInfo
46− { _cabalPackageInfo_packageRoot :: FilePath
55+ { _cabalPackageInfo_packageFile :: FilePath
56+ , _cabalPackageInfo_packageName :: T.Text
57+ , _cabalPackageInfo_packageRoot :: FilePath
58+ , _cabalPackageInfo_buildable :: Bool
4759 , _cabalPackageInfo_sourceDirs :: NE.NonEmpty FilePath
4860 -- ^ List of hs src dirs of the library component
4961 , _cabalPackageInfo_defaultExtensions :: [Extension]
@@ -54,20 +66,19 @@ data CabalPackageInfo = CabalPackageInfo
5466 -- ^ List of compiler-specific options (e.g., the "ghc-options" field of the cabal file)
5567 }
5668
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
69+-- | Used to signal to obelisk that it's being invoked as a preprocessor
70+preprocessorIdentifier :: String
71+preprocessorIdentifier = "__preprocessor-apply-packages"
72+
73+run :: MonadObelisk m => m ()
74+run = withProjectRoot "." $ \root -> do
75+ pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
76+ withGhciScript pkgs root $ \dotGhciPath -> do
6477 freePort <- getFreePort
65− assets <- withProjectRoot "." $ \root -> do
66− let importableRoot = if "/" `isInfixOf` root
67− then root
68− else "./" <> root
78+ assets <- do
79+ let importableRoot = toNixPath root
6980 isDerivation <- readProcessAndLogStderr Debug $
70− proc nixPath
81+ proc nixExePath
7182 [ "eval"
7283 , "-f"
7384 , root
@@ -79,83 +90,138 @@ run = do
7990 -- Check whether the impure static files are a derivation (and so must be built)
8091 if isDerivation == "1"
8192 then fmap T.strip $ readProcessAndLogStderr Debug $ -- Strip whitespace here because nix-build has no --raw option
82− proc nixBuildPath
93+ proc nixBuildExePath
8394 [ "--no-out-link"
8495 , "-E", "(import " <> importableRoot <> "{}).passthru.staticFilesImpure"
8596 ]
8697 else readProcessAndLogStderr Debug $
87− proc nixPath ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
98+ proc nixExePath ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
8899 putLog Debug $ "Assets impurely loaded from: " <> assets
89− runGhcid dotGhciPath $ Just $ unwords
100+ runGhcid root True dotGhciPath pkgs $ Just $ unwords
90101 [ "Obelisk.Run.run"
91102 , show freePort
92− , "(runServeAsset " ++ show assets ++ ")"
103+ , "(Obelisk.Run.runServeAsset " ++ show assets ++ ")"
93104 , "Backend.backend"
94105 , "Frontend.frontend"
95106 ]
96107
97108runRepl :: MonadObelisk m => m ()
98−runRepl = do
99− pkgs <- getLocalPkgs
100− withGhciScript pkgs $ \dotGhciPath -> do
101− runGhciRepl dotGhciPath
109+runRepl = withProjectRoot "." $ \root -> do
110+ pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
111+ withGhciScript pkgs "." $ \dotGhciPath ->
112+ runGhciRepl root pkgs dotGhciPath
102113
103114runWatch :: MonadObelisk m => m ()
104−runWatch = do
105− pkgs <- getLocalPkgs
106− withGhciScript pkgs $ \dotGhciPath -> runGhcid dotGhciPath Nothing
115+runWatch = withProjectRoot "." $ \root -> do
116+ pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
117+ withGhciScript pkgs root $ \dotGhciPath ->
118+ runGhcid root True dotGhciPath pkgs Nothing
119+
120+-- | Relative paths to local packages of an obelisk project.
121+--
122+-- These are a combination of the obelisk predefined local packages,
123+-- and any packages that the user has set with the @packages@ argument
124+-- to the Nix @project@ function.
125+getLocalPkgs :: MonadObelisk m => FilePath -> m [FilePath]
126+getLocalPkgs root = do
127+ (_exitCode, out, err) <- readCreateProcessWithExitCode $
128+ proc findExePath ["-L", root, "(", "-name", "*.cabal", "-o", "-name", Hpack.packageConfig, ")", "-a", "-type", "f"]
129+ putLog Debug $ T.strip $ T.pack err
130+
131+ let
132+ -- We ignore any path that has ".obelisk" in it, but keep the root ".obelisk" paths
133+ packagePaths = filter (not . isIgnored) $ map T.unpack $ T.lines $ T.strip $ T.pack out
134+ isIgnored path = obeliskDirName `elem` dropPrefix (splitPath $ toObeliskDir root) (splitPath path)
135+ pure packagePaths
107136
108137data GuessPackageFileError = GuessPackageFileError_Ambiguous [FilePath] | GuessPackageFileError_NotFound
109138 deriving (Eq, Ord, Show)
110139instance Exception GuessPackageFileError
111140
141+newtype HPackFilePath = HPackFilePath { unHPackFilePath :: FilePath } deriving (Eq, Ord, Show)
142+newtype CabalFilePath = CabalFilePath { unCabalFilePath :: FilePath } deriving (Eq, Ord, Show)
143+
112144-- | Given a directory, try to guess what the appropriate @.cabal@ or @package.yaml@ file is for the package.
113145guessCabalPackageFile
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
146+ :: (MonadIO m)
147+ => FilePath -- ^ Directory or path to search for cabal package
148+ -> m (Either GuessPackageFileError (Either CabalFilePath HPackFilePath))
149+guessCabalPackageFile pkg = do
150+ liftIO (doesDirectoryExist pkg) >>= \case
151+ False -> case cabalOrHpackFile pkg of
152+ (Just hpack@(Right _)) -> pure $ Right hpack
153+ (Just cabal@(Left (CabalFilePath cabalFilePath))) -> do
154+ -- If the cabal file has a sibling hpack file, we use that instead
155+ -- since running hpack often generates a sibling cabal file
156+ let possibleHpackSibling = takeDirectory cabalFilePath </> Hpack.packageConfig
157+ hasHpackSibling <- liftIO $ doesFileExist possibleHpackSibling
158+ pure $ Right $ if hasHpackSibling then Right (HPackFilePath possibleHpackSibling) else cabal
159+ Nothing -> pure $ Left GuessPackageFileError_NotFound
160+ True -> do
161+ candidates <- liftIO $
162+ filterM (doesFileExist . either unCabalFilePath unHPackFilePath)
163+ =<< mapMaybe (cabalOrHpackFile . (pkg </>)) <$> listDirectory pkg
164+ pure $ case partitionEithers candidates of
165+ ([hpack], _) -> Right $ Left hpack
166+ ([], [cabal]) -> Right $ Right cabal
167+ ([], []) -> Left GuessPackageFileError_NotFound
168+ (hpacks, cabals) -> Left $ GuessPackageFileError_Ambiguous $ coerce hpacks <> coerce cabals
128169
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"]
170+cabalOrHpackFile :: FilePath -> Maybe (Either CabalFilePath HPackFilePath)
171+cabalOrHpackFile = \case
172+ x | takeExtension x == ".cabal" -> Just (Left $ CabalFilePath x)
173+ | takeFileName x == Hpack.packageConfig -> Just (Right $ HPackFilePath x)
174+ | otherwise -> Nothing
133175
176+-- | Parses the cabal package in a given directory.
177+-- This automatically figures out which .cabal file or package.yaml (hpack) file to use in the given directory.
134178parseCabalPackage
135− :: (MonadObelisk m)
136− => FilePath -- ^ package directory
179+ :: MonadObelisk m
180+ => FilePath -- ^ Package directory
137181 -> 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
182+parseCabalPackage dir = parseCabalPackage' dir >>= \case
183+ Left err -> Nothing <$ putLog Error err
184+ Right (warnings, pkgInfo) -> do
185+ for_ warnings $ putLog Warning . T.pack . show
186+ pure $ Just pkgInfo
187+
188+-- | Like 'parseCabalPackage' but returns errors and warnings directly so as to avoid 'MonadObelisk'.
189+parseCabalPackage'
190+ :: (MonadIO m)
191+ => FilePath -- ^ Package directory
192+ -> m (Either T.Text ([Dist.PWarning], CabalPackageInfo))
193+parseCabalPackage' pkg = runExceptT $ do
194+ (cabalContents, packageFile, packageName) <- guessCabalPackageFile pkg >>= \case
195+ Left GuessPackageFileError_NotFound -> throwError $ "No .cabal or package.yaml file found in " <> T.pack pkg
196+ Left (GuessPackageFileError_Ambiguous _) -> throwError $ "Unable to determine which .cabal file to use in " <> T.pack pkg
197+ Right (Left (CabalFilePath file)) -> (, file, takeBaseName file) <$> liftIO (readUTF8File file)
198+ Right (Right (HPackFilePath file)) -> do
144199 let
145− file = dir </> hpackFileName
146200 decodeOptions = Hpack.DecodeOptions (Hpack.ProgramName "ob") file Nothing Hpack.decodeYaml
147201 liftIO (Hpack.readPackageConfig decodeOptions) >>= \case
148202 Left err -> throwError $ T.pack $ "Failed to parse " <> file <> ": " <> err
149− Right (Hpack.DecodeResult hpackPackage _ _ _) -> pure (Hpack.renderPackage [] hpackPackage, file)
203+ Right (Hpack.DecodeResult hpackPackage _ _ _) -> pure (Hpack.renderPackage [] hpackPackage, file, Hpack.packageName hpackPackage)
150204
151− let (warnings, result) = runParseResult $ parseGenericPackageDescription $
152− toUTF8BS cabalContents
153− for_ warnings $ putLog Warning . T.pack . show
205+ let
206+ (warnings, result) = runParseResult $ parseGenericPackageDescription $ toUTF8BS cabalContents
207+ osConfVar = case System.Info.os of
208+ "linux" -> Just Dist.Linux
209+ "darwin" -> Just Dist.OSX
210+ _ -> trace "Unrecgonized System.Info.os" Nothing
211+ archConfVar = Just Dist.X86_64 -- TODO: Actually infer this
212+ evalConfVar v = Right $ case v of
213+ OS osVar -> Just osVar == osConfVar
214+ Arch archVar -> Just archVar == archConfVar
215+ Impl GHC _ -> True -- TODO: Actually check version range
216+ _ -> False
154217 case condLibrary <$> result of
155218 Right (Just condLib) -> do
156− let (_, lib) = simplifyCondTree (const $ pure True) condLib
157− pure $ CabalPackageInfo
158− { _cabalPackageInfo_packageRoot = dir
219+ let (_, lib) = simplifyCondTree evalConfVar condLib
220+ pure $ (warnings,) $ CabalPackageInfo
221+ { _cabalPackageInfo_packageName = T.pack packageName
222+ , _cabalPackageInfo_packageFile = packageFile
223+ , _cabalPackageInfo_packageRoot = takeDirectory packageFile
224+ , _cabalPackageInfo_buildable = buildable $ libBuildInfo lib
159225 , _cabalPackageInfo_sourceDirs =
160226 fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
161227 , _cabalPackageInfo_defaultExtensions =
@@ -168,45 +234,51 @@ parseCabalPackage dir = either ((Nothing <$) . putLog Error) (pure . Just) <=< r
168234 Left (_, errors) ->
169235 throwError $ T.pack $ "Failed to parse " <> packageFile <> ":\n" <> unlines (map show errors)
170236
237+parsePackagesOrFail :: MonadObelisk m => [FilePath] -> m (NE.NonEmpty CabalPackageInfo)
238+parsePackagesOrFail dirs = do
239+ (pkgDirErrs, packageInfos') <- fmap partitionEithers $ for dirs $ \dir -> do
240+ flip fmap (parseCabalPackage dir) $ \case
241+ Just packageInfo
242+ | _cabalPackageInfo_buildable packageInfo -> Right packageInfo
243+ _ -> Left dir
244+
245+ packageInfos <- case NE.nonEmpty packageInfos' of
246+ Nothing -> failWith $ T.pack $ "No valid, buildable packages found in " <> intercalate ", " dirs
247+ Just xs -> pure xs
248+
249+ unless (null pkgDirErrs) $
250+ putLog Warning $ T.pack $ "Failed to find buildable packages in " <> intercalate ", " pkgDirErrs
251+
252+ pure packageInfos
253+
254+packageInfoToNamePathMap :: [CabalPackageInfo] -> Map Text FilePath
255+packageInfoToNamePathMap = Map.fromList . map (_cabalPackageInfo_packageName &&& _cabalPackageInfo_packageRoot)
256+
171257-- | Create ghci configuration to load the given packages
172258withGhciScript
173259 :: MonadObelisk m
174− => [FilePath] -- ^ List of packages to load into ghci
260+ => [CabalPackageInfo] -- ^ List of packages to load into ghci
261+ -> FilePath -- ^ All paths written to the .ghci file will be relative to this path
175262 -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporary .ghci
176263 -> 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
264+withGhciScript packageInfos pathBase f = do
265+ selfExe <- liftIO getExecutablePath
266+ let
267+ packageNames = Set.fromList $ map _cabalPackageInfo_packageName packageInfos
268+ modulesToLoad = mconcat
269+ [ [ "Obelisk.Run" | "obelisk-run" `Set.member` packageNames ]
270+ , [ "Backend" | "backend" `Set.member` packageNames ]
271+ , [ "Frontend" | "frontend" `Set.member` packageNames ]
272+ ]
273+ dotGhci = unlines
274+ -- TODO: Shell escape
275+ [ ":set -F -pgmF " <> selfExe <> " -optF " <> preprocessorIdentifier <> " " <> unwords (map (("-optF " <>) . makeRelative pathBase . _cabalPackageInfo_packageFile) packageInfos)
276+ , ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
277+ , if null modulesToLoad then "" else ":load " <> unwords modulesToLoad
278+ , "import qualified Obelisk.Run"
279+ , "import qualified Frontend"
280+ , "import qualified Backend"
281+ ]
210282 withSystemTempDirectory "ob-ghci" $ \fp -> do
211283 let dotGhciPath = fp </> ".ghci"
212284 liftIO $ writeFile dotGhciPath dotGhci
@@ -214,35 +286,44 @@ withGhciScript dirs f = do
214286
215287 where
216288 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 ()
289+ makeRelative pathBase . (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
222290
223291-- | Run ghci repl
224292runGhciRepl
225293 :: MonadObelisk m
226− => FilePath -- ^ Path to .ghci
294+ => FilePath -- ^ Path to project root
295+ -> [CabalPackageInfo]
296+ -> FilePath -- ^ Path to .ghci
227297 -> m ()
228−runGhciRepl dotGhci = inProjectShell "ghc" $ "ghci " <> makeBaseGhciOptions dotGhci
298+runGhciRepl root packages dotGhci =
299+ -- NOTE: We do *not* want to use $(staticWhich "ghci") here because we need the
300+ -- ghc that is provided by the shell in the user's project.
301+ nixShellWithPkgs root True False (packageInfoToNamePathMap packages) $ Just $ "ghci " <> makeBaseGhciOptions dotGhci -- TODO: Shell escape
229302
230303-- | Run ghcid
231304runGhcid
232305 :: MonadObelisk m
233− => FilePath -- ^ Path to .ghci
306+ => FilePath -- ^ Path to project root
307+ -> Bool -- ^ Should we chdir to root when running this process?
308+ -> FilePath -- ^ Path to .ghci
309+ -> [CabalPackageInfo]
234310 -> Maybe String -- ^ Optional command to run at every reload
235311 -> m ()
236−runGhcid dotGhci mcmd = callCommand $ unwords $ $(staticWhich "ghcid") : opts
312+runGhcid root chdirToRoot dotGhci packages mcmd =
313+ nixShellWithPkgs root True chdirToRoot (packageInfoToNamePathMap packages) (Just $ unwords $ ghcidExePath : opts) -- TODO: Shell escape
237314 where
238315 opts =
239316 [ "-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"
317+ , "--command='ghci -ignore-dot-ghci " <> makeBaseGhciOptions dotGhci <> "' "
243318 , "--outputfile=ghcid-output.txt"
244− ] <> testCmd
245− testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'")
319+ ] <> map (\x -> "--reload='" <> x <> "'") reloadFiles
320+ <> map (\x -> "--restart='" <> x <> "'") restartFiles
321+ <> testCmd
322+ testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'") -- TODO: Shell escape
323+
324+ adjustRoot x = if chdirToRoot then makeRelative root x else x
325+ reloadFiles = map adjustRoot [root </> "config"]
326+ restartFiles = map (adjustRoot . _cabalPackageInfo_packageFile) packages
246327
247328makeBaseGhciOptions :: FilePath -> String
248329makeBaseGhciOptions dotGhci =
lib/command/src/Obelisk/Command/Utils.hs+12−5
@@ -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
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/route/src/Obelisk/Route.hs+11−4
@@ -20,10 +20,11 @@
2020{-# LANGUAGE ViewPatterns #-}
2121module Obelisk.Route
2222 ( R
23− , pattern (:/)
2423 , (:.)
25− , pattern (:.)
24+ , (?/)
2625 , hoistR
26+ , pattern (:.)
27+ , pattern (:/)
2728 , PageName
2829 , PathQuery
2930 , Encoder
@@ -194,10 +195,16 @@ import Data.Aeson (FromJSON, ToJSON)
194195
195196type R f = DSum f Identity --TODO: Better name
196197
197−{-# COMPLETE (:/) #-}
198−infixr 5 :/
198+-- | Convenience builder for an 'R' using 'Identity' for the functor.
199199pattern (:/) :: f a -> a -> R f
200200pattern a :/ b = a :=> Identity b
201+{-# COMPLETE (:/) #-}
202+infixr 5 :/
203+
204+-- | Like '(:/)' but adds a 'Just' wrapper around the right-hand side.
205+(?/) :: f (Maybe a) -> a -> R f
206+r ?/ a = r :/ Just a
207+infixr 5 ?/
201208
202209mapSome :: (forall a. f a -> g a) -> Some f -> Some g
203210mapSome f (Some a) = Some $ f a
lib/selftest/src/Obelisk/SelfTest.hs+153−80
@@ -1,3 +1,4 @@
1+{-# LANGUAGE DoAndIfThenElse #-}
12{-# LANGUAGE EmptyCase #-}
23{-# LANGUAGE LambdaCase #-}
34{-# LANGUAGE OverloadedStrings #-}
@@ -12,7 +13,7 @@ import Control.Monad
1213import Control.Monad.IO.Class
1314import Data.Bool (bool)
1415import Data.Function (fix)
15−import Data.Semigroup (Semigroup, (<>))
16+import Data.Semigroup ((<>))
1617import qualified Data.Set as Set
1718import Data.String
1819import Data.Text (Text)
@@ -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
@@ -49,13 +50,35 @@ data ObRunState
4950cp :: FilePath
5051cp = $(staticWhich "cp")
5152
53+cabalPath :: FilePath
54+cabalPath = $(staticWhich "cabal")
55+
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+
77+gitUserConfig :: [Text]
78+gitUserConfig = ["-c", "user.name=Obelisk Selftest", "-c", "user.email=noreply@example.com"]
79+
5280commit :: Text -> Sh ()
53−commit msg = void $ run "git"
54− [ "-c"
55− , "user.name=Obelisk Selftest"
56− , "-c"
57− , "user.email=noreply@example.com"
58− , "commit"
81+commit msg = void $ run gitPath $ gitUserConfig <> [ "commit"
5982 , "--no-gpg-sign"
6083 , "--allow-empty"
6184 , "-m"
@@ -75,6 +98,14 @@ tshow = T.pack . show
7598shellyOb :: MonadIO m => (Sh a -> Sh a) -> Sh a -> m a
7699shellyOb f obTest = shelly $ f obTest
77100
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
78109
79110main :: IO ()
80111main = do
@@ -83,82 +114,97 @@ main = do
83114 unless isVerbose $
84115 putStrLn "Tests may take longer to run if there are unbuilt derivations: use -v for verbose output"
85116 let verbosity = bool silently verbosely isVerbose
86− nixBuild args = run "nix-build" ("--no-out-link" : args)
87− 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 []
88123 let
89− 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 =
90131 withSystemTempDirectory "obelisk-impl-copy" $ \(fromString -> obeliskImpl) -> do
91132 void . shellyOb verbosity $ chdir obeliskImpl $ do
92− user <- T.strip <$> run "whoami" []
93− run_ cp ["-rT", toTextIgnore obeliskImplRaw, toTextIgnore obeliskImpl]
94− run_ "chown" ["-R", user, toTextIgnore obeliskImpl]
95− 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]
96137 f obeliskImpl
97138
98139 withInitCache f obeliskImpl =
99140 withSystemTempDirectory "init Cache λ" $ \initCache -> do
100141 -- Setup the ob init cache
101142 void . shellyOb verbosity $ chdir initCache $ do
102− run_ "ob" ["init", "--symlink", toTextIgnore obeliskImpl]
103− run_ "git" ["init"]
143+ runOb_ ["init", "--symlink", toTextIgnore obeliskImpl]
144+ run_ gitPath ["init"]
104145
105146 f initCache
106147
107− withObeliskImplAndInitCache f =
108− withObeliskImpl $ \impl -> withInitCache (f impl) impl
148+ withObeliskImplDirtyAndInitCache f =
149+ withObeliskImplDirty $ \impl -> withInitCache (f impl) impl
109150
110151 httpManager <- HTTP.newManager HTTP.defaultManagerSettings
111152
112− withObeliskImplAndInitCache $ \obeliskImpl initCache ->
153+ withObeliskImplDirtyAndInitCache $ \obeliskImplDirty initCache ->
113154 hspec $ parallel $ do
114155 let shelly_ = void . shellyOb verbosity
115156
116− inTmp :: (FilePath -> Sh a) -> IO ()
117− inTmp f = withTmp (chdir <*> f)
157+ defaultTmpDirName = "test λ"
158+
159+ inTmp' :: FilePath -> (FilePath -> Sh a) -> IO ()
160+ inTmp' dirname f = withTmp' dirname (chdir <*> f)
161+ inTmp = inTmp' defaultTmpDirName
118162
119− withTmp f = shelly_ . withSystemTempDirectory "test λ" $ f . fromString
163+ withTmp' dirname f = shelly_ . withSystemTempDirectory dirname $ f . fromString
164+ withTmp = withTmp' defaultTmpDirName
120165
121− inTmpObInit f = inTmp $ \dir -> do
166+ inTmpObInit' dirname f = inTmp' dirname $ \dir -> do
122167 run_ cp ["-rT", fromString initCache, toTextIgnore dir]
123168 f dir
169+ inTmpObInit = inTmpObInit' defaultTmpDirName
124170
125171 -- To be used in tests that change the obelisk impl directory
126172 inTmpObInitWithImplCopy f = inTmpObInit $ \dir ->
127− withObeliskImpl $ \(fromString -> implCopy) -> do
128− run_ "rm" [thunk]
129− run_ "ln" ["-s", implCopy, thunk]
173+ withObeliskImplClean $ \(fromString -> implClean) -> do
174+ run_ rmPath [thunk]
175+ run_ lnPath ["-s", implClean, thunk]
130176 f dir
131177
132178 assertRevEQ a b = liftIO . assertEqual "" "" =<< diff a b
133179 assertRevNE a b = liftIO . assertBool "" . (/= "") =<< diff a b
134180
135− revParseHead = T.strip <$> run "git" ["rev-parse", "HEAD"]
181+ revParseHead = T.strip <$> run gitPath ["rev-parse", "HEAD"]
136182
137183 commitAll = do
138− run_ "git" ["add", "."]
184+ run_ gitPath ["add", "."]
139185 commit "checkpoint"
140186 revParseHead
141187
142188 thunk = ".obelisk/impl"
143− update = run "ob" ["thunk", "update", thunk] >> commitAll
144− pack = run "ob" ["thunk", "pack", thunk] >> commitAll
145− 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
146192
147− diff a b = run "git" ["diff", a, b]
193+ diff a b = run gitPath ["diff", a, b]
148194
149195 describe "ob init" $ parallel $ do
150− it "works with default impl" $ inTmp $ \_ -> run "ob" ["init"]
151− it "works with master branch impl" $ inTmp $ \_ -> run "ob" ["init", "--branch", "master"]
152− 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]
153199 it "doesn't silently overwrite existing files" $ withSystemTempDirectory "ob-init λ" $ \dir -> do
154− 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 }
155201 (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p False) ""
156202 (ExitFailure _, _, _) <- readCreateProcessWithExitCode (p False) ""
157203 (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p True) ""
158204 pure ()
159205
160206 it "doesn't create anything when given an invalid impl" $ inTmp $ \tmp -> do
161− void $ errExit False $ run "ob" ["init", "--symlink", "/dev/null"]
207+ void $ errExit False $ runOb ["init", "--symlink", "/dev/null"]
162208 ls tmp >>= liftIO . assertEqual "" []
163209
164210 it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $
@@ -169,17 +215,17 @@ main = do
169215 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
170216 -- when not run by 'nix-build --attr selftest'
171217 describe "ob run" $ {- NOT parallel $ -} do
172− it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir Nothing httpManager
173− 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
174220 it "works with differently named cabal files" $ inTmpObInit $ \_ -> do
175221 changeCabalPackageName "backend/backend.cabal" "new-backend"
176− testObRunInDir Nothing httpManager
222+ testObRunInDir' Nothing httpManager
177223
178224 describe "obelisk project" $ parallel $ do
179− it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskImpl]
180− it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", toTextIgnore obeliskImpl]
181− it "can build obelisk shell" $ inTmpObInit $ \_ -> nixBuild ["-A", "shell", toTextIgnore obeliskImpl]
182− 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]
183229
184230 describe "blank initialized project" $ parallel $ do
185231
@@ -200,9 +246,12 @@ main = do
200246 forM_ ["ghc", "ghcjs"] $ \compiler -> do
201247 let
202248 shellName = "shells." <> compiler
203− inShell cmd' = run "nix-shell" ["default.nix", "-A", fromString shellName, "--run", cmd']
249+ inShell cmd' = run_ "nix-shell" ["default.nix", "-A", fromString shellName, "--run", cmd']
204250 it ("can enter " <> shellName) $ inTmpObInit $ \_ -> inShell "exit"
205− it ("can build in " <> shellName) $ inTmpObInit $ \_ -> inShell $ "cabal new-build --" <> fromString compiler <> " all"
251+ -- NOTE: We override the temporary directory name because cabal has a bug preventing new-build from working
252+ -- in a path that has unicode characters.
253+ it ("can build in " <> shellName) $ inTmpObInit' "test" $ \_ -> inShell $
254+ T.pack cabalPath <> " --version; " <> T.pack cabalPath <> " new-build --" <> T.pack compiler <> " all"
206255
207256 it "has idempotent thunk update" $ inTmpObInitWithImplCopy $ \_ -> do
208257 _ <- pack
@@ -226,76 +275,93 @@ main = do
226275
227276 it "unpacks the correct branch" $ withTmp $ \dir -> do
228277 let branch = "master"
229− run_ "git" ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch]
230− run_ "ob" ["thunk", "pack", toTextIgnore dir]
231− run_ "ob" ["thunk", "unpack", toTextIgnore dir]
232− 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"]
233282 liftIO $ assertEqual "" branch (T.strip branch')
234283
235284 it "can pack and unpack plain git repos" $
236285 shelly_ $ withSystemTempDirectory "git-repo" $ \dir -> do
237286 let repo = toTextIgnore $ dir </> ("repo" :: String)
238− run_ "git" ["clone", "https://github.com/haskell/process.git", repo]
287+ run_ gitPath ["clone", "https://github.com/haskell/process.git", repo]
239288 origHash <- chdir (fromText repo) revParseHead
240289
241− run_ "ob" ["thunk", "pack", repo]
290+ runOb_ ["thunk", "pack", repo]
242291 packedFiles <- Set.fromList <$> ls (fromText repo)
243292 liftIO $ assertEqual "" packedFiles $ Set.fromList $ (repo </>) <$>
244293 ["default.nix", "github.json", ".attr-cache" :: String]
245294
246− run_ "ob" ["thunk", "unpack", repo]
295+ runOb_ ["thunk", "unpack", repo]
247296 chdir (fromText repo) $ do
248297 unpackHash <- revParseHead
249298 assertRevEQ origHash unpackHash
250299
251− testThunkPack $ fromText repo
300+ testThunkPack' $ fromText repo
252301
253− it "aborts thunk pack when there are uncommitted files" $ inTmpObInit $ \dir -> do
254− testThunkPack (dir </> thunk)
302+ it "aborts thunk pack when there are uncommitted files" $ inTmpObInitWithImplCopy $ \dir -> do
303+ testThunkPack' (dir </> thunk)
255304
256305 describe "ob thunk update --branch" $ parallel $ do
257306 it "can change a thunk to the latest version of a desired branch" $ withTmp $ \dir -> do
258307 let branch1 = "master"
259308 branch2 = "develop"
260− run_ "git" ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch1]
261− run_ "ob" ["thunk" , "pack", toTextIgnore dir]
262− 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]
263312
264313 it "doesn't create anything when given an invalid branch" $ withTmp $ \dir -> do
265314 let checkDir dir' = liftIO $ getDirectoryContents $ T.unpack $ toTextIgnore dir'
266− run_ "git" ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", "master"]
267− 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]
268317 startingContents <- checkDir dir
269− void $ errExit False $ run "ob" ["thunk", "update", toTextIgnore dir, "--branch", "dumble-palooza"]
318+ void $ errExit False $ runOb ["thunk", "update", toTextIgnore dir, "--branch", "dumble-palooza"]
270319 checkDir dir >>= liftIO . assertEqual "" startingContents
271320
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)
272338
273339-- | Run `ob run` in the given directory (maximum of one level deep)
274−testObRunInDir :: Maybe FilePath -> HTTP.Manager -> Sh ()
275−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
276342 [p0, p1] <- liftIO $ getFreePorts 2
277343 let uri p = "http://localhost:" <> T.pack (show p) <> "/" -- trailing slash required for comparison
278344 writefile "config/common/route" $ uri p0
279− maybe id chdir mdir $ runHandle "ob" ["run", "-v"] $ \stdout -> do
280− firstUri <- handleObRunStdout httpManager stdout
345+ maybe id chdir mdir $ runHandles executable extraArgs [] $ \_stdin stdout stderr -> do
346+ firstUri <- handleObRunStdout httpManager stdout stderr
281347 let newUri = uri p1
282348 when (firstUri == newUri) $ errorExit $
283349 "Startup URI (" <> firstUri <> ") is the same as test URI (" <> newUri <> ")"
284350 maybe id (\_ -> chdir "..") mdir $ alterRouteTo newUri stdout
285− runningUri <- handleObRunStdout httpManager stdout
351+ runningUri <- handleObRunStdout httpManager stdout stderr
286352 if runningUri /= newUri
287353 then errorExit $ "Reloading failed: expected " <> newUri <> " but got " <> runningUri
288354 else exit 0
289355
290−testThunkPack :: FilePath -> Sh ()
291−testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $ \file handle -> do
292− 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) ""
293359 ensureThunkPackFails q = liftIO $ pack' >>= \case
294360 (code, out, err)
295− | 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 <> "'"
296362 | q `T.isInfixOf` T.pack (out <> err) -> pure ()
297− | otherwise -> fail $ "ob thunk pack failed for an unexpected reason: " <> show out <> "\nstderr: " <> err
298− 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
299365 -- Untracked files
300366 ensureThunkPackFails "Untracked files"
301367 void $ git ["add", T.pack file]
@@ -308,7 +374,7 @@ testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $
308374 liftIO $ T.hPutStrLn handle "test file" >> hClose handle
309375 ensureThunkPackFails "modified"
310376 -- Existing stashes
311− void $ git ["stash"]
377+ void $ git $ gitUserConfig <> [ "stash" ]
312378 ensureThunkPackFails "has stashes"
313379
314380-- | Blocks until a non-empty line is available
@@ -329,9 +395,12 @@ alterRouteTo uri stdout = do
329395 "Reloading failed: " <> T.pack (show t)
330396
331397-- | Handle stdout of `ob run`: check that the frontend and backend servers are started correctly
332−handleObRunStdout :: HTTP.Manager -> Handle -> Sh Text
333−handleObRunStdout httpManager stdout = flip fix (ObRunState_Init, []) $ \loop (state, msgs) ->
334− 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
335404 ObRunState_Init
336405 | "Running test..." `T.isPrefixOf` t -> loop (ObRunState_BackendStarted, msgs)
337406 ObRunState_Startup
@@ -343,8 +412,12 @@ handleObRunStdout httpManager stdout = flip fix (ObRunState_Init, []) $ \loop (s
343412 obRunCheck httpManager stdout uri
344413 pure uri
345414 | not (T.null t) -> errorExit $ "Started: " <> t -- If theres any other output here, startup failed
346− _ | "Failed" `T.isPrefixOf` t -> errorExit $ "ob run failed: " <> T.unlines (reverse $ t : msgs)
415+ _ | "Failed" `T.isPrefixOf` t -> handleObRunError (t : msgs)
347416 | 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
348421
349422-- | Make requests to frontend/backend servers to check they are working properly
350423obRunCheck :: 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
release.nix+3−1
@@ -1,8 +1,10 @@
11{
22 supportedSystems ? [ "x86_64-linux" "x86_64-darwin" ]
33}:
4−{
4+rec {
55 recurseForDerivations = true;
66 build = import ./all-builds.nix { inherit supportedSystems; };
77 test = import ./all-tests.nix { inherit supportedSystems; };
8+
9+ inherit (build) metaCache;
810}
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
skeleton/frontend/frontend.cabal+1
@@ -9,6 +9,7 @@ library
99 , common
1010 , obelisk-frontend
1111 , obelisk-route
12+ , jsaddle
1213 , reflex-dom
1314 , obelisk-executable-config-lookup
1415 , obelisk-generated-static
skeleton/frontend/src/Frontend.hs+12
@@ -4,8 +4,10 @@
44
55module Frontend where
66
7+import Control.Monad
78import qualified Data.Text as T
89import qualified Data.Text.Encoding as T
10+import Language.Javascript.JSaddle (eval, liftJSM)
911
1012import Obelisk.Frontend
1113import Obelisk.Configs
@@ -18,6 +20,9 @@ import Common.Api
1820import Common.Route
1921
2022
23+-- This runs in a monad that can be run on the client or the server.
24+-- To run code in a pure client or pure server context, use one of the
25+-- `prerender` functions.
2126frontend :: Frontend (R FrontendRoute)
2227frontend = Frontend
2328 { _frontend_head = do
@@ -26,6 +31,13 @@ frontend = Frontend
2631 , _frontend_body = do
2732 el "h1" $ text "Welcome to Obelisk!"
2833 el "p" $ text $ T.pack commonStuff
34+
35+ -- `prerender` and `prerender_` let you choose a widget to run on the server
36+ -- during prerendering and a different widget to run on the client with
37+ -- JavaScript. The following will generate a `blank` widget on the server and
38+ -- print "Hello, World!" on the client.
39+ prerender_ blank $ liftJSM $ void $ eval ("console.log('Hello, World!')" :: T.Text)
40+
2941 elAttr "img" ("src" =: static @"obelisk.jpg") blank
3042 el "div" $ do
3143 exampleConfig <- getConfig "common/example"