on master help sign in
Merge branch 'develop' into ts-route-maybe
commit873a153168e3b2d376dc8ad67572a3feacd7beed authorElliot Cameron <eacameron@gmail.com> dateWed 15 Jan 2020, 13:21 (7 years ago) parent261501504d9aae
59 files changed+1732−804
.github/pull_request_template.md+8 .gitignore+1 .hlint.yaml+65 CONTRIBUTING.md+5 ChangeLog.md+36−1 FAQ.md+2−2 README.md+60−15 all-builds.nix+117 all-tests.nix+145 default.nix+54−52 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/github.json+3−3 jobsets.nix−71 lib/asset/README.md+10−10 lib/asset/serve-snap/src/Obelisk/Asset/Serve/Snap.hs+7−6 lib/backend/src/Obelisk/Backend.hs+14−17 lib/cliapp/obelisk-cliapp.cabal+1 lib/cliapp/src/Obelisk/CliApp.hs+13−6 lib/cliapp/src/Obelisk/CliApp/Demo.hs+2−2 lib/cliapp/src/Obelisk/CliApp/Process.hs+86−38 lib/cliapp/src/Obelisk/CliApp/Types.hs+7−3 lib/command/obelisk-command.cabal+3−2 lib/command/src/Obelisk/App.hs+5−2 lib/command/src/Obelisk/Command.hs+56−17 lib/command/src/Obelisk/Command/Deploy.hs+29−29 lib/command/src/Obelisk/Command/Nix.hs+1−2 lib/command/src/Obelisk/Command/Project.hs+78−33 lib/command/src/Obelisk/Command/Run.hs+100−70 lib/command/src/Obelisk/Command/Thunk.hs+182−104 lib/command/src/Obelisk/Command/Utils.hs+28−14 lib/command/src/Obelisk/Command/VmBuilder.hs+2−2 lib/executable-config/default.nix+1−1 lib/executable-config/lookup/default.nix−26 lib/executable-config/lookup/obelisk-executable-config-lookup.cabal+1−1 lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig/Lookup.hs+17−8 lib/executable-config/lookup/src/Obelisk/Configs.hs+5 lib/frontend/src/Obelisk/Frontend.hs+24−16 lib/route/src/Obelisk/Route.hs+249−31 lib/route/src/Obelisk/Route/Frontend.hs+36−9 lib/run/obelisk-run.cabal+7 lib/run/src/Obelisk/Run.hs+67−25 lib/selftest/obelisk-selftest.cabal+4−2 lib/selftest/src/Obelisk/SelfTest.hs+118−63 lib/tabulation/src/Data/Tabulation.hs+1−1 release.nix+8−89 scripts/ghcid-this-lib+1−1 skeleton/.gitignore+1 skeleton/backend/src/Backend.hs+1−3 skeleton/common/common.cabal−3 skeleton/common/src/Common/Route.hs+13−18 skeleton/frontend/frontend.cabal+1 skeleton/frontend/src-bin/main.hs+1−1 skeleton/frontend/src/Frontend.hs+23−5 skeleton/shell.nix+1 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
CONTRIBUTING.md+5
@@ -5,6 +5,7 @@ Contributions and issue reports are encouraged and appreciated!
55- [Opening Issues](#opening-issues)
66- [Submitting Changes](#submitting-changes)
77 - [Guidelines for Commit Messages](#guidelines-for-commit-messages)
8+ - [Guidelines for Pull Requests](#guidelines-for-pull-requests)
89 - [Code Quality](#code-quality)
910 - [Documentation](#documentation)
1011
@@ -34,6 +35,10 @@ Commits that update a dependency should include some information about why the d
3435#### Body
3536For breaking changes, new features, refactors, or other major changes, the body of the commit message should describe the motivation behind the change in greater detail and may include references to the issue tracker. The body shouldn't repeat code/comments from the diff.
3637
38+### Guidelines for Pull Requests
39+
40+Wherever possible, pull requests should add a single feature or fix a single bug. Pull requests should not bundle several unrelated changes.
41+
3742### Code Quality
3843
3944#### Warnings
ChangeLog.md+36−1
@@ -3,13 +3,48 @@
33This project's release branch is `master`. This log is written from the perspective of the release branch: when changes hit `master`, they are considered released, and the date should reflect that release.
44
55## Unreleased
6+
7+* Add `Obelisk.Route.(?/)`, a convenience function for constructing routes nested in `Maybe`. ([#457](https://github.com/obsidiansystems/obelisk/pull/457))
8+
9+## v0.4.0.0 - 2020-01-10
10+
11+* Bump reflex-platform which, notably, bumps nixpkgs to 19.09. ([#585](https://github.com/obsidiansystems/obelisk/pull/585))
12+* 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))
13+* 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))
14+* 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))
15+* Improve error messaging when a dependency doesn't have the expected `.cabal` or `package.yaml` file. ([#597](https://github.com/obsidiansystems/obelisk/pull/597))
16+* Improve the skeleton in small ways. ([#593](https://github.com/obsidiansystems/obelisk/pull/593), [#589](https://github.com/obsidiansystems/obelisk/pull/589))
17+* Fix `ob` commands to again support running from any subdirectory of an obelisk project ([#591](https://github.com/obsidiansystems/obelisk/pull/591))
18+* 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))
19+
20+## v0.3.0.0 - 2019-12-20
21+
22+* Generalised `pathSegmentEncoder`, added `pathFieldEncoder`.
23+* Added some `Prism`s to the encoder library for manipulating `DSum`s.
24+* Add `ob doc` command, which lists paths to haddock documentation for specified packages.
25+* Bump reflex-platform so that obelisk now uses GHC 8.6.5 and the nixos-19.03 nixpkgs set.
26+* Add support in `obelisk-route` for single parameters in URL paths.
27+* Bump reflex-platform so that obelisk now uses reflex-dom 0.5.2.0.
28+* Use a `--pure` nix shell in `ob run` for parity with `ob repl` and more resilience against "works on my machine".
29+* Pin usages of `<nixpkgs>` in obelisk thunks, etc. to the nixpkgs used by the project's obelisk impl.
30+* Backport ACMEv2 support in obelisk server to regain LetsEncrypt account creation.
31+* Enable HTTPS in `ob run`.
32+* `ob run` now handles `ghci` errors better, and includes a custom `ghcid`
33+ version. As a result, you no longer need to have ghcid installed to
34+ use `ob run`, as we provide one for you.
35+* `ob` commands now complain less on systems with umasks other than `0022`.
36+* Ignore package environment files in `ob run` and `ob repl`.
37+* Add `Obelisk.Route.Frontend.routeLinkDynAttr`.
38+
39+## v0.2.0.0 - 2019-8-17
40+
641* Configs become ByteStrings.
742* FrontendConfigsT has been changed into ConfigsT and configs are made available via getConfig/getConfigs
843 * The frontend will still only have access to configs that are placed in config/frontend and config/common, while the backend has access to the entire contents of the config directory via `Obelisk.ExecutableConfig.Lookup.getConfigs`.
944* The backend no longer runs in BackendConfigsT.
1045* Add tabulation package. See Data.Tabulation for details.
1146* Add encoders for `DMap`, `HasFields` (cf. Data.Tabulation), and JSON.
12−* Add `?/`, a convenience function for constructing routes nested in `Maybe`.
47+* Use IP address for nginx proxy pass instead of localhost
1348
1449## v0.1.1.0 - 2019-05-17
1550
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
README.md+60−15
@@ -5,7 +5,9 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
55- [Installing Obelisk](#installing-obelisk)
66- [Developing an Obelisk project](#developing-an-obelisk-project)
77 - [Hoogle](#hoogle)
8+ - [Adding Packages](#adding-packages)
89 - [Adding Package Overrides](#adding-package-overrides)
10+ - [Running over https](#running-over-https)
911- [Deploying](#deploying)
1012 - [Locally](#locally)
1113 - [EC2](#ec2)
@@ -21,14 +23,15 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
21231. Set up nix caches
2224 1. If you are running NixOS, add this to `/etc/nixos/configuration.nix`:
2325 ```
24− nix.binaryCaches = [ "https://cache.nixos.org/" "https://nixcache.reflex-frp.org" ];
26+ nix.binaryCaches = [ "https://nixcache.reflex-frp.org" ];
2527 nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];
2628 ```
2729 and rebuild your NixOS configuration (e.g. `sudo nixos-rebuild switch`).
2830 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)):
2931 ```
30− substituters = https://cache.nixos.org https://nixcache.reflex-frp.org
31− trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
32+ binary-caches = https://cache.nixos.org https://nixcache.reflex-frp.org
33+ binary-cache-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
34+ binary-caches-parallel-connections = 40
3235 ```
3336 * 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)
3437 ```
@@ -59,6 +62,28 @@ Or to launch ghcid for `lib/command` project:
5962nix-shell -A obeliskEnvs.obelisk-command --run "cd lib/command && ghcid -c 'cabal new-repl'"
6063```
6164
65+To re-install `ob` from source do
66+```
67+nix-env -f /path/to/obelisk -iA command
68+```
69+
70+Note that `ob` will defer to the version found in your project's `.obelisk/impl` directory. To update that version specifically:
71+
72+```shell
73+ob thunk unpack ./.obelisk/impl
74+cd ./.obelisk/impl
75+# apply your changes
76+```
77+
78+If you want to commit your changes, first push them to your fork of obelisk and then
79+
80+```shell
81+cd /your/project/root
82+ob thunk pack .obelisk/impl
83+git add .obelisk/impl
84+git commit -m "Bump obelisk"
85+```
86+
6287### Accessing private repositories
6388
6489To allow the Nix builder to access private git repositories, you must be set up
@@ -82,7 +107,8 @@ Obelisk leverages ghcid to provide a live-reloading server that handles both fro
82107ob run
83108```
84109
85−Now go to http://localhost:8000 (or the address/port specified in `config/common/route`) to access your app.
110+Now, with an appropriate browser, go to http://localhost:8000 (or the address/port specified in `config/common/route`) to access your app.
111+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.
86112
87113Every 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.
88114
@@ -92,16 +118,20 @@ To enter a nix-shell from which you can run the hoogle command-line client or a
92118
93119`nix-shell -A shells.ghc --arg withHoogle true`
94120
121+### Adding packages
122+
123+In 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.
124+
95125### Adding package overrides
96126
97−To add a version override to any Haskell package, or to add a Haskell package that doesn't exist in the nixpkgs used by Obelisk, use the `overrides` attribute in your project's `default.nix`. For example, to use a specific version of the `aeson` package, your `default.nix` will look like:
127+To add a version override to any Haskell package, or to add a Haskell package that doesn't exist in the nixpkgs used by Obelisk, use the `overrides` attribute in your project's `default.nix`. For example, to use a specific version of the `aeson` package fetched from GitHub and a specific version of the `waargonaut` package fetched from Hackage, your `default.nix` will look like:
98128
99129```nix
100130# ...
101131project ./. ({ pkgs, ... }: {
102132# ...
103133 overrides = self: super: let
104− aeson = pkgs.fetchFromGitHub {
134+ aesonSrc = pkgs.fetchFromGitHub {
105135 owner = "obsidiansystems";
106136 repo = "aeson-gadt-th";
107137 rev = "ed573c2cccf54d72aa6279026752a3fecf9c1383";
@@ -109,13 +139,24 @@ project ./. ({ pkgs, ... }: {
109139 };
110140 in
111141 {
112− aeson = self.callCabal2nix "aeson" aeson {};
142+ aeson = self.callCabal2nix "aeson" aesonSrc {};
143+ waargonaut = self.callHackageDirect {
144+ pkg = "waargonaut";
145+ ver = "0.8.0.1";
146+ sha256 = "1zv28np3k3hg378vqm89v802xr0g8cwk7gy3mr77xrzy5jbgpa39";
147+ } {};
113148 };
114149# ...
115150```
116151
117152For further information see [the Haskell section](https://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure) of nixpkgs Contributors Guide.
118153
154+### Running over https
155+
156+To run your app locally over https, update the protocol in `config/common/route` to `https`, and then use `ob run` as normal.
157+
158+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.
159+
119160## Deploying
120161
121162### Locally
@@ -123,14 +164,16 @@ For further information see [the Haskell section](https://nixos.org/nixpkgs/manu
123164Build everything:
124165
125166```
126−nix-build -A exe -o result-exe
167+nix-build -A exe --no-out-link
127168```
128169
129−Run the server:
170+Copy the result to a new directory, add configuration, and run!
130171
131172```
132−cd result-exe
133−./backend
173+mkdir test-app
174+ln -s $(nix-build -A exe --no-out-link)/* test-app/
175+cp -r config test-app
176+(cd test-app && ./backend)
134177```
135178
136179### EC2
@@ -162,9 +205,11 @@ ob deploy init \
162205 ~/code/myapp-deploy
163206```
164207
165−NOTE: HTTPS is enabled by default; to disable https, pass `--disable-https` to the `ob deploy init` command above.
208+HTTPS is enabled by default; to disable https, pass `--disable-https` to the `ob deploy init` command above.
209+
210+This step will also require that you manually verify the authenticity of the host `$SERVER`. Obelisk will save the fingerprint in a deployment-specific configuration. **Obelisk deployments do *not* rely on the `known_hosts` of your local machine.** This is because, in the event that you need to switch from one deploy machine / bastion host to another, you want to be absolutely sure that you're still connecting to the machines you think you are, even if that deploy machine / bastion host has never connected to them before. Obelisk explicitly avoids a workflow that encourages people to accept host keys without checking them, since that could result in leaking production secrets to anyone who manages to MITM you, e.g. via DNS spoofing or cache poisoning. (Note that an active attack is a circumstance where you may need to quickly switch bastion hosts, e.g. because the attacker has taken one down or you have taken it down in case it was compromised. In this circumstance you might need to deploy to production to fix an exploit or rotate keys, etc.) When you run `ob deploy` later it will rely on the saved verification in this step.
166211
167−Then go to that created deployment configuration directory, and initiate the deployment:
212+Next, go to the deployment directory that you just initialized and deploy!
168213
169214```
170215cd ~/code/myapp-deploy
@@ -290,9 +335,9 @@ This command will accomplish the following:
2903351. Build a Signed Android apk for your application
2913361. Deploy the Signed apk to your connected Android device
292337
293−In the event that you change your key or keystore password, you will have to update your credentials within the JSON object found in `android_keytool_config.json`
338+In the event that you change your key or keystore password, you will have to update your credentials within the JSON object found in `android_keytool_config.json`.
294339
295−Additional documentation on java key stores can be found [here] (https://docs.oracle.com/javase/8/docs/technotes/tools/unix/keytool.html)
340+Additional documentation on Java key stores can be found [here](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/keytool.html).
296341
297342This should copy over and install the application on your device (if you see a "*signatures do not match*" error, simply uninstall the previous app from the device before retrying the deploy). The name of the installed application will be what you have specified for `android.displayName` in the `default.nix`.
298343
all-builds.nixnew file+117
@@ -0,0 +1,117 @@
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+ concatDepends = let
42+ extractDeps = x: (x.override {
43+ mkDerivation = drv: {
44+ out = builtins.concatLists [
45+ (drv.buildDepends or [])
46+ (drv.libraryHaskellDepends or [])
47+ (drv.executableHaskellDepends or [])
48+ ];
49+ };
50+ }).out;
51+ in pkgAttrs: builtins.concatLists (map extractDeps (builtins.attrValues pkgAttrs));
52+
53+ perPlatform = lib.genAttrs cacheBuildSystems (system: let
54+ reflex-platform = import ./dep/reflex-platform { inherit system; };
55+ perProfiling = profiling: let
56+ obelisk = import ./. (self-args // { inherit system profiling; });
57+ ghc = pnameToAttrs
58+ obelisk.haskellPackageSets.ghc
59+ obeliskPackagesBackend;
60+ ghcjs = pnameToAttrs
61+ obelisk.haskellPackageSets.ghcjs
62+ obeliskPackagesCommon;
63+ cachePackages = builtins.concatLists [
64+ (builtins.attrValues ghc)
65+ (lib.optionals (!profiling) (builtins.attrValues ghcjs))
66+ (concatDepends ghc)
67+ (lib.optionals (!profiling) (concatDepends ghcjs))
68+ (lib.optional reflex-platform.androidSupport androidSkeleton)
69+ (lib.optional reflex-platform.iosSupport iosSkeleton)
70+ (lib.optionals (!profiling) [
71+ command
72+ serverSkeletonExe
73+ serverSkeletonShell
74+ ])
75+ ];
76+ command = obelisk.command;
77+ skeleton = import ./skeleton { inherit obelisk; };
78+ serverSkeletonExe = skeleton.exe;
79+ # TODO fix nixpkgs so it doesn't try to run the result of haskell shells as setup hooks.
80+ serverSkeletonShell = local-self.nixpkgs.runCommand "shell-safe-for-dep" {} ''
81+ touch "$out"
82+ echo "return" >> "$out"
83+ cat "${skeleton.shells.ghc}" >> "$out"
84+ '';
85+ androidSkeleton = (import ./skeleton { inherit obelisk; }).android.frontend;
86+ iosSkeleton = (import ./skeleton { inherit obelisk; }).ios.frontend;
87+ nameSuffix = if profiling then "profiled" else "unprofiled";
88+ in {
89+ inherit
90+ ghc
91+ ;
92+ cache = reflex-platform.pinBuildInputs "obelisk-${system}-${nameSuffix}" cachePackages;
93+ } // lib.optionalAttrs (!profiling) {
94+ inherit
95+ command
96+ ghcjs
97+ serverSkeletonExe
98+ serverSkeletonShell
99+ ;
100+ } // lib.optionalAttrs reflex-platform.androidSupport {
101+ inherit androidSkeleton;
102+ } // lib.optionalAttrs reflex-platform.iosSupport {
103+ inherit iosSkeleton;
104+ };
105+ profiled = perProfiling true;
106+ unprofiled = perProfiling false;
107+ cachePackages = map (p: p.cache) [ profiled unprofiled ];
108+ in {
109+ inherit profiled unprofiled;
110+ cache = reflex-platform.pinBuildInputs "obelisk-${system}" cachePackages;
111+ });
112+
113+ metaCache = local-self.reflex-platform.pinBuildInputs
114+ "obelisk-everywhere"
115+ (map (a: a.cache) (builtins.attrValues perPlatform));
116+
117+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": false' ~/code/myapp/git.json""")
143+ client.succeed("ob -v thunk unpack ~/code/myapp")
144+ '';
145+ }) {}
default.nix+54−52
@@ -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;
@@ -32,13 +33,12 @@ let
3233 # Fix misc upstream packages
3334 (self: super: let
3435 pkgs = self.callPackage ({ pkgs }: pkgs) {};
36+ haskellLib = pkgs.haskell.lib;
3537 in {
36− hnix = pkgs.haskell.lib.dontCheck (self.callCabal2nix "hnix" (pkgs.fetchFromGitHub {
37− owner = "haskell-nix";
38− repo = "hnix";
39− rev = "42afdc21da5d9e076eab57eaa42bfdde938192b8";
40− sha256 = "0psw384dx9bw2dp93xrzw8rd9amvcwgzn64jzzwby7sfspj6k349";
41− }) {});
38+ hnix = haskellLib.dontCheck (haskellLib.doJailbreak (self.callCabal2nix "hnix" (hackGet dep/hnix) {}));
39+ 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,33 @@ 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
52− obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (hackGet ./lib/asset + "/manifest") {};
53− obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (hackGet ./lib/asset + "/serve-snap") {};
55+ obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (cleanSource ./lib/asset/manifest) {};
56+ 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+ ];
75+ };
6176 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
6277 tabulation = self.callCabal2nix "tabulation" (cleanSource ./lib/tabulation) {};
6378 })
@@ -65,31 +80,11 @@ let
6580 (self: super: let
6681 pkgs = self.callPackage ({ pkgs }: pkgs) {};
6782 haskellLib = pkgs.haskell.lib;
68− #TODO: Upstream
69− # Modify a Haskell package to add completion scripts for the given
70− # executable produced by it. These completion scripts will be picked up
71− # automatically if the resulting derivation is installed, e.g. by
72− # `nix-env -i`.
73− addOptparseApplicativeCompletionScripts = exeName: pkg: haskellLib.overrideCabal pkg (drv: {
74− postInstall = (drv.postInstall or "") + ''
75− BASH_COMP_DIR="$out/share/bash-completion/completions"
76− mkdir -p "$BASH_COMP_DIR"
77− "$out/bin/${exeName}" --bash-completion-script "$out/bin/${exeName}" >"$BASH_COMP_DIR/ob"
78−
79− ZSH_COMP_DIR="$out/share/zsh/vendor-completions"
80− mkdir -p "$ZSH_COMP_DIR"
81− "$out/bin/${exeName}" --zsh-completion-script "$out/bin/${exeName}" >"$ZSH_COMP_DIR/_ob"
82−
83− FISH_COMP_DIR="$out/share/fish/vendor_completions.d"
84− mkdir -p "$FISH_COMP_DIR"
85− "$out/bin/${exeName}" --fish-completion-script "$out/bin/${exeName}" >"$FISH_COMP_DIR/ob.fish"
86− '';
87− });
8883 in {
8984 # Dynamic linking with split objects dramatically increases startup time (about
9085 # 0.5 seconds on a decent machine with SSD), so we do `justStaticExecutables`.
9186 obelisk-command = haskellLib.overrideCabal
92− (addOptparseApplicativeCompletionScripts "ob"
87+ (haskellLib.generateOptparseApplicativeCompletion "ob"
9388 (haskellLib.justStaticExecutables super.obelisk-command))
9489 (drv: {
9590 buildTools = (drv.buildTools or []) ++ [ pkgs.buildPackages.makeWrapper ];
@@ -126,19 +121,19 @@ in rec {
126121 pathGit = ./.; # Used in CI by the migration graph hash algorithm to correctly ignore files.
127122 path = reflex-platform.filterGit ./.;
128123 obelisk = ghcObelisk;
129− obeliskEnvs = ghcObeliskEnvs;
124+ obeliskEnvs = pkgs.lib.filterAttrs (k: _: pkgs.lib.strings.hasPrefix "obelisk-" k) ghcObeliskEnvs;
130125 command = ghcObelisk.obelisk-command;
131− shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs) [];
126+ shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs);
132127
133128 selftest = pkgs.writeScript "selftest" ''
134129 #!/usr/bin/env bash
135130 set -euo pipefail
136131
137132 PATH="${command}/bin:$PATH"
138− export OBELISK_IMPL="${hackGet ./.}"
133+ export OBELISK_IMPL="$(mktemp -d)"
134+ git clone file://${pathGit} $OBELISK_IMPL
139135 "${ghcObelisk.obelisk-selftest}/bin/obelisk-selftest" +RTS -N -RTS "$@"
140136 '';
141− #TODO: Why can't I build ./skeleton directly as a derivation? `nix-build -E ./.` doesn't work
142137 skeleton = pkgs.runCommand "skeleton" {
143138 dir = builtins.filterSource (path: type: builtins.trace path (baseNameOf path != ".obelisk")) ./skeleton;
144139 } ''
@@ -157,12 +152,17 @@ in rec {
157152 obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
158153 '';
159154
160− compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" { buildInputs = [ pkgs.closurecompiler ]; } ''
155+ compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
161156 mkdir $out
162157 cd $out
158+ # TODO profiling + static shouldn't break and need an ad-hoc workaround like that
163159 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
164− closure-compiler --externs "${reflex-platform.ghcjsExternsJs}" -O ${optimizationLevel} --jscomp_warning=checkVars --create_source_map="all.js.map" --source_map_format=V3 --js_output_file="all.js" all.unminified.js
165− echo "//# sourceMappingURL=all.js.map" >> all.js
160+ ${if optimizationLevel == null then ''
161+ ln -s all.unminified.js all.js
162+ '' else ''
163+ ${pkgs.closurecompiler}/bin/closure-compiler --externs "${reflex-platform.ghcjsExternsJs}" -O ${optimizationLevel} --jscomp_warning=checkVars --create_source_map="all.js.map" --source_map_format=V3 --js_output_file="all.js" all.unminified.js
164+ echo "//# sourceMappingURL=all.js.map" >> all.js
165+ ''}
166166 '';
167167
168168 serverModules = {
@@ -198,7 +198,7 @@ in rec {
198198 enableACME = enableHttps;
199199 forceSSL = enableHttps;
200200 locations.${baseUrl} = {
201− proxyPass = "http://localhost:" + toString internalPort;
201+ proxyPass = "http://127.0.0.1:" + toString internalPort;
202202 proxyWebsockets = true;
203203 };
204204 };
@@ -207,10 +207,11 @@ in rec {
207207 wantedBy = [ "multi-user.target" ];
208208 after = [ "network.target" ];
209209 restartIfChanged = true;
210+ path = [ pkgs.gnutar ];
210211 script = ''
211212 ln -sft . '${exe}'/*
212213 mkdir -p log
213− exec ./backend ${backendArgs} >>backend.out 2>>backend.err </dev/null
214+ exec ./backend ${backendArgs} </dev/null
214215 '';
215216 serviceConfig = {
216217 User = user;
@@ -237,7 +238,7 @@ in rec {
237238 pkgs.runCommand "serverExe" {} ''
238239 mkdir $out
239240 set -eux
240− ln -s "${haskellLib.justStaticExecutables backend}"/bin/* $out/
241+ ln -s "${if profiling then backend else haskellLib.justStaticExecutables backend}"/bin/* $out/
241242 ln -s "${mkAssets assets}" $out/static.assets
242243 ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
243244 echo ${version} > $out/version
@@ -268,7 +269,7 @@ in rec {
268269 , tools ? _: []
269270 , shellToolOverrides ? _: _: {}
270271 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
271− , __closureCompilerOptimizationLevel ? "ADVANCED"
272+ , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
272273 }:
273274 let frontendName = "frontend";
274275 backendName = "backend";
@@ -335,18 +336,19 @@ in rec {
335336 passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __iosWithConfig __androidWithConfig; };
336337 };
337338 in mkProject (projectDefinition args));
338− serverOn = sys: version: serverExe
339− (projectOut sys).ghc.backend
340− (projectOut system).ghcjs.frontend
341− (projectOut sys).passthru.staticFiles
342− (projectOut sys).passthru.__closureCompilerOptimizationLevel
339+ mainProjectOut = projectOut system;
340+ serverOn = projectInst: version: serverExe
341+ projectInst.ghc.backend
342+ mainProjectOut.ghcjs.frontend
343+ projectInst.passthru.staticFiles
344+ projectInst.passthru.__closureCompilerOptimizationLevel
343345 version;
344− linuxExe = serverOn "x86_64-linux";
346+ linuxExe = serverOn (projectOut "x86_64-linux");
345347 dummyVersion = "Version number is only available for deployments";
346− in projectOut system // {
348+ in mainProjectOut // {
347349 linuxExeConfigurable = linuxExe;
348350 linuxExe = linuxExe dummyVersion;
349− exe = serverOn system dummyVersion;
351+ exe = serverOn mainProjectOut dummyVersion;
350352 server = args@{ hostName, adminEmail, routeHost, enableHttps, version }:
351353 server (args // { exe = linuxExe version; });
352354 obelisk = import (base + "/.obelisk/impl") {};
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/github.json+3−3
@@ -1,7 +1,7 @@
11{
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
4− "branch": "cg-bump-reflex-dom",
5− "rev": "7ecb4c255bb053a7a4796ffd6331cdbe210de91c",
6− "sha256": "1mngxa24cfpvxxq4hgh77nw36vny4abl5wi2xmlwpkk25wzm0h0x"
4+ "branch": "develop",
5+ "rev": "90351bdb2c99d416ea95fd5b8b474e8f9fab2942",
6+ "sha256": "0bbzz4ckdx0ilhhpd3drfbsc0zdb2rnhw2m3nls9villbs3lz3lh"
77}
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/Serve/Snap.hs+7−6
@@ -19,6 +19,7 @@ import Control.Applicative ((<|>))
1919import Control.Exception (handleJust, try, throwIO)
2020import Control.Monad (forM, liftM)
2121import Control.Monad.IO.Class (liftIO)
22+import Control.Monad.Fail (MonadFail)
2223import Data.Attoparsec.ByteString (parseOnly, endOfInput)
2324import Data.ByteString (ByteString)
2425import qualified Data.ByteString as BS
@@ -38,7 +39,7 @@ import System.Posix (getFileStatus, fileSize)
3839-- current version of the asset
3940-- (e.g. @0rx5yvkkgkig2pcqf4ngi1l7vh89qqajdzc6aiayaibyhvj0d853-test.png@). Hashed
4041-- asset names will be sent with permanent caching headers.
41−serveAssets :: MonadSnap m => FilePath -> FilePath -> m ()
42+serveAssets :: (MonadSnap m, MonadFail m) => FilePath -> FilePath -> m ()
4243serveAssets = serveAssets' True
4344
4445-- | Serve static assets from an asset directory generated via @assets.nix@ or, failing that, from a regular directory.
@@ -47,15 +48,15 @@ serveAssets = serveAssets' True
4748-- browser for the logical asset name (e.g. @test.png@) but instead the current
4849-- version of the asset with to point the browser at the unique filename for the
4950-- current version of the asset.
50−serveAssetsInPlace :: MonadSnap m => FilePath -> FilePath -> m ()
51+serveAssetsInPlace :: (MonadSnap m, MonadFail m) => FilePath -> FilePath -> m ()
5152serveAssetsInPlace = serveAssets' False
5253
5354-- | Like 'serveAssets', but only serves a single specified asset
54−serveAsset :: MonadSnap m => FilePath -> FilePath -> FilePath -> m ()
55+serveAsset :: (MonadSnap m, MonadFail m) => FilePath -> FilePath -> FilePath -> m ()
5556serveAsset = serveAsset' True
5657
5758-- | Like 'serveAssetsInPlace', but only serves a single specified asset
58−serveAssetInPlace :: MonadSnap m => FilePath -> FilePath -> FilePath -> m ()
59+serveAssetInPlace :: (MonadSnap m, MonadFail m) => FilePath -> FilePath -> FilePath -> m ()
5960serveAssetInPlace = serveAsset' False
6061
6162-- | Serve static assets from an asset directory generated via @assets.nix@ or, failing that, from a regular directory.
@@ -63,13 +64,13 @@ serveAssetInPlace = serveAsset' False
6364-- For assets generated from @assets.nix@, the @Bool@ argument @doRedirect@ controls whether redirects will be sent to the browser if a request is made for
6465-- an unhashed asset name, e.g. @test.png@. For @True@, a redirect will be sent, yielding more round trips to the server but better caching behavior if the
6566-- asset doesn't change often. Conversely for @False@, the asset will be served "in place" but made uncacheable.
66−serveAssets' :: MonadSnap m => Bool -> FilePath -> FilePath -> m ()
67+serveAssets' :: (MonadSnap m, MonadFail m) => Bool -> FilePath -> FilePath -> m ()
6768serveAssets' doRedirect base fallback = do
6869 pRaw <- getSafePath
6970 serveAsset' doRedirect base fallback $ if "/" `isSuffixOf` pRaw || pRaw == "" then pRaw <> "index.html" else pRaw
7071
7172-- | Serve a single static asset from an asset directory generated via @assets.nix@ or, failing that, from a regular directory.
72−serveAsset' :: MonadSnap m => Bool -> FilePath -> FilePath -> FilePath -> m ()
73+serveAsset' :: (MonadFail m, MonadSnap m) => Bool -> FilePath -> FilePath -> FilePath -> m ()
7374serveAsset' doRedirect base fallback p = do
7475 assetType <- liftIO $ try $ BS.readFile $ base </> p </> "type"
7576 case assetType of
lib/backend/src/Obelisk/Backend.hs+14−17
@@ -26,12 +26,12 @@ import Prelude hiding (id, (.))
2626import Control.Category
2727import Control.Monad
2828import Control.Monad.Except
29+import Control.Monad.Fail (MonadFail)
2930import Control.Categorical.Bifunctor
3031import Data.ByteString (ByteString)
3132import qualified Data.ByteString.Char8 as BSC8
3233import Data.Default (Default (..))
3334import Data.Dependent.Sum
34−import Data.Functor.Sum
3535import Data.Functor.Identity
3636import Data.Map (Map)
3737import qualified Data.Map as Map
@@ -47,12 +47,12 @@ import Obelisk.Snap.Extras (doNotCache, serveFileIfExistsAs)
4747import Reflex.Dom
4848import Snap (MonadSnap, Snap, commandLineConfig, defaultConfig, getsRequest, httpServe, modifyResponse
4949 , rqPathInfo, rqQueryString, setContentType, writeBS, writeText
50− , rqCookies, Cookie(..))
50+ , rqCookies, Cookie(..) , setHeader)
5151import Snap.Internal.Http.Server.Config (Config (accessLog, errorLog), ConfigLog (ConfigIoLog))
5252import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
5353
5454data Backend backendRoute frontendRoute = Backend
55− { _backend_routeEncoder :: Encoder (Either Text) Identity (R (Sum backendRoute (ObeliskRoute frontendRoute))) PageName
55+ { _backend_routeEncoder :: Encoder (Either Text) Identity (R (FullRoute backendRoute frontendRoute)) PageName
5656 , _backend_run :: ((R backendRoute -> Snap ()) -> IO ()) -> IO ()
5757 }
5858
@@ -65,7 +65,7 @@ data GhcjsApp route = GhcjsApp
6565-- | Serve a frontend, which must be the same frontend that Obelisk has built and placed in the default location
6666--TODO: The frontend should be provided together with the asset paths so that this isn't so easily breakable; that will probably make this function obsolete
6767serveDefaultObeliskApp
68− :: (MonadSnap m, HasCookies m)
68+ :: (MonadSnap m, HasCookies m, MonadFail m)
6969 => (R appRoute
7070 -> Text)
7171 -> ([Text]
@@ -114,11 +114,7 @@ getPageName :: (MonadSnap m) => m PageName
114114getPageName = do
115115 p <- getsRequest rqPathInfo
116116 q <- getsRequest rqQueryString
117− let pageNameEncoder' :: Encoder Identity Identity PageName (String, String)
118− pageNameEncoder' = bimap
119− (unpackTextEncoder . pathSegmentsTextEncoder . listToNonEmptyEncoder)
120− (unpackTextEncoder . queryParametersTextEncoder . toListMapEncoder)
121− return $ decode pageNameEncoder' (T.unpack (decodeUtf8 p), T.unpack (decodeUtf8 q))
117+ return $ byteStringsToPageName p q
122118
123119getRouteWith :: (MonadSnap m) => Encoder Identity parse route PageName -> m (parse route)
124120getRouteWith e = do
@@ -126,7 +122,7 @@ getRouteWith e = do
126122 return $ tryDecode e pageName
127123
128124serveObeliskApp
129− :: (MonadSnap m, HasCookies m)
125+ :: (MonadSnap m, HasCookies m, MonadFail m)
130126 => (R appRoute -> Text)
131127 -> ([Text] -> m ())
132128 -> GhcjsApp (R appRoute)
@@ -144,7 +140,7 @@ serveObeliskApp urlEnc serveStaticAsset frontendApp config = \case
144140 writeText msg
145141 ResourceRoute_Version :=> Identity () -> doNotCache >> serveFileIfExistsAs "text/plain" "version"
146142
147−serveStaticAssets :: MonadSnap m => StaticAssets -> [Text] -> m ()
143+serveStaticAssets :: (MonadSnap m, MonadFail m) => StaticAssets -> [Text] -> m ()
148144serveStaticAssets assets pathSegments = serveAsset (_staticAssets_processed assets) (_staticAssets_unprocessed assets) $ T.unpack $ T.intercalate "/" pathSegments
149145
150146data StaticAssets = StaticAssets
@@ -162,7 +158,7 @@ staticRenderContentType = "text/html; charset=utf-8"
162158
163159--TODO: Don't assume we're being served at "/"
164160serveGhcjsApp
165− :: (MonadSnap m, HasCookies m)
161+ :: (MonadSnap m, HasCookies m, MonadFail m)
166162 => (R appRouteComponent -> Text)
167163 -> GhcjsApp (R appRouteComponent)
168164 -> Map Text ByteString
@@ -171,10 +167,11 @@ serveGhcjsApp
171167serveGhcjsApp urlEnc app config = \case
172168 GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest -> do
173169 modifyResponse $ setContentType staticRenderContentType
170+ modifyResponse $ setHeader "Cache-Control" "no-store private"
174171 writeBS <=< renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) config $ _ghcjsApp_value app
175172 GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
176173
177−runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
174+runBackend :: Backend backendRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
178175runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend of
179176 Left e -> fail $ "backend error:\n" <> T.unpack e
180177 Right validFullEncoder -> do
@@ -183,15 +180,15 @@ runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend
183180 runSnapWithCommandLineArgs $ do
184181 getRouteWith validFullEncoder >>= \case
185182 Identity r -> case r of
186− InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
187− InR obeliskRoute :=> Identity a ->
183+ FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
184+ FullRoute_Frontend obeliskRoute :/ a ->
188185 serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend publicConfigs $
189186 obeliskRoute :/ a
190187
191−mkRouteToUrl :: Encoder Identity parse (R (Sum f (ObeliskRoute r))) PageName -> R r -> Text
188+mkRouteToUrl :: Encoder Identity parse (R (FullRoute br fr)) PageName -> R fr -> Text
192189mkRouteToUrl validFullEncoder =
193190 let pageNameEncoder' :: Encoder Identity (Either Text) PageName PathQuery = pageNameEncoder
194− in \(k :/ v) -> T.pack . uncurry (<>) . encode pageNameEncoder' . encode validFullEncoder $ (InR $ ObeliskRoute_App k) :/ v
191+ in \(k :/ v) -> T.pack . uncurry (<>) . encode pageNameEncoder' . encode validFullEncoder $ (FullRoute_Frontend $ ObeliskRoute_App k) :/ v
195192
196193renderGhcjsFrontend
197194 :: (MonadSnap m, HasCookies m)
lib/cliapp/obelisk-cliapp.cabal+1
@@ -10,6 +10,7 @@ library
1010 ansi-terminal
1111 , base
1212 , bytestring
13+ , containers
1314 , exceptions
1415 , io-streams
1516 , lens
lib/cliapp/src/Obelisk/CliApp.hs+13−6
@@ -33,16 +33,23 @@ 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
4548 , reconstructCommand
49+ , setCwd
50+ , setDelegateCtlc
51+ , setEnvOverride
52+ , shell
4653 ) where
4754
4855import Control.Monad.Log (Severity (..))
lib/cliapp/src/Obelisk/CliApp/Demo.hs+2−2
@@ -4,17 +4,17 @@
44module Obelisk.CliApp.Demo where
55
66import Control.Concurrent (threadDelay)
7+import Control.Monad.Fail
78import Control.Monad.IO.Class
89import Data.Semigroup ((<>))
910import qualified Data.Text as T
10−import System.Process (proc)
1111
1212import Control.Monad.Catch (MonadMask)
1313
1414import Obelisk.CliApp
1515
1616cliDemo
17− :: ( MonadIO m, MonadMask m
17+ :: ( MonadIO m, MonadMask m, MonadFail m
1818 , CliLog m, HasCliConfig e m, CliThrow e m, AsProcessFailure e, AsUnstructuredError e)
1919 => m ()
2020cliDemo = withSpinner "CLI Demo" $ do
lib/cliapp/src/Obelisk/CliApp/Process.hs+86−38
@@ -9,44 +9,78 @@
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
2225 , reconstructCommand
26+ , setCwd
27+ , setDelegateCtlc
28+ , setEnvOverride
29+ , shell
2330 ) where
2431
2532import Control.Monad ((<=<), join, void)
2633import Control.Monad.Except (throwError)
34+import Control.Monad.Fail
2735import Control.Monad.IO.Class (MonadIO, liftIO)
2836import Control.Lens (Prism', review)
2937import qualified Data.ByteString as BS
3038import qualified Data.ByteString.Char8 as BSC
3139import Data.Function (fix)
40+import Data.Map (Map)
41+import qualified Data.Map as Map
3242import Data.Monoid ((<>))
3343import Data.Text (Text)
3444import qualified Data.Text as T
3545import qualified Data.Text.Encoding as T
3646import Data.Text.Encoding.Error (lenientDecode)
47+import System.Environment (getEnvironment)
3748import System.Exit (ExitCode (..))
3849import System.IO (Handle)
3950import System.IO.Streams (InputStream, handleToInputStream)
4051import qualified System.IO.Streams as Streams
4152import System.IO.Streams.Concurrent (concurrentMerge)
42−import System.Process (CreateProcess, ProcessHandle, StdStream (CreatePipe), cmdspec, std_err, std_out,
43− waitForProcess)
53+import System.Process (CreateProcess, ProcessHandle, StdStream (CreatePipe), std_err, std_out, waitForProcess)
4454import qualified System.Process as Process
4555
4656import Control.Monad.Log (Severity (..))
4757import Obelisk.CliApp.Logging (putLog, putLogRaw)
4858import Obelisk.CliApp.Types (CliLog, CliThrow)
4959
60+data ProcessSpec = ProcessSpec
61+ { _processSpec_createProcess :: !CreateProcess
62+ , _processSpec_overrideEnv :: !(Maybe (Map String String -> Map String String))
63+ }
64+
65+proc :: FilePath -> [String] -> ProcessSpec
66+proc cmd args = ProcessSpec (Process.proc cmd args) Nothing
67+
68+shell :: String -> ProcessSpec
69+shell cmd = ProcessSpec (Process.shell cmd) Nothing
70+
71+setEnvOverride :: (Map String String -> Map String String) -> ProcessSpec -> ProcessSpec
72+setEnvOverride f p = p { _processSpec_overrideEnv = Just f }
73+
74+overCreateProcess :: (CreateProcess -> CreateProcess) -> ProcessSpec -> ProcessSpec
75+overCreateProcess f (ProcessSpec p x) = ProcessSpec (f p) x
76+
77+setDelegateCtlc :: Bool -> ProcessSpec -> ProcessSpec
78+setDelegateCtlc b = overCreateProcess (\p -> p { Process.delegate_ctlc = b })
79+
80+setCwd :: Maybe FilePath -> ProcessSpec -> ProcessSpec
81+setCwd fp = overCreateProcess (\p -> p { Process.cwd = fp })
82+
83+
5084-- TODO put back in `Obelisk.CliApp.Process` and use prisms for extensible exceptions
5185data ProcessFailure = ProcessFailure Process.CmdSpec Int -- exit code
5286 deriving Show
@@ -60,8 +94,8 @@ instance AsProcessFailure ProcessFailure where
6094 asProcessFailure = id
6195
6296readProcessAndLogStderr
63− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
64− => Severity -> CreateProcess -> m Text
97+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
98+ => Severity -> ProcessSpec -> m Text
6599readProcessAndLogStderr sev process = do
66100 (out, _err) <- withProcess process $ \_out err -> do
67101 streamToLog =<< liftIO (streamHandle sev err)
@@ -69,9 +103,10 @@ readProcessAndLogStderr sev process = do
69103
70104readCreateProcessWithExitCode
71105 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
72− => CreateProcess -> m (ExitCode, String, String)
73−readCreateProcessWithExitCode process = do
74− putLog Debug $ "Creating process: " <> reconstructCommand (cmdspec process)
106+ => ProcessSpec -> m (ExitCode, String, String)
107+readCreateProcessWithExitCode procSpec = do
108+ process <- mkCreateProcess procSpec
109+ putLog Debug $ "Creating process: " <> reconstructProcSpec procSpec
75110 liftIO $ Process.readCreateProcessWithExitCode process ""
76111
77112-- | Like `System.Process.readProcess` but logs the combined output (stdout and stderr)
@@ -82,11 +117,11 @@ readCreateProcessWithExitCode process = do
82117-- which case it is advisable to call it with a non-Error severity for stderr, like
83118-- `callProcessAndLogOutput (Debug, Debug)`.
84119readProcessAndLogOutput
85− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
86− => (Severity, Severity) -> CreateProcess -> m Text
120+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
121+ => (Severity, Severity) -> ProcessSpec -> m Text
87122readProcessAndLogOutput (sev_out, sev_err) process = do
88− (_, Just out, Just err, p) <- createProcess $ process
89− { std_out = CreatePipe , std_err = CreatePipe }
123+ (_, Just out, Just err, p) <- createProcess $ overCreateProcess
124+ (\p -> p { std_out = CreatePipe , std_err = CreatePipe }) process
90125
91126 -- TODO interleave stdout and stderr in log correctly
92127 streamToLog =<< liftIO (streamHandle sev_err err)
@@ -95,9 +130,9 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
95130
96131 liftIO (waitForProcess p) >>= \case
97132 ExitSuccess -> pure outText
98− ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (cmdspec process) code
133+ ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess process) code
99134
100−-- | Like `System.Process.callProcess` but logs the combined output (stdout and stderr)
135+-- | Like 'System.Process.callProcess' but logs the combined output (stdout and stderr)
101136-- with the corresponding severity.
102137--
103138-- Usually this function is called as `callProcessAndLogOutput (Debug, Error)`. However
@@ -105,9 +140,8 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
105140-- which case it is advisable to call it with a non-Error severity for stderr, like
106141-- `callProcessAndLogOutput (Debug, Debug)`.
107142callProcessAndLogOutput
108−
109− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
110− => (Severity, Severity) -> CreateProcess -> m ()
143+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
144+ => (Severity, Severity) -> ProcessSpec -> m ()
111145callProcessAndLogOutput (sev_out, sev_err) process =
112146 void $ withProcess process $ \out err -> do
113147 stream <- liftIO $ join $ combineStream
@@ -117,21 +151,31 @@ callProcessAndLogOutput (sev_out, sev_err) process =
117151 where
118152 combineStream s1 s2 = concurrentMerge [s1, s2]
119153
120−-- | Like `System.Process.createProcess` but also logs (debug) the process being run
154+-- | Like 'System.Process.createProcess' but also logs (debug) the process being run
121155createProcess
122156 :: (MonadIO m, CliLog m)
123− => CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
124−createProcess p = do
125− putLog Debug $ "Creating process: " <> reconstructCommand (cmdspec p)
157+ => ProcessSpec -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
158+createProcess procSpec = do
159+ p <- mkCreateProcess procSpec
160+ putLog Debug $ "Creating process: " <> reconstructProcSpec procSpec
126161 liftIO $ Process.createProcess p
127162
128163-- | Like `System.Process.createProcess_` but also logs (debug) the process being run
129164createProcess_
130165 :: (MonadIO m, CliLog m)
131− => String -> CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
132−createProcess_ name p = do
133− putLog Debug $ "Creating process " <> T.pack name <> ": " <> reconstructCommand (cmdspec p)
134− liftIO $ Process.createProcess p
166+ => String -> ProcessSpec -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
167+createProcess_ name procSpec = do
168+ p <- mkCreateProcess procSpec
169+ putLog Debug $ "Creating process " <> T.pack name <> ": " <> reconstructProcSpec procSpec
170+ liftIO $ Process.createProcess_ name p
171+
172+mkCreateProcess :: MonadIO m => ProcessSpec -> m Process.CreateProcess
173+mkCreateProcess (ProcessSpec p override') = do
174+ case override' of
175+ Nothing -> pure p
176+ Just override -> do
177+ procEnv <- Map.fromList <$> maybe (liftIO getEnvironment) pure (Process.env p)
178+ pure $ p { Process.env = Just $ Map.toAscList (override procEnv) }
135179
136180-- | Like `System.Process.callProcess` but also logs (debug) the process being run
137181callProcess
@@ -150,18 +194,18 @@ callCommand cmd = do
150194 liftIO $ Process.callCommand cmd
151195
152196withProcess
153− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
154− => CreateProcess -> (Handle -> Handle -> m ()) -> m (Handle, Handle)
197+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
198+ => ProcessSpec -> (Handle -> Handle -> m ()) -> m (Handle, Handle)
155199withProcess process f = do -- TODO: Use bracket.
156200 -- FIXME: Using `withCreateProcess` here leads to something operating illegally on closed handles.
157− (_, Just out, Just err, p) <- createProcess $ process
158− { std_out = CreatePipe , std_err = CreatePipe }
201+ (_, Just out, Just err, p) <- createProcess $ overCreateProcess
202+ (\x -> x { std_out = CreatePipe , std_err = CreatePipe }) process
159203
160204 f out err -- Pass the handles to the passed function
161205
162206 liftIO (waitForProcess p) >>= \case
163207 ExitSuccess -> return (out, err)
164− ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (cmdspec process) code
208+ ExitFailure code -> throwError $ review asProcessFailure $ ProcessFailure (Process.cmdspec $ _processSpec_createProcess process) code
165209
166210-- Create an input stream from the file handle, associating each item with the given severity.
167211streamHandle :: Severity -> Handle -> IO (InputStream (Severity, BSC.ByteString))
@@ -178,8 +222,12 @@ streamToLog stream = fix $ \loop -> do
178222
179223-- | Pretty print a 'CmdSpec'
180224reconstructCommand :: Process.CmdSpec -> Text
181−reconstructCommand (Process.ShellCommand str) = T.pack str
182−reconstructCommand (Process.RawCommand c as) = processToShellString c as
225+reconstructCommand p = case p of
226+ Process.ShellCommand str -> T.pack str
227+ Process.RawCommand c as -> processToShellString c as
183228 where
184229 processToShellString cmd args = T.unwords $ map quoteAndEscape (cmd : args)
185230 quoteAndEscape x = "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
231+
232+reconstructProcSpec :: ProcessSpec -> Text
233+reconstructProcSpec = reconstructCommand . Process.cmdspec . _processSpec_createProcess
lib/cliapp/src/Obelisk/CliApp/Types.hs+7−3
@@ -6,12 +6,14 @@
66{-# LANGUAGE GeneralizedNewtypeDeriving #-}
77{-# LANGUAGE MultiParamTypeClasses #-}
88{-# LANGUAGE OverloadedStrings #-}
9+{-# LANGUAGE StandaloneDeriving #-}
910{-# LANGUAGE UndecidableInstances #-}
1011module Obelisk.CliApp.Types where
1112
1213import Control.Concurrent.MVar (MVar)
1314import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
14−import Control.Monad.Log (LoggingT, MonadLog, Severity (..), WithSeverity (..), logMessage)
15+import Control.Monad.Fail (MonadFail)
16+import Control.Monad.Log (LoggingT(..), MonadLog, Severity (..), WithSeverity (..), logMessage)
1517import Control.Monad.Reader (MonadIO, ReaderT (..), MonadReader (..), ask, mapReaderT)
1618import Control.Monad.Writer (WriterT)
1719import Control.Monad.State (StateT)
@@ -45,9 +47,11 @@ type CliThrow e m = MonadError e m
4547putLog :: CliLog m => Severity -> Text -> m ()
4648putLog sev = logMessage . Output_Log . WithSeverity sev
4749
50+deriving instance MonadFail m => MonadFail (LoggingT Output m)
51+
4852newtype DieT e m a = DieT { unDieT :: ReaderT (e -> (Text, Int)) (LoggingT Output m) a }
4953 deriving
50− ( Functor, Applicative, Monad, MonadIO
54+ ( Functor, Applicative, Monad, MonadIO, MonadFail
5155 , MonadThrow, MonadCatch, MonadMask
5256 , MonadLog Output
5357 )
@@ -114,7 +118,7 @@ newtype CliT e m a = CliT
114118 { unCliT :: ReaderT (CliConfig e) (DieT e m) a
115119 }
116120 deriving
117− ( Functor, Applicative, Monad, MonadIO
121+ ( Functor, Applicative, Monad, MonadIO, MonadFail
118122 , MonadThrow, MonadCatch, MonadMask
119123 , MonadLog Output -- CliLog
120124 , MonadError e -- CliThrow
lib/command/obelisk-command.cabal+3−2
@@ -1,5 +1,5 @@
11name: obelisk-command
2−version: 0.1
2+version: 0.4
33cabal-version: >= 1.8
44build-type: Simple
55
@@ -23,7 +23,7 @@ library
2323 , filepath
2424 , github
2525 , here
26− , hit
26+ , git
2727 , hnix
2828 , hpack
2929 , io-streams
@@ -46,6 +46,7 @@ library
4646 , transformers
4747 , unix
4848 , unordered-containers
49+ , which
4950 , yaml
5051 exposed-modules:
5152 Obelisk.App
lib/command/src/Obelisk/App.hs+5−2
@@ -12,6 +12,7 @@ module Obelisk.App where
1212
1313import Control.Lens
1414import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
15+import Control.Monad.Fail (MonadFail)
1516import Control.Monad.Reader (MonadIO, ReaderT (..), ask, runReaderT)
1617import Control.Monad.Writer (WriterT)
1718import Control.Monad.State (StateT)
@@ -61,10 +62,11 @@ newtype ObeliskT m a = ObeliskT
6162 { unObeliskT :: ReaderT Obelisk (CliT ObeliskError m) a
6263 }
6364 deriving
64− ( Functor, Applicative, Monad, MonadIO, MonadThrow, MonadCatch, MonadMask
65+ ( Functor, Applicative, Monad, MonadIO, MonadThrow, MonadCatch, MonadMask, MonadFail
6566 , MonadLog Output -- CliLog
6667 , MonadError ObeliskError -- CliThrow ObeliskError
67− , HasCliConfig ObeliskError)
68+ , HasCliConfig ObeliskError
69+ )
6870
6971instance MonadTrans ObeliskT where
7072 lift = ObeliskT . lift . lift
@@ -104,6 +106,7 @@ type MonadInfallibleObelisk m =
104106type MonadObelisk m =
105107 ( MonadInfallibleObelisk m
106108 , CliThrow ObeliskError m
109+ , MonadFail m
107110 )
108111
109112getObeliskUserStateDir :: IO FilePath
lib/command/src/Obelisk/Command.hs+56−17
@@ -13,6 +13,7 @@ import qualified Data.Binary as Binary
1313import Data.Bool (bool)
1414import qualified Data.ByteString.Base16 as Base16
1515import qualified Data.ByteString.Lazy as LBS
16+import Data.Foldable (for_)
1617import Data.List
1718import Data.Maybe (catMaybes)
1819import qualified Data.Text as T
@@ -20,6 +21,7 @@ import Data.Text.Encoding
2021import Data.Text.Encoding.Error (lenientDecode)
2122import GHC.StaticPtr
2223import Options.Applicative
24+import Options.Applicative.Help.Pretty (text, (<$$>))
2325import System.Directory
2426import System.Environment
2527import System.FilePath
@@ -93,6 +95,7 @@ data ObCommand
9395 | ObCommand_Repl
9496 | ObCommand_Watch
9597 | ObCommand_Shell ShellOpts
98+ | ObCommand_Doc String [String] -- shell and list of packages
9699 | ObCommand_Internal ObInternal
97100 deriving Show
98101
@@ -104,7 +107,7 @@ data ObInternal
104107inNixShell' :: MonadObelisk m => StaticPtr (ObeliskT IO ()) -> m ()
105108inNixShell' p = withProjectRoot "." $ \root -> do
106109 cmd <- liftIO $ unwords <$> mkCmd -- TODO: shell escape instead of unwords
107− projectShell root False "ghc" (Just cmd)
110+ projectShell root True "ghc" (Just cmd)
108111 where
109112 mkCmd = do
110113 argsCfg <- getArgsConfig
@@ -129,6 +132,11 @@ obCommand cfg = hsubparser
129132 , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
130133 , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
131134 , command "shell" $ info (ObCommand_Shell <$> shellOpts) $ progDesc "Enter a shell with project dependencies"
135+ , command "doc" $ info (ObCommand_Doc <$> shellFlags <*> packageNames) $
136+ progDesc "List paths to haddock documentation for specified packages"
137+ <> footerDoc (Just $
138+ text "Hint: To open the documentation you can pipe the output of this command like"
139+ <$$> text "ob doc reflex reflex-dom-core | xargs -n1 xdg-open")
132140 ])
133141 <|> subparser
134142 (mconcat
@@ -136,6 +144,9 @@ obCommand cfg = hsubparser
136144 , command "internal" (info (ObCommand_Internal <$> internalCommand) mempty)
137145 ])
138146
147+packageNames :: Parser [String]
148+packageNames = some (strArgument (metavar "PACKAGE-NAME..."))
149+
139150deployCommand :: ArgsConfig -> Parser DeployCommand
140151deployCommand cfg = hsubparser $ mconcat
141152 [ command "init" $ info (DeployCommand_Init <$> deployInitOpts) $ progDesc "Initialize a deployment configuration directory"
@@ -210,20 +221,35 @@ thunkDirectoryParser = fmap (dropTrailingPathSeparator . normalise) . strArgumen
210221 , help "Path to directory containing thunk data"
211222 ]
212223
224+thunkConfig :: Parser ThunkConfig
225+thunkConfig = ThunkConfig
226+ <$>
227+ ( flag' (Just True) (long "private" <> help "Mark thunks as pointing to a private repository")
228+ <|> flag' (Just False) (long "public" <> help "Mark thunks as pointing to a public repository")
229+ <|> pure Nothing
230+ )
231+
232+thunkUpdateConfig :: Parser ThunkUpdateConfig
233+thunkUpdateConfig = ThunkUpdateConfig
234+ <$> optional (strOption (long "branch" <> metavar "BRANCH" <> help "Use the given branch when looking for the latest revision"))
235+ <*> thunkConfig
236+
237+thunkPackConfig :: Parser ThunkPackConfig
238+thunkPackConfig = ThunkPackConfig
239+ <$> 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.")
240+ <*> thunkConfig
241+
213242data ThunkCommand
214− = ThunkCommand_Update [FilePath] (Maybe String)
243+ = ThunkCommand_Update [FilePath] ThunkUpdateConfig
215244 | ThunkCommand_Unpack [FilePath]
216− | ThunkCommand_Pack [FilePath] Bool
245+ | ThunkCommand_Pack [FilePath] ThunkPackConfig
217246 deriving Show
218247
219−forceFlag :: Parser Bool
220−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."
221−
222248thunkCommand :: Parser ThunkCommand
223249thunkCommand = hsubparser $ mconcat
224− [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> optional (strOption (long "branch" <> metavar "BRANCH"))) $ progDesc "Update thunk to latest revision available"
250+ [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> thunkUpdateConfig) $ progDesc "Update thunk to latest revision available"
225251 , command "unpack" $ info (ThunkCommand_Unpack <$> some thunkDirectoryParser) $ progDesc "Unpack thunk into git checkout of revision it points to"
226− , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser <*> forceFlag) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
252+ , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser <*> thunkPackConfig) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
227253 ]
228254
229255data ShellOpts
@@ -233,12 +259,15 @@ data ShellOpts
233259 }
234260 deriving Show
235261
262+shellFlags :: Parser String
263+shellFlags =
264+ flag' "ghc" (long "ghc" <> help "Enter a shell environment having ghc (default)")
265+ <|> flag "ghc" "ghcjs" (long "ghcjs" <> help "Enter a shell having ghcjs rather than ghc")
266+ <|> strOption (short 'A' <> long "argument" <> metavar "NIXARG" <> help "Use the environment specified by the given nix argument of `shells'")
267+
236268shellOpts :: Parser ShellOpts
237269shellOpts = ShellOpts
238− <$> ( flag' "ghc" (long "ghc" <> help "Enter a shell environment having ghc (default)")
239− <|> flag "ghc" "ghcjs" (long "ghcjs" <> help "Enter a shell having ghcjs rather than ghc")
240− <|> strOption (short 'A' <> long "argument" <> metavar "NIXARG" <> help "Use the environment specified by the given nix argument of `shells'")
241− )
270+ <$> shellFlags
242271 <*> optional (strArgument (metavar "COMMAND"))
243272
244273parserPrefs :: ParserPrefs
@@ -254,7 +283,8 @@ mkObeliskConfig :: IO Obelisk
254283mkObeliskConfig = do
255284 cliArgs <- getArgs
256285 -- This function should not use argument parser (full argument parsing happens post handoff)
257− let logLevel = toLogLevel $ "-v" `elem` cliArgs
286+ -- TODO: See if we can use the argument parser with a subset of the parsers to get logging level out.
287+ let logLevel = toLogLevel $ any (`elem` ["-v", "--verbose"]) cliArgs
258288 notInteractive <- not <$> isInteractiveTerm
259289 cliConf <- newCliConfig logLevel notInteractive notInteractive $ \case
260290 ObeliskError_ProcessError (ProcessFailure p code) ann ->
@@ -360,15 +390,16 @@ ob = \case
360390 DeployCommand_Update -> deployUpdate "."
361391 DeployCommand_Test (platform, extraArgs) -> deployMobile platform extraArgs
362392 ObCommand_Run -> inNixShell' $ static run
363− -- inNixShell ($(mkClosure 'ghcidAction) ())
364393 ObCommand_Thunk tc -> case tc of
365− ThunkCommand_Update thunks mBranch -> mapM_ ((flip updateThunkToLatest) mBranch) thunks
366− ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
367− ThunkCommand_Pack thunks force -> forM_ thunks (packThunk force)
394+ ThunkCommand_Update thunks config -> for_ thunks (updateThunkToLatest config)
395+ ThunkCommand_Unpack thunks -> for_ thunks unpackThunk
396+ ThunkCommand_Pack thunks config -> for_ thunks (packThunk config)
368397 ObCommand_Repl -> runRepl
369398 ObCommand_Watch -> inNixShell' $ static runWatch
370399 ObCommand_Shell so -> withProjectRoot "." $ \root ->
371400 projectShell root False (_shellOpts_shell so) (_shellOpts_command so)
401+ ObCommand_Doc shell' pkgs -> withProjectRoot "." $ \root ->
402+ projectShell root False shell' (Just $ haddockCommand pkgs)
372403 ObCommand_Internal icmd -> case icmd of
373404 ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case
374405 Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
@@ -377,6 +408,14 @@ ob = \case
377408 liftIO $ runObelisk c $ deRefStaticPtr p
378409 ObInternal_CLIDemo -> cliDemo
379410
411+haddockCommand :: [String] -> String
412+haddockCommand pkgs = unwords
413+ [ "for p in"
414+ , unwords [getHaddockPath p ++ "/index.html" | p <- pkgs]
415+ , "; do echo $p; done"
416+ ]
417+ where getHaddockPath p = "$(ghc-pkg field " ++ p ++ " haddock-html --simple-output)"
418+
380419--TODO: Clean up all the magic strings throughout this codebase
381420
382421getArgsConfig :: IO ArgsConfig
lib/command/src/Obelisk/Command/Deploy.hs+29−29
@@ -4,7 +4,6 @@
44{-# LANGUAGE ScopedTypeVariables #-}
55{-# LANGUAGE ViewPatterns #-}
66{-# LANGUAGE DeriveGeneric #-}
7−{-# LANGUAGE TypeApplications #-}
87module Obelisk.Command.Deploy where
98
109import Control.Lens
@@ -22,26 +21,27 @@ import qualified Data.Set as Set
2221import qualified Data.Text as T
2322import qualified Data.Text.IO as T
2423import GHC.Generics
25−import Nix.Convert
2624import Nix.Pretty
25+import Nix.String (principledMakeNixStringWithoutContext)
2726import Nix.Value
2827import System.Directory
29−import System.Environment (getEnvironment)
3028import System.FilePath
3129import System.IO
3230import System.Posix.Files
33−import System.Process (delegate_ctlc, env, proc, cwd)
3431import Text.URI (URI)
3532import qualified Text.URI as URI
3633import Text.URI.Lens
3734
3835import Obelisk.App (MonadObelisk)
39−import Obelisk.CliApp (Severity (..), callProcessAndLogOutput, failWith, putLog, withSpinner)
36+import Obelisk.CliApp (
37+ Severity (..), callProcessAndLogOutput, failWith, proc, putLog,
38+ setCwd, setDelegateCtlc, setEnvOverride, withSpinner)
4039import Obelisk.Command.Nix
4140import Obelisk.Command.Project
4241import Obelisk.Command.Thunk
4342import Obelisk.Command.Utils
4443
44+
4545deployInit
4646 :: MonadObelisk m
4747 => ThunkPtr
@@ -58,7 +58,7 @@ deployInit thunkPtr deployDir sshKeyPath hostnames route adminEmail enableHttps
5858 False -> failWith $ T.pack $ "ob deploy init: file does not exist: " <> sshKeyPath
5959 True -> pure $ deployDir </> "ssh_key"
6060 callProcessAndLogOutput (Notice, Error) $
61− proc "cp" [sshKeyPath, localKey]
61+ proc cp [sshKeyPath, localKey]
6262 liftIO $ setFileMode localKey $ ownerReadMode .|. ownerWriteMode
6363 return localKey
6464 withSpinner "Validating configuration" $ do
@@ -83,7 +83,7 @@ deployInit thunkPtr deployDir sshKeyPath hostnames route adminEmail enableHttps
8383 writeDeployConfig deployDir "backend_hosts" $ unlines hostnames
8484 writeDeployConfig deployDir "enable_https" $ show enableHttps
8585 writeDeployConfig deployDir "admin_email" adminEmail
86− writeDeployConfig deployDir ("config" </> "common" </> "route") $ route
86+ writeDeployConfig deployDir ("config" </> "common" </> "route") route
8787 withSpinner "Creating source thunk (./src)" $ liftIO $ do
8888 createThunk (deployDir </> "src") thunkPtr
8989 setupObeliskImpl deployDir
@@ -108,7 +108,7 @@ deployPush deployPath getNixBuilders = do
108108 Right (ThunkData_Packed ptr) -> return ptr
109109 Right (ThunkData_Checkout _) -> do
110110 checkGitCleanStatus srcPath True >>= \case
111− True -> packThunk False srcPath
111+ True -> packThunk (ThunkPackConfig False (ThunkConfig Nothing)) srcPath
112112 False -> failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
113113 Left err -> failWith $ "ob deploy push: couldn't read src thunk: " <> T.pack (show err)
114114 let version = show . _thunkRev_commit $ _thunkPtr_rev thunkPtr
@@ -158,7 +158,7 @@ deployPush deployPath getNixBuilders = do
158158 ]
159159 isClean <- checkGitCleanStatus deployPath True
160160 when (not isClean) $ do
161− withSpinner "Commiting changes to Git" $ do
161+ withSpinner "Committing changes to Git" $ do
162162 callProcessAndLogOutput (Debug, Error) $ proc "git"
163163 ["-C", deployPath, "add", "."]
164164 callProcessAndLogOutput (Debug, Error) $ proc "git"
@@ -166,20 +166,20 @@ deployPush deployPath getNixBuilders = do
166166 putLog Notice $ "Deployed => " <> T.pack route
167167 where
168168 callProcess' envMap cmd args = do
169− processEnv <- Map.toList . (envMap <>) . Map.fromList <$> liftIO getEnvironment
170− let p = (proc cmd args) { delegate_ctlc = True, env = Just processEnv }
169+ let p = setEnvOverride (envMap <>) $ setDelegateCtlc True $ proc cmd args
171170 callProcessAndLogOutput (Notice, Notice) p
172171
173172deployUpdate :: MonadObelisk m => FilePath -> m ()
174−deployUpdate deployPath = updateThunkToLatest (deployPath </> "src") Nothing
173+deployUpdate deployPath = updateThunkToLatest (ThunkUpdateConfig Nothing (ThunkConfig Nothing)) (deployPath </> "src")
175174
176−keytoolToAndroidConfig :: KeytoolConfig -> HM.HashMap Text (NValueNF Identity)
177−keytoolToAndroidConfig conf = runIdentity $ do
178− path <- toValue $ Path $ _keytoolConfig_keystore conf
179− storepass <- toValue $ T.pack $ _keytoolConfig_storepass conf
180− alias <- toValue $ T.pack $ _keytoolConfig_alias conf
181− keypass <- toValue $ T.pack $ _keytoolConfig_keypass conf
182− return $ HM.fromList
175+keytoolToAndroidConfig :: KeytoolConfig -> HM.HashMap Text (NValue t Identity m)
176+keytoolToAndroidConfig conf =
177+ let path = nvPath $ _keytoolConfig_keystore conf
178+ fromStr = nvStr . principledMakeNixStringWithoutContext . T.pack
179+ storepass = fromStr $ _keytoolConfig_storepass conf
180+ alias = fromStr $ _keytoolConfig_alias conf
181+ keypass = fromStr $ _keytoolConfig_keypass conf
182+ in HM.fromList
183183 [ ("storeFile", path)
184184 , ("storePassword", storepass)
185185 , ("keyAlias", alias)
@@ -194,7 +194,7 @@ renderPlatformDeployment = \case
194194 Android -> "android"
195195 IOS -> "ios"
196196
197−deployMobile :: MonadObelisk m => PlatformDeployment -> [String] -> m ()
197+deployMobile :: forall m. MonadObelisk m => PlatformDeployment -> [String] -> m ()
198198deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
199199 let srcDir = root </> "src"
200200 configDir = root </> "config"
@@ -219,17 +219,17 @@ 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"
226226 keytoolConfContents <- liftIO $ BSL.readFile keytoolConfPath
227− liftIO $ putStrLn $ show keytoolConfContents
228− releaseKey <- case eitherDecode keytoolConfContents of
227+ liftIO $ print keytoolConfContents
228+ releaseKey :: String <- case eitherDecode keytoolConfContents :: Either String KeytoolConfig of
229229 Left err -> failWith $ T.pack err
230− Right conf -> do
231− let nvset = toValue @(HM.HashMap Text (NValueNF Identity)) @Identity @(NValueNF Identity) $ keytoolToAndroidConfig conf
232− return $ printNix $ runIdentity nvset
230+ Right conf -> return $
231+ (printNix :: MonadObelisk m => NValue t Identity m -> String) $
232+ nvSet (keytoolToAndroidConfig conf) HM.empty
233233 let expr = mconcat
234234 [ "with (import ", srcDir, " {});"
235235 , "android.frontend.override (drv: { "
@@ -255,7 +255,7 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
255255 result <- nixCmd $ NixCmd_Build $ def
256256 & nixBuildConfig_outLink .~ OutLink_None
257257 & nixCmdConfig_target .~ nixBuildTarget
258− putLog Notice $ T.pack $ "Your recently built android apk can be found at the following path: " <> (show result)
258+ putLog Notice $ T.pack $ "Your recently built android apk can be found at the following path: " <> show result
259259 callProcessAndLogOutput (Notice, Error) $ proc (result </> "bin" </> "deploy") mobileArgs
260260 where
261261 withEcho showEcho f = do
@@ -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" ["-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+1−2
@@ -37,7 +37,6 @@ import Data.List (intercalate)
3737import Data.Maybe
3838import Data.Monoid ((<>))
3939import qualified Data.Text as T
40−import System.Process (proc)
4140
4241import Obelisk.App (MonadObelisk)
4342import Obelisk.CliApp
@@ -160,7 +159,7 @@ instance Default NixCmd where
160159
161160nixCmd :: MonadObelisk m => NixCmd -> m FilePath
162161nixCmd cmdCfg = withSpinner' ("Running " <> cmd <> desc) (Just $ const $ "Built " <> desc) $ do
163− output <- readProcessAndLogStderr Debug $ proc (T.unpack cmd) $ options
162+ output <- readProcessAndLogStderr Debug $ proc (T.unpack cmd) options
164163 -- Remove final newline that Nix appends
165164 Just (outPath, '\n') <- pure $ T.unsnoc output
166165 pure $ T.unpack outPath
lib/command/src/Obelisk/Command/Project.hs+78−33
@@ -1,6 +1,7 @@
11{-# LANGUAGE FlexibleContexts #-}
22{-# LANGUAGE LambdaCase #-}
33{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE TemplateHaskell #-}
45module Obelisk.Command.Project
56 ( InitSource (..)
67 , initProject
@@ -15,6 +16,7 @@ module Obelisk.Command.Project
1516 , toImplDir
1617 ) where
1718
19+import Control.Concurrent.MVar (MVar, newMVar, withMVarMasked)
1820import Control.Monad
1921import Control.Monad.IO.Class (liftIO)
2022import Control.Monad.State
@@ -22,11 +24,14 @@ import Data.Bits
2224import Data.Function (on)
2325import qualified Data.Text as T
2426import System.Directory
27+import System.Environment (lookupEnv)
2528import System.FilePath
2629import System.IO.Temp
27−import System.Posix (FileStatus, UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
30+import System.IO.Unsafe (unsafePerformIO)
31+import System.Posix (FileStatus, FileMode, CMode (..), UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
2832import System.Posix.Files
29−import System.Process (CreateProcess, cwd, proc, waitForProcess, delegate_ctlc)
33+import System.Process (waitForProcess)
34+import System.Which (staticWhich)
3035
3136import GitHub.Data.GitData (Branch)
3237import GitHub.Data.Name (Name)
@@ -34,6 +39,7 @@ import GitHub.Data.Name (Name)
3439import Obelisk.App (MonadObelisk)
3540import Obelisk.CliApp
3641import Obelisk.Command.Thunk
42+
3743--TODO: Make this module resilient to random exceptions
3844
3945--TODO: Don't hardcode this
@@ -47,13 +53,14 @@ obeliskSourceWithBranch branch = ThunkSource_GitHub $ GitHubSource
4753 { _gitHubSource_owner = "obsidiansystems"
4854 , _gitHubSource_repo = "obelisk"
4955 , _gitHubSource_branch = Just branch
56+ , _gitHubSource_private = False
5057 }
5158
5259data InitSource
53− = InitSource_Default
54− | InitSource_Branch (Name Branch)
55− | InitSource_Symlink FilePath
56− deriving Show
60+ = InitSource_Default
61+ | InitSource_Branch (Name Branch)
62+ | InitSource_Symlink FilePath
63+ deriving Show
5764
5865-- | Path to obelisk directory in given path
5966toObeliskDir :: FilePath -> FilePath
@@ -114,15 +121,16 @@ initProject source force = withSystemTempDirectory "ob-init" $ \tmpDir -> do
114121findProjectObeliskCommand :: MonadObelisk m => FilePath -> m (Maybe FilePath)
115122findProjectObeliskCommand target = do
116123 myUid <- liftIO getRealUserID
124+ processUmask <- liftIO getUmask
117125 targetStat <- liftIO $ getFileStatus target
118− (result, insecurePaths) <- flip runStateT [] $ walkToProjectRoot target targetStat myUid >>= \case
126+ (result, insecurePaths) <- flip runStateT [] $ walkToProjectRoot target targetStat processUmask myUid >>= \case
119127 Nothing -> pure Nothing
120128 Just projectRoot -> liftIO (doesDirectoryExist $ toImplDir projectRoot) >>= \case
121129 False -> do
122130 putLog Warning $ "Found obelisk directory in " <> T.pack projectRoot <> " but the implementation (impl) file is missing"
123131 pure Nothing
124132 True -> do
125− walkToImplDir projectRoot myUid -- For security check
133+ walkToImplDir projectRoot myUid processUmask -- For security check
126134 return $ Just projectRoot
127135 case (result, insecurePaths) of
128136 (Just projDir, []) -> do
@@ -133,16 +141,36 @@ findProjectObeliskCommand target = do
133141 putLog Error $ T.unlines
134142 [ "Error: Found a project at " <> T.pack (normalise projDir) <> ", but had to traverse one or more insecure directories to get there:"
135143 , 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."
144+ , "Please ensure that all of these directories are owned by you, not world-writable, and no more group-writable than permitted by your umask."
137145 ]
138146 return Nothing
139147
148+-- | Get the umask for the Obelisk process.
149+--
150+-- Because of
151+-- http://man7.org/linux/man-pages/man2/umask.2.html#NOTES we have to set the
152+-- umask to read it. We are using 'withMVarMasked' to guarantee that setting and
153+-- reading isn't interrupted by any exception or interleaved with another thread.
154+getUmask :: IO FileMode
155+getUmask = withMVarMasked globalUmaskLock $ \() -> do
156+ initialMask <- setFileCreationMask safeUmask
157+ void (setFileCreationMask initialMask)
158+ pure initialMask
159+ where
160+ safeUmask :: FileMode
161+ safeUmask = CMode 0o22
162+
163+{-# NOINLINE globalUmaskLock #-}
164+globalUmaskLock :: MVar ()
165+globalUmaskLock = unsafePerformIO (newMVar ())
166+
140167-- | Get the FilePath to the containing project directory, if there is one
141168findProjectRoot :: MonadObelisk m => FilePath -> m (Maybe FilePath)
142169findProjectRoot target = do
143170 myUid <- liftIO getRealUserID
144171 targetStat <- liftIO $ getFileStatus target
145− (result, _) <- liftIO $ runStateT (walkToProjectRoot target targetStat myUid) []
172+ umask <- liftIO getUmask
173+ (result, _) <- liftIO $ runStateT (walkToProjectRoot target targetStat umask myUid) []
146174 return result
147175
148176withProjectRoot :: MonadObelisk m => FilePath -> (FilePath -> m a) -> m a
@@ -155,15 +183,15 @@ withProjectRoot target f = findProjectRoot target >>= \case
155183-- traversed in the process. Return the project root directory, if found.
156184walkToProjectRoot
157185 :: (MonadState [FilePath] m, MonadIO m)
158− => FilePath -> FileStatus -> UserID -> m (Maybe FilePath)
159−walkToProjectRoot this thisStat myUid = liftIO (doesDirectoryExist this) >>= \case
186+ => FilePath -> FileStatus -> FileMode -> UserID -> m (Maybe FilePath)
187+walkToProjectRoot this thisStat desiredUmask myUid = liftIO (doesDirectoryExist this) >>= \case
160188 -- It's not a directory, so it can't be a project
161189 False -> do
162190 let dir = takeDirectory this
163191 dirStat <- liftIO $ getFileStatus dir
164− walkToProjectRoot dir dirStat myUid
192+ walkToProjectRoot dir dirStat desiredUmask myUid
165193 True -> do
166− when (not $ isWritableOnlyBy thisStat myUid) $ modify (this:)
194+ when (not $ isWellOwnedAndWellPermissioned thisStat myUid desiredUmask) $ modify (this:)
167195 liftIO (doesDirectoryExist $ toObeliskDir this) >>= \case
168196 True -> return $ Just this
169197 False -> do
@@ -173,25 +201,38 @@ walkToProjectRoot this thisStat myUid = liftIO (doesDirectoryExist this) >>= \ca
173201 isSameFileAs = (==) `on` fileIdentity
174202 if thisStat `isSameFileAs` nextStat
175203 then return Nothing -- Found a cycle; probably hit root directory
176− else walkToProjectRoot next nextStat myUid
204+ else walkToProjectRoot next nextStat desiredUmask myUid
177205
178206-- | Walk from the given project root directory to its Obelisk implementation
179207-- directory, accumulating potentially insecure directories that were traversed
180208-- in the process.
181−walkToImplDir :: (MonadState [FilePath] m, MonadIO m) => FilePath -> UserID -> m ()
182−walkToImplDir projectRoot myUid = do
209+walkToImplDir :: (MonadState [FilePath] m, MonadIO m) => FilePath -> UserID -> FileMode -> m ()
210+walkToImplDir projectRoot myUid umask = do
183211 let obDir = toObeliskDir projectRoot
184212 obDirStat <- liftIO $ getFileStatus obDir
185− when (not $ isWritableOnlyBy obDirStat myUid) $ modify (obDir:)
213+ when (not $ isWellOwnedAndWellPermissioned obDirStat myUid umask) $ modify (obDir:)
186214 let implThunk = obDir </> "impl"
187215 implThunkStat <- liftIO $ getFileStatus implThunk
188− when (not $ isWritableOnlyBy implThunkStat myUid) $ modify (implThunk:)
216+ when (not $ isWellOwnedAndWellPermissioned implThunkStat myUid umask) $ modify (implThunk:)
217+
218+-- | Check to see if directory is writable by a user whose User ID matches the
219+-- second argument provided, and if the fact that other people can write to that
220+-- directory is in accordance with the umask of the system, passed as the third
221+-- argument.
222+isWellOwnedAndWellPermissioned :: FileStatus -> UserID -> FileMode -> Bool
223+isWellOwnedAndWellPermissioned s uid umask = isOwnedBy s uid && filePermissionIsSafe s umask
189224
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
225+isOwnedBy :: FileStatus -> UserID -> Bool
226+isOwnedBy s uid = fileOwner s == uid
227+
228+-- | Check to see if a directory respect the umask, but check explicitly that
229+-- it's not world writable in any case.
230+filePermissionIsSafe :: FileStatus -> FileMode -> Bool
231+filePermissionIsSafe s umask = not fileWorldWritable && fileGroupWritable <= umaskGroupWritable
232+ where
233+ fileWorldWritable = fileMode s .&. 0o002 == 0o002
234+ fileGroupWritable = fileMode s .&. 0o020 == 0o020
235+ umaskGroupWritable = umask .&. 0o020 == 0
195236
196237-- | Run a command in the given shell for the current project
197238inProjectShell :: MonadObelisk m => String -> String -> m ()
@@ -204,17 +245,21 @@ inImpureProjectShell shellName command = withProjectRoot "." $ \root ->
204245
205246projectShell :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
206247projectShell root isPure shellName command = do
207− (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCtlc $ setCwd (Just root) $ proc "nix-shell" $
208− [ "--pure" | isPure ] <>
248+ let nixPath = $(staticWhich "nix")
249+ nixpkgsPath <- fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $ proc nixPath ["eval", "(import .obelisk/impl {}).nixpkgs.path"]
250+ nixRemote <- liftIO $ lookupEnv "NIX_REMOTE"
251+ (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setDelegateCtlc True $ setCwd (Just root) $ proc "nix-shell" $
252+ [ "default.nix"] <>
253+ ["--pure" | isPure] <>
209254 [ "-A"
210255 , "shells." <> shellName
211256 ] <> case command of
212257 Nothing -> []
213− Just c -> ["--run", c]
258+ Just c ->
259+ -- TODO: Escape nixpkgsPath and nixRemote
260+ [ "--run"
261+ , "export NIX_PATH=nixpkgs=" <> T.unpack nixpkgsPath <> "; " <>
262+ maybe "" (\v -> "export NIX_REMOTE=" <> v <> "; ") nixRemote <>
263+ c
264+ ]
214265 void $ liftIO $ waitForProcess ph
215−
216−setCtlc :: CreateProcess -> CreateProcess
217−setCtlc cfg = cfg { delegate_ctlc = True }
218−
219−setCwd :: Maybe FilePath -> CreateProcess -> CreateProcess
220−setCwd fp cfg = cfg { cwd = fp }
lib/command/src/Obelisk/Command/Run.hs+100−70
@@ -3,17 +3,23 @@
33{-# LANGUAGE MultiParamTypeClasses #-}
44{-# LANGUAGE OverloadedStrings #-}
55{-# LANGUAGE ScopedTypeVariables #-}
6+{-# LANGUAGE TemplateHaskell #-}
7+{-# LANGUAGE TupleSections #-}
68module Obelisk.Command.Run where
79
8−import Control.Exception (bracket)
10+import Control.Applicative (liftA2)
11+import Control.Exception (Exception, bracket)
912import Control.Monad
13+import Control.Monad.Except (runExceptT, throwError)
1014import Control.Monad.IO.Class (liftIO)
1115import Control.Monad.Reader (MonadIO)
1216import Data.Either
17+import Data.Foldable (for_)
1318import qualified Data.List as L
1419import qualified Data.List.NonEmpty as NE
1520import Data.Maybe
1621import qualified Data.Text as T
22+import Distribution.Compiler (CompilerFlavor(..))
1723import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
1824import Distribution.Parsec.ParseResult (runParseResult)
1925import Distribution.Pretty
@@ -22,22 +28,18 @@ import Distribution.Types.CondTree
2228import Distribution.Types.GenericPackageDescription
2329import Distribution.Types.Library
2430import Distribution.Utils.Generic
25−import Hpack.Config
26−import Hpack.Render
27−import Hpack.Yaml
31+import qualified Hpack.Config as Hpack
32+import qualified Hpack.Render as Hpack
33+import qualified Hpack.Yaml as Hpack
2834import Language.Haskell.Extension
2935import Network.Socket hiding (Debug)
3036import System.Directory
3137import System.FilePath
32−import System.Process (proc)
3338import System.IO.Temp (withSystemTempDirectory)
34−import Data.ByteString (ByteString)
39+import System.Which (staticWhich)
3540
3641import Obelisk.App (MonadObelisk, ObeliskT)
37−import Obelisk.CliApp
38− ( CliT (..), HasCliConfig, Severity (..)
39− , callCommand, failWith, getCliConfig, putLog
40− , readProcessAndLogStderr, runCli)
42+import Obelisk.CliApp (Severity (..) , callCommand, failWith, putLog, proc, readProcessAndLogStderr)
4143import Obelisk.Command.Project (inProjectShell, withProjectRoot)
4244
4345data CabalPackageInfo = CabalPackageInfo
@@ -48,11 +50,15 @@ data CabalPackageInfo = CabalPackageInfo
4850 -- ^ List of globally enable extensions of the library component
4951 , _cabalPackageInfo_defaultLanguage :: Maybe Language
5052 -- ^ List of globally set languages of the library component
53+ , _cabalPackageInfo_compilerOptions :: [(CompilerFlavor, [String])]
54+ -- ^ List of compiler-specific options (e.g., the "ghc-options" field of the cabal file)
5155 }
5256
5357-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
5458run :: ObeliskT IO ()
5559run = do
60+ let nixPath = $(staticWhich "nix")
61+ nixBuildPath = $(staticWhich "nix-build")
5662 pkgs <- getLocalPkgs
5763 withGhciScript pkgs $ \dotGhciPath -> do
5864 freePort <- getFreePort
@@ -61,7 +67,7 @@ run = do
6167 then root
6268 else "./" <> root
6369 isDerivation <- readProcessAndLogStderr Debug $
64− proc "nix"
70+ proc nixPath
6571 [ "eval"
6672 , "-f"
6773 , root
@@ -73,9 +79,12 @@ run = do
7379 -- Check whether the impure static files are a derivation (and so must be built)
7480 if isDerivation == "1"
7581 then fmap T.strip $ readProcessAndLogStderr Debug $ -- Strip whitespace here because nix-build has no --raw option
76− proc "nix-build" ["-E", "(import " <> importableRoot <> "{}).passthru.staticFilesImpure"]
82+ proc nixBuildPath
83+ [ "--no-out-link"
84+ , "-E", "(import " <> importableRoot <> "{}).passthru.staticFilesImpure"
85+ ]
7786 else readProcessAndLogStderr Debug $
78− proc "nix" ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
87+ proc nixPath ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
7988 putLog Debug $ "Assets impurely loaded from: " <> assets
8089 runGhcid dotGhciPath $ Just $ unwords
8190 [ "Obelisk.Run.run"
@@ -96,6 +105,27 @@ runWatch = do
96105 pkgs <- getLocalPkgs
97106 withGhciScript pkgs $ \dotGhciPath -> runGhcid dotGhciPath Nothing
98107
108+data GuessPackageFileError = GuessPackageFileError_Ambiguous [FilePath] | GuessPackageFileError_NotFound
109+ deriving (Eq, Ord, Show)
110+instance Exception GuessPackageFileError
111+
112+-- | Given a directory, try to guess what the appropriate @.cabal@ or @package.yaml@ file is for the package.
113+guessCabalPackageFile
114+ :: MonadIO m
115+ => FilePath -- ^ Directory to search for cabal package
116+ -> m (Either GuessPackageFileError (Either FilePath FilePath))
117+ -- ^ 'Right' 'Left' for hpack package, 'Right' 'Right' for cabal package; 'FilePath' is relative to given directory.
118+guessCabalPackageFile dir = do
119+ candidates <- liftIO $
120+ filterM (doesFileExist . (dir </>))
121+ =<< filter (liftA2 (||) (== Hpack.packageConfig) (".cabal" `L.isSuffixOf`))
122+ <$> listDirectory dir
123+ pure $ case L.partition (== Hpack.packageConfig) candidates of
124+ ([hpack], _) -> Right $ Left hpack
125+ ([], [cabal]) -> Right $ Right cabal
126+ ([], []) -> Left GuessPackageFileError_NotFound
127+ (hpacks, cabals) -> Left $ GuessPackageFileError_Ambiguous $ hpacks <> cabals
128+
99129-- | Relative paths to local packages of an obelisk project
100130-- TODO a way to query this
101131getLocalPkgs :: Applicative f => f [FilePath]
@@ -105,66 +135,53 @@ parseCabalPackage
105135 :: (MonadObelisk m)
106136 => FilePath -- ^ package directory
107137 -> m (Maybe CabalPackageInfo)
108−parseCabalPackage dir = do
109− let cabalFp = dir </> (takeBaseName dir <> ".cabal")
110− hpackFp = dir </> "package.yaml"
111− hasCabal <- liftIO $ doesFileExist cabalFp
112− hasHpack <- liftIO $ doesFileExist hpackFp
113−
114− mCabalContents <- if hasCabal
115− then Just <$> liftIO (readUTF8File cabalFp)
116− else if hasHpack
117− then do
118− let decodeOptions = DecodeOptions hpackFp Nothing decodeYaml
119− liftIO (readPackageConfig decodeOptions) >>= \case
120− Left err -> do
121− putLog Error $ T.pack $ "Failed to parse " <> hpackFp <> ": " <> err
122− return Nothing
123− Right (DecodeResult hpackPackage _ _) -> do
124− return $ Just $ renderPackage [] hpackPackage
125− else return Nothing
126−
127− fmap join $ forM mCabalContents $ \cabalContents -> do
128− let (warnings, result) = runParseResult $ parseGenericPackageDescription $
129− toUTF8BS $ cabalContents
130− mapM_ (putLog Warning) $ fmap (T.pack . show) warnings
131− case result of
132− Right gpkg -> do
133− return $ do
134− (_, lib) <- simplifyCondTree (const $ pure True) <$> condLibrary gpkg
135− pure $ CabalPackageInfo
136− { _cabalPackageInfo_packageRoot = takeDirectory cabalFp
137− , _cabalPackageInfo_sourceDirs =
138− fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
139− , _cabalPackageInfo_defaultExtensions =
140− defaultExtensions $ libBuildInfo lib
141− , _cabalPackageInfo_defaultLanguage =
142− defaultLanguage $ libBuildInfo lib
143− }
144− Left (_, errors) -> do
145− putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
146− mapM_ (putLog Error) $ fmap (T.pack . show) errors
147− return Nothing
148−
149−withUTF8FileContentsM :: (MonadIO m, HasCliConfig e m) => FilePath -> (ByteString -> CliT e IO a) -> m a
150−withUTF8FileContentsM fp f = do
151− c <- getCliConfig
152− liftIO $ withUTF8FileContents fp $ runCli c . f . toUTF8BS
138+parseCabalPackage dir = either ((Nothing <$) . putLog Error) (pure . Just) <=< runExceptT $ do
139+ (cabalContents, packageFile) <- guessCabalPackageFile dir >>= \case
140+ Left GuessPackageFileError_NotFound -> throwError $ "No .cabal or package.yaml file found in " <> T.pack dir
141+ Left (GuessPackageFileError_Ambiguous _) -> throwError $ "Unable to determine which .cabal file to use in " <> T.pack dir
142+ Right (Right cabalFileName) -> let file = dir </> cabalFileName in (, file) <$> liftIO (readUTF8File file)
143+ Right (Left hpackFileName) -> do
144+ let
145+ file = dir </> hpackFileName
146+ decodeOptions = Hpack.DecodeOptions (Hpack.ProgramName "ob") file Nothing Hpack.decodeYaml
147+ liftIO (Hpack.readPackageConfig decodeOptions) >>= \case
148+ Left err -> throwError $ T.pack $ "Failed to parse " <> file <> ": " <> err
149+ Right (Hpack.DecodeResult hpackPackage _ _ _) -> pure (Hpack.renderPackage [] hpackPackage, file)
150+
151+ let (warnings, result) = runParseResult $ parseGenericPackageDescription $
152+ toUTF8BS cabalContents
153+ for_ warnings $ putLog Warning . T.pack . show
154+ case condLibrary <$> result of
155+ Right (Just condLib) -> do
156+ let (_, lib) = simplifyCondTree (const $ pure True) condLib
157+ pure $ CabalPackageInfo
158+ { _cabalPackageInfo_packageRoot = dir
159+ , _cabalPackageInfo_sourceDirs =
160+ fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
161+ , _cabalPackageInfo_defaultExtensions =
162+ defaultExtensions $ libBuildInfo lib
163+ , _cabalPackageInfo_defaultLanguage =
164+ defaultLanguage $ libBuildInfo lib
165+ , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
166+ }
167+ Right Nothing -> throwError "Haskell package has no library component"
168+ Left (_, errors) ->
169+ throwError $ T.pack $ "Failed to parse " <> packageFile <> ":\n" <> unlines (map show errors)
153170
154171-- | Create ghci configuration to load the given packages
155172withGhciScript
156173 :: MonadObelisk m
157174 => [FilePath] -- ^ List of packages to load into ghci
158− -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporory .ghci
175+ -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporary .ghci
159176 -> m ()
160−withGhciScript pkgs f = do
161− (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM pkgs $ \pkg -> do
162− flip fmap (parseCabalPackage pkg) $ \case
163− Nothing -> Left pkg
177+withGhciScript dirs f = do
178+ (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM dirs $ \dir -> do
179+ flip fmap (parseCabalPackage dir) $ \case
180+ Nothing -> Left dir
164181 Just packageInfo -> Right packageInfo
165182
166183 when (null packageInfos) $
167− failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " pkgs
184+ failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " dirs
168185 unless (null pkgDirErrs) $
169186 putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
170187
@@ -174,11 +191,16 @@ withGhciScript pkgs f = do
174191 language = NE.toList $ fromMaybe (Haskell98 NE.:| []) $ NE.nonEmpty languageFromPkgs
175192 extensionsLine = if extensions == mempty
176193 then ""
177− else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
178− dotGhci = unlines $
194+ else ":set " <> unwords (("-X" <>) . prettyShow <$> extensions)
195+ ghcOptions = concat $ mapMaybe (\case (GHC, xs) -> Just xs; _ -> Nothing) $
196+ packageInfos >>= _cabalPackageInfo_compilerOptions
197+ dotGhci = unlines
179198 [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
199+ , case ghcOptions of
200+ [] -> ""
201+ xs -> ":set " <> unwords xs
180202 , extensionsLine
181− , ":set " <> intercalate " " (("-X" <>) . prettyShow <$> language)
203+ , ":set " <> unwords (("-X" <>) . prettyShow <$> language)
182204 , ":load Backend Frontend"
183205 , "import Obelisk.Run"
184206 , "import qualified Frontend"
@@ -203,7 +225,7 @@ runGhciRepl
203225 :: MonadObelisk m
204226 => FilePath -- ^ Path to .ghci
205227 -> m ()
206−runGhciRepl dotGhci = inProjectShell "ghc" $ unwords $ "ghci" : ["-no-user-package-db", "-ghci-script", dotGhci]
228+runGhciRepl dotGhci = inProjectShell "ghc" $ "ghci " <> makeBaseGhciOptions dotGhci
207229
208230-- | Run ghcid
209231runGhcid
@@ -211,17 +233,25 @@ runGhcid
211233 => FilePath -- ^ Path to .ghci
212234 -> Maybe String -- ^ Optional command to run at every reload
213235 -> m ()
214−runGhcid dotGhci mcmd = callCommand $ unwords $ "ghcid" : opts
236+runGhcid dotGhci mcmd = callCommand $ unwords $ $(staticWhich "ghcid") : opts
215237 where
216238 opts =
217239 [ "-W"
218240 --TODO: The decision of whether to use -fwarn-redundant-constraints should probably be made by the user
219− , "--command='ghci -Wall -ignore-dot-ghci -fwarn-redundant-constraints -no-user-package-db -ghci-script " <> dotGhci <> "' "
241+ , "--command='ghci -Wall -ignore-dot-ghci -fwarn-redundant-constraints " <> makeBaseGhciOptions dotGhci <> "' "
220242 , "--reload=config"
221243 , "--outputfile=ghcid-output.txt"
222244 ] <> testCmd
223245 testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'")
224246
247+makeBaseGhciOptions :: FilePath -> String
248+makeBaseGhciOptions dotGhci =
249+ unwords
250+ [ "-no-user-package-db"
251+ , "-package-env -"
252+ , "-ghci-script " <> dotGhci
253+ ]
254+
225255getFreePort :: MonadIO m => m PortNumber
226256getFreePort = liftIO $ withSocketsDo $ do
227257 addr:_ <- getAddrInfo (Just defaultHints) (Just "127.0.0.1") (Just "0")
lib/command/src/Obelisk/Command/Thunk.hs+182−104
@@ -8,25 +8,30 @@
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 (..)
2021 , createThunk
2122 , createThunkWithLatest
23+ , getLatestRev
24+ , getThunkGitBranch
25+ , getThunkPtr
26+ , gitUriToText
2227 , nixBuildAttrWithCache
2328 , nixBuildThunkAttrWithCache
24− , unpackThunk
2529 , packThunk
30+ , parseGitUri
2631 , readThunk
32+ , unpackThunk
2733 , updateThunk
28− , getThunkPtr
29− , parseGitUri
34+ , updateThunkToLatest
3035 , uriThunkPtr
3136 ) where
3237
@@ -68,8 +73,7 @@ import System.FilePath
6873import System.IO.Error
6974import System.IO.Temp
7075import System.Posix (getSymbolicLinkStatus, modificationTime)
71−import System.Process (proc)
72−import Text.URI
76+import qualified Text.URI as URI
7377
7478import Obelisk.App (MonadObelisk)
7579import Obelisk.CliApp
@@ -94,7 +98,7 @@ type NixSha256 = Text --TODO: Use a smart constructor and make this actually ver
9498
9599-- | A specific revision of data; it may be available from multiple sources
96100data ThunkRev = ThunkRev
97− { _thunkRev_commit :: Ref
101+ { _thunkRev_commit :: Ref Ref.SHA1
98102 , _thunkRev_nixSha256 :: NixSha256
99103 }
100104 deriving (Show, Eq, Ord)
@@ -111,37 +115,63 @@ data GitHubSource = GitHubSource
111115 { _gitHubSource_owner :: Name Owner
112116 , _gitHubSource_repo :: Name Repo
113117 , _gitHubSource_branch :: Maybe (Name Branch)
118+ , _gitHubSource_private :: Bool
114119 }
115120 deriving (Show, Eq, Ord)
116121
122+newtype GitUri = GitUri URI.URI deriving (Eq, Ord, Show)
123+
124+gitUriToText :: GitUri -> Text
125+gitUriToText (GitUri uri)
126+ | (T.toLower . URI.unRText <$> URI.uriScheme uri) == Just "file"
127+ , Just (_, path) <- URI.uriPath uri
128+ = "/" <> T.intercalate "/" (map URI.unRText $ NonEmpty.toList path)
129+ | otherwise = URI.render uri
130+
117131data GitSource = GitSource
118− { _gitSource_url :: URI
132+ { _gitSource_url :: GitUri
119133 , _gitSource_branch :: Maybe (Name Branch)
120134 , _gitSource_fetchSubmodules :: Bool
135+ , _gitSource_private :: Bool
121136 }
122137 deriving (Show, Eq, Ord)
123138
139+newtype ThunkConfig = ThunkConfig
140+ { _thunkConfig_private :: Maybe Bool
141+ } deriving Show
142+
143+data ThunkUpdateConfig = ThunkUpdateConfig
144+ { _thunkUpdateConfig_branch :: Maybe String
145+ , _thunkUpdateConfig_config :: ThunkConfig
146+ } deriving Show
147+
148+data ThunkPackConfig = ThunkPackConfig
149+ { _thunkPackConfig_force :: Bool
150+ , _thunkPackConfig_config :: ThunkConfig
151+ } deriving Show
152+
124153-- | Convert a GitHub source to a regular Git source. Assumes no submodules.
125154forgetGithub :: Bool -> GitHubSource -> GitSource
126155forgetGithub 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
156+ { _gitSource_url = GitUri $ URI.URI
157+ { URI.uriScheme = Just $ fromRight' $ URI.mkScheme $ if useSsh then "ssh" else "https"
158+ , URI.uriAuthority = Right $ URI.Authority
159+ { URI.authUserInfo = URI.UserInfo (fromRight' $ URI.mkUsername "git") Nothing
131160 <$ guard useSsh
132− , authHost = fromRight' $ mkHost "github.com"
133− , authPort = Nothing
161+ , URI.authHost = fromRight' $ URI.mkHost "github.com"
162+ , URI.authPort = Nothing
134163 }
135− , uriPath = Just ( False
136− , fromRight' . mkPathPiece <$>
164+ , URI.uriPath = Just ( False
165+ , fromRight' . URI.mkPathPiece <$>
137166 untagName (_gitHubSource_owner s)
138167 :| [ untagName (_gitHubSource_repo s) <> ".git" ]
139168 )
140− , uriQuery = []
141− , uriFragment = Nothing
169+ , URI.uriQuery = []
170+ , URI.uriFragment = Nothing
142171 }
143172 , _gitSource_branch = _gitHubSource_branch s
144173 , _gitSource_fetchSubmodules = False
174+ , _gitSource_private = _gitHubSource_private s
145175 }
146176
147177getThunkGitBranch :: ThunkPtr -> Maybe Text
@@ -149,26 +179,26 @@ getThunkGitBranch (ThunkPtr _ src) = fmap untagName $ case src of
149179 ThunkSource_GitHub s -> _gitHubSource_branch s
150180 ThunkSource_Git s -> _gitSource_branch s
151181
152−commitNameToRef :: Name Commit -> Ref
182+commitNameToRef :: Name Commit -> Ref Ref.SHA1
153183commitNameToRef (N c) = Ref.fromHex $ encodeUtf8 c
154184
155185-- TODO: Use spinner here.
156186getNixSha256ForUriUnpacked
157187 :: MonadObelisk m
158− => URI
188+ => GitUri
159189 -> m NixSha256
160190getNixSha256ForUriUnpacked uri =
161− withExitFailMessage ("nix-prefetch-url: Failed to determine sha256 hash of URL " <> render uri) $ do
191+ withExitFailMessage ("nix-prefetch-url: Failed to determine sha256 hash of URL " <> gitUriToText uri) $ do
162192 [hash] <- fmap T.lines $ readProcessAndLogOutput (Debug, Debug) $
163− proc "nix-prefetch-url" ["--unpack" , "--type" , "sha256" , T.unpack $ render uri]
193+ proc "nix-prefetch-url" ["--unpack", "--type", "sha256", T.unpack $ gitUriToText uri]
164194 pure hash
165195
166−nixPrefetchGit :: MonadObelisk m => URI -> Text -> Bool -> m NixSha256
196+nixPrefetchGit :: MonadObelisk m => GitUri -> Text -> Bool -> m NixSha256
167197nixPrefetchGit uri rev fetchSubmodules =
168− withExitFailMessage ("nix-prefetch-git: Failed to determine sha256 hash of Git repo " <> render uri <> " at " <> rev) $ do
198+ withExitFailMessage ("nix-prefetch-git: Failed to determine sha256 hash of Git repo " <> gitUriToText uri <> " at " <> rev) $ do
169199 out <- readProcessAndLogStderr Debug $
170200 proc "nix-prefetch-git" $ filter (/="")
171− [ "--url", T.unpack $ render uri
201+ [ "--url", T.unpack $ gitUriToText uri
172202 , "--rev", T.unpack rev
173203 , if fetchSubmodules then "--fetch-submodules" else ""
174204 , "--quiet"
@@ -286,10 +316,12 @@ parseGitHubSource v = do
286316 owner <- v Aeson..: "owner"
287317 repo <- v Aeson..: "repo"
288318 branch <- v Aeson..:! "branch"
319+ private <- v Aeson..:? "private"
289320 pure $ GitHubSource
290321 { _gitHubSource_owner = owner
291322 , _gitHubSource_repo = repo
292323 , _gitHubSource_branch = branch
324+ , _gitHubSource_private = fromMaybe False private
293325 }
294326
295327parseGitSource :: Aeson.Object -> Aeson.Parser GitSource
@@ -297,10 +329,12 @@ parseGitSource v = do
297329 Just url <- parseGitUri <$> v Aeson..: "url"
298330 branch <- v Aeson..:! "branch"
299331 fetchSubmodules <- v Aeson..:! "fetchSubmodules"
332+ private <- v Aeson..:? "private"
300333 pure $ GitSource
301334 { _gitSource_url = url
302335 , _gitSource_branch = branch
303336 , _gitSource_fetchSubmodules = fromMaybe False fetchSubmodules
337+ , _gitSource_private = fromMaybe False private
304338 }
305339
306340overwriteThunk :: MonadObelisk m => FilePath -> ThunkPtr -> m ()
@@ -327,13 +361,15 @@ encodeThunkPtrData (ThunkPtr rev src) = case src of
327361 , ("branch" .=) <$> _gitHubSource_branch s
328362 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
329363 , Just $ "sha256" .= _thunkRev_nixSha256 rev
364+ , Just $ "private" .= _gitHubSource_private s
330365 ]
331366 ThunkSource_Git s -> encodePretty' plainGitCfg $ Aeson.object $ catMaybes
332− [ Just $ "url" .= render (_gitSource_url s)
367+ [ Just $ "url" .= gitUriToText (_gitSource_url s)
333368 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
334369 , ("branch" .=) <$> _gitSource_branch s
335370 , Just $ "sha256" .= _thunkRev_nixSha256 rev
336371 , Just $ "fetchSubmodules" .= _gitSource_fetchSubmodules s
372+ , Just $ "private" .= _gitSource_private s
337373 ]
338374 where
339375 githubCfg = defConfig
@@ -354,6 +390,7 @@ encodeThunkPtrData (ThunkPtr rev src) = case src of
354390 [ "url"
355391 , "rev"
356392 , "sha256"
393+ , "private"
357394 , "fetchSubmodules"
358395 ] <> compare
359396 , confTrailingNewline = True
@@ -377,8 +414,8 @@ createThunkWithLatest target s = do
377414 , _thunkPtr_rev = rev
378415 }
379416
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
417+updateThunkToLatest :: MonadObelisk m => ThunkUpdateConfig -> FilePath -> m ()
418+updateThunkToLatest (ThunkUpdateConfig mBranch thunkConfig) target = withSpinner' ("Updating thunk " <> T.pack target <> " to latest") (pure $ const $ "Thunk " <> T.pack target <> " updated to latest") $ do
382419 checkThunkDirectory "ob thunk update directory cannot be '.'" target
383420 -- check to see if thunk should be updated to a specific branch or just update it's current branch
384421 case mBranch of
@@ -390,7 +427,7 @@ updateThunkToLatest target mBranch = withSpinner' ("Updating thunk " <> T.pack t
390427 ThunkData_Checkout _ -> failWith "cannot update an unpacked thunk"
391428 let src = _thunkPtr_source ptr
392429 rev <- getLatestRev src
393− overwriteThunk overwrite $ ThunkPtr
430+ overwriteThunk overwrite $ modifyThunkPtrByConfig thunkConfig $ ThunkPtr
394431 { _thunkPtr_source = src
395432 , _thunkPtr_rev = rev
396433 }
@@ -398,24 +435,25 @@ updateThunkToLatest target mBranch = withSpinner' ("Updating thunk " <> T.pack t
398435 Left err -> failWith $ T.pack $ "thunk update: " <> show err
399436 Right c -> case c of
400437 ThunkData_Packed t -> case _thunkPtr_source t of
401− ThunkSource_Git tsg -> setThunk target tsg branch
438+ ThunkSource_Git tsg -> setThunk thunkConfig target tsg branch
402439 ThunkSource_GitHub tsgh -> do
403440 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."
441+ setThunk thunkConfig target tsg branch
442+ 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."
406443
407−setThunk :: MonadObelisk m => FilePath -> GitSource -> String -> m ()
408−setThunk target gs branch = do
444+setThunk :: MonadObelisk m => ThunkConfig -> FilePath -> GitSource -> String -> m ()
445+setThunk thunkConfig target gs branch = do
409446 newThunkPtr <- uriThunkPtr (_gitSource_url gs) (Just $ T.pack branch) Nothing
410447 overwriteThunk target newThunkPtr
411− updateThunkToLatest target Nothing
448+ updateThunkToLatest (ThunkUpdateConfig Nothing thunkConfig) target
412449
413450-- | All recognized github standalone loaders, ordered from newest to oldest.
414451-- This tool will only ever produce the newest one when it writes a thunk.
415452gitHubStandaloneLoaders :: NonEmpty Text
416453gitHubStandaloneLoaders =
417− gitHubStandaloneLoaderV2 :|
454+ gitHubStandaloneLoaderV4 :|
418455 [ gitHubStandaloneLoaderV3
456+ , gitHubStandaloneLoaderV2
419457 , gitHubStandaloneLoaderV1
420458 ]
421459
@@ -455,10 +493,23 @@ gitHubStandaloneLoaderV3 = T.unlines
455493 , "in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))"
456494 ]
457495
496+gitHubStandaloneLoaderV4 :: Text
497+gitHubStandaloneLoaderV4 = T.unlines
498+ [ "# DO NOT HAND-EDIT THIS FILE"
499+ , "let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:"
500+ , " if !fetchSubmodules && !private then builtins.fetchTarball {"
501+ , " url = \"https://github.com/${owner}/${repo}/archive/${rev}.tar.gz\"; inherit sha256;"
502+ , " } else (import <nixpkgs> {}).fetchFromGitHub {"
503+ , " inherit owner repo rev sha256 fetchSubmodules private;"
504+ , " };"
505+ , "in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))"
506+ ]
507+
458508plainGitStandaloneLoaders :: NonEmpty Text
459509plainGitStandaloneLoaders =
460− plainGitStandaloneLoaderV1 :|
461− [ plainGitStandaloneLoaderV2
510+ plainGitStandaloneLoaderV3 :|
511+ [ plainGitStandaloneLoaderV1
512+ , plainGitStandaloneLoaderV2
462513 ]
463514
464515plainGitStandaloneLoaderV1 :: Text
@@ -479,6 +530,22 @@ plainGitStandaloneLoaderV2 = T.unlines
479530 , "in import (fetchGit (builtins.fromJSON (builtins.readFile ./git.json)))"
480531 ]
481532
533+plainGitStandaloneLoaderV3 :: Text
534+plainGitStandaloneLoaderV3 = T.unlines
535+ [ "# DO NOT HAND-EDIT THIS FILE"
536+ , "let fetch = {url, rev, ref ? null, sha256 ? null, fetchSubmodules ? false, private ? false, ...}:"
537+ , " let realUrl = let firstChar = builtins.substring 0 1 url; in"
538+ , " if firstChar == \"/\" then /. + url"
539+ , " else if firstChar == \".\" then ./. + url"
540+ , " else url;"
541+ , " in if !fetchSubmodules && private then builtins.fetchGit {"
542+ , " url = realUrl; inherit rev;"
543+ , " } else (import <nixpkgs> {}).fetchgit {"
544+ , " url = realUrl; inherit rev sha256;"
545+ , " };"
546+ , "in import (fetch (builtins.fromJSON (builtins.readFile ./git.json)))"
547+ ]
548+
482549thunkFileNames :: [FilePath]
483550thunkFileNames = ["default.nix", "github.json", "git.json"]
484551
@@ -520,7 +587,7 @@ nixBuildThunkAttrWithCache thunkDir attr = do
520587 Just c -> return c
521588 Nothing -> do
522589 putLog Warning $ T.pack $ mconcat [thunkDir, ": ", attr, " not cached, building ..."]
523− _ <- nixCmd $ NixCmd_Build$ def
590+ _ <- nixCmd $ NixCmd_Build $ def
524591 Lens.& nixBuildConfig_outLink Lens..~ OutLink_IndirectRoot cachePath
525592 Lens.& nixCmdConfig_target Lens..~ Target
526593 { _target_path = Just thunkDir
@@ -568,13 +635,13 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
568635 Right (ThunkData_Packed _) -> do
569636 let tmpThunk = tmpDir </> "thunk"
570637 callProcessAndLogOutput (Notice, Error) $
571− proc "cp" ["-r", "-T", thunkDir, tmpThunk]
638+ proc cp ["-r", "-T", thunkDir, tmpThunk]
572639 return tmpThunk
573− Right _ -> failWith $ "Thunk is not packed"
640+ Right _ -> failWith "Thunk is not packed"
574641 updateThunkFromTmp p' = do
575− _ <- packThunk' True False p'
642+ _ <- packThunk' True (ThunkPackConfig False (ThunkConfig Nothing)) p'
576643 callProcessAndLogOutput (Notice, Error) $
577− proc "cp" ["-r", "-T", p', p]
644+ proc cp ["-r", "-T", p', p]
578645
579646finalMsg :: Bool -> (a -> Text) -> Maybe (a -> Text)
580647finalMsg noTrail s = if noTrail then Nothing else Just s
@@ -606,7 +673,7 @@ unpackThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from with
606673 let git = callProcessAndLogOutput (Notice, Notice) . gitProc tmpRepo
607674 git $ [ "clone" ]
608675 ++ ("--recursive" <$ guard (_gitSource_fetchSubmodules s))
609− ++ [ T.unpack $ render $ _gitSource_url s ]
676+ ++ [ T.unpack $ gitUriToText $ _gitSource_url s ]
610677 ++ do branch <- maybeToList $ _gitSource_branch s
611678 [ "--branch", T.unpack $ untagName branch ]
612679 git ["reset", "--hard", Ref.toHexString $ _thunkRev_commit $ _thunkPtr_rev tptr]
@@ -615,31 +682,39 @@ unpackThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from with
615682
616683 liftIO $ createDirectory obGitDir
617684 callProcessAndLogOutput (Notice, Error) $
618− proc "cp" ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
685+ proc cp ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
619686 callProcessAndLogOutput (Notice, Error) $
620687 proc "rm" ["-r", thunkDir]
621688 callProcessAndLogOutput (Notice, Error) $
622689 proc "mv" ["-T", tmpRepo, thunkDir]
623690
624691--TODO: add a rollback mode to pack to the original thunk
625−packThunk :: MonadObelisk m => Bool -> FilePath -> m ThunkPtr
692+packThunk :: MonadObelisk m => ThunkPackConfig -> FilePath -> m ThunkPtr
626693packThunk = packThunk' False
627694
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
695+packThunk' :: MonadObelisk m => Bool -> ThunkPackConfig -> FilePath -> m ThunkPtr
696+packThunk' noTrail (ThunkPackConfig force thunkConfig) thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
630697 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
631698 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
632699 Right (ThunkData_Checkout _) -> do
633700 withSpinner' ("Packing thunk " <> T.pack thunkDir)
634701 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
635− thunkPtr <- getThunkPtr (not force) thunkDir
702+ thunkPtr <- modifyThunkPtrByConfig thunkConfig <$> getThunkPtr (not force) thunkDir
636703 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
637704 liftIO $ createThunk thunkDir thunkPtr
638705 pure thunkPtr
639706
707+modifyThunkPtrByConfig :: ThunkConfig -> ThunkPtr -> ThunkPtr
708+modifyThunkPtrByConfig (ThunkConfig markPrivate') ptr = case markPrivate' of
709+ Nothing -> ptr
710+ Just markPrivate -> ptr { _thunkPtr_source = case _thunkPtr_source ptr of
711+ ThunkSource_Git s -> ThunkSource_Git $ s { _gitSource_private = markPrivate }
712+ ThunkSource_GitHub s -> ThunkSource_GitHub $ s { _gitHubSource_private = markPrivate }
713+ }
714+
640715getThunkPtr :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
641716getThunkPtr checkClean thunkDir = do
642− when checkClean $ ensureCleanGitRepo thunkDir True $
717+ when checkClean $ ensureCleanGitRepo thunkDir True
643718 "thunk pack: thunk checkout contains unsaved modifications"
644719
645720 -- Check whether there are any stashes
@@ -661,7 +736,7 @@ getThunkPtr checkClean thunkDir = do
661736 <$> T.lines
662737 <$> readGitProcess thunkDir ["rev-parse", "HEAD"]
663738 case b of
664− (Just "HEAD") -> failWith $ T.unlines $
739+ (Just "HEAD") -> failWith $ T.unlines
665740 [ "thunk pack: You are in 'detached HEAD' state."
666741 , "If you want to pack at the current ref \
667742 \then please create a new branch with 'git checkout -b <new-branch-name>' and push this upstream."
@@ -733,7 +808,7 @@ getThunkPtr checkClean thunkDir = do
733808 [ "thunk pack: Certain branches in the thunk have commits not yet pushed upstream:"
734809 , ""
735810 ] ++
736− (flip map (Map.toList nonGood) $ \(branch, (upstream, (ahead, behind))) -> mconcat
811+ flip map (Map.toList nonGood) (\(branch, (upstream, (ahead, behind))) -> mconcat
737812 [" ", branch, " ahead: ", T.pack (show ahead), " behind: ", T.pack (show behind), " remote branch ", upstream]) ++
738813 [ ""
739814 , "Please push these upstream and try again. (Or just fetch, if they are somehow \
@@ -741,7 +816,7 @@ getThunkPtr checkClean thunkDir = do
741816 ]
742817
743818 -- 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."
819+ putLog Informational "All changes safe in git remotes. OK to pack thunk."
745820
746821 let remote = maybe "origin" snd $ flip Map.lookup headUpstream =<< mCurrentBranch
747822
@@ -773,7 +848,7 @@ getLatestRev os = do
773848-- performance. If that doesn't work (e.g. authentication issue), we fall back
774849-- on just doing things the normal way for git repos in general, and save it as
775850-- a regular git thunk.
776−uriThunkPtr :: MonadObelisk m => URI -> Maybe Text -> Maybe Text -> m ThunkPtr
851+uriThunkPtr :: MonadObelisk m => GitUri -> Maybe Text -> Maybe Text -> m ThunkPtr
777852uriThunkPtr uri mbranch mcommit = do
778853 commit <- case mcommit of
779854 Nothing -> gitGetCommitBranch uri mbranch >>= return . snd
@@ -784,7 +859,7 @@ uriThunkPtr uri mbranch mcommit = do
784859 case rev of
785860 Right r -> pure (ThunkSource_GitHub s, r)
786861 Left e -> do
787− putLog Warning $ "\
862+ putLog Warning "\
788863\Failed to fetch archive from GitHub. This is probably a private repo. \
789864\Falling back on normal fetchgit. Original failure:"
790865 errorToWarning e
@@ -801,33 +876,33 @@ uriThunkPtr uri mbranch mcommit = do
801876-- If the thunk is a GitHub thunk and fails, we do *not* fall back like with
802877-- `uriThunkPtr`. Unlike a plain URL, a thunk src explicitly states which method
803878-- 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
879+uriToThunkSource :: GitUri -> Maybe Text -> ThunkSource
880+uriToThunkSource (GitUri u)
881+ | Right uriAuth <- URI.uriAuthority u
882+ , Just scheme <- URI.unRText <$> URI.uriScheme u
808883 , case scheme of
809− "ssh" -> uriAuth == Authority
810− { authUserInfo = Just $ UserInfo (fromRight' $ mkUsername "git") Nothing
811− , authHost = fromRight' $ mkHost "github.com"
812− , authPort = Nothing
884+ "ssh" -> uriAuth == URI.Authority
885+ { URI.authUserInfo = Just $ URI.UserInfo (fromRight' $ URI.mkUsername "git") Nothing
886+ , URI.authHost = fromRight' $ URI.mkHost "github.com"
887+ , URI.authPort = Nothing
813888 }
814889 s -> s `L.elem` [ "git", "https", "http" ] -- "http:" just redirects to "https:"
815− && unRText (authHost uriAuth) == "github.com"
816− , Just (_, owner :| [repoish]) <- uriPath u
890+ && URI.unRText (URI.authHost uriAuth) == "github.com"
891+ , Just (_, owner :| [repoish]) <- URI.uriPath u
817892 = \mbranch -> ThunkSource_GitHub $ GitHubSource
818− { _gitHubSource_owner = N $ unRText owner
893+ { _gitHubSource_owner = N $ URI.unRText owner
819894 , _gitHubSource_repo = N $ let
820− repoish' = unRText repoish
821− in case T.stripSuffix ".git" repoish' of
822− Just repo -> repo
823− Nothing -> repoish'
895+ repoish' = URI.unRText repoish
896+ in fromMaybe repoish' $ T.stripSuffix ".git" repoish'
824897 , _gitHubSource_branch = N <$> mbranch
898+ , _gitHubSource_private = False -- TODO: Can we try something to infer this?
825899 }
826900
827901 | otherwise = \mbranch -> ThunkSource_Git $ GitSource
828− { _gitSource_url = u
902+ { _gitSource_url = GitUri u
829903 , _gitSource_branch = N <$> mbranch
830904 , _gitSource_fetchSubmodules = False -- TODO: How do we determine if this should be true?
905+ , _gitSource_private = False -- TODO: Can we try something to infer this?
831906 }
832907
833908-- Funny signature indicates no effects depend on the optional branch name.
@@ -840,19 +915,19 @@ githubThunkRev
840915githubThunkRev s commit = do
841916 owner <- forcePP $ _gitHubSource_owner s
842917 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
918+ revTarball <- URI.mkPathPiece $ commit <> ".tar.gz"
919+ let archiveUri = GitUri $ URI.URI
920+ { URI.uriScheme = Just $ fromRight' $ URI.mkScheme "https"
921+ , URI.uriAuthority = Right $ URI.Authority
922+ { URI.authUserInfo = Nothing
923+ , URI.authHost = fromRight' $ URI.mkHost "github.com"
924+ , URI.authPort = Nothing
850925 }
851− , uriPath = Just ( False
852− , owner :| [ repo, fromRight' $ mkPathPiece "archive", revTarball ]
926+ , URI.uriPath = Just ( False
927+ , owner :| [ repo, fromRight' $ URI.mkPathPiece "archive", revTarball ]
853928 )
854− , uriQuery = []
855− , uriFragment = Nothing
929+ , URI.uriQuery = []
930+ , URI.uriFragment = Nothing
856931 }
857932 hash <- getNixSha256ForUriUnpacked archiveUri
858933 putLog Debug $ "Nix sha256 is " <> hash
@@ -861,8 +936,8 @@ githubThunkRev s commit = do
861936 , _thunkRev_nixSha256 = hash
862937 }
863938 where
864− forcePP :: Name entity -> m (RText 'PathPiece)
865− forcePP = mkPathPiece . untagName
939+ forcePP :: Name entity -> m (URI.RText 'URI.PathPiece)
940+ forcePP = URI.mkPathPiece . untagName
866941
867942gitThunkRev
868943 :: MonadObelisk m
@@ -871,11 +946,11 @@ gitThunkRev
871946 -> m ThunkRev
872947gitThunkRev s commit = do
873948 let u = _gitSource_url s
874− protocols = ["https", "ssh", "git"]
875− Just scheme <- pure $ unRText <$> uriScheme u
949+ protocols = ["file", "https", "ssh", "git"]
950+ scheme = maybe "file" URI.unRText $ URI.uriScheme $ (\(GitUri x) -> x) u
876951 unless (T.toLower scheme `elem` protocols) $
877952 failWith $ "obelisk currently only supports "
878− <> T.intercalate ", " protocols <> " protocols for non-GitHub remotes"
953+ <> T.intercalate ", " protocols <> " protocols for plain Git remotes"
879954 hash <- nixPrefetchGit u commit $ _gitSource_fetchSubmodules s
880955 putLog Informational $ "Nix sha256 is " <> hash
881956 pure $ ThunkRev
@@ -889,10 +964,10 @@ gitThunkRev s commit = do
889964-- If the branch name is passed in, it is returned exactly as-is. If it is not
890965-- passed it, the default branch of the repo is used instead.
891966gitGetCommitBranch
892− :: MonadObelisk m => URI -> Maybe Text -> m (Text, CommitId)
967+ :: MonadObelisk m => GitUri -> Maybe Text -> m (Text, CommitId)
893968gitGetCommitBranch uri mbranch = withExitFailMessage ("Failure for git remote " <> uriMsg) $ do
894969 (_, bothMaps) <- gitLsRemote
895− (T.unpack $ render uri)
970+ (T.unpack $ gitUriToText uri)
896971 (GitRef_Branch <$> mbranch)
897972 Nothing
898973 branch <- case mbranch of
@@ -908,18 +983,21 @@ gitGetCommitBranch uri mbranch = withExitFailMessage ("Failure for git remote "
908983 pure (branch, commit)
909984 where
910985 rethrowE = either failWith pure
911− uriMsg = render uri
986+ uriMsg = gitUriToText uri
987+
988+parseGitUri :: Text -> Maybe GitUri
989+parseGitUri x = GitUri <$> (parseFileURI x <|> parseAbsoluteURI x <|> parseSshShorthand x)
912990
913−parseGitUri :: Text -> Maybe URI
914−parseGitUri x = parseAbsoluteURI x <|> parseSshShorthand x
991+parseFileURI :: Text -> Maybe URI.URI
992+parseFileURI uri = if "/" `T.isPrefixOf` uri then parseAbsoluteURI ("file://" <> uri) else Nothing
915993
916−parseAbsoluteURI :: Text -> Maybe URI
994+parseAbsoluteURI :: Text -> Maybe URI.URI
917995parseAbsoluteURI uri = do
918− parsedUri <- mkURI uri
919− guard $ isPathAbsolute parsedUri
920− pure $ parsedUri
996+ parsedUri <- URI.mkURI uri
997+ guard $ URI.isPathAbsolute parsedUri
998+ pure parsedUri
921999
922−parseSshShorthand :: Text -> Maybe URI
1000+parseSshShorthand :: Text -> Maybe URI.URI
9231001parseSshShorthand uri = do
9241002 -- This is what git does to check that the remote
9251003 -- is not a local file path when parsing shorthand.
@@ -932,4 +1010,4 @@ parseSshShorthand uri = do
9321010 -- This check is used to disambiguate a filepath containing a colon from shorthand
9331011 guard $ isNothing (T.findIndex (=='/') authAndHostname)
9341012 && not (T.null colonAndPath)
935− mkURI properUri
1013+ URI.mkURI properUri
lib/command/src/Obelisk/Command/Utils.hs+28−14
@@ -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)
@@ -23,7 +24,7 @@ import Data.Void (Void)
2324import System.Directory (canonicalizePath)
2425import System.Environment (getExecutablePath)
2526import System.Exit (ExitCode)
26−import qualified System.Process as P
27+import System.Which (staticWhich)
2728import Text.Megaparsec as MP
2829import Text.Megaparsec.Char as MP
2930
@@ -33,6 +34,9 @@ import Obelisk.CliApp
3334getObeliskExe :: IO FilePath
3435getObeliskExe = getExecutablePath >>= canonicalizePath
3536
37+cp :: FilePath
38+cp = $(staticWhich "cp")
39+
3640-- Check whether the working directory is clean
3741checkGitCleanStatus :: MonadObelisk m => FilePath -> Bool -> m Bool
3842checkGitCleanStatus repo withIgnored = do
@@ -61,20 +65,30 @@ initGit repo = do
6165 git ["add", "."]
6266 git ["commit", "-m", "Initial commit."]
6367
64−gitProcNoRepo :: [String] -> P.CreateProcess
65−gitProcNoRepo = P.proc "git"
68+gitProcNoRepo :: [String] -> ProcessSpec
69+gitProcNoRepo args = setEnvOverride (M.singleton "GIT_TERMINAL_PROMPT" "0" <>) $ proc "git" args
6670
67−gitProc :: FilePath -> [String] -> P.CreateProcess
71+gitProc :: FilePath -> [String] -> ProcessSpec
6872gitProc repo = gitProcNoRepo . runGitInDir
6973 where
7074 runGitInDir args' = case filter (not . null) args' of
7175 args@("clone":_) -> args <> [repo]
7276 args -> ["-C", repo] <> args
7377
74−-- | Recursively copy a directory using `cp -a`
75−copyDir :: FilePath -> FilePath -> P.CreateProcess
78+isolateGitProc :: ProcessSpec -> ProcessSpec
79+isolateGitProc = setEnvOverride (overrides <>)
80+ where
81+ overrides = M.fromList
82+ [ ("HOME", "/dev/null")
83+ , ("GIT_CONFIG_NOSYSTEM", "1")
84+ , ("GIT_TERMINAL_PROMPT", "0")
85+ , ("GIT_SSH_COMMAND", "ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no -o GSSAPIAuthentication=no")
86+ ]
87+
88+-- | Recursively copy a directory using `cp -a` -- TODO: Should use -rT instead of -a
89+copyDir :: FilePath -> FilePath -> ProcessSpec
7690copyDir src dest =
77− (P.proc "cp" ["-a", ".", dest]) { P.cwd = Just src }
91+ setCwd (Just src) $ proc cp ["-a", ".", dest] -- TODO: This will break if dest is relative since we change cwd
7892
7993readGitProcess :: MonadObelisk m => FilePath -> [String] -> m Text
8094readGitProcess repo = readProcessAndLogOutput (Debug, Notice) . gitProc repo
@@ -87,15 +101,15 @@ processToShellString cmd args = unwords $ map quoteAndEscape (cmd : args)
87101 where quoteAndEscape x = T.unpack $ "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
88102
89103-- | A simpler wrapper for CliApp's most used process function with sensible defaults.
90−runProc :: MonadObelisk m => P.CreateProcess -> m ()
104+runProc :: MonadObelisk m => ProcessSpec -> m ()
91105runProc = callProcessAndLogOutput (Notice, Error)
92106
93107-- | Like runProc, but all output goes to Debug logging level
94−runProcSilently :: MonadObelisk m => P.CreateProcess -> m ()
108+runProcSilently :: MonadObelisk m => ProcessSpec -> m ()
95109runProcSilently = callProcessAndLogOutput (Debug, Debug)
96110
97111-- | A simpler wrapper for CliApp's readProcessAndLogStderr with sensible defaults.
98−readProc :: MonadObelisk m => P.CreateProcess -> m Text
112+readProc :: MonadObelisk m => ProcessSpec -> m Text
99113readProc = readProcessAndLogOutput (Debug, Error)
100114
101115tshow :: Show a => a -> Text
@@ -129,13 +143,13 @@ gitLsRemote repository mRef mBranch = do
129143 (exitCode, out, _err) <- case mBranch of
130144 Nothing -> readCreateProcessWithExitCode $ gitProcNoRepo $
131145 ["ls-remote", "--exit-code", "--symref", repository]
132− ++ (maybeToList $ T.unpack . showGitRef <$> mRef)
133− Just branchName -> readCreateProcessWithExitCode $ gitProcNoRepo $
146+ ++ maybeToList (T.unpack . showGitRef <$> mRef)
147+ Just branchName -> readCreateProcessWithExitCode $ gitProcNoRepo
134148 ["ls-remote", "--exit-code", repository, branchName]
135149 let t = T.pack out
136150 maps <- case MP.runParser parseLsRemote "" t of
137− Left err -> failWith $ T.pack $ MP.parseErrorPretty' t err
138− Right table -> pure $ bimap M.fromList M.fromList $ partitionEithers $ table
151+ Left err -> failWith $ T.pack $ MP.errorBundlePretty err
152+ Right table -> pure $ bimap M.fromList M.fromList $ partitionEithers table
139153 putLog Debug $ "git ls-remote maps: " <> T.pack (show maps)
140154 pure (exitCode, maps)
141155
lib/command/src/Obelisk/Command/VmBuilder.hs+2−2
@@ -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
@@ -156,7 +155,8 @@ testLinuxBuild stateDir
156155 | System.Info.os == "linux" = failWith "Using the docker builder is not necessary on linux."
157156 | otherwise = do
158157 (exitCode, _stdout, stderr) <- readCreateProcessWithExitCode $ proc "nix-build"
159− [ "-E", "(import <nixpkgs> { system = \"x86_64-linux\"; }).writeText \"test\" builtins.currentTime"
158+ [ "--no-out-link"
159+ , "-E", "(import <nixpkgs> { system = \"x86_64-linux\"; }).writeText \"test\" builtins.currentTime"
160160 , "--builders", nixBuildersArgString stateDir
161161 ]
162162 unless (exitCode == ExitSuccess) $ do
lib/executable-config/default.nix+1−1
@@ -20,7 +20,7 @@ let injectConfig = config: assets: nixpkgs.runCommand "inject-config" {} (''
2020 '');
2121in with nixpkgs.haskell.lib; {
2222 haskellOverlay = self: super: {
23− obelisk-executable-config-lookup = self.callPackage (filterGitSource ./lookup) {};
23+ obelisk-executable-config-lookup = self.callCabal2nix "obelisk-executable-config-lookup" (filterGitSource ./lookup) {};
2424 };
2525 platforms = {
2626 android = {
lib/executable-config/lookup/default.nixdeleted−26
@@ -1,26 +0,0 @@
1−{ hostPlatform, ghc
2−, mkDerivation, base, base64-bytestring, bytestring, filepath, stdenv, text, transformers
3−, transformers-base, ref-tf, primitive, monad-control, reflex, reflex-dom
4−, jsaddle
5−, android-activity ? null, jsaddle-wkwebview ? null, ghcjs-dom ? null
6−}:
7−let isAndroid = hostPlatform.libc == "bionic";
8− isIOS = hostPlatform.isDarwin && hostPlatform.isAarch64;
9− isGhcjs = ghc.isGhcjs or false;
10−in mkDerivation {
11− pname = "obelisk-executable-config-lookup";
12− version = "0.1";
13− src = ./.;
14− libraryHaskellDepends = [
15− base bytestring filepath text transformers transformers-base ref-tf primitive monad-control reflex reflex-dom jsaddle
16− ] ++ (if isAndroid then [
17− android-activity
18− ] else if isIOS then [
19− jsaddle-wkwebview
20− ] else if isGhcjs then [
21− ghcjs-dom
22− ] else [
23− ]);
24− license = stdenv.lib.licenses.bsd3;
25−}
26−
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-ghcjs/Obelisk/ExecutableConfig/Lookup.hs+17−8
@@ -15,28 +15,37 @@ import GHCJS.DOM.Document (getHead)
1515import GHCJS.DOM.DOMStringMap (get)
1616import GHCJS.DOM.Element (getInnerHTML)
1717import GHCJS.DOM.HTMLElement (HTMLElement(HTMLElement), getDataset)
18−import GHCJS.DOM.NodeList (item, getLength)
18+import GHCJS.DOM.NodeList (IsNodeList, item, getLength)
1919import GHCJS.DOM.ParentNode (querySelectorAll)
20−import GHCJS.DOM.Types (Node(Node), castTo)
20+import GHCJS.DOM.Types (MonadJSM, Node(Node), castTo)
2121
2222getConfigs :: IO (Map Text ByteString)
2323getConfigs = do
2424 Just doc <- currentDocument
2525 Just hd <- getHead doc
26− nodes <- collToList =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
26+ nodes <- nodeListNodes =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
2727 fmap Map.fromList $ for nodes $ \node -> do
2828 e <- castTo HTMLElement node >>= \case
2929 Nothing -> error "Found node with data attribute obelisk-executable-config-inject-key that is not an HTMLElement."
3030 Just htmlE -> return htmlE
3131 dataset <- getDataset e
3232 (,)
33− <$> get dataset ("obelisk-executable-config-inject-key" :: Text)
33+ -- the key is camelCased: https://html.spec.whatwg.org/multipage/dom.html#dom-dataset
34+ <$> get dataset ("obeliskExecutableConfigInjectKey" :: Text)
3435 <*> (fmap decodeOrFail (getInnerHTML e))
3536 where
36− collToList es = do
37− len <- getLength es
38− list <- traverse (item es) [0..len-1]
39− pure $ catMaybes list
4037 decodeOrFail x = case B64.decode (T.encodeUtf8 x) of
4138 Left e -> error ("Obelisk.ExecutableConfig.Lookup.getConfigs: error when decoding base64: " ++ e)
4239 Right x' -> x'
40+
41+-- | Collect all nodes in the node list.
42+--
43+-- TODO: this and the version in obelisk-frontend should be
44+-- upstreamed to jsaddle.
45+nodeListNodes :: (IsNodeList l, MonadJSM m) => l -> m [Node]
46+nodeListNodes es = do
47+ len <- getLength es
48+ -- Warning! len is unsigned. If the NodeList is empty, we must avoid
49+ -- accidentally traversing over [0..maxBound::Word]
50+ nodes <- traverse (item es) $ if len == 0 then [] else [0..len-1]
51+ pure $ catMaybes nodes
lib/executable-config/lookup/src/Obelisk/Configs.hs+5
@@ -15,6 +15,7 @@ module Obelisk.Configs
1515 , ConfigsT
1616 , runConfigsT
1717 , mapConfigsT
18+ , getTextConfig
1819 ) where
1920
2021import Control.Applicative
@@ -31,6 +32,7 @@ import Data.ByteString (ByteString)
3132import Data.Map (Map)
3233import qualified Data.Map as Map
3334import Data.Text (Text)
35+import qualified Data.Text.Encoding as T
3436import Reflex
3537import Reflex.Host.Class
3638import Reflex.Dom
@@ -50,6 +52,9 @@ class Monad m => HasConfigs m where
5052instance Monad m => HasConfigs (ConfigsT m) where
5153 getConfigs = ConfigsT ask
5254
55+getTextConfig :: HasConfigs m => Text -> m (Maybe Text)
56+getTextConfig k = fmap T.decodeUtf8 <$> getConfig k
57+
5358instance HasConfigs m => HasConfigs (BehaviorWriterT t w m)
5459instance HasConfigs m => HasConfigs (DynamicWriterT t w m)
5560instance HasConfigs m => HasConfigs (EventWriterT t w m)
lib/frontend/src/Obelisk/Frontend.hs+24−16
@@ -43,11 +43,11 @@ import qualified GHCJS.DOM as DOM
4343import qualified GHCJS.DOM.Types as DOM
4444import qualified GHCJS.DOM.History as DOM
4545import qualified GHCJS.DOM.Window as DOM
46−import Language.Javascript.JSaddle (JSM, jsNull)
46+import Language.Javascript.JSaddle (MonadJSM, JSM, jsNull)
4747import GHCJS.DOM (currentDocument)
4848import GHCJS.DOM.Document (getHead)
49−import GHCJS.DOM.Node (removeChild_)
50−import GHCJS.DOM.NodeList (item, getLength)
49+import GHCJS.DOM.Node (Node, removeChild_)
50+import GHCJS.DOM.NodeList (IsNodeList, item, getLength)
5151import GHCJS.DOM.ParentNode (querySelectorAll)
5252import Obelisk.Frontend.Cookie
5353import Obelisk.Route.Frontend
@@ -83,6 +83,7 @@ type ObeliskWidget js t route m =
8383 , PrebuildAgnostic t route (Client m)
8484 , HasConfigs m
8585 , HasCookies m
86+ , MonadIO (Performable m)
8687 )
8788
8889type PrebuildAgnostic t route m =
@@ -105,13 +106,20 @@ removeHTMLConfigs :: JSM ()
105106removeHTMLConfigs = void $ runMaybeT $ do
106107 doc <- MaybeT currentDocument
107108 hd <- MaybeT $ getHead doc
108− es <- collToList =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
109+ es <- nodeListNodes =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
109110 for_ es $ removeChild_ hd
110− where
111− collToList es = do
112− len <- getLength es
113− lst <- traverse (item es) $ take (fromIntegral len) $ [0..] -- fun with unsigned types ...
114− pure $ catMaybes lst
111+
112+-- | Collect all nodes in the node list.
113+--
114+-- TODO: this and the version in exe-config/ghcjs/lookup should be
115+-- upstreamed to jsaddle.
116+nodeListNodes :: (IsNodeList l, MonadJSM m) => l -> m [Node]
117+nodeListNodes es = do
118+ len <- getLength es
119+ -- Warning! len is unsigned. If the NodeList is empty, we must avoid
120+ -- accidentally traversing over [0..maxBound::Word]
121+ nodes <- traverse (item es) $ if len == 0 then [] else [0..len-1]
122+ pure $ catMaybes nodes
115123
116124setInitialRoute :: Bool -> JSM ()
117125setInitialRoute useHash = do
@@ -137,7 +145,7 @@ data FrontendMode = FrontendMode
137145-- Selects FrontendMode based on platform; this doesn't work for jsaddle-warp
138146runFrontend
139147 :: forall backendRoute route
140− . Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
148+ . Encoder Identity Identity (R (FullRoute backendRoute route)) PageName
141149 -> Frontend (R route)
142150 -> JSM ()
143151runFrontend validFullEncoder frontend = do
@@ -165,14 +173,14 @@ runFrontend validFullEncoder frontend = do
165173 runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend
166174
167175runFrontendWithConfigsAndCurrentRoute
168− :: forall backendRoute route
176+ :: forall backendRoute frontendRoute
169177 . FrontendMode
170178 -> Map Text ByteString
171− -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
172− -> Frontend (R route)
179+ -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName
180+ -> Frontend (R frontendRoute)
173181 -> JSM ()
174182runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = do
175− let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
183+ let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _FullRoute_Frontend . _ObeliskRoute_App))
176184 errorLeft = \case
177185 Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
178186 Right x -> Identity x
@@ -221,7 +229,7 @@ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = d
221229renderFrontendHtml
222230 :: ( t ~ DomTimeline
223231 , MonadIO m
224− , 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))))))))
225233 )
226234 => Map Text ByteString
227235 -> Cookies
@@ -233,7 +241,7 @@ renderFrontendHtml
233241 -> m ByteString
234242renderFrontendHtml configs cookies urlEnc route frontend headExtra bodyExtra = do
235243 --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
236− 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) $
237245 el "html" $ do
238246 el "head" $ do
239247 baseTag
lib/route/src/Obelisk/Route.hs+249−31
@@ -12,6 +12,7 @@
1212{-# LANGUAGE PatternSynonyms #-}
1313{-# LANGUAGE RankNTypes #-}
1414{-# LANGUAGE ScopedTypeVariables #-}
15+{-# LANGUAGE StandaloneDeriving #-}
1516{-# LANGUAGE TemplateHaskell #-}
1617{-# LANGUAGE TypeApplications #-}
1718{-# LANGUAGE TypeFamilies #-}
@@ -19,9 +20,11 @@
1920{-# LANGUAGE ViewPatterns #-}
2021module Obelisk.Route
2122 ( R
22− , pattern (:/)
23+ , (:.)
2324 , (?/)
2425 , hoistR
26+ , pattern (:.)
27+ , pattern (:/)
2528 , PageName
2629 , PathQuery
2730 , Encoder
@@ -43,6 +46,9 @@ module Obelisk.Route
4346 , checkEnum1EncoderFunc
4447 , unitEncoder
4548 , pathOnlyEncoder
49+ , addPathSegmentEncoder
50+ , pathParamEncoder
51+ , pathLiteralEncoder
4652 , singletonListEncoder
4753 , unpackTextEncoder
4854 , prefixTextEncoder
@@ -63,10 +69,15 @@ module Obelisk.Route
6369 , shadowEncoder
6470 , prismEncoder
6571 , rPrism
72+ , _R
6673 , obeliskRouteEncoder
6774 , obeliskRouteSegment
6875 , pageNameEncoder
6976 , handleEncoder
77+ , FullRoute (..)
78+ , _FullRoute_Frontend
79+ , _FullRoute_Backend
80+ , mkFullRouteEncoder
7081 , ObeliskRoute (..)
7182 , _ObeliskRoute_App
7283 , _ObeliskRoute_Resource
@@ -91,7 +102,9 @@ module Obelisk.Route
91102 , Decoder(..)
92103 , dmapEncoder
93104 , fieldMapEncoder
105+ , pathFieldEncoder
94106 , jsonEncoder
107+ , byteStringsToPageName
95108 ) where
96109
97110import Prelude hiding ((.), id)
@@ -102,8 +115,31 @@ import qualified Control.Categorical.Functor as Cat
102115import Control.Categorical.Bifunctor
103116import Control.Category.Associative
104117import Control.Category.Monoidal
105−import Control.Lens (Identity (..), Prism', makePrisms, itraverse, imap, prism, (^.), re, matching, (^?), _Just, _Nothing, Iso', from, view, Wrapped (..))
118+import Control.Category.Braided
119+import Control.Lens
120+ ( Identity (..)
121+ , (^.)
122+ , (^?)
123+ , _Just
124+ , _Nothing
125+ , Cons(..)
126+ , from
127+ , imap
128+ , iso
129+ , Iso'
130+ , itraverse
131+ , makePrisms
132+ , Prism'
133+ , prism'
134+ , re
135+ , view
136+ , Wrapped (..)
137+ )
106138import Control.Monad.Except
139+import Control.Monad.Writer (execWriter, tell)
140+import qualified Control.Monad.State.Strict as State
141+import Control.Monad.Trans (lift)
142+import qualified Data.ByteString as BS
107143import qualified Data.ByteString.Lazy as BSL
108144import Data.Dependent.Sum (DSum (..))
109145import Data.Dependent.Map (DMap)
@@ -174,6 +210,48 @@ mapSome f (Some a) = Some $ f a
174210hoistR :: (forall x. f x -> g x) -> R f -> R g
175211hoistR f (x :=> Identity y) = f x :/ y
176212
213+--------------------------------------------------------------------------------
214+-- Dealing with pairs (i.e. non-dependently-typed subroutes/paths)
215+--------------------------------------------------------------------------------
216+
217+infixr 5 :.
218+type (:.) = (,)
219+
220+{-# COMPLETE (:.) #-}
221+pattern (:.) :: a -> b -> a :. b
222+pattern a :. b = (a, b)
223+
224+addPathSegmentEncoder
225+ :: ( Applicative check
226+ , MonadError Text parse
227+ )
228+ => Encoder check parse (Text, PageName) PageName
229+addPathSegmentEncoder = unsafeMkEncoder $ EncoderImpl
230+ { _encoderImpl_encode = \(ph, (pt, q)) -> (ph : pt, q)
231+ , _encoderImpl_decode = \(p, q) -> case p of
232+ [] -> throwError "Expected a path segment"
233+ ph : pt -> pure (ph, (pt, q))
234+ }
235+
236+pathParamEncoder
237+ :: forall check parse item rest.
238+ ( Applicative check
239+ , MonadError Text parse
240+ )
241+ => Encoder check parse item Text
242+ -> Encoder check parse rest PageName
243+ -> Encoder check parse (item :. rest) PageName
244+pathParamEncoder itemUnchecked restUnchecked = addPathSegmentEncoder . bimap itemUnchecked restUnchecked
245+
246+pathLiteralEncoder
247+ :: ( Applicative check
248+ , MonadError Text parse
249+ )
250+ => Text
251+ -> Encoder check parse a PageName
252+ -> Encoder check parse a PageName
253+pathLiteralEncoder t e = addPathSegmentEncoder . bimap (unitEncoder t) e . coidl
254+
177255--------------------------------------------------------------------------------
178256-- Encoder fundamentals
179257--------------------------------------------------------------------------------
@@ -207,7 +285,7 @@ decode e x = runIdentity (tryDecode e x)
207285tryDecode :: Encoder Identity parse decoded encoded -> encoded -> parse decoded
208286tryDecode (Encoder (Identity impl)) x = _encoderImpl_decode impl x
209287
210−-- | Similar to 'decode' above, once an encoder has been checked so that its check monad is Identity, it
288+-- | Similar to 'decode', once an encoder has been checked so that its check monad is Identity, it
211289-- can be used to actually encode by using this. Note that while there's no constraint on the parse monad here,
212290-- one should usually be applying decode and encode to the same 'Encoder'
213291encode :: Encoder Identity parse decoded encoded -> decoded -> encoded
@@ -257,6 +335,10 @@ instance Monad parse => Bifunctor (,) (EncoderImpl parse) (EncoderImpl parse) (E
257335 , _encoderImpl_decode = \(a, b) -> liftA2 (,) (_encoderImpl_decode f a) (_encoderImpl_decode g b)
258336 }
259337
338+instance (Monad parse, Applicative check) => Braided (Encoder check parse) (,) where
339+ braid = isoEncoder (iso swap swap)
340+
341+
260342instance (Applicative check, Monad parse) => PFunctor (,) (Encoder check parse) (Encoder check parse) where
261343 first f = bimap f id
262344instance (Applicative check, Monad parse) => QFunctor (,) (Encoder check parse) (Encoder check parse) where
@@ -270,6 +352,34 @@ instance (Traversable f, Monad parse) => Cat.Functor f (EncoderImpl parse) (Enco
270352 , _encoderImpl_decode = traverse $ _encoderImpl_decode ve
271353 }
272354
355+instance Monad parse => PFunctor Either (EncoderImpl parse) (EncoderImpl parse) where
356+ first f = bimap f id
357+instance Monad parse => QFunctor Either (EncoderImpl parse) (EncoderImpl parse) where
358+ second g = bimap id g
359+instance Monad parse => Bifunctor Either (EncoderImpl parse) (EncoderImpl parse) (EncoderImpl parse) where
360+ bimap f g = EncoderImpl
361+ { _encoderImpl_encode = bimap (_encoderImpl_encode f) (_encoderImpl_encode g)
362+ , _encoderImpl_decode = \case
363+ Left a -> Left <$> _encoderImpl_decode f a
364+ Right b -> Right <$> _encoderImpl_decode g b
365+ }
366+
367+instance (Monad parse, Applicative check) => QFunctor Either (Encoder check parse) (Encoder check parse) where
368+ second g = bimap id g
369+instance (Monad parse, Applicative check) => PFunctor Either (Encoder check parse) (Encoder check parse) where
370+ first f = bimap f id
371+instance (Monad parse, Applicative check) => Bifunctor Either (Encoder check parse) (Encoder check parse) (Encoder check parse) where
372+ bimap f g = Encoder $ liftA2 bimap (unEncoder f) (unEncoder g)
373+
374+instance (Applicative check, Monad parse) => Associative (Encoder check parse) Either where
375+ associate = isoEncoder (iso (associate @(->) @Either) disassociate)
376+ disassociate = isoEncoder (iso disassociate associate)
377+
378+instance (Monad parse, Applicative check) => Braided (Encoder check parse) Either where
379+ braid = isoEncoder (iso swap swap)
380+
381+
382+
273383instance (Traversable f, Monad check, Monad parse) => Cat.Functor f (Encoder check parse) (Encoder check parse) where
274384 fmap e = Encoder $ do
275385 ve <- unEncoder e
@@ -447,8 +557,9 @@ chainEncoder cons this rest = Encoder $ do
447557 pure $ EncoderImpl
448558 { _encoderImpl_decode = \v -> do
449559 (here, following) <- _encoderImpl_decode consValid v
450− Some r <- _encoderImpl_decode thisValid here
451− (r :/) <$> _encoderImpl_decode (runIdentity . unEncoder $ rest r) following
560+ _encoderImpl_decode thisValid here >>= \case
561+ Some r ->
562+ (r :/) <$> _encoderImpl_decode (runIdentity . unEncoder $ rest r) following
452563 , _encoderImpl_encode = \(r :/ s) ->
453564 _encoderImpl_encode consValid
454565 ( _encoderImpl_encode thisValid $ Some r
@@ -668,10 +779,47 @@ joinPairTextEncoder = Encoder . \case
668779 _ -> return (kt, T.drop (T.length separator) vt)
669780 }
670781
671−--TODO: Rewrite this by composing the given prism with a lens on the first element of the DSum
672−-- Or, more likely, the user can compose it themselves
673−rPrism :: forall f g. (forall a. Prism' (f a) (g a)) -> Prism' (R f) (R g)
674−rPrism p = prism (\(g :/ x) -> g ^. re p :/ x) (\(f :/ x) -> bimap (:/ x) (:/ x) $ matching p f)
782+-- This slight generalization of 'rPrism' happens to be enough to write all our
783+-- prism combinators so far.
784+dSumPrism
785+ :: forall f f' g
786+ . (forall a. Prism' (f a) (f' a))
787+ -> Prism' (DSum f g) (DSum f' g)
788+dSumPrism p = prism'
789+ (\(f' :=> x) -> f' ^. re p :=> x)
790+ (\(f :=> x) -> (:=> x) <$> (f ^? p))
791+
792+-- already in obelisk
793+rPrism
794+ :: forall f f'
795+ . (forall a. Prism' (f a) (f' a))
796+ -> Prism' (R f) (R f')
797+rPrism p = dSumPrism p
798+
799+dSumPrism'
800+ :: forall f g a
801+ . (forall b. Prism' (f b) (a :~: b))
802+ -> Prism' (DSum f g) (g a)
803+dSumPrism' p = dSumPrism p . iso (\(Refl :=> b) -> b) (Refl :=>)
804+
805+dSumGEqPrism
806+ :: GEq f
807+ => f a
808+ -> Prism' (DSum f g) (g a)
809+dSumGEqPrism variant = dSumPrism' $ prism' (\Refl -> variant) (\x -> geq variant x)
810+
811+-- | Given a 'tag :: f a', make a prism for 'R f'. This generalizes the usual
812+-- prisms for a sum type (the ones that 'mkPrisms' would make), just as 'R'
813+-- generalized a usual sum type.
814+--
815+-- [This is given the '_R' name of the "cannonical" prism not because it is the
816+-- most general, but because it seems the most useful for routes, and 'R' itself
817+-- trades generality for route-specificity.]
818+_R
819+ :: GEq f
820+ => f a
821+ -> Prism' (R f) a
822+_R variant = dSumGEqPrism variant . iso runIdentity Identity
675823
676824-- | An encoder that only works on the items available via the prism. An error will be thrown in the parse monad
677825-- if the prism doesn't match.
@@ -715,6 +863,45 @@ handleEncoder recover e = Encoder $ do
715863-- Actual obelisk route info
716864--------------------------------------------------------------------------------
717865
866+-- | The typical full route type comprising all of an Obelisk application's routes.
867+-- Parameterised by the top level GADTs that define backend and frontend routes, respectively.
868+data FullRoute :: (* -> *) -> (* -> *) -> * -> * where
869+ FullRoute_Backend :: br a -> FullRoute br fr a
870+ FullRoute_Frontend :: ObeliskRoute fr a -> FullRoute br fr a
871+
872+instance (GShow br, GShow fr) => GShow (FullRoute br fr) where
873+ gshowsPrec p = \case
874+ FullRoute_Backend x -> showParen (p > 10) (showString "FullRoute_Backend " . gshowsPrec 11 x)
875+ FullRoute_Frontend x -> showParen (p > 10) (showString "FullRoute_Frontend " . gshowsPrec 11 x)
876+
877+instance (GEq br, GEq fr) => GEq (FullRoute br fr) where
878+ geq (FullRoute_Backend x) (FullRoute_Backend y) = geq x y
879+ geq (FullRoute_Frontend x) (FullRoute_Frontend y) = geq x y
880+ geq _ _ = Nothing
881+
882+instance (GCompare br, GCompare fr) => GCompare (FullRoute br fr) where
883+ gcompare (FullRoute_Backend _) (FullRoute_Frontend _) = GLT
884+ gcompare (FullRoute_Frontend _) (FullRoute_Backend _) = GGT
885+ gcompare (FullRoute_Backend x) (FullRoute_Backend y) = gcompare x y
886+ gcompare (FullRoute_Frontend x) (FullRoute_Frontend y) = gcompare x y
887+
888+instance (UniverseSome br, UniverseSome fr) => UniverseSome (FullRoute br fr) where
889+ universeSome = [Some (FullRoute_Backend x) | Some x <- universeSome]
890+ ++ [Some (FullRoute_Frontend x) | Some x <- universeSome]
891+
892+-- | Build the typical top level application route encoder from a route for handling 404's,
893+-- and segment encoders for backend and frontend routes.
894+mkFullRouteEncoder
895+ :: (GCompare br, GCompare fr, GShow br, GShow fr, UniverseSome br, UniverseSome fr)
896+ => R (FullRoute br fr) -- ^ 404 handler
897+ -> (forall a. br a -> SegmentResult (Either Text) (Either Text) a) -- ^ How to encode a single backend route segment
898+ -> (forall a. fr a -> SegmentResult (Either Text) (Either Text) a) -- ^ How to encode a single frontend route segment
899+ -> Encoder (Either Text) Identity (R (FullRoute br fr)) PageName
900+mkFullRouteEncoder missing backendSegment frontendSegment = handleEncoder (const missing) $
901+ pathComponentEncoder $ \case
902+ FullRoute_Backend backendRoute -> backendSegment backendRoute
903+ FullRoute_Frontend obeliskRoute -> obeliskRouteSegment obeliskRoute frontendSegment
904+
718905-- | A type which can represent Obelisk-specific resource routes, in addition to application specific routes which serve your
719906-- frontend.
720907data ObeliskRoute :: (* -> *) -> * -> * where
@@ -844,7 +1031,7 @@ concat <$> mapM deriveRouteComponent
8441031 ]
8451032
8461033makePrisms ''ObeliskRoute
847−
1034+makePrisms ''FullRoute
8481035deriveGEq ''Void1
8491036deriveGCompare ''Void1
8501037
@@ -852,27 +1039,27 @@ deriveGCompare ''Void1
8521039-- and query string). See 'checkEncoder' for how to produce a checked encoder.
8531040renderBackendRoute
8541041 :: forall br a.
855− Encoder Identity Identity (R (Sum br a)) PageName
1042+ Encoder Identity Identity (R (FullRoute br a)) PageName
8561043 -> R br
8571044 -> Text
858−renderBackendRoute enc = renderObeliskRoute enc . hoistR InL
1045+renderBackendRoute enc = renderObeliskRoute enc . hoistR FullRoute_Backend
8591046
8601047-- | Renders a frontend route with the supplied checked encoder
8611048renderFrontendRoute
8621049 :: forall a fr.
863− Encoder Identity Identity (R (Sum a (ObeliskRoute fr))) PageName
1050+ Encoder Identity Identity (R (FullRoute a fr)) PageName
8641051 -> R fr
8651052 -> Text
866−renderFrontendRoute enc = renderObeliskRoute enc . hoistR (InR . ObeliskRoute_App)
1053+renderFrontendRoute enc = renderObeliskRoute enc . hoistR (FullRoute_Frontend . ObeliskRoute_App)
8671054
8681055-- | Renders a route of the form typically found in an Obelisk project
8691056renderObeliskRoute
8701057 :: forall a b.
871− Encoder Identity Identity (R (Sum a b)) PageName
872− -> R (Sum a b)
1058+ Encoder Identity Identity (R (FullRoute a b)) PageName
1059+ -> R (FullRoute a b)
8731060 -> Text
8741061renderObeliskRoute e r =
875− let enc :: Encoder Identity (Either Text) (R (Sum a b)) PathQuery
1062+ let enc :: Encoder Identity (Either Text) (R (FullRoute a b)) PathQuery
8761063 enc = (pageNameEncoder . hoistParse (pure . runIdentity) e)
8771064 in (T.pack . uncurry (<>)) $ encode enc r
8781065
@@ -882,14 +1069,9 @@ readShowEncoder = singlePathSegmentEncoder . unsafeTshowEncoder
8821069integralEncoder :: (MonadError Text parse, Applicative check, Integral a) => Encoder check parse a Integer
8831070integralEncoder = prismEncoder (Numeric.Lens.integral)
8841071
885−pathSegmentEncoder :: (MonadError Text parse, Applicative check) =>
886− Encoder check parse (Text, PageName) PageName
887−pathSegmentEncoder = unsafeMkEncoder EncoderImpl
888− { _encoderImpl_encode = \(x, (y, z)) -> (x:y, z)
889− , _encoderImpl_decode = \(xss, y) -> case xss of
890− [] -> throwError "not enough path segments"
891− (x:xs) -> pure (x, (xs, y))
892− }
1072+pathSegmentEncoder :: (MonadError Text parse, Applicative check, Cons as as a a) =>
1073+ Encoder check parse (a, (as, b)) (as, b)
1074+pathSegmentEncoder = first (prismEncoder _Cons) . disassociate
8931075
8941076newtype Decoder check parse b a = Decoder { toEncoder :: Encoder check parse a b }
8951077
@@ -917,12 +1099,12 @@ dmapEncoder keyEncoder' valueEncoderFor = unsafeEncoder $ do
9171099 , encode (toEncoder (DMap.findWithDefault (error . keyError $ gshow k') k' valueDecoders)) v'
9181100 )
9191101 , _encoderImpl_decode = \m -> fmap DMap.fromList . forM (Map.toList m) $ \(k,v) -> do
920− Some (k' :: k' t) <- tryDecode keyEncoder k
921− case DMap.lookup k' valueDecoders of
922− Nothing -> throwError . T.pack . keyError $ gshow k'
923− Just (Decoder e) -> do
924− v' <- tryDecode e v
925− return (k' :=> Identity v')
1102+ tryDecode keyEncoder k >>= \case
1103+ Some (k' :: k' t) -> case DMap.lookup k' valueDecoders of
1104+ Nothing -> throwError . T.pack . keyError $ gshow k'
1105+ Just (Decoder e) -> do
1106+ v' <- tryDecode e v
1107+ return (k' :=> Identity v')
9261108 }
9271109
9281110fieldMapEncoder :: forall check parse r.
@@ -943,6 +1125,32 @@ fieldMapEncoder = unsafeEncoder $ do
9431125 Just (Identity v) -> return v
9441126 }
9451127
1128+-- this is in base 4.12 (GHC 8.6);
1129+newtype Ap f a = Ap {getAp :: f a}
1130+
1131+instance (Applicative f, Semigroup a) => Semigroup (Ap f a) where
1132+ Ap x <> Ap y = Ap (liftA2 (<>) x y)
1133+
1134+instance (Applicative f, Monoid a) => Monoid (Ap f a) where
1135+ mappend = (<>)
1136+ mempty = Ap (pure mempty)
1137+
1138+pathFieldEncoder :: forall a p check parse . (HasFields a, Monad check, MonadError Text parse, GCompare (Field a)) => (forall x. Field a x -> Encoder check parse x p) -> Encoder check parse (a, [p]) [p]
1139+pathFieldEncoder fieldEncoder = unsafeEncoder $ do
1140+ fieldEncoderPureMap :: DMap.DMap (Field a) (Decoder Identity parse p) <- getAp $ getConst $ tabulateFieldsA @a $ \f -> Const (Ap $ fmap (DMap.singleton f . Decoder) $ (checkEncoder @Identity) $ fieldEncoder f)
1141+ let fieldEncoderPure :: forall x. Field a x -> Encoder Identity parse x p
1142+ fieldEncoderPure f = toEncoder (DMap.findWithDefault (error "bad") f fieldEncoderPureMap)
1143+ pure $ EncoderImpl
1144+ { _encoderImpl_encode = \(x, rest) -> execWriter $ do
1145+ _ <- traverseWithField (\f x_i -> tell (pure $ encode (fieldEncoderPure f) x_i) *> pure x_i) x
1146+ tell rest
1147+ , _encoderImpl_decode = State.runStateT $ tabulateFieldsA $ \f -> State.get >>= \case
1148+ [] -> throwError $ T.pack "not enough path components"
1149+ p:ps -> do
1150+ State.put ps
1151+ lift $ tryDecode (fieldEncoderPure f) p
1152+ }
1153+
9461154-- | Use ToJSON/FromJSON to encode to Text. The correctness of this encoder is dependent on the encoding being injective and round-tripping correctly.
9471155jsonEncoder :: forall check parse r.
9481156 ( ToJSON r
@@ -959,4 +1167,14 @@ jsonEncoder = unsafeEncoder $ do
9591167 Right x -> return x
9601168 }
9611169
1170+-- Useful for app server integration.
1171+-- p must not start with slashes
1172+byteStringsToPageName :: BS.ByteString -> BS.ByteString -> PageName
1173+byteStringsToPageName p q =
1174+ let pageNameEncoder' :: Encoder Identity Identity PageName (String, String)
1175+ pageNameEncoder' = bimap
1176+ (unpackTextEncoder . pathSegmentsTextEncoder . listToNonEmptyEncoder)
1177+ (unpackTextEncoder . queryParametersTextEncoder . toListMapEncoder)
1178+ in decode pageNameEncoder' (T.unpack (T.decodeUtf8 p), T.unpack (T.decodeUtf8 q))
1179+
9621180--TODO: decodeURIComponent as appropriate
lib/route/src/Obelisk/Route/Frontend.hs+36−9
@@ -35,6 +35,8 @@ module Obelisk.Route.Frontend
3535 , mapRoutedT
3636 , subRoute
3737 , subRoute_
38+ , subPairRoute
39+ , subPairRoute_
3840 , maybeRoute
3941 , maybeRoute_
4042 , maybeRouted
@@ -51,6 +53,7 @@ module Obelisk.Route.Frontend
5153 , runRouteToUrlT
5254 , mapRouteToUrlT
5355 , routeLink
56+ , routeLinkDynAttr
5457 , dynRouteLink
5558 , adaptedUriPath
5659 , setAdaptedUriPath
@@ -72,11 +75,13 @@ import Control.Monad.Trans.Control
7275import Data.Coerce
7376import Data.Dependent.Sum (DSum (..))
7477import Data.GADT.Compare
78+import Data.Map (Map)
7579import Data.Monoid
7680import Data.Proxy
7781import Data.Text (Text)
7882import qualified Data.Text as T
7983import Data.Functor.Compose
84+import Data.Functor.Misc
8085import Reflex.Class
8186import Reflex.Host.Class
8287import Reflex.PostBuild.Class
@@ -202,10 +207,18 @@ subRoute_ :: (MonadFix m, MonadHold t m, GEq r, Adjustable t m) => (forall a. r
202207subRoute_ f = factorRouted $ strictDynWidget_ $ \(c :=> r') -> do
203208 runRoutedT (f c) r'
204209
210+-- | Like 'subRoute_', but with a pair rather than an R
211+subPairRoute_ :: (MonadFix m, MonadHold t m, Eq a, Adjustable t m) => (a -> RoutedT t b m ()) -> RoutedT t (a, b) m ()
212+subPairRoute_ f = withRoutedT (fmap (\(a, b) -> Const2 a :/ b)) $ subRoute_ (\(Const2 a) -> f a)
213+
205214subRoute :: (MonadFix m, MonadHold t m, GEq r, Adjustable t m) => (forall a. r a -> RoutedT t a m b) -> RoutedT t (R r) m (Dynamic t b)
206215subRoute f = factorRouted $ strictDynWidget $ \(c :=> r') -> do
207216 runRoutedT (f c) r'
208217
218+-- | Like 'subRoute_', but with a pair rather than an R
219+subPairRoute :: (MonadFix m, MonadHold t m, Eq a, Adjustable t m) => (a -> RoutedT t b m c) -> RoutedT t (a, b) m (Dynamic t c)
220+subPairRoute f = withRoutedT (fmap (\(a, b) -> Const2 a :/ b)) $ subRoute (\(Const2 a) -> f a)
221+
209222maybeRoute_ :: (MonadFix m, MonadHold t m, Adjustable t m) => m () -> RoutedT t r m () -> RoutedT t (Maybe r) m ()
210223maybeRoute_ n j = maybeRouted $ strictDynWidget_ $ \case
211224 Nothing -> n
@@ -500,10 +513,10 @@ runRouteViewT routeEncoder switchover useHash a = do
500513routeLink
501514 :: forall t m a route.
502515 ( DomBuilder t m
503− , RouteToUrl (R route) m
504− , SetRoute t (R route) m
516+ , RouteToUrl route m
517+ , SetRoute t route m
505518 )
506− => R route -- ^ Target route
519+ => route -- ^ Target route
507520 -> m a -- ^ Child widget
508521 -> m a
509522routeLink r w = do
@@ -515,9 +528,7 @@ routeLink r w = do
515528 setRoute $ r <$ domEvent Click e
516529 return a
517530
518−-- | A link widget that, when clicked, sets the route to current value of the
519−-- provided dynamic route. In non-javascript contexts the value of the dynamic post
520−-- build is used so the link still works like 'routeLink'.
531+-- | Like 'routeLinkDynAttr' but without custom attributes.
521532dynRouteLink
522533 :: forall t m a route.
523534 ( DomBuilder t m
@@ -526,11 +537,27 @@ dynRouteLink
526537 , SetRoute t (R route) m
527538 )
528539 => Dynamic t (R route) -- ^ Target route
529− -> m a -- ^ Child widget
540+ -> m a -- ^ Child widget of the @a@ element
541+ -> m a
542+dynRouteLink = routeLinkDynAttr mempty
543+
544+-- | An @a@-tag link widget that, when clicked, sets the route to current value of the
545+-- provided dynamic route. In non-JavaScript contexts the value of the dynamic post
546+-- build is used so the link still works like 'routeLink'.
547+routeLinkDynAttr
548+ :: forall t m a route.
549+ ( DomBuilder t m
550+ , PostBuild t m
551+ , RouteToUrl (R route) m
552+ , SetRoute t (R route) m
553+ )
554+ => Dynamic t (Map AttributeName Text) -- ^ Attributes for @a@ element. Note that if @href@ is present it will be ignored
555+ -> Dynamic t (R route) -- ^ Target route
556+ -> m a -- ^ Child widget of the @a@ element
530557 -> m a
531−dynRouteLink dr w = do
558+routeLinkDynAttr dAttr dr w = do
532559 enc <- askRouteToUrl
533− er <- dynamicAttributesToModifyAttributes $ ("href" =:) . enc <$> dr
560+ er <- dynamicAttributesToModifyAttributes $ zipDynWith (<>) (("href" =:) . enc <$> dr) dAttr
534561 let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
535562 & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
536563 & elementConfig_modifyAttributes .~ er
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+67−25
@@ -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 #-}
@@ -20,11 +25,11 @@ import Control.Exception
2025import Control.Lens ((%~), (^?), _Just, _Right)
2126import qualified Data.Attoparsec.ByteString.Char8 as A
2227import Data.ByteString (ByteString)
28+import qualified Data.ByteString as BS
2329import qualified Data.ByteString.Char8 as BSC
2430import qualified Data.ByteString.Lazy.Char8 as BSLC
25−import Data.Dependent.Sum (DSum (..))
31+import qualified Data.ByteString.UTF8 as BSUTF8
2632import Data.Functor.Identity
27−import Data.Functor.Sum
2833import Data.List (uncons)
2934import Data.Map (Map)
3035import qualified Data.Map as Map
@@ -35,6 +40,7 @@ import Data.String (fromString)
3540import Data.Text (Text)
3641import qualified Data.Text as T
3742import qualified Data.Text.Encoding as T
43+import Data.Time.Clock (getCurrentTime, addUTCTime)
3844import Language.Javascript.JSaddle.Run (syncPoint)
3945import Language.Javascript.JSaddle.WebSockets
4046import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)
@@ -44,6 +50,7 @@ import Network.Socket
4450import Network.Wai (Application)
4551import qualified Network.Wai as W
4652import Network.Wai.Handler.Warp
53+import Network.Wai.Handler.WarpTLS
4754import Network.Wai.Handler.Warp.Internal (settingsHost, settingsPort)
4855import Network.WebSockets (ConnectionOptions)
4956import Network.WebSockets.Connection (defaultConnectionOptions)
@@ -51,17 +58,24 @@ import qualified Obelisk.Asset.Serve.Snap as Snap
5158import Obelisk.Backend
5259import Obelisk.Frontend
5360import 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
5465import Reflex.Dom.Core
5566import Snap.Core (Snap)
5667import System.Environment
5768import System.IO
5869import System.Process
59−import System.Exit (ExitCode(..))
6070import Text.URI (URI)
6171import qualified Text.URI as URI
6272import Text.URI.Lens
6373import Web.Cookie
6474
75+#if defined(IPROUTE_SUPPORTED)
76+import qualified System.Which
77+#endif
78+
6579run
6680 :: Int -- ^ Port to run the backend
6781 -> ([Text] -> Snap ()) -- ^ Static asset handler
@@ -81,8 +95,8 @@ run port serveStaticAsset backend frontend = do
8195 runSnapWithCommandLineArgs $ do
8296 getRouteWith validFullEncoder >>= \case
8397 Identity r -> case r of
84− InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
85− InR obeliskRoute :=> Identity a ->
98+ FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
99+ FullRoute_Frontend obeliskRoute :/ a ->
86100 serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend publicConfigs $ obeliskRoute :/ a
87101 let conf = defRunConfig { _runConfig_redirectPort = port }
88102 runWidget conf publicConfigs frontend validFullEncoder `finally` killThread backendTid
@@ -93,7 +107,7 @@ runServeAsset rootPath = Snap.serveAsset "" rootPath . T.unpack . T.intercalate
93107
94108getConfigRoute :: Map Text ByteString -> Either Text URI
95109getConfigRoute configs = case Map.lookup "common/route" configs of
96− Just r ->
110+ Just r ->
97111 let stripped = T.strip (T.decodeUtf8 r)
98112 in case URI.mkURI stripped of
99113 Just route -> Right route
@@ -103,8 +117,8 @@ getConfigRoute configs = case Map.lookup "common/route" configs of
103117runWidget
104118 :: RunConfig
105119 -> Map Text ByteString
106− -> Frontend (R route)
107− -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
120+ -> Frontend (R frontendRoute)
121+ -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName
108122 -> IO ()
109123runWidget conf configs frontend validFullEncoder = do
110124 uri <- either (fail . T.unpack) pure $ getConfigRoute configs
@@ -114,20 +128,43 @@ runWidget conf configs frontend validFullEncoder = do
114128 beforeMainLoop = do
115129 putStrLn $ "Frontend running on " <> T.unpack (URI.render uri)
116130 settings = setBeforeMainLoop beforeMainLoop (setPort port (setTimeout 3600 defaultSettings))
131+ -- Providing TLS here will also incidentally provide it to proxied requests to the backend.
132+ prepareRunner = case uri ^? uriScheme . _Just . unRText of
133+ Just "https" -> do
134+ -- Generate a private key and self-signed certificate for TLS
135+ privateKey <- RSA.generateRSAKey' 2048 3
136+
137+ certRequest <- X509Request.newX509Req
138+ _ <- X509Request.setPublicKey certRequest privateKey
139+ _ <- X509Request.signX509Req certRequest privateKey Nothing
140+
141+ cert <- X509.newX509 >>= X509Request.makeX509FromReq certRequest
142+ _ <- X509.setPublicKey cert privateKey
143+ now <- getCurrentTime
144+ _ <- X509.setNotBefore cert $ addUTCTime (-1) now
145+ _ <- X509.setNotAfter cert $ addUTCTime (365 * 24 * 60 * 60) now
146+ _ <- X509.signX509 cert privateKey Nothing
147+
148+ certByteString <- BSUTF8.fromString <$> PEM.writeX509 cert
149+ privateKeyByteString <- BSUTF8.fromString <$> PEM.writePKCS8PrivateKey privateKey Nothing
150+
151+ return $ runTLSSocket (tlsSettingsMemory certByteString privateKeyByteString)
152+ _ -> return runSettingsSocket
153+ runner <- prepareRunner
117154 bracket
118155 (bindPortTCPRetry settings (logPortBindErr port) (_runConfig_retryTimeout conf))
119156 close
120157 (\skt -> do
121158 man <- newManager defaultManagerSettings
122159 app <- obeliskApp configs defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
123− runSettingsSocket settings skt app)
160+ runner settings skt app)
124161
125162obeliskApp
126− :: forall route backendRoute
163+ :: forall frontendRoute backendRoute
127164 . Map Text ByteString
128165 -> ConnectionOptions
129− -> Frontend (R route)
130− -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
166+ -> Frontend (R frontendRoute)
167+ -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName
131168 -> URI
132169 -> Application
133170 -> IO Application
@@ -143,14 +180,14 @@ obeliskApp configs opts frontend validFullEncoder uri backend = do
143180 let jsaddleUri = BSLC.fromStrict $ URI.renderBs $ uri & uriPath %~ (<>[jsaddlePath])
144181 Right (jsaddleWarpRouteValidEncoder :: Encoder Identity (Either Text) (R JSaddleWarpRoute) PageName) <- return $ checkEncoder jsaddleWarpRouteEncoder
145182 jsaddle <- jsaddleWithAppOr opts entryPoint $ \_ sendResponse -> sendResponse $ W.responseLBS H.status500 [("Content-Type", "text/plain")] "obeliskApp: jsaddle got a bad URL"
146− return $ \req sendResponse -> case tryDecode validFullEncoder (W.pathInfo req, mempty) of --TODO: Query strings
183+ return $ \req sendResponse -> case tryDecode validFullEncoder $ byteStringsToPageName (BS.dropWhile (== (fromIntegral $ fromEnum '/')) $ W.rawPathInfo req) (BS.drop 1 $ W.rawQueryString req) of
147184 Identity r -> case r of
148− InR (ObeliskRoute_Resource ResourceRoute_JSaddleWarp) :=> Identity jsaddleRoute -> case jsaddleRoute of
185+ FullRoute_Frontend (ObeliskRoute_Resource ResourceRoute_JSaddleWarp) :/ jsaddleRoute -> case jsaddleRoute of
149186 JSaddleWarpRoute_JavaScript :/ () -> sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] $ jsaddleJs' (Just jsaddleUri) False
150187 _ -> flip jsaddle sendResponse $ req
151188 { W.pathInfo = fst $ encode jsaddleWarpRouteValidEncoder jsaddleRoute
152189 }
153− InR (ObeliskRoute_App appRouteComponent) :=> Identity appRouteRest -> do
190+ FullRoute_Frontend (ObeliskRoute_App appRouteComponent) :/ appRouteRest -> do
154191 let cookies = maybe [] parseCookies $ lookup (fromString "Cookie") (W.requestHeaders req)
155192 html <- renderJsaddleFrontend configs cookies (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
156193 sendResponse $ W.responseLBS H.status200 [("Content-Type", staticRenderContentType)] $ BSLC.fromStrict html
@@ -183,17 +220,22 @@ logPortBindErr p e = getProcessIdForPort p >>= \case
183220 Nothing -> putStrLn $ "runWidget: " <> show e
184221 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."]
185222
223+ssPath :: Maybe String
224+ssPath =
225+#if defined(IPROUTE_SUPPORTED)
226+ Just $(System.Which.staticWhich "ss")
227+#else
228+ Nothing
229+#endif
230+
186231getProcessIdForPort :: Int -> IO (Maybe Int)
187−getProcessIdForPort port = do
188− -- First check if 'ss' is available
189− (c, _, _) <- readProcessWithExitCode "which" ["ss"] mempty
190− case c of
191− ExitSuccess -> do
192− xs <- lines <$> readProcess "ss" ["-lptn", "sport = " <> show port] mempty
193− case uncons xs of
194− Just (_, x:_) -> return $ A.maybeResult $ A.parse parseSsPid $ BSC.pack x
195− _ -> return Nothing
196− _ -> return Nothing
232+getProcessIdForPort port = case ssPath of
233+ Just ss -> do
234+ xs <- lines <$> readProcess ss ["-lptn", "sport = " <> show port] mempty
235+ case uncons xs of
236+ Just (_, x:_) -> return $ A.maybeResult $ A.parse parseSsPid $ BSC.pack x
237+ _ -> return Nothing
238+ _ -> return Nothing
197239
198240parseSsPid :: A.Parser Int
199241parseSsPid = 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+118−63
@@ -3,7 +3,8 @@
33{-# LANGUAGE OverloadedStrings #-}
44{-# LANGUAGE PatternGuards #-}
55{-# LANGUAGE ScopedTypeVariables #-}
6−{-# LANGUAGE TypeApplications #-}
6+{-# LANGUAGE ViewPatterns #-}
7+{-# LANGUAGE TemplateHaskell #-}
78module Obelisk.SelfTest where
89
910import Control.Exception (bracket, throw)
@@ -11,7 +12,7 @@ import Control.Monad
1112import Control.Monad.IO.Class
1213import Data.Bool (bool)
1314import Data.Function (fix)
14−import Data.Semigroup (Semigroup, (<>))
15+import Data.Semigroup ((<>))
1516import qualified Data.Set as Set
1617import Data.String
1718import Data.Text (Text)
@@ -21,11 +22,13 @@ import qualified Data.Text.IO as T
2122import qualified Network.HTTP.Client as HTTP
2223import qualified Network.HTTP.Types as HTTP
2324import qualified Network.Socket as Socket
24−import Shelly
25+import Shelly hiding (cp)
2526import System.Directory (withCurrentDirectory, getDirectoryContents)
2627import System.Environment
2728import System.Exit (ExitCode (..))
28−import System.Info
29+import System.FilePath (replaceBaseName, takeBaseName)
30+import System.Which (staticWhich)
31+import qualified System.Info
2932import System.IO (Handle, hClose)
3033import System.IO.Temp
3134import System.Process (readProcessWithExitCode, CreateProcess(cwd), readCreateProcessWithExitCode, proc)
@@ -43,16 +46,21 @@ 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+gitUserConfig :: [Text]
56+gitUserConfig = ["-c", "user.name=Obelisk Selftest", "-c", "user.email=noreply@example.com"]
4857
4958commit :: Text -> Sh ()
50−commit msg = void $ run "git"
51− [ "commit"
59+commit msg = void $ run "git" $ gitUserConfig <> [ "commit"
5260 , "--no-gpg-sign"
5361 , "--allow-empty"
5462 , "-m"
55− , doubleQuotes msg
63+ , msg
5664 ]
5765
5866-- TODO replace Void and stop using loosely typed synchronous exceptions
@@ -68,32 +76,64 @@ tshow = T.pack . show
6876shellyOb :: MonadIO m => (Sh a -> Sh a) -> Sh a -> m a
6977shellyOb f obTest = shelly $ f obTest
7078
79+
7180main :: IO ()
7281main = do
7382 -- Note: you can pass hspec arguments as well, eg: `-m <pattern>`
74− isVerbose <- (elem "-v") <$> getArgs
83+ isVerbose <- elem "-v" <$> getArgs
7584 unless isVerbose $
7685 putStrLn "Tests may take longer to run if there are unbuilt derivations: use -v for verbose output"
7786 let verbosity = bool silently verbosely isVerbose
78− obeliskImpl <- fromString <$> getEnv "OBELISK_IMPL"
87+ nixBuild args = run "nix-build" ("--no-out-link" : args)
88+ obeliskImplRaw <- fromString <$> getEnv "OBELISK_IMPL"
89+ let
90+ withObeliskImpl f =
91+ withSystemTempDirectory "obelisk-impl-copy" $ \(fromString -> obeliskImpl) -> do
92+ void . shellyOb verbosity $ chdir obeliskImpl $ do
93+ user <- T.strip <$> run "whoami" []
94+ run_ cp ["-rT", toTextIgnore obeliskImplRaw, toTextIgnore obeliskImpl]
95+ run_ "chown" ["-R", user, toTextIgnore obeliskImpl]
96+ run_ "chmod" ["-R", "g-rw,o-rw", toTextIgnore obeliskImpl]
97+ f obeliskImpl
98+
99+ withInitCache f obeliskImpl =
100+ withSystemTempDirectory "init Cache λ" $ \initCache -> do
101+ -- Setup the ob init cache
102+ void . shellyOb verbosity $ chdir initCache $ do
103+ run_ "ob" ["init", "--symlink", toTextIgnore obeliskImpl]
104+ run_ "git" ["init"]
105+
106+ f initCache
107+
108+ withObeliskImplAndInitCache f =
109+ withObeliskImpl $ \impl -> withInitCache (f impl) impl
110+
79111 httpManager <- HTTP.newManager HTTP.defaultManagerSettings
80− [p0, p1, p2, p3] <- liftIO $ getFreePorts 4
81− withSystemTempDirectory "initCache" $ \initCache -> do
82− -- Setup the ob init cache
83− void . shellyOb verbosity $ chdir (fromString initCache) $ do
84− run_ "ob" ["init"]
85− run_ "git" ["init"]
112+
113+ withObeliskImplAndInitCache $ \obeliskImpl initCache ->
86114 hspec $ parallel $ do
87115 let shelly_ = void . shellyOb verbosity
88116
89− inTmp :: (Shelly.FilePath -> Sh a) -> IO ()
90− inTmp f = withTmp (chdir <*> f)
117+ defaultTmpDirName = "test λ"
118+
119+ inTmp' :: FilePath -> (FilePath -> Sh a) -> IO ()
120+ inTmp' dirname f = withTmp' dirname (chdir <*> f)
121+ inTmp = inTmp' defaultTmpDirName
91122
92− withTmp f = shelly_ . withSystemTempDirectory "test" $ f . fromString
123+ withTmp' dirname f = shelly_ . withSystemTempDirectory dirname $ f . fromString
124+ withTmp = withTmp' defaultTmpDirName
93125
94− inTmpObInit f = inTmp $ \dir -> do
95− run_ "cp" ["-a", fromString $ initCache <> "/.", toTextIgnore dir]
126+ inTmpObInit' dirname f = inTmp' dirname $ \dir -> do
127+ run_ cp ["-rT", fromString initCache, toTextIgnore dir]
96128 f dir
129+ inTmpObInit = inTmpObInit' defaultTmpDirName
130+
131+ -- To be used in tests that change the obelisk impl directory
132+ inTmpObInitWithImplCopy f = inTmpObInit $ \dir ->
133+ withObeliskImpl $ \(fromString -> implCopy) -> do
134+ run_ "rm" [thunk]
135+ run_ "ln" ["-s", implCopy, thunk]
136+ f dir
97137
98138 assertRevEQ a b = liftIO . assertEqual "" "" =<< diff a b
99139 assertRevNE a b = liftIO . assertBool "" . (/= "") =<< diff a b
@@ -115,9 +155,9 @@ main = do
115155 describe "ob init" $ parallel $ do
116156 it "works with default impl" $ inTmp $ \_ -> run "ob" ["init"]
117157 it "works with master branch impl" $ inTmp $ \_ -> run "ob" ["init", "--branch", "master"]
118− it "works with symlink" $ inTmp $ \_ -> run "ob" ["init", "--symlink", obeliskImpl]
119− it "doesn't silently overwrite existing files" $ withSystemTempDirectory "ob-init" $ \dir -> do
120− let p force = (proc "ob" $ "--no-handoff" : "init" : if force then ["--force"] else []) { cwd = Just dir }
158+ it "works with symlink" $ inTmp $ \_ -> run "ob" ["init", "--symlink", toTextIgnore obeliskImpl]
159+ it "doesn't silently overwrite existing files" $ withSystemTempDirectory "ob-init λ" $ \dir -> do
160+ let p force = (System.Process.proc "ob" $ "--no-handoff" : "init" : ["--force"|force]) { cwd = Just dir }
121161 (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p False) ""
122162 (ExitFailure _, _, _) <- readCreateProcessWithExitCode (p False) ""
123163 (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p True) ""
@@ -127,55 +167,62 @@ main = do
127167 void $ errExit False $ run "ob" ["init", "--symlink", "/dev/null"]
128168 ls tmp >>= liftIO . assertEqual "" []
129169
130− it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $ do
131− configs <- getConfigs
132− withCurrentDirectory (T.unpack $ toTextIgnore tmp) $
170+ it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $
171+ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ do
172+ configs <- getConfigs
133173 return (either (const Nothing) Just $ getConfigRoute configs) `shouldNotReturn` Nothing
134174
135175 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
136176 -- when not run by 'nix-build --attr selftest'
137− describe "ob run" $ parallel $ do
138− it "works in root directory" $ inTmpObInit $ \_ -> do
139− testObRunInDir p0 p1 Nothing httpManager
140− it "works in sub directory" $ inTmpObInit $ \_ -> do
141− testObRunInDir p2 p3 (Just "frontend") httpManager
177+ describe "ob run" $ {- NOT parallel $ -} do
178+ it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir Nothing httpManager
179+ it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir (Just "frontend") httpManager
180+ it "works with differently named cabal files" $ inTmpObInit $ \_ -> do
181+ changeCabalPackageName "backend/backend.cabal" "new-backend"
182+ testObRunInDir Nothing httpManager
142183
143184 describe "obelisk project" $ parallel $ do
144− it "can build obelisk command" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "command" , obeliskImpl]
145− it "can build obelisk skeleton" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "skeleton", obeliskImpl]
146− it "can build obelisk shell" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "shell", obeliskImpl]
147− -- See https://github.com/obsidiansystems/obelisk/issues/101
148− -- it "can build everything" $ shelly_ $ run "nix-build" [obeliskImpl]
185+ it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskImpl]
186+ it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", toTextIgnore obeliskImpl]
187+ it "can build obelisk shell" $ inTmpObInit $ \_ -> nixBuild ["-A", "shell", toTextIgnore obeliskImpl]
188+ it "can build everything" $ inTmpObInit $ \_ -> nixBuild [toTextIgnore obeliskImpl]
149189
150190 describe "blank initialized project" $ parallel $ do
151191
152− it "can build ghc.backend" $ inTmpObInit $ \_ -> do
153− run "nix-build" ["--no-out-link", "-A", "ghc.backend"]
154− it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> do
155− run "nix-build" ["--no-out-link", "-A", "ghcjs.frontend"]
192+ it "can build ghc.backend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghc.backend"]
193+ it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> nixBuild ["-A", "ghcjs.frontend"]
156194
157− if os == "darwin"
158− then it "can build ios" $ inTmpObInit $ \_ -> run "nix-build" ["--no-out-link", "-A", "ios.frontend" ]
159− else it "can build android" $ inTmpObInit $ \_ -> run "nix-build" ["--no-out-link", "-A", "android.frontend"]
195+ if System.Info.os == "darwin"
196+ then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
197+ else it "can build android after accepting license" $ inTmpObInit $ \dir -> do
198+ let defaultNixPath = dir </> ("default.nix" :: FilePath)
199+ writefile defaultNixPath
200+ =<< T.replace
201+ "# config.android_sdk.accept_license = false;"
202+ "config.android_sdk.accept_license = true;"
203+ <$> readfile defaultNixPath
204+ nixBuild ["-A", "android.frontend"]
160205
161206 forM_ ["ghc", "ghcjs"] $ \compiler -> do
162207 let
163− shell = "shells." <> compiler
164− inShell cmd' = run "nix-shell" ["-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 "can build reflex project" $ inTmpObInit $ \_ -> do
169− run "nix-build" []
170−
171− it "has idempotent thunk update" $ inTmpObInit $ \_ -> do
208+ shellName = "shells." <> compiler
209+ inShell cmd' = run_ "nix-shell" ["default.nix", "-A", fromString shellName, "--run", cmd']
210+ it ("can enter " <> shellName) $ inTmpObInit $ \_ -> inShell "exit"
211+ -- NOTE: We override the temporary directory name because cabal has a bug preventing new-build from working
212+ -- in a path that has unicode characters.
213+ it ("can build in " <> shellName) $ inTmpObInit' "test" $ \_ -> inShell $
214+ T.pack cabalPath <> " --version; " <> T.pack cabalPath <> " new-build --" <> T.pack compiler <> " all"
215+
216+ it "has idempotent thunk update" $ inTmpObInitWithImplCopy $ \_ -> do
217+ _ <- pack
172218 u <- update
173219 uu <- update
174220 assertRevEQ u uu
175221
176222 describe "ob thunk pack/unpack" $ parallel $ do
177− it "has thunk pack and unpack inverses" $ inTmpObInit $ \_ -> do
223+ it "has thunk pack and unpack inverses" $ inTmpObInitWithImplCopy $ \_ -> do
178224
225+ _ <- pack
179226 e <- commitAll
180227 eu <- unpack
181228 eup <- pack
@@ -194,7 +241,7 @@ main = do
194241 branch' <- chdir dir $ run "git" ["rev-parse", "--abbrev-ref", "HEAD"]
195242 liftIO $ assertEqual "" branch (T.strip branch')
196243
197− it "can pack and unpack plain git repos" $ do
244+ it "can pack and unpack plain git repos" $
198245 shelly_ $ withSystemTempDirectory "git-repo" $ \dir -> do
199246 let repo = toTextIgnore $ dir </> ("repo" :: String)
200247 run_ "git" ["clone", "https://github.com/haskell/process.git", repo]
@@ -213,7 +260,6 @@ main = do
213260 testThunkPack $ fromText repo
214261
215262 it "aborts thunk pack when there are uncommitted files" $ inTmpObInit $ \dir -> do
216− void $ unpack
217263 testThunkPack (dir </> thunk)
218264
219265 describe "ob thunk update --branch" $ parallel $ do
@@ -234,11 +280,12 @@ main = do
234280
235281
236282-- | Run `ob run` in the given directory (maximum of one level deep)
237−testObRunInDir :: Socket.PortNumber -> Socket.PortNumber -> Maybe Shelly.FilePath -> HTTP.Manager -> Sh ()
238−testObRunInDir p0 p1 mdir httpManager = handle_sh (\case ExitSuccess -> pure (); e -> throw e) $ do
283+testObRunInDir :: Maybe FilePath -> HTTP.Manager -> Sh ()
284+testObRunInDir mdir httpManager = handle_sh (\case ExitSuccess -> pure (); e -> throw e) $ do
285+ [p0, p1] <- liftIO $ getFreePorts 2
239286 let uri p = "http://localhost:" <> T.pack (show p) <> "/" -- trailing slash required for comparison
240287 writefile "config/common/route" $ uri p0
241− maybe id chdir mdir $ runHandle "ob" ["run"] $ \stdout -> do
288+ maybe id chdir mdir $ runHandle "ob" ["run", "-v"] $ \stdout -> do
242289 firstUri <- handleObRunStdout httpManager stdout
243290 let newUri = uri p1
244291 when (firstUri == newUri) $ errorExit $
@@ -249,7 +296,7 @@ testObRunInDir p0 p1 mdir httpManager = handle_sh (\case ExitSuccess -> pure ();
249296 then errorExit $ "Reloading failed: expected " <> newUri <> " but got " <> runningUri
250297 else exit 0
251298
252−testThunkPack :: Shelly.FilePath -> Sh ()
299+testThunkPack :: FilePath -> Sh ()
253300testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $ \file handle -> do
254301 let pack' = readProcessWithExitCode "ob" ["thunk", "pack", T.unpack $ toTextIgnore path'] ""
255302 ensureThunkPackFails q = liftIO $ pack' >>= \case
@@ -270,7 +317,7 @@ testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $
270317 liftIO $ T.hPutStrLn handle "test file" >> hClose handle
271318 ensureThunkPackFails "modified"
272319 -- Existing stashes
273− void $ git ["stash"]
320+ void $ git $ gitUserConfig <> [ "stash" ]
274321 ensureThunkPackFails "has stashes"
275322
276323-- | Blocks until a non-empty line is available
@@ -292,7 +339,7 @@ alterRouteTo uri stdout = do
292339
293340-- | Handle stdout of `ob run`: check that the frontend and backend servers are started correctly
294341handleObRunStdout :: HTTP.Manager -> Handle -> Sh Text
295−handleObRunStdout httpManager stdout = flip fix (ObRunState_Init, []) $ \loop (state, msgs) -> do
342+handleObRunStdout httpManager stdout = flip fix (ObRunState_Init, []) $ \loop (state, msgs) ->
296343 liftIO (T.hGetLine stdout) >>= \t -> case state of
297344 ObRunState_Init
298345 | "Running test..." `T.isPrefixOf` t -> loop (ObRunState_BackendStarted, msgs)
@@ -315,6 +362,14 @@ obRunCheck httpManager _stdout frontendUri = do
315362 req frontendUri >>= \r -> when (HTTP.responseStatus r /= HTTP.ok200) $ errorExit $
316363 "Request to frontend server failed: " <> T.pack (show r)
317364
365+-- | Rename a cabal file and do a really dumb, brittle search/replace in its content to update the name.
366+changeCabalPackageName :: FilePath -> Text -> Sh ()
367+changeCabalPackageName cabalFile newName = do
368+ contents <- readfile cabalFile
369+ writefile (replaceBaseName cabalFile (T.unpack newName)) $
370+ T.replace (" " <> T.pack (takeBaseName cabalFile)) (" " <> newName) contents -- WARNING: Super brittle
371+ rm cabalFile
372+
318373getFreePorts :: Int -> IO [Socket.PortNumber]
319374getFreePorts 0 = pure []
320375getFreePorts n = Socket.withSocketsDo $ do
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−89
@@ -1,91 +1,10 @@
1−{ self-args ? {}
2−, local-self ? import ./. self-args
1+{
2+ supportedSystems ? [ "x86_64-linux" "x86_64-darwin" ]
33}:
4+rec {
5+ recurseForDerivations = true;
6+ build = import ./all-builds.nix { inherit supportedSystems; };
7+ test = import ./all-tests.nix { inherit supportedSystems; };
48
5−let
6− inherit (local-self.nixpkgs) lib runCommand nix;
7−
8− cacheBuildSystems = [ "x86_64-linux" "x86_64-darwin" ];
9−
10− obeliskPackagesCommon = [
11− "obelisk-frontend"
12− "obelisk-route"
13− "obelisk-executable-config-lookup"
14− ];
15−
16− obeliskPackagesBackend = obeliskPackagesCommon ++ [
17− "obelisk-asset-manifest"
18− "obelisk-asset-serve-snap"
19− "obelisk-backend"
20− "obelisk-cliapp"
21− "obelisk-command"
22− "obelisk-executable-config-inject"
23− "obelisk-frontend"
24− "obelisk-run"
25− "obelisk-route"
26− "obelisk-selftest"
27− "obelisk-snap-extras"
28− ];
29−
30− pnameToAttrs = pkgsSet: pnames:
31− lib.listToAttrs (map
32− (name: { inherit name; value = pkgsSet.${name}; })
33− pnames);
34−
35− concatDepends = let
36− extractDeps = x: (x.override {
37− mkDerivation = drv: {
38− out = builtins.concatLists [
39− (drv.buildDepends or [])
40− (drv.libraryHaskellDepends or [])
41− (drv.executableHaskellDepends or [])
42− ];
43− };
44− }).out;
45− in pkgAttrs: builtins.concatLists (map extractDeps (builtins.attrValues pkgAttrs));
46−
47− perPlatform = lib.genAttrs cacheBuildSystems (system: let
48− obelisk = import ./. { inherit system; };
49− reflex-platform = obelisk.reflex-platform;
50− ghc = pnameToAttrs
51− obelisk.haskellPackageSets.ghc
52− obeliskPackagesBackend;
53− ghcjs = pnameToAttrs
54− obelisk.haskellPackageSets.ghcjs
55− obeliskPackagesCommon;
56− cachePackages = builtins.concatLists [
57− (builtins.attrValues ghc)
58− (builtins.attrValues ghcjs)
59− (concatDepends ghc)
60− (concatDepends ghcjs)
61− ];
62− command = local-self.command;
63− serverExeSkeleton = (import ./skeleton { obelisk = local-self; }).exe;
64− androidObelisk = import ./. (self-args // {
65− config.android_sdk.accept_license = true;
66− });
67− androidSkeleton = (import ./skeleton { obelisk = androidObelisk; }).android.frontend;
68− iosObelisk = import ./. (self-args // {
69− system = "x86_64-darwin";
70− iosSdkVersion = "10.2";
71− });
72− iosSkeleton = (import ./skeleton { obelisk = iosObelisk; }).ios.frontend;
73− in {
74− inherit
75− command
76− ghc ghcjs
77− serverExeSkeleton
78− iosSkeleton
79− androidSkeleton;
80− cache = reflex-platform.pinBuildInputs
81− "obelisk-${system}"
82− cachePackages
83− [command serverExeSkeleton iosSkeleton androidSkeleton];
84− });
85−
86− metaCache = local-self.pinBuildInputs
87− "obelisk-everywhere"
88− (map (a: a.cache) (builtins.attrValues perPlatform))
89− [];
90−
91−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+1−3
@@ -1,6 +1,4 @@
1−{-# LANGUAGE EmptyCase #-}
21{-# LANGUAGE FlexibleContexts #-}
3−{-# LANGUAGE LambdaCase #-}
42{-# LANGUAGE RankNTypes #-}
53{-# LANGUAGE TypeFamilies #-}
64module Backend where
@@ -11,5 +9,5 @@ import Obelisk.Backend
119backend :: Backend BackendRoute FrontendRoute
1210backend = Backend
1311 { _backend_run = \serve -> serve $ const $ return ()
14− , _backend_routeEncoder = backendRouteEncoder
12+ , _backend_routeEncoder = fullRouteEncoder
1513 }
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+13−18
@@ -1,15 +1,14 @@
11{-# LANGUAGE EmptyCase #-}
22{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE FlexibleInstances #-}
34{-# LANGUAGE GADTs #-}
4−{-# LANGUAGE LambdaCase #-}
5−{-# LANGUAGE RankNTypes #-}
6−{-# LANGUAGE TemplateHaskell #-}
75{-# LANGUAGE KindSignatures #-}
8−{-# LANGUAGE EmptyCase #-}
6+{-# LANGUAGE LambdaCase #-}
97{-# LANGUAGE MultiParamTypeClasses #-}
10−{-# LANGUAGE FlexibleInstances #-}
118{-# LANGUAGE OverloadedStrings #-}
12−
9+{-# LANGUAGE RankNTypes #-}
10+{-# LANGUAGE TemplateHaskell #-}
11+{-# LANGUAGE TypeFamilies #-}
1312module Common.Route where
1413
1514{- -- You will probably want these imports for composing Encoders.
@@ -19,7 +18,6 @@ import Control.Category
1918
2019import Data.Text (Text)
2120import Data.Functor.Identity
22−import Data.Functor.Sum
2321
2422import Obelisk.Route
2523import Obelisk.Route.TH
@@ -34,17 +32,14 @@ data FrontendRoute :: * -> * where
3432 FrontendRoute_Main :: FrontendRoute ()
3533 -- This type is used to define frontend routes, i.e. ones for which the backend will serve the frontend.
3634
37−backendRouteEncoder
38− :: Encoder (Either Text) Identity (R (Sum BackendRoute (ObeliskRoute FrontendRoute))) PageName
39−backendRouteEncoder = handleEncoder (const (InL BackendRoute_Missing :/ ())) $
40− pathComponentEncoder $ \case
41− InL backendRoute -> case backendRoute of
42− BackendRoute_Missing -> PathSegment "missing" $ unitEncoder mempty
43− InR obeliskRoute -> obeliskRouteSegment obeliskRoute $ \case
44− -- The encoder given to PathEnd determines how to parse query parameters,
45− -- in this example, we have none, so we insist on it.
46− FrontendRoute_Main -> PathEnd $ unitEncoder mempty
47−
35+fullRouteEncoder
36+ :: Encoder (Either Text) Identity (R (FullRoute BackendRoute FrontendRoute)) PageName
37+fullRouteEncoder = mkFullRouteEncoder
38+ (FullRoute_Backend BackendRoute_Missing :/ ())
39+ (\case
40+ BackendRoute_Missing -> PathSegment "missing" $ unitEncoder mempty)
41+ (\case
42+ FrontendRoute_Main -> PathEnd $ unitEncoder mempty)
4843
4944concat <$> mapM deriveRouteComponent
5045 [ ''BackendRoute
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-bin/main.hs+1−1
@@ -6,5 +6,5 @@ import Reflex.Dom
66
77main :: IO ()
88main = do
9− let Right validFullEncoder = checkEncoder backendRouteEncoder
9+ let Right validFullEncoder = checkEncoder fullRouteEncoder
1010 run $ runFrontend validFullEncoder frontend
skeleton/frontend/src/Frontend.hs+23−5
@@ -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
25− exampleConfig <- getConfig "common/example"
43+ 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/shell.nixnew file+1
@@ -0,0 +1 @@
1+(import ./. {}).shells.ghc
skeleton/static/main.cssnew file+3
@@ -0,0 +1,3 @@
1+p {
2+ color: red;
3+}