on master help sign in
Merge remote-tracking branch 'origin/develop' into eac@route-to-all.js
commit2fadf7c14f293fa89a534b02cb7330e72e33a83d authorElliot Cameron <eacameron@gmail.com> dateWed 12 Feb 2020, 15:01 (7 years ago) parentb555c55ea1957f
57 files changed+2114−1026
.github/pull_request_template.md+8 .gitignore+1 .hlint.yaml+65 ChangeLog.md+42−5 FAQ.md+2−2 LICENSE+30 README.md+37−14 all-builds.nix+106 all-tests.nix+145 default.nix+145−98 dep/.gitignore+1 dep/ghcid/default.nix+7 dep/ghcid/github.json+7 dep/hnix/default.nix+7 dep/hnix/github.json+7 dep/reflex-platform/default.nix+7−6 dep/reflex-platform/github.json+4−3 jobsets.nix−71 lib/asset/README.md+10−10 lib/asset/serve-snap/src/Obelisk/Asset/Accept.hs+6−6 lib/cliapp/obelisk-cliapp.cabal+3−2 lib/cliapp/src/Obelisk/CliApp.hs+14−6 lib/cliapp/src/Obelisk/CliApp/Demo.hs−53 lib/cliapp/src/Obelisk/CliApp/Logging.hs+11−10 lib/cliapp/src/Obelisk/CliApp/Process.hs+96−34 lib/command/obelisk-command.cabal+11−10 lib/command/src/Obelisk/Command.hs+64−74 lib/command/src/Obelisk/Command/Deploy.hs+14−14 lib/command/src/Obelisk/Command/Nix.hs+45−15 lib/command/src/Obelisk/Command/Preprocessor.hs+96 lib/command/src/Obelisk/Command/Project.hs+142−54 lib/command/src/Obelisk/Command/Run.hs+256−146 lib/command/src/Obelisk/Command/Thunk.hs+258−121 lib/command/src/Obelisk/Command/Utils.hs+39−17 lib/command/src/Obelisk/Command/VmBuilder.hs−1 lib/executable-config/lookup/obelisk-executable-config-lookup.cabal+1−1 lib/executable-config/lookup/src-other/Obelisk/ExecutableConfig/Lookup.hs+1−8 lib/frontend/src/Obelisk/Frontend.hs+4−4 lib/frontend/src/Obelisk/Frontend/Cookie.hs+2−1 lib/route/src/Obelisk/Route.hs+12−5 lib/route/src/Obelisk/Route/Frontend.hs+96−16 lib/run/obelisk-run.cabal+7 lib/run/src/Obelisk/Run.hs+56−13 lib/selftest/obelisk-selftest.cabal+4−2 lib/selftest/src/Obelisk/SelfTest.hs+205−95 lib/snap-extras/obelisk-snap-extras.cabal+1−1 lib/snap-extras/src/Obelisk/Snap/Extras.hs+1−1 lib/tabulation/src/Data/Tabulation.hs+1−1 release.nix+8−95 scripts/ghcid-this-lib+1−1 skeleton/.gitignore+1 skeleton/backend/src/Backend.hs−3 skeleton/common/common.cabal−3 skeleton/common/src/Common/Route.hs+1 skeleton/frontend/frontend.cabal+1 skeleton/frontend/src/Frontend.hs+22−4 skeleton/static/main.css+3
.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)
.gitignore+1
@@ -2,6 +2,7 @@
22**/.cabal-sandbox
33**/cabal.sandbox.config
44result
5+result-*
56*.o
67*.hi
78TAGS
.hlint.yamlnew file+65
@@ -0,0 +1,65 @@
1+# HLint configuration file
2+# https://github.com/ndmitchell/hlint
3+##########################
4+
5+# This file contains a template configuration file, which is typically
6+# placed as .hlint.yaml in the root of your project
7+
8+
9+# Specify additional command line arguments
10+#
11+# - arguments: [--color, --cpp-simple, -XQuasiQuotes]
12+
13+
14+# Control which extensions/flags/modules/functions can be used
15+#
16+# - extensions:
17+# - default: false # all extension are banned by default
18+# - name: [PatternGuards, ViewPatterns] # only these listed extensions can be used
19+# - {name: CPP, within: CrossPlatform} # CPP can only be used in a given module
20+#
21+# - flags:
22+# - {name: -w, within: []} # -w is allowed nowhere
23+#
24+# - modules:
25+# - {name: [Data.Set, Data.HashSet], as: Set} # if you import Data.Set qualified, it must be as 'Set'
26+# - {name: Control.Arrow, within: []} # Certain modules are banned entirely
27+#
28+# - functions:
29+# - {name: unsafePerformIO, within: []} # unsafePerformIO can only appear in no modules
30+
31+
32+# Add custom hints for this project
33+#
34+# Will suggest replacing "wibbleMany [myvar]" with "wibbleOne myvar"
35+# - error: {lhs: "wibbleMany [x]", rhs: wibbleOne x}
36+
37+
38+# Turn on hints that are off by default
39+#
40+# Ban "module X(module X) where", to require a real export list
41+# - warn: {name: Use explicit module export list}
42+#
43+# Replace a $ b $ c with a . b $ c
44+# - group: {name: dollar, enabled: true}
45+#
46+# Generalise map to fmap, ++ to <>
47+# - group: {name: generalise, enabled: true}
48+
49+
50+# Ignore some builtin hints
51+- ignore: {name: Redundant do}
52+- ignore: {name: Redundant flip}
53+- ignore: {name: Use <$>}
54+- ignore: {name: Use camelCase}
55+- ignore: {name: Use if}
56+- ignore: {name: Use unless}
57+# - ignore: {name: Use const, within: SpecialModule} # Only within certain modules
58+
59+
60+# Define some custom infix operators
61+# - fixity: infixr 3 ~^#^~
62+
63+
64+# To generate a suitable file for HLint do:
65+# $ hlint --default > .hlint.yaml
ChangeLog.md+42−5
@@ -4,11 +4,9 @@ This project's release branch is `master`. This log is written from the perspect
44
55## Unreleased
66
7−* Generalised pathSegmentEncoder, added pathFieldEncoder.
8−* Added some Prisms to the encoder library for manipulating DSums (perhaps they should get moved to dependent-sum before release?)
9−* Add "ob doc" command, which lists paths to haddock documentation for specified packages
10−* Bump reflex-platform so that obelisk now uses GHC 8.6.5 and the nixos-19.03 nixpkgs set
11−* Add support in obelisk-route for single parameters in URL paths
7+* Fix a bug in `Obelisk.Route.Frontend` where `routeLink`, `routeLinkDynAttr`, and `dynRouteLink` would not behave exactly like `<a href="...">` when run by JavaScript. These functions now scroll to the top of the page when the link is clicked. ([#540](https://github.com/obsidiansystems/obelisk/pull/540))
8+* Fix a bug in `ob run`/`ob watch`/`ob repl` where nested Obelisk projects would also be loaded into the session. These are now ignored. ([#652](https://github.com/obsidiansystems/obelisk/pull/652))
9+* Improve behavior of `ob run`/`ob watch`/`ob repl` when multiple packages with the same name are encountered. Obelisk now issues a warning and tells you which one it will use. ([#653](https://github.com/obsidiansystems/obelisk/pull/653))
1210* Removed `Obelisk.Backend.mkRouteToUrl` since it is easily written in terms of `Obelisk.Route.renderObeliskRoute`:
1311
1412 mkRouteToUrl validFullEncoder (k :/ v) = renderObeliskRoute validFullEncoder (FullRoute_Frontend (ObeliskRoute_App k) :/ v)
@@ -16,6 +14,45 @@ This project's release branch is `master`. This log is written from the perspect
1614* Added `Obelisk.Backend.renderAllJsPath` to expose URL path to `ghcjs/all.js`.
1715* Added argument to `serveDefaultObeliskApp`, `serveObeliskApp`, and `serveGhcjsApp` to take the path to `all.js` instead of hard-coding it.
1816
17+## v0.5.0.0 - 2020-02-07
18+
19+* Add `Obelisk.Route.(?/)`, a convenience function for constructing routes nested in `Maybe`. ([#457](https://github.com/obsidiansystems/obelisk/pull/457))
20+* 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))
21+* 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))
22+* `ob thunk pack` will now attempt to automatically detect if the thunk is a private or public repo. To avoid this detection, specify `--private` or `--public` manually. ([#607](https://github.com/obsidiansystems/obelisk/pull/607))
23+* Fix a bug in the plain git thunk loader for thunks marked as 'private' when the revision is not in the default branch. ([#648](https://github.com/obsidiansystems/obelisk/pull/648))
24+* Improve handling of runtime nix dependencies. This may fix some issues encountered particularly by users on systems other than NixOS.
25+
26+## v0.4.0.0 - 2020-01-10
27+
28+* Bump reflex-platform which, notably, bumps nixpkgs to 19.09. ([#585](https://github.com/obsidiansystems/obelisk/pull/585))
29+* 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))
30+* 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))
31+* Add `--public`/`--private` options to `ob thunk pack` to specify if a repository should be treated as a public or private. ([#594](https://github.com/obsidiansystems/obelisk/pull/594))
32+* Improve error messaging when a dependency doesn't have the expected `.cabal` or `package.yaml` file. ([#597](https://github.com/obsidiansystems/obelisk/pull/597))
33+* Improve the skeleton in small ways. ([#593](https://github.com/obsidiansystems/obelisk/pull/593), [#589](https://github.com/obsidiansystems/obelisk/pull/589))
34+* Fix `ob` commands to again support running from any subdirectory of an obelisk project ([#591](https://github.com/obsidiansystems/obelisk/pull/591))
35+* 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))
36+
37+## v0.3.0.0 - 2019-12-20
38+
39+* Generalised `pathSegmentEncoder`, added `pathFieldEncoder`.
40+* Added some `Prism`s to the encoder library for manipulating `DSum`s.
41+* Add `ob doc` command, which lists paths to haddock documentation for specified packages.
42+* Bump reflex-platform so that obelisk now uses GHC 8.6.5 and the nixos-19.03 nixpkgs set.
43+* Add support in `obelisk-route` for single parameters in URL paths.
44+* Bump reflex-platform so that obelisk now uses reflex-dom 0.5.2.0.
45+* Use a `--pure` nix shell in `ob run` for parity with `ob repl` and more resilience against "works on my machine".
46+* Pin usages of `<nixpkgs>` in obelisk thunks, etc. to the nixpkgs used by the project's obelisk impl.
47+* Backport ACMEv2 support in obelisk server to regain LetsEncrypt account creation.
48+* Enable HTTPS in `ob run`.
49+* `ob run` now handles `ghci` errors better, and includes a custom `ghcid`
50+ version. As a result, you no longer need to have ghcid installed to
51+ use `ob run`, as we provide one for you.
52+* `ob` commands now complain less on systems with umasks other than `0022`.
53+* Ignore package environment files in `ob run` and `ob repl`.
54+* Add `Obelisk.Route.Frontend.routeLinkDynAttr`.
55+
1956## v0.2.0.0 - 2019-8-17
2057
2158* Configs become ByteStrings.
FAQ.md+2−2
@@ -81,7 +81,7 @@ project's dependencies and,
8181Assuming we are in a project created with `ob init`, `ob run` calls (see
8282`lib/command/src/Obelisk/Command.hs`):
8383
84− nix-shell -A shells.ghc --run 'ob --no-handoff internal run-static-io <real-run-function>'
84+ nix-shell --pure -A shells.ghc --run 'ob --no-handoff internal run-static-io <real-run-function>'
8585
8686where
8787
@@ -109,7 +109,7 @@ It is defined in `lib/run/src/Obelisk/Run.hs`:
109109 * or to `serveDefaultObeliskApp` (`lib/backend/src/Obelisk/Backend.hs`) to
110110 serve static assets.
111111* Starts `runWidget`
112− which itself runs
112+ which itself runs
113113 [runSettingsSocket](https://hackage.haskell.org/package/warp-3.2.26/docs/Network-Wai-Handler-Warp.html#v:runSettingsSocket)
114114 (the *“TCP listen loop”*):
115115 * it binds to TCP socket, and
LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2020 Obsidian Systems
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions are met:
7+
8+ * Redistributions of source code must retain the above copyright
9+ notice, this list of conditions and the following disclaimer.
10+
11+ * Redistributions in binary form must reproduce the above
12+ copyright notice, this list of conditions and the following
13+ disclaimer in the documentation and/or other materials provided
14+ with the distribution.
15+
16+ * Neither the name of Elliot Cameron nor the names of other
17+ contributors may be used to endorse or promote products derived
18+ from this software without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
README.md+37−14
@@ -1,12 +1,16 @@
11# Obelisk
22
3+[![Haskell Programming Language](https://img.shields.io/badge/language-Haskell-blue.svg)](http://www.haskell.org)
4+[![BSD3 License](http://img.shields.io/badge/license-BSD3-brightgreen.svg)](https://tldrlegal.com/license/bsd-3-clause-license-%28revised%29)
5+
6+
37Obelisk provides an easy way to develop and deploy your [Reflex](https://github.com/reflex-frp/reflex) project as web apps and as mobile apps.
48
59- [Installing Obelisk](#installing-obelisk)
610- [Developing an Obelisk project](#developing-an-obelisk-project)
7− - [Hoogle](#hoogle)
811 - [Adding Packages](#adding-packages)
912 - [Adding Package Overrides](#adding-package-overrides)
13+ - [Running over https](#running-over-https)
1014- [Deploying](#deploying)
1115 - [Locally](#locally)
1216 - [EC2](#ec2)
@@ -22,14 +26,15 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
22261. Set up nix caches
2327 1. If you are running NixOS, add this to `/etc/nixos/configuration.nix`:
2428 ```
25− nix.binaryCaches = [ "https://cache.nixos.org/" "https://nixcache.reflex-frp.org" ];
29+ nix.binaryCaches = [ "https://nixcache.reflex-frp.org" ];
2630 nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];
2731 ```
2832 and rebuild your NixOS configuration (e.g. `sudo nixos-rebuild switch`).
2933 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)):
3034 ```
31− substituters = https://cache.nixos.org https://nixcache.reflex-frp.org
32− trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
35+ binary-caches = https://cache.nixos.org https://nixcache.reflex-frp.org
36+ binary-cache-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
37+ binary-caches-parallel-connections = 40
3338 ```
3439 * 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)
3540 ```
@@ -68,8 +73,8 @@ nix-env -f /path/to/obelisk -iA command
6873Note that `ob` will defer to the version found in your project's `.obelisk/impl` directory. To update that version specifically:
6974
7075```shell
71−ob thunk unpack ./obelisk/impl
72−cd ./obelisk/impl
76+ob thunk unpack ./.obelisk/impl
77+cd ./.obelisk/impl
7378# apply your changes
7479```
7580
@@ -105,16 +110,11 @@ Obelisk leverages ghcid to provide a live-reloading server that handles both fro
105110ob run
106111```
107112
108−Now go to http://localhost:8000 (or the address/port specified in `config/common/route`) to access your app.
113+Now, with an appropriate browser, go to http://localhost:8000 (or the address/port specified in `config/common/route`) to access your app.
114+Firefox will not be able to properly run the development website due to [issue 460](https://github.com/obsidiansystems/obelisk/issues/460). Fortunately, this problem does not occur on a fully built website.
109115
110116Every 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.
111117
112−### Hoogle
113−
114−To enter a nix-shell from which you can run the hoogle command-line client or a hoogle server for your project:
115−
116−`nix-shell -A shells.ghc --arg withHoogle true`
117−
118118### Adding packages
119119
120120In 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.
@@ -148,6 +148,29 @@ project ./. ({ pkgs, ... }: {
148148
149149For further information see [the Haskell section](https://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure) of nixpkgs Contributors Guide.
150150
151+### Adding extra local packages
152+
153+If the standard packages (`frontend`, `backend`, and `common`) are not
154+enough, to add more local Haskell packages, define them with the
155+`packages` parameter. The sources of these packages will be
156+automatically reloaded by `ob run`.
157+
158+```nix
159+# ...
160+project ./. ({ pkgs, ... }: {
161+# ...
162+ packages = {
163+ another = ./another;
164+ };
165+# ...
166+```
167+
168+### Running over https
169+
170+To run your app locally over https, update the protocol in `config/common/route` to `https`, and then use `ob run` as normal.
171+
172+Since Obelisk generates a self-signed certificate for running https, the browser will issue a warning about using an invalid certificate. On Chrome, you can go to `chrome://flags/#allow-insecure-localhost` to enable invalid certificates for localhost.
173+
151174## Deploying
152175
153176### Locally
@@ -173,7 +196,7 @@ In this section we will demonstrate how to deploy your Obelisk app to an Amazon
173196
174197First create a new EC2 instance:
175198
176−1. Launch a NixOS 18.09 EC2 instance (we recommend [this AMI](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#LaunchInstanceWizard:ami=ami-009c9c3f1af480ff3))
199+1. Launch a NixOS 19.09 EC2 instance (we recommend [this AMI](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#LaunchInstanceWizard:ami=ami-00a8eeaf232a74f84))
1772001. In the instance configuration wizard ensure that your instance has at least 1GB RAM and 10GB disk space.
1782011. When prompted save your AWS private key (`~/myaws.pem`) somewhere safe. We'll need it later during deployment.
1792021. Go to "Security Groups", select your instance's security group and under "Inbound" tab add a new rule for HTTP port 80 and 443.
all-builds.nixnew file+106
@@ -0,0 +1,106 @@
1+{ self-args ? {
2+ config.android_sdk.accept_license = true;
3+ iosSdkVersion = "10.2";
4+ }
5+, local-self ? import ./. self-args
6+, supportedSystems ? [
7+ "x86_64-linux"
8+ "x86_64-darwin"
9+ ]
10+}:
11+
12+let
13+ inherit (local-self.nixpkgs) lib runCommand nix;
14+ cacheBuildSystems = supportedSystems;
15+
16+ obeliskPackagesCommon = [
17+ "obelisk-frontend"
18+ "obelisk-route"
19+ "obelisk-executable-config-lookup"
20+ ];
21+
22+ obeliskPackagesBackend = obeliskPackagesCommon ++ [
23+ "obelisk-asset-manifest"
24+ "obelisk-asset-serve-snap"
25+ "obelisk-backend"
26+ "obelisk-cliapp"
27+ "obelisk-command"
28+ "obelisk-executable-config-inject"
29+ "obelisk-frontend"
30+ "obelisk-run"
31+ "obelisk-route"
32+ "obelisk-selftest"
33+ "obelisk-snap-extras"
34+ ];
35+
36+ pnameToAttrs = pkgsSet: pnames:
37+ lib.listToAttrs (map
38+ (name: { inherit name; value = pkgsSet.${name}; })
39+ pnames);
40+
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 [];
46+
47+ perPlatform = lib.genAttrs cacheBuildSystems (system: let
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));
100+ });
101+
102+ metaCache = local-self.reflex-platform.pinBuildInputs
103+ "obelisk-everywhere"
104+ (map (a: a.cache) (builtins.attrValues perPlatform));
105+
106+in perPlatform // { inherit metaCache; }
all-tests.nixnew file+145
@@ -0,0 +1,145 @@
1+{
2+ supportedSystems ? [ builtins.currentSystem ]
3+}:
4+let
5+ nginxRoot = "/run/nginx";
6+ obelisk = import ./default.nix {};
7+ # Get NixOS a pre-release 20.03 that contains the python based tests and recursive nix
8+ pkgs = import (builtins.fetchTarball https://github.com/nixos/nixpkgs/archive/3de5266.tar.gz) {};
9+ sshKeys = import (pkgs.path + /nixos/tests/ssh-keys.nix) pkgs;
10+ make-test = import (pkgs.path + /nixos/tests/make-test-python.nix);
11+ obelisk-everywhere = (import ./all-builds.nix { inherit supportedSystems; }).x86_64-linux.cache;
12+ snakeOilPrivateKey = sshKeys.snakeOilPrivateKey.text;
13+ snakeOilPublicKey = sshKeys.snakeOilPublicKey;
14+in
15+ make-test ({...}: {
16+ name = "obelisk";
17+ nodes = {
18+ githost = {
19+ networking.firewall.allowedTCPPorts = [ 22 80 ];
20+ services.openssh = {
21+ enable = true;
22+ };
23+ environment.systemPackages = [
24+ pkgs.git
25+ ];
26+ users.users.root.openssh.authorizedKeys.keys = [
27+ snakeOilPublicKey
28+ ];
29+ };
30+
31+ client = {
32+ imports = [
33+ (pkgs.path + /nixos/modules/installer/cd-dvd/channel.nix)
34+ ];
35+ nix.useSandbox = false;
36+ nix.binaryCaches = [];
37+ environment.systemPackages = [
38+ obelisk.command
39+ obelisk.shell
40+ obelisk-everywhere
41+ pkgs.git
42+ ];
43+ };
44+ };
45+
46+ testScript =
47+ let
48+ 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+ '';
55+ sshConfigFile = pkgs.writeText "ssh_config" ''
56+ Host *
57+ StrictHostKeyChecking no
58+ UserKnownHostsFile=/dev/null
59+ ConnectionAttempts=1
60+ ConnectTimeout=1
61+ IdentityFile=~/.ssh/id_rsa
62+ User=root
63+ '';
64+ in ''
65+ start_all()
66+ githost.wait_for_open_port("22")
67+
68+ with subtest("test obelisk is installed"):
69+ client.succeed("ob --help")
70+
71+ with subtest("test the client can access the server via ssh"):
72+ client.succeed("mkdir -p ~/.ssh/")
73+ client.succeed(
74+ "cp ${privateKeyFile} ~/.ssh/id_rsa"
75+ )
76+ client.succeed("chmod 600 ~/.ssh/id_rsa")
77+ client.wait_until_succeeds(
78+ "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa githost true"
79+ )
80+ client.succeed(
81+ "cp ${sshConfigFile} ~/.ssh/config"
82+ )
83+ client.wait_until_succeeds("ssh githost true")
84+
85+ with subtest("test a remote bare repo can be started"):
86+ githost.succeed("mkdir -p ~/myorg/myapp.git")
87+ githost.succeed("cd ~/myorg/myapp.git && git init --bare")
88+
89+ with subtest("test a git project can be configured with a remote using ssh"):
90+ client.succeed("mkdir -p ~/code/myapp")
91+ client.succeed("cd ~/code/myapp && git init")
92+ client.succeed(
93+ "cp ${thunkableSample} ~/code/myapp/default.nix"
94+ )
95+ client.succeed("cd ~/code/myapp && git add .")
96+ client.succeed('git config --global user.email "you@example.com"')
97+ client.succeed('git config --global user.name "Your Name"')
98+ client.succeed('cd ~/code/myapp && git commit -m "Initial"')
99+ client.succeed(
100+ "cd ~/code/myapp && git remote add origin root@githost:/root/myorg/myapp.git"
101+ )
102+
103+
104+ with subtest("test pushing code to the remote"):
105+ client.succeed("cd ~/code/myapp && git push -u origin master")
106+ client.succeed("cd ~/code/myapp && git status")
107+
108+ with subtest("test obelisk can pack"):
109+ client.succeed("ob -v thunk pack ~/code/myapp")
110+ client.succeed("grep -qF 'git' ~/code/myapp/default.nix")
111+ 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")
139+
140+ with subtest("test obelisk can detect private repos"):
141+ client.succeed("ob -v thunk pack ~/code/myapp")
142+ client.succeed("""grep -qF '"private": true' ~/code/myapp/git.json""")
143+ client.succeed("ob -v thunk unpack ~/code/myapp")
144+ '';
145+ }) {}
default.nix+145−98
@@ -2,6 +2,7 @@
22, profiling ? false
33, iosSdkVersion ? "10.2"
44, config ? {}
5+, reflex-platform-func ? import ./dep/reflex-platform
56}:
67let
78 cleanSource = builtins.filterSource (name: _: let baseName = builtins.baseNameOf name; in !(
@@ -16,7 +17,7 @@ let
1617 openssh
1718 ];
1819
19− getReflexPlatform = sys: import ./dep/reflex-platform {
20+ getReflexPlatform = sys: reflex-platform-func {
2021 inherit iosSdkVersion config;
2122 system = sys;
2223 enableLibraryProfiling = profiling;
@@ -34,11 +35,10 @@ let
3435 pkgs = self.callPackage ({ pkgs }: pkgs) {};
3536 haskellLib = pkgs.haskell.lib;
3637 in {
37− hnix = haskellLib.overrideCabal (self.callHackage "hnix" "0.6.1" { these = self.these_0_8; }) (_: {
38− jailbreak = true;
39− doCheck = false;
40− });
38+ hnix = haskellLib.dontCheck (haskellLib.doJailbreak (self.callCabal2nix "hnix" (hackGet dep/hnix) {}));
4139 hnix-store-core = self.callHackage "hnix-store-core" "0.1.0.0" {};
40+
41+ ghcid = self.callCabal2nix "ghcid" (hackGet ./dep/ghcid) {};
4242 })
4343
4444 pkgs.obeliskExecutableConfig.haskellOverlay
@@ -46,18 +46,35 @@ let
4646 # Add obelisk packages
4747 (self: super: let
4848 pkgs = self.callPackage ({ pkgs }: pkgs) {};
49+ onLinux = pkg: f: if pkgs.stdenv.isLinux then f pkg else pkg;
4950 in {
51+ shelly = self.callHackage "shelly" "1.9.0" {};
52+
5053 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
5154
5255 obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (cleanSource ./lib/asset/manifest) {};
5356 obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (cleanSource ./lib/asset/serve-snap) {};
5457 obelisk-backend = self.callCabal2nix "obelisk-backend" (cleanSource ./lib/backend) {};
5558 obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanSource ./lib/cliapp) {};
56− obelisk-command = self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {};
59+ obelisk-command = haskellLib.overrideCabal (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {}) {
60+ librarySystemDepends = [
61+ pkgs.nix
62+ (haskellLib.justStaticExecutables self.ghcid)
63+ ];
64+ };
5765 obelisk-frontend = self.callCabal2nix "obelisk-frontend" (cleanSource ./lib/frontend) {};
58− obelisk-run = self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {};
66+ obelisk-run = onLinux (self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {}) (pkg:
67+ haskellLib.overrideCabal pkg (drv: { librarySystemDepends = [ pkgs.iproute ]; })
68+ );
5969 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
60− 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+ };
6178 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
6279 tabulation = self.callCabal2nix "tabulation" (cleanSource ./lib/tabulation) {};
6380 })
@@ -69,7 +86,7 @@ let
6986 # Dynamic linking with split objects dramatically increases startup time (about
7087 # 0.5 seconds on a decent machine with SSD), so we do `justStaticExecutables`.
7188 obelisk-command = haskellLib.overrideCabal
72− (haskellLib.addOptparseApplicativeCompletionScripts "ob"
89+ (haskellLib.generateOptparseApplicativeCompletion "ob"
7390 (haskellLib.justStaticExecutables super.obelisk-command))
7491 (drv: {
7592 buildTools = (drv.buildTools or []) ++ [ pkgs.buildPackages.makeWrapper ];
@@ -106,19 +123,18 @@ in rec {
106123 pathGit = ./.; # Used in CI by the migration graph hash algorithm to correctly ignore files.
107124 path = reflex-platform.filterGit ./.;
108125 obelisk = ghcObelisk;
109− obeliskEnvs = ghcObeliskEnvs;
126+ obeliskEnvs = pkgs.lib.filterAttrs (k: _: pkgs.lib.strings.hasPrefix "obelisk-" k) ghcObeliskEnvs;
110127 command = ghcObelisk.obelisk-command;
111128 shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs);
112129
113130 selftest = pkgs.writeScript "selftest" ''
114− #!/usr/bin/env bash
131+ #!${pkgs.runtimeShell}
115132 set -euo pipefail
116133
117134 PATH="${command}/bin:$PATH"
118− export OBELISK_IMPL="${hackGet ./.}"
135+ cd ${./.}
119136 "${ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" +RTS -N -RTS "$@"
120137 '';
121− #TODO: Why can't I build ./skeleton directly as a derivation? `nix-build -E ./.` doesn't work
122138 skeleton = pkgs.runCommand "skeleton" {
123139 dir = builtins.filterSource (path: type: builtins.trace path (baseNameOf path != ".obelisk")) ./skeleton;
124140 } ''
@@ -140,6 +156,7 @@ in rec {
140156 compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
141157 mkdir $out
142158 cd $out
159+ # TODO profiling + static shouldn't break and need an ad-hoc workaround like that
143160 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
144161 ${if optimizationLevel == null then ''
145162 ln -s all.unminified.js all.js
@@ -222,7 +239,7 @@ in rec {
222239 pkgs.runCommand "serverExe" {} ''
223240 mkdir $out
224241 set -eux
225− ln -s "${haskellLib.justStaticExecutables backend}"/bin/* $out/
242+ ln -s "${if profiling then backend else haskellLib.justStaticExecutables backend}"/bin/* $out/
226243 ln -s "${mkAssets assets}" $out/static.assets
227244 ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
228245 echo ${version} > $out/version
@@ -241,100 +258,130 @@ in rec {
241258 };
242259 };
243260
244−
245261 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
246− project = base: projectDefinition:
247− let projectOut = sys: (getReflexPlatform sys).project (args@{ nixpkgs, ... }:
248− let mkProject = { android ? null #TODO: Better error when missing
249− , ios ? null #TODO: Better error when missing
250− , packages ? {}
251− , overrides ? _: _: {}
252− , staticFiles ? base + /static
253− , tools ? _: []
254− , shellToolOverrides ? _: _: {}
255− , withHoogle ? false # Setting this to `true` makes shell reloading far slower
256− , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
257− }:
258− let frontendName = "frontend";
259− backendName = "backend";
260− commonName = "common";
261− staticName = "obelisk-generated-static";
262− staticFilesImpure = if lib.isDerivation staticFiles then staticFiles else toString staticFiles;
263− processedStatic = processAssets { src = staticFiles; };
264− # The packages whose names and roles are defined by this package
265− predefinedPackages = lib.filterAttrs (_: x: x != null) {
266− ${frontendName} = nullIfAbsent (base + "/frontend");
267− ${commonName} = nullIfAbsent (base + "/common");
268− ${backendName} = nullIfAbsent (base + "/backend");
269− };
270− combinedPackages = predefinedPackages // packages;
271− projectOverrides = self: super: {
272− ${staticName} = haskellLib.dontHaddock (self.callCabal2nix staticName processedStatic.haskellManifest {});
273− ${backendName} = haskellLib.addBuildDepend super.${backendName} self.obelisk-run;
274− };
275− totalOverrides = lib.composeExtensions projectOverrides overrides;
276− inherit (lib.strings) hasPrefix;
277− privateConfigDirs = ["config/backend"];
278− injectableConfig = builtins.filterSource (path: _:
279− !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
280− );
281− __androidWithConfig = configPath: {
282− ${if android == null then null else frontendName} = {
283− executableName = "frontend";
284− ${if builtins.pathExists staticFiles then "assets" else null} =
285− nixpkgs.obeliskExecutableConfig.platforms.android.inject
286− (injectableConfig configPath)
287− processedStatic.symlinked;
288− } // 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;
289350 };
290− __iosWithConfig = configPath: {
291− ${if ios == null then null else frontendName} = {
292− executableName = "frontend";
293− ${if builtins.pathExists staticFiles then "staticSrc" else null} =
294− nixpkgs.obeliskExecutableConfig.platforms.ios.inject
295− (injectableConfig configPath)
296− processedStatic.symlinked;
297− } // 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+ ;
298364 };
299− in {
300− inherit shellToolOverrides tools withHoogle;
301− overrides = totalOverrides;
302− packages = combinedPackages;
303− shells = {
304− ${if android == null && ios == null then null else "ghcSavedSplices"} = (lib.filter (x: lib.hasAttr x combinedPackages) [
305− commonName
306− frontendName
307− ]);
308− ghc = (lib.filter (x: lib.hasAttr x combinedPackages) [
309− backendName
310− commonName
311− frontendName
312− ]);
313− ghcjs = lib.filter (x: lib.hasAttr x combinedPackages) [
314− frontendName
315− commonName
316− ];
317365 };
318− android = __androidWithConfig (base + "/config");
319− ios = __iosWithConfig (base + "/config");
320− passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __iosWithConfig __androidWithConfig; };
321− };
322− in mkProject (projectDefinition args));
323− serverOn = sys: version: serverExe
324− (projectOut sys).ghc.backend
325− (projectOut system).ghcjs.frontend
326− (projectOut sys).passthru.staticFiles
327− (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
328375 version;
329− linuxExe = serverOn "x86_64-linux";
376+ linuxExe = serverOn (projectOut "x86_64-linux");
330377 dummyVersion = "Version number is only available for deployments";
331− in projectOut system // {
378+ in mainProjectOut // {
332379 linuxExeConfigurable = linuxExe;
333380 linuxExe = linuxExe dummyVersion;
334− exe = serverOn system dummyVersion;
381+ exe = serverOn mainProjectOut dummyVersion;
335382 server = args@{ hostName, adminEmail, routeHost, enableHttps, version }:
336383 server (args // { exe = linuxExe version; });
337− obelisk = import (base + "/.obelisk/impl") {};
384+ obelisk = import (base' + "/.obelisk/impl") {};
338385 };
339386 haskellPackageSets = {
340387 inherit (reflex-platform) ghc ghcjs;
dep/.gitignore+1
@@ -1,4 +1,5 @@
11*
2+!*/
23!.gitignore
34!*/*.json
45!*/default.nix
dep/ghcid/default.nixnew file+7
@@ -0,0 +1,7 @@
1+# DO NOT HAND-EDIT THIS FILE
2+import ((import <nixpkgs> {}).fetchFromGitHub (
3+ let json = builtins.fromJSON (builtins.readFile ./github.json);
4+ in { inherit (json) owner repo rev sha256;
5+ private = json.private or false;
6+ }
7+))
dep/ghcid/github.jsonnew file+7
@@ -0,0 +1,7 @@
1+{
2+ "owner": "ndmitchell",
3+ "repo": "ghcid",
4+ "branch": "master",
5+ "rev": "f572318f32b1617f6054248e5888af68222f8e50",
6+ "sha256": "1icg3r70lg2kmd9gdc024ih1n9nrja98yav74z9nvykqygvv5w0n"
7+}
dep/hnix/default.nixnew file+7
@@ -0,0 +1,7 @@
1+# DO NOT HAND-EDIT THIS FILE
2+import ((import <nixpkgs> {}).fetchFromGitHub (
3+ let json = builtins.fromJSON (builtins.readFile ./github.json);
4+ in { inherit (json) owner repo rev sha256;
5+ private = json.private or false;
6+ }
7+))
dep/hnix/github.jsonnew file+7
@@ -0,0 +1,7 @@
1+{
2+ "owner": "haskell-nix",
3+ "repo": "hnix",
4+ "branch": "master",
5+ "rev": "6c9c7c310c54372b3db0fdf5a0137b395cde1bdb",
6+ "sha256": "1i5903b7lxqn2s3jarb14h6wdq8bxiik1hp0xy43w5w1hgvvq0g5"
7+}
dep/reflex-platform/default.nix+7−6
@@ -1,7 +1,8 @@
11# DO NOT HAND-EDIT THIS FILE
2−import ((import <nixpkgs> {}).fetchFromGitHub (
3− let json = builtins.fromJSON (builtins.readFile ./github.json);
4− in { inherit (json) owner repo rev sha256;
5− private = json.private or false;
6− }
7−))
2+let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:
3+ if !fetchSubmodules && !private then builtins.fetchTarball {
4+ url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256;
5+ } else (import <nixpkgs> {}).fetchFromGitHub {
6+ inherit owner repo rev sha256 fetchSubmodules private;
7+ };
8+in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))
dep/reflex-platform/github.json+4−3
@@ -1,7 +1,8 @@
11{
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
4− "branch": "develop",
5− "rev": "b75dfe258c31eb1688090d121f33b62a07071589",
6− "sha256": "0isvqn6y8nnfb4a3nabjsafrz4mfa01vhkn6i61idh6zfb8gciqf"
4+ "branch": "master",
5+ "private": false,
6+ "rev": "5429278830e1555a577f2550e045ce7f7164aa65",
7+ "sha256": "1lp86cgccmim573rarsjny5vh0ygkfp5afq7006li0k9w2sw2d4c"
78}
jobsets.nixdeleted−71
@@ -1,71 +0,0 @@
1−{ prs }:
2−
3−let
4− self = import ./. {};
5− pkgs = self.nixpkgs;
6− mkFetchGithub = value: {
7− inherit value;
8− type = "git";
9− emailresponsible = false;
10− };
11−in
12−with pkgs.lib;
13−let
14− defaults = jobs: {
15− inherit (jobs) description;
16− enabled = 1;
17− hidden = false;
18− keepnr = 10;
19− schedulingshares = 100;
20− checkinterval = 120;
21− enableemail = false;
22− emailoverride = "";
23− nixexprinput = "obelisk";
24− nixexprpath = "release.nix";
25− inputs = jobs.inputs // {
26− nixpkgs = {
27− type = "git";
28− value = "https://github.com/NixOS/nixpkgs-channels nixos-unstable";
29− emailresponsible = false;
30− };
31− };
32− self-args = {
33− type = "nix";
34− value = "{ config.android_sdk.accept_license = true; }";
35− emailresponsible = false;
36− };
37− };
38− branchJobset = branch: defaults {
39− description = "obelisk-${branch}";
40− inputs = {
41− obelisk = {
42− value = "https://github.com/obsidiansystems/obelisk ${branch}";
43− type = "git";
44− emailresponsible = false;
45− };
46− };
47− };
48− makePr = num: info: {
49− name = "obelisk-pr-${num}";
50− value = defaults {
51− description = "#${num}: ${info.title}";
52− inputs = {
53− obelisk = {
54− #NOTE: This should really use "pull/${num}/merge"; however, GitHub's
55− #status checks only operate on PR heads. This creates a race
56− #condition, which can currently only be solved by requiring PRs to be
57− #up to date before they're merged. See
58− #https://github.com/isaacs/github/issues/1002
59− value = "https://github.com/obsidiansystems/obelisk pull/${num}/head";
60− type = "git";
61− emailresponsible = false;
62− };
63− };
64− };
65− };
66− processedPrs = mapAttrs' makePr (builtins.fromJSON (builtins.readFile prs));
67− jobsetsAttrs = processedPrs //
68− genAttrs ["master"] branchJobset;
69−in {
70− jobsets = pkgs.writeText "spec.json" (builtins.toJSON jobsetsAttrs);
71−}
lib/asset/README.md+10−10
@@ -1,7 +1,7 @@
11## obelisk-asset
22
3−The 'assets.nix' file contains nix expressions that are a mutually
4−recursive set of attributes used to create static asset directories
3+The 'assets.nix' file contains nix expressions that are a mutually
4+recursive set of attributes used to create static asset directories
55with hashable file encodings. This file should be incorporated within
66your project's 'default.nix' file. (See ./example/default.nix , line 2)
77```
@@ -10,20 +10,20 @@ your project's 'default.nix' file. (See ./example/default.nix , line 2)
1010```
1111
1212The 'mkAssets' and 'mkAssetsWith' functions within 'assets.nix' only
13−need to be passed an encoding('zopfliEncodings', 'gzipEncodings',
13+need to be passed an encoding('zopfliEncodings', 'gzipEncodings',
1414'noEncodings') and a static directory of your choice as arguments.
1515
16−In this example, 'mkAssets' is used on a directory that has a .png
16+In this example, 'mkAssets' is used on a directory that has a .png
1717file inside of it. (See ./example/default.nix, line 11)
1818```
19−myAssets = assets.mkAssets ./static;
19+myAssets = assets.mkAssets ./static;
2020```
2121
22−Once you have successfully incorporated 'assets.nix' into your
23−project's nix file(s), use nix-build to generate a symlink of
24−your 'mkAssets' expression. The following command from
25−example/run-example (line 3) will generate an immutable symlink
26−that holds hashed static assets.
22+Once you have successfully incorporated 'assets.nix' into your
23+project's nix file(s), use nix-build to generate a symlink of
24+your 'mkAssets' expression. The following command from
25+example/run-example (line 3) will generate an immutable symlink
26+that holds hashed static assets.
2727```bash
2828nix-build -o static.assets -A myAssets
2929```
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/cliapp/obelisk-cliapp.cabal+3−2
@@ -7,9 +7,11 @@ 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
14+ , containers
1315 , exceptions
1416 , io-streams
1517 , lens
@@ -23,7 +25,6 @@ library
2325 , transformers
2426 exposed-modules:
2527 Obelisk.CliApp
26− Obelisk.CliApp.Demo
2728 other-modules:
2829 Obelisk.CliApp.Logging
2930 Obelisk.CliApp.Process
lib/cliapp/src/Obelisk/CliApp.hs+14−6
@@ -33,16 +33,24 @@ module Obelisk.CliApp
3333 , Severity (..)
3434
3535 -- .Process
36− , ProcessFailure (..)
3736 , AsProcessFailure (..)
38− , readProcessAndLogStderr
39− , readProcessAndLogOutput
40− , readCreateProcessWithExitCode
37+ , ProcessFailure (..)
38+ , ProcessSpec (..)
39+ , callCommand
40+ , callProcess
4141 , callProcessAndLogOutput
4242 , createProcess_
43− , callProcess
44− , callCommand
43+ , overCreateProcess
44+ , proc
45+ , readCreateProcessWithExitCode
46+ , readProcessAndLogOutput
47+ , readProcessAndLogStderr
48+ , readProcessJSONAndLogStderr
4549 , reconstructCommand
50+ , setCwd
51+ , setDelegateCtlc
52+ , setEnvOverride
53+ , shell
4654 ) where
4755
4856import Control.Monad.Log (Severity (..))
lib/cliapp/src/Obelisk/CliApp/Demo.hsdeleted−53
@@ -1,53 +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−import System.Process (proc)
12−
13−import Control.Monad.Catch (MonadMask)
14−
15−import Obelisk.CliApp
16−
17−cliDemo
18− :: ( MonadIO m, MonadMask m, MonadFail m
19− , CliLog m, HasCliConfig e m, CliThrow e m, AsProcessFailure e, AsUnstructuredError e)
20− => m ()
21−cliDemo = withSpinner "CLI Demo" $ do
22− putLog Notice "This demo will showcase the CLI library functionality"
23− _ <- withSpinner' "Searching long for something" (Just $ \c -> "Discovered " <> T.pack (show c) <> " objects") $ do
24− delay
25− withSpinner "Nested task" $ do
26− delay
27− putLog Notice "In nested task"
28− -- This is a 'no trail' spinner that won't leave a trail (even its sub spinners)
29− withSpinnerNoTrail "In between, doing something temporary" $ do
30− withSpinner "Runnning something deep inside" $ do
31− delay
32− putLog Notice "You can still log from inside no-trail spinners"
33− delay
34− delay
35− putLog Error "Top nested task finished"
36− delay
37− putLog Error "Some user error while spinning"
38− delay
39− putLog Notice "This is some info mesage"
40− putLog Warning "And now a warning as well"
41− delay
42− return (42 :: Integer)
43− putLog Notice "Now we start a 2nd spinner, run a couple of process, the last of which fails:"
44− withSpinner "Looking around" $ do
45− delay
46− output <- readProcessAndLogStderr Notice $ proc "ls" ["-l", "/"]
47− putLog Notice $ "Output was: " <> output
48− delay
49− callProcessAndLogOutput (Notice, Error) $ proc "ls" ["-l", "/does-not-exist"]
50− delay
51− failWith "Something dangerous happened"
52− where
53− 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+96−34
@@ -9,17 +9,25 @@
99-- | An extension of `System.Process` that integrates with logging (`Obelisk.CLI.Logging`)
1010-- and is thus spinner friendly.
1111module Obelisk.CliApp.Process
12− ( ProcessFailure (..)
13− , AsProcessFailure (..)
14− , readProcessAndLogStderr
15− , readProcessAndLogOutput
16− , readCreateProcessWithExitCode
12+ ( AsProcessFailure (..)
13+ , ProcessFailure (..)
14+ , ProcessSpec (..)
15+ , callCommand
16+ , callProcess
1717 , callProcessAndLogOutput
1818 , createProcess
1919 , createProcess_
20− , callProcess
21− , callCommand
20+ , overCreateProcess
21+ , proc
22+ , readCreateProcessWithExitCode
23+ , readProcessAndLogOutput
24+ , readProcessAndLogStderr
25+ , readProcessJSONAndLogStderr
2226 , reconstructCommand
27+ , setCwd
28+ , setDelegateCtlc
29+ , setEnvOverride
30+ , shell
2331 ) where
2432
2533import Control.Monad ((<=<), join, void)
@@ -30,24 +38,51 @@ import Control.Lens (Prism', review)
3038import qualified Data.ByteString as BS
3139import qualified Data.ByteString.Char8 as BSC
3240import Data.Function (fix)
41+import Data.Map (Map)
42+import qualified Data.Map as Map
3343import Data.Monoid ((<>))
3444import Data.Text (Text)
3545import qualified Data.Text as T
3646import qualified Data.Text.Encoding as T
3747import Data.Text.Encoding.Error (lenientDecode)
48+import System.Environment (getEnvironment)
3849import System.Exit (ExitCode (..))
3950import System.IO (Handle)
4051import System.IO.Streams (InputStream, handleToInputStream)
4152import qualified System.IO.Streams as Streams
4253import System.IO.Streams.Concurrent (concurrentMerge)
43−import System.Process (CreateProcess, ProcessHandle, StdStream (CreatePipe), cmdspec, std_err, std_out,
44− waitForProcess)
54+import System.Process (CreateProcess, ProcessHandle, StdStream (CreatePipe), std_err, std_out, waitForProcess)
4555import qualified System.Process as Process
56+import qualified Data.Aeson as Aeson
4657
4758import Control.Monad.Log (Severity (..))
4859import Obelisk.CliApp.Logging (putLog, putLogRaw)
4960import Obelisk.CliApp.Types (CliLog, CliThrow)
5061
62+data ProcessSpec = ProcessSpec
63+ { _processSpec_createProcess :: !CreateProcess
64+ , _processSpec_overrideEnv :: !(Maybe (Map String String -> Map String String))
65+ }
66+
67+proc :: FilePath -> [String] -> ProcessSpec
68+proc cmd args = ProcessSpec (Process.proc cmd args) Nothing
69+
70+shell :: String -> ProcessSpec
71+shell cmd = ProcessSpec (Process.shell cmd) Nothing
72+
73+setEnvOverride :: (Map String String -> Map String String) -> ProcessSpec -> ProcessSpec
74+setEnvOverride f p = p { _processSpec_overrideEnv = Just f }
75+
76+overCreateProcess :: (CreateProcess -> CreateProcess) -> ProcessSpec -> ProcessSpec
77+overCreateProcess f (ProcessSpec p x) = ProcessSpec (f p) x
78+
79+setDelegateCtlc :: Bool -> ProcessSpec -> ProcessSpec
80+setDelegateCtlc b = overCreateProcess (\p -> p { Process.delegate_ctlc = b })
81+
82+setCwd :: Maybe FilePath -> ProcessSpec -> ProcessSpec
83+setCwd fp = overCreateProcess (\p -> p { Process.cwd = fp })
84+
85+
5186-- TODO put back in `Obelisk.CliApp.Process` and use prisms for extensible exceptions
5287data ProcessFailure = ProcessFailure Process.CmdSpec Int -- exit code
5388 deriving Show
@@ -62,17 +97,31 @@ instance AsProcessFailure ProcessFailure where
6297
6398readProcessAndLogStderr
6499 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
65− => Severity -> CreateProcess -> m Text
100+ => Severity -> ProcessSpec -> m Text
66101readProcessAndLogStderr sev process = do
67102 (out, _err) <- withProcess process $ \_out err -> do
68103 streamToLog =<< liftIO (streamHandle sev err)
69104 liftIO $ T.decodeUtf8With lenientDecode <$> BS.hGetContents out
70105
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+
71119readCreateProcessWithExitCode
72120 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
73− => CreateProcess -> m (ExitCode, String, String)
74−readCreateProcessWithExitCode process = do
75− putLog Debug $ "Creating process: " <> reconstructCommand (cmdspec process)
121+ => ProcessSpec -> m (ExitCode, String, String)
122+readCreateProcessWithExitCode procSpec = do
123+ process <- mkCreateProcess procSpec
124+ putLog Debug $ "Creating process: " <> reconstructProcSpec procSpec
76125 liftIO $ Process.readCreateProcessWithExitCode process ""
77126
78127-- | Like `System.Process.readProcess` but logs the combined output (stdout and stderr)
@@ -84,10 +133,10 @@ readCreateProcessWithExitCode process = do
84133-- `callProcessAndLogOutput (Debug, Debug)`.
85134readProcessAndLogOutput
86135 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
87− => (Severity, Severity) -> CreateProcess -> m Text
136+ => (Severity, Severity) -> ProcessSpec -> m Text
88137readProcessAndLogOutput (sev_out, sev_err) process = do
89− (_, Just out, Just err, p) <- createProcess $ process
90− { std_out = CreatePipe , std_err = CreatePipe }
138+ (_, Just out, Just err, p) <- createProcess $ overCreateProcess
139+ (\p -> p { std_out = CreatePipe , std_err = CreatePipe }) process
91140
92141 -- TODO interleave stdout and stderr in log correctly
93142 streamToLog =<< liftIO (streamHandle sev_err err)
@@ -96,9 +145,9 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
96145
97146 liftIO (waitForProcess p) >>= \case
98147 ExitSuccess -> pure outText
99− ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (cmdspec process) code
148+ ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess process) code
100149
101−-- | Like `System.Process.callProcess` but logs the combined output (stdout and stderr)
150+-- | Like 'System.Process.callProcess' but logs the combined output (stdout and stderr)
102151-- with the corresponding severity.
103152--
104153-- Usually this function is called as `callProcessAndLogOutput (Debug, Error)`. However
@@ -106,9 +155,8 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
106155-- which case it is advisable to call it with a non-Error severity for stderr, like
107156-- `callProcessAndLogOutput (Debug, Debug)`.
108157callProcessAndLogOutput
109−
110158 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
111− => (Severity, Severity) -> CreateProcess -> m ()
159+ => (Severity, Severity) -> ProcessSpec -> m ()
112160callProcessAndLogOutput (sev_out, sev_err) process =
113161 void $ withProcess process $ \out err -> do
114162 stream <- liftIO $ join $ combineStream
@@ -118,21 +166,31 @@ callProcessAndLogOutput (sev_out, sev_err) process =
118166 where
119167 combineStream s1 s2 = concurrentMerge [s1, s2]
120168
121−-- | Like `System.Process.createProcess` but also logs (debug) the process being run
169+-- | Like 'System.Process.createProcess' but also logs (debug) the process being run
122170createProcess
123171 :: (MonadIO m, CliLog m)
124− => CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
125−createProcess p = do
126− putLog Debug $ "Creating process: " <> reconstructCommand (cmdspec p)
172+ => ProcessSpec -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
173+createProcess procSpec = do
174+ p <- mkCreateProcess procSpec
175+ putLog Debug $ "Creating process: " <> reconstructProcSpec procSpec
127176 liftIO $ Process.createProcess p
128177
129178-- | Like `System.Process.createProcess_` but also logs (debug) the process being run
130179createProcess_
131180 :: (MonadIO m, CliLog m)
132− => String -> CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
133−createProcess_ name p = do
134− putLog Debug $ "Creating process " <> T.pack name <> ": " <> reconstructCommand (cmdspec p)
135− liftIO $ Process.createProcess p
181+ => String -> ProcessSpec -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
182+createProcess_ name procSpec = do
183+ p <- mkCreateProcess procSpec
184+ putLog Debug $ "Creating process " <> T.pack name <> ": " <> reconstructProcSpec procSpec
185+ liftIO $ Process.createProcess_ name p
186+
187+mkCreateProcess :: MonadIO m => ProcessSpec -> m Process.CreateProcess
188+mkCreateProcess (ProcessSpec p override') = do
189+ case override' of
190+ Nothing -> pure p
191+ Just override -> do
192+ procEnv <- Map.fromList <$> maybe (liftIO getEnvironment) pure (Process.env p)
193+ pure $ p { Process.env = Just $ Map.toAscList (override procEnv) }
136194
137195-- | Like `System.Process.callProcess` but also logs (debug) the process being run
138196callProcess
@@ -152,17 +210,17 @@ callCommand cmd = do
152210
153211withProcess
154212 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
155− => CreateProcess -> (Handle -> Handle -> m ()) -> m (Handle, Handle)
213+ => ProcessSpec -> (Handle -> Handle -> m ()) -> m (Handle, Handle)
156214withProcess process f = do -- TODO: Use bracket.
157215 -- FIXME: Using `withCreateProcess` here leads to something operating illegally on closed handles.
158− (_, Just out, Just err, p) <- createProcess $ process
159− { std_out = CreatePipe , std_err = CreatePipe }
216+ (_, Just out, Just err, p) <- createProcess $ overCreateProcess
217+ (\x -> x { std_out = CreatePipe , std_err = CreatePipe }) process
160218
161219 f out err -- Pass the handles to the passed function
162220
163221 liftIO (waitForProcess p) >>= \case
164222 ExitSuccess -> return (out, err)
165− ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (cmdspec process) code
223+ ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess process) code
166224
167225-- Create an input stream from the file handle, associating each item with the given severity.
168226streamHandle :: Severity -> Handle -> IO (InputStream (Severity, BSC.ByteString))
@@ -179,8 +237,12 @@ streamToLog stream = fix $ \loop -> do
179237
180238-- | Pretty print a 'CmdSpec'
181239reconstructCommand :: Process.CmdSpec -> Text
182−reconstructCommand (Process.ShellCommand str) = T.pack str
183−reconstructCommand (Process.RawCommand c as) = processToShellString c as
240+reconstructCommand p = case p of
241+ Process.ShellCommand str -> T.pack str
242+ Process.RawCommand c as -> processToShellString c as
184243 where
185244 processToShellString cmd args = T.unwords $ map quoteAndEscape (cmd : args)
186245 quoteAndEscape x = "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
246+
247+reconstructProcSpec :: ProcessSpec -> Text
248+reconstructProcSpec = reconstructCommand . Process.cmdspec . _processSpec_createProcess
lib/command/obelisk-command.cabal+11−10
@@ -1,5 +1,5 @@
11name: obelisk-command
2−version: 0.1
2+version: 0.5
33cabal-version: >= 1.8
44build-type: Simple
55
@@ -7,35 +7,35 @@ 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
2323 , filepath
24+ , git
2425 , github
2526 , here
26− , git
2727 , hnix
2828 , hpack
2929 , io-streams
30− , exceptions
31− , logging-effect
3230 , lens
31+ , logging-effect
32+ , megaparsec
3333 , modern-uri
3434 , monad-loops
3535 , mtl
36− , megaparsec
3736 , network
3837 , network-uri
38+ , obelisk-cliapp
3939 , optparse-applicative
4040 , placeholders
4141 , process
@@ -46,6 +46,7 @@ library
4646 , transformers
4747 , unix
4848 , unordered-containers
49+ , which
4950 , yaml
5051 exposed-modules:
5152 Obelisk.App
@@ -57,8 +58,8 @@ library
5758 Obelisk.Command.Thunk
5859 Obelisk.Command.Utils
5960 Obelisk.Command.VmBuilder
60− -- -fobject-code is so that the StaticPointers extension can work in ghci
61− ghc-options: -Wall -fobject-code
61+ Obelisk.Command.Preprocessor
62+ ghc-options: -Wall
6263
6364executable ob
6465 main-is: src-bin/ob.hs
lib/command/src/Obelisk/Command.hs+64−74
@@ -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
@@ -13,8 +11,8 @@ import qualified Data.Binary as Binary
1311import Data.Bool (bool)
1412import qualified Data.ByteString.Base16 as Base16
1513import qualified Data.ByteString.Lazy as LBS
14+import Data.Foldable (for_)
1615import Data.List
17−import Data.Maybe (catMaybes)
1816import qualified Data.Text as T
1917import Data.Text.Encoding
2018import Data.Text.Encoding.Error (lenientDecode)
@@ -30,13 +28,12 @@ import System.Posix.Process (executeFile)
3028
3129import Obelisk.App
3230import Obelisk.CliApp
33−import Obelisk.CliApp.Demo (cliDemo)
3431import Obelisk.Command.Deploy
3532import Obelisk.Command.Project
3633import Obelisk.Command.Run
3734import Obelisk.Command.Thunk
38−import Obelisk.Command.Utils
3935import qualified Obelisk.Command.VmBuilder as VmBuilder
36+import qualified Obelisk.Command.Preprocessor as Preprocessor
4037
4138
4239data Args = Args
@@ -95,53 +92,33 @@ data ObCommand
9592 | ObCommand_Watch
9693 | ObCommand_Shell ShellOpts
9794 | ObCommand_Doc String [String] -- shell and list of packages
95+ | ObCommand_Hoogle String Int -- shell and port
9896 | ObCommand_Internal ObInternal
9997 deriving Show
10098
10199data ObInternal
102− = ObInternal_RunStaticIO StaticKey
103− | 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]
104103 deriving Show
105104
106−inNixShell' :: MonadObelisk m => StaticPtr (ObeliskT IO ()) -> m ()
107−inNixShell' p = withProjectRoot "." $ \root -> do
108− cmd <- liftIO $ unwords <$> mkCmd -- TODO: shell escape instead of unwords
109− projectShell root False "ghc" (Just cmd)
110− where
111− mkCmd = do
112− argsCfg <- getArgsConfig
113− myArgs <- getArgs
114− obArgs <- parseCLIArgs argsCfg myArgs
115− progName <- getObeliskExe
116− return $ progName : catMaybes
117− [ Just "--no-handoff"
118− , bool Nothing (Just "--verbose") $ _args_verbose obArgs
119− , Just "internal"
120− , Just "run-static-io"
121− , Just $ encodeStaticKey $ staticKey p
122− ]
123−
124105obCommand :: ArgsConfig -> Parser ObCommand
125106obCommand cfg = hsubparser
126− (mconcat
127− [ command "init" $ info (ObCommand_Init <$> initSource <*> initForce) $ progDesc "Initialize an Obelisk project"
128− , command "deploy" $ info (ObCommand_Deploy <$> deployCommand cfg) $ progDesc "Prepare a deployment for an Obelisk project"
129− , command "run" $ info (pure ObCommand_Run) $ progDesc "Run current project in development mode"
130− , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
131− , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
132− , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
133− , command "shell" $ info (ObCommand_Shell <$> shellOpts) $ progDesc "Enter a shell with project dependencies"
134− , command "doc" $ info (ObCommand_Doc <$> shellFlags <*> packageNames) $
135− progDesc "List paths to haddock documentation for specified packages"
136− <> footerDoc (Just $
137− text "Hint: To open the documentation you can pipe the output of this command like"
138− <$$> text "ob doc reflex reflex-dom-core | xargs -n1 xdg-open")
139− ])
140− <|> subparser
141− (mconcat
142− [ internal
143− , command "internal" (info (ObCommand_Internal <$> internalCommand) mempty)
144− ])
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+ ])
145122
146123packageNames :: Parser [String]
147124packageNames = some (strArgument (metavar "PACKAGE-NAME..."))
@@ -206,12 +183,6 @@ data DeployInitOpts = DeployInitOpts
206183 }
207184 deriving Show
208185
209−internalCommand :: Parser ObInternal
210−internalCommand = subparser $ mconcat
211− [ command "run-static-io" $ info (ObInternal_RunStaticIO <$> argument (eitherReader decodeStaticKey) (action "static-key")) mempty
212− , command "clidemo" $ info (pure ObInternal_CLIDemo) mempty
213− ]
214−
215186--TODO: Result should provide normalised path and also original user input for error reporting.
216187thunkDirectoryParser :: Parser FilePath
217188thunkDirectoryParser = fmap (dropTrailingPathSeparator . normalise) . strArgument $ mconcat
@@ -220,20 +191,35 @@ thunkDirectoryParser = fmap (dropTrailingPathSeparator . normalise) . strArgumen
220191 , help "Path to directory containing thunk data"
221192 ]
222193
194+thunkConfig :: Parser ThunkConfig
195+thunkConfig = ThunkConfig
196+ <$>
197+ ( flag' (Just True) (long "private" <> help "Mark thunks as pointing to a private repository")
198+ <|> flag' (Just False) (long "public" <> help "Mark thunks as pointing to a public repository")
199+ <|> pure Nothing
200+ )
201+
202+thunkUpdateConfig :: Parser ThunkUpdateConfig
203+thunkUpdateConfig = ThunkUpdateConfig
204+ <$> optional (strOption (long "branch" <> metavar "BRANCH" <> help "Use the given branch when looking for the latest revision"))
205+ <*> thunkConfig
206+
207+thunkPackConfig :: Parser ThunkPackConfig
208+thunkPackConfig = ThunkPackConfig
209+ <$> switch (long "force" <> short 'f' <> help "Force packing thunks even if there are branches not pushed upstream, uncommitted changes, stashes. This will cause changes that have not been pushed upstream to be lost; use with care.")
210+ <*> thunkConfig
211+
223212data ThunkCommand
224− = ThunkCommand_Update [FilePath] (Maybe String)
213+ = ThunkCommand_Update [FilePath] ThunkUpdateConfig
225214 | ThunkCommand_Unpack [FilePath]
226− | ThunkCommand_Pack [FilePath] Bool
215+ | ThunkCommand_Pack [FilePath] ThunkPackConfig
227216 deriving Show
228217
229−forceFlag :: Parser Bool
230−forceFlag = switch $ long "force" <> short 'f' <> help "Force packing thunks even if there are branches not pushed upstream, uncommitted changes, stashes. This will cause changes that have not been pushed upstream to be lost; use with care."
231−
232218thunkCommand :: Parser ThunkCommand
233219thunkCommand = hsubparser $ mconcat
234− [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> optional (strOption (long "branch" <> metavar "BRANCH"))) $ progDesc "Update thunk to latest revision available"
220+ [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> thunkUpdateConfig) $ progDesc "Update thunk to latest revision available"
235221 , command "unpack" $ info (ThunkCommand_Unpack <$> some thunkDirectoryParser) $ progDesc "Unpack thunk into git checkout of revision it points to"
236− , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser <*> forceFlag) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
222+ , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser <*> thunkPackConfig) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
237223 ]
238224
239225data ShellOpts
@@ -254,20 +240,24 @@ shellOpts = ShellOpts
254240 <$> shellFlags
255241 <*> optional (strArgument (metavar "COMMAND"))
256242
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+
257246parserPrefs :: ParserPrefs
258247parserPrefs = defaultPrefs
259248 { prefShowHelpOnEmpty = True
260249 }
261250
262251parseCLIArgs :: ArgsConfig -> [String] -> IO Args
263−parseCLIArgs cfg as = pure as >>= handleParseResult . execParserPure parserPrefs (argsInfo cfg)
252+parseCLIArgs cfg = handleParseResult . execParserPure parserPrefs (argsInfo cfg)
264253
265254-- | Create an Obelisk config for the current process.
266255mkObeliskConfig :: IO Obelisk
267256mkObeliskConfig = do
268257 cliArgs <- getArgs
269258 -- This function should not use argument parser (full argument parsing happens post handoff)
270− let logLevel = toLogLevel $ "-v" `elem` cliArgs
259+ -- TODO: See if we can use the argument parser with a subset of the parsers to get logging level out.
260+ let logLevel = toLogLevel $ any (`elem` ["-v", "--verbose"]) cliArgs
271261 notInteractive <- not <$> isInteractiveTerm
272262 cliConf <- newCliConfig logLevel notInteractive notInteractive $ \case
273263 ObeliskError_ProcessError (ProcessFailure p code) ann ->
@@ -332,6 +322,9 @@ main' argsCfg = do
332322 liftIO $ executeFile impl False ("--no-handoff" : myArgs) Nothing
333323 case myArgs of
334324 "--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
335328 a:as -- Otherwise bash completion would always hand-off even if the user isn't trying to
336329 | "--bash-completion" `isPrefixOf` a
337330 && "--no-handoff" `elem` as -> go (a:as)
@@ -358,7 +351,7 @@ ob = \case
358351 Right (ThunkData_Packed ptr) -> return ptr
359352 Right (ThunkData_Checkout (Just ptr)) -> return ptr
360353 Right (ThunkData_Checkout Nothing) ->
361− getThunkPtr False root
354+ getThunkPtr False root Nothing
362355 let sshKeyPath = _deployInitOpts_sshKey deployOpts
363356 hostname = _deployInitOpts_hostname deployOpts
364357 route = _deployInitOpts_route deployOpts
@@ -372,25 +365,22 @@ ob = \case
372365 Just RemoteBuilder_ObeliskVM -> (:[]) <$> VmBuilder.getNixBuildersArg
373366 DeployCommand_Update -> deployUpdate "."
374367 DeployCommand_Test (platform, extraArgs) -> deployMobile platform extraArgs
375− ObCommand_Run -> inNixShell' $ static run
376− -- inNixShell ($(mkClosure 'ghcidAction) ())
368+ ObCommand_Run -> run
377369 ObCommand_Thunk tc -> case tc of
378− ThunkCommand_Update thunks mBranch -> mapM_ ((flip updateThunkToLatest) mBranch) thunks
379− ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
380− ThunkCommand_Pack thunks force -> forM_ thunks (packThunk force)
370+ ThunkCommand_Update thunks config -> for_ thunks (updateThunkToLatest config)
371+ ThunkCommand_Unpack thunks -> for_ thunks unpackThunk
372+ ThunkCommand_Pack thunks config -> for_ thunks (packThunk config)
381373 ObCommand_Repl -> runRepl
382− ObCommand_Watch -> inNixShell' $ static runWatch
374+ ObCommand_Watch -> runWatch
383375 ObCommand_Shell so -> withProjectRoot "." $ \root ->
384376 projectShell root False (_shellOpts_shell so) (_shellOpts_command so)
385− ObCommand_Doc shell pkgs -> withProjectRoot "." $ \root ->
386− projectShell root False shell (Just $ haddockCommand pkgs)
377+ ObCommand_Doc shell' pkgs -> withProjectRoot "." $ \root ->
378+ 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"
387381 ObCommand_Internal icmd -> case icmd of
388− ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case
389− Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
390− Just p -> do
391− c <- getObelisk
392− liftIO $ runObelisk c $ deRefStaticPtr p
393− ObInternal_CLIDemo -> cliDemo
382+ ObInternal_ApplyPackages origPath inPath outPath packagePaths -> do
383+ liftIO $ Preprocessor.applyPackages origPath inPath outPath packagePaths
394384
395385haddockCommand :: [String] -> String
396386haddockCommand pkgs = unwords
lib/command/src/Obelisk/Command/Deploy.hs+14−14
@@ -25,22 +25,23 @@ import Nix.Pretty
2525import Nix.String (principledMakeNixStringWithoutContext)
2626import Nix.Value
2727import System.Directory
28−import System.Environment (getEnvironment)
2928import System.FilePath
3029import System.IO
3130import System.Posix.Files
32−import System.Process (delegate_ctlc, env, proc, cwd)
3331import Text.URI (URI)
3432import qualified Text.URI as URI
3533import Text.URI.Lens
3634
3735import Obelisk.App (MonadObelisk)
38−import Obelisk.CliApp (Severity (..), callProcessAndLogOutput, failWith, putLog, withSpinner)
36+import Obelisk.CliApp (
37+ Severity (..), callProcessAndLogOutput, failWith, proc, putLog,
38+ setCwd, setDelegateCtlc, setEnvOverride, withSpinner)
3939import Obelisk.Command.Nix
4040import Obelisk.Command.Project
4141import Obelisk.Command.Thunk
4242import Obelisk.Command.Utils
4343
44+
4445deployInit
4546 :: MonadObelisk m
4647 => ThunkPtr
@@ -57,7 +58,7 @@ deployInit thunkPtr deployDir sshKeyPath hostnames route adminEmail enableHttps
5758 False -> failWith $ T.pack $ "ob deploy init: file does not exist: " <> sshKeyPath
5859 True -> pure $ deployDir </> "ssh_key"
5960 callProcessAndLogOutput (Notice, Error) $
60− proc "cp" [sshKeyPath, localKey]
61+ proc cp [sshKeyPath, localKey]
6162 liftIO $ setFileMode localKey $ ownerReadMode .|. ownerWriteMode
6263 return localKey
6364 withSpinner "Validating configuration" $ do
@@ -82,7 +83,7 @@ deployInit thunkPtr deployDir sshKeyPath hostnames route adminEmail enableHttps
8283 writeDeployConfig deployDir "backend_hosts" $ unlines hostnames
8384 writeDeployConfig deployDir "enable_https" $ show enableHttps
8485 writeDeployConfig deployDir "admin_email" adminEmail
85− writeDeployConfig deployDir ("config" </> "common" </> "route") $ route
86+ writeDeployConfig deployDir ("config" </> "common" </> "route") route
8687 withSpinner "Creating source thunk (./src)" $ liftIO $ do
8788 createThunk (deployDir </> "src") thunkPtr
8889 setupObeliskImpl deployDir
@@ -107,7 +108,7 @@ deployPush deployPath getNixBuilders = do
107108 Right (ThunkData_Packed ptr) -> return ptr
108109 Right (ThunkData_Checkout _) -> do
109110 checkGitCleanStatus srcPath True >>= \case
110− True -> packThunk False srcPath
111+ True -> packThunk (ThunkPackConfig False (ThunkConfig Nothing)) srcPath
111112 False -> failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
112113 Left err -> failWith $ "ob deploy push: couldn't read src thunk: " <> T.pack (show err)
113114 let version = show . _thunkRev_commit $ _thunkPtr_rev thunkPtr
@@ -157,7 +158,7 @@ deployPush deployPath getNixBuilders = do
157158 ]
158159 isClean <- checkGitCleanStatus deployPath True
159160 when (not isClean) $ do
160− withSpinner "Commiting changes to Git" $ do
161+ withSpinner "Committing changes to Git" $ do
161162 callProcessAndLogOutput (Debug, Error) $ proc "git"
162163 ["-C", deployPath, "add", "."]
163164 callProcessAndLogOutput (Debug, Error) $ proc "git"
@@ -165,12 +166,11 @@ deployPush deployPath getNixBuilders = do
165166 putLog Notice $ "Deployed => " <> T.pack route
166167 where
167168 callProcess' envMap cmd args = do
168− processEnv <- Map.toList . (envMap <>) . Map.fromList <$> liftIO getEnvironment
169− let p = (proc cmd args) { delegate_ctlc = True, env = Just processEnv }
169+ let p = setEnvOverride (envMap <>) $ setDelegateCtlc True $ proc cmd args
170170 callProcessAndLogOutput (Notice, Notice) p
171171
172172deployUpdate :: MonadObelisk m => FilePath -> m ()
173−deployUpdate deployPath = updateThunkToLatest (deployPath </> "src") Nothing
173+deployUpdate deployPath = updateThunkToLatest (ThunkUpdateConfig Nothing (ThunkConfig Nothing)) (deployPath </> "src")
174174
175175keytoolToAndroidConfig :: KeytoolConfig -> HM.HashMap Text (NValue t Identity m)
176176keytoolToAndroidConfig conf =
@@ -219,7 +219,7 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
219219 , _keytoolConfig_storepass = keyStorePassword
220220 , _keytoolConfig_keypass = keyStorePassword
221221 }
222− createKeystore root $ keyToolConf
222+ createKeystore root keyToolConf
223223 liftIO $ BSL.writeFile keytoolConfPath $ encode keyToolConf
224224 checkKeytoolConfExist <- liftIO $ doesFileExist keytoolConfPath
225225 unless checkKeytoolConfExist $ failWith "Missing android KeytoolConfig"
@@ -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
@@ -275,13 +275,13 @@ instance ToJSON KeytoolConfig
275275createKeystore :: MonadObelisk m => FilePath -> KeytoolConfig -> m ()
276276createKeystore root config = do
277277 let expr = "with (import " <> toImplDir root <> ").reflex-platform.nixpkgs; pkgs.mkShell { buildInputs = [ pkgs.jdk ]; }"
278− callProcessAndLogOutput (Notice,Notice) $ (proc "nix-shell" ["default.nix", "-E" , expr, "--run" , keytoolCmd]) { cwd = Just root }
278+ callProcessAndLogOutput (Notice,Notice) $ setCwd (Just root) $ proc "nix-shell" ["default.nix", "-E" , expr, "--run" , keytoolCmd]
279279 where
280280 keytoolCmd = processToShellString "keytool"
281281 [ "-genkeypair", "-noprompt"
282282 , "-keystore", _keytoolConfig_keystore config
283283 , "-keyalg", "RSA", "-keysize", "2048"
284− , "-validity", "1000000000"
284+ , "-validity", "1000000"
285285 , "-storepass", _keytoolConfig_storepass config
286286 , "-alias", _keytoolConfig_alias config
287287 , "-keypass", _keytoolConfig_keypass config
lib/command/src/Obelisk/Command/Nix.hs+45−15
@@ -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
@@ -37,7 +46,6 @@ import Data.List (intercalate)
3746import Data.Maybe
3847import Data.Monoid ((<>))
3948import qualified Data.Text as T
40−import System.Process (proc)
4149
4250import Obelisk.App (MonadObelisk)
4351import Obelisk.CliApp
@@ -63,10 +71,10 @@ data Arg
6371 deriving (Eq, Show)
6472
6573strArg :: String -> String -> Arg
66−strArg k = Arg_Str k
74+strArg = Arg_Str
6775
6876rawArg :: String -> String -> Arg
69−rawArg k = Arg_Expr k
77+rawArg = Arg_Expr
7078
7179boolArg :: String -> Bool -> Arg
7280boolArg k = Arg_Expr k . bool "false" "true"
@@ -86,7 +94,7 @@ makeClassy ''NixCommonConfig
8694instance Default NixCommonConfig where
8795 def = NixCommonConfig def mempty mempty
8896
89−runNixCommonConfig :: NixCommonConfig -> [FilePath]
97+runNixCommonConfig :: NixCommonConfig -> [String]
9098runNixCommonConfig cfg = mconcat [maybeToList path, attrArg, exprArg, args, buildersArg]
9199 where
92100 path = _target_path $ _nixCmdConfig_target cfg
@@ -124,7 +132,7 @@ instance HasNixCommonConfig NixBuildConfig where
124132instance Default NixBuildConfig where
125133 def = NixBuildConfig def def
126134
127−runNixBuildConfig :: NixBuildConfig -> [FilePath]
135+runNixBuildConfig :: NixBuildConfig -> [String]
128136runNixBuildConfig cfg = mconcat
129137 [ runNixCommonConfig $ cfg ^. nixCommonConfig
130138 , case _nixBuildConfig_outLink cfg of
@@ -145,12 +153,26 @@ instance HasNixCommonConfig NixInstantiateConfig where
145153instance Default NixInstantiateConfig where
146154 def = NixInstantiateConfig def False
147155
148−runNixInstantiateConfig :: NixInstantiateConfig -> [FilePath]
156+runNixInstantiateConfig :: NixInstantiateConfig -> [String]
149157runNixInstantiateConfig cfg = mconcat
150158 [ runNixCommonConfig $ cfg ^. nixCommonConfig
151159 , "--eval" <$ guard (_nixInstantiateConfig_eval cfg)
152160 ]
153161
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+
154176data NixCmd
155177 = NixCmd_Build NixBuildConfig
156178 | NixCmd_Instantiate NixInstantiateConfig
@@ -158,9 +180,17 @@ data NixCmd
158180instance Default NixCmd where
159181 def = NixCmd_Build def
160182
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+
161191nixCmd :: MonadObelisk m => NixCmd -> m FilePath
162192nixCmd cmdCfg = withSpinner' ("Running " <> cmd <> desc) (Just $ const $ "Built " <> desc) $ do
163− output <- readProcessAndLogStderr Debug $ proc (T.unpack cmd) $ options
193+ output <- readProcessAndLogStderr Debug $ proc (T.unpack cmd) options
164194 -- Remove final newline that Nix appends
165195 Just (outPath, '\n') <- pure $ T.unsnoc output
166196 pure $ T.unpack outPath
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+142−54
@@ -3,37 +3,55 @@
33{-# LANGUAGE OverloadedStrings #-}
44module Obelisk.Command.Project
55 ( InitSource (..)
6− , initProject
76 , findProjectObeliskCommand
87 , findProjectRoot
9− , withProjectRoot
10− , inProjectShell
11− , inImpureProjectShell
8+ , initProject
9+ , nixShellRunConfig
10+ , nixShellRunProc
11+ , nixShellWithHoogle
12+ , nixShellWithPkgs
13+ , obeliskDirName
1214 , projectShell
13−
14− , toObeliskDir
1515 , toImplDir
16+ , toNixPath
17+ , toObeliskDir
18+ , withProjectRoot
1619 ) where
1720
21+import Control.Concurrent.MVar (MVar, newMVar, withMVarMasked)
22+import Control.Lens ((.~), (?~), (<&>))
1823import Control.Monad
1924import Control.Monad.IO.Class (liftIO)
2025import Control.Monad.State
26+import qualified Data.Aeson as Json
2127import Data.Bits
22−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)
2334import qualified Data.Text as T
35+import Data.Text.Encoding (decodeUtf8)
36+import Data.Traversable (for)
2437import System.Directory
38+import System.Environment (lookupEnv)
2539import System.FilePath
2640import System.IO.Temp
27−import System.Posix (FileStatus, UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
41+import System.IO.Unsafe (unsafePerformIO)
42+import System.Posix (FileStatus, FileMode, CMode (..), UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
2843import System.Posix.Files
29−import System.Process (CreateProcess, cwd, proc, waitForProcess, delegate_ctlc)
44+import System.Process (waitForProcess)
3045
3146import GitHub.Data.GitData (Branch)
3247import GitHub.Data.Name (Name)
3348
3449import Obelisk.App (MonadObelisk)
3550import Obelisk.CliApp
51+import Obelisk.Command.Nix
3652import Obelisk.Command.Thunk
53+import Obelisk.Command.Utils (nixExePath)
54+
3755--TODO: Make this module resilient to random exceptions
3856
3957--TODO: Don't hardcode this
@@ -47,17 +65,21 @@ obeliskSourceWithBranch branch = ThunkSource_GitHub $ GitHubSource
4765 { _gitHubSource_owner = "obsidiansystems"
4866 , _gitHubSource_repo = "obelisk"
4967 , _gitHubSource_branch = Just branch
68+ , _gitHubSource_private = False
5069 }
5170
5271data InitSource
53− = InitSource_Default
54− | InitSource_Branch (Name Branch)
55− | InitSource_Symlink FilePath
56− deriving Show
72+ = InitSource_Default
73+ | InitSource_Branch (Name Branch)
74+ | InitSource_Symlink FilePath
75+ deriving Show
76+
77+obeliskDirName :: FilePath
78+obeliskDirName = ".obelisk"
5779
5880-- | Path to obelisk directory in given path
5981toObeliskDir :: FilePath -> FilePath
60−toObeliskDir p = p </> ".obelisk"
82+toObeliskDir p = p </> obeliskDirName
6183
6284-- | Path to impl file in given path
6385toImplDir :: FilePath -> FilePath
@@ -114,15 +136,16 @@ initProject source force = withSystemTempDirectory "ob-init" $ \tmpDir -> do
114136findProjectObeliskCommand :: MonadObelisk m => FilePath -> m (Maybe FilePath)
115137findProjectObeliskCommand target = do
116138 myUid <- liftIO getRealUserID
139+ processUmask <- liftIO getUmask
117140 targetStat <- liftIO $ getFileStatus target
118− (result, insecurePaths) <- flip runStateT [] $ walkToProjectRoot target targetStat myUid >>= \case
141+ (result, insecurePaths) <- flip runStateT [] $ walkToProjectRoot target targetStat processUmask myUid >>= \case
119142 Nothing -> pure Nothing
120143 Just projectRoot -> liftIO (doesDirectoryExist $ toImplDir projectRoot) >>= \case
121144 False -> do
122145 putLog Warning $ "Found obelisk directory in " <> T.pack projectRoot <> " but the implementation (impl) file is missing"
123146 pure Nothing
124147 True -> do
125− walkToImplDir projectRoot myUid -- For security check
148+ walkToImplDir projectRoot myUid processUmask -- For security check
126149 return $ Just projectRoot
127150 case (result, insecurePaths) of
128151 (Just projDir, []) -> do
@@ -133,17 +156,37 @@ findProjectObeliskCommand target = do
133156 putLog Error $ T.unlines
134157 [ "Error: Found a project at " <> T.pack (normalise projDir) <> ", but had to traverse one or more insecure directories to get there:"
135158 , T.unlines $ fmap (T.pack . normalise) insecurePaths
136− , "Please ensure that all of these directories are owned by you and are not writable by anyone else."
159+ , "Please ensure that all of these directories are owned by you, not world-writable, and no more group-writable than permitted by your umask."
137160 ]
138161 return Nothing
139162
163+-- | Get the umask for the Obelisk process.
164+--
165+-- Because of
166+-- http://man7.org/linux/man-pages/man2/umask.2.html#NOTES we have to set the
167+-- umask to read it. We are using 'withMVarMasked' to guarantee that setting and
168+-- reading isn't interrupted by any exception or interleaved with another thread.
169+getUmask :: IO FileMode
170+getUmask = withMVarMasked globalUmaskLock $ \() -> do
171+ initialMask <- setFileCreationMask safeUmask
172+ void (setFileCreationMask initialMask)
173+ pure initialMask
174+ where
175+ safeUmask :: FileMode
176+ safeUmask = CMode 0o22
177+
178+{-# NOINLINE globalUmaskLock #-}
179+globalUmaskLock :: MVar ()
180+globalUmaskLock = unsafePerformIO (newMVar ())
181+
140182-- | Get the FilePath to the containing project directory, if there is one
141183findProjectRoot :: MonadObelisk m => FilePath -> m (Maybe FilePath)
142184findProjectRoot target = do
143185 myUid <- liftIO getRealUserID
144186 targetStat <- liftIO $ getFileStatus target
145− (result, _) <- liftIO $ runStateT (walkToProjectRoot target targetStat myUid) []
146− return result
187+ umask <- liftIO getUmask
188+ (result, _) <- liftIO $ runStateT (walkToProjectRoot target targetStat umask myUid) []
189+ return $ makeRelative "." <$> result
147190
148191withProjectRoot :: MonadObelisk m => FilePath -> (FilePath -> m a) -> m a
149192withProjectRoot target f = findProjectRoot target >>= \case
@@ -155,15 +198,15 @@ withProjectRoot target f = findProjectRoot target >>= \case
155198-- traversed in the process. Return the project root directory, if found.
156199walkToProjectRoot
157200 :: (MonadState [FilePath] m, MonadIO m)
158− => FilePath -> FileStatus -> UserID -> m (Maybe FilePath)
159−walkToProjectRoot this thisStat myUid = liftIO (doesDirectoryExist this) >>= \case
201+ => FilePath -> FileStatus -> FileMode -> UserID -> m (Maybe FilePath)
202+walkToProjectRoot this thisStat desiredUmask myUid = liftIO (doesDirectoryExist this) >>= \case
160203 -- It's not a directory, so it can't be a project
161204 False -> do
162205 let dir = takeDirectory this
163206 dirStat <- liftIO $ getFileStatus dir
164− walkToProjectRoot dir dirStat myUid
207+ walkToProjectRoot dir dirStat desiredUmask myUid
165208 True -> do
166− when (not $ isWritableOnlyBy thisStat myUid) $ modify (this:)
209+ when (not $ isWellOwnedAndWellPermissioned thisStat myUid desiredUmask) $ modify (this:)
167210 liftIO (doesDirectoryExist $ toObeliskDir this) >>= \case
168211 True -> return $ Just this
169212 False -> do
@@ -173,49 +216,94 @@ walkToProjectRoot this thisStat myUid = liftIO (doesDirectoryExist this) >>= \ca
173216 isSameFileAs = (==) `on` fileIdentity
174217 if thisStat `isSameFileAs` nextStat
175218 then return Nothing -- Found a cycle; probably hit root directory
176− else walkToProjectRoot next nextStat myUid
219+ else walkToProjectRoot next nextStat desiredUmask myUid
177220
178221-- | Walk from the given project root directory to its Obelisk implementation
179222-- directory, accumulating potentially insecure directories that were traversed
180223-- in the process.
181−walkToImplDir :: (MonadState [FilePath] m, MonadIO m) => FilePath -> UserID -> m ()
182−walkToImplDir projectRoot myUid = do
224+walkToImplDir :: (MonadState [FilePath] m, MonadIO m) => FilePath -> UserID -> FileMode -> m ()
225+walkToImplDir projectRoot myUid umask = do
183226 let obDir = toObeliskDir projectRoot
184227 obDirStat <- liftIO $ getFileStatus obDir
185− when (not $ isWritableOnlyBy obDirStat myUid) $ modify (obDir:)
228+ when (not $ isWellOwnedAndWellPermissioned obDirStat myUid umask) $ modify (obDir:)
186229 let implThunk = obDir </> "impl"
187230 implThunkStat <- liftIO $ getFileStatus implThunk
188− when (not $ isWritableOnlyBy implThunkStat myUid) $ modify (implThunk:)
231+ when (not $ isWellOwnedAndWellPermissioned implThunkStat myUid umask) $ modify (implThunk:)
189232
190−--TODO: Is there a better way to ask if anyone else can write things?
191−--E.g. what about ACLs?
192−-- | Check to see if directory is only writable by a user whose User ID matches the second argument provided
193−isWritableOnlyBy :: FileStatus -> UserID -> Bool
194−isWritableOnlyBy s uid = fileOwner s == uid && fileMode s .&. 0o22 == 0
233+-- | Check to see if directory is writable by a user whose User ID matches the
234+-- second argument provided, and if the fact that other people can write to that
235+-- directory is in accordance with the umask of the system, passed as the third
236+-- argument.
237+isWellOwnedAndWellPermissioned :: FileStatus -> UserID -> FileMode -> Bool
238+isWellOwnedAndWellPermissioned s uid umask = isOwnedBy s uid && filePermissionIsSafe s umask
195239
196−-- | Run a command in the given shell for the current project
197−inProjectShell :: MonadObelisk m => String -> String -> m ()
198−inProjectShell shellName command = withProjectRoot "." $ \root ->
199− projectShell root True shellName (Just command)
240+isOwnedBy :: FileStatus -> UserID -> Bool
241+isOwnedBy s uid = fileOwner s == uid
200242
201−inImpureProjectShell :: MonadObelisk m => String -> String -> m ()
202−inImpureProjectShell shellName command = withProjectRoot "." $ \root ->
203− projectShell root False shellName (Just command)
243+-- | Check to see if a directory respect the umask, but check explicitly that
244+-- it's not world writable in any case.
245+filePermissionIsSafe :: FileStatus -> FileMode -> Bool
246+filePermissionIsSafe s umask = not fileWorldWritable && fileGroupWritable <= umaskGroupWritable
247+ where
248+ fileWorldWritable = fileMode s .&. 0o002 == 0o002
249+ fileGroupWritable = fileMode s .&. 0o020 == 0o020
250+ umaskGroupWritable = umask .&. 0o020 == 0
204251
205−projectShell :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
206−projectShell root isPure shellName command = do
207− (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCtlc $ setCwd (Just root) $ proc "nix-shell" $
208− [ "default.nix"] <>
209− [ "--pure" | isPure ] <>
210− [ "-A"
211− , "shells." <> shellName
212− ] <> case command of
213− Nothing -> []
214− Just c -> ["--run", c]
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
258+
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+ ]
215290 void $ liftIO $ waitForProcess ph
216291
217−setCtlc :: CreateProcess -> CreateProcess
218−setCtlc cfg = cfg { delegate_ctlc = True }
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
219302
220−setCwd :: Maybe FilePath -> CreateProcess -> CreateProcess
221−setCwd fp cfg = cfg { cwd = fp }
303+projectShell :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
304+projectShell root isPure shellName command = do
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)
309+ void $ liftIO $ waitForProcess ph
lib/command/src/Obelisk/Command/Run.hs+256−146
@@ -3,46 +3,59 @@
33{-# LANGUAGE MultiParamTypeClasses #-}
44{-# LANGUAGE OverloadedStrings #-}
55{-# LANGUAGE ScopedTypeVariables #-}
6+{-# LANGUAGE TupleSections #-}
67module Obelisk.Command.Run where
78
8−import Control.Exception (bracket)
9−import Control.Monad
9+import Control.Arrow ((&&&))
10+import Control.Exception (Exception, bracket)
11+import Control.Lens (ifor)
12+import Control.Monad (filterM, unless)
13+import Control.Monad.Except (runExceptT, throwError)
1014import Control.Monad.IO.Class (liftIO)
1115import Control.Monad.Reader (MonadIO)
16+import Data.Coerce (coerce)
1217import Data.Either
13−import qualified Data.List as L
18+import Data.Foldable (for_, toList)
1419import qualified Data.List.NonEmpty as NE
20+import Data.Map (Map)
21+import qualified Data.Map as Map
1522import Data.Maybe
23+import qualified Data.Set as Set
24+import Data.Text (Text)
1625import qualified Data.Text as T
26+import Data.Traversable (for)
27+import Debug.Trace (trace)
1728import Distribution.Compiler (CompilerFlavor(..))
1829import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
1930import Distribution.Parsec.ParseResult (runParseResult)
20−import Distribution.Pretty
31+import qualified Distribution.System as Dist
2132import Distribution.Types.BuildInfo
2233import Distribution.Types.CondTree
2334import Distribution.Types.GenericPackageDescription
2435import Distribution.Types.Library
2536import Distribution.Utils.Generic
26−import Hpack.Config
27−import Hpack.Render
28−import Hpack.Yaml
37+import qualified Distribution.Parsec.Common as Dist
38+import qualified Hpack.Config as Hpack
39+import qualified Hpack.Render as Hpack
40+import qualified Hpack.Yaml as Hpack
2941import Language.Haskell.Extension
3042import Network.Socket hiding (Debug)
3143import System.Directory
44+import System.Environment (getExecutablePath)
3245import System.FilePath
33−import System.Process (proc)
46+import qualified System.Info
3447import System.IO.Temp (withSystemTempDirectory)
35−import Data.ByteString (ByteString)
3648
37−import Obelisk.App (MonadObelisk, ObeliskT)
38−import Obelisk.CliApp
39− ( CliT (..), HasCliConfig, Severity (..)
40− , callCommand, failWith, getCliConfig, putLog
41− , readProcessAndLogStderr, runCli)
42−import Obelisk.Command.Project (inProjectShell, withProjectRoot)
49+import Obelisk.App (MonadObelisk)
50+import Obelisk.CliApp (Severity (..) , failWith, putLog, proc, readCreateProcessWithExitCode, readProcessAndLogStderr, setCwd)
51+import Obelisk.Command.Project (withProjectRoot, nixShellWithPkgs)
52+import Obelisk.Command.Utils (findExePath, ghcidExePath, nixBuildExePath, nixExePath)
4353
4454data CabalPackageInfo = CabalPackageInfo
45− { _cabalPackageInfo_packageRoot :: FilePath
55+ { _cabalPackageInfo_packageFile :: FilePath
56+ , _cabalPackageInfo_packageName :: T.Text
57+ , _cabalPackageInfo_packageRoot :: FilePath
58+ , _cabalPackageInfo_buildable :: Bool
4659 , _cabalPackageInfo_sourceDirs :: NE.NonEmpty FilePath
4760 -- ^ List of hs src dirs of the library component
4861 , _cabalPackageInfo_defaultExtensions :: [Extension]
@@ -53,150 +66,230 @@ data CabalPackageInfo = CabalPackageInfo
5366 -- ^ List of compiler-specific options (e.g., the "ghc-options" field of the cabal file)
5467 }
5568
56−-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
57−run :: ObeliskT IO ()
58−run = do
59− pkgs <- getLocalPkgs
60− withGhciScript pkgs $ \dotGhciPath -> do
61− freePort <- getFreePort
62− assets <- withProjectRoot "." $ \root -> do
63− let importableRoot = if "/" `isInfixOf` root
64− then root
65− else "./" <> root
66− isDerivation <- readProcessAndLogStderr Debug $
67− proc "nix"
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
77+ assets <- do
78+ isDerivation <- readProcessAndLogStderr Debug $ setCwd (Just root) $
79+ proc nixExePath
6880 [ "eval"
69− , "-f"
70− , root
71− , "(let a = import " <> importableRoot <> " {}; in toString (a.reflex.nixpkgs.lib.isDerivation a.passthru.staticFilesImpure))"
81+ , "(let a = import ./. {}; in toString (a.reflex.nixpkgs.lib.isDerivation a.passthru.staticFilesImpure))"
7282 , "--raw"
7383 -- `--raw` is not available with old nix-instantiate. It drops quotation
7484 -- marks and trailing newline, so is very convenient for shelling out.
7585 ]
7686 -- Check whether the impure static files are a derivation (and so must be built)
7787 if isDerivation == "1"
78− then fmap T.strip $ readProcessAndLogStderr Debug $ -- Strip whitespace here because nix-build has no --raw option
79− proc "nix-build"
88+ then fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $ -- Strip whitespace here because nix-build has no --raw option
89+ proc nixBuildExePath
8090 [ "--no-out-link"
81− , "-E", "(import " <> importableRoot <> "{}).passthru.staticFilesImpure"
91+ , "-E", "(import ./. {}).passthru.staticFilesImpure"
8292 ]
83− else readProcessAndLogStderr Debug $
84− proc "nix" ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
93+ else readProcessAndLogStderr Debug $ setCwd (Just root) $
94+ proc nixExePath ["eval", "-f", ".", "passthru.staticFilesImpure", "--raw"]
8595 putLog Debug $ "Assets impurely loaded from: " <> assets
86− runGhcid dotGhciPath $ Just $ unwords
96+ freePort <- getFreePort
97+ runGhcid root True dotGhciPath pkgs $ Just $ unwords
8798 [ "Obelisk.Run.run"
8899 , show freePort
89− , "(runServeAsset " ++ show assets ++ ")"
100+ , "(Obelisk.Run.runServeAsset " ++ show assets ++ ")"
90101 , "Backend.backend"
91102 , "Frontend.frontend"
92103 ]
93104
94105runRepl :: MonadObelisk m => m ()
95−runRepl = do
96− pkgs <- getLocalPkgs
97− withGhciScript pkgs $ \dotGhciPath -> do
98− runGhciRepl dotGhciPath
106+runRepl = withProjectRoot "." $ \root -> do
107+ pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
108+ withGhciScript pkgs "." $ \dotGhciPath ->
109+ runGhciRepl root pkgs dotGhciPath
99110
100111runWatch :: MonadObelisk m => m ()
101−runWatch = do
102− pkgs <- getLocalPkgs
103− withGhciScript pkgs $ \dotGhciPath -> runGhcid dotGhciPath Nothing
112+runWatch = withProjectRoot "." $ \root -> do
113+ pkgs <- fmap toList . parsePackagesOrFail =<< getLocalPkgs root
114+ withGhciScript pkgs root $ \dotGhciPath ->
115+ runGhcid root True dotGhciPath pkgs Nothing
116+
117+-- | Relative paths to local packages of an obelisk project.
118+--
119+-- These are a combination of the obelisk predefined local packages,
120+-- and any packages that the user has set with the @packages@ argument
121+-- to the Nix @project@ function.
122+getLocalPkgs :: MonadObelisk m => FilePath -> m [FilePath]
123+getLocalPkgs root = do
124+ obeliskPaths <- runFind ["-L", root, "-name", ".obelisk", "-type", "d"]
125+
126+ -- We do not want to find packages that are embedded inside other obelisk projects, unless that
127+ -- obelisk project is our own.
128+ let exclusions = filter (/= root) $ map takeDirectory obeliskPaths
129+ fmap (map (makeRelative ".")) $ runFind $
130+ ["-L", root, "(", "-name", "*.cabal", "-o", "-name", Hpack.packageConfig, ")", "-a", "-type", "f"]
131+ <> concat [["-not", "-path", p </> "*"] | p <- exclusions]
132+ where
133+ runFind args = do
134+ (_exitCode, out, err) <- readCreateProcessWithExitCode $ proc findExePath args
135+ putLog Debug $ T.strip $ T.pack err
136+ pure $ map T.unpack $ T.lines $ T.strip $ T.pack out
137+
138+data GuessPackageFileError = GuessPackageFileError_Ambiguous [FilePath] | GuessPackageFileError_NotFound
139+ deriving (Eq, Ord, Show)
140+instance Exception GuessPackageFileError
104141
105−-- | Relative paths to local packages of an obelisk project
106−-- TODO a way to query this
107−getLocalPkgs :: Applicative f => f [FilePath]
108−getLocalPkgs = pure ["backend", "common", "frontend"]
142+newtype HPackFilePath = HPackFilePath { unHPackFilePath :: FilePath } deriving (Eq, Ord, Show)
143+newtype CabalFilePath = CabalFilePath { unCabalFilePath :: FilePath } deriving (Eq, Ord, Show)
109144
145+-- | Given a directory, try to guess what the appropriate @.cabal@ or @package.yaml@ file is for the package.
146+guessCabalPackageFile
147+ :: (MonadIO m)
148+ => FilePath -- ^ Directory or path to search for cabal package
149+ -> m (Either GuessPackageFileError (Either CabalFilePath HPackFilePath))
150+guessCabalPackageFile pkg = do
151+ liftIO (doesDirectoryExist pkg) >>= \case
152+ False -> case cabalOrHpackFile pkg of
153+ (Just hpack@(Right _)) -> pure $ Right hpack
154+ (Just cabal@(Left (CabalFilePath cabalFilePath))) -> do
155+ -- If the cabal file has a sibling hpack file, we use that instead
156+ -- since running hpack often generates a sibling cabal file
157+ let possibleHpackSibling = takeDirectory cabalFilePath </> Hpack.packageConfig
158+ hasHpackSibling <- liftIO $ doesFileExist possibleHpackSibling
159+ pure $ Right $ if hasHpackSibling then Right (HPackFilePath possibleHpackSibling) else cabal
160+ Nothing -> pure $ Left GuessPackageFileError_NotFound
161+ True -> do
162+ candidates <- liftIO $
163+ filterM (doesFileExist . either unCabalFilePath unHPackFilePath)
164+ =<< mapMaybe (cabalOrHpackFile . (pkg </>)) <$> listDirectory pkg
165+ pure $ case partitionEithers candidates of
166+ ([hpack], _) -> Right $ Left hpack
167+ ([], [cabal]) -> Right $ Right cabal
168+ ([], []) -> Left GuessPackageFileError_NotFound
169+ (hpacks, cabals) -> Left $ GuessPackageFileError_Ambiguous $ coerce hpacks <> coerce cabals
170+
171+cabalOrHpackFile :: FilePath -> Maybe (Either CabalFilePath HPackFilePath)
172+cabalOrHpackFile = \case
173+ x | takeExtension x == ".cabal" -> Just (Left $ CabalFilePath x)
174+ | takeFileName x == Hpack.packageConfig -> Just (Right $ HPackFilePath x)
175+ | otherwise -> Nothing
176+
177+-- | Parses the cabal package in a given directory.
178+-- This automatically figures out which .cabal file or package.yaml (hpack) file to use in the given directory.
110179parseCabalPackage
111− :: (MonadObelisk m)
112− => FilePath -- ^ package directory
180+ :: MonadObelisk m
181+ => FilePath -- ^ Package directory
113182 -> m (Maybe CabalPackageInfo)
114−parseCabalPackage dir = do
115− let cabalFp = dir </> (takeBaseName dir <> ".cabal")
116− hpackFp = dir </> "package.yaml"
117− hasCabal <- liftIO $ doesFileExist cabalFp
118− hasHpack <- liftIO $ doesFileExist hpackFp
119−
120− mCabalContents <- if hasCabal
121− then Just <$> liftIO (readUTF8File cabalFp)
122− else if hasHpack
123− then do
124− let decodeOptions = DecodeOptions (ProgramName "ob") hpackFp Nothing decodeYaml
125− liftIO (readPackageConfig decodeOptions) >>= \case
126− Left err -> do
127− putLog Error $ T.pack $ "Failed to parse " <> hpackFp <> ": " <> err
128− return Nothing
129− Right (DecodeResult hpackPackage _ _ _) -> do
130− return $ Just $ renderPackage [] hpackPackage
131− else return Nothing
132−
133− fmap join $ forM mCabalContents $ \cabalContents -> do
134− let (warnings, result) = runParseResult $ parseGenericPackageDescription $
135− toUTF8BS $ cabalContents
136− mapM_ (putLog Warning) $ fmap (T.pack . show) warnings
137− case result of
138− Right gpkg -> do
139− return $ do
140− (_, lib) <- simplifyCondTree (const $ pure True) <$> condLibrary gpkg
141− pure $ CabalPackageInfo
142− { _cabalPackageInfo_packageRoot = takeDirectory cabalFp
143− , _cabalPackageInfo_sourceDirs =
144− fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
145− , _cabalPackageInfo_defaultExtensions =
146− defaultExtensions $ libBuildInfo lib
147− , _cabalPackageInfo_defaultLanguage =
148− defaultLanguage $ libBuildInfo lib
149− , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
150− }
151− Left (_, errors) -> do
152− putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
153− mapM_ (putLog Error) $ fmap (T.pack . show) errors
154− return Nothing
155−
156−withUTF8FileContentsM :: (MonadIO m, HasCliConfig e m) => FilePath -> (ByteString -> CliT e IO a) -> m a
157−withUTF8FileContentsM fp f = do
158− c <- getCliConfig
159− liftIO $ withUTF8FileContents fp $ runCli c . f . toUTF8BS
183+parseCabalPackage dir = parseCabalPackage' dir >>= \case
184+ Left err -> Nothing <$ putLog Error err
185+ Right (warnings, pkgInfo) -> do
186+ for_ warnings $ putLog Warning . T.pack . show
187+ pure $ Just pkgInfo
188+
189+-- | Like 'parseCabalPackage' but returns errors and warnings directly so as to avoid 'MonadObelisk'.
190+parseCabalPackage'
191+ :: (MonadIO m)
192+ => FilePath -- ^ Package directory
193+ -> m (Either T.Text ([Dist.PWarning], CabalPackageInfo))
194+parseCabalPackage' pkg = runExceptT $ do
195+ (cabalContents, packageFile, packageName) <- guessCabalPackageFile pkg >>= \case
196+ Left GuessPackageFileError_NotFound -> throwError $ "No .cabal or package.yaml file found in " <> T.pack pkg
197+ Left (GuessPackageFileError_Ambiguous _) -> throwError $ "Unable to determine which .cabal file to use in " <> T.pack pkg
198+ Right (Left (CabalFilePath file)) -> (, file, takeBaseName file) <$> liftIO (readUTF8File file)
199+ Right (Right (HPackFilePath file)) -> do
200+ let
201+ decodeOptions = Hpack.DecodeOptions (Hpack.ProgramName "ob") file Nothing Hpack.decodeYaml
202+ liftIO (Hpack.readPackageConfig decodeOptions) >>= \case
203+ Left err -> throwError $ T.pack $ "Failed to parse " <> file <> ": " <> err
204+ Right (Hpack.DecodeResult hpackPackage _ _ _) -> pure (Hpack.renderPackage [] hpackPackage, file, Hpack.packageName hpackPackage)
205+
206+ let
207+ (warnings, result) = runParseResult $ parseGenericPackageDescription $ toUTF8BS cabalContents
208+ osConfVar = case System.Info.os of
209+ "linux" -> Just Dist.Linux
210+ "darwin" -> Just Dist.OSX
211+ _ -> trace "Unrecgonized System.Info.os" Nothing
212+ archConfVar = Just Dist.X86_64 -- TODO: Actually infer this
213+ evalConfVar v = Right $ case v of
214+ OS osVar -> Just osVar == osConfVar
215+ Arch archVar -> Just archVar == archConfVar
216+ Impl GHC _ -> True -- TODO: Actually check version range
217+ _ -> False
218+ case condLibrary <$> result of
219+ Right (Just condLib) -> do
220+ let (_, lib) = simplifyCondTree evalConfVar condLib
221+ pure $ (warnings,) $ CabalPackageInfo
222+ { _cabalPackageInfo_packageName = T.pack packageName
223+ , _cabalPackageInfo_packageFile = packageFile
224+ , _cabalPackageInfo_packageRoot = takeDirectory packageFile
225+ , _cabalPackageInfo_buildable = buildable $ libBuildInfo lib
226+ , _cabalPackageInfo_sourceDirs =
227+ fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
228+ , _cabalPackageInfo_defaultExtensions =
229+ defaultExtensions $ libBuildInfo lib
230+ , _cabalPackageInfo_defaultLanguage =
231+ defaultLanguage $ libBuildInfo lib
232+ , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
233+ }
234+ Right Nothing -> throwError "Haskell package has no library component"
235+ Left (_, errors) ->
236+ throwError $ T.pack $ "Failed to parse " <> packageFile <> ":\n" <> unlines (map show errors)
237+
238+parsePackagesOrFail :: MonadObelisk m => [FilePath] -> m (NE.NonEmpty CabalPackageInfo)
239+parsePackagesOrFail dirs = do
240+ (pkgDirErrs, packageInfos') <- fmap partitionEithers $ for dirs $ \dir -> do
241+ flip fmap (parseCabalPackage dir) $ \case
242+ Just packageInfo
243+ | _cabalPackageInfo_buildable packageInfo -> Right packageInfo
244+ _ -> Left dir
245+
246+ let packagesByName = Map.fromListWith (<>) [(_cabalPackageInfo_packageName p, p NE.:| []) | p <- packageInfos']
247+ unambiguous <- ifor packagesByName $ \packageName ps -> case ps of
248+ p NE.:| [] -> pure p -- No ambiguity here
249+ p NE.:| _ -> do
250+ putLog Warning $ T.pack $
251+ "Packages named '" <> T.unpack packageName <> "' appear in " <> show (length ps) <> " different locations: "
252+ <> intercalate ", " (map _cabalPackageInfo_packageFile $ toList ps)
253+ <> "; Picking " <> _cabalPackageInfo_packageFile p
254+ pure p
255+
256+ packageInfos <- case NE.nonEmpty $ toList unambiguous of
257+ Nothing -> failWith $ T.pack $ "No valid, buildable packages found in " <> intercalate ", " dirs
258+ Just xs -> pure xs
259+
260+ unless (null pkgDirErrs) $
261+ putLog Warning $ T.pack $ "Failed to find buildable packages in " <> intercalate ", " pkgDirErrs
262+
263+ pure packageInfos
264+
265+packageInfoToNamePathMap :: [CabalPackageInfo] -> Map Text FilePath
266+packageInfoToNamePathMap = Map.fromList . map (_cabalPackageInfo_packageName &&& _cabalPackageInfo_packageRoot)
160267
161268-- | Create ghci configuration to load the given packages
162269withGhciScript
163270 :: MonadObelisk m
164− => [FilePath] -- ^ List of packages to load into ghci
165− -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporory .ghci
271+ => [CabalPackageInfo] -- ^ List of packages to load into ghci
272+ -> FilePath -- ^ All paths written to the .ghci file will be relative to this path
273+ -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporary .ghci
166274 -> m ()
167−withGhciScript pkgs f = do
168− (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM pkgs $ \pkg -> do
169− flip fmap (parseCabalPackage pkg) $ \case
170− Nothing -> Left pkg
171− Just packageInfo -> Right packageInfo
172−
173− when (null packageInfos) $
174− failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " pkgs
175− unless (null pkgDirErrs) $
176− putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
177−
178− let extensions = packageInfos >>= _cabalPackageInfo_defaultExtensions
179− languageFromPkgs = L.nub $ mapMaybe _cabalPackageInfo_defaultLanguage packageInfos
180− -- NOTE when no default-language is present cabal sets Haskell98
181− language = NE.toList $ fromMaybe (Haskell98 NE.:| []) $ NE.nonEmpty languageFromPkgs
182− extensionsLine = if extensions == mempty
183− then ""
184− else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
185− ghcOptions = concat $ mapMaybe (\case (GHC, xs) -> Just xs; _ -> Nothing) $
186− packageInfos >>= _cabalPackageInfo_compilerOptions
187− dotGhci = unlines $
188− [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
189− , case ghcOptions of
190− [] -> ""
191− xs -> ":set " <> intercalate " " xs
192− , extensionsLine
193− , ":set " <> intercalate " " (("-X" <>) . prettyShow <$> language)
194− , ":load Backend Frontend"
195− , "import Obelisk.Run"
196− , "import qualified Frontend"
197− , "import qualified Backend"
198− ]
199− warnDifferentLanguages language
275+withGhciScript packageInfos pathBase f = do
276+ selfExe <- liftIO getExecutablePath
277+ let
278+ packageNames = Set.fromList $ map _cabalPackageInfo_packageName packageInfos
279+ modulesToLoad = mconcat
280+ [ [ "Obelisk.Run" | "obelisk-run" `Set.member` packageNames ]
281+ , [ "Backend" | "backend" `Set.member` packageNames ]
282+ , [ "Frontend" | "frontend" `Set.member` packageNames ]
283+ ]
284+ dotGhci = unlines
285+ -- TODO: Shell escape
286+ [ ":set -F -pgmF " <> selfExe <> " -optF " <> preprocessorIdentifier <> " " <> unwords (map (("-optF " <>) . makeRelative pathBase . _cabalPackageInfo_packageFile) packageInfos)
287+ , ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
288+ , if null modulesToLoad then "" else ":load " <> unwords modulesToLoad
289+ , "import qualified Obelisk.Run"
290+ , "import qualified Frontend"
291+ , "import qualified Backend"
292+ ]
200293 withSystemTempDirectory "ob-ghci" $ \fp -> do
201294 let dotGhciPath = fp </> ".ghci"
202295 liftIO $ writeFile dotGhciPath dotGhci
@@ -204,35 +297,52 @@ withGhciScript pkgs f = do
204297
205298 where
206299 rootedSourceDirs pkg = NE.toList $
207− (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
208−
209−warnDifferentLanguages :: MonadObelisk m => [Language] -> m ()
210−warnDifferentLanguages (_:_:_) = putLog Warning "Different languages detected across packages which may result in errors when loading the repl"
211−warnDifferentLanguages _ = return ()
300+ makeRelative pathBase . (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
212301
213302-- | Run ghci repl
214303runGhciRepl
215304 :: MonadObelisk m
216− => FilePath -- ^ Path to .ghci
305+ => FilePath -- ^ Path to project root
306+ -> [CabalPackageInfo]
307+ -> FilePath -- ^ Path to .ghci
217308 -> m ()
218−runGhciRepl dotGhci = inProjectShell "ghc" $ unwords $ "ghci" : ["-no-user-package-db", "-ghci-script", dotGhci]
309+runGhciRepl root packages dotGhci =
310+ -- NOTE: We do *not* want to use $(staticWhich "ghci") here because we need the
311+ -- ghc that is provided by the shell in the user's project.
312+ nixShellWithPkgs root True False (packageInfoToNamePathMap packages) $ Just $ "ghci " <> makeBaseGhciOptions dotGhci -- TODO: Shell escape
219313
220314-- | Run ghcid
221315runGhcid
222316 :: MonadObelisk m
223− => FilePath -- ^ Path to .ghci
317+ => FilePath -- ^ Path to project root
318+ -> Bool -- ^ Should we chdir to root when running this process?
319+ -> FilePath -- ^ Path to .ghci
320+ -> [CabalPackageInfo]
224321 -> Maybe String -- ^ Optional command to run at every reload
225322 -> m ()
226−runGhcid dotGhci mcmd = callCommand $ unwords $ "ghcid" : opts
323+runGhcid root chdirToRoot dotGhci packages mcmd =
324+ nixShellWithPkgs root True chdirToRoot (packageInfoToNamePathMap packages) (Just $ unwords $ ghcidExePath : opts) -- TODO: Shell escape
227325 where
228326 opts =
229327 [ "-W"
230− --TODO: The decision of whether to use -fwarn-redundant-constraints should probably be made by the user
231− , "--command='ghci -Wall -ignore-dot-ghci -fwarn-redundant-constraints -no-user-package-db -ghci-script " <> dotGhci <> "' "
232− , "--reload=config"
328+ , "--command='ghci -ignore-dot-ghci " <> makeBaseGhciOptions dotGhci <> "' "
233329 , "--outputfile=ghcid-output.txt"
234− ] <> testCmd
235− testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'")
330+ ] <> map (\x -> "--reload='" <> x <> "'") reloadFiles
331+ <> map (\x -> "--restart='" <> x <> "'") restartFiles
332+ <> testCmd
333+ testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'") -- TODO: Shell escape
334+
335+ adjustRoot x = if chdirToRoot then makeRelative root x else x
336+ reloadFiles = map adjustRoot [root </> "config"]
337+ restartFiles = map (adjustRoot . _cabalPackageInfo_packageFile) packages
338+
339+makeBaseGhciOptions :: FilePath -> String
340+makeBaseGhciOptions dotGhci =
341+ unwords
342+ [ "-no-user-package-db"
343+ , "-package-env -"
344+ , "-ghci-script " <> dotGhci
345+ ]
236346
237347getFreePort :: MonadIO m => m PortNumber
238348getFreePort = liftIO $ withSocketsDo $ do
lib/command/src/Obelisk/Command/Thunk.hs+258−121
@@ -8,25 +8,31 @@
88{-# LANGUAGE ScopedTypeVariables #-}
99{-# LANGUAGE TypeApplications #-}
1010module Obelisk.Command.Thunk
11− ( ThunkPtr (..)
11+ ( GitHubSource (..)
12+ , GitUri (..)
13+ , ReadThunkError (..)
14+ , ThunkConfig (..)
15+ , ThunkData (..)
16+ , ThunkPackConfig (..)
17+ , ThunkPtr (..)
1218 , ThunkRev (..)
1319 , ThunkSource (..)
14− , ThunkData (..)
15− , ReadThunkError (..)
16− , GitHubSource (..)
17− , getThunkGitBranch
18− , getLatestRev
19− , updateThunkToLatest
20+ , ThunkUpdateConfig (..)
21+ , attrCacheFileName
2022 , createThunk
2123 , createThunkWithLatest
24+ , getLatestRev
25+ , getThunkGitBranch
26+ , getThunkPtr
27+ , gitUriToText
2228 , nixBuildAttrWithCache
2329 , nixBuildThunkAttrWithCache
24− , unpackThunk
2530 , packThunk
31+ , parseGitUri
2632 , readThunk
33+ , unpackThunk
2734 , updateThunk
28− , getThunkPtr
29− , parseGitUri
35+ , updateThunkToLatest
3036 , uriThunkPtr
3137 ) where
3238
@@ -42,6 +48,7 @@ import qualified Data.Aeson as Aeson
4248import Data.Aeson.Encode.Pretty
4349import qualified Data.Aeson.Types as Aeson
4450import qualified Data.ByteString.Lazy as LBS
51+import Data.Containers.ListUtils (nubOrd)
4552import Data.Default
4653import Data.Either.Combinators (fromRight', rightToMaybe)
4754import Data.Git.Ref (Ref)
@@ -64,12 +71,12 @@ import GitHub
6471import GitHub.Data.Name
6572import Obelisk.Command.Nix
6673import System.Directory
74+import System.Exit
6775import System.FilePath
6876import System.IO.Error
6977import System.IO.Temp
7078import System.Posix (getSymbolicLinkStatus, modificationTime)
71−import System.Process (proc)
72−import Text.URI
79+import qualified Text.URI as URI
7380
7481import Obelisk.App (MonadObelisk)
7582import Obelisk.CliApp
@@ -111,37 +118,63 @@ data GitHubSource = GitHubSource
111118 { _gitHubSource_owner :: Name Owner
112119 , _gitHubSource_repo :: Name Repo
113120 , _gitHubSource_branch :: Maybe (Name Branch)
121+ , _gitHubSource_private :: Bool
114122 }
115123 deriving (Show, Eq, Ord)
116124
125+newtype GitUri = GitUri { unGitUri :: URI.URI } deriving (Eq, Ord, Show)
126+
127+gitUriToText :: GitUri -> Text
128+gitUriToText (GitUri uri)
129+ | (T.toLower . URI.unRText <$> URI.uriScheme uri) == Just "file"
130+ , Just (_, path) <- URI.uriPath uri
131+ = "/" <> T.intercalate "/" (map URI.unRText $ NonEmpty.toList path)
132+ | otherwise = URI.render uri
133+
117134data GitSource = GitSource
118− { _gitSource_url :: URI
135+ { _gitSource_url :: GitUri
119136 , _gitSource_branch :: Maybe (Name Branch)
120137 , _gitSource_fetchSubmodules :: Bool
138+ , _gitSource_private :: Bool
121139 }
122140 deriving (Show, Eq, Ord)
123141
142+newtype ThunkConfig = ThunkConfig
143+ { _thunkConfig_private :: Maybe Bool
144+ } deriving Show
145+
146+data ThunkUpdateConfig = ThunkUpdateConfig
147+ { _thunkUpdateConfig_branch :: Maybe String
148+ , _thunkUpdateConfig_config :: ThunkConfig
149+ } deriving Show
150+
151+data ThunkPackConfig = ThunkPackConfig
152+ { _thunkPackConfig_force :: Bool
153+ , _thunkPackConfig_config :: ThunkConfig
154+ } deriving Show
155+
124156-- | Convert a GitHub source to a regular Git source. Assumes no submodules.
125157forgetGithub :: Bool -> GitHubSource -> GitSource
126158forgetGithub useSsh s = GitSource
127− { _gitSource_url = URI
128− { uriScheme = Just $ fromRight' $ mkScheme $ if useSsh then "ssh" else "https"
129− , uriAuthority = Right $ Authority
130− { authUserInfo = UserInfo (fromRight' $ mkUsername "git") Nothing
159+ { _gitSource_url = GitUri $ URI.URI
160+ { URI.uriScheme = Just $ fromRight' $ URI.mkScheme $ if useSsh then "ssh" else "https"
161+ , URI.uriAuthority = Right $ URI.Authority
162+ { URI.authUserInfo = URI.UserInfo (fromRight' $ URI.mkUsername "git") Nothing
131163 <$ guard useSsh
132− , authHost = fromRight' $ mkHost "github.com"
133− , authPort = Nothing
164+ , URI.authHost = fromRight' $ URI.mkHost "github.com"
165+ , URI.authPort = Nothing
134166 }
135− , uriPath = Just ( False
136− , fromRight' . mkPathPiece <$>
167+ , URI.uriPath = Just ( False
168+ , fromRight' . URI.mkPathPiece <$>
137169 untagName (_gitHubSource_owner s)
138170 :| [ untagName (_gitHubSource_repo s) <> ".git" ]
139171 )
140− , uriQuery = []
141− , uriFragment = Nothing
172+ , URI.uriQuery = []
173+ , URI.uriFragment = Nothing
142174 }
143175 , _gitSource_branch = _gitHubSource_branch s
144176 , _gitSource_fetchSubmodules = False
177+ , _gitSource_private = _gitHubSource_private s
145178 }
146179
147180getThunkGitBranch :: ThunkPtr -> Maybe Text
@@ -155,20 +188,20 @@ commitNameToRef (N c) = Ref.fromHex $ encodeUtf8 c
155188-- TODO: Use spinner here.
156189getNixSha256ForUriUnpacked
157190 :: MonadObelisk m
158− => URI
191+ => GitUri
159192 -> m NixSha256
160193getNixSha256ForUriUnpacked uri =
161− withExitFailMessage ("nix-prefetch-url: Failed to determine sha256 hash of URL " <> render uri) $ do
194+ withExitFailMessage ("nix-prefetch-url: Failed to determine sha256 hash of URL " <> gitUriToText uri) $ do
162195 [hash] <- fmap T.lines $ readProcessAndLogOutput (Debug, Debug) $
163− proc "nix-prefetch-url" ["--unpack" , "--type" , "sha256" , T.unpack $ render uri]
196+ proc "nix-prefetch-url" ["--unpack", "--type", "sha256", T.unpack $ gitUriToText uri]
164197 pure hash
165198
166−nixPrefetchGit :: MonadObelisk m => URI -> Text -> Bool -> m NixSha256
199+nixPrefetchGit :: MonadObelisk m => GitUri -> Text -> Bool -> m NixSha256
167200nixPrefetchGit uri rev fetchSubmodules =
168− withExitFailMessage ("nix-prefetch-git: Failed to determine sha256 hash of Git repo " <> render uri <> " at " <> rev) $ do
201+ withExitFailMessage ("nix-prefetch-git: Failed to determine sha256 hash of Git repo " <> gitUriToText uri <> " at " <> rev) $ do
169202 out <- readProcessAndLogStderr Debug $
170203 proc "nix-prefetch-git" $ filter (/="")
171− [ "--url", T.unpack $ render uri
204+ [ "--url", T.unpack $ gitUriToText uri
172205 , "--rev", T.unpack rev
173206 , if fetchSubmodules then "--fetch-submodules" else ""
174207 , "--quiet"
@@ -199,6 +232,9 @@ readThunk thunkDir = do
199232 False -> return $ return Nothing
200233 False -> liftIO $ fmap ThunkData_Packed <$> readPackedThunk thunkDir
201234
235+attrCacheFileName :: FilePath
236+attrCacheFileName = ".attr-cache"
237+
202238data ThunkType = ThunkType
203239 { _thunkType_loader :: FilePath
204240 , _thunkType_json :: FilePath
@@ -211,7 +247,7 @@ gitHubThunkType :: ThunkType
211247gitHubThunkType = ThunkType
212248 { _thunkType_loader = "default.nix"
213249 , _thunkType_json = "github.json"
214− , _thunkType_optional = Set.fromList [".attr-cache"]
250+ , _thunkType_optional = Set.fromList [attrCacheFileName]
215251 , _thunkType_loaderVersions = gitHubStandaloneLoaders
216252 , _thunkType_parser = parseThunkPtr $ \v ->
217253 ThunkSource_GitHub <$> parseGitHubSource v <|> ThunkSource_Git <$> parseGitSource v
@@ -221,7 +257,7 @@ gitThunkType :: ThunkType
221257gitThunkType = ThunkType
222258 { _thunkType_loader = "default.nix"
223259 , _thunkType_json = "git.json"
224− , _thunkType_optional = Set.fromList [".attr-cache"]
260+ , _thunkType_optional = Set.fromList [attrCacheFileName]
225261 , _thunkType_loaderVersions = plainGitStandaloneLoaders
226262 , _thunkType_parser = parseThunkPtr $ fmap ThunkSource_Git . parseGitSource
227263 }
@@ -286,10 +322,12 @@ parseGitHubSource v = do
286322 owner <- v Aeson..: "owner"
287323 repo <- v Aeson..: "repo"
288324 branch <- v Aeson..:! "branch"
325+ private <- v Aeson..:? "private"
289326 pure $ GitHubSource
290327 { _gitHubSource_owner = owner
291328 , _gitHubSource_repo = repo
292329 , _gitHubSource_branch = branch
330+ , _gitHubSource_private = fromMaybe False private
293331 }
294332
295333parseGitSource :: Aeson.Object -> Aeson.Parser GitSource
@@ -297,10 +335,12 @@ parseGitSource v = do
297335 Just url <- parseGitUri <$> v Aeson..: "url"
298336 branch <- v Aeson..:! "branch"
299337 fetchSubmodules <- v Aeson..:! "fetchSubmodules"
338+ private <- v Aeson..:? "private"
300339 pure $ GitSource
301340 { _gitSource_url = url
302341 , _gitSource_branch = branch
303342 , _gitSource_fetchSubmodules = fromMaybe False fetchSubmodules
343+ , _gitSource_private = fromMaybe False private
304344 }
305345
306346overwriteThunk :: MonadObelisk m => FilePath -> ThunkPtr -> m ()
@@ -327,13 +367,15 @@ encodeThunkPtrData (ThunkPtr rev src) = case src of
327367 , ("branch" .=) <$> _gitHubSource_branch s
328368 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
329369 , Just $ "sha256" .= _thunkRev_nixSha256 rev
370+ , Just $ "private" .= _gitHubSource_private s
330371 ]
331372 ThunkSource_Git s -> encodePretty' plainGitCfg $ Aeson.object $ catMaybes
332− [ Just $ "url" .= render (_gitSource_url s)
373+ [ Just $ "url" .= gitUriToText (_gitSource_url s)
333374 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
334375 , ("branch" .=) <$> _gitSource_branch s
335376 , Just $ "sha256" .= _thunkRev_nixSha256 rev
336377 , Just $ "fetchSubmodules" .= _gitSource_fetchSubmodules s
378+ , Just $ "private" .= _gitSource_private s
337379 ]
338380 where
339381 githubCfg = defConfig
@@ -354,6 +396,7 @@ encodeThunkPtrData (ThunkPtr rev src) = case src of
354396 [ "url"
355397 , "rev"
356398 , "sha256"
399+ , "private"
357400 , "fetchSubmodules"
358401 ] <> compare
359402 , confTrailingNewline = True
@@ -361,7 +404,7 @@ encodeThunkPtrData (ThunkPtr rev src) = case src of
361404
362405createThunk :: MonadIO m => FilePath -> ThunkPtr -> m ()
363406createThunk target thunk = liftIO $ do
364− createDirectoryIfMissing True (target </> ".attr-cache")
407+ createDirectoryIfMissing True (target </> attrCacheFileName)
365408 T.writeFile (target </> "default.nix") (thunkPtrLoader thunk)
366409 let
367410 jsonFileName = case _thunkPtr_source thunk of
@@ -377,8 +420,8 @@ createThunkWithLatest target s = do
377420 , _thunkPtr_rev = rev
378421 }
379422
380−updateThunkToLatest :: MonadObelisk m => FilePath -> Maybe String -> m ()
381−updateThunkToLatest target mBranch = withSpinner' ("Updating thunk " <> T.pack target <> " to latest") (pure $ const $ "Thunk " <> T.pack target <> " updated to latest") $ do
423+updateThunkToLatest :: MonadObelisk m => ThunkUpdateConfig -> FilePath -> m ()
424+updateThunkToLatest (ThunkUpdateConfig mBranch thunkConfig) target = withSpinner' ("Updating thunk " <> T.pack target <> " to latest") (pure $ const $ "Thunk " <> T.pack target <> " updated to latest") $ do
382425 checkThunkDirectory "ob thunk update directory cannot be '.'" target
383426 -- check to see if thunk should be updated to a specific branch or just update it's current branch
384427 case mBranch of
@@ -390,7 +433,7 @@ updateThunkToLatest target mBranch = withSpinner' ("Updating thunk " <> T.pack t
390433 ThunkData_Checkout _ -> failWith "cannot update an unpacked thunk"
391434 let src = _thunkPtr_source ptr
392435 rev <- getLatestRev src
393− overwriteThunk overwrite $ ThunkPtr
436+ overwriteThunk overwrite $ modifyThunkPtrByConfig thunkConfig $ ThunkPtr
394437 { _thunkPtr_source = src
395438 , _thunkPtr_rev = rev
396439 }
@@ -398,24 +441,25 @@ updateThunkToLatest target mBranch = withSpinner' ("Updating thunk " <> T.pack t
398441 Left err -> failWith $ T.pack $ "thunk update: " <> show err
399442 Right c -> case c of
400443 ThunkData_Packed t -> case _thunkPtr_source t of
401− ThunkSource_Git tsg -> setThunk target tsg branch
444+ ThunkSource_Git tsg -> setThunk thunkConfig target tsg branch
402445 ThunkSource_GitHub tsgh -> do
403446 let tsg = forgetGithub False tsgh
404− setThunk target tsg branch
405− ThunkData_Checkout _ -> failWith $ T.pack $ "thunk located at " <> (show target) <> " is unpacked. Use ob thunk pack on the desired directory and then try ob thunk update again."
447+ setThunk thunkConfig target tsg branch
448+ ThunkData_Checkout _ -> failWith $ T.pack $ "thunk located at " <> show target <> " is unpacked. Use ob thunk pack on the desired directory and then try ob thunk update again."
406449
407−setThunk :: MonadObelisk m => FilePath -> GitSource -> String -> m ()
408−setThunk target gs branch = do
409− newThunkPtr <- uriThunkPtr (_gitSource_url gs) (Just $ T.pack branch) Nothing
450+setThunk :: MonadObelisk m => ThunkConfig -> FilePath -> GitSource -> String -> m ()
451+setThunk thunkConfig target gs branch = do
452+ newThunkPtr <- uriThunkPtr (_gitSource_url gs) (_thunkConfig_private thunkConfig) (Just $ T.pack branch) Nothing
410453 overwriteThunk target newThunkPtr
411− updateThunkToLatest target Nothing
454+ updateThunkToLatest (ThunkUpdateConfig Nothing thunkConfig) target
412455
413456-- | All recognized github standalone loaders, ordered from newest to oldest.
414457-- This tool will only ever produce the newest one when it writes a thunk.
415458gitHubStandaloneLoaders :: NonEmpty Text
416459gitHubStandaloneLoaders =
417− gitHubStandaloneLoaderV2 :|
460+ gitHubStandaloneLoaderV4 :|
418461 [ gitHubStandaloneLoaderV3
462+ , gitHubStandaloneLoaderV2
419463 , gitHubStandaloneLoaderV1
420464 ]
421465
@@ -455,10 +499,24 @@ gitHubStandaloneLoaderV3 = T.unlines
455499 , "in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))"
456500 ]
457501
502+gitHubStandaloneLoaderV4 :: Text
503+gitHubStandaloneLoaderV4 = T.unlines
504+ [ "# DO NOT HAND-EDIT THIS FILE"
505+ , "let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:"
506+ , " if !fetchSubmodules && !private then builtins.fetchTarball {"
507+ , " url = \"https://github.com/${owner}/${repo}/archive/${rev}.tar.gz\"; inherit sha256;"
508+ , " } else (import <nixpkgs> {}).fetchFromGitHub {"
509+ , " inherit owner repo rev sha256 fetchSubmodules private;"
510+ , " };"
511+ , "in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))"
512+ ]
513+
458514plainGitStandaloneLoaders :: NonEmpty Text
459515plainGitStandaloneLoaders =
460− plainGitStandaloneLoaderV1 :|
461− [ plainGitStandaloneLoaderV2
516+ plainGitStandaloneLoaderV4 :|
517+ [ plainGitStandaloneLoaderV1
518+ , plainGitStandaloneLoaderV2
519+ , plainGitStandaloneLoaderV3
462520 ]
463521
464522plainGitStandaloneLoaderV1 :: Text
@@ -479,6 +537,41 @@ plainGitStandaloneLoaderV2 = T.unlines
479537 , "in import (fetchGit (builtins.fromJSON (builtins.readFile ./git.json)))"
480538 ]
481539
540+-- This loader has a bug because @builtins.fetchGit@ is not given a @ref@
541+-- and will fail to find commits without this because it does shallow clones.
542+plainGitStandaloneLoaderV3 :: Text
543+plainGitStandaloneLoaderV3 = T.unlines
544+ [ "# DO NOT HAND-EDIT THIS FILE"
545+ , "let fetch = {url, rev, ref ? null, sha256 ? null, fetchSubmodules ? false, private ? false, ...}:"
546+ , " let realUrl = let firstChar = builtins.substring 0 1 url; in"
547+ , " if firstChar == \"/\" then /. + url"
548+ , " else if firstChar == \".\" then ./. + url"
549+ , " else url;"
550+ , " in if !fetchSubmodules && private then builtins.fetchGit {"
551+ , " url = realUrl; inherit rev;"
552+ , " } else (import <nixpkgs> {}).fetchgit {"
553+ , " url = realUrl; inherit rev sha256;"
554+ , " };"
555+ , "in import (fetch (builtins.fromJSON (builtins.readFile ./git.json)))"
556+ ]
557+
558+plainGitStandaloneLoaderV4 :: Text
559+plainGitStandaloneLoaderV4 = T.unlines
560+ [ "# DO NOT HAND-EDIT THIS FILE"
561+ , "let fetch = {url, rev, branch ? null, sha256 ? null, fetchSubmodules ? false, private ? false, ...}:"
562+ , " let realUrl = let firstChar = builtins.substring 0 1 url; in"
563+ , " if firstChar == \"/\" then /. + url"
564+ , " else if firstChar == \".\" then ./. + url"
565+ , " else url;"
566+ , " in if !fetchSubmodules && private then builtins.fetchGit {"
567+ , " url = realUrl; inherit rev;"
568+ , " ${if branch == null then null else \"ref\"} = branch;"
569+ , " } else (import <nixpkgs> {}).fetchgit {"
570+ , " url = realUrl; inherit rev sha256;"
571+ , " };"
572+ , "in import (fetch (builtins.fromJSON (builtins.readFile ./git.json)))"
573+ ]
574+
482575thunkFileNames :: [FilePath]
483576thunkFileNames = ["default.nix", "github.json", "git.json"]
484577
@@ -505,7 +598,7 @@ nixBuildThunkAttrWithCache thunkDir attr = do
505598 let cacheErrHandler e
506599 | isDoesNotExistError e = return Nothing -- expected from a cache miss
507600 | otherwise = putLog Error (T.pack $ displayException e) >> return Nothing
508− cacheDir = thunkDir </> ".attr-cache"
601+ cacheDir = thunkDir </> attrCacheFileName
509602 cachePath = cacheDir </> attr <.> "out"
510603 latestChange <- liftIO $ do
511604 createDirectoryIfMissing False cacheDir
@@ -520,7 +613,7 @@ nixBuildThunkAttrWithCache thunkDir attr = do
520613 Just c -> return c
521614 Nothing -> do
522615 putLog Warning $ T.pack $ mconcat [thunkDir, ": ", attr, " not cached, building ..."]
523− _ <- nixCmd $ NixCmd_Build$ def
616+ _ <- nixCmd $ NixCmd_Build $ def
524617 Lens.& nixBuildConfig_outLink Lens..~ OutLink_IndirectRoot cachePath
525618 Lens.& nixCmdConfig_target Lens..~ Target
526619 { _target_path = Just thunkDir
@@ -568,13 +661,13 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
568661 Right (ThunkData_Packed _) -> do
569662 let tmpThunk = tmpDir </> "thunk"
570663 callProcessAndLogOutput (Notice, Error) $
571− proc "cp" ["-r", "-T", thunkDir, tmpThunk]
664+ proc cp ["-r", "-T", thunkDir, tmpThunk]
572665 return tmpThunk
573− Right _ -> failWith $ "Thunk is not packed"
666+ Right _ -> failWith "Thunk is not packed"
574667 updateThunkFromTmp p' = do
575− _ <- packThunk' True False p'
668+ _ <- packThunk' True (ThunkPackConfig False (ThunkConfig Nothing)) p'
576669 callProcessAndLogOutput (Notice, Error) $
577− proc "cp" ["-r", "-T", p', p]
670+ proc cp ["-r", "-T", p', p]
578671
579672finalMsg :: Bool -> (a -> Text) -> Maybe (a -> Text)
580673finalMsg noTrail s = if noTrail then Nothing else Just s
@@ -606,7 +699,7 @@ unpackThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from with
606699 let git = callProcessAndLogOutput (Notice, Notice) . gitProc tmpRepo
607700 git $ [ "clone" ]
608701 ++ ("--recursive" <$ guard (_gitSource_fetchSubmodules s))
609− ++ [ T.unpack $ render $ _gitSource_url s ]
702+ ++ [ T.unpack $ gitUriToText $ _gitSource_url s ]
610703 ++ do branch <- maybeToList $ _gitSource_branch s
611704 [ "--branch", T.unpack $ untagName branch ]
612705 git ["reset", "--hard", Ref.toHexString $ _thunkRev_commit $ _thunkPtr_rev tptr]
@@ -615,31 +708,39 @@ unpackThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from with
615708
616709 liftIO $ createDirectory obGitDir
617710 callProcessAndLogOutput (Notice, Error) $
618− proc "cp" ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
711+ proc cp ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
619712 callProcessAndLogOutput (Notice, Error) $
620713 proc "rm" ["-r", thunkDir]
621714 callProcessAndLogOutput (Notice, Error) $
622715 proc "mv" ["-T", tmpRepo, thunkDir]
623716
624717--TODO: add a rollback mode to pack to the original thunk
625−packThunk :: MonadObelisk m => Bool -> FilePath -> m ThunkPtr
718+packThunk :: MonadObelisk m => ThunkPackConfig -> FilePath -> m ThunkPtr
626719packThunk = packThunk' False
627720
628−packThunk' :: MonadObelisk m => Bool -> Bool -> FilePath -> m ThunkPtr
629−packThunk' noTrail force thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
721+packThunk' :: MonadObelisk m => Bool -> ThunkPackConfig -> FilePath -> m ThunkPtr
722+packThunk' noTrail (ThunkPackConfig force thunkConfig) thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
630723 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
631724 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
632725 Right (ThunkData_Checkout _) -> do
633726 withSpinner' ("Packing thunk " <> T.pack thunkDir)
634727 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
635− thunkPtr <- getThunkPtr (not force) thunkDir
728+ thunkPtr <- modifyThunkPtrByConfig thunkConfig <$> getThunkPtr (not force) thunkDir (_thunkConfig_private thunkConfig)
636729 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
637730 liftIO $ createThunk thunkDir thunkPtr
638731 pure thunkPtr
639732
640−getThunkPtr :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
641−getThunkPtr checkClean thunkDir = do
642− when checkClean $ ensureCleanGitRepo thunkDir True $
733+modifyThunkPtrByConfig :: ThunkConfig -> ThunkPtr -> ThunkPtr
734+modifyThunkPtrByConfig (ThunkConfig markPrivate') ptr = case markPrivate' of
735+ Nothing -> ptr
736+ Just markPrivate -> ptr { _thunkPtr_source = case _thunkPtr_source ptr of
737+ ThunkSource_Git s -> ThunkSource_Git $ s { _gitSource_private = markPrivate }
738+ ThunkSource_GitHub s -> ThunkSource_GitHub $ s { _gitHubSource_private = markPrivate }
739+ }
740+
741+getThunkPtr :: forall m. MonadObelisk m => Bool -> FilePath -> Maybe Bool -> m ThunkPtr
742+getThunkPtr checkClean thunkDir mPrivate = do
743+ when checkClean $ ensureCleanGitRepo thunkDir True
643744 "thunk pack: thunk checkout contains unsaved modifications"
644745
645746 -- Check whether there are any stashes
@@ -652,7 +753,7 @@ getThunkPtr checkClean thunkDir = do
652753 ] ++ T.lines stashOutput
653754 True -> return ()
654755
655− -- Get current branch ``
756+ -- Get current branch
656757 (mCurrentBranch, mCurrentCommit) <- do
657758 b <- listToMaybe
658759 <$> T.lines
@@ -661,7 +762,7 @@ getThunkPtr checkClean thunkDir = do
661762 <$> T.lines
662763 <$> readGitProcess thunkDir ["rev-parse", "HEAD"]
663764 case b of
664− (Just "HEAD") -> failWith $ T.unlines $
765+ (Just "HEAD") -> failWith $ T.unlines
665766 [ "thunk pack: You are in 'detached HEAD' state."
666767 , "If you want to pack at the current ref \
667768 \then please create a new branch with 'git checkout -b <new-branch-name>' and push this upstream."
@@ -733,7 +834,7 @@ getThunkPtr checkClean thunkDir = do
733834 [ "thunk pack: Certain branches in the thunk have commits not yet pushed upstream:"
734835 , ""
735836 ] ++
736− (flip map (Map.toList nonGood) $ \(branch, (upstream, (ahead, behind))) -> mconcat
837+ flip map (Map.toList nonGood) (\(branch, (upstream, (ahead, behind))) -> mconcat
737838 [" ", branch, " ahead: ", T.pack (show ahead), " behind: ", T.pack (show behind), " remote branch ", upstream]) ++
738839 [ ""
739840 , "Please push these upstream and try again. (Or just fetch, if they are somehow \
@@ -741,7 +842,7 @@ getThunkPtr checkClean thunkDir = do
741842 ]
742843
743844 -- We assume it's safe to pack the thunk at this point
744− putLog Informational $ "All changes safe in git remotes. OK to pack thunk."
845+ putLog Informational "All changes safe in git remotes. OK to pack thunk."
745846
746847 let remote = maybe "origin" snd $ flip Map.lookup headUpstream =<< mCurrentBranch
747848
@@ -754,7 +855,7 @@ getThunkPtr checkClean thunkDir = do
754855 remoteUri <- case parseGitUri remoteUri' of
755856 Nothing -> failWith $ "Could not identify git remote: " <> remoteUri'
756857 Just uri -> pure uri
757− uriThunkPtr remoteUri mCurrentBranch mCurrentCommit
858+ uriThunkPtr remoteUri mPrivate mCurrentBranch mCurrentCommit
758859
759860-- | Get the latest revision available from the given source
760861getLatestRev :: MonadObelisk m => ThunkSource -> m ThunkRev
@@ -773,18 +874,18 @@ getLatestRev os = do
773874-- performance. If that doesn't work (e.g. authentication issue), we fall back
774875-- on just doing things the normal way for git repos in general, and save it as
775876-- a regular git thunk.
776−uriThunkPtr :: MonadObelisk m => URI -> Maybe Text -> Maybe Text -> m ThunkPtr
777−uriThunkPtr uri mbranch mcommit = do
877+uriThunkPtr :: MonadObelisk m => GitUri -> Maybe Bool -> Maybe Text -> Maybe Text -> m ThunkPtr
878+uriThunkPtr uri mPrivate mbranch mcommit = do
778879 commit <- case mcommit of
779880 Nothing -> gitGetCommitBranch uri mbranch >>= return . snd
780881 (Just c) -> return c
781− (src, rev) <- case uriToThunkSource uri mbranch of
882+ (src, rev) <- uriToThunkSource uri mPrivate mbranch >>= \case
782883 ThunkSource_GitHub s -> do
783884 rev <- runExceptT $ githubThunkRev s commit
784885 case rev of
785886 Right r -> pure (ThunkSource_GitHub s, r)
786887 Left e -> do
787− putLog Warning $ "\
888+ putLog Warning "\
788889\Failed to fetch archive from GitHub. This is probably a private repo. \
789890\Falling back on normal fetchgit. Original failure:"
790891 errorToWarning e
@@ -801,34 +902,66 @@ uriThunkPtr uri mbranch mcommit = do
801902-- If the thunk is a GitHub thunk and fails, we do *not* fall back like with
802903-- `uriThunkPtr`. Unlike a plain URL, a thunk src explicitly states which method
803904-- should be employed, and so we respect that.
804−uriToThunkSource :: URI -> Maybe Text -> ThunkSource
805−uriToThunkSource u
806− | Right uriAuth <- uriAuthority u
807− , Just scheme <- unRText <$> uriScheme u
905+uriToThunkSource :: MonadObelisk m => GitUri -> Maybe Bool -> Maybe Text -> m ThunkSource
906+uriToThunkSource (GitUri u) mPrivate
907+ | Right uriAuth <- URI.uriAuthority u
908+ , Just scheme <- URI.unRText <$> URI.uriScheme u
808909 , case scheme of
809− "ssh" -> uriAuth == Authority
810− { authUserInfo = Just $ UserInfo (fromRight' $ mkUsername "git") Nothing
811− , authHost = fromRight' $ mkHost "github.com"
812− , authPort = Nothing
910+ "ssh" -> uriAuth == URI.Authority
911+ { URI.authUserInfo = Just $ URI.UserInfo (fromRight' $ URI.mkUsername "git") Nothing
912+ , URI.authHost = fromRight' $ URI.mkHost "github.com"
913+ , URI.authPort = Nothing
813914 }
814915 s -> s `L.elem` [ "git", "https", "http" ] -- "http:" just redirects to "https:"
815− && unRText (authHost uriAuth) == "github.com"
816− , Just (_, owner :| [repoish]) <- uriPath u
817− = \mbranch -> ThunkSource_GitHub $ GitHubSource
818− { _gitHubSource_owner = N $ unRText owner
819− , _gitHubSource_repo = N $ let
820− repoish' = unRText repoish
821− in case T.stripSuffix ".git" repoish' of
822− Just repo -> repo
823− Nothing -> repoish'
824− , _gitHubSource_branch = N <$> mbranch
825− }
916+ && URI.unRText (URI.authHost uriAuth) == "github.com"
917+ , Just (_, owner :| [repoish]) <- URI.uriPath u
918+ = \mbranch -> do
919+ isPrivate <- getIsPrivate
920+ pure $ ThunkSource_GitHub $ GitHubSource
921+ { _gitHubSource_owner = N $ URI.unRText owner
922+ , _gitHubSource_repo = N $ let
923+ repoish' = URI.unRText repoish
924+ in fromMaybe repoish' $ T.stripSuffix ".git" repoish'
925+ , _gitHubSource_branch = N <$> mbranch
926+ , _gitHubSource_private = isPrivate
927+ }
826928
827− | otherwise = \mbranch -> ThunkSource_Git $ GitSource
828− { _gitSource_url = u
829− , _gitSource_branch = N <$> mbranch
830− , _gitSource_fetchSubmodules = False -- TODO: How do we determine if this should be true?
831− }
929+ | otherwise = \mbranch -> do
930+ isPrivate <- getIsPrivate
931+ pure $ ThunkSource_Git $ GitSource
932+ { _gitSource_url = GitUri u
933+ , _gitSource_branch = N <$> mbranch
934+ , _gitSource_fetchSubmodules = False -- TODO: How do we determine if this should be true?
935+ , _gitSource_private = isPrivate
936+ }
937+ where
938+ getIsPrivate = maybe (guessGitRepoIsPrivate $ GitUri u) pure mPrivate
939+
940+guessGitRepoIsPrivate :: MonadObelisk m => GitUri -> m Bool
941+guessGitRepoIsPrivate uri = flip fix urisToTry $ \loop -> \case
942+ [] -> pure True
943+ uriAttempt:xs -> do
944+ result <- readCreateProcessWithExitCode $
945+ isolateGitProc $
946+ gitProcNoRepo
947+ [ "ls-remote"
948+ , "--quiet"
949+ , "--exit-code"
950+ , "--symref"
951+ , T.unpack $ gitUriToText uriAttempt
952+ ]
953+ case result of
954+ (ExitSuccess, _, _) -> pure False -- Must be a public repo
955+ _ -> loop xs
956+ where
957+ urisToTry = nubOrd $
958+ -- Include the original URI if it isn't using SSH because SSH will certainly fail.
959+ [uri | fmap URI.unRText (URI.uriScheme (unGitUri uri)) /= Just "ssh"] <>
960+ [changeScheme "https" uri, changeScheme "http" uri]
961+ changeScheme scheme (GitUri u) = GitUri $ u
962+ { URI.uriScheme = URI.mkScheme scheme
963+ , URI.uriAuthority = (\x -> x { URI.authUserInfo = Nothing }) <$> URI.uriAuthority u
964+ }
832965
833966-- Funny signature indicates no effects depend on the optional branch name.
834967githubThunkRev
@@ -840,19 +973,19 @@ githubThunkRev
840973githubThunkRev s commit = do
841974 owner <- forcePP $ _gitHubSource_owner s
842975 repo <- forcePP $ _gitHubSource_repo s
843− revTarball <- mkPathPiece $ commit <> ".tar.gz"
844− let archiveUri = URI
845− { uriScheme = Just $ fromRight' $ mkScheme "https"
846− , uriAuthority = Right $ Authority
847− { authUserInfo = Nothing
848− , authHost = fromRight' $ mkHost "github.com"
849− , authPort = Nothing
976+ revTarball <- URI.mkPathPiece $ commit <> ".tar.gz"
977+ let archiveUri = GitUri $ URI.URI
978+ { URI.uriScheme = Just $ fromRight' $ URI.mkScheme "https"
979+ , URI.uriAuthority = Right $ URI.Authority
980+ { URI.authUserInfo = Nothing
981+ , URI.authHost = fromRight' $ URI.mkHost "github.com"
982+ , URI.authPort = Nothing
850983 }
851− , uriPath = Just ( False
852− , owner :| [ repo, fromRight' $ mkPathPiece "archive", revTarball ]
984+ , URI.uriPath = Just ( False
985+ , owner :| [ repo, fromRight' $ URI.mkPathPiece "archive", revTarball ]
853986 )
854− , uriQuery = []
855− , uriFragment = Nothing
987+ , URI.uriQuery = []
988+ , URI.uriFragment = Nothing
856989 }
857990 hash <- getNixSha256ForUriUnpacked archiveUri
858991 putLog Debug $ "Nix sha256 is " <> hash
@@ -861,8 +994,8 @@ githubThunkRev s commit = do
861994 , _thunkRev_nixSha256 = hash
862995 }
863996 where
864− forcePP :: Name entity -> m (RText 'PathPiece)
865− forcePP = mkPathPiece . untagName
997+ forcePP :: Name entity -> m (URI.RText 'URI.PathPiece)
998+ forcePP = URI.mkPathPiece . untagName
866999
8671000gitThunkRev
8681001 :: MonadObelisk m
@@ -871,11 +1004,11 @@ gitThunkRev
8711004 -> m ThunkRev
8721005gitThunkRev s commit = do
8731006 let u = _gitSource_url s
874− protocols = ["https", "ssh", "git"]
875− Just scheme <- pure $ unRText <$> uriScheme u
1007+ protocols = ["file", "https", "ssh", "git"]
1008+ scheme = maybe "file" URI.unRText $ URI.uriScheme $ (\(GitUri x) -> x) u
8761009 unless (T.toLower scheme `elem` protocols) $
8771010 failWith $ "obelisk currently only supports "
878− <> T.intercalate ", " protocols <> " protocols for non-GitHub remotes"
1011+ <> T.intercalate ", " protocols <> " protocols for plain Git remotes"
8791012 hash <- nixPrefetchGit u commit $ _gitSource_fetchSubmodules s
8801013 putLog Informational $ "Nix sha256 is " <> hash
8811014 pure $ ThunkRev
@@ -888,11 +1021,12 @@ gitThunkRev s commit = do
8881021--
8891022-- If the branch name is passed in, it is returned exactly as-is. If it is not
8901023-- passed it, the default branch of the repo is used instead.
1024+
8911025gitGetCommitBranch
892− :: MonadObelisk m => URI -> Maybe Text -> m (Text, CommitId)
1026+ :: MonadObelisk m => GitUri -> Maybe Text -> m (Text, CommitId)
8931027gitGetCommitBranch uri mbranch = withExitFailMessage ("Failure for git remote " <> uriMsg) $ do
8941028 (_, bothMaps) <- gitLsRemote
895− (T.unpack $ render uri)
1029+ (T.unpack $ gitUriToText uri)
8961030 (GitRef_Branch <$> mbranch)
8971031 Nothing
8981032 branch <- case mbranch of
@@ -908,18 +1042,21 @@ gitGetCommitBranch uri mbranch = withExitFailMessage ("Failure for git remote "
9081042 pure (branch, commit)
9091043 where
9101044 rethrowE = either failWith pure
911− uriMsg = render uri
1045+ uriMsg = gitUriToText uri
1046+
1047+parseGitUri :: Text -> Maybe GitUri
1048+parseGitUri x = GitUri <$> (parseFileURI x <|> parseAbsoluteURI x <|> parseSshShorthand x)
9121049
913−parseGitUri :: Text -> Maybe URI
914−parseGitUri x = parseAbsoluteURI x <|> parseSshShorthand x
1050+parseFileURI :: Text -> Maybe URI.URI
1051+parseFileURI uri = if "/" `T.isPrefixOf` uri then parseAbsoluteURI ("file://" <> uri) else Nothing
9151052
916−parseAbsoluteURI :: Text -> Maybe URI
1053+parseAbsoluteURI :: Text -> Maybe URI.URI
9171054parseAbsoluteURI uri = do
918− parsedUri <- mkURI uri
919− guard $ isPathAbsolute parsedUri
920− pure $ parsedUri
1055+ parsedUri <- URI.mkURI uri
1056+ guard $ URI.isPathAbsolute parsedUri
1057+ pure parsedUri
9211058
922−parseSshShorthand :: Text -> Maybe URI
1059+parseSshShorthand :: Text -> Maybe URI.URI
9231060parseSshShorthand uri = do
9241061 -- This is what git does to check that the remote
9251062 -- is not a local file path when parsing shorthand.
@@ -932,4 +1069,4 @@ parseSshShorthand uri = do
9321069 -- This check is used to disambiguate a filepath containing a colon from shorthand
9331070 guard $ isNothing (T.findIndex (=='/') authAndHostname)
9341071 && not (T.null colonAndPath)
935− mkURI properUri
1072+ URI.mkURI properUri
lib/command/src/Obelisk/Command/Utils.hs+39−17
@@ -4,6 +4,7 @@
44{-# LANGUAGE ScopedTypeVariables #-}
55{-# LANGUAGE OverloadedStrings #-}
66{-# LANGUAGE PatternGuards #-}
7+{-# LANGUAGE TemplateHaskell #-}
78module Obelisk.Command.Utils where
89
910import Control.Applicative hiding (many)
@@ -20,18 +21,28 @@ import Data.Maybe (maybeToList)
2021import Data.Text (Text)
2122import qualified Data.Text as T
2223import Data.Void (Void)
23−import System.Directory (canonicalizePath)
24−import System.Environment (getExecutablePath)
2524import System.Exit (ExitCode)
26−import qualified System.Process as P
25+import System.Which (staticWhich)
2726import Text.Megaparsec as MP
2827import Text.Megaparsec.Char as MP
2928
3029import Obelisk.App (MonadObelisk)
3130import Obelisk.CliApp
3231
33−getObeliskExe :: IO FilePath
34−getObeliskExe = getExecutablePath >>= canonicalizePath
32+cp :: FilePath
33+cp = $(staticWhich "cp")
34+
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")
3546
3647-- Check whether the working directory is clean
3748checkGitCleanStatus :: MonadObelisk m => FilePath -> Bool -> m Bool
@@ -61,20 +72,31 @@ initGit repo = do
6172 git ["add", "."]
6273 git ["commit", "-m", "Initial commit."]
6374
64−gitProcNoRepo :: [String] -> P.CreateProcess
65−gitProcNoRepo = P.proc "git"
75+gitProcNoRepo :: [String] -> ProcessSpec
76+gitProcNoRepo args = setEnvOverride (M.singleton "GIT_TERMINAL_PROMPT" "0" <>) $ proc "git" args
6677
67−gitProc :: FilePath -> [String] -> P.CreateProcess
78+gitProc :: FilePath -> [String] -> ProcessSpec
6879gitProc repo = gitProcNoRepo . runGitInDir
6980 where
7081 runGitInDir args' = case filter (not . null) args' of
7182 args@("clone":_) -> args <> [repo]
7283 args -> ["-C", repo] <> args
7384
74−-- | Recursively copy a directory using `cp -a`
75−copyDir :: FilePath -> FilePath -> P.CreateProcess
85+isolateGitProc :: ProcessSpec -> ProcessSpec
86+isolateGitProc = setEnvOverride (overrides <>)
87+ where
88+ overrides = M.fromList
89+ [ ("HOME", "/dev/null")
90+ , ("GIT_CONFIG_NOSYSTEM", "1")
91+ , ("GIT_TERMINAL_PROMPT", "0") -- git 2.3+
92+ , ("GIT_ASKPASS", "echo") -- pre git 2.3 to just use empty password
93+ , ("GIT_SSH_COMMAND", "ssh -o PreferredAuthentications password -o PubkeyAuthentication no -o GSSAPIAuthentication no")
94+ ]
95+
96+-- | Recursively copy a directory using `cp -a` -- TODO: Should use -rT instead of -a
97+copyDir :: FilePath -> FilePath -> ProcessSpec
7698copyDir src dest =
77− (P.proc "cp" ["-a", ".", dest]) { P.cwd = Just src }
99+ setCwd (Just src) $ proc cp ["-a", ".", dest] -- TODO: This will break if dest is relative since we change cwd
78100
79101readGitProcess :: MonadObelisk m => FilePath -> [String] -> m Text
80102readGitProcess repo = readProcessAndLogOutput (Debug, Notice) . gitProc repo
@@ -87,15 +109,15 @@ processToShellString cmd args = unwords $ map quoteAndEscape (cmd : args)
87109 where quoteAndEscape x = T.unpack $ "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
88110
89111-- | A simpler wrapper for CliApp's most used process function with sensible defaults.
90−runProc :: MonadObelisk m => P.CreateProcess -> m ()
112+runProc :: MonadObelisk m => ProcessSpec -> m ()
91113runProc = callProcessAndLogOutput (Notice, Error)
92114
93115-- | Like runProc, but all output goes to Debug logging level
94−runProcSilently :: MonadObelisk m => P.CreateProcess -> m ()
116+runProcSilently :: MonadObelisk m => ProcessSpec -> m ()
95117runProcSilently = callProcessAndLogOutput (Debug, Debug)
96118
97119-- | A simpler wrapper for CliApp's readProcessAndLogStderr with sensible defaults.
98−readProc :: MonadObelisk m => P.CreateProcess -> m Text
120+readProc :: MonadObelisk m => ProcessSpec -> m Text
99121readProc = readProcessAndLogOutput (Debug, Error)
100122
101123tshow :: Show a => a -> Text
@@ -129,13 +151,13 @@ gitLsRemote repository mRef mBranch = do
129151 (exitCode, out, _err) <- case mBranch of
130152 Nothing -> readCreateProcessWithExitCode $ gitProcNoRepo $
131153 ["ls-remote", "--exit-code", "--symref", repository]
132− ++ (maybeToList $ T.unpack . showGitRef <$> mRef)
133− Just branchName -> readCreateProcessWithExitCode $ gitProcNoRepo $
154+ ++ maybeToList (T.unpack . showGitRef <$> mRef)
155+ Just branchName -> readCreateProcessWithExitCode $ gitProcNoRepo
134156 ["ls-remote", "--exit-code", repository, branchName]
135157 let t = T.pack out
136158 maps <- case MP.runParser parseLsRemote "" t of
137159 Left err -> failWith $ T.pack $ MP.errorBundlePretty err
138− Right table -> pure $ bimap M.fromList M.fromList $ partitionEithers $ table
160+ Right table -> pure $ bimap M.fromList M.fromList $ partitionEithers table
139161 putLog Debug $ "git ls-remote maps: " <> T.pack (show maps)
140162 pure (exitCode, maps)
141163
lib/command/src/Obelisk/Command/VmBuilder.hs−1
@@ -18,7 +18,6 @@ import System.Directory (createDirectoryIfMissing)
1818import System.Exit (ExitCode(..))
1919import System.FilePath ((<.>), (</>))
2020import qualified System.Info
21−import System.Process (proc)
2221
2322import Obelisk.App (MonadObelisk, getObeliskUserStateDir)
2423import Obelisk.CliApp
lib/executable-config/lookup/obelisk-executable-config-lookup.cabal+1−1
@@ -27,7 +27,7 @@ library
2727 build-depends: directory
2828
2929 build-depends:
30− base,
30+ base,
3131 base64-bytestring,
3232 bytestring,
3333 containers,
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+4−4
@@ -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
@@ -229,7 +229,7 @@ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = d
229229renderFrontendHtml
230230 :: ( t ~ DomTimeline
231231 , MonadIO m
232− , widget ~ RoutedT t r (SetRouteT t r (RouteToUrlT r (ConfigsT (CookiesT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost)))))))
232+ , widget ~ RoutedT t r (SetRouteT t r (RouteToUrlT r (ConfigsT (CookiesT (HydratableT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost))))))))
233233 )
234234 => Map Text ByteString
235235 -> Cookies
@@ -241,7 +241,7 @@ renderFrontendHtml
241241 -> m ByteString
242242renderFrontendHtml configs cookies urlEnc route frontend headExtra bodyExtra = do
243243 --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
244− html <- fmap snd $ liftIO $ renderStatic $ fmap fst $ runCookiesT cookies $ runConfigsT configs $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
244+ html <- fmap snd $ liftIO $ renderStatic $ runHydratableT $ fmap fst $ runCookiesT cookies $ runConfigsT configs $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
245245 el "html" $ do
246246 el "head" $ do
247247 baseTag
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+12−5
@@ -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
@@ -881,7 +888,7 @@ instance (GCompare br, GCompare fr) => GCompare (FullRoute br fr) where
881888 gcompare (FullRoute_Frontend x) (FullRoute_Frontend y) = gcompare x y
882889
883890instance (UniverseSome br, UniverseSome fr) => UniverseSome (FullRoute br fr) where
884− universeSome = [Some (FullRoute_Backend x) | Some x <- universeSome]
891+ universeSome = [Some (FullRoute_Backend x) | Some x <- universeSome]
885892 ++ [Some (FullRoute_Frontend x) | Some x <- universeSome]
886893
887894-- | Build the typical top level application route encoder from a route for handling 404's,
lib/route/src/Obelisk/Route/Frontend.hs+96−16
@@ -7,15 +7,11 @@
77{-# LANGUAGE FlexibleContexts #-}
88{-# LANGUAGE FlexibleInstances #-}
99{-# LANGUAGE GADTs #-}
10−{-# LANGUAGE KindSignatures #-}
1110{-# LANGUAGE LambdaCase #-}
12−{-# LANGUAGE MultiParamTypeClasses #-}
1311{-# LANGUAGE OverloadedStrings #-}
1412{-# LANGUAGE PatternSynonyms #-}
1513{-# LANGUAGE RankNTypes #-}
1614{-# LANGUAGE ScopedTypeVariables #-}
17−{-# LANGUAGE TemplateHaskell #-}
18−{-# LANGUAGE TupleSections #-}
1915{-# LANGUAGE TypeFamilies #-}
2016{-# LANGUAGE TypeOperators #-}
2117{-# LANGUAGE ViewPatterns #-}
@@ -53,6 +49,7 @@ module Obelisk.Route.Frontend
5349 , runRouteToUrlT
5450 , mapRouteToUrlT
5551 , routeLink
52+ , routeLinkDynAttr
5653 , dynRouteLink
5754 , adaptedUriPath
5855 , setAdaptedUriPath
@@ -63,7 +60,7 @@ import Prelude hiding ((.), id)
6360import Obelisk.Route
6461
6562import Control.Category (Category (..), (.))
66−import Control.Category.Cartesian
63+import Control.Category.Cartesian ((&&&))
6764import Control.Lens hiding (Bifunctor, bimap, universe, element)
6865import Control.Monad ((<=<))
6966import Control.Monad.Fix
@@ -74,6 +71,7 @@ import Control.Monad.Trans.Control
7471import Data.Coerce
7572import Data.Dependent.Sum (DSum (..))
7673import Data.GADT.Compare
74+import Data.Map (Map)
7775import Data.Monoid
7876import Data.Proxy
7977import Data.Text (Text)
@@ -92,7 +90,9 @@ import Reflex.Dom.Builder.Class
9290import Data.Type.Coercion
9391import Language.Javascript.JSaddle --TODO: Get rid of this - other platforms can also be routed
9492import Reflex.Dom.Core
93+import qualified GHCJS.DOM as DOM
9594import qualified GHCJS.DOM.Types as DOM
95+import qualified GHCJS.DOM.Window as Window
9696import Network.URI
9797import Data.Maybe (fromMaybe)
9898import qualified Data.List as L
@@ -509,46 +509,126 @@ runRouteViewT routeEncoder switchover useHash a = do
509509-- | A link widget that, when clicked, sets the route to the provided route. In non-javascript
510510-- contexts, this widget falls back to using @href@s to control navigation
511511routeLink
512− :: forall t m a route.
512+ :: forall t m a route js.
513513 ( DomBuilder t m
514514 , RouteToUrl route m
515515 , SetRoute t route m
516+ , Prerender js t m
516517 )
517518 => route -- ^ Target route
518519 -> m a -- ^ Child widget
519520 -> m a
520521routeLink r w = do
522+ (e, a) <- routeLinkImpl r w
523+ scrollToTop e
524+ return a
525+
526+-- | Raw implementation of 'routeLink'. Does not scroll to the top of the page on clicks.
527+routeLinkImpl
528+ :: forall t m a route.
529+ ( DomBuilder t m
530+ , RouteToUrl route m
531+ , SetRoute t route m
532+ )
533+ => route -- ^ Target route
534+ -> m a -- ^ Child widget
535+ -> m (Event t (), a)
536+routeLinkImpl r w = do
521537 enc <- askRouteToUrl
522538 let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
523539 & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
524540 & elementConfig_initialAttributes .~ "href" =: enc r
525541 (e, a) <- element "a" cfg w
526542 setRoute $ r <$ domEvent Click e
527− return a
543+ return (domEvent Click e, a)
528544
529−-- | A link widget that, when clicked, sets the route to current value of the
530−-- provided dynamic route. In non-javascript contexts the value of the dynamic post
531−-- build is used so the link still works like 'routeLink'.
545+scrollToTop :: forall m t js. (Prerender js t m, Monad m) => Event t () -> m ()
546+scrollToTop e = prerender_ blank $ performEvent_ $ ffor e $ \_ -> liftJSM $ DOM.currentWindow >>= \case
547+ Nothing -> pure ()
548+ Just win -> Window.scrollTo win 0 0
549+
550+-- | Like 'routeLinkDynAttr' but without custom attributes.
532551dynRouteLink
533− :: forall t m a route.
552+ :: forall t m a route js.
534553 ( DomBuilder t m
535554 , PostBuild t m
536− , RouteToUrl (R route) m
537− , SetRoute t (R route) m
555+ , RouteToUrl route m
556+ , SetRoute t route m
557+ , Prerender js t m
538558 )
539− => Dynamic t (R route) -- ^ Target route
559+ => Dynamic t route -- ^ Target route
540560 -> m a -- ^ Child widget
541561 -> m a
542−dynRouteLink dr w = do
562+dynRouteLink r w = do
563+ (e, a) <- dynRouteLinkImpl r w
564+ scrollToTop e
565+ return a
566+
567+-- | Raw implementation of 'dynRouteLink'. Does not scroll to the top of the page on clicks.
568+dynRouteLinkImpl
569+ :: forall t m a route.
570+ ( DomBuilder t m
571+ , PostBuild t m
572+ , RouteToUrl route m
573+ , SetRoute t route m
574+ )
575+ => Dynamic t route -- ^ Target route
576+ -> m a -- ^ Child widget
577+ -> m (Event t (), a)
578+dynRouteLinkImpl dr w = do
543579 enc <- askRouteToUrl
544580 er <- dynamicAttributesToModifyAttributes $ ("href" =:) . enc <$> dr
545581 let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
546582 & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
547583 & elementConfig_modifyAttributes .~ er
548584 (e, a) <- element "a" cfg w
549− setRoute $ tag (current dr) $ domEvent Click e
585+ let clk = domEvent Click e
586+ setRoute $ tag (current dr) clk
587+ return (clk, a)
588+
589+-- | An @a@-tag link widget that, when clicked, sets the route to current value of the
590+-- provided dynamic route. In non-JavaScript contexts the value of the dynamic post
591+-- build is used so the link still works like 'routeLink'.
592+routeLinkDynAttr
593+ :: forall t m a route js.
594+ ( DomBuilder t m
595+ , PostBuild t m
596+ , RouteToUrl (R route) m
597+ , SetRoute t (R route) m
598+ , Prerender js t m
599+ )
600+ => Dynamic t (Map AttributeName Text) -- ^ Attributes for @a@ element. Note that if @href@ is present it will be ignored
601+ -> Dynamic t (R route) -- ^ Target route
602+ -> m a -- ^ Child widget of the @a@ element
603+ -> m a
604+routeLinkDynAttr dAttr dr w = do
605+ (e, a) <- routeLinkDynAttrImpl dAttr dr w
606+ scrollToTop e
550607 return a
551608
609+-- | Raw implementation of 'routeLinkDynAttr'. Does not scroll to the top of the page on clicks.
610+routeLinkDynAttrImpl
611+ :: forall t m a route.
612+ ( DomBuilder t m
613+ , PostBuild t m
614+ , RouteToUrl (R route) m
615+ , SetRoute t (R route) m
616+ )
617+ => Dynamic t (Map AttributeName Text) -- ^ Attributes for @a@ element. Note that if @href@ is present it will be ignored
618+ -> Dynamic t (R route) -- ^ Target route
619+ -> m a -- ^ Child widget of the @a@ element
620+ -> m (Event t (), a)
621+routeLinkDynAttrImpl dAttr dr w = do
622+ enc <- askRouteToUrl
623+ er <- dynamicAttributesToModifyAttributes $ zipDynWith (<>) (("href" =:) . enc <$> dr) dAttr
624+ let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
625+ & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
626+ & elementConfig_modifyAttributes .~ er
627+ (e, a) <- element "a" cfg w
628+ let clk = domEvent Click e
629+ setRoute $ tag (current dr) clk
630+ return (clk, a)
631+
552632-- On ios due to sandboxing when loading the page from a file adapt the
553633-- path to be based on the hash.
554634
lib/run/obelisk-run.cabal+7
@@ -15,6 +15,7 @@ library
1515 , dependent-sum
1616 , dependent-sum-template
1717 , ghcjs-dom
18+ , HsOpenSSL
1819 , http-client
1920 , http-reverse-proxy
2021 , http-types
@@ -36,11 +37,17 @@ library
3637 , snap-core
3738 , streaming-commons
3839 , text
40+ , time
3941 , universe
42+ , utf8-string
4043 , wai
4144 , wai-websockets
4245 , warp
46+ , warp-tls
4347 , websockets
4448 exposed-modules:
4549 Obelisk.Run
4650 ghc-options: -Wall -fwarn-redundant-constraints
51+ if os(linux)
52+ cpp-options: -DIPROUTE_SUPPORTED
53+ build-depends: which
lib/run/src/Obelisk/Run.hs+56−13
@@ -1,3 +1,8 @@
1+{-# LANGUAGE CPP #-}
2+#if defined(IPROUTE_SUPPORTED)
3+{-# LANGUAGE TemplateHaskell #-}
4+#endif
5+
16{-# LANGUAGE ConstraintKinds #-}
27{-# LANGUAGE FlexibleContexts #-}
38{-# LANGUAGE FlexibleInstances #-}
@@ -23,6 +28,7 @@ import Data.ByteString (ByteString)
2328import qualified Data.ByteString as BS
2429import qualified Data.ByteString.Char8 as BSC
2530import qualified Data.ByteString.Lazy.Char8 as BSLC
31+import qualified Data.ByteString.UTF8 as BSUTF8
2632import Data.Functor.Identity
2733import Data.List (uncons)
2834import Data.Map (Map)
@@ -34,6 +40,7 @@ import Data.String (fromString)
3440import Data.Text (Text)
3541import qualified Data.Text as T
3642import qualified Data.Text.Encoding as T
43+import Data.Time.Clock (getCurrentTime, addUTCTime)
3744import Language.Javascript.JSaddle.Run (syncPoint)
3845import Language.Javascript.JSaddle.WebSockets
3946import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)
@@ -43,6 +50,7 @@ import Network.Socket
4350import Network.Wai (Application)
4451import qualified Network.Wai as W
4552import Network.Wai.Handler.Warp
53+import Network.Wai.Handler.WarpTLS
4654import Network.Wai.Handler.Warp.Internal (settingsHost, settingsPort)
4755import Network.WebSockets (ConnectionOptions)
4856import Network.WebSockets.Connection (defaultConnectionOptions)
@@ -50,21 +58,28 @@ import qualified Obelisk.Asset.Serve.Snap as Snap
5058import Obelisk.Backend
5159import Obelisk.Frontend
5260import Obelisk.Route.Frontend
61+import qualified OpenSSL.PEM as PEM
62+import qualified OpenSSL.RSA as RSA
63+import qualified OpenSSL.X509 as X509
64+import qualified OpenSSL.X509.Request as X509Request
5365import Reflex.Dom.Core
5466import Snap.Core (Snap)
5567import System.Environment
5668import System.IO
5769import System.Process
58−import System.Exit (ExitCode(..))
5970import Text.URI (URI)
6071import qualified Text.URI as URI
6172import Text.URI.Lens
6273import Web.Cookie
6374
75+#if defined(IPROUTE_SUPPORTED)
76+import qualified System.Which
77+#endif
78+
6479run
6580 :: Int -- ^ Port to run the backend
6681 -> ([Text] -> Snap ()) -- ^ Static asset handler
67− -> Backend fullRoute frontendRoute -- ^ Backend
82+ -> Backend backendRoute frontendRoute -- ^ Backend
6883 -> Frontend (R frontendRoute) -- ^ Frontend
6984 -> IO ()
7085run port serveStaticAsset backend frontend = do
@@ -117,13 +132,36 @@ runWidget conf configs frontend validFullEncoder = do
117132 beforeMainLoop = do
118133 putStrLn $ "Frontend running on " <> T.unpack (URI.render uri)
119134 settings = setBeforeMainLoop beforeMainLoop (setPort port (setTimeout 3600 defaultSettings))
135+ -- Providing TLS here will also incidentally provide it to proxied requests to the backend.
136+ prepareRunner = case uri ^? uriScheme . _Just . unRText of
137+ Just "https" -> do
138+ -- Generate a private key and self-signed certificate for TLS
139+ privateKey <- RSA.generateRSAKey' 2048 3
140+
141+ certRequest <- X509Request.newX509Req
142+ _ <- X509Request.setPublicKey certRequest privateKey
143+ _ <- X509Request.signX509Req certRequest privateKey Nothing
144+
145+ cert <- X509.newX509 >>= X509Request.makeX509FromReq certRequest
146+ _ <- X509.setPublicKey cert privateKey
147+ now <- getCurrentTime
148+ _ <- X509.setNotBefore cert $ addUTCTime (-1) now
149+ _ <- X509.setNotAfter cert $ addUTCTime (365 * 24 * 60 * 60) now
150+ _ <- X509.signX509 cert privateKey Nothing
151+
152+ certByteString <- BSUTF8.fromString <$> PEM.writeX509 cert
153+ privateKeyByteString <- BSUTF8.fromString <$> PEM.writePKCS8PrivateKey privateKey Nothing
154+
155+ return $ runTLSSocket (tlsSettingsMemory certByteString privateKeyByteString)
156+ _ -> return runSettingsSocket
157+ runner <- prepareRunner
120158 bracket
121159 (bindPortTCPRetry settings (logPortBindErr port) (_runConfig_retryTimeout conf))
122160 close
123161 (\skt -> do
124162 man <- newManager defaultManagerSettings
125163 app <- obeliskApp configs defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
126− runSettingsSocket settings skt app)
164+ runner settings skt app)
127165
128166obeliskApp
129167 :: forall frontendRoute backendRoute
@@ -187,17 +225,22 @@ logPortBindErr p e = getProcessIdForPort p >>= \case
187225 Nothing -> putStrLn $ "runWidget: " <> show e
188226 Just pid -> putStrLn $ unwords [ "Port", show p, "is being used by process ID", show pid <> ".", "Please kill that process or change the port in config/common/route."]
189227
228+ssPath :: Maybe String
229+ssPath =
230+#if defined(IPROUTE_SUPPORTED)
231+ Just $(System.Which.staticWhich "ss")
232+#else
233+ Nothing
234+#endif
235+
190236getProcessIdForPort :: Int -> IO (Maybe Int)
191−getProcessIdForPort port = do
192− -- First check if 'ss' is available
193− (c, _, _) <- readProcessWithExitCode "which" ["ss"] mempty
194− case c of
195− ExitSuccess -> do
196− xs <- lines <$> readProcess "ss" ["-lptn", "sport = " <> show port] mempty
197− case uncons xs of
198− Just (_, x:_) -> return $ A.maybeResult $ A.parse parseSsPid $ BSC.pack x
199− _ -> return Nothing
200− _ -> return Nothing
237+getProcessIdForPort port = case ssPath of
238+ Just ss -> do
239+ xs <- lines <$> readProcess ss ["-lptn", "sport = " <> show port] mempty
240+ case uncons xs of
241+ Just (_, x:_) -> return $ A.maybeResult $ A.parse parseSsPid $ BSC.pack x
242+ _ -> return Nothing
243+ _ -> return Nothing
201244
202245parseSsPid :: A.Parser Int
203246parseSsPid = do
lib/selftest/obelisk-selftest.cabal+4−2
@@ -11,16 +11,18 @@ library
1111 base
1212 , containers
1313 , directory
14+ , filepath
1415 , hspec
1516 , http-client
1617 , http-types
1718 , HUnit
19+ , network
1820 , obelisk-cliapp
1921 , obelisk-executable-config-lookup
2022 , obelisk-run
23+ , which
2124 , process
22− , network
23− , shelly
25+ , shelly >= 1.9.0
2426 , temporary
2527 , text
2628 exposed-modules:
lib/selftest/src/Obelisk/SelfTest.hs+205−95
@@ -1,9 +1,11 @@
1+{-# LANGUAGE DoAndIfThenElse #-}
12{-# LANGUAGE EmptyCase #-}
23{-# LANGUAGE LambdaCase #-}
34{-# LANGUAGE OverloadedStrings #-}
45{-# LANGUAGE PatternGuards #-}
56{-# LANGUAGE ScopedTypeVariables #-}
6−{-# LANGUAGE TypeApplications #-}
7+{-# LANGUAGE ViewPatterns #-}
8+{-# LANGUAGE TemplateHaskell #-}
79module Obelisk.SelfTest where
810
911import Control.Exception (bracket, throw)
@@ -11,7 +13,7 @@ import Control.Monad
1113import Control.Monad.IO.Class
1214import Data.Bool (bool)
1315import Data.Function (fix)
14−import Data.Semigroup (Semigroup, (<>))
16+import Data.Semigroup ((<>))
1517import qualified Data.Set as Set
1618import Data.String
1719import Data.Text (Text)
@@ -21,12 +23,13 @@ import qualified Data.Text.IO as T
2123import qualified Network.HTTP.Client as HTTP
2224import qualified Network.HTTP.Types as HTTP
2325import qualified Network.Socket as Socket
24−import Shelly
25−import System.Directory (withCurrentDirectory, getDirectoryContents)
26+import Shelly hiding (cp)
27+import System.Directory (getCurrentDirectory, withCurrentDirectory, getDirectoryContents)
2628import System.Environment
2729import System.Exit (ExitCode (..))
28−import System.Info
29−import System.IO (Handle, hClose)
30+import System.Which (staticWhich)
31+import qualified System.Info
32+import System.IO (Handle, hClose, hIsEOF, hGetContents)
3033import System.IO.Temp
3134import System.Process (readProcessWithExitCode, CreateProcess(cwd), readCreateProcessWithExitCode, proc)
3235import Test.Hspec
@@ -43,16 +46,42 @@ data ObRunState
4346 | ObRunState_BackendStarted
4447 deriving (Eq, Show)
4548
46−doubleQuotes :: (IsString a, Semigroup a) => a -> a
47−doubleQuotes s = "\"" <> s <> "\""
49+cp :: FilePath
50+cp = $(staticWhich "cp")
51+
52+cabalPath :: FilePath
53+cabalPath = $(staticWhich "cabal")
54+
55+gitPath :: FilePath
56+gitPath = $(staticWhich "git")
57+
58+chownPath :: FilePath
59+chownPath = $(staticWhich "chown")
60+
61+chmodPath :: FilePath
62+chmodPath = $(staticWhich "chmod")
63+
64+whoamiPath :: FilePath
65+whoamiPath = $(staticWhich "whoami")
66+
67+nixBuildPath :: FilePath
68+nixBuildPath = $(staticWhich "nix-build")
69+
70+lnPath :: FilePath
71+lnPath = $(staticWhich "ln")
72+
73+rmPath :: FilePath
74+rmPath = $(staticWhich "rm")
75+
76+gitUserConfig :: [Text]
77+gitUserConfig = ["-c", "user.name=Obelisk Selftest", "-c", "user.email=noreply@example.com"]
4878
4979commit :: Text -> Sh ()
50−commit msg = void $ run "git"
51− [ "commit"
80+commit msg = void $ run gitPath $ gitUserConfig <> [ "commit"
5281 , "--no-gpg-sign"
5382 , "--allow-empty"
5483 , "-m"
55− , doubleQuotes msg
84+ , msg
5685 ]
5786
5887-- TODO replace Void and stop using loosely typed synchronous exceptions
@@ -68,111 +97,168 @@ tshow = T.pack . show
6897shellyOb :: MonadIO m => (Sh a -> Sh a) -> Sh a -> m a
6998shellyOb f obTest = shelly $ f obTest
7099
100+-- Set "ob" in a single place so it can be replaced with a
101+-- link to obelisk in the nix store in the future,
102+-- and avoid PATH hacking before calling this script.
103+ob :: FilePath
104+ob = "ob"
105+
106+augmentWithVerbosity :: (String -> [Text] -> a) -> String -> Bool -> [Text] -> a
107+augmentWithVerbosity runner executable isVerbose args = runner executable $ (if isVerbose then ("-v" :) else id) args
108+
71109main :: IO ()
72110main = do
73111 -- Note: you can pass hspec arguments as well, eg: `-m <pattern>`
74− isVerbose <- (elem "-v") <$> getArgs
112+ isVerbose <- elem "-v" <$> getArgs
75113 unless isVerbose $
76114 putStrLn "Tests may take longer to run if there are unbuilt derivations: use -v for verbose output"
77115 let verbosity = bool silently verbosely isVerbose
78− nixBuild args = run "nix-build" ("--no-out-link" : args)
79− obeliskImpl <- fromString <$> getEnv "OBELISK_IMPL"
116+ nixBuild args = run nixBuildPath ("--no-out-link" : args)
117+ obeliskImplDirtyReadOnly <- fromString <$> getCurrentDirectory
118+ let runOb_ = augmentWithVerbosity run_ ob isVerbose
119+ let runOb = augmentWithVerbosity run ob isVerbose
120+ let testObRunInDir' = augmentWithVerbosity testObRunInDir ob isVerbose ["run"]
121+ let testThunkPack' = augmentWithVerbosity testThunkPack ob isVerbose []
122+ let
123+ withObeliskImplClean f =
124+ withSystemTempDirectory "obelisk-impl-clean" $ \(fromString -> obeliskImpl) -> do
125+ void . shellyOb verbosity $ chdir obeliskImpl $ do
126+ run_ gitPath ["clone", "file://" <> toTextIgnore obeliskImplDirtyReadOnly, toTextIgnore obeliskImpl]
127+ f obeliskImpl
128+
129+ withObeliskImplDirty f =
130+ withSystemTempDirectory "obelisk-impl-copy" $ \(fromString -> obeliskImpl) -> do
131+ void . shellyOb verbosity $ chdir obeliskImpl $ do
132+ user <- T.strip <$> run whoamiPath []
133+ run_ cp ["-rT", "--no-preserve=mode", toTextIgnore obeliskImplDirtyReadOnly, toTextIgnore obeliskImpl]
134+ run_ chownPath ["-R", user, toTextIgnore obeliskImpl]
135+ run_ chmodPath ["-R", "g-rw,o-rw", toTextIgnore obeliskImpl]
136+ f obeliskImpl
137+
138+ withInitCache f obeliskImpl =
139+ withSystemTempDirectory "init Cache λ" $ \initCache -> do
140+ -- Setup the ob init cache
141+ void . shellyOb verbosity $ chdir initCache $ do
142+ runOb_ ["init", "--symlink", toTextIgnore obeliskImpl]
143+ run_ gitPath ["init"]
144+
145+ f initCache
146+
147+ withObeliskImplDirtyAndInitCache f =
148+ withObeliskImplDirty $ \impl -> withInitCache (f impl) impl
149+
80150 httpManager <- HTTP.newManager HTTP.defaultManagerSettings
81− [p0, p1, p2, p3] <- liftIO $ getFreePorts 4
82− withSystemTempDirectory "initCache" $ \initCache -> do
83− -- Setup the ob init cache
84− void . shellyOb verbosity $ chdir (fromString initCache) $ do
85− run_ "ob" ["init"]
86− run_ "git" ["init"]
151+
152+ withObeliskImplDirtyAndInitCache $ \obeliskImplDirty initCache ->
87153 hspec $ parallel $ do
88154 let shelly_ = void . shellyOb verbosity
89155
90− inTmp :: (Shelly.FilePath -> Sh a) -> IO ()
91− inTmp f = withTmp (chdir <*> f)
156+ defaultTmpDirName = "test λ"
157+
158+ inTmp' :: FilePath -> (FilePath -> Sh a) -> IO ()
159+ inTmp' dirname f = withTmp' dirname (chdir <*> f)
160+ inTmp = inTmp' defaultTmpDirName
92161
93− withTmp f = shelly_ . withSystemTempDirectory "test" $ f . fromString
162+ withTmp' dirname f = shelly_ . withSystemTempDirectory dirname $ f . fromString
163+ withTmp = withTmp' defaultTmpDirName
94164
95− inTmpObInit f = inTmp $ \dir -> do
96− run_ "cp" ["-a", fromString $ initCache <> "/.", toTextIgnore dir]
165+ inTmpObInit' dirname f = inTmp' dirname $ \dir -> do
166+ run_ cp ["-rT", fromString initCache, toTextIgnore dir]
97167 f dir
168+ inTmpObInit = inTmpObInit' defaultTmpDirName
169+
170+ -- To be used in tests that change the obelisk impl directory
171+ inTmpObInitWithImplCopy f = inTmpObInit $ \dir ->
172+ withObeliskImplClean $ \(fromString -> implClean) -> do
173+ run_ rmPath [thunk]
174+ run_ lnPath ["-s", implClean, thunk]
175+ f dir
98176
99177 assertRevEQ a b = liftIO . assertEqual "" "" =<< diff a b
100178 assertRevNE a b = liftIO . assertBool "" . (/= "") =<< diff a b
101179
102− revParseHead = T.strip <$> run "git" ["rev-parse", "HEAD"]
180+ revParseHead = T.strip <$> run gitPath ["rev-parse", "HEAD"]
103181
104182 commitAll = do
105− run_ "git" ["add", "."]
183+ run_ gitPath ["add", "."]
106184 commit "checkpoint"
107185 revParseHead
108186
109187 thunk = ".obelisk/impl"
110− update = run "ob" ["thunk", "update", thunk] >> commitAll
111− pack = run "ob" ["thunk", "pack", thunk] >> commitAll
112− unpack = run "ob" ["thunk", "unpack", thunk] >> commitAll
188+ update = runOb ["thunk", "update", thunk] >> commitAll
189+ pack = runOb ["thunk", "pack", thunk] >> commitAll
190+ unpack = runOb ["thunk", "unpack", thunk] >> commitAll
113191
114− diff a b = run "git" ["diff", a, b]
192+ diff a b = run gitPath ["diff", a, b]
115193
116194 describe "ob init" $ parallel $ do
117− it "works with default impl" $ inTmp $ \_ -> run "ob" ["init"]
118− it "works with master branch impl" $ inTmp $ \_ -> run "ob" ["init", "--branch", "master"]
119− it "works with symlink" $ inTmp $ \_ -> run "ob" ["init", "--symlink", obeliskImpl]
120− it "doesn't silently overwrite existing files" $ withSystemTempDirectory "ob-init" $ \dir -> do
121− let p force = (proc "ob" $ "--no-handoff" : "init" : if force then ["--force"] else []) { cwd = Just dir }
195+ it "works with default impl" $ inTmp $ \_ -> runOb ["init"]
196+ it "works with master branch impl" $ inTmp $ \_ -> runOb ["init", "--branch", "master"]
197+ it "works with symlink" $ inTmp $ \_ -> runOb ["init", "--symlink", toTextIgnore obeliskImplDirty]
198+ it "doesn't silently overwrite existing files" $ withSystemTempDirectory "ob-init λ" $ \dir -> do
199+ let p force = (System.Process.proc ob $ "--no-handoff" : "-v" : "init" : ["--force"|force]) { cwd = Just dir }
122200 (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p False) ""
123201 (ExitFailure _, _, _) <- readCreateProcessWithExitCode (p False) ""
124202 (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p True) ""
125203 pure ()
126204
127205 it "doesn't create anything when given an invalid impl" $ inTmp $ \tmp -> do
128− void $ errExit False $ run "ob" ["init", "--symlink", "/dev/null"]
206+ void $ errExit False $ runOb ["init", "--symlink", "/dev/null"]
129207 ls tmp >>= liftIO . assertEqual "" []
130208
131− it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $ do
209+ it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $
132210 withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ do
133211 configs <- getConfigs
134212 return (either (const Nothing) Just $ getConfigRoute configs) `shouldNotReturn` Nothing
135213
136214 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
137215 -- when not run by 'nix-build --attr selftest'
138− describe "ob run" $ parallel $ do
139− it "works in root directory" $ inTmpObInit $ \_ -> do
140− testObRunInDir p0 p1 Nothing httpManager
141− it "works in sub directory" $ inTmpObInit $ \_ -> do
142− testObRunInDir p2 p3 (Just "frontend") httpManager
216+ describe "ob run" $ {- NOT parallel $ -} do
217+ it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir' Nothing httpManager
218+ it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir' (Just "frontend") httpManager
143219
144220 describe "obelisk project" $ parallel $ do
145− it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , obeliskImpl]
146− it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", obeliskImpl]
147− it "can build obelisk shell" $ inTmpObInit $ \_ -> nixBuild ["-A", "shell", obeliskImpl]
148− it "can build everything" $ inTmpObInit $ \_ -> nixBuild [obeliskImpl]
221+ it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskImplDirty]
222+ it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", toTextIgnore obeliskImplDirty]
223+ it "can build obelisk shell" $ inTmpObInit $ \_ -> nixBuild ["-A", "shell", toTextIgnore obeliskImplDirty]
224+ it "can build everything" $ inTmpObInit $ \_ -> nixBuild [toTextIgnore obeliskImplDirty]
149225
150226 describe "blank initialized project" $ parallel $ do
151227
152− it "can build ghc.backend" $ inTmpObInit $ \_ -> do
153− nixBuild ["-A", "ghc.backend"]
154− it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> do
155− nixBuild ["-A", "ghcjs.frontend"]
228+ it "can build ghc.backend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghc.backend"]
229+ it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghcjs.frontend"]
156230
157− if os == "darwin"
158− then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
159− else it "can build android" $ inTmpObInit $ \_ -> nixBuild ["-A", "android.frontend"]
231+ if System.Info.os == "darwin"
232+ then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
233+ else it "can build android after accepting license" $ inTmpObInit $ \dir -> do
234+ let defaultNixPath = dir </> ("default.nix" :: FilePath)
235+ writefile defaultNixPath
236+ =<< T.replace
237+ "# config.android_sdk.accept_license = false;"
238+ "config.android_sdk.accept_license = true;"
239+ <$> readfile defaultNixPath
240+ nixBuild ["-A", "android.frontend"]
160241
161242 forM_ ["ghc", "ghcjs"] $ \compiler -> do
162243 let
163− shell = "shells." <> compiler
164− inShell cmd' = run "nix-shell" ["default.nix", "-A", fromString shell, "--run", cmd']
165− it ("can enter " <> shell) $ inTmpObInit $ \_ -> inShell "exit"
166− it ("can build in " <> shell) $ inTmpObInit $ \_ -> inShell $ "cabal new-build --" <> fromString compiler <> " all"
167−
168− it "has idempotent thunk update" $ inTmpObInit $ \_ -> do
244+ shellName = "shells." <> compiler
245+ inShell cmd' = run_ "nix-shell" ["default.nix", "-A", fromString shellName, "--run", cmd']
246+ it ("can enter " <> shellName) $ inTmpObInit $ \_ -> inShell "exit"
247+ -- NOTE: We override the temporary directory name because cabal has a bug preventing new-build from working
248+ -- in a path that has unicode characters.
249+ it ("can build in " <> shellName) $ inTmpObInit' "test" $ \_ -> inShell $
250+ T.pack cabalPath <> " --version; " <> T.pack cabalPath <> " new-build --" <> T.pack compiler <> " all"
251+
252+ it "has idempotent thunk update" $ inTmpObInitWithImplCopy $ \_ -> do
253+ _ <- pack
169254 u <- update
170255 uu <- update
171256 assertRevEQ u uu
172257
173258 describe "ob thunk pack/unpack" $ parallel $ do
174− it "has thunk pack and unpack inverses" $ inTmpObInit $ \_ -> do
259+ it "has thunk pack and unpack inverses" $ inTmpObInitWithImplCopy $ \_ -> do
175260
261+ _ <- pack
176262 e <- commitAll
177263 eu <- unpack
178264 eup <- pack
@@ -185,76 +271,93 @@ main = do
185271
186272 it "unpacks the correct branch" $ withTmp $ \dir -> do
187273 let branch = "master"
188− run_ "git" ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch]
189− run_ "ob" ["thunk", "pack", toTextIgnore dir]
190− run_ "ob" ["thunk", "unpack", toTextIgnore dir]
191− branch' <- chdir dir $ run "git" ["rev-parse", "--abbrev-ref", "HEAD"]
274+ run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch]
275+ runOb_ ["thunk", "pack", toTextIgnore dir]
276+ runOb_ ["thunk", "unpack", toTextIgnore dir]
277+ branch' <- chdir dir $ run gitPath ["rev-parse", "--abbrev-ref", "HEAD"]
192278 liftIO $ assertEqual "" branch (T.strip branch')
193279
194− it "can pack and unpack plain git repos" $ do
280+ it "can pack and unpack plain git repos" $
195281 shelly_ $ withSystemTempDirectory "git-repo" $ \dir -> do
196282 let repo = toTextIgnore $ dir </> ("repo" :: String)
197− run_ "git" ["clone", "https://github.com/haskell/process.git", repo]
283+ run_ gitPath ["clone", "https://github.com/haskell/process.git", repo]
198284 origHash <- chdir (fromText repo) revParseHead
199285
200− run_ "ob" ["thunk", "pack", repo]
286+ runOb_ ["thunk", "pack", repo]
201287 packedFiles <- Set.fromList <$> ls (fromText repo)
202288 liftIO $ assertEqual "" packedFiles $ Set.fromList $ (repo </>) <$>
203289 ["default.nix", "github.json", ".attr-cache" :: String]
204290
205− run_ "ob" ["thunk", "unpack", repo]
291+ runOb_ ["thunk", "unpack", repo]
206292 chdir (fromText repo) $ do
207293 unpackHash <- revParseHead
208294 assertRevEQ origHash unpackHash
209295
210− testThunkPack $ fromText repo
296+ testThunkPack' $ fromText repo
211297
212− it "aborts thunk pack when there are uncommitted files" $ inTmpObInit $ \dir -> do
213− void $ unpack
214− testThunkPack (dir </> thunk)
298+ it "aborts thunk pack when there are uncommitted files" $ inTmpObInitWithImplCopy $ \dir -> do
299+ testThunkPack' (dir </> thunk)
215300
216301 describe "ob thunk update --branch" $ parallel $ do
217302 it "can change a thunk to the latest version of a desired branch" $ withTmp $ \dir -> do
218303 let branch1 = "master"
219304 branch2 = "develop"
220− run_ "git" ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch1]
221− run_ "ob" ["thunk" , "pack", toTextIgnore dir]
222− run_ "ob" ["thunk", "update", toTextIgnore dir, "--branch", branch2]
305+ run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch1]
306+ runOb_ ["thunk" , "pack", toTextIgnore dir]
307+ runOb_ ["thunk", "update", toTextIgnore dir, "--branch", branch2]
223308
224309 it "doesn't create anything when given an invalid branch" $ withTmp $ \dir -> do
225310 let checkDir dir' = liftIO $ getDirectoryContents $ T.unpack $ toTextIgnore dir'
226− run_ "git" ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", "master"]
227− run_ "ob" ["thunk" , "pack", toTextIgnore dir]
311+ run_ gitPath ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", "master"]
312+ runOb_ ["thunk" , "pack", toTextIgnore dir]
228313 startingContents <- checkDir dir
229− void $ errExit False $ run "ob" ["thunk", "update", toTextIgnore dir, "--branch", "dumble-palooza"]
314+ void $ errExit False $ runOb ["thunk", "update", toTextIgnore dir, "--branch", "dumble-palooza"]
230315 checkDir dir >>= liftIO . assertEqual "" startingContents
231316
317+ describe "ob hoogle" $ {- NOT parallel -} do
318+ it "starts a hoogle server on the given port" $ inTmpObInit $ \_ -> do
319+ [p0] <- liftIO $ getFreePorts 1
320+ maskExitSuccess $ runHandle "ob" ["hoogle", "--port", T.pack (show p0)] $ \stdout -> fix $ \loop -> do
321+ ln <- liftIO $ T.hGetLine stdout
322+ echo ln
323+ let search = "Server starting on port " <> T.pack (show p0)
324+ case search `T.isInfixOf` ln of
325+ False -> loop -- keep waiting
326+ True -> do
327+ let req uri = liftIO $ HTTP.parseRequest uri >>= flip HTTP.httpLbs httpManager
328+ req ("http://127.0.0.1:" <> show p0) >>= \r -> case HTTP.responseStatus r == HTTP.ok200 of
329+ False -> errorExit $ "Request to hoogle server failed: " <> T.pack (show r)
330+ True -> exit 0
331+
332+maskExitSuccess :: Sh () -> Sh ()
333+maskExitSuccess = handle_sh (\case ExitSuccess -> pure (); e -> throw e)
232334
233335-- | Run `ob run` in the given directory (maximum of one level deep)
234−testObRunInDir :: Socket.PortNumber -> Socket.PortNumber -> Maybe Shelly.FilePath -> HTTP.Manager -> Sh ()
235−testObRunInDir p0 p1 mdir httpManager = handle_sh (\case ExitSuccess -> pure (); e -> throw e) $ do
336+testObRunInDir :: String -> [Text] -> Maybe FilePath -> HTTP.Manager -> Sh ()
337+testObRunInDir executable extraArgs mdir httpManager = maskExitSuccess $ do
338+ [p0, p1] <- liftIO $ getFreePorts 2
236339 let uri p = "http://localhost:" <> T.pack (show p) <> "/" -- trailing slash required for comparison
237340 writefile "config/common/route" $ uri p0
238− maybe id chdir mdir $ runHandle "ob" ["run"] $ \stdout -> do
239− firstUri <- handleObRunStdout httpManager stdout
341+ maybe id chdir mdir $ runHandles executable extraArgs [] $ \_stdin stdout stderr -> do
342+ firstUri <- handleObRunStdout httpManager stdout stderr
240343 let newUri = uri p1
241344 when (firstUri == newUri) $ errorExit $
242345 "Startup URI (" <> firstUri <> ") is the same as test URI (" <> newUri <> ")"
243346 maybe id (\_ -> chdir "..") mdir $ alterRouteTo newUri stdout
244− runningUri <- handleObRunStdout httpManager stdout
347+ runningUri <- handleObRunStdout httpManager stdout stderr
245348 if runningUri /= newUri
246349 then errorExit $ "Reloading failed: expected " <> newUri <> " but got " <> runningUri
247350 else exit 0
248351
249−testThunkPack :: Shelly.FilePath -> Sh ()
250−testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $ \file handle -> do
251− let pack' = readProcessWithExitCode "ob" ["thunk", "pack", T.unpack $ toTextIgnore path'] ""
352+testThunkPack :: String -> [Text] -> FilePath -> Sh ()
353+testThunkPack executable args path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $ \file handle -> do
354+ let pack' = readProcessWithExitCode executable (T.unpack <$> ["thunk", "pack", toTextIgnore path'] ++ args) ""
252355 ensureThunkPackFails q = liftIO $ pack' >>= \case
253356 (code, out, err)
254− | code == ExitSuccess -> fail "ob thunk pack succeeded when it should have failed"
357+ | code == ExitSuccess -> fail $ "ob thunk pack succeeded when it should have failed with error '" <> show q <> "'"
255358 | q `T.isInfixOf` T.pack (out <> err) -> pure ()
256− | otherwise -> fail $ "ob thunk pack failed for an unexpected reason: " <> show out <> "\nstderr: " <> err
257− git = chdir path' . run "git"
359+ | otherwise -> fail $ "ob thunk pack failed for an unexpected reason, expecting '" <> show q <> "', received: " <> show out <> "\nstderr: " <> err
360+ git = chdir path' . run gitPath
258361 -- Untracked files
259362 ensureThunkPackFails "Untracked files"
260363 void $ git ["add", T.pack file]
@@ -267,7 +370,7 @@ testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $
267370 liftIO $ T.hPutStrLn handle "test file" >> hClose handle
268371 ensureThunkPackFails "modified"
269372 -- Existing stashes
270− void $ git ["stash"]
373+ void $ git $ gitUserConfig <> [ "stash" ]
271374 ensureThunkPackFails "has stashes"
272375
273376-- | Blocks until a non-empty line is available
@@ -288,9 +391,12 @@ alterRouteTo uri stdout = do
288391 "Reloading failed: " <> T.pack (show t)
289392
290393-- | Handle stdout of `ob run`: check that the frontend and backend servers are started correctly
291−handleObRunStdout :: HTTP.Manager -> Handle -> Sh Text
292−handleObRunStdout httpManager stdout = flip fix (ObRunState_Init, []) $ \loop (state, msgs) -> do
293− liftIO (T.hGetLine stdout) >>= \t -> case state of
394+handleObRunStdout :: HTTP.Manager -> Handle -> Handle -> Sh Text
395+handleObRunStdout httpManager stdout stderr = flip fix (ObRunState_Init, []) $ \loop (state, msgs) -> do
396+ isEOF <- liftIO $ hIsEOF stdout
397+ if isEOF
398+ then handleObRunError msgs
399+ else liftIO (T.hGetLine stdout) >>= \t -> case state of
294400 ObRunState_Init
295401 | "Running test..." `T.isPrefixOf` t -> loop (ObRunState_BackendStarted, msgs)
296402 ObRunState_Startup
@@ -302,8 +408,12 @@ handleObRunStdout httpManager stdout = flip fix (ObRunState_Init, []) $ \loop (s
302408 obRunCheck httpManager stdout uri
303409 pure uri
304410 | not (T.null t) -> errorExit $ "Started: " <> t -- If theres any other output here, startup failed
305− _ | "Failed" `T.isPrefixOf` t -> errorExit $ "ob run failed: " <> T.unlines (reverse $ t : msgs)
411+ _ | "Failed" `T.isPrefixOf` t -> handleObRunError (t : msgs)
306412 | otherwise -> loop (state, t : msgs)
413+ where
414+ handleObRunError msgs = do
415+ stderrContent <- liftIO $ hGetContents stderr
416+ errorExit $ "ob run failed: " <> T.unlines (reverse msgs) <> " stderr: " <> T.pack stderrContent
307417
308418-- | Make requests to frontend/backend servers to check they are working properly
309419obRunCheck :: 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
lib/tabulation/src/Data/Tabulation.hs+1−1
@@ -8,7 +8,7 @@ import Control.Lens
88-- | This is a class for record types whose fields can be enumerated by an associated GADT. It's closely related to the concept of a representable functor, except without the functor part, and the fields are not all the same type.
99class HasFields a where
1010 type Field a :: * -> *
11−
11+
1212 fieldLens :: Field a x -> Lens' a x
1313 tabulateFieldsA :: Applicative f => (forall x. Field a x -> f x) -> f a
1414
release.nix+8−95
@@ -1,97 +1,10 @@
1−{ self-args ? {
2− config.android_sdk.accept_license = true;
3− iosSdkVersion = "10.2";
4− }
5−, local-self ? import ./. self-args
1+{
2+ supportedSystems ? [ "x86_64-linux" "x86_64-darwin" ]
63}:
4+rec {
5+ recurseForDerivations = true;
6+ build = import ./all-builds.nix { inherit supportedSystems; };
7+ test = import ./all-tests.nix { inherit supportedSystems; };
78
8−let
9− inherit (local-self.nixpkgs) lib runCommand nix;
10−
11− cacheBuildSystems = [ "x86_64-linux" "x86_64-darwin" ];
12−
13− obeliskPackagesCommon = [
14− "obelisk-frontend"
15− "obelisk-route"
16− "obelisk-executable-config-lookup"
17− ];
18−
19− obeliskPackagesBackend = obeliskPackagesCommon ++ [
20− "obelisk-asset-manifest"
21− "obelisk-asset-serve-snap"
22− "obelisk-backend"
23− "obelisk-cliapp"
24− "obelisk-command"
25− "obelisk-executable-config-inject"
26− "obelisk-frontend"
27− "obelisk-run"
28− "obelisk-route"
29− "obelisk-selftest"
30− "obelisk-snap-extras"
31− ];
32−
33− pnameToAttrs = pkgsSet: pnames:
34− lib.listToAttrs (map
35− (name: { inherit name; value = pkgsSet.${name}; })
36− pnames);
37−
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));
49−
50− 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;
91− });
92−
93− metaCache = local-self.reflex-platform.pinBuildInputs
94− "obelisk-everywhere"
95− (map (a: a.cache) (builtins.attrValues perPlatform));
96−
97−in perPlatform // { inherit metaCache; }
9+ inherit (build) metaCache;
10+}
scripts/ghcid-this-lib+1−1
@@ -1,4 +1,4 @@
11#!/usr/bin/env bash
22set -euo pipefail
33
4−nix-shell -E "with (import ../.. {}); reflex-platform.workOn haskellPackageSets.ghc haskellPackageSets.ghc.obelisk-$(basename $PWD)" --run "cabal configure ; ghcid -c'cabal repl'"
4+nix-shell -E "with (import ../.. {}); reflex-platform.workOn haskellPackageSets.ghc haskellPackageSets.ghc.obelisk-$(basename $PWD)" --run "ghcid -c 'cabal new-repl'"
skeleton/.gitignore+1
@@ -1,5 +1,6 @@
11dist-newstyle
22result
3+result-*
34result-android
45result-ios
56result-exe
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/common/common.cabal−3
@@ -9,9 +9,6 @@ library
99 , obelisk-route
1010 , mtl
1111 , text
12− default-extensions:
13− TypeFamilies
14− PolyKinds
1512 exposed-modules:
1613 Common.Api
1714 Common.Route
skeleton/common/src/Common/Route.hs+1
@@ -8,6 +8,7 @@
88{-# LANGUAGE OverloadedStrings #-}
99{-# LANGUAGE RankNTypes #-}
1010{-# LANGUAGE TemplateHaskell #-}
11+{-# LANGUAGE TypeFamilies #-}
1112module Common.Route where
1213
1314{- -- You will probably want these imports for composing Encoders.
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+22−4
@@ -1,30 +1,48 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE OverloadedStrings #-}
33{-# LANGUAGE TypeApplications #-}
4+
45module Frontend where
56
7+import Control.Monad
68import qualified Data.Text as T
79import qualified Data.Text.Encoding as T
10+import Language.Javascript.JSaddle (eval, liftJSM)
11+
812import Obelisk.Frontend
913import Obelisk.Configs
1014import Obelisk.Route
15+import Obelisk.Generated.Static
16+
1117import Reflex.Dom.Core
1218
1319import Common.Api
1420import Common.Route
15−import Obelisk.Generated.Static
1621
22+
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.
1726frontend :: Frontend (R FrontendRoute)
1827frontend = Frontend
19− { _frontend_head = el "title" $ text "Obelisk Minimal Example"
28+ { _frontend_head = do
29+ el "title" $ text "Obelisk Minimal Example"
30+ elAttr "link" ("href" =: static @"main.css" <> "type" =: "text/css" <> "rel" =: "stylesheet") blank
2031 , _frontend_body = do
21− text "Welcome to Obelisk!"
32+ el "h1" $ text "Welcome to Obelisk!"
2233 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+
2341 elAttr "img" ("src" =: static @"obelisk.jpg") blank
2442 el "div" $ do
2543 exampleConfig <- getConfig "common/example"
2644 case exampleConfig of
2745 Nothing -> text "No config file found in config/common/example"
28− Just s -> text (T.decodeUtf8 s)
46+ Just s -> text $ T.decodeUtf8 s
2947 return ()
3048 }
skeleton/static/main.cssnew file+3
@@ -0,0 +1,3 @@
1+p {
2+ color: red;
3+}